feat: Enhance event management with FightEdit and GroupEdit components, including improved data handling and new functionalities

This commit is contained in:
2025-05-22 19:41:49 +02:00
parent 1da279bb24
commit 5277c9a3fc
9 changed files with 371 additions and 112 deletions

View File

@ -1,45 +1,36 @@
<script lang="ts"> <script lang="ts">
import type { EventFight } from "@type/event"; import type { EventFight, EventFightEdit, ResponseGroups, UpdateEventGroup, GroupUpdateEdit, SWEvent } from "@type/event";
import { fromAbsolute, now, ZonedDateTime } from "@internationalized/date"; import { fromAbsolute, now, ZonedDateTime } 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";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { gamemodes, maps } from "@components/stores/stores"; import { gamemodes, maps } from "@components/stores/stores";
import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command"; import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command";
import { ChevronsUpDown, Check } from "lucide-svelte"; import { ChevronsUpDown, Check, ChevronsUpDownIcon, PlusIcon, CheckIcon, MinusIcon } 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 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@components/ui/dialog";
import { eventRepo } from "@components/repo/event";
import GroupEdit from "./GroupEdit.svelte";
const { const {
fight, fight,
teams, teams,
event,
actions, actions,
onSave, onSave,
groups,
}: { }: {
fight: EventFight | null; fight: EventFight | null;
teams: Team[]; teams: Team[];
event: SWEvent;
groups: ResponseGroups[];
actions: Snippet<[boolean, () => void]>; actions: Snippet<[boolean, () => void]>;
onSave: (fight: { onSave: (fight: EventFightEdit) => void;
spielmodus: string;
map: string;
blueTeam: {
id: number;
name: string;
kuerzel: string;
color: string;
};
redTeam: {
id: number;
name: string;
kuerzel: string;
color: string;
};
start: number;
ergebnis: number;
}) => void;
} = $props(); } = $props();
let fightModus = $state(fight?.spielmodus); let fightModus = $state(fight?.spielmodus);
@ -48,33 +39,63 @@
let fightRedTeam = $state(fight?.redTeam); let fightRedTeam = $state(fight?.redTeam);
let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : now("Europe/Berlin")); let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : now("Europe/Berlin"));
let fightErgebnis = $state(fight?.ergebnis ?? 0); let fightErgebnis = $state(fight?.ergebnis ?? 0);
let fightSpectatePort = $state(fight?.spectatePort?.toString() ?? 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 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 ||
fightMap !== fight?.map || fightMap !== fight?.map ||
fightBlueTeam !== fight?.blueTeam || fightBlueTeam?.id !== fight?.blueTeam?.id ||
fightRedTeam !== fight?.redTeam || fightRedTeam?.id !== fight?.redTeam?.id ||
fightStart.toDate().getTime() !== fight?.start || fightStart.toDate().getTime() !== fight?.start ||
fightErgebnis !== fight?.ergebnis fightErgebnis !== fight?.ergebnis ||
fightSpectatePort !== (fight?.spectatePort?.toString() ?? null) ||
fightGroup !== (fight?.group?.id ?? null)
); );
function submit() { let loading = $state(false);
onSave({
spielmodus: fightModus!, async function submit() {
map: fightMap!, loading = true;
blueTeam: fightBlueTeam!, try {
redTeam: fightRedTeam!, await onSave({
start: fightStart?.toDate().getTime(), spielmodus: fightModus!,
ergebnis: fightErgebnis, map: fightMap!,
}); blueTeam: fightBlueTeam!,
redTeam: fightRedTeam!,
start: fightStart?.toDate().getTime(),
ergebnis: fightErgebnis,
spectatePort: fightSpectatePort ? +fightSpectatePort : null,
group: fightGroup,
});
} finally {
loading = false;
}
}
async function handleGroupSave(group: GroupUpdateEdit) {
let g = await $eventRepo.createGroup(event.id.toString(), group);
groups.push(g);
fightGroup = g.id;
createOpen = false;
groupSelectOpen = false;
} }
</script> </script>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<Label for="fight-modus">Modus</Label> <Label for="fight-modus">Modus</Label>
<Popover> <Popover bind:open={gamemodeSelectOpen}>
<PopoverTrigger> <PopoverTrigger>
{#snippet child({ props })} {#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox"> <Button variant="outline" class="justify-between" {...props} role="combobox">
@ -94,6 +115,7 @@
value={modus} value={modus}
onSelect={() => { onSelect={() => {
fightModus = modus; fightModus = modus;
gamemodeSelectOpen = false;
}} }}
> >
<Check class={cn("mr-2 size-4", modus !== fightModus && "text-transparent")} /> <Check class={cn("mr-2 size-4", modus !== fightModus && "text-transparent")} />
@ -106,7 +128,7 @@
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<Label for="fight-map">Map</Label> <Label for="fight-map">Map</Label>
<Popover> <Popover bind:open={mapSelectOpen}>
<PopoverTrigger> <PopoverTrigger>
{#snippet child({ props })} {#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox"> <Button variant="outline" class="justify-between" {...props} role="combobox">
@ -126,6 +148,7 @@
value={map} value={map}
onSelect={() => { onSelect={() => {
fightMap = map; fightMap = map;
mapSelectOpen = false;
}} }}
> >
<Check class={cn("mr-2 size-4", map !== fightMap && "text-transparent")} /> <Check class={cn("mr-2 size-4", map !== fightMap && "text-transparent")} />
@ -138,7 +161,7 @@
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<Label for="fight-blue-team">Blue Team</Label> <Label for="fight-blue-team">Blue Team</Label>
<Popover> <Popover bind:open={blueTeamSelectOpen}>
<PopoverTrigger> <PopoverTrigger>
{#snippet child({ props })} {#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox"> <Button variant="outline" class="justify-between" {...props} role="combobox">
@ -158,6 +181,7 @@
value={team.name} value={team.name}
onSelect={() => { onSelect={() => {
fightBlueTeam = team; fightBlueTeam = team;
blueTeamSelectOpen = false;
}} }}
> >
<Check class={cn("mr-2 size-4", team !== fightBlueTeam && "text-transparent")} /> <Check class={cn("mr-2 size-4", team !== fightBlueTeam && "text-transparent")} />
@ -170,7 +194,7 @@
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<Label for="fight-red-team">Red Team</Label> <Label for="fight-red-team">Red Team</Label>
<Popover> <Popover bind:open={redTeamSelectOpen}>
<PopoverTrigger> <PopoverTrigger>
{#snippet child({ props })} {#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox"> <Button variant="outline" class="justify-between" {...props} role="combobox">
@ -190,6 +214,7 @@
value={team.name} value={team.name}
onSelect={() => { onSelect={() => {
fightRedTeam = team; fightRedTeam = team;
redTeamSelectOpen = false;
}} }}
> >
<Check class={cn("mr-2 size-4", team !== fightRedTeam && "text-transparent")} /> <Check class={cn("mr-2 size-4", team !== fightRedTeam && "text-transparent")} />
@ -203,17 +228,88 @@
</Popover> </Popover>
<Label>Start</Label> <Label>Start</Label>
<DateTimePicker bind:value={fightStart} /> <DateTimePicker bind:value={fightStart} />
<Label for="fight-ergebnis">Ergebnis</Label> {#if fight !== null}
<Select type="single" value={fightErgebnis?.toString()} onValueChange={(v) => (fightErgebnis = +v)}> <Label for="fight-ergebnis">Ergebnis</Label>
<SelectTrigger> <Select type="single" value={fightErgebnis?.toString()} onValueChange={(v) => (fightErgebnis = +v)}>
{fightErgebnis === 0 ? "Unentschieden" : (fightErgebnis === 1 ? fightBlueTeam?.name : fightRedTeam?.name) + " gewinnt"} <SelectTrigger>
</SelectTrigger> {fightErgebnis === 0 ? "Unentschieden" : (fightErgebnis === 1 ? fightBlueTeam?.name : fightRedTeam?.name) + " gewinnt"}
<SelectContent> </SelectTrigger>
<SelectItem value={"0"}>Unentschieden</SelectItem> <SelectContent>
<SelectItem value={"1"}>{fightBlueTeam?.name ?? "Team Blau"} gewinnt</SelectItem> <SelectItem value={"0"}>Unentschieden</SelectItem>
<SelectItem value={"2"}>{fightRedTeam?.name ?? "Team Blau"} gewinnt</SelectItem> <SelectItem value={"1"}>{fightBlueTeam?.name ?? "Team Blau"} gewinnt</SelectItem>
</SelectContent> <SelectItem value={"2"}>{fightRedTeam?.name ?? "Team Blau"} gewinnt</SelectItem>
</Select> </SelectContent>
</Select>
{/if}
<Label for="fight-group">Gruppe</Label>
<Dialog bind:open={createOpen}>
<Popover bind:open={groupSelectOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Button id="fight-group" variant="outline" class="justify-between" {...props} role="combobox">
{selectedGroup?.name || "Keine Gruppe"}
<ChevronsUpDownIcon class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Gruppe suchen..." />
<CommandList>
<CommandGroup>
<CommandItem value={"new"} onSelect={() => (createOpen = true)}>
<PlusIcon class={"mr-2 size-4"} />
Neue Gruppe
</CommandItem>
<CommandItem
value={"none"}
onSelect={() => {
fightGroup = null;
groupSelectOpen = false;
}}
>
{#if fightGroup === null}
<CheckIcon class={"mr-2 size-4"} />
{:else}
<MinusIcon class={"mr-2 size-4"} />
{/if}
Keine Gruppe
</CommandItem>
{#each groups as group}
<CommandItem
value={group.id.toString()}
onSelect={() => {
fightGroup = group.id;
groupSelectOpen = false;
}}
>
<CheckIcon class={cn("mr-2 size-4", fightGroup !== group.id && "text-transparent")} />
{group.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<DialogContent>
<DialogHeader>
<DialogTitle>Neue Gruppe erstellen</DialogTitle>
<DialogDescription>Hier kannst du eine neue Gruppe erstellen</DialogDescription>
</DialogHeader>
<GroupEdit group={null} onSave={handleGroupSave}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
</DialogFooter>
{/snippet}
</GroupEdit>
</DialogContent>
</Dialog>
<Label for="spectate-port">Spectate Port</Label>
<Input id="spectate-port" bind:value={fightSpectatePort} type="number" placeholder="2001" />
</div> </div>
{@render actions(dirty, submit)} {@render actions(dirty && !loading, submit)}

View File

@ -0,0 +1,78 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { ResponseGroups, GroupUpdateEdit } from "@type/event";
import { Label } from "@components/ui/label";
import { Input } from "@components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
const {
group,
actions,
onSave,
}: {
group: ResponseGroups | null;
actions: Snippet<[boolean, () => void]>;
onSave: (groupData: GroupUpdateEdit) => void;
} = $props();
let groupName = $state(group?.name ?? "");
let groupType = $state(group?.type ?? "GROUP_STAGE");
let pointsPerWin = $state(group?.pointsPerWin ?? 3);
let pointsPerLoss = $state(group?.pointsPerLoss ?? 0);
let pointsPerDraw = $state(group?.pointsPerDraw ?? 1);
let canSave = $derived(groupName.length > 0 && (groupType === "GROUP_STAGE" || groupType === "ELIMINATION_STAGE") && pointsPerWin !== null && pointsPerLoss !== null && pointsPerDraw !== null);
let dirty = $derived(
groupName !== (group ? group.name : "") ||
groupType !== (group ? group.type : "GROUP_STAGE") ||
pointsPerWin !== (group ? group.pointsPerWin : 3) ||
pointsPerLoss !== (group ? group.pointsPerLoss : 0) ||
pointsPerDraw !== (group ? group.pointsPerDraw : 1)
);
function submit() {
onSave({
name: groupName,
type: groupType,
pointsPerWin: pointsPerWin,
pointsPerLoss: pointsPerLoss,
pointsPerDraw: pointsPerDraw,
});
}
</script>
<div class="flex flex-col gap-2">
<Label for="group-name">Name</Label>
<Input id="group-name" bind:value={groupName} placeholder="z.B. Gruppenphase A" />
<Label for="group-type">Typ</Label>
<Select
value={groupType}
type="single"
onValueChange={(v) => {
if (v) groupType = v as "GROUP_STAGE" | "ELIMINATION_STAGE";
}}
>
<SelectTrigger id="group-type" placeholder="Wähle einen Gruppentyp">
{groupType === "GROUP_STAGE" ? "Gruppenphase" : "Eliminierungsphase"}
</SelectTrigger>
<SelectContent>
<SelectItem value="GROUP_STAGE">Gruppenphase</SelectItem>
<SelectItem value="ELIMINATION_STAGE">Eliminierungsphase</SelectItem>
</SelectContent>
</Select>
{#if groupType === "GROUP_STAGE" && group !== null}
<Label for="points-win">Punkte pro Sieg</Label>
<Input id="points-win" type="number" bind:value={pointsPerWin} placeholder="3" />
<Label for="points-loss">Punkte pro Niederlage</Label>
<Input id="points-loss" type="number" bind:value={pointsPerLoss} placeholder="0" />
<Label for="points-draw">Punkte pro Unentschieden</Label>
<Input id="points-draw" type="number" bind:value={pointsPerDraw} placeholder="1" />
{/if}
</div>
{@render actions(group === null ? canSave : dirty, submit)}

View File

@ -22,25 +22,29 @@
import GroupEditRow from "./GroupEditRow.svelte"; import GroupEditRow from "./GroupEditRow.svelte";
import type { ExtendedEvent } from "@type/event"; import type { EventFightEdit, ExtendedEvent } from "@type/event";
import { createSvelteTable, FlexRender } from "@components/ui/data-table"; import { createSvelteTable, FlexRender } from "@components/ui/data-table";
import { type ColumnFiltersState, getCoreRowModel, getFilteredRowModel, getGroupedRowModel, getSortedRowModel, type RowSelectionState, type SortingState } from "@tanstack/table-core"; import { type ColumnFiltersState, getCoreRowModel, getFilteredRowModel, getGroupedRowModel, getSortedRowModel, type RowSelectionState, type SortingState } from "@tanstack/table-core";
import { columns } from "./columns"; import { columns } from "./columns";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
import { Checkbox } from "@components/ui/checkbox"; import { Checkbox } from "@components/ui/checkbox";
import { Menubar, MenubarContent, MenubarItem, MenubarGroup, MenubarGroupHeading, MenubarMenu, MenubarSeparator, MenubarTrigger } from "@components/ui/menubar"; import { Menubar, MenubarContent, MenubarItem, MenubarGroup, MenubarGroupHeading, MenubarMenu, MenubarSeparator, MenubarTrigger } from "@components/ui/menubar";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@components/ui/dialog";
import FightEdit from "@components/moderator/components/FightEdit.svelte";
import { Button } from "@components/ui/button"; import { Button } from "@components/ui/button";
import { MenuIcon } from "lucide-svelte"; import { eventRepo } from "@components/repo/event";
let { data = $bindable() }: { data: ExtendedEvent } = $props(); let { data = $bindable() }: { data: ExtendedEvent } = $props();
let fights = $state(data.fights);
let sorting = $state<SortingState>([]); let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]); let columnFilters = $state<ColumnFiltersState>([]);
let selection = $state<RowSelectionState>({}); let selection = $state<RowSelectionState>({});
const table = createSvelteTable({ const table = createSvelteTable({
get data() { get data() {
return data.fights; return fights;
}, },
initialState: { initialState: {
columnOrder: ["auswahl", "begegnung", "group"], columnOrder: ["auswahl", "begegnung", "group"],
@ -88,30 +92,58 @@
groupedColumnMode: "remove", groupedColumnMode: "remove",
getRowId: (row) => row.id.toString(), getRowId: (row) => row.id.toString(),
}); });
let createOpen = $state(false);
async function handleSave(fight: EventFightEdit) {
await $eventRepo.createFight(data.event.id.toString(), {
...fight,
blueTeam: fight.blueTeam.id,
redTeam: fight.redTeam.id,
});
fights = await $eventRepo.listFights(data.event.id.toString());
createOpen = false;
}
</script> </script>
<div class="w-fit"> <div class="w-fit">
<Menubar> <Dialog bind:open={createOpen}>
<MenubarMenu> <Menubar>
<MenubarTrigger>Mehrfach Bearbeiten</MenubarTrigger> <MenubarMenu>
<MenubarContent> <MenubarTrigger>Mehrfach Bearbeiten</MenubarTrigger>
<MenubarItem disabled>Gruppe Ändern</MenubarItem> <MenubarContent>
<MenubarItem disabled>Startzeit Verschieben</MenubarItem> <MenubarItem disabled>Gruppe Ändern</MenubarItem>
<MenubarItem disabled>Spectate Port Ändern</MenubarItem> <MenubarItem disabled>Startzeit Verschieben</MenubarItem>
</MenubarContent> <MenubarItem disabled>Spectate Port Ändern</MenubarItem>
</MenubarMenu> </MenubarContent>
<MenubarMenu> </MenubarMenu>
<MenubarTrigger>Erstellen</MenubarTrigger> <MenubarMenu>
<MenubarContent> <MenubarTrigger>Erstellen</MenubarTrigger>
<MenubarItem disabled>Fight Erstellen</MenubarItem> <MenubarContent>
<MenubarGroup> <MenubarItem onclick={() => (createOpen = true)}>Fight Erstellen</MenubarItem>
<MenubarGroupHeading>Generatoren</MenubarGroupHeading> <MenubarGroup>
<MenubarItem disabled>Gruppenphase</MenubarItem> <MenubarGroupHeading>Generatoren</MenubarGroupHeading>
<MenubarItem disabled>K.O. Phase</MenubarItem> <MenubarItem disabled>Gruppenphase</MenubarItem>
</MenubarGroup> <MenubarItem disabled>K.O. Phase</MenubarItem>
</MenubarContent> </MenubarGroup>
</MenubarMenu> </MenubarContent>
</Menubar> </MenubarMenu>
</Menubar>
<DialogContent>
<DialogHeader>
<DialogTitle>Fight Erstellen</DialogTitle>
<DialogDescription>Hier kannst du einen neuen Fight erstellen</DialogDescription>
</DialogHeader>
<FightEdit fight={null} teams={data.teams} event={data.event} groups={data.groups} onSave={handleSave}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
</DialogFooter>
{/snippet}
</FightEdit>
</DialogContent>
</Dialog>
</div> </div>
<Table> <Table>
@ -131,7 +163,7 @@
<TableBody> <TableBody>
{#each table.getRowModel().rows as groupRow (groupRow.id)} {#each table.getRowModel().rows as groupRow (groupRow.id)}
{#if groupRow.getIsGrouped()} {#if groupRow.getIsGrouped()}
<TableRow class="bg-muted font-bold"> <TableRow class="font-bold">
<TableCell colspan={columns.length - 1}> <TableCell colspan={columns.length - 1}>
<Checkbox <Checkbox
checked={groupRow.getIsSelected()} checked={groupRow.getIsSelected()}
@ -155,7 +187,13 @@
</TableCell> </TableCell>
{/each} {/each}
<TableCell class="text-right"> <TableCell class="text-right">
<FightEditRow fight={row.original} teams={data.teams}></FightEditRow> <FightEditRow
fight={row.original}
teams={data.teams}
groups={data.groups}
event={data.event}
onupdate={(update) => (fights = fights.map((v) => (v.id === update.id ? update : v)))}
></FightEditRow>
</TableCell> </TableCell>
</TableRow> </TableRow>
{/each} {/each}

View File

@ -24,7 +24,7 @@
import RefereesList from "@components/moderator/pages/event/RefereesList.svelte"; import RefereesList from "@components/moderator/pages/event/RefereesList.svelte";
import TeamTable from "@components/moderator/pages/event/TeamTable.svelte"; import TeamTable from "@components/moderator/pages/event/TeamTable.svelte";
let { event }: { event: ExtendedEvent } = $props(); let { event = $bindable() }: { event: ExtendedEvent } = $props();
</script> </script>
<div class="flex flex-col m-4 p-4 rounded-md border gap-4"> <div class="flex flex-col m-4 p-4 rounded-md border gap-4">

View File

@ -1,21 +1,32 @@
<script lang="ts"> <script lang="ts">
import type { EventFight } from "@type/event"; import type { EventFight, EventFightEdit, ResponseGroups, SWEvent } from "@type/event";
import { Button } from "@components/ui/button"; import { Button } from "@components/ui/button";
import { EditIcon, MenuIcon } from "lucide-svelte"; import { EditIcon, MenuIcon, GroupIcon } 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";
import FightEdit from "@components/moderator/components/FightEdit.svelte"; import FightEdit from "@components/moderator/components/FightEdit.svelte";
import type { Team } from "@components/types/team"; import type { Team } from "@components/types/team";
import { fightRepo } from "@components/repo/fight";
const { fight, teams }: { fight: EventFight; teams: Team[] } = $props(); let { fight, teams, groups, event, onupdate }: { fight: EventFight; teams: Team[]; groups: ResponseGroups[]; event: SWEvent; onupdate: (update: EventFight) => void } = $props();
function handleSave(fightData) { let editOpen = $state(false);
// Handle the save action here
console.log("Fight data saved:", fightData); async function handleSave(fightData: EventFightEdit) {
let f = await $fightRepo.updateFight(event.id, fight.id, {
...fightData,
blueTeam: fightData.blueTeam.id,
redTeam: fightData.redTeam.id,
group: fightData.group ?? -1,
});
onupdate(f);
editOpen = false;
} }
</script> </script>
<div> <div>
<Dialog> <Dialog bind:open={editOpen}>
<DialogTrigger> <DialogTrigger>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<EditIcon /> <EditIcon />
@ -26,7 +37,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} onSave={handleSave}> <FightEdit {fight} {teams} {groups} {event} 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>
@ -35,7 +46,4 @@
</FightEdit> </FightEdit>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Button variant="ghost" size="icon">
<MenuIcon />
</Button>
</div> </div>

View File

@ -77,4 +77,28 @@ export const columns: ColumnDef<EventFight> = [
}); });
}, },
}, },
{
header: "Spielmodus",
accessorKey: "spielmodus",
},
{
header: "Map",
accessorKey: "map",
},
{
header: "Ergebnis",
accessorKey: "ergebnis",
cell: ({ row }) => {
const fight = row.original;
if (fight.ergebnis === 0 && fight.start > Date.now()) {
return "Noch nicht gespielt";
} else if (fight.ergebnis === 1) {
return fight.blueTeam.name + " hat gewonnen";
} else if (fight.ergebnis === 2) {
return fight.redTeam.name + " hat gewonnen";
} else {
return "Unentschieden";
}
},
},
]; ];

View File

@ -128,6 +128,7 @@ export class EventRepo {
.then((value) => z.array(EventFightSchema).parse(value)); .then((value) => z.array(EventFightSchema).parse(value));
} }
public async createFight(eventId: string, fight: any): Promise<EventFight> { public async createFight(eventId: string, fight: any): Promise<EventFight> {
delete fight.ergebnis;
return await fetchWithToken(this.token, `/events/${eventId}/fights`, { return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
method: "POST", method: "POST",
body: JSON.stringify(fight), body: JSON.stringify(fight),
@ -153,7 +154,10 @@ export class EventRepo {
CreateEventGroupSchema.parse(group); CreateEventGroupSchema.parse(group);
return await fetchWithToken(this.token, `/events/${eventId}/groups`, { return await fetchWithToken(this.token, `/events/${eventId}/groups`, {
method: "POST", method: "POST",
body: JSON.stringify(group), body: JSON.stringify({
name: group.name,
type: group.type,
}),
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}) })
.then((value) => value.json()) .then((value) => value.json())

View File

@ -17,12 +17,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type {EventFight} from "@type/event.js"; import type { EventFight } from "@type/event.js";
import {fetchWithToken, tokenStore} from "./repo"; import { fetchWithToken, tokenStore } from "./repo";
import {z} from "zod"; import { z } from "zod";
import {EventFightSchema} from "@type/event.js"; import { EventFightSchema } from "@type/event.js";
import type {Dayjs} from "dayjs"; import type { Dayjs } from "dayjs";
import {derived} from "svelte/store"; import { derived } from "svelte/store";
export interface CreateFight { export interface CreateFight {
spielmodus: string; spielmodus: string;
@ -39,23 +39,22 @@ export interface UpdateFight {
map: string | null; map: string | null;
blueTeam: number | null; blueTeam: number | null;
redTeam: number | null; redTeam: number | null;
start: Dayjs | null; start: number | null;
spectatePort: number | null; spectatePort: number | null;
group: string | null; group: number | null;
} }
export class FightRepo { export class FightRepo {
constructor(private token: string) { constructor(private token: string) {}
}
public async listFights(eventId: number): Promise<EventFight[]> { public async listFights(eventId: number): Promise<EventFight[]> {
return await fetchWithToken(this.token, `/events/${eventId}/fights`) return await fetchWithToken(this.token, `/events/${eventId}/fights`)
.then(value => value.json()) .then((value) => value.json())
.then(value => z.array(EventFightSchema).parse(value)); .then((value) => z.array(EventFightSchema).parse(value));
} }
public async createFight(eventId: number, fight: CreateFight): Promise<EventFight> { public async createFight(eventId: number, fight: CreateFight): Promise<EventFight> {
return await fetchWithToken(this.token, "/fights", { return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
event: eventId, event: eventId,
@ -67,28 +66,25 @@ export class FightRepo {
spectatePort: fight.spectatePort, spectatePort: fight.spectatePort,
group: fight.group, group: fight.group,
}), }),
}).then(value => value.json()) })
.then((value) => value.json())
.then(EventFightSchema.parse); .then(EventFightSchema.parse);
} }
public async updateFight(fightId: number, fight: UpdateFight): Promise<EventFight> { public async updateFight(eventId: number, fightId: number, fight: UpdateFight): Promise<EventFight> {
return await fetchWithToken(this.token, `/fights/${fightId}`, { return await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
spielmodus: fight.spielmodus, ...fight,
map: fight.map,
blueTeam: fight.blueTeam,
redTeam: fight.redTeam,
start: fight.start?.valueOf(), start: fight.start?.valueOf(),
spectatePort: fight.spectatePort,
group: fight.group,
}), }),
}).then(value => value.json()) })
.then((value) => value.json())
.then(EventFightSchema.parse); .then(EventFightSchema.parse);
} }
public async deleteFight(fightId: number): Promise<void> { public async deleteFight(eventId: number, fightId: number): Promise<void> {
const res = await fetchWithToken(this.token, `/fights/${fightId}`, { const res = await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
method: "DELETE", method: "DELETE",
}); });

View File

@ -45,6 +45,15 @@ export const EventFightSchema = z.object({
export type EventFight = z.infer<typeof EventFightSchema>; export type EventFight = z.infer<typeof EventFightSchema>;
export const EventFightEditSchema = EventFightSchema.omit({
id: true,
group: true,
}).extend({
group: z.number().nullable(),
});
export type EventFightEdit = z.infer<typeof EventFightEditSchema>;
export type ResponseGroups = z.infer<typeof ResponseGroupsSchema>; export type ResponseGroups = z.infer<typeof ResponseGroupsSchema>;
export const ResponseRelationSchema = z.object({ export const ResponseRelationSchema = z.object({
@ -111,6 +120,12 @@ export const UpdateEventGroupSchema = z.object({
}); });
export type UpdateEventGroup = z.infer<typeof UpdateEventGroupSchema>; export type UpdateEventGroup = z.infer<typeof UpdateEventGroupSchema>;
export const GroupEditSchema = ResponseGroupsSchema.omit({
id: true,
points: true,
});
export type GroupUpdateEdit = z.infer<typeof GroupEditSchema>;
export const CreateEventRelationSchema = z.object({ export const CreateEventRelationSchema = z.object({
fightId: z.number(), fightId: z.number(),
team: z.enum(["RED", "BLUE"]), team: z.enum(["RED", "BLUE"]),