/* ──────────────────────────────────────────────────────────────── /app/components/MatchDetails.tsx - Zeigt pro Team einen eigenen „Spieler bearbeiten“-Button - Öffnet das Modal nur für das angeklickte Team - Reagiert auf SSE-Events (match-lineup-updated / matches-updated) ─────────────────────────────────────────────────────────────────*/ 'use client' import { useState, useEffect, useMemo } from 'react' import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' import { format } from 'date-fns' import { de } from 'date-fns/locale' import Table from './Table' import PremierRankBadge from './PremierRankBadge' import CompRankBadge from './CompRankBadge' import EditMatchMetaModal from './EditMatchMetaModal' import EditMatchPlayersModal from './EditMatchPlayersModal' import type { EditSide } from './EditMatchPlayersModal' import type { Match, MatchPlayer } from '../types/match' import Button from './Button' import { MAP_OPTIONS } from '../lib/mapOptions' import MapVetoBanner from './MapVetoBanner' import { useSSEStore } from '@/app/lib/useSSEStore' import { Team } from '../types/team' import Alert from './Alert' import Image from 'next/image' import { MATCH_EVENTS } from '../lib/sseEvents' type TeamWithPlayers = Team & { players?: MatchPlayer[] } /* ─────────────────── Hilfsfunktionen ────────────────────────── */ const kdr = (k?: number, d?: number) => typeof k === 'number' && typeof d === 'number' ? d === 0 ? '∞' : (k / d).toFixed(2) : '-' const adr = (dmg?: number, rounds?: number) => typeof dmg === 'number' && typeof rounds === 'number' && rounds > 0 ? (dmg / rounds).toFixed(1) : '-' const normalizeMapKey = (raw?: string) => (raw ?? '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '') /* ─────────────────── Komponente ─────────────────────────────── */ export function MatchDetails({ match, initialNow }: { match: Match; initialNow: number }) { const { data: session } = useSession() const { lastEvent } = useSSEStore() const router = useRouter() const isAdmin = !!session?.user?.isAdmin const [now, setNow] = useState(initialNow) const [editMetaOpen, setEditMetaOpen] = useState(false) /* ─── Rollen & Rechte ─────────────────────────────────────── */ const me = session?.user const userId = me?.steamId const isLeaderA = !!userId && userId === match.teamA?.leader?.steamId const isLeaderB = !!userId && userId === match.teamB?.leader?.steamId const canEditA = isAdmin || isLeaderA const canEditB = isAdmin || isLeaderB const teamAPlayers = (match.teamA as TeamWithPlayers).players ?? [] const teamBPlayers = (match.teamB as TeamWithPlayers).players ?? [] /* ─── Map ─────────────────────────────────────────────────── */ const mapKey = normalizeMapKey(match.map) const mapLabel = MAP_OPTIONS.find(opt => opt.key === mapKey)?.label ?? MAP_OPTIONS.find(opt => opt.key === 'lobby_mapveto')?.label ?? 'Unbekannte Map' /* ─── Match-Zeitpunkt ─────────────────────────────────────── */ const dateString = match.matchDate ?? match.demoDate const readableDate = dateString ? format(new Date(dateString), 'PPpp', { locale: de }) : 'Unbekannt' /* ─── Modal-State: null = geschlossen, 'A' / 'B' = offen ─── */ const [editSide, setEditSide] = useState(null) /* ─── Live-Uhr (für Veto-Zeitpunkt) ───────────────────────── */ useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000) return () => clearInterval(id) }, []) const vetoOpensAtTs = useMemo(() => { const base = match.mapVeto?.opensAt ? new Date(match.mapVeto.opensAt).getTime() : new Date(match.matchDate ?? match.demoDate ?? initialNow).getTime() - 60 * 60 * 1000 return base }, [match.mapVeto?.opensAt, match.matchDate, match.demoDate, initialNow]) const endDate = new Date(vetoOpensAtTs) const mapVetoStarted = (match.mapVeto?.isOpen ?? false) || now >= vetoOpensAtTs const showEditA = canEditA && !mapVetoStarted const showEditB = canEditB && !mapVetoStarted /* ─── SSE-Listener ─────────────────────────────────────────── */ useEffect(() => { if (!lastEvent) return // Match gelöscht? → zurück zur Liste if (lastEvent.type === 'match-deleted' && lastEvent.payload?.matchId === match.id) { router.replace('/schedule') return } // Alle Match-Events → Seite frisch rendern if (MATCH_EVENTS.has(lastEvent.type) && lastEvent.payload?.matchId === match.id) { router.refresh() } }, [lastEvent, match.id, router]) /* ─── Tabellen-Layout (fixe Breiten) ───────────────────────── */ const ColGroup = () => ( {Array.from({ length: 13 }).map((_, i) => ( ))} ) /* ─── Match löschen ────────────────────────────────────────── */ const handleDelete = async () => { if (!confirm('Match wirklich löschen? Das kann nicht rückgängig gemacht werden.')) return try { const res = await fetch(`/api/matches/${match.id}/delete`, { method: 'POST' }) if (!res.ok) { const j = await res.json().catch(() => ({})) alert(j.message ?? 'Löschen fehlgeschlagen') return } router.push('/schedule') } catch (e) { console.error('[MatchDetails] delete failed', e) alert('Löschen fehlgeschlagen.') } } /* ─── Spieler-Tabelle (pure; keine Hooks hier drin!) ──────── */ const renderTable = (players: MatchPlayer[]) => { const sorted = [...players].sort( (a, b) => (b.stats?.totalDamage ?? 0) - (a.stats?.totalDamage ?? 0), ) return ( {[ 'Spieler', 'Rank', 'Aim', 'K', 'A', 'D', '1K', '2K', '3K', '4K', '5K', 'K/D', 'ADR', 'HS%', 'Damage', ].map((h) => ( {h} ))} {sorted.map((p) => ( router.push(`/profile/${p.user.steamId}`)} > {p.user.name}
{p.user.name ?? 'Unbekannt'}
{match.matchType === 'premier' ? ( ) : ( )} {match.matchType === 'premier' && typeof p.stats?.rankChange === 'number' && ( 0 ? 'text-green-500' : p.stats.rankChange < 0 ? 'text-red-500' : '' }`} > {p.stats.rankChange > 0 ? '+' : ''} {p.stats.rankChange} )}
{Number.isFinite(Number(p.stats?.aim)) ? `${Number(p.stats?.aim).toFixed(0)} %` : '-'} {p.stats?.kills ?? '-'} {p.stats?.assists ?? '-'} {p.stats?.deaths ?? '-'} {p.stats?.oneK ?? '-'} {p.stats?.twoK ?? '-'} {p.stats?.threeK ?? '-'} {p.stats?.fourK ?? '-'} {p.stats?.fiveK ?? '-'} {kdr(p.stats?.kills, p.stats?.deaths)} {adr(p.stats?.totalDamage, match.roundCount)} {((p.stats?.headshotPct ?? 0) * 100).toFixed(0)}% {p.stats?.totalDamage?.toFixed(0) ?? '-'}
))}
) } /* ─── Render ─────────────────────────────────────────────── */ return (

Match auf {mapLabel} ({match.matchType})

{isAdmin && (
)}

Datum: {readableDate}

Teams: {match.teamA?.name ?? 'Unbekannt'} vs. {match.teamB?.name ?? 'Unbekannt'}
Score: {match.scoreA ?? 0}:{match.scoreB ?? 0}
{/* ───────── Team-Blöcke ───────── */}
{/* Team A */}

{match.teamA?.name ?? 'Team A'}

{showEditA && ( Du kannst die Aufstellung noch bis {format(endDate, 'dd.MM.yyyy HH:mm')} bearbeiten. )}
{renderTable(teamAPlayers)}
{/* Team B */}

{match.teamB?.logo && ( Teamlogo )} {match.teamB?.name ?? 'Team B'}

{showEditB && ( Du kannst die Aufstellung noch bis {format(endDate, 'dd.MM.yyyy HH:mm')} bearbeiten. )}
{renderTable(teamBPlayers)}
{/* ───────── Modal ───────── */} {editSide && ( setEditSide(null)} matchId={match.id} teamA={match.teamA} teamB={match.teamB} side={editSide} initialA={teamAPlayers.map((mp) => mp.user.steamId)} initialB={teamBPlayers.map((mp) => mp.user.steamId)} onSaved={() => router.refresh()} // sanfter als window.location.reload() /> )} {editMetaOpen && ( setEditMetaOpen(false)} matchId={match.id} defaultTitle={match.title} defaultTeamAId={match.teamA?.id ?? null} defaultTeamBId={match.teamB?.id ?? null} defaultTeamAName={match.teamA?.name ?? null} defaultTeamBName={match.teamB?.name ?? null} defaultDateISO={match.matchDate ?? match.demoDate ?? null} defaultMap={match.map ?? null} defaultVetoLeadMinutes={60} onSaved={() => { router.refresh() }} /> )}
) }