feat: Enhance event management with FightEdit and GroupEdit components, including improved data handling and new functionalities
This commit is contained in:
@ -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({
|
|
||||||
|
async function submit() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
await onSave({
|
||||||
spielmodus: fightModus!,
|
spielmodus: fightModus!,
|
||||||
map: fightMap!,
|
map: fightMap!,
|
||||||
blueTeam: fightBlueTeam!,
|
blueTeam: fightBlueTeam!,
|
||||||
redTeam: fightRedTeam!,
|
redTeam: fightRedTeam!,
|
||||||
start: fightStart?.toDate().getTime(),
|
start: fightStart?.toDate().getTime(),
|
||||||
ergebnis: fightErgebnis,
|
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,6 +228,7 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
<Label>Start</Label>
|
<Label>Start</Label>
|
||||||
<DateTimePicker bind:value={fightStart} />
|
<DateTimePicker bind:value={fightStart} />
|
||||||
|
{#if fight !== null}
|
||||||
<Label for="fight-ergebnis">Ergebnis</Label>
|
<Label for="fight-ergebnis">Ergebnis</Label>
|
||||||
<Select type="single" value={fightErgebnis?.toString()} onValueChange={(v) => (fightErgebnis = +v)}>
|
<Select type="single" value={fightErgebnis?.toString()} onValueChange={(v) => (fightErgebnis = +v)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@ -214,6 +240,76 @@
|
|||||||
<SelectItem value={"2"}>{fightRedTeam?.name ?? "Team Blau"} gewinnt</SelectItem>
|
<SelectItem value={"2"}>{fightRedTeam?.name ?? "Team Blau"} gewinnt</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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)}
|
||||||
|
|||||||
78
src/components/moderator/components/GroupEdit.svelte
Normal file
78
src/components/moderator/components/GroupEdit.svelte
Normal 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)}
|
||||||
@ -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,9 +92,23 @@
|
|||||||
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">
|
||||||
|
<Dialog bind:open={createOpen}>
|
||||||
<Menubar>
|
<Menubar>
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<MenubarTrigger>Mehrfach Bearbeiten</MenubarTrigger>
|
<MenubarTrigger>Mehrfach Bearbeiten</MenubarTrigger>
|
||||||
@ -103,7 +121,7 @@
|
|||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<MenubarTrigger>Erstellen</MenubarTrigger>
|
<MenubarTrigger>Erstellen</MenubarTrigger>
|
||||||
<MenubarContent>
|
<MenubarContent>
|
||||||
<MenubarItem disabled>Fight Erstellen</MenubarItem>
|
<MenubarItem onclick={() => (createOpen = true)}>Fight Erstellen</MenubarItem>
|
||||||
<MenubarGroup>
|
<MenubarGroup>
|
||||||
<MenubarGroupHeading>Generatoren</MenubarGroupHeading>
|
<MenubarGroupHeading>Generatoren</MenubarGroupHeading>
|
||||||
<MenubarItem disabled>Gruppenphase</MenubarItem>
|
<MenubarItem disabled>Gruppenphase</MenubarItem>
|
||||||
@ -112,6 +130,20 @@
|
|||||||
</MenubarContent>
|
</MenubarContent>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
</Menubar>
|
</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}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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"]),
|
||||||
|
|||||||
Reference in New Issue
Block a user