// /src/app/api/matches/[id]/route.ts import { NextResponse, type NextRequest } from 'next/server' import { prisma } from '@/app/lib/prisma' import { getServerSession } from 'next-auth' import { authOptions } from '@/app/lib/auth' import { isFuture, buildCommunityFuturePayload, buildDefaultPayload, } from './_builders' import { sendServerSSEMessage } from '@/app/lib/sse-server-client' /* ───────────────────────── GET ───────────────────────── */ export async function GET(_: Request, { params }: { params: { matchId: string } }) { const id = params.matchId if (!id) return NextResponse.json({ error: 'Missing ID' }, { status: 400 }) try { const m = await prisma.match.findUnique({ where: { id }, include: { teamA: { include: { leader: true } }, teamB: { include: { leader: true } }, players: { include: { user: true, stats: true, team: true } }, teamAUsers: { include: { team: true } }, teamBUsers: { include: { team: true } }, mapVote: { include: { steps: { orderBy: { order: 'asc' }, select: { order: true, action: true, map: true, teamId: true, chosenAt: true, chosenBy: true }, }, }, }, }, }) if (!m) return NextResponse.json({ error: 'Match nicht gefunden' }, { status: 404 }) const payload = m.matchType === 'community' && isFuture(m) ? await buildCommunityFuturePayload(m) : buildDefaultPayload(m); // ⬇️ Zusatz: opensAt (und leadMinutes) an die Antwort hängen const baseTs = (m.matchDate ?? m.demoDate)?.getTime?.() ?? null; const opensAt = m.mapVote?.opensAt ?? null; const leadMinutes = opensAt && baseTs != null ? Math.max(0, Math.round((baseTs - opensAt.getTime()) / 60000)) : null; return NextResponse.json({ ...payload, mapVote: { ...(payload as any).mapVote, opensAt, // <- wichtig leadMinutes, // optional, aber nett zu haben }, }, { headers: { 'Cache-Control': 'no-store' } }); } catch (err) { console.error(`GET /matches/${params.matchId} failed:`, err) return NextResponse.json({ error: 'Failed to load match' }, { status: 500 }) } } /* ───────────────────────── PUT ───────────────────────── */ export async function PUT(req: NextRequest, { params }: { params: { matchId: string } }) { const id = params.matchId const session = await getServerSession(authOptions(req)) const me = session?.user if (!me?.steamId) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) // Erwartet: players: Array<{ steamId: string; teamId: string | null }> const body = await req.json() const input = (body?.players ?? []) as Array<{ steamId?: string; teamId?: string | null }> const match = await prisma.match.findUnique({ where: { id }, include: { teamA: { include: { leader: true } }, teamB: { include: { leader: true } }, teamAUsers: true, teamBUsers: true, }, }) if (!match) return NextResponse.json({ error: 'Match not found' }, { status: 404 }) // Eingaben säubern + deduplizieren const bySteam = new Map() for (const p of input) { if (typeof p?.steamId === 'string' && p.steamId.trim()) { bySteam.set(p.steamId.trim(), { steamId: p.steamId.trim(), teamId: p.teamId ?? null }) } } const unique = Array.from(bySteam.values()) const isAdmin = !!me.isAdmin const isLeaderA = !!match.teamAId && match.teamA?.leader?.steamId === me.steamId const isLeaderB = !!match.teamBId && match.teamB?.leader?.steamId === me.steamId // Community-Roster-Mitgliedschaft const rosterA = new Set(match.teamAUsers.map(u => u.steamId)) const rosterB = new Set(match.teamBUsers.map(u => u.steamId)) const isInA = rosterA.has(me.steamId) const isInB = rosterB.has(me.steamId) // Welche Seite darf der User ändern? let editorSide: 'A' | 'B' | 'both' | null = null if (isAdmin) editorSide = 'both' else if (isLeaderA || isInA) editorSide = 'A' else if (isLeaderB || isInB) editorSide = 'B' else editorSide = null if (!editorSide) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) const hasTeamIds = Boolean(match.teamAId && match.teamBId) // Seite zuordnen (mit/ohne echte Team-IDs) const incomingA = unique.filter(p => hasTeamIds ? p.teamId === match.teamAId : rosterA.has(p.steamId)) const incomingB = unique.filter(p => hasTeamIds ? p.teamId === match.teamBId : rosterB.has(p.steamId)) // Validierung: nur eigene Seite prüfen if (!isAdmin) { if (editorSide === 'A') { const invalid = incomingA.some(p => hasTeamIds ? p.teamId !== match.teamAId : !rosterA.has(p.steamId)) if (invalid) return NextResponse.json({ error: 'Ungültige Spielerzuweisung (A)' }, { status: 403 }) } if (editorSide === 'B') { const invalid = incomingB.some(p => hasTeamIds ? p.teamId !== match.teamBId : !rosterB.has(p.steamId)) if (invalid) return NextResponse.json({ error: 'Ungültige Spielerzuweisung (B)' }, { status: 403 }) } } // SteamId-Listen für die Relation const aSteamIds = Array.from(new Set(incomingA.map(p => p.steamId))) const bSteamIds = Array.from(new Set(incomingB.map(p => p.steamId))) try { await prisma.$transaction(async (tx) => { // 1) teamAUsers / teamBUsers SETZEN (nur die Seite(n), die editiert werden) const data: any = {} if (editorSide === 'both' || editorSide === 'A') { data.teamAUsers = { set: [], // erst leeren connect: aSteamIds.map(steamId => ({ steamId })), } } if (editorSide === 'both' || editorSide === 'B') { data.teamBUsers = { set: [], connect: bSteamIds.map(steamId => ({ steamId })), } } if (Object.keys(data).length) { await tx.match.update({ where: { id }, data }) } // 2) (optional) matchPlayer für die geänderten Seiten synchron halten // – so bleiben Teilnehmer + spätere Stats konsistent. if (editorSide === 'both' || editorSide === 'A') { if (hasTeamIds) { await tx.matchPlayer.deleteMany({ where: { matchId: id, teamId: match.teamAId! } }) if (aSteamIds.length) { await tx.matchPlayer.createMany({ data: aSteamIds.map(steamId => ({ matchId: id, steamId, teamId: match.teamAId! })), skipDuplicates: true, }) } } else { // Community ohne teamId → anhand Roster löschen await tx.matchPlayer.deleteMany({ where: { matchId: id, steamId: { in: Array.from(rosterA) } } }) if (aSteamIds.length) { await tx.matchPlayer.createMany({ data: aSteamIds.map(steamId => ({ matchId: id, steamId, teamId: null })), skipDuplicates: true, }) } } } if (editorSide === 'both' || editorSide === 'B') { if (hasTeamIds) { await tx.matchPlayer.deleteMany({ where: { matchId: id, teamId: match.teamBId! } }) if (bSteamIds.length) { await tx.matchPlayer.createMany({ data: bSteamIds.map(steamId => ({ matchId: id, steamId, teamId: match.teamBId! })), skipDuplicates: true, }) } } else { await tx.matchPlayer.deleteMany({ where: { matchId: id, steamId: { in: Array.from(rosterB) } } }) if (bSteamIds.length) { await tx.matchPlayer.createMany({ data: bSteamIds.map(steamId => ({ matchId: id, steamId, teamId: null })), skipDuplicates: true, }) } } } }) // Antwort wie GET const updated = await prisma.match.findUnique({ where: { id }, include: { teamA: { include: { leader: true } }, teamB: { include: { leader: true } }, players: { include: { user: true, stats: true, team: true } }, teamAUsers: { include: { team: true } }, teamBUsers: { include: { team: true } }, mapVote: { include: { steps: { orderBy: { order: 'asc' }, select: { order: true, action: true, map: true, teamId: true, chosenAt: true, chosenBy: true }, }, }, }, }, }) if (!updated) return NextResponse.json({ error: 'Match konnte nach Update nicht geladen werden' }, { status: 500 }) await sendServerSSEMessage({ type: 'match-lineup-updated', payload: { matchId: updated.id, teamA: updated.teamAUsers.map(u => u.steamId), teamB: updated.teamBUsers.map(u => u.steamId), updatedAt: new Date().toISOString(), }, }) // Trennung für Response (gleich wie bisher) const setA = new Set(updated.teamAUsers.map(u => u.steamId)) const setB = new Set(updated.teamBUsers.map(u => u.steamId)) const playersA = updated.players.filter(p => setA.has(p.steamId)).map(p => ({ user: p.user, stats: p.stats, team: p.team?.name ?? 'Team A' })) const playersB = updated.players.filter(p => setB.has(p.steamId)).map(p => ({ user: p.user, stats: p.stats, team: p.team?.name ?? 'Team B' })) return NextResponse.json({ id: updated.id, title: updated.title, description: updated.description, demoDate: updated.demoDate, matchType: updated.matchType, roundCount: updated.roundCount, map: updated.map, teamA: { id: updated.teamAId ?? null, name: updated.teamAUsers[0]?.team?.name ?? updated.teamA?.name ?? 'Team A', logo: updated.teamAUsers[0]?.team?.logo ?? updated.teamA?.logo ?? null, score: updated.scoreA, players: playersA, }, teamB: { id: updated.teamBId ?? null, name: updated.teamBUsers[0]?.team?.name ?? updated.teamB?.name ?? 'Team B', logo: updated.teamBUsers[0]?.team?.logo ?? updated.teamB?.logo ?? null, score: updated.scoreB, players: playersB, }, }, { headers: { 'Cache-Control': 'no-store' } }) } catch (err) { console.error(`PUT /matches/${id} failed:`, err) return NextResponse.json({ error: 'Failed to update match' }, { status: 500 }) } } /* ───────────────────────── DELETE ───────────────────────── */ export async function DELETE(req: NextRequest, { params }: { params: { matchId: string } }) { const id = params.matchId const session = await getServerSession(authOptions(req)) if (!session?.user?.isAdmin) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) try { await prisma.matchPlayer.deleteMany({ where: { matchId: id } }) await prisma.match.delete({ where: { id } }) return NextResponse.json({ success: true }) } catch (err) { console.error(`DELETE /matches/${id} failed:`, err) return NextResponse.json({ error: 'Failed to delete match' }, { status: 500 }) } }