brackt/app/routes/admin.sports-seasons.$id.events.$eventId.tsx

565 lines
24 KiB
TypeScript
Raw Normal View History

import { Form, Link } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId";
import { loader, action } from "./admin.sports-seasons.$id.events.$eventId.server";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { Badge } from "~/components/ui/badge";
import { ArrowLeft, Trophy, CheckCircle2, Pencil, Trash2, Brackets, Save } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import { useState } from "react";
export { loader, action };
export default function EventResults({
loaderData,
actionData,
}: Route.ComponentProps) {
const { sportsSeason, event, participants, results, participantResults, seasonResults, qpConfig, qpStandings } = loaderData;
const [hasChanges, setHasChanges] = useState(false);
// Create a map of participants with results for easy lookup
const participantResultsMap = new Map(
results.map((r: { participant: { id: string } }) => [r.participant.id, r])
);
// Get participants without results
const participantsWithoutResults = participants.filter(
(p: { id: string }) => !participantResultsMap.has(p.id)
);
// Sort results by placement
const sortedResults = [...results].sort((a, b) => (a.placement || 999) - (b.placement || 999));
// Create a map of season results for easy lookup
const seasonResultsMap = seasonResults
? new Map(
seasonResults.map((r: { participantId: string }) => [r.participantId, r])
)
: new Map();
const getEventTypeLabel = (type: string) => {
switch (type) {
case "playoff_game":
return "Bracket";
case "major_tournament":
return "Major Tournament";
case "final_standings":
return "Final Standings";
default:
return type;
}
};
return (
<div className="p-8">
<div className="max-w-4xl">
<div className="mb-6">
<Button variant="ghost" size="sm" asChild className="mb-2">
<Link to={`/admin/sports-seasons/${sportsSeason.id}/events`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Events
</Link>
</Button>
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">{event.name}</h1>
<p className="text-muted-foreground mt-1">
{sportsSeason.sport.name} - {sportsSeason.name} {" "}
{getEventTypeLabel(event.eventType)}
{event.playoffRound && `${event.playoffRound}`}
</p>
</div>
<div className="flex items-center gap-2">
{event.eventType === "playoff_game" && (
<Button variant="outline" asChild>
<Link to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}/bracket`}>
<Brackets className="mr-2 h-4 w-4" />
Manage Bracket
</Link>
</Button>
)}
{event.isComplete ? (
<Badge variant="default" className="bg-green-500">
<CheckCircle2 className="mr-1 h-3 w-3" />
Completed
</Badge>
) : (
<Badge variant="secondary">In Progress</Badge>
)}
{event.isQualifyingEvent && (
<Badge variant="outline" className="border-amber-500 text-amber-600">
Qualifying Event
</Badge>
)}
</div>
</div>
</div>
<div className="space-y-6">
{/* Debug info and manual QP processing for completed major tournaments */}
{event.isComplete && event.eventType === "major_tournament" && (
<Card className="border-blue-200 dark:border-blue-800">
<CardHeader>
<CardTitle className="text-blue-600 dark:text-blue-400">
Event Processing Status
</CardTitle>
<CardDescription>
<div className="space-y-1 font-mono text-xs">
<div>Event Type: {event.eventType}</div>
<div>Is Qualifying Event: {event.isQualifyingEvent ? 'Yes' : 'No'}</div>
<div>Sports Season Pattern: {sportsSeason.scoringPattern || 'not set'}</div>
<div>Results Count: {results.length}</div>
</div>
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{!event.isQualifyingEvent && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
This event needs to be marked as a qualifying event to award qualifying points.
</p>
<Form method="post">
<input type="hidden" name="intent" value="mark-qualifying" />
<Button type="submit" variant="outline">
Mark as Qualifying Event
</Button>
</Form>
</div>
)}
{event.isQualifyingEvent && (
<Form method="post">
<input type="hidden" name="intent" value="process-qp" />
<Button type="submit" className="bg-blue-600 hover:bg-blue-700">
<Trophy className="mr-2 h-4 w-4" />
Process/Reprocess Qualifying Points
</Button>
</Form>
)}
</div>
</CardContent>
</Card>
)}
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
{actionData?.success && (
<div className="bg-green-500/15 text-green-700 dark:text-green-400 px-4 py-3 rounded-md text-sm">
{actionData.success}
</div>
)}
{/* Season Standings Table for final_standings events */}
{event.eventType === "final_standings" && !event.isComplete && (
<>
<Card>
<CardHeader>
<CardTitle>Season Standings Tracker</CardTitle>
<CardDescription>
Enter championship points for each participant. Positions are automatically calculated based on points (highest = 1st).
When the season ends, mark this event as complete to assign fantasy points to the top 8.
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="update-standings" />
<Table>
<TableHeader>
<TableRow>
<TableHead>Participant</TableHead>
<TableHead className="text-right">Championship Points</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{participants.map((participant: { id: string; name: string }) => {
const result = seasonResultsMap.get(participant.id);
const currentPoints = result?.currentPoints || "";
return (
<TableRow key={participant.id}>
<TableCell className="font-medium">
{participant.name}
</TableCell>
<TableCell className="text-right">
<Input
type="number"
name={`points-${participant.id}`}
defaultValue={currentPoints}
placeholder="e.g., 250"
step="0.01"
min="0"
className="w-32 ml-auto"
onChange={() => setHasChanges(true)}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="flex justify-between items-center pt-4">
<p className="text-sm text-muted-foreground">
{hasChanges
? "You have unsaved changes"
: "Update standings after each race/event during the season"}
</p>
<Button type="submit">
<Save className="mr-2 h-4 w-4" />
Save Standings
</Button>
</div>
</Form>
</CardContent>
</Card>
{/* Complete Season Button */}
<Card className="border-orange-200 dark:border-orange-800">
<CardHeader>
<CardTitle className="text-orange-600 dark:text-orange-400">
<Trophy className="inline mr-2 h-5 w-5" />
Finalize Season
</CardTitle>
<CardDescription>
When the season is complete, mark this event as complete to:
<ul className="list-disc list-inside mt-2 space-y-1">
<li>Convert the top 8 participants (by position) to fantasy placements 1-8</li>
<li>Award fantasy points based on league scoring rules</li>
<li>Update all league standings</li>
</ul>
<p className="mt-2 font-semibold text-orange-600 dark:text-orange-400">
Make sure all standings are saved before completing
</p>
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post">
<input type="hidden" name="intent" value="complete" />
<Button type="submit" variant="default" className="bg-orange-600 hover:bg-orange-700">
<CheckCircle2 className="mr-2 h-4 w-4" />
Mark Season Complete & Assign Fantasy Points
</Button>
</Form>
</CardContent>
</Card>
</>
)}
{/* Regular Result Entry for other event types */}
{event.eventType !== "final_standings" && !event.isComplete && (
<Card>
<CardHeader>
<CardTitle>Add Result</CardTitle>
<CardDescription>
Enter the placement for a participant in this event
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="add-result" />
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="participantId">Participant</Label>
<Select name="participantId" required>
<SelectTrigger id="participantId">
<SelectValue placeholder="Select participant" />
</SelectTrigger>
<SelectContent>
{participantsWithoutResults.length === 0 ? (
<div className="p-2 text-sm text-muted-foreground">
All participants have results
</div>
) : (
participantsWithoutResults.map((participant: { id: string; name: string }) => (
<SelectItem
key={participant.id}
value={participant.id}
>
{participant.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="placement">Placement</Label>
<Input
id="placement"
name="placement"
type="number"
min="1"
max="100"
placeholder="e.g., 1"
required
/>
</div>
</div>
<Button
type="submit"
className="w-full"
disabled={participantsWithoutResults.length === 0}
>
<Trophy className="mr-2 h-4 w-4" />
Add Result
</Button>
</Form>
</CardContent>
</Card>
)}
{/* Manual QP Processing Button (for existing events) */}
{event.isComplete &&
event.eventType === "major_tournament" &&
sportsSeason.scoringPattern === "qualifying_points" &&
results.length > 0 && (
<Card className="border-amber-200 dark:border-amber-800">
<CardHeader>
<CardTitle className="text-amber-600 dark:text-amber-400">
Process Qualifying Points
</CardTitle>
<CardDescription>
This event is complete but qualifying points haven't been awarded yet.
Click below to award QP based on the results entered.
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post">
<input type="hidden" name="intent" value="process-qp" />
<Button type="submit" className="bg-amber-600 hover:bg-amber-700">
<Trophy className="mr-2 h-4 w-4" />
Award Qualifying Points Now
</Button>
</Form>
</CardContent>
</Card>
)}
{/* Current Results */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Results</CardTitle>
<CardDescription>
{results.length} of {participants.length} participants have
results
</CardDescription>
</div>
{!event.isComplete && results.length > 0 && event.eventType !== "final_standings" && (
<Form method="post">
<input type="hidden" name="intent" value="complete" />
<Button type="submit" variant="outline" size="sm">
<CheckCircle2 className="mr-2 h-4 w-4" />
Mark Event Complete
</Button>
</Form>
)}
</div>
</CardHeader>
<CardContent>
{sortedResults.length === 0 ? (
<p className="text-sm text-muted-foreground">
No results added yet. Add results using the form above.
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-24">Placement</TableHead>
<TableHead>Participant</TableHead>
{event.isQualifyingEvent && event.isComplete && (
<TableHead className="w-32 text-right">QP Awarded</TableHead>
)}
{!event.isComplete && <TableHead className="w-32">Actions</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{sortedResults.map((result) => (
<TableRow key={result.id}>
<TableCell>
<div className="flex items-center gap-2">
{result.placement === 1 && (
<span className="text-xl">🥇</span>
)}
{result.placement === 2 && (
<span className="text-xl">🥈</span>
)}
{result.placement === 3 && (
<span className="text-xl">🥉</span>
)}
<span className="font-semibold">
{result.placement}
</span>
</div>
</TableCell>
<TableCell>{result.participant.name}</TableCell>
{event.isQualifyingEvent && event.isComplete && (
<TableCell className="text-right">
{result.qualifyingPointsAwarded ? (
<span className="font-semibold text-amber-600 dark:text-amber-400">
{parseFloat(result.qualifyingPointsAwarded).toFixed(2)} QP
</span>
) : (
<span className="text-muted-foreground">0 QP</span>
)}
</TableCell>
)}
{!event.isComplete && (
<TableCell>
<div className="flex items-center gap-2">
<Form method="post" className="inline">
<input type="hidden" name="intent" value="update-result" />
<input type="hidden" name="resultId" value={result.id} />
<Input
type="number"
name="placement"
defaultValue={result.placement || ""}
min="1"
max="100"
className="w-16 h-8 text-sm"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.form?.requestSubmit();
}
}}
/>
<Button type="submit" size="sm" variant="ghost" className="h-8 w-8 p-0">
<Pencil className="h-3 w-3" />
</Button>
</Form>
<Form method="post" className="inline">
<input type="hidden" name="intent" value="delete-result" />
<input type="hidden" name="resultId" value={result.id} />
<Button
type="submit"
size="sm"
variant="ghost"
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
onClick={(e) => {
if (!confirm(`Delete result for ${result.participant.name}?`)) {
e.preventDefault();
}
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</Form>
</div>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* Participant Results with Fantasy Points */}
{participantResults && participantResults.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Fantasy Points Awarded</CardTitle>
<CardDescription>
Points calculated from bracket placements (sorted by position)
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-32">Final Position</TableHead>
<TableHead>Participant</TableHead>
<TableHead className="w-32 text-right">Fantasy Points</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...participantResults]
.sort((a: any, b: any) => {
const posA = a.finalPosition ?? 999;
const posB = b.finalPosition ?? 999;
return posA - posB;
})
.map((result: any) => (
<TableRow key={result.id}>
<TableCell>
<div className="flex items-center gap-2">
{result.finalPosition === 1 && (
<span className="text-xl">🥇</span>
)}
{result.finalPosition === 2 && (
<span className="text-xl">🥈</span>
)}
{result.finalPosition === 3 && (
<span className="text-xl">🥉</span>
)}
<span className="font-semibold">
{result.finalPosition === 0 ? (
<span className="text-muted-foreground">Early Elimination</span>
) : (
`${result.finalPosition}${
result.finalPosition === 1
? "st"
: result.finalPosition === 2
? "nd"
: result.finalPosition === 3
? "rd"
: "th"
}`
)}
</span>
</div>
</TableCell>
<TableCell>{result.participant.name}</TableCell>
<TableCell className="text-right font-semibold">
{result.qualifyingPoints ? (
<span className={result.qualifyingPoints === "0.00" ? "text-muted-foreground" : "text-green-600 dark:text-green-400"}>
{parseFloat(result.qualifyingPoints).toFixed(2)} pts
</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
</div>
</div>
</div>
);
}