regionals enrollments

This commit is contained in:
yohlo
2026-02-21 23:12:21 -06:00
parent 7f60b4d200
commit b9e16e2b64
27 changed files with 1212 additions and 83 deletions

View File

@@ -43,6 +43,7 @@ import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_aut
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
import { Route as AuthedAdminTournamentsIdAssignPartnersRouteImport } from './routes/_authed/admin/tournaments/$id/assign-partners'
const RefreshSessionRoute = RefreshSessionRouteImport.update({
id: '/refresh-session',
@@ -221,6 +222,12 @@ const AuthedAdminTournamentsIdTeamsRoute =
path: '/tournaments/$id/teams',
getParentRoute: () => AuthedAdminRoute,
} as any)
const AuthedAdminTournamentsIdAssignPartnersRoute =
AuthedAdminTournamentsIdAssignPartnersRouteImport.update({
id: '/tournaments/$id/assign-partners',
path: '/tournaments/$id/assign-partners',
getParentRoute: () => AuthedAdminRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof AuthedIndexRoute
@@ -252,6 +259,7 @@ export interface FileRoutesByFullPath {
'/tournaments/': typeof AuthedTournamentsIndexRoute
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
'/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
@@ -286,6 +294,7 @@ export interface FileRoutesByTo {
'/tournaments': typeof AuthedTournamentsIndexRoute
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
'/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
@@ -323,6 +332,7 @@ export interface FileRoutesById {
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
'/_authed/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
@@ -360,6 +370,7 @@ export interface FileRouteTypes {
| '/tournaments/'
| '/tournaments/$id/bracket'
| '/admin/tournaments/'
| '/admin/tournaments/$id/assign-partners'
| '/admin/tournaments/$id/teams'
| '/admin/tournaments/run/$id'
| '/api/files/$collection/$recordId/$file'
@@ -394,6 +405,7 @@ export interface FileRouteTypes {
| '/tournaments'
| '/tournaments/$id/bracket'
| '/admin/tournaments'
| '/admin/tournaments/$id/assign-partners'
| '/admin/tournaments/$id/teams'
| '/admin/tournaments/run/$id'
| '/api/files/$collection/$recordId/$file'
@@ -430,6 +442,7 @@ export interface FileRouteTypes {
| '/_authed/tournaments/'
| '/_authed/tournaments/$id/bracket'
| '/_authed/admin/tournaments/'
| '/_authed/admin/tournaments/$id/assign-partners'
| '/_authed/admin/tournaments/$id/teams'
| '/_authed/admin/tournaments/run/$id'
| '/api/files/$collection/$recordId/$file'
@@ -695,6 +708,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedAdminTournamentsIdTeamsRouteImport
parentRoute: typeof AuthedAdminRoute
}
'/_authed/admin/tournaments/$id/assign-partners': {
id: '/_authed/admin/tournaments/$id/assign-partners'
path: '/tournaments/$id/assign-partners'
fullPath: '/admin/tournaments/$id/assign-partners'
preLoaderRoute: typeof AuthedAdminTournamentsIdAssignPartnersRouteImport
parentRoute: typeof AuthedAdminRoute
}
}
}
@@ -704,6 +724,7 @@ interface AuthedAdminRouteChildren {
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
AuthedAdminTournamentsIdAssignPartnersRoute: typeof AuthedAdminTournamentsIdAssignPartnersRoute
AuthedAdminTournamentsIdTeamsRoute: typeof AuthedAdminTournamentsIdTeamsRoute
AuthedAdminTournamentsRunIdRoute: typeof AuthedAdminTournamentsRunIdRoute
AuthedAdminTournamentsIdIndexRoute: typeof AuthedAdminTournamentsIdIndexRoute
@@ -715,6 +736,8 @@ const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
AuthedAdminTournamentsIdAssignPartnersRoute:
AuthedAdminTournamentsIdAssignPartnersRoute,
AuthedAdminTournamentsIdTeamsRoute: AuthedAdminTournamentsIdTeamsRoute,
AuthedAdminTournamentsRunIdRoute: AuthedAdminTournamentsRunIdRoute,
AuthedAdminTournamentsIdIndexRoute: AuthedAdminTournamentsIdIndexRoute,

View File

@@ -0,0 +1,167 @@
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
import { tournamentQueries, useFreeAgents, useTournament } from "@/features/tournaments/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { Stack, Text, Button, Alert, LoadingOverlay, Group } from "@mantine/core";
import { useState } from "react";
import useGenerateRandomTeams from "@/features/tournaments/hooks/use-generate-random-teams";
import useConfirmTeamAssignments from "@/features/tournaments/hooks/use-confirm-team-assignments";
import TeamAssignmentPreview from "@/features/tournaments/components/team-assignment-preview";
import { WarningCircleIcon, ShuffleIcon, CheckCircleIcon } from "@phosphor-icons/react";
import { PlayerInfo } from "@/features/players/types";
import { useQueryClient } from "@tanstack/react-query";
export const Route = createFileRoute("/_authed/admin/tournaments/$id/assign-partners")({
beforeLoad: async ({ context, params }) => {
const { queryClient } = context;
const tournament = await ensureServerQueryData(
queryClient,
tournamentQueries.details(params.id)
);
if (!tournament) throw redirect({ to: "/admin/tournaments" });
return { tournament };
},
loader: ({ context }) => ({
header: {
withBackButton: true,
title: `Manage ${context.tournament.name}`,
},
}),
component: RouteComponent,
});
interface TeamAssignment {
player1: PlayerInfo;
player2: PlayerInfo;
teamName: string;
}
function RouteComponent() {
const { id } = Route.useParams();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: freeAgents } = useFreeAgents(id);
const [assignments, setAssignments] = useState<TeamAssignment[] | null>(null);
const [currentSeed, setCurrentSeed] = useState<number | undefined>(undefined);
const generateMutation = useGenerateRandomTeams();
const confirmMutation = useConfirmTeamAssignments();
const hasOddPlayers = freeAgents.length % 2 !== 0;
const hasEnoughPlayers = freeAgents.length >= 2;
const handleGenerate = () => {
generateMutation.mutate(
{ data: { tournamentId: id, seed: currentSeed } },
{
onSuccess: (result) => {
setAssignments(result.assignments);
setCurrentSeed(result.seed);
},
}
);
};
const handleReroll = () => {
if (currentSeed === undefined) return;
generateMutation.mutate(
{ data: { tournamentId: id, seed: currentSeed + 1 } },
{
onSuccess: (result) => {
setAssignments(result.assignments);
setCurrentSeed(result.seed);
},
}
);
};
const handleConfirm = () => {
if (!assignments) return;
const formattedAssignments = assignments.map((a) => ({
player1Id: a.player1.id,
player2Id: a.player2.id,
teamName: a.teamName,
}));
confirmMutation.mutate(
{ data: { tournamentId: id, assignments: formattedAssignments } },
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: tournamentQueries.details(id).queryKey });
queryClient.invalidateQueries({ queryKey: tournamentQueries.free_agents(id).queryKey });
navigate({ to: "/admin/tournaments/$id", params: { id } });
},
}
);
};
return (
<Stack gap="lg" pos="relative">
<LoadingOverlay visible={confirmMutation.isPending} />
<Stack gap="xs">
<Group gap="xs" align="baseline">
<Text size="xl" fw={700}>
{freeAgents.length}
</Text>
<Text size="sm" c="dimmed">
{freeAgents.length === 1 ? "player enrolled" : "players enrolled"}
</Text>
</Group>
{!hasEnoughPlayers && (
<Alert color="yellow" icon={<WarningCircleIcon size={16} />}>
Need at least 2 players to create teams
</Alert>
)}
{hasOddPlayers && (
<Alert color="red" icon={<WarningCircleIcon size={16} />}>
Cannot create teams with an odd number of players. Please have one player unenroll.
</Alert>
)}
{!assignments && hasEnoughPlayers && !hasOddPlayers && (
<Button
leftSection={<ShuffleIcon size={18} />}
onClick={handleGenerate}
loading={generateMutation.isPending}
>
Generate Random Pairings
</Button>
)}
</Stack>
{assignments && (
<Stack gap="md">
<Group justify="space-between" align="center">
<Text size="lg" fw={600}>
Partner Assignments
</Text>
<Group gap="sm">
<Button
variant="subtle"
leftSection={<ShuffleIcon size={16} />}
onClick={handleReroll}
loading={generateMutation.isPending}
size="sm"
>
Re-roll
</Button>
<Button
leftSection={<CheckCircleIcon size={18} />}
onClick={handleConfirm}
loading={confirmMutation.isPending}
size="sm"
>
Confirm & Create Teams
</Button>
</Group>
</Group>
<TeamAssignmentPreview assignments={assignments} />
</Stack>
)}
</Stack>
);
}