regionals enrollments
This commit is contained in:
@@ -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,
|
||||
|
||||
167
src/app/routes/_authed/admin/tournaments/$id/assign-partners.tsx
Normal file
167
src/app/routes/_authed/admin/tournaments/$id/assign-partners.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user