2025-10-31 22:13:12 -07:00
|
|
|
|
import { Form, Link } from "react-router";
|
|
|
|
|
|
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId";
|
2026-03-10 12:10:52 -07:00
|
|
|
|
|
2025-10-31 22:13:12 -07:00
|
|
|
|
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";
|
2025-11-08 22:35:07 -08:00
|
|
|
|
import { ArrowLeft, Trophy, CheckCircle2, Pencil, Trash2, Brackets, Save } from "lucide-react";
|
2026-03-07 21:59:29 -08:00
|
|
|
|
import { getEventTypeLabel } from "~/models/scoring-event";
|
2025-10-31 22:13:12 -07:00
|
|
|
|
import {
|
|
|
|
|
|
Table,
|
|
|
|
|
|
TableBody,
|
|
|
|
|
|
TableCell,
|
|
|
|
|
|
TableHead,
|
|
|
|
|
|
TableHeader,
|
|
|
|
|
|
TableRow,
|
|
|
|
|
|
} from "~/components/ui/table";
|
2026-03-15 10:22:42 -07:00
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
|
|
import { format } from "date-fns";
|
|
|
|
|
|
import { localDateTimeToUtcIso } from "~/lib/date-utils";
|
2025-10-31 22:13:12 -07:00
|
|
|
|
|
2026-03-10 12:10:52 -07:00
|
|
|
|
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
|
|
|
|
|
return [{ title: `${data?.event?.name ?? "Event"} — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 22:13:12 -07:00
|
|
|
|
export { loader, action };
|
|
|
|
|
|
|
2026-03-15 10:22:42 -07:00
|
|
|
|
function EditEventCard({ event }: { event: { name: string; eventDate?: string | null; eventStartsAt?: Date | string | null } }) {
|
|
|
|
|
|
// Keep the UTC ISO value for the hidden field (safe for SSR)
|
|
|
|
|
|
const [eventStartsAtUtc, setEventStartsAtUtc] = useState(
|
|
|
|
|
|
event.eventStartsAt ? new Date(event.eventStartsAt).toISOString() : ""
|
|
|
|
|
|
);
|
|
|
|
|
|
// Compute the display value client-side only — format() uses the runtime timezone,
|
|
|
|
|
|
// so running it on the server would pre-fill the wrong time for non-UTC admins.
|
|
|
|
|
|
const [displayValue, setDisplayValue] = useState("");
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (event.eventStartsAt) {
|
|
|
|
|
|
setDisplayValue(format(new Date(event.eventStartsAt), "yyyy-MM-dd'T'HH:mm"));
|
|
|
|
|
|
}
|
2026-03-21 09:44:05 -07:00
|
|
|
|
}, [event.eventStartsAt]);
|
2026-03-15 10:22:42 -07:00
|
|
|
|
|
2026-03-07 23:43:24 -08:00
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Edit Event</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post" className="space-y-4">
|
|
|
|
|
|
<input type="hidden" name="intent" value="update-event" />
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="name">Name</Label>
|
|
|
|
|
|
<Input id="name" name="name" defaultValue={event.name} required />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-2">
|
2026-03-15 10:22:42 -07:00
|
|
|
|
<Label htmlFor="eventStartsAtLocal">Event Date & Time (Optional)</Label>
|
2026-03-07 23:43:24 -08:00
|
|
|
|
<Input
|
2026-03-15 10:22:42 -07:00
|
|
|
|
id="eventStartsAtLocal"
|
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
|
value={displayValue}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
setDisplayValue(e.target.value);
|
|
|
|
|
|
setEventStartsAtUtc(localDateTimeToUtcIso(e.target.value) ?? "");
|
|
|
|
|
|
}}
|
2026-03-07 23:43:24 -08:00
|
|
|
|
/>
|
2026-03-15 10:22:42 -07:00
|
|
|
|
<input type="hidden" name="eventStartsAt" value={eventStartsAtUtc} />
|
2026-03-07 23:43:24 -08:00
|
|
|
|
</div>
|
|
|
|
|
|
<Button type="submit" variant="outline">
|
|
|
|
|
|
<Save className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Save Changes
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 22:13:12 -07:00
|
|
|
|
export default function EventResults({
|
|
|
|
|
|
loaderData,
|
|
|
|
|
|
actionData,
|
|
|
|
|
|
}: Route.ComponentProps) {
|
2026-03-07 23:43:24 -08:00
|
|
|
|
const { sportsSeason, event, participants, results, participantResults, seasonResults } = loaderData;
|
2025-11-08 22:35:07 -08:00
|
|
|
|
const [hasChanges, setHasChanges] = useState(false);
|
2025-10-31 22:13:12 -07:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-03-21 09:44:05 -07:00
|
|
|
|
const sortedResults = [...results].toSorted((a, b) => (a.placement || 999) - (b.placement || 999));
|
2025-10-31 22:13:12 -07:00
|
|
|
|
|
2025-11-08 22:35:07 -08:00
|
|
|
|
// Create a map of season results for easy lookup
|
|
|
|
|
|
const seasonResultsMap = seasonResults
|
|
|
|
|
|
? new Map(
|
|
|
|
|
|
seasonResults.map((r: { participantId: string }) => [r.participantId, r])
|
|
|
|
|
|
)
|
|
|
|
|
|
: new Map();
|
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
|
// Non-scoring events have a simple view — edit name/date and toggle updated status
|
|
|
|
|
|
if (event.eventType === "schedule_event") {
|
2026-03-15 12:28:39 -07:00
|
|
|
|
const isPast = event.eventStartsAt
|
|
|
|
|
|
? new Date(event.eventStartsAt) < new Date()
|
2026-03-21 09:44:05 -07:00
|
|
|
|
: event.eventDate !== null && event.eventDate !== undefined && String(event.eventDate).slice(0, 10) < new Date().toISOString().split("T")[0];
|
2026-03-07 21:59:29 -08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="p-8">
|
|
|
|
|
|
<div className="max-w-2xl">
|
|
|
|
|
|
<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} • Non-Scoring
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{event.isComplete ? (
|
|
|
|
|
|
<Badge variant="default" className="bg-emerald-500">
|
|
|
|
|
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
|
|
|
|
|
Updated
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
{isPast ? "Results Pending" : "Upcoming"}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{actionData?.error && (
|
|
|
|
|
|
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm mb-4">
|
|
|
|
|
|
{actionData.error}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{actionData?.success && (
|
|
|
|
|
|
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm mb-4">
|
|
|
|
|
|
{actionData.success}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
2026-03-07 23:43:24 -08:00
|
|
|
|
<EditEventCard event={event} />
|
2026-03-07 21:59:29 -08:00
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Scoring Status</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
{event.isComplete
|
|
|
|
|
|
? "This event has been noted as updated in standings."
|
|
|
|
|
|
: isPast
|
|
|
|
|
|
? "This event has passed — mark it once standings have been updated."
|
|
|
|
|
|
: "This event hasn't occurred yet."}
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
{event.isComplete ? (
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="uncomplete" />
|
|
|
|
|
|
<Button type="submit" variant="outline" size="sm">
|
|
|
|
|
|
Mark as Not Updated
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="complete" />
|
|
|
|
|
|
<Button type="submit" size="sm">
|
|
|
|
|
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Mark as Updated
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-10-31 22:13:12 -07:00
|
|
|
|
|
|
|
|
|
|
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>
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
<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 ? (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Badge variant="default" className="bg-emerald-500">
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
|
|
|
|
|
Completed
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Badge variant="secondary">In Progress</Badge>
|
|
|
|
|
|
)}
|
2025-11-11 10:08:25 -08:00
|
|
|
|
{event.isQualifyingEvent && (
|
|
|
|
|
|
<Badge variant="outline" className="border-amber-500 text-amber-600">
|
|
|
|
|
|
Qualifying Event
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
</div>
|
2025-10-31 22:13:12 -07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-6">
|
2026-03-07 23:43:24 -08:00
|
|
|
|
{/* Edit Event Name/Date */}
|
|
|
|
|
|
<EditEventCard event={event} />
|
|
|
|
|
|
|
|
|
|
|
|
{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-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
|
|
|
|
|
|
{actionData.success}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-11-11 10:08:25 -08:00
|
|
|
|
{/* Debug info and manual QP processing for completed major tournaments */}
|
|
|
|
|
|
{event.isComplete && event.eventType === "major_tournament" && (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Card className="border-electric/30">
|
2025-11-11 10:08:25 -08:00
|
|
|
|
<CardHeader>
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<CardTitle className="text-electric">
|
2025-11-11 10:08:25 -08:00
|
|
|
|
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>
|
|
|
|
|
|
)}
|
2025-10-31 22:13:12 -07:00
|
|
|
|
|
2025-11-08 22:35:07 -08:00
|
|
|
|
{/* 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 */}
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Card className="border-amber-accent/30">
|
2025-11-08 22:35:07 -08:00
|
|
|
|
<CardHeader>
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<CardTitle className="text-amber-accent">
|
2025-11-08 22:35:07 -08:00
|
|
|
|
<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 */}
|
2026-03-07 23:43:24 -08:00
|
|
|
|
{event.eventType !== "final_standings" && event.eventType !== "playoff_game" && !event.isComplete && (
|
2025-10-31 22:13:12 -07:00
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-11-11 10:08:25 -08:00
|
|
|
|
{/* 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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-11-14 20:01:21 -08:00
|
|
|
|
{/* Current Results - Hide for playoff events since they use the bracket */}
|
|
|
|
|
|
{event.eventType !== "playoff_game" && (
|
|
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2025-10-31 22:13:12 -07:00
|
|
|
|
</div>
|
2025-11-14 20:01:21 -08:00
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
{sortedResults.length === 0 ? (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
No results added yet. Add results using the form above.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : (
|
2025-10-31 22:13:12 -07:00
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead className="w-24">Placement</TableHead>
|
|
|
|
|
|
<TableHead>Participant</TableHead>
|
2025-11-11 10:08:25 -08:00
|
|
|
|
{event.isQualifyingEvent && event.isComplete && (
|
|
|
|
|
|
<TableHead className="w-32 text-right">QP Awarded</TableHead>
|
|
|
|
|
|
)}
|
2025-10-31 22:13:12 -07:00
|
|
|
|
{!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>
|
2025-11-11 10:08:25 -08:00
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2025-10-31 22:13:12 -07:00
|
|
|
|
{!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>
|
2025-11-14 20:01:21 -08:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Bracket Explanation Card for Playoff Events */}
|
|
|
|
|
|
{event.eventType === "playoff_game" && !participantResults?.length && (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Card className="border-electric/30 bg-electric/10">
|
2025-11-14 20:01:21 -08:00
|
|
|
|
<CardHeader>
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<CardTitle className="text-electric">
|
2025-11-14 20:01:21 -08:00
|
|
|
|
<Brackets className="inline mr-2 h-5 w-5" />
|
|
|
|
|
|
Bracket Event
|
|
|
|
|
|
</CardTitle>
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<CardDescription className="text-electric">
|
2025-11-14 20:01:21 -08:00
|
|
|
|
This is a bracket/playoff event. Results are managed through the bracket interface.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<div className="space-y-3 text-sm">
|
|
|
|
|
|
<p>To complete this event:</p>
|
|
|
|
|
|
<ol className="list-decimal list-inside space-y-2 ml-2">
|
|
|
|
|
|
<li>Click "Manage Bracket" above to set match winners</li>
|
|
|
|
|
|
<li>Once all matches are complete, click "Finalize Bracket"</li>
|
|
|
|
|
|
<li>Fantasy placements and points will appear below automatically</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
<div className="pt-3">
|
|
|
|
|
|
<Button variant="outline" asChild>
|
|
|
|
|
|
<Link to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}/bracket`}>
|
|
|
|
|
|
<Brackets className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Go to Bracket Manager
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
2025-11-04 22:09:44 -08:00
|
|
|
|
|
|
|
|
|
|
{/* Participant Results with Fantasy Points */}
|
|
|
|
|
|
{participantResults && participantResults.length > 0 && (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Card className={event.eventType === "playoff_game" ? "border-emerald-500/30" : ""}>
|
2025-11-04 22:09:44 -08:00
|
|
|
|
<CardHeader>
|
2025-11-14 20:01:21 -08:00
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<CardTitle className={event.eventType === "playoff_game" ? "text-emerald-400" : ""}>
|
2025-11-14 20:01:21 -08:00
|
|
|
|
{event.eventType === "playoff_game" ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Trophy className="inline mr-2 h-5 w-5" />
|
|
|
|
|
|
Fantasy Points Awarded (from Bracket)
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
"Fantasy Points Awarded"
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
{event.eventType === "playoff_game"
|
|
|
|
|
|
? `${participantResults.length} participants assigned placements from bracket results`
|
|
|
|
|
|
: "Points calculated from bracket placements (sorted by position)"
|
|
|
|
|
|
}
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{event.eventType === "playoff_game" && event.isComplete && (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Badge variant="default" className="bg-emerald-500">
|
2025-11-14 20:01:21 -08:00
|
|
|
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
|
|
|
|
|
Bracket Finalized
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-11-04 22:09:44 -08:00
|
|
|
|
</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]
|
2026-03-21 09:44:05 -07:00
|
|
|
|
.toSorted((a: { finalPosition?: number | null }, b: { finalPosition?: number | null }) => {
|
2025-11-04 22:09:44 -08:00
|
|
|
|
const posA = a.finalPosition ?? 999;
|
|
|
|
|
|
const posB = b.finalPosition ?? 999;
|
|
|
|
|
|
return posA - posB;
|
|
|
|
|
|
})
|
2026-03-21 09:44:05 -07:00
|
|
|
|
.map((result) => (
|
2025-11-04 22:09:44 -08:00
|
|
|
|
<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>
|
2026-03-21 09:44:05 -07:00
|
|
|
|
<TableCell>{result.participant?.name}</TableCell>
|
2025-11-04 22:09:44 -08:00
|
|
|
|
<TableCell className="text-right font-semibold">
|
|
|
|
|
|
{result.qualifyingPoints ? (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<span className={result.qualifyingPoints === "0.00" ? "text-muted-foreground" : "text-emerald-400"}>
|
2025-11-11 10:08:25 -08:00
|
|
|
|
{parseFloat(result.qualifyingPoints).toFixed(2)} pts
|
2025-11-04 22:09:44 -08:00
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-muted-foreground">-</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
2025-10-31 22:13:12 -07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|