import { useState, useEffect } from "react"; import { Form, Link } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.$id.participants"; import { logger } from "~/lib/logger"; import { findSportsSeasonById, findSportsSeasonsBySportId } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId, findParticipantByName, createParticipant, deleteParticipant, updateParticipant, copyParticipantsFromSeason, } from "~/models/season-participant"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Textarea } from "~/components/ui/textarea"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { Plus, Trash2, ArrowLeft, Pencil, Check, X, Copy } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Participants — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; } export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); if (!sportsSeason) { throw new Response("Sports season not found", { status: 404 }); } const participants = await findParticipantsBySportsSeasonId(params.id); const sameSportSeasons = (await findSportsSeasonsBySportId(sportsSeason.sportId)) .filter((s) => s.id !== params.id) .toSorted((a, b) => b.year - a.year); // Type assertion since we know the sport relation is included return { sportsSeason: sportsSeason as typeof sportsSeason & { sport: { id: string; name: string; type: string; slug: string } }, participants, sameSportSeasons, }; } export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "delete") { const participantId = formData.get("participantId"); if (typeof participantId === "string") { await deleteParticipant(participantId); } return { success: true }; } if (intent === "update-participant") { const participantId = formData.get("participantId"); const newName = formData.get("newName"); const newExternalId = formData.get("newExternalId"); if (typeof participantId !== "string") { return { error: "Invalid participant", intent: "update-participant" }; } if (typeof newName !== "string" || !newName.trim()) { return { error: "Name is required", intent: "update-participant" }; } const trimmedName = newName.trim(); const existing = await findParticipantByName(params.id, trimmedName); if (existing && existing.id !== participantId) { return { error: `"${trimmedName}" already exists in this season.`, intent: "update-participant" }; } const externalIdValue = typeof newExternalId === "string" && newExternalId.trim() ? newExternalId.trim() : null; try { await updateParticipant(participantId, { name: trimmedName, externalId: externalIdValue }); return { success: true, intent: "update-participant" }; } catch (error) { logger.error("Error updating participant:", error); return { error: "Failed to update participant. Please try again.", intent: "update-participant" }; } } if (intent === "update-name") { const participantId = formData.get("participantId"); const newName = formData.get("newName"); if (typeof participantId !== "string") { return { error: "Invalid participant", intent: "update-name" }; } if (typeof newName !== "string" || !newName.trim()) { return { error: "Name is required", intent: "update-name" }; } const trimmedName = newName.trim(); const existing = await findParticipantByName(params.id, trimmedName); if (existing && existing.id !== participantId) { return { error: `"${trimmedName}" already exists in this season.`, intent: "update-name" }; } try { await updateParticipant(participantId, { name: trimmedName }); return { success: true, intent: "update-name" }; } catch (error) { logger.error("Error updating participant:", error); return { error: "Failed to update participant. Please try again.", intent: "update-name" }; } } // Bulk add participants if (intent === "bulk") { const bulkNames = formData.get("bulkNames"); if (typeof bulkNames !== "string" || !bulkNames.trim()) { return { error: "Please enter at least one participant name" }; } // Dedup case-insensitively, preserving first occurrence's casing const seenKeys = new Map(); for (const raw of bulkNames.split("\n").map(n => n.trim()).filter(n => n.length > 0)) { const key = raw.toLowerCase(); if (!seenKeys.has(key)) seenKeys.set(key, raw); } const names = [...seenKeys.values()]; if (names.length === 0) { return { error: "Please enter at least one participant name" }; } try { const existingParticipants = await findParticipantsBySportsSeasonId(params.id); const existingNames = new Set(existingParticipants.map(p => p.name.toLowerCase())); const skipped: string[] = []; let added = 0; for (const name of names) { if (existingNames.has(name.toLowerCase())) { skipped.push(name); continue; } await createParticipant({ sportsSeasonId: params.id, name, shortName: null, externalId: null, expectedValue: "0", }); added++; } return { success: true, count: added, skipped }; } catch (error) { logger.error("Error creating participants:", error); return { error: "Failed to add participants. Please try again." }; } } if (intent === "copy-from-season") { const sourceSeasonId = formData.get("sourceSeasonId"); if (typeof sourceSeasonId !== "string" || !sourceSeasonId) { return { error: "Please select a source season.", intent: "copy-from-season" }; } const targetSeason = await findSportsSeasonById(params.id); if (!targetSeason) { return { error: "Target season not found.", intent: "copy-from-season" }; } const sourceSeason = await findSportsSeasonById(sourceSeasonId); if (!sourceSeason) { return { error: "Source season not found.", intent: "copy-from-season" }; } if (sourceSeason.sportId !== targetSeason.sportId) { return { error: "Source season must be the same sport.", intent: "copy-from-season" }; } try { const result = await copyParticipantsFromSeason(sourceSeasonId, params.id); return { success: true, intent: "copy-from-season", copyResult: result }; } catch (error) { logger.error("Error copying participants:", error); return { error: "Failed to copy participants. Please try again.", intent: "copy-from-season" }; } } // Add single participant const name = formData.get("name"); if (typeof name !== "string" || !name.trim()) { return { error: "Participant name is required" }; } const trimmedName = name.trim(); const existing = await findParticipantByName(params.id, trimmedName); if (existing) { return { error: `"${trimmedName}" already exists in this season.` }; } try { await createParticipant({ sportsSeasonId: params.id, name: trimmedName, shortName: null, externalId: null, expectedValue: "0", }); return { success: true }; } catch (error) { logger.error("Error creating participant:", error); return { error: "Failed to add participant. Please try again." }; } } export default function ManageParticipants({ loaderData, actionData }: Route.ComponentProps) { const { sportsSeason, participants, sameSportSeasons } = loaderData; const [editing, setEditing] = useState<{ id: string; name: string; externalId: string } | null>(null); const [sourceSeasonId, setSourceSeasonId] = useState(""); const cancelEdit = () => setEditing(null); // Use success state to reset forms by changing the key const formKey = actionData?.success ? Date.now() : 'static'; useEffect(() => { if (actionData?.success && (actionData?.intent === "update-name" || actionData?.intent === "update-participant")) { cancelEdit(); } }, [actionData]); return (

Manage Participants

{sportsSeason.sport.name} - {sportsSeason.name}

Add Participant Add a {sportsSeason.sport.type === "team" ? "team" : "player"} to this season
{actionData?.error && !actionData?.count && actionData?.intent !== "update-name" && actionData?.intent !== "update-participant" && (
{actionData.error}
)} {actionData?.success && !actionData?.count && actionData?.intent !== "update-participant" && (
Participant added successfully!
)}
Bulk Add Participants Add multiple participants at once (one per line)