"use client"; import { useState, useMemo, useCallback, useEffect } from "react"; import { Match, Team } from "@/lib/types"; import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings"; import { qualifiedThirdGroups, resolveAnnexC, LATER_ROUNDS } from "@/lib/bracket"; import { resolveBracket, ResolvedTie } from "@/lib/resolve-bracket"; import { loadOverrides, saveOverrides, buildSimMatches, defaultScore, SimOverrides, } from "@/lib/simulation"; import Bracket from "./Bracket"; import ThirdPlace from "./ThirdPlace"; import Flag from "./Flag"; function teamById(teams: Team[], id: string | null) { return id ? teams.find((t) => t.id === id) : undefined; } function fmtDate(iso: string): string { const d = new Date(iso); return d.toLocaleString("de-DE", { weekday: "short", day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit", }); } // Label für die Spielphase in der Liste function phaseLabel(match: Match): string { if (match.stage === "GROUP" && match.group) return `Gruppe ${match.group}`; if (match.stage === "R32") return "Sechzehntelfinale"; if (match.stage === "R16") return "Achtelfinale"; if (match.stage === "QF") return "Viertelfinale"; if (match.stage === "SF") return "Halbfinale"; if (match.stage === "3RD") return "Spiel um Platz 3"; if (match.stage === "FINAL") return "Finale"; return match.stage; } export default function Simulation({ teams, matches }: { teams: Team[]; matches: Match[] }) { const [overrides, setOverrides] = useState({}); useEffect(() => { setOverrides(loadOverrides()); }, []); const handleScore = useCallback((matchId: string, homeScore: number, awayScore: number) => { setOverrides((prev) => { const next = { ...prev, [matchId]: { homeScore, awayScore } }; saveOverrides(next); return next; }); }, []); const handleReset = useCallback(() => { setOverrides({}); saveOverrides({}); }, []); const handleClear = useCallback((matchId: string) => { setOverrides((prev) => { const next = { ...prev }; delete next[matchId]; saveOverrides(next); return next; }); }, []); // ---- Simulations-Pipeline ---- const simMatches = useMemo(() => buildSimMatches(matches, overrides), [matches, overrides]); const simTables = useMemo(() => computeGroupTables(teams, simMatches), [teams, simMatches]); const simThirds = useMemo(() => computeThirdPlaceTable(simTables), [simTables]); const simQGroups = useMemo(() => qualifiedThirdGroups(simThirds), [simThirds]); const simAnnex = useMemo( () => (simQGroups.length === 8 ? resolveAnnexC(simQGroups) : null), [simQGroups], ); const simAnnexResolved = simAnnex != null; const simBracket = useMemo( () => resolveBracket(simMatches, teams, simTables, simThirds, simAnnex, simAnnexResolved, true), [simMatches, teams, simTables, simThirds, simAnnex, simAnnexResolved], ); // Echte Tabellen/Bracket für fix/provisional-Flags (nur tatsächlich FINISHED). const realTables = useMemo(() => computeGroupTables(teams, matches), [teams, matches]); const realThirds = useMemo(() => computeThirdPlaceTable(realTables), [realTables]); const realQGroups = useMemo(() => qualifiedThirdGroups(realThirds), [realThirds]); const realAnnex = useMemo( () => (realQGroups.length === 8 ? resolveAnnexC(realQGroups) : null), [realQGroups], ); const realBracket = useMemo( () => resolveBracket(matches, teams, realTables, realThirds, realAnnex, realAnnex != null), [matches, teams, realTables, realThirds, realAnnex], ); // R32: provisional-Flags aus dem echten Bracket übernehmen. const realProvisional = useMemo(() => { const m = new Map(); for (const tie of realBracket.r32) { m.set(tie.matchNumber, { home: tie.home.provisional, away: tie.away.provisional }); } return m; }, [realBracket]); // Folgerunden: decided-Flags aus echten Matches (status === FINISHED). const realDecided = useMemo(() => { const m = new Map(); for (const mt of matches) { if (mt.group == null && mt.matchNumber >= 73) { m.set(mt.matchNumber, mt.status === "FINISHED"); } } return m; }, [matches]); // Sim-Bracket mit echten provisional-Flags mergen (inkl. Tooltip-Korrektur). const preResolved = useMemo(() => { const fixTooltip = (t: string | null, prov: boolean): string | null => { if (!t) return t; if (prov) return t.startsWith("aktuell ") ? t : `aktuell ${t}`; return t.startsWith("aktuell ") ? t.slice(8) : t; }; const r32Fixed: ResolvedTie[] = simBracket.r32.map((tie) => { const rp = realProvisional.get(tie.matchNumber); if (!rp) return tie; return { ...tie, home: { ...tie.home, provisional: rp.home, tooltip: fixTooltip(tie.home.tooltip, rp.home) }, away: { ...tie.away, provisional: rp.away, tooltip: fixTooltip(tie.away.tooltip, rp.away) }, }; }); const laterFixed: Record = {}; for (const km of LATER_ROUNDS) { const tie = simBracket.later[km.matchNumber]; if (!tie) continue; const homeProv = !(realDecided.get(km.fromHome) ?? false); const awayProv = !(realDecided.get(km.fromAway) ?? false); laterFixed[km.matchNumber] = { ...tie, home: { ...tie.home, provisional: homeProv }, away: { ...tie.away, provisional: awayProv }, }; } return { r32: r32Fixed, later: laterFixed }; }, [simBracket, realProvisional, realDecided]); // ---- Eingabeliste: offene Spiele aus dem echten Feed ---- const openMatches = useMemo( () => matches .filter((m) => m.status !== "FINISHED") .sort((a, b) => +new Date(a.utcDate) - +new Date(b.utcDate)), [matches], ); const groupPhase = openMatches.filter((m) => m.stage === "GROUP"); const koPhase = openMatches.filter((m) => m.stage !== "GROUP"); // KO-Eingaben erst freigeben, wenn alle Gruppenspiele beendet sind. const groupPhaseComplete = matches .filter((m) => m.group != null) .every((m) => m.status === "FINISHED"); const hasChanges = Object.keys(overrides).length > 0; return (
Simulation — Ergebnisse frei wählbar, beeinflusst nicht die echten Daten. Alle ungespielten Spiele sind vorbelegt mit dem Polymarket-Favoriten. Eingaben werden im Browser gespeichert.
{/* ---- Eingabeliste ---- */}

Offene Spiele ({openMatches.length})

{hasChanges && ( )}
{groupPhase.length > 0 && (
Gruppenphase
{groupPhase.map((m) => ( ))}
)} {koPhase.length > 0 && (
K.o.-Phase
{groupPhaseComplete ? (
{koPhase.map((m) => ( ))}
) : (
K.o.-Spiele werden zur Eingabe freigegeben, sobald die Gruppenphase abgeschlossen ist.
)}
)}
{/* ---- Drittplatzierte (simuliert) ---- */}

Drittplatzierte (simuliert)

{/* ---- Simulierter K.o.-Baum ---- */}

Simulierter K.o.-Baum

); } // ---- Einzelne Eingabezeile ---- function SimRow({ match, teams, overrides, onScore, onClear, }: { match: Match; teams: Team[]; overrides: SimOverrides; onScore: (id: string, h: number, a: number) => void; onClear: (id: string) => void; }) { const home = teamById(teams, match.homeTeamId); const away = teamById(teams, match.awayTeamId); const def = defaultScore(match); const ov = overrides[match.id]; const homeVal = ov?.homeScore ?? def?.homeScore; const awayVal = ov?.awayScore ?? def?.awayScore; const homeChanged = ov != null; const isKO = match.stage !== "GROUP"; const inputStyle = (changed: boolean): React.CSSProperties => ({ width: 36, height: 28, textAlign: "center", fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 700, color: changed ? "var(--turf)" : "var(--ink)", background: "var(--bg-raised)", border: `1px solid ${changed ? "var(--turf)" : "var(--line)"}`, borderRadius: "var(--radius-sm)", outline: "none", MozAppearance: "textfield", }); return (
{phaseLabel(match)} {home?.localisedName ?? home?.name ?? "—"} { const v = parseInt(e.target.value, 10); if (!isNaN(v) && v >= 0) { onScore(match.id, v, awayVal ?? (isKO ? 0 : 1)); } else if (e.target.value === "") { // Bei Leerung: Standard wiederherstellen const fallback = def; if (fallback) onScore(match.id, fallback.homeScore, fallback.awayScore); } }} /> : { const v = parseInt(e.target.value, 10); if (!isNaN(v) && v >= 0) { onScore(match.id, homeVal ?? (isKO ? 0 : 1), v); } else if (e.target.value === "") { const fallback = def; if (fallback) onScore(match.id, fallback.homeScore, fallback.awayScore); else onClear(match.id); } }} /> {away?.localisedName ?? away?.name ?? "—"} {fmtDate(match.utcDate)}
); }