Refactor event handling and introduce TeamSelector component for improved fight management
All checks were successful
SteamWarCI Build successful

This commit is contained in:
2025-09-28 10:26:08 +02:00
parent 54d49cca5b
commit c0f4a852b5
8 changed files with 320 additions and 172 deletions

View File

@ -30,6 +30,7 @@
import Event from "@components/moderator/pages/event/Event.svelte"; import Event from "@components/moderator/pages/event/Event.svelte";
import Pages from "@components/moderator/pages/pages/Pages.svelte"; import Pages from "@components/moderator/pages/pages/Pages.svelte";
import Generator from "@components/moderator/pages/generators/Generator.svelte"; import Generator from "@components/moderator/pages/generators/Generator.svelte";
import { Tooltip } from "bits-ui";
const routes: RouteDefinition = { const routes: RouteDefinition = {
"/": Dashboard, "/": Dashboard,
@ -52,5 +53,8 @@
</div> </div>
</div> </div>
</div> </div>
<Router {routes} />
<Tooltip.Provider>
<Router {routes} />
</Tooltip.Provider>
</div> </div>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import GroupSelector from "./GroupSelector.svelte"; import GroupSelector from "./GroupSelector.svelte";
import type { EventFight, EventFightEdit, ResponseGroups, SWEvent } from "@type/event"; import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
import { fromAbsolute } from "@internationalized/date"; import { fromAbsolute } from "@internationalized/date";
import { Label } from "@components/ui/label"; import { Label } from "@components/ui/label";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte"; import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
@ -11,46 +11,36 @@
import { ChevronsUpDown, Check } from "lucide-svelte"; import { ChevronsUpDown, Check } from "lucide-svelte";
import { Button } from "@components/ui/button"; import { Button } from "@components/ui/button";
import { cn } from "@components/utils"; import { cn } from "@components/utils";
import type { Team } from "@components/types/team";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { Input } from "@components/ui/input"; import { Input } from "@components/ui/input";
import TeamSelector from "./TeamSelector.svelte";
import type { EventModel } from "../pages/event/eventmodel.svelte";
let { let {
fight, fight,
teams,
event,
actions, actions,
onSave, onSave,
groups = $bindable(), data,
}: { }: {
fight: EventFight | null; fight: EventFight | null;
teams: Team[];
event: SWEvent;
groups: ResponseGroups[];
actions: Snippet<[boolean, () => void]>; actions: Snippet<[boolean, () => void]>;
onSave: (fight: EventFightEdit) => void; onSave: (fight: EventFightEdit) => void;
data: EventModel;
} = $props(); } = $props();
let fightModus = $state(fight?.spielmodus); let fightModus = $state(fight?.spielmodus);
let fightMap = $state(fight?.map); let fightMap = $state(fight?.map);
let fightBlueTeam = $state(fight?.blueTeam); let fightBlueTeam = $state(fight?.blueTeam);
let fightRedTeam = $state(fight?.redTeam); let fightRedTeam = $state(fight?.redTeam);
let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : fromAbsolute(event.start, "Europe/Berlin")); let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : fromAbsolute(data.event.start, "Europe/Berlin"));
let fightErgebnis = $state(fight?.ergebnis ?? 0); let fightErgebnis = $state(fight?.ergebnis ?? 0);
let fightSpectatePort = $state(fight?.spectatePort?.toString() ?? null); let fightSpectatePort = $state(fight?.spectatePort?.toString() ?? null);
let fightGroup = $state(fight?.group?.id ?? null); let fightGroup = $state(fight?.group?.id ?? null);
let selectedGroup = $derived(groups.find((group) => group.id === fightGroup));
let mapsStore = $derived(maps(fightModus ?? "null")); let mapsStore = $derived(maps(fightModus ?? "null"));
let gamemodeSelectOpen = $state(false); let gamemodeSelectOpen = $state(false);
let mapSelectOpen = $state(false); let mapSelectOpen = $state(false);
let blueTeamSelectOpen = $state(false);
let redTeamSelectOpen = $state(false);
let createOpen = $state(false);
let groupSelectOpen = $state(false);
let dirty = $derived( let dirty = $derived(
fightModus !== fight?.spielmodus || fightModus !== fight?.spielmodus ||
@ -151,128 +141,10 @@
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<Label for="fight-blue-team">Blue Team</Label> <Label>Blue Team</Label>
<Popover bind:open={blueTeamSelectOpen}> <TeamSelector bind:selectedTeam={fightBlueTeam} {data} fightId={fight?.id} team="BLUE" />
<PopoverTrigger> <Label>Red Team</Label>
{#snippet child({ props })} <TeamSelector bind:selectedTeam={fightRedTeam} {data} fightId={fight?.id} team="RED" />
<Button variant="outline" class="justify-between" {...props} role="combobox">
{teams.find((value) => value.id === fightBlueTeam?.id)?.name || fightBlueTeam?.name || "Select a team..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
fightBlueTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
blueTeamSelectOpen = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
fightBlueTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
blueTeamSelectOpen = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each teams as team}
<CommandItem
value={team.name}
onSelect={() => {
fightBlueTeam = team;
blueTeamSelectOpen = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== fightBlueTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label for="fight-red-team">Red Team</Label>
<Popover bind:open={redTeamSelectOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{teams.find((value) => value.id === fightRedTeam?.id)?.name || fightRedTeam?.name || "Select a team..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
fightRedTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
redTeamSelectOpen = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
fightRedTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
redTeamSelectOpen = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each teams as team}
<CommandItem
value={team.name}
onSelect={() => {
fightRedTeam = team;
redTeamSelectOpen = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== fightRedTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label>Start</Label> <Label>Start</Label>
<DateTimePicker bind:value={fightStart} /> <DateTimePicker bind:value={fightStart} />
{#if fight !== null} {#if fight !== null}
@ -290,7 +162,7 @@
{/if} {/if}
<Label for="fight-group">Gruppe</Label> <Label for="fight-group">Gruppe</Label>
<GroupSelector {event} bind:value={fightGroup} bind:groups></GroupSelector> <GroupSelector event={data.event} bind:value={fightGroup} bind:groups={data.groups}></GroupSelector>
<Label for="spectate-port">Spectate Port</Label> <Label for="spectate-port">Spectate Port</Label>
<Input id="spectate-port" bind:value={fightSpectatePort} type="number" placeholder="2001" /> <Input id="spectate-port" bind:value={fightSpectatePort} type="number" placeholder="2001" />
</div> </div>

View File

@ -0,0 +1,253 @@
<script lang="ts">
import type { ResponseRelation } from "@components/types/event";
import type { Team } from "@components/types/team";
import { Button } from "@components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/ui/tooltip";
import { cn } from "@components/utils";
import { Check, ChevronsUpDown, GitPullRequestArrow, Plus } from "lucide-svelte";
import type { EventModel } from "../pages/event/eventmodel.svelte";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Label } from "@components/ui/label";
import { eventRepo } from "@components/repo/event";
interface Props {
selectedTeam: Team | undefined;
open?: boolean;
team: "BLUE" | "RED";
data: EventModel;
fightId?: number;
onSelect?: (team: Team) => void;
}
let { selectedTeam = $bindable(), data, team, open = $bindable(false), fightId, onSelect }: Props = $props();
const currentRelation = $derived(data.relations.find((r) => r.fight === fightId && r.team === team));
let fromType = $state<"FIGHT" | "GROUP">(currentRelation?.type ?? "FIGHT");
let fromFight = $state<string | undefined>(currentRelation?.fromFight?.id?.toString());
let fromFightData = $derived(data.fights.find((f) => f.id.toString() === fromFight));
let fromGroup = $state<string | undefined>(currentRelation?.fromGroup?.id?.toString());
let fromGroupData = $derived(data.groups.find((g) => g.id.toString() === fromGroup));
let fromPlace = $state<string | undefined>(currentRelation?.fromPlace?.toString());
let relationOpen = $state(false);
async function saveRelation() {
relationOpen = false;
if (currentRelation === undefined) {
await $eventRepo.createRelation(data.event.id, {
fightId: fightId!,
team,
fromType,
fromId: fromType === "FIGHT" ? parseInt(fromFight!) : parseInt(fromGroup!),
fromPlace: parseInt(fromPlace!),
});
} else {
await $eventRepo.updateRelation(data.event.id, currentRelation.id, {
from: {
fromType,
fromId: fromType === "FIGHT" ? parseInt(fromFight!) : parseInt(fromGroup!),
fromPlace: parseInt(fromPlace!),
},
});
}
data.relations = await $eventRepo.listRelations(data.event.id);
reset();
}
async function clearRelation() {
relationOpen = false;
if (currentRelation !== undefined) {
await $eventRepo.deleteRelation(data.event.id, currentRelation.id);
data.relations = await $eventRepo.listRelations(data.event.id);
}
reset();
}
function reset() {
fromType = currentRelation?.type ?? "FIGHT";
fromFight = currentRelation?.fromFight?.id.toString();
fromGroup = currentRelation?.fromGroup?.id.toString();
fromPlace = currentRelation?.fromPlace.toString();
}
let canSave = $derived(
(fromType !== currentRelation?.type ||
fromFight !== (currentRelation?.fromFight?.id.toString() ?? "") ||
fromGroup !== (currentRelation?.fromGroup?.id.toString() ?? "") ||
fromPlace !== (currentRelation?.fromPlace.toString() ?? "")) &&
((fromType === "FIGHT" && fromFight !== "" && fromPlace !== "") || (fromType === "GROUP" && fromGroup !== "" && fromPlace !== ""))
);
</script>
<div class="flex gap-2">
<Popover bind:open>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between flex-1" {...props} role="combobox">
{#if selectedTeam?.id === -1}
???
{:else if selectedTeam?.id === 0}
PUB
{:else}
{data.teams.find((v) => v.id === selectedTeam?.id)?.name || selectedTeam?.name || "Select a team..."}
{/if}
{#if currentRelation !== undefined}
({#if currentRelation.type === "FIGHT"}
{currentRelation.fromPlace === 0 ? "Gewinner" : "Verlierer"} von {currentRelation.fromFight?.blueTeam.name} vs {currentRelation.fromFight?.redTeam.name} ({new Date(
currentRelation.fromFight?.start ?? 0
).toLocaleTimeString("de-DE", {
timeStyle: "short",
})})
{:else}
{currentRelation.fromPlace + 1}. Platz von {currentRelation.fromGroup?.name}
{/if})
{/if}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
selectedTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
onSelect?.(selectedTeam);
open = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
selectedTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
onSelect?.(selectedTeam);
open = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each data.teams as team}
<CommandItem
value={team.name}
onSelect={() => {
selectedTeam = team;
onSelect?.(selectedTeam);
open = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== selectedTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Popover bind:open={relationOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Tooltip>
<TooltipTrigger>
<Button {...props} size="icon" variant={currentRelation !== undefined ? "default" : "outline"} disabled={fightId === undefined}>
<GitPullRequestArrow />
</Button>
</TooltipTrigger>
<TooltipContent>Kampfverbindung</TooltipContent>
</Tooltip>
{/snippet}
</PopoverTrigger>
<PopoverContent>
<Tabs bind:value={fromType}>
<TabsList>
<TabsTrigger value="FIGHT">Kampf</TabsTrigger>
<TabsTrigger value="GROUP">Gruppe</TabsTrigger>
</TabsList>
<TabsContent value="FIGHT">
<Label>Kampf</Label>
<Select bind:value={fromFight} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromFightData
? `${new Date(fromFightData.start).toLocaleString("de-DE", { timeStyle: "short" })}: ${fromFightData.blueTeam.kuerzel} vs. ${fromFightData.redTeam.kuerzel}`
: "Kampf auswählen..."}
</SelectTrigger>
<SelectContent>
{#each data.fights.filter((v) => v.id !== fightId) as fight (fight.id)}
<SelectItem value={fight.id.toString()}
>{new Date(fight.start).toLocaleString("de-DE", {
timeStyle: "short",
})}: {fight.blueTeam.kuerzel} vs. {fight.redTeam.kuerzel}</SelectItem
>
{/each}
</SelectContent>
</Select>
<Label>Team</Label>
<Select bind:value={fromPlace} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromPlace ? (fromPlace === "0" ? "Gewinner" : "Verlierer") : "Platz auswählen..."}
</SelectTrigger>
<SelectContent>
<SelectItem value={"0"}>Gewinner</SelectItem>
<SelectItem value={"1"}>Verlierer</SelectItem>
</SelectContent>
</Select>
</TabsContent>
<TabsContent value="GROUP">
<Label>Gruppe</Label>
<Select bind:value={fromGroup} type="single" disabled={data.groups.length === 0}>
<SelectTrigger>
{fromGroupData ? fromGroupData.name : "Kampf auswählen..."}
</SelectTrigger>
<SelectContent>
{#each data.groups as group (group.id)}
<SelectItem value={group.id.toString()}>{group.name}</SelectItem>
{/each}
</SelectContent>
</Select>
<Label>Platz</Label>
<Select bind:value={fromPlace} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromPlace ? `${parseInt(fromPlace) + 1}. Platz` : "Platz auswählen..."}
</SelectTrigger>
<SelectContent>
{#each Array(32) as _, i}
<SelectItem value={i.toString()}>{i + 1}. Platz</SelectItem>
{/each}
</SelectContent>
</Select>
</TabsContent>
</Tabs>
<div class="flex justify-end gap-2 mt-2">
<Button onclick={clearRelation} variant="destructive">Löschen</Button>
<Button onclick={saveRelation} disabled={!canSave}>Übernehmen</Button>
</div>
</PopoverContent>
</Popover>
</div>

View File

@ -134,7 +134,7 @@
<DialogTitle>Fight Erstellen</DialogTitle> <DialogTitle>Fight Erstellen</DialogTitle>
<DialogDescription>Hier kannst du einen neuen Fight erstellen</DialogDescription> <DialogDescription>Hier kannst du einen neuen Fight erstellen</DialogDescription>
</DialogHeader> </DialogHeader>
<FightEdit fight={null} teams={data.teams} event={data.event} groups={data.groups} onSave={handleSave}> <FightEdit fight={null} {data} onSave={handleSave}>
{#snippet actions(dirty, submit)} {#snippet actions(dirty, submit)}
<DialogFooter> <DialogFooter>
<Button disabled={!dirty} onclick={submit}>Speichern</Button> <Button disabled={!dirty} onclick={submit}>Speichern</Button>
@ -291,14 +291,7 @@
</TableCell> </TableCell>
{/each} {/each}
<TableCell class="text-right"> <TableCell class="text-right">
<FightEditRow <FightEditRow fight={row.original} {data} onupdate={(update) => (data._fights = data._fights.map((v) => (v.id === update.id ? update : v)))} {refresh}></FightEditRow>
fight={row.original}
teams={data.teams}
bind:groups={data.groups}
event={data.event}
onupdate={(update) => (data.fights = data.fights.map((v) => (v.id === update.id ? update : v)))}
{refresh}
></FightEditRow>
</TableCell> </TableCell>
</TableRow> </TableRow>
{/each} {/each}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { EventFight, EventFightEdit, ResponseGroups, SWEvent } from "@type/event"; import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
import { Button } from "@components/ui/button"; import { Button } from "@components/ui/button";
import { EditIcon, CopyIcon } from "lucide-svelte"; import { EditIcon, CopyIcon } from "lucide-svelte";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog";
@ -7,21 +7,15 @@
import type { Team } from "@components/types/team"; import type { Team } from "@components/types/team";
import { fightRepo } from "@components/repo/fight"; import { fightRepo } from "@components/repo/fight";
import { eventRepo } from "@components/repo/event"; import { eventRepo } from "@components/repo/event";
import type { EventModel } from "./eventmodel.svelte";
let { let { fight, onupdate, refresh, data }: { fight: EventFight; onupdate: (update: EventFight) => void; refresh: () => void; data: EventModel } = $props();
fight,
teams,
groups = $bindable(),
event,
onupdate,
refresh,
}: { fight: EventFight; teams: Team[]; groups: ResponseGroups[]; event: SWEvent; onupdate: (update: EventFight) => void; refresh: () => void } = $props();
let editOpen = $state(false); let editOpen = $state(false);
let duplicateOpen = $state(false); let duplicateOpen = $state(false);
async function handleSave(fightData: EventFightEdit) { async function handleSave(fightData: EventFightEdit) {
let f = await $fightRepo.updateFight(event.id, fight.id, { let f = await $fightRepo.updateFight(data.event.id, fight.id, {
...fightData, ...fightData,
blueTeam: fightData.blueTeam.id, blueTeam: fightData.blueTeam.id,
redTeam: fightData.redTeam.id, redTeam: fightData.redTeam.id,
@ -34,7 +28,7 @@
} }
async function handlyCopy(fightData: EventFightEdit) { async function handlyCopy(fightData: EventFightEdit) {
await $eventRepo.createFight(event.id.toString(), { await $eventRepo.createFight(data.event.id.toString(), {
...fightData, ...fightData,
blueTeam: fightData.blueTeam.id, blueTeam: fightData.blueTeam.id,
redTeam: fightData.redTeam.id, redTeam: fightData.redTeam.id,
@ -58,7 +52,7 @@
<DialogTitle>Fight bearbeiten</DialogTitle> <DialogTitle>Fight bearbeiten</DialogTitle>
<DialogDescription>Hier kannst du die Daten des Kampfes bearbeiten.</DialogDescription> <DialogDescription>Hier kannst du die Daten des Kampfes bearbeiten.</DialogDescription>
</DialogHeader> </DialogHeader>
<FightEdit {fight} {teams} bind:groups {event} onSave={handleSave}> <FightEdit {fight} {data} onSave={handleSave}>
{#snippet actions(dirty, submit)} {#snippet actions(dirty, submit)}
<DialogFooter> <DialogFooter>
<Button disabled={!dirty} onclick={submit}>Speichern</Button> <Button disabled={!dirty} onclick={submit}>Speichern</Button>
@ -78,7 +72,7 @@
<DialogTitle>Fight duplizieren</DialogTitle> <DialogTitle>Fight duplizieren</DialogTitle>
<DialogDescription>Hier kannst du die Daten des duplizierten Fights ändern</DialogDescription> <DialogDescription>Hier kannst du die Daten des duplizierten Fights ändern</DialogDescription>
</DialogHeader> </DialogHeader>
<FightEdit {fight} {teams} bind:groups {event} onSave={handlyCopy}> <FightEdit {fight} {data} onSave={handlyCopy}>
{#snippet actions(dirty, submit)} {#snippet actions(dirty, submit)}
<DialogFooter> <DialogFooter>
<Button onclick={submit}>Speichern</Button> <Button onclick={submit}>Speichern</Button>

View File

@ -1,21 +1,52 @@
import type { ResponseUser } from "@components/repo/event"; import type { ResponseUser } from "@components/repo/event";
import type { EventFight, ExtendedEvent, ResponseGroups, ResponseRelation, SWEvent } from "@components/types/event"; import type { EventFight, ExtendedEvent, ResponseGroups, ResponseRelation, SWEvent } from "@components/types/event";
import type { Team } from "@components/types/team"; import type { Team } from "@components/types/team";
import { derived } from "svelte/store";
export class EventModel { export class EventModel {
public event: SWEvent = $state({} as SWEvent); public event: SWEvent = $state({} as SWEvent);
public teams: Array<Team> = $state([]); public teams: Array<Team> = $state([]);
public groups: Array<ResponseGroups> = $state([]); public groups: Array<ResponseGroups> = $state([]);
public fights: Array<EventFight> = $state([]); public _fights: Array<EventFight> = $state([]);
public referees: Array<ResponseUser> = $state([]); public referees: Array<ResponseUser> = $state([]);
public relations: Array<ResponseRelation> = $state([]); public relations: Array<ResponseRelation> = $state([]);
public fights = $derived(this.remapFights(this._fights, this.relations));
constructor(data: ExtendedEvent) { constructor(data: ExtendedEvent) {
this.event = data.event; this.event = data.event;
this.relations = data.relations;
this.teams = data.teams; this.teams = data.teams;
this.groups = data.groups; this.groups = data.groups;
this.fights = data.fights; this._fights = data.fights;
this.referees = data.referees; this.referees = data.referees;
this.relations = data.relations; }
private remapFights(v: Array<EventFight>, rels: Array<ResponseRelation>) {
return v.map((fight) => {
let f = JSON.parse(JSON.stringify(fight)) as EventFight;
let relations = rels.filter((relation) => relation.fight === f.id);
relations.forEach((relation) => {
let str = "";
if (relation.type === "FIGHT") {
str += `${relation.fromPlace === 0 ? "Gewinner" : "Verlierer"} von ${relation.fromFight?.blueTeam.name} vs ${relation.fromFight?.redTeam.name} (${new Date(
relation.fromFight?.start ?? 0
).toLocaleTimeString("de-DE", {
timeStyle: "short",
})})`;
} else {
str += `${relation.fromPlace + 1}. Platz von ${relation.fromGroup?.name}`;
}
if (relation.team === "BLUE") {
f.blueTeam.name += ` (${str})`;
} else {
f.redTeam.name += ` (${str})`;
}
});
return f;
});
} }
} }

View File

@ -186,12 +186,12 @@ export class EventRepo {
} }
// Relations // Relations
public async listRelations(eventId: string): Promise<ResponseRelation[]> { public async listRelations(eventId: number): Promise<ResponseRelation[]> {
return await fetchWithToken(this.token, `/events/${eventId}/relations`) return await fetchWithToken(this.token, `/events/${eventId}/relations`)
.then((value) => value.json()) .then((value) => value.json())
.then((value) => z.array(ResponseRelationSchema).parse(value)); .then((value) => z.array(ResponseRelationSchema).parse(value));
} }
public async createRelation(eventId: string, relation: CreateEventRelation): Promise<ResponseRelation> { public async createRelation(eventId: number, relation: CreateEventRelation): Promise<ResponseRelation> {
CreateEventRelationSchema.parse(relation); CreateEventRelationSchema.parse(relation);
return await fetchWithToken(this.token, `/events/${eventId}/relations`, { return await fetchWithToken(this.token, `/events/${eventId}/relations`, {
method: "POST", method: "POST",
@ -206,7 +206,7 @@ export class EventRepo {
.then((value) => value.json()) .then((value) => value.json())
.then(ResponseRelationSchema.parse); .then(ResponseRelationSchema.parse);
} }
public async updateRelation(eventId: string, relationId: string, relation: UpdateEventRelation): Promise<ResponseRelation> { public async updateRelation(eventId: number, relationId: number, relation: UpdateEventRelation): Promise<ResponseRelation> {
UpdateEventRelationSchema.parse(relation); UpdateEventRelationSchema.parse(relation);
return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, { return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
method: "PUT", method: "PUT",
@ -216,7 +216,7 @@ export class EventRepo {
.then((value) => value.json()) .then((value) => value.json())
.then(ResponseRelationSchema.parse); .then(ResponseRelationSchema.parse);
} }
public async deleteRelation(eventId: string, relationId: string): Promise<boolean> { public async deleteRelation(eventId: number, relationId: number): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, { const res = await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
method: "DELETE", method: "DELETE",
}); });

View File

@ -60,10 +60,11 @@ export type ResponseGroups = z.infer<typeof ResponseGroupsSchema>;
export const ResponseRelationSchema = z.object({ export const ResponseRelationSchema = z.object({
id: z.number(), id: z.number(),
fight: EventFightSchema, fight: z.number(),
team: z.enum(["RED", "BLUE"]),
type: z.enum(["FIGHT", "GROUP"]), type: z.enum(["FIGHT", "GROUP"]),
fromFight: EventFightSchema.nullable(), fromFight: EventFightSchema.optional(),
fromGroup: ResponseGroupsSchema.nullable(), fromGroup: ResponseGroupsSchema.optional(),
fromPlace: z.number(), fromPlace: z.number(),
}); });