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 { getEventTypeLabel } from "~/models/scoring-event"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { useState, useEffect } from "react"; import { format } from "date-fns"; import { localDateTimeToUtcIso } from "~/lib/date-utils"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `${data?.event?.name ?? "Event"} — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; } export { loader, action }; 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")); } }, [event.eventStartsAt]); return ( Edit Event
{ setDisplayValue(e.target.value); setEventStartsAtUtc(localDateTimeToUtcIso(e.target.value) ?? ""); }} />
); } export default function EventResults({ loaderData, actionData, }: Route.ComponentProps) { const { sportsSeason, event, participants, results, participantResults, seasonResults } = 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].toSorted((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(); // Non-scoring events have a simple view — edit name/date and toggle updated status if (event.eventType === "schedule_event") { const isPast = event.eventStartsAt ? new Date(event.eventStartsAt) < new Date() : event.eventDate !== null && event.eventDate !== undefined && String(event.eventDate).slice(0, 10) < new Date().toISOString().split("T")[0]; return (

{event.name}

{sportsSeason.sport.name} — {sportsSeason.name} • Non-Scoring

{event.isComplete ? ( Updated ) : ( {isPast ? "Results Pending" : "Upcoming"} )}
{actionData?.error && (
{actionData.error}
)} {actionData?.success && (
{actionData.success}
)}
Scoring Status {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."} {event.isComplete ? (
) : (
)}
); } return (

{event.name}

{sportsSeason.sport.name} - {sportsSeason.name} •{" "} {getEventTypeLabel(event.eventType)} {event.playoffRound && ` • ${event.playoffRound}`}

{event.eventType === "playoff_game" && ( )} {event.isComplete ? ( Completed ) : ( In Progress )} {event.isQualifyingEvent && ( Qualifying Event )}
{/* Edit Event Name/Date */} {actionData?.error && (
{actionData.error}
)} {actionData?.success && (
{actionData.success}
)} {/* Debug info and manual QP processing for completed major tournaments */} {event.isComplete && event.eventType === "major_tournament" && ( Event Processing Status
Event Type: {event.eventType}
Is Qualifying Event: {event.isQualifyingEvent ? 'Yes' : 'No'}
Sports Season Pattern: {sportsSeason.scoringPattern || 'not set'}
Results Count: {results.length}
{!event.isQualifyingEvent && (

This event needs to be marked as a qualifying event to award qualifying points.

)} {event.isQualifyingEvent && (
)}
)} {/* Season Standings Table for final_standings events */} {event.eventType === "final_standings" && !event.isComplete && ( <> Season Standings Tracker 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.
Participant Championship Points {participants.map((participant: { id: string; name: string }) => { const result = seasonResultsMap.get(participant.id); const currentPoints = result?.currentPoints || ""; return ( {participant.name} setHasChanges(true)} /> ); })}

{hasChanges ? "You have unsaved changes" : "Update standings after each race/event during the season"}

{/* Complete Season Button */} Finalize Season When the season is complete, mark this event as complete to:
  • Convert the top 8 participants (by position) to fantasy placements 1-8
  • Award fantasy points based on league scoring rules
  • Update all league standings

⚠️ Make sure all standings are saved before completing

)} {/* Regular Result Entry for other event types */} {event.eventType !== "final_standings" && event.eventType !== "playoff_game" && !event.isComplete && ( Add Result Enter the placement for a participant in this event
)} {/* Manual QP Processing Button (for existing events) */} {event.isComplete && event.eventType === "major_tournament" && sportsSeason.scoringPattern === "qualifying_points" && results.length > 0 && ( Process Qualifying Points This event is complete but qualifying points haven't been awarded yet. Click below to award QP based on the results entered.
)} {/* Current Results - Hide for playoff events since they use the bracket */} {event.eventType !== "playoff_game" && (
Results {results.length} of {participants.length} participants have results
{!event.isComplete && results.length > 0 && event.eventType !== "final_standings" && (
)}
{sortedResults.length === 0 ? (

No results added yet. Add results using the form above.

) : ( Placement Participant {event.isQualifyingEvent && event.isComplete && ( QP Awarded )} {!event.isComplete && Actions} {sortedResults.map((result) => (
{result.placement === 1 && ( 🥇 )} {result.placement === 2 && ( 🥈 )} {result.placement === 3 && ( 🥉 )} {result.placement}
{result.participant.name} {event.isQualifyingEvent && event.isComplete && ( {result.qualifyingPointsAwarded ? ( {parseFloat(result.qualifyingPointsAwarded).toFixed(2)} QP ) : ( 0 QP )} )} {!event.isComplete && (
{ if (e.key === 'Enter') { e.currentTarget.form?.requestSubmit(); } }} />
)}
))}
)}
)} {/* Bracket Explanation Card for Playoff Events */} {event.eventType === "playoff_game" && !participantResults?.length && ( Bracket Event This is a bracket/playoff event. Results are managed through the bracket interface.

To complete this event:

  1. Click "Manage Bracket" above to set match winners
  2. Once all matches are complete, click "Finalize Bracket"
  3. Fantasy placements and points will appear below automatically
)} {/* Participant Results with Fantasy Points */} {participantResults && participantResults.length > 0 && (
{event.eventType === "playoff_game" ? ( <> Fantasy Points Awarded (from Bracket) ) : ( "Fantasy Points Awarded" )} {event.eventType === "playoff_game" ? `${participantResults.length} participants assigned placements from bracket results` : "Points calculated from bracket placements (sorted by position)" }
{event.eventType === "playoff_game" && event.isComplete && ( Bracket Finalized )}
Final Position Participant Fantasy Points {[...participantResults] .toSorted((a: { finalPosition?: number | null }, b: { finalPosition?: number | null }) => { const posA = a.finalPosition ?? 999; const posB = b.finalPosition ?? 999; return posA - posB; }) .map((result) => (
{result.finalPosition === 1 && ( 🥇 )} {result.finalPosition === 2 && ( 🥈 )} {result.finalPosition === 3 && ( 🥉 )} {result.finalPosition === 0 ? ( Early Elimination ) : ( `${result.finalPosition}${ result.finalPosition === 1 ? "st" : result.finalPosition === 2 ? "nd" : result.finalPosition === 3 ? "rd" : "th" }` )}
{result.participant?.name} {result.qualifyingPoints ? ( {parseFloat(result.qualifyingPoints).toFixed(2)} pts ) : ( - )}
))}
)}
); }