2025-09-20 21:28:10 +02:00

283 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /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<string, { steamId: string; teamId: string | null }>()
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 })
}
}