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 { isBracketMajor } from "~/lib/event-utils";
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";
import { BatchResultEntry } from "~/components/BatchResultEntry";
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
);
}
export default function EventResults({
loaderData,
actionData,
}: Route.ComponentProps) {
const { sportsSeason, event, participants, results, participantResults, seasonResults, notParticipatingIds, isReadOnlySibling, canonicalTournamentId } = loaderData;
const [hasChanges, setHasChanges] = useState(false);
// Banner shown on read-only sibling events: scoring lives on the canonical
// tournament page, and results fan out here automatically.
const readOnlySiblingBanner = isReadOnlySibling ? (
This event is part of a shared major. Scoring is done once on the{" "}
tournament page
{" "}
and fans out to every window automatically — the controls here are read-only.
) : null;
const isBracketEvent = event.eventType === "playoff_game" || isBracketMajor(sportsSeason.sport?.simulatorType);
// Create a map of participants with results for easy lookup
const participantResultsMap = new Map(
results.map((r: { seasonParticipant: { id: string } }) => [r.seasonParticipant.id, r])
);
// Get participants without results (and not marked as DNP)
const participantsWithoutResults = participants.filter(
(p: { id: string }) => !participantResultsMap.has(p.id) && !notParticipatingIds.has(p.id)
);
// Sort results by placement, excluding DNP rows (those are shown in the DNP card)
const sortedResults = [...results]
.filter((r: { notParticipating?: boolean | null }) => !r.notParticipating)
.toSorted((a, b) => (a.placement || 999) - (b.placement || 999));
// Map of participantId → result ID for DNP rows (needed for unmark action)
const dnpResultIdMap = new Map(
results
.filter((r: { notParticipating?: boolean | null; seasonParticipant: { id: string } }) => r.notParticipating)
.map((r: { id: string; seasonParticipant: { id: string } }) => [r.seasonParticipant.id, r.id])
);
// 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 (
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 ? (
) : (
)}
)}
{/* 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.
{/* 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
>
)}
{/* Not Participating — mark known withdrawals before the event runs */}
{event.isQualifyingEvent && event.eventType === "major_tournament" && !event.isComplete && (
Not Participating
Mark participants who will not compete in this event (e.g. injury withdrawal).
The simulator will exclude them from this event's draw when calculating EV.
{/* Currently marked DNP */}
{notParticipatingIds.size > 0 && (
)}
{/* Mark a participant as DNP */}
{participantsWithoutResults.length > 0 && (
)}
{notParticipatingIds.size === 0 && participantsWithoutResults.length === 0 && (
All participants already have results entered.
)}
)}
{/* Batch Paste Results — primary entry for qualifying events */}
{event.isQualifyingEvent && event.eventType !== "final_standings" && !isBracketEvent && !event.isComplete && (
r.seasonParticipant.id))
}
/>
)}
{/* Regular Result Entry for other event types */}
{event.eventType !== "final_standings" && !isBracketEvent && !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 bracket events since they use the bracket */}
{!isBracketEvent && (
Results
{results.length} of {participants.length} participants have
results
)}
)}
{/* Bracket Explanation Card for Bracket Events */}
{isBracketEvent && !participantResults?.length && (
Bracket Event
This is a bracket/playoff event. Results are managed through the bracket interface.
To complete this event:
Click "Manage Bracket" above to set match winners
Once all matches are complete, click "Finalize Bracket"
Fantasy placements and points will appear below automatically
)}
{/* Participant Results with Fantasy Points */}
{participantResults && participantResults.length > 0 && (
{isBracketEvent ? (
<>
Fantasy Points Awarded (from Bracket)
>
) : (
"Fantasy Points Awarded"
)}
{isBracketEvent
? `${participantResults.length} participants assigned placements from bracket results`
: "Points calculated from bracket placements (sorted by position)"
}