import { Form, Link, useFetcher } from "react-router"; import { Fragment, useState, useEffect, useMemo, useRef } from "react"; import { localDateTimeToUtcIso, utcIsoToLocalDateTime } from "~/lib/date-utils"; import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket"; import { loader, action } from "./admin.sports-seasons.$id.events.$eventId.bracket.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 { ArrowLeft, Plus, Trophy, X, Check, ChevronDown, ChevronRight, Calendar, TrendingUp, Trash2 } from "lucide-react"; import { getAllBracketTemplates, getBracketTemplate, getOrderedRoundsFromMatches, buildNCAA68SlotMap, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates"; import { computeGroupStandings } from "~/models/group-stage-match"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { Badge } from "~/components/ui/badge"; import { ParticipantSelector } from "~/components/ParticipantSelector"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Bracket — ${data?.event?.name ?? "Event"} - Brackt Admin` }]; } /** * Schedule editor for a single group-stage match. * * The admin enters a local wall-clock time; on submit it is converted to a UTC * ISO string (in the hidden `scheduledAt` field) so the DB stores a true UTC * instant. The stored UTC value is rendered back into the input in the * browser's local timezone via a client-only effect (avoids SSR hydration * mismatch, since the server runs in UTC). */ function GroupMatchScheduleForm({ match, localTzAbbr, }: { match: { id: string; scheduledAt: string | Date | null | undefined }; localTzAbbr: string; }) { const inputRef = useRef(null); useEffect(() => { if (inputRef.current) { inputRef.current.value = utcIsoToLocalDateTime(match.scheduledAt); } }, [match.scheduledAt]); return (
{ const form = e.currentTarget; const local = form.elements.namedItem("scheduledAtLocal") as HTMLInputElement; const hidden = form.elements.namedItem("scheduledAt") as HTMLInputElement; if (hidden) hidden.value = localDateTimeToUtcIso(local?.value) ?? ""; }} > {/* Hidden field holds the UTC ISO string written by onSubmit */} ); } export { loader, action }; /** * One "possible duplicate" row in the draw preview. Uses a fetcher so resolving * it (rename existing / create as new) submits in the background — the preview * stays on screen instead of reloading the page and forcing a fresh dry run. */ function DuplicateRow({ p, }: { p: { name: string; externalId: string | null; suggestion: string; suggestionId: string }; }) { const fetcher = useFetcher<{ success?: string; error?: string }>(); const busy = fetcher.state !== "idle"; return (
  • {p.name} → looks like {p.suggestion} {fetcher.data?.success ? ( ✓ {fetcher.data.success} ) : ( )} {fetcher.data?.error && ( {fetcher.data.error} )}
  • ); } export default function EventBracket({ loaderData, actionData, }: Route.ComponentProps) { const { sportsSeason, event, participants, matches, tournamentGroups } = loaderData; const [selectedTemplate, setSelectedTemplate] = useState("simple_8"); const [selectedParticipants, setSelectedParticipants] = useState>({}); const [selectedWinners, setSelectedWinners] = useState>({}); const [knockoutAssignments, setKnockoutAssignments] = useState>({}); const [expandedMatches, setExpandedMatches] = useState>(new Set()); const localTzAbbr = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" }) .formatToParts(new Date()) .find(p => p.type === "timeZoneName")?.value ?? "local"; const toggleMatchExpanded = (matchId: string) => { setExpandedMatches(prev => { const next = new Set(prev); if (next.has(matchId)) next.delete(matchId); else next.add(matchId); return next; }); }; const templates = getAllBracketTemplates(); const template = templates.find((t) => t.id === selectedTemplate); // Determine if this is a group-stage event const hasGroupStage = template?.groupStage !== null && template?.groupStage !== undefined; // Determine if this template uses named regions (e.g. NCAA 68) const hasRegions = (template?.regions?.length ?? 0) > 0; // Per-region configuration state: name + which seed slots are play-ins // Initialized from: stored event config → template defaults → empty const buildDefaultRegionConfig = (tmpl: typeof template) => { const source = (event.bracketRegionConfig as unknown as BracketRegion[] | null) ?? tmpl?.regions ?? []; return source.map((r) => ({ name: r.name, playInSeeds: r.playIns.map((pi) => pi.seedSlot), })); }; const [regionConfig, setRegionConfig] = useState(() => buildDefaultRegionConfig(template)); // Derive effective BracketRegion[] from regionConfig state const effectiveRegions: BracketRegion[] = useMemo(() => { return regionConfig.map((cfg) => { const playInSet = new Set(cfg.playInSeeds); return { name: cfg.name, directSeeds: ALL_16_SEEDS.filter((s) => !playInSet.has(s)), playIns: cfg.playInSeeds.map((s) => ({ seedSlot: s, teams: 2 as const })), }; }); }, [regionConfig]); const regionSlotMap = useMemo( () => (effectiveRegions.length > 0 ? buildNCAA68SlotMap(effectiveRegions) : null), [effectiveRegions] ); // Region config handlers const updateRegionName = (regionIdx: number, name: string) => { setRegionConfig((prev) => prev.map((r, i) => (i === regionIdx ? { ...r, name } : r)) ); }; const addPlayIn = (regionIdx: number) => { setRegionConfig((prev) => prev.map((r, i) => { if (i !== regionIdx || r.playInSeeds.length >= 2) return r; // Default to seed 16 if no play-ins yet, otherwise seed 11 const defaultSeed = r.playInSeeds.length === 0 ? 16 : 11; const seed = r.playInSeeds.includes(defaultSeed) ? ([11, 16].find((s) => !r.playInSeeds.includes(s)) ?? 11) : defaultSeed; return { ...r, playInSeeds: [...r.playInSeeds, seed] }; }) ); // Reset participant selections since slot indices change setSelectedParticipants({}); }; const removePlayIn = (regionIdx: number, piIdx: number) => { setRegionConfig((prev) => prev.map((r, i) => i === regionIdx ? { ...r, playInSeeds: r.playInSeeds.filter((_, j) => j !== piIdx) } : r ) ); setSelectedParticipants({}); }; const updatePlayInSeed = (regionIdx: number, piIdx: number, seed: number) => { setRegionConfig((prev) => prev.map((r, i) => i === regionIdx ? { ...r, playInSeeds: r.playInSeeds.map((s, j) => (j === piIdx ? seed : s)), } : r ) ); setSelectedParticipants({}); }; const isGroupStageEvent = tournamentGroups.length > 0; // Determine the phase for group-stage events const knockoutPopulated = useMemo(() => { if (!isGroupStageEvent) return false; // Check if any Round of 32 match has participants assigned const r32Matches = matches.filter((m) => m.round === "Round of 32"); return r32Matches.some((m) => m.participant1Id || m.participant2Id); }, [isGroupStageEvent, matches]); // Group stage stats const groupStageStats = useMemo(() => { if (!isGroupStageEvent) return null; let eliminated = 0; let total = 0; for (const group of tournamentGroups) { for (const member of group.members || []) { total++; if (member.eliminated) eliminated++; } } return { eliminated, advancing: total - eliminated, total }; }, [isGroupStageEvent, tournamentGroups]); // Advancing participants (non-eliminated) for knockout assignment const advancingParticipants = useMemo(() => { if (!isGroupStageEvent) return []; const advancing: Array<{ id: string; name: string }> = []; for (const group of tournamentGroups) { for (const member of group.members || []) { if (!member.eliminated && member.participant) { advancing.push({ id: member.participant.id, name: member.participant.name, }); } } } return advancing; }, [isGroupStageEvent, tournamentGroups]); // Available advancing participants for knockout assignment (exclude already assigned) const availableAdvancingMap = useMemo(() => { const assignedIds = new Set(Object.values(knockoutAssignments).filter(Boolean)); const map: Record = {}; // Create 32 slots (16 matches × 2 slots) for (let m = 1; m <= 16; m++) { for (const slot of ["participant1Id", "participant2Id"]) { const key = `match-${m}-${slot}`; const currentSelection = knockoutAssignments[key]; map[key] = advancingParticipants.filter( (p) => !assignedIds.has(p.id) || p.id === currentSelection ); } } return map; }, [knockoutAssignments, advancingParticipants]); // Clear selected winners after successful batch submission useEffect(() => { if (actionData?.success) { setSelectedWinners({}); } }, [actionData?.success]); // Reset selected participants and region config when template changes const handleTemplateChange = (newTemplateId: string) => { setSelectedTemplate(newTemplateId); setSelectedParticipants({}); const newTemplate = templates.find((t) => t.id === newTemplateId); setRegionConfig(buildDefaultRegionConfig(newTemplate)); }; // Update selected participant for a seed const handleParticipantChange = (seedIndex: number, participantId: string) => { setSelectedParticipants(prev => ({ ...prev, [seedIndex]: participantId })); }; // Memoized available participants calculation const availableParticipantsMap = useMemo(() => { const selectedIdsSet = new Set(Object.values(selectedParticipants)); const map: Record = {}; const totalSeeds = template?.totalTeams || 8; for (let i = 0; i < totalSeeds; i++) { const currentSelection = selectedParticipants[i]; map[i] = participants.filter((p: { id: string; name: string }) => { return !selectedIdsSet.has(p.id) || p.id === currentSelection; }); } return map; }, [selectedParticipants, participants, template?.totalTeams]); const getAvailableParticipants = (seedIndex: number) => { return availableParticipantsMap[seedIndex] || participants; }; // Update selected winner for a match const handleWinnerChange = (matchId: string, winnerId: string) => { setSelectedWinners(prev => ({ ...prev, [matchId]: winnerId })); }; // Get matches with pending winner selections in a round const getPendingWinnersInRound = (round: string) => { return Object.entries(selectedWinners) .filter(([matchId]) => { const match = matches.find((m) => m.id === matchId); return match && match.round === round && !match.isComplete; }); }; // Memoized: Group matches by round const matchesByRound = useMemo(() => { const grouped: Record = {}; for (const match of matches) { if (!grouped[match.round]) { grouped[match.round] = []; } grouped[match.round].push(match); } return grouped; }, [matches]); // Memoized: Get available rounds in chronological order const availableRounds = useMemo(() => { const bracketTemplate = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : undefined; return getOrderedRoundsFromMatches(matches, bracketTemplate); }, [matches, event.bracketTemplateId]); // Check if all matches are complete const allMatchesComplete = useMemo(() => { return matches.length > 0 && matches.every((m) => m.isComplete); }, [matches]); // No bracket and no groups yet = setup phase const showSetup = matches.length === 0 && !isGroupStageEvent; return (

    Playoff Bracket

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

    {actionData?.error && (
    {actionData.error}
    )} {actionData?.success && (
    {actionData.success}
    )} {/* Possible-duplicate review after a draw sync */} {actionData?.drawSyncResult && actionData.drawSyncResult.unmatched.length > 0 && ( Review {actionData.drawSyncResult.unmatched.length} possible duplicate {actionData.drawSyncResult.unmatched.length === 1 ? "" : "s"} These players were auto-created but closely resemble an existing participant — if a duplicate, results won't reach the drafted copy. To fix: on the{" "} participants page , set the correct participant's external ID to the Wikipedia name, delete the duplicate, then click Sync Draw again.
      {actionData.drawSyncResult.unmatched.map((p) => (
    • {p.name} {p.externalId && p.externalId !== p.name && ( ({p.externalId}) )}
    • ))}
    )} {/* Dry-run preview of a draw sync */} {actionData?.drawPreview && ( Draw preview (no changes made) {actionData.drawPreview.article}
    Players: {actionData.drawPreview.totalPlayers}
    Matched:{" "} {actionData.drawPreview.matched}
    Will create:{" "} {actionData.drawPreview.willCreate.length}
    Matches:{" "} {actionData.drawPreview.completedMatches}/{actionData.drawPreview.totalMatches}{" "} done
    Unfilled R1 slots:{" "} {actionData.drawPreview.tbdFirstRound}
    {actionData.drawPreview.possibleDuplicates.length > 0 && (

    Possible duplicates ({actionData.drawPreview.possibleDuplicates.length}) — would be created but resemble an existing participant:

      {actionData.drawPreview.possibleDuplicates.map((p) => ( ))}

    Rename if it's the same player (links the existing participant so it matches);{" "} Create as new if they're different people. Then re-run Preview or Sync Draw.

    )} {actionData.drawPreview.willCreate.length > 0 && (
    Show all {actionData.drawPreview.willCreate.length} players that would be created
      {actionData.drawPreview.willCreate.map((p) => (
    • {p.name}
    • ))}
    )}

    Fix any duplicates on the{" "} participants page {" "} first, then click Sync Draw.

    )} {/* Sync Draw from Wikipedia — tennis Grand Slam auto-populate + auto-score */} {event.isQualifyingEvent && sportsSeason.sport.simulatorType === "tennis_qualifying_points" && ( Sync Draw from Wikipedia Auto-populate and score this Grand Slam bracket from its Wikipedia draw article. Re-run during the tournament to pull in completed matches.

    Paste the article URL or type the title — both work. Use{" "} Preview first to see matches and any new/duplicate players before writing anything.

    )} {/* Reprocess Bracket - Full rebuild of participant results. Hidden once every match is complete — at that point all placements are final and "finalize bracket" should be used instead. */} {matches.length > 0 && !matches.every((m) => m.isComplete) && ( Reprocess Bracket Replay all completed matches from scratch and re-mark non-bracket participants as eliminated. Use this to fix scoring data after rule changes or when results look incorrect.
    )} {/* Clear Bracket - the only escape hatch for a mis-seeded bracket. Nothing else can rewrite a match's participants, so a wrong seeding has to be torn down and rebuilt via the setup form below, which reappears once this runs. */} {matches.length > 0 && ( Clear Bracket Delete every match in this bracket so it can be set up again from scratch. Use this when the wrong participants were seeded. This also clears the season's placements and the points derived from them.
    { if ( !confirm( `Delete all ${matches.length} match(es) in this bracket? Recorded results and placements will be lost.` ) ) { e.preventDefault(); } }} >
    )} {/* ====== SETUP PHASE ====== */} {showSetup && ( Generate Bracket Create the bracket structure for this playoff event
    {/* Intent depends on whether this is a group-stage template */}
    {template && (

    {template.groupStage ? ( <> Groups: {template.groupStage.groupCount} groups of {template.groupStage.teamsPerGroup} {" | "}Knockout: {template.rounds.map(r => r.name).join(" \u2192 ")} ) : ( <>Rounds: {template.rounds.map(r => r.name).join(" \u2192 ")} )}

    )}
    {/* Group-stage template: show group assignments */} {hasGroupStage && template?.groupStage && (

    Assign {template.totalTeams} participants across {template.groupStage.groupCount} groups ({template.groupStage.teamsPerGroup} per group).

    {template.groupStage.groupLabels.map((label, groupIndex) => ( Group {label} {[...Array(template.groupStage?.teamsPerGroup ?? 0)].map((_, teamIndex) => { const seedIndex = groupIndex * (template.groupStage?.teamsPerGroup ?? 0) + teamIndex; const availableParticipants = getAvailableParticipants(seedIndex); return (
    handleParticipantChange(seedIndex, value)} placeholder={`Team ${teamIndex + 1}...`} />
    ); })}
    ))}
    )} {/* Non-group-stage template: region-grouped or flat seeded list */} {!hasGroupStage && hasRegions && regionSlotMap && (
    {/* ── Region configuration ─────────────────────────────── */}

    Configure Regions

    Set each region's name and which seed slots are play-in games.

    {regionConfig.map((region, r) => ( // eslint-disable-next-line react/no-array-index-key
    updateRegionName(r, e.target.value)} placeholder={`Region ${r + 1}`} /> {region.playInSeeds.map((seed, pi) => ( // eslint-disable-next-line react/no-array-index-key
    play-in seed:
    ))} {region.playInSeeds.length < 2 && ( )}
    ))} {/* Hidden fields so server receives the region config */} {regionConfig.map((region, r) => ( // eslint-disable-next-line react/no-array-index-key ))}
    {/* ── Seed entry per region ─────────────────────────────── */}

    Select participants by region. Use search to find teams quickly.

    {effectiveRegions.map((region, r) => { const directOffset = regionSlotMap.directOffsets[r]; return ( // eslint-disable-next-line react/no-array-index-key

    {region.name || `Region ${r + 1}`}

    {region.directSeeds.map((seedNum, pos) => { const globalIdx = directOffset + pos; const opponentSeed = 17 - seedNum; const opponentIsPlayIn = region.playIns.some((pi) => pi.seedSlot === opponentSeed); return (
    handleParticipantChange(globalIdx, value)} placeholder={`Seed ${seedNum}…`} />
    {opponentIsPlayIn && ( ← faces play-in )}
    ); })} {region.playIns.length > 0 && (

    {region.playIns.map((pi) => `Seed ${pi.seedSlot}`).join(" & ")} via play-in ↓

    )}
    ); })}
    {/* ── Play-in team entry ─────────────────────────────────── */} {regionSlotMap.playInOffsets.length > 0 && (

    Play-In Teams

    Enter both teams for each play-in game. The winner advances to the main bracket.

    {regionSlotMap.playInOffsets.map((pi, ffIdx) => { const regionName = effectiveRegions[pi.regionIndex]?.name || `Region ${pi.regionIndex + 1}`; const idx1 = pi.startIndex; const idx2 = pi.startIndex + 1; return (

    Play-In {ffIdx + 1} — {regionName} {pi.seedSlot}-seed

    {[idx1, idx2].map((globalIdx, slot) => (
    handleParticipantChange(globalIdx, value)} placeholder={`${regionName} ${pi.seedSlot}-seed play-in team ${slot + 1}…`} />
    ))}
    ); })}
    )}
    )} {/* Non-group-stage, non-region template: flat seeded list */} {!hasGroupStage && !hasRegions && (

    Select {template?.totalTeams || 8} participants for the bracket. Use search to quickly find participants.

    {[...Array(template?.totalTeams || 8)].map((_, i) => { const availableParticipants = getAvailableParticipants(i); const slotLabel = template?.participantLabels?.[i] ?? `Seed ${i + 1}`; return ( // eslint-disable-next-line react/no-array-index-key
    handleParticipantChange(i, value)} placeholder={`Select ${slotLabel.toLowerCase()}...`} />
    ); })}
    )}
    )} {/* ====== GROUP STAGE MANAGEMENT (Phase 2) ====== */} {isGroupStageEvent && !knockoutPopulated && ( <> {/* Group Stage Summary */} {groupStageStats && ( Group Stage Manage group stage eliminations. Mark teams as eliminated, then assign advancing teams to the knockout bracket.
    {groupStageStats.total} total teams {groupStageStats.eliminated} eliminated {groupStageStats.advancing} advancing
    )} {/* Group Cards */}
    {tournamentGroups.map((group) => { const members = group.members || []; const groupMatches = group.groupMatches || []; const standings = computeGroupStandings( members .filter((m) => m.participant !== null && m.participant !== undefined) .map((m) => ({ participantId: m.participant?.id ?? "", participantName: m.participant?.name ?? "", })), groupMatches.map((m) => ({ participant1Id: m.participant1Id, participant2Id: m.participant2Id, participant1Score: m.participant1Score, participant2Score: m.participant2Score, isComplete: m.isComplete, })) ); return ( Group {group.groupName} {/* Standings table */} {standings.length > 0 && (
    {standings.map((row, idx) => { const member = members.find((m) => m.participant?.id === row.participantId); const isTop2 = idx < 2; return ( ); })}
    Team P W D L GD Pts
    {row.participantName} {row.played} {row.wins} {row.draws} {row.losses} {row.gd > 0 ? `+${row.gd}` : row.gd} {row.points}
    )} {/* Elimination toggles */}
    {members.map((member) => (
    {member.participant?.name || "Unknown"}
    ))}
    {/* Match score entry */} {groupMatches.length > 0 && (

    Matches

    {groupMatches.map((match) => (
    {/* Schedule row */} {/* Score row */}
    {match.participant1?.name ?? "?"} {match.participant2?.name ?? "?"} {!match.isComplete && ( )} {match.isComplete && ( FT )}
    ))}
    )}
    ); })}
    {/* Knockout Assignment Section */} {groupStageStats && groupStageStats.advancing === 32 && ( Assign Knockout Bracket 32 teams are advancing. Assign them to the Round of 32 knockout bracket slots.
    {[...Array(16)].map((_, matchIndex) => { const matchNum = matchIndex + 1; const key1 = `match-${matchNum}-participant1Id`; const key2 = `match-${matchNum}-participant2Id`; return (
    Match {matchNum}
    setKnockoutAssignments((prev) => ({ ...prev, [key1]: value })) } placeholder="Team 1..." />
    vs
    setKnockoutAssignments((prev) => ({ ...prev, [key2]: value })) } placeholder="Team 2..." />
    ); })}
    )} {groupStageStats && groupStageStats.advancing !== 32 && groupStageStats.eliminated > 0 && (

    {groupStageStats.advancing} teams advancing (need exactly 32 to populate knockout bracket). {groupStageStats.advancing > 32 && ` Eliminate ${groupStageStats.advancing - 32} more.`} {groupStageStats.advancing < 32 && ` Reinstate ${32 - groupStageStats.advancing} teams or adjust eliminations.`}

    )} )} {/* ====== KNOCKOUT BRACKET DISPLAY (Phase 3) ====== */} {/* Show bracket rounds when: non-group event with matches, OR group event with knockout populated */} {((matches.length > 0 && !isGroupStageEvent) || knockoutPopulated) && ( <> {availableRounds.map((round: string) => ( {round} {matchesByRound[round].length} {matchesByRound[round].length === 1 ? "match" : "matches"} Match Participant 1 Score vs Score Participant 2 Winner Actions {matchesByRound[round].map((match) => ( {match.matchNumber} {match.participant1?.name || "TBD"} {match.participant1Score ? parseFloat(match.participant1Score) : "-"} vs {match.participant2Score ? parseFloat(match.participant2Score) : "-"} {match.participant2?.name || "TBD"} {match.winner?.name ? (
    {match.winner.name}
    ) : ( "-" )}
    {!match.isComplete && match.participant1Id && match.participant2Id ? ( ) : match.isComplete ? ( Complete ) : ( Waiting )}
    {/* Expanded: Games & Odds panel */} {expandedMatches.has(match.id) && (
    {/* Games section */}
    Game Schedule
    {/* Existing games */} {match.games?.length > 0 ? (
    {match.games.map((game) => (
    Game {game.gameNumber} {game.scheduledAt ? new Date(game.scheduledAt).toLocaleString() : "No date set"} {game.status} {game.status === "complete" && game.winner && ( W: {game.winner.name} )}
    ))}
    ) : (

    No games scheduled yet.

    )} {/* Add game form */}
    { const form = e.currentTarget; // Read from the datetime-local display input (not submitted directly) const scheduledAtLocal = form.elements.namedItem("scheduledAtLocal") as HTMLInputElement; // Write the UTC ISO string into the hidden input that IS submitted const scheduledAtHidden = form.elements.namedItem("scheduledAt") as HTMLInputElement; if (scheduledAtHidden) { scheduledAtHidden.value = localDateTimeToUtcIso(scheduledAtLocal?.value) ?? ""; } }}> {/* Hidden field holds the UTC ISO string written by onSubmit */}
    {/* Odds section */}
    Moneyline Odds
    {match.odds?.length > 0 ? (
    {match.odds.map((odd) => (
    {odd.participant?.name} 0 ? "text-emerald-500" : "text-muted-foreground"}`}> {odd.moneylineOdds > 0 ? "+" : ""}{odd.moneylineOdds} ({(parseFloat(odd.impliedProbability) * 100).toFixed(1)}%) {odd.oddsSource && ( {odd.oddsSource} )}
    ))}
    ) : (

    No odds set.

    )} {/* Set odds forms for each participant */} {[ { id: match.participant1Id, name: match.participant1?.name }, { id: match.participant2Id, name: match.participant2?.name }, ].filter(p => p.id).map(participant => (
    ))}
    )}
    ))}
    {/* Batch Submit Winners */} {getPendingWinnersInRound(round).length > 0 && (
    {getPendingWinnersInRound(round).map(([matchId, winnerId]) => ( ))}
    )}
    ))} {/* Finalize Bracket Button */} {allMatchesComplete && !event.isComplete && ( Finalize Bracket All matches are complete! Process all rounds and assign final placements.

    This will:

    • Process all playoff rounds in order
    • Assign fantasy placements (1st-8th) based on bracket results
    • Assign 0 points to participants not in the bracket
    • Mark the event as complete
    • Recalculate all team standings
    )} )} {/* Event Complete Badge */} {event.isComplete && (
    Event Complete {event.completedAt && new Date(event.completedAt).toLocaleDateString()}
    )}
    ); }