From 61c75b1c8c58139fd65fa11e1b4a9c08c9e93d12 Mon Sep 17 00:00:00 2001 From: Linrador Date: Wed, 13 Aug 2025 23:44:44 +0200 Subject: [PATCH] update --- prisma/schema.prisma | 18 +- src/app/api/matches/[id]/map-vote/route.ts | 187 +++++---- src/app/api/matches/[id]/route.ts | 385 ++++++++---------- src/app/api/matches/create/route.ts | 55 ++- src/app/api/matches/route.ts | 4 +- src/app/api/schedule/route.ts | 83 ++-- src/app/api/team/[teamId]/route.ts | 135 +++--- src/app/components/CommunityMatchList.tsx | 18 +- src/app/components/EditMatchPlayersModal.tsx | 95 +++-- .../{MapVoteBanner.tsx => MapVetoBanner.tsx} | 4 +- .../{MapVotePanel.tsx => MapVetoPanel.tsx} | 6 +- ...ProfileCard.tsx => MapVetoProfileCard.tsx} | 2 +- src/app/components/MatchDetails.tsx | 26 +- src/app/lib/auth.ts | 6 +- .../match-details/[matchId]/map-vote/page.tsx | 6 +- src/app/types/{mapvote.ts => mapveto.ts} | 16 +- src/app/types/match.ts | 12 +- src/app/types/team.ts | 8 - src/generated/prisma/edge.js | 6 +- src/generated/prisma/index.js | 6 +- src/generated/prisma/package.json | 2 +- src/generated/prisma/schema.prisma | 18 +- 22 files changed, 599 insertions(+), 499 deletions(-) rename src/app/components/{MapVoteBanner.tsx => MapVetoBanner.tsx} (98%) rename src/app/components/{MapVotePanel.tsx => MapVetoPanel.tsx} (99%) rename src/app/components/{MapVoteProfileCard.tsx => MapVetoProfileCard.tsx} (98%) rename src/app/types/{mapvote.ts => mapveto.ts} (63%) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e0ec210..65d5604 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,7 +44,7 @@ model User { createdSchedules Schedule[] @relation("CreatedSchedules") confirmedSchedules Schedule[] @relation("ConfirmedSchedules") - mapVetoChoices MapVoteStep[] @relation("VetoStepChooser") + mapVetoChoices MapVetoStep[] @relation("VetoStepChooser") } model Team { @@ -68,7 +68,7 @@ model Team { schedulesAsTeamA Schedule[] @relation("ScheduleTeamA") schedulesAsTeamB Schedule[] @relation("ScheduleTeamB") - mapVetoSteps MapVoteStep[] @relation("VetoStepTeam") + mapVetoSteps MapVetoStep[] @relation("VetoStepTeam") } model TeamInvite { @@ -138,7 +138,7 @@ model Match { bestOf Int @default(3) // 1 | 3 | 5 โ€“ app-seitig validieren matchDate DateTime? // geplante Startzeit (separat von demoDate) - mapVote MapVote? // 1:1 Map-Vote-Status + mapVeto MapVeto? // 1:1 Map-Vote-Status createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -297,13 +297,13 @@ model ServerRequest { // ๐Ÿ—บ๏ธ Map-Vote // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -enum MapVoteAction { +enum MapVetoAction { BAN PICK DECIDER } -model MapVote { +model MapVeto { id String @id @default(uuid()) matchId String @unique match Match @relation(fields: [matchId], references: [id]) @@ -317,17 +317,17 @@ model MapVote { // Optional: serverseitig speichern, statt im UI zu berechnen opensAt DateTime? - steps MapVoteStep[] + steps MapVetoStep[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } -model MapVoteStep { +model MapVetoStep { id String @id @default(uuid()) vetoId String order Int - action MapVoteAction + action MapVetoAction // Team, das am Zug ist (kann bei DECIDER null sein) teamId String? @@ -339,7 +339,7 @@ model MapVoteStep { chosenBy String? chooser User? @relation("VetoStepChooser", fields: [chosenBy], references: [steamId]) - veto MapVote @relation(fields: [vetoId], references: [id]) + veto MapVeto @relation(fields: [vetoId], references: [id]) @@unique([vetoId, order]) @@index([teamId]) diff --git a/src/app/api/matches/[id]/map-vote/route.ts b/src/app/api/matches/[id]/map-vote/route.ts index 1e03805..a0f1991 100644 --- a/src/app/api/matches/[id]/map-vote/route.ts +++ b/src/app/api/matches/[id]/map-vote/route.ts @@ -74,7 +74,7 @@ function shapeState(veto: any) { } } -// Leader -> Player-Shape fรผr das Frontend mappen +// Leader -> Player-Shape fรผrs Frontend function shapeLeader(leader: any | null) { if (!leader) return null return { @@ -87,13 +87,82 @@ function shapeLeader(leader: any | null) { } } +// Player -> Player-Shape (falls wir aus Team-API รผbernehmen) +function shapePlayer(p: any) { + if (!p) return null + return { + steamId : p.steamId, + name : p.name ?? '', + avatar : p.avatar ?? '', + location : p.location ?? undefined, + premierRank: p.premierRank ?? undefined, + isAdmin : p.isAdmin ?? undefined, + } +} + +// Base-URL aus Request ableiten (lokal/proxy-fรคhig) +function getBaseUrl(req: NextRequest | NextResponse) { + // NextRequest hat headers; bei internen Aufrufen ggf. NextResponse, hier aber nur Request relevant + const proto = (req.headers.get('x-forwarded-proto') || 'http').split(',')[0].trim() + const host = (req.headers.get('x-forwarded-host') || req.headers.get('host') || '').split(',')[0].trim() + return `${proto}://${host}` +} + +async function fetchTeamApi(teamId: string | null | undefined, req: NextRequest) { + if (!teamId) return null + const base = getBaseUrl(req) + const url = `${base}/api/team/${teamId}` + + try { + const r = await fetch(url, { + // interne Server-Fetches dรผrfen nicht gecacht werden + cache: 'no-store', + headers: { + // Forward auth/proxy headers, falls nรถtig (nicht zwingend) + 'x-forwarded-proto': req.headers.get('x-forwarded-proto') || '', + 'x-forwarded-host' : req.headers.get('x-forwarded-host') || '', + } + }) + if (!r.ok) return null + const json = await r.json() + return json as { + id: string + name?: string | null + logo?: string | null + leader?: string | null // LeaderId + activePlayers: any[] + inactivePlayers: any[] + invitedPlayers: any[] + } + } catch { + return null + } +} + +// Leader bevorzugt aus Match-Relation; Fallback รผber Team-API (LeaderId -> Player aus Listen) +function resolveLeaderPlayer(matchTeam: any | null | undefined, teamApi: any | null) { + const leaderFromMatch = shapeLeader(matchTeam?.leader ?? null) + if (leaderFromMatch) return leaderFromMatch + + const leaderId: string | null = teamApi?.leader ?? null + if (!leaderId) return null + + const pool: any[] = [ + ...(teamApi?.activePlayers ?? []), + ...(teamApi?.inactivePlayers ?? []), + ...(teamApi?.invitedPlayers ?? []), + ] + const found = pool.find(p => p?.steamId === leaderId) + return shapePlayer(found) ?? { steamId: leaderId, name: '', avatar: '' } +} + async function ensureVeto(matchId: string) { const match = await prisma.match.findUnique({ where: { id: matchId }, include: { teamA : { include: { - // WICHTIG: Leader-Relation als Objekt laden + // Leader-Relation als Objekt laden leader: { select: { steamId: true, @@ -161,9 +230,37 @@ function computeAvailableMaps(mapPool: string[], steps: Array<{ map: string | nu return mapPool.filter(m => !used.has(m)) } +// Teams-Payload (mit Spielern) zusammenbauen +async function buildTeamsPayload(match: any, req: NextRequest) { + const [teamAApi, teamBApi] = await Promise.all([ + fetchTeamApi(match.teamA?.id, req), + fetchTeamApi(match.teamB?.id, req), + ]) + + const teamAPlayers = (teamAApi?.activePlayers ?? []).map(shapePlayer).filter(Boolean) + const teamBPlayers = (teamBApi?.activePlayers ?? []).map(shapePlayer).filter(Boolean) + + return { + teamA: { + id : match.teamA?.id ?? null, + name : match.teamA?.name ?? null, + logo : match.teamA?.logo ?? null, + leader: resolveLeaderPlayer(match.teamA, teamAApi), + players: teamAPlayers, + }, + teamB: { + id : match.teamB?.id ?? null, + name : match.teamB?.name ?? null, + logo : match.teamB?.logo ?? null, + leader: resolveLeaderPlayer(match.teamB, teamBApi), + players: teamBPlayers, + }, + } +} + /* -------------------- GET -------------------- */ -export async function GET(_req: NextRequest, { params }: { params: { id: string } }) { +export async function GET(req: NextRequest, { params }: { params: { id: string } }) { try { const matchId = params.id if (!matchId) return NextResponse.json({ message: 'Missing id' }, { status: 400 }) @@ -171,26 +268,12 @@ export async function GET(_req: NextRequest, { params }: { params: { id: string const { match, veto } = await ensureVeto(matchId) if (!match || !veto) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 }) - // Veto-State + Teams (mit Leader-Objekt) zurรผckgeben - const payload = { - ...shapeState(veto), - teams: { - teamA: { - id : match.teamA?.id ?? null, - name : match.teamA?.name ?? null, - logo : match.teamA?.logo ?? null, - leader: shapeLeader(match.teamA?.leader ?? null), - }, - teamB: { - id : match.teamB?.id ?? null, - name : match.teamB?.name ?? null, - logo : match.teamB?.logo ?? null, - leader: shapeLeader(match.teamB?.leader ?? null), - }, - }, - } + const teams = await buildTeamsPayload(match, req) - return NextResponse.json(payload, { headers: { 'Cache-Control': 'no-store' } }) + return NextResponse.json( + { ...shapeState(veto), teams }, + { headers: { 'Cache-Control': 'no-store' } }, + ) } catch (e) { console.error('[map-vote][GET] error', e) return NextResponse.json({ message: 'Fehler beim Laden' }, { status: 500 }) @@ -238,23 +321,9 @@ export async function POST(req: NextRequest, { params }: { params: { id: string // ๐Ÿ”” Broadcast (flat) await sendServerSSEMessage({ type: 'map-vote-updated', matchId }) - return NextResponse.json({ - ...shapeState(updated), - teams: { - teamA: { - id : match.teamA?.id ?? null, - name : match.teamA?.name ?? null, - logo : match.teamA?.logo ?? null, - leader: shapeLeader(match.teamA?.leader ?? null), - }, - teamB: { - id : match.teamB?.id ?? null, - name : match.teamB?.name ?? null, - logo : match.teamB?.logo ?? null, - leader: shapeLeader(match.teamB?.leader ?? null), - }, - }, - }) + const teams = await buildTeamsPayload(match, req) + + return NextResponse.json({ ...shapeState(updated), teams }) } const available = computeAvailableMaps(veto.mapPool, stepsSorted) @@ -284,23 +353,9 @@ export async function POST(req: NextRequest, { params }: { params: { id: string // ๐Ÿ”” Broadcast (flat) await sendServerSSEMessage({ type: 'map-vote-updated', matchId }) - return NextResponse.json({ - ...shapeState(updated), - teams: { - teamA: { - id : match.teamA?.id ?? null, - name : match.teamA?.name ?? null, - logo : match.teamA?.logo ?? null, - leader: shapeLeader(match.teamA?.leader ?? null), - }, - teamB: { - id : match.teamB?.id ?? null, - name : match.teamB?.name ?? null, - logo : match.teamB?.logo ?? null, - leader: shapeLeader(match.teamB?.leader ?? null), - }, - }, - }) + const teams = await buildTeamsPayload(match, req) + + return NextResponse.json({ ...shapeState(updated), teams }) } // Rechte prรผfen (Admin oder Leader des Teams am Zug) โ€“ weiterhin via leaderId @@ -369,23 +424,9 @@ export async function POST(req: NextRequest, { params }: { params: { id: string // ๐Ÿ”” Broadcast (flat) await sendServerSSEMessage({ type: 'map-vote-updated', matchId }) - return NextResponse.json({ - ...shapeState(updated), - teams: { - teamA: { - id : match.teamA?.id ?? null, - name : match.teamA?.name ?? null, - logo : match.teamA?.logo ?? null, - leader: shapeLeader(match.teamA?.leader ?? null), - }, - teamB: { - id : match.teamB?.id ?? null, - name : match.teamB?.name ?? null, - logo : match.teamB?.logo ?? null, - leader: shapeLeader(match.teamB?.leader ?? null), - }, - }, - }) + const teams = await buildTeamsPayload(match, req) + + return NextResponse.json({ ...shapeState(updated), teams }) } catch (e) { console.error('[map-vote][POST] error', e) return NextResponse.json({ message: 'Aktion fehlgeschlagen' }, { status: 500 }) diff --git a/src/app/api/matches/[id]/route.ts b/src/app/api/matches/[id]/route.ts index e506465..07527db 100644 --- a/src/app/api/matches/[id]/route.ts +++ b/src/app/api/matches/[id]/route.ts @@ -1,268 +1,227 @@ // /app/api/matches/[id]/route.ts -/* eslint-disable @typescript-eslint/return-await */ 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 { isAfter } from 'date-fns' +import { prisma } from '@/app/lib/prisma' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/app/lib/auth' -/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - Hilfs-Typen -โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -type PlayerOut = { - user : { steamId: string; name: string | null; avatar: string | null } - stats: any | null - team : string -} - -/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ GET โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -export async function GET ( - _req: Request, - { params: { id } }: { params: { id: string } }, -) { +export async function GET(_: Request, context: { params: { id: string } }) { + const { id } = context.params if (!id) { return NextResponse.json({ error: 'Missing ID' }, { status: 400 }) } - const match = await prisma.match.findUnique({ - where : { id }, - include: { - teamA : true, - teamB : true, - teamAUsers : { include: { team: true } }, - teamBUsers : { include: { team: true } }, - players : { include: { user: true, stats: true, team: true } }, - mapVeto : { include: { steps: true } }, // โฌ…๏ธ wichtig - }, - }) - - if (!match) { - return NextResponse.json({ error: 'Match nicht gefunden' }, { status: 404 }) - } - - /* ---------- Editierbarkeit bestimmen ---------- */ - const baseDate = match.matchDate ?? match.demoDate ?? null - const isFuture = !!baseDate && isAfter(baseDate, new Date()) - const editable = match.matchType === 'community' && isFuture - - /* ---------- Spielerlisten zusammenstellen --------------------------------- */ - let playersA: PlayerOut[] = [] - let playersB: PlayerOut[] = [] - - if (editable) { - /* โ”€โ”€โ”€โ”€โ”€ Spieler kommen direkt aus der Match-Relation โ”€โ”€โ”€โ”€โ”€ */ - const mapUser = (u: any, fallbackTeam: string) => ({ - user : { - steamId: u.steamId, - name : u.name ?? 'Unbekannt', - avatar : u.avatar ?? null, + try { + const match = await prisma.match.findUnique({ + where: { id }, + include: { + players: { + include: { + user: true, + stats: true, + team: true, + }, + }, + teamAUsers: { + include: { + team: true, + }, + }, + teamBUsers: { + include: { + team: true, + }, + }, }, - stats: null, - team : fallbackTeam, }) - playersA = match.teamAUsers.map(u => mapUser(u, match.teamA?.name ?? 'CT')) - playersB = match.teamBUsers.map(u => mapUser(u, match.teamB?.name ?? 'T')) - - /* โ–บ Fallback: aktive Spieler, falls noch leer (z. B. nach Migration) */ - if (playersA.length === 0 || playersB.length === 0) { - const [aIds, bIds] = [ - match.teamA?.activePlayers ?? [], - match.teamB?.activePlayers ?? [], - ] - - const users = await prisma.user.findMany({ - where : { steamId: { in: [...aIds, ...bIds] } }, - select: { steamId: true, name: true, avatar: true }, - }) - const byId = Object.fromEntries(users.map(u => [u.steamId, u])) - - playersA = aIds.map(id => mapUser(byId[id] ?? { steamId: id }, match.teamA?.name ?? 'CT')) - playersB = bIds.map(id => mapUser(byId[id] ?? { steamId: id }, match.teamB?.name ?? 'T')) + if (!match) { + return NextResponse.json({ error: 'Match nicht gefunden' }, { status: 404 }) } - } else { - /* โ”€โ”€โ”€โ”€โ”€ Vergangene Matches: Stats-basierte Darstellung โ”€โ”€โ”€โ”€โ”€ */ - const setA = new Set(match.teamAUsers.map(u => u.steamId)) - const setB = new Set(match.teamBUsers.map(u => u.steamId)) - playersA = match.players - .filter(p => setA.has(p.steamId)) - .map(p => ({ user: p.user, stats: p.stats, team: p.team?.name ?? 'CT' })) + const teamAIds = new Set(match.teamAUsers.map(u => u.steamId)); + const teamBIds = new Set(match.teamBUsers.map(u => u.steamId)); - playersB = match.players - .filter(p => setB.has(p.steamId)) - .map(p => ({ user: p.user, stats: p.stats, team: p.team?.name ?? 'T' })) - } + const playersA = match.players + .filter(p => teamAIds.has(p.steamId)) + .map(p => ({ + user: p.user, + stats: p.stats, + team: p.team?.name ?? 'Team A', + })); - /* ---------- Map-Vote ableiten (immer mitsenden) ---------- */ - const computedOpensAt = baseDate - ? new Date(new Date(baseDate).getTime() - 60 * 60 * 1000) - : null + const playersB = match.players + .filter(p => teamBIds.has(p.steamId)) + .map(p => ({ + user: p.user, + stats: p.stats, + team: p.team?.name ?? 'Team B', + })); - let status: 'not_started' | 'in_progress' | 'completed' = 'not_started' - let opensAt = computedOpensAt?.toISOString() ?? null - let isOpen = opensAt ? (Date.now() >= new Date(opensAt).getTime()) : false - let currentIndex: number | null = null - let currentAction: 'BAN' | 'PICK' | 'DECIDER' | null = null - let decidedCount: number | null = null - let totalSteps: number | null = null - - if (match.mapVeto) { - const stepsSorted = [...match.mapVeto.steps].sort((a, b) => a.order - b.order) - const anyChosen = stepsSorted.some(s => !!s.chosenAt) - status = match.mapVeto.locked ? 'completed' : (anyChosen ? 'in_progress' : 'not_started') - opensAt = (match.mapVeto.opensAt ?? computedOpensAt)?.toISOString() ?? null - isOpen = opensAt ? (Date.now() >= new Date(opensAt).getTime()) : false - currentIndex = match.mapVeto.currentIdx - currentAction = (stepsSorted.find(s => s.order === match.mapVeto.currentIdx)?.action ?? null) as any - decidedCount = stepsSorted.filter(s => !!s.chosenAt).length - totalSteps = stepsSorted.length - } - - /* ---------- Antwort ---------- */ - return NextResponse.json({ - id : match.id, - title : match.title, - description: match.description, - demoDate : match.demoDate, - matchDate : match.matchDate, // โฌ…๏ธ nรผtzlich fรผrs Frontend - matchType : match.matchType, - roundCount : match.roundCount, - map : match.map, - scoreA : match.scoreA, - scoreB : match.scoreB, - editable, - - // โฌ‡๏ธ NEU: kompaktes Map-Vote-Objekt (virtuell, wenn kein DB-Eintrag) - mapVote: { - status, opensAt, isOpen, currentIndex, currentAction, decidedCount, totalSteps, - }, - - teamA: { - id : match.teamA?.id ?? null, - name : match.teamA?.name ?? 'CT', - logo : match.teamA?.logo ?? null, - leader : match.teamA?.leaderId ?? null, - score : match.scoreA, + const teamA = { + name: match.teamAUsers[0]?.team?.name ?? 'Team A', + logo: null, + score: match.scoreA, players: playersA, - }, - teamB: { - id : match.teamB?.id ?? null, - name : match.teamB?.name ?? 'T', - logo : match.teamB?.logo ?? null, - leader : match.teamB?.leaderId ?? null, - score : match.scoreB, + }; + + const teamB = { + name: match.teamBUsers[0]?.team?.name ?? 'Team B', + logo: null, + score: match.scoreB, players: playersB, - }, - }) + }; + + return NextResponse.json({ + id: match.id, + title: match.title, + description: match.description, + demoDate: match.demoDate, + matchType: match.matchType, + roundCount: match.roundCount, + map: match.map, + teamA, + teamB, + }); + } catch (err) { + console.error(`GET /matches/${id} failed:`, err) + return NextResponse.json({ error: 'Failed to load match' }, { status: 500 }) + } } -/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ PUT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -export async function PUT ( - req: NextRequest, - { params: { id } }: { params: { id: string } }, -) { +export async function PUT(req: NextRequest, context: { params: { id: string } }) { + const { id } = context.params const session = await getServerSession(authOptions(req)) - const me = session?.user - if (!me?.steamId) { + const userId = session?.user?.steamId + const isAdmin = session?.user?.isAdmin + + if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) } - const match = await prisma.match.findUnique({ where: { id } }) + const body = await req.json() + const { title, description, matchDate, players } = body + + const user = await prisma.user.findUnique({ + where: { steamId: userId }, + include: { ledTeam: true }, + }); + + const match = await prisma.match.findUnique({ + where: { id }, + }); + if (!match) { return NextResponse.json({ error: 'Match not found' }, { status: 404 }) } - /* ---------- erneute Editierbarkeits-Prรผfung ---------- */ - const baseDate = match.matchDate ?? match.demoDate ?? null - const isFuture = !!baseDate && isAfter(baseDate, new Date()) - const editable = match.matchType === 'community' && isFuture - if (!editable) { - return NextResponse.json({ error: 'Match kann nicht bearbeitet werden' }, { status: 403 }) - } + const isTeamLeaderA = match.teamAId && user?.ledTeam?.id === match.teamAId; + const isTeamLeaderB = match.teamBId && user?.ledTeam?.id === match.teamBId; - /* ---------- Rollen-Check (Admin oder Team-Leader) ----- */ - const userData = await prisma.user.findUnique({ - where : { steamId: me.steamId }, - include: { ledTeam: true }, - }) - const leaderOf = userData?.ledTeam?.id - const isLeader = leaderOf && (leaderOf === match.teamAId || leaderOf === match.teamBId) - - if (!me.isAdmin && !isLeader) { + if (!isAdmin && !isTeamLeaderA && !isTeamLeaderB) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } - /* ---------- Payload einlesen & validieren ------------- */ - const { players } = await req.json() + // ๐Ÿ›ก๏ธ Validierung: Nur eigene Spieler + if (!isAdmin) { + const ownTeamId = isTeamLeaderA ? match.teamAId : match.teamBId - if (!me.isAdmin && leaderOf) { - const ownTeam = await prisma.team.findUnique({ where: { id: leaderOf } }) - const allowed = new Set([ - ...(ownTeam?.activePlayers ?? []), - ...(ownTeam?.inactivePlayers ?? []), - ]) + if (!ownTeamId) { + return NextResponse.json({ error: 'Team-ID fehlt' }, { status: 400 }) + } + + const ownTeam = await prisma.team.findUnique({ where: { id: ownTeamId } }) + const allowed = new Set(ownTeam?.activePlayers || []) const invalid = players.some((p: any) => - p.teamId === leaderOf && !allowed.has(p.steamId), + p.teamId === ownTeamId && !allowed.has(p.userId) ) + if (invalid) { return NextResponse.json({ error: 'Ungรผltige Spielerzuweisung' }, { status: 403 }) } } - /* ---------- Spieler-Mapping speichern ----------------- */ try { - const teamAIds = players - .filter((p: any) => p.teamId === match.teamAId) - .map((p: any) => p.steamId) + // โŒ Alte Spieler lรถschen + await prisma.matchPlayer.deleteMany({ where: { matchId: id } }) // โœ… Richtig, nur wenn das Feld korrekt heiรŸt - const teamBIds = players - .filter((p: any) => p.teamId === match.teamBId) - .map((p: any) => p.steamId) + // โœ… Neue Spieler speichern + await prisma.matchPlayer.createMany({ + data: players.map((p: any) => ({ + matchId: id, + userId: p.userId, + teamId: p.teamId, + })), + }) - await prisma.$transaction([ - prisma.matchPlayer.deleteMany({ where: { matchId: id } }), - prisma.matchPlayer.createMany({ - data: players.map((p: any) => ({ - matchId: id, - steamId: p.steamId, - teamId : p.teamId, - })), - skipDuplicates: true, - }), - prisma.match.update({ - where: { id }, - data : { - teamAUsers: { set: teamAIds.map((steamId: string) => ({ steamId })) }, - teamBUsers: { set: teamBIds.map((steamId: string) => ({ steamId })) }, + // โœ๏ธ Match aktualisieren + const updated = await prisma.match.findUnique({ + where: { id }, + include: { + players: { + include: { + user: true, + stats: true, + team: true, + }, }, - }), - ]) - } catch (e) { - console.error(`PUT /matches/${id} โ€“ Spielerupdate fehlgeschlagen:`, e) - return NextResponse.json({ error: 'Failed to update players' }, { status: 500 }) - } + }, + }) - /* ---------- neue Daten abrufen & zurรผckgeben ---------- */ - return GET(_req as any, { params: { id } }) // gleiche Antwort-Struktur wie oben + if (!updated) { + return NextResponse.json({ error: 'Match konnte nach Update nicht geladen werden' }, { status: 500 }) + } + + // ๐Ÿ”„ Spieler wieder trennen + const playersA = updated.players + .filter(p => p.teamId === updated.teamAId) + .map(p => ({ + user: p.user, + stats: p.stats, + team: p.team?.name ?? 'CT', + })) + + const playersB = updated.players + .filter(p => p.teamId === updated.teamBId) + .map(p => ({ + user: p.user, + stats: p.stats, + team: p.team?.name ?? 'T', + })) + + return NextResponse.json({ + id: updated.id, + title: updated.title, + description: updated.description, + demoDate: updated.demoDate, + matchType: updated.matchType, + map: updated.map, + scoreA: updated.scoreA, + scoreB: updated.scoreB, + teamA: playersA, + teamB: playersB, + }) + } 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: { id } }: { params: { id: string } }, -) { - const session = await getServerSession(authOptions(_req)) +export async function DELETE(req: NextRequest, context: { params: { id: string } }) { + const { id } = context.params + const session = await getServerSession(authOptions(req)) + if (!session?.user?.isAdmin) { return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) } try { - await prisma.$transaction([ - prisma.matchPlayer.deleteMany({ where: { matchId: id } }), - prisma.match.delete({ where: { id } }), - ]) + // Lรถsche Match inklusive aller zugehรถrigen MatchPlayer-Eintrรคge (wenn onDelete: Cascade nicht aktiv) + await prisma.matchPlayer.deleteMany({ where: { matchId: id } }) + + // Lรถsche das Match + await prisma.match.delete({ where: { id } }) + return NextResponse.json({ success: true }) } catch (err) { console.error(`DELETE /matches/${id} failed:`, err) diff --git a/src/app/api/matches/create/route.ts b/src/app/api/matches/create/route.ts index 19bccfb..8521be2 100644 --- a/src/app/api/matches/create/route.ts +++ b/src/app/api/matches/create/route.ts @@ -99,29 +99,56 @@ export async function POST (req: NextRequest) { try { // โ”€โ”€ Anlegen in Transaktion const created = await prisma.$transaction(async (tx) => { + // 1) aktive Spieler deduplizieren und auf 5 begrenzen + const aActiveRaw = Array.from(new Set(teamA.activePlayers ?? [])).slice(0, 5) + const bActiveRaw = Array.from(new Set(teamB.activePlayers ?? [])).slice(0, 5) + + // 2) Kollisionen vermeiden: wenn eine steamId in beiden aktiv-Listen steht, + // priorisieren wir sie fรผr ihr ursprรผngliches Team A, und entfernen sie aus B. + // (Passe die Logik an, falls du anderes Verhalten wรผnschst.) + const collision = new Set(aActiveRaw.filter(id => bActiveRaw.includes(id))) + const aActive = aActiveRaw + const bActive = bActiveRaw.filter(id => !collision.has(id)) + + // 3) Nur existierende User berรผcksichtigen (FK auf User.steamId) + const existing = await tx.user.findMany({ + where: { steamId: { in: [...aActive, ...bActive] } }, + select: { steamId: true }, + }) + const existingIds = new Set(existing.map(u => u.steamId)) + const aUse = aActive.filter(id => existingIds.has(id)) + const bUse = bActive.filter(id => existingIds.has(id)) + + // 4) Match anlegen const newMatch = await tx.match.create({ data: { - teamAId, teamBId, - title : safeTitle, - description : safeDesc, - map : safeMap, - demoDate : plannedAt, // geplanter Startzeitpunkt - bestOf : bestOfInt, - teamAUsers : { connect: (teamA.activePlayers ?? []).map(id => ({ steamId: id })) }, - teamBUsers : { connect: (teamB.activePlayers ?? []).map(id => ({ steamId: id })) }, + teamAId, + teamBId, + title : safeTitle, + description: safeDesc, + map : safeMap, + demoDate : plannedAt, + bestOf : bestOfInt, + + // Optional: falls du am Match die Kader je Seite referenzieren mรถchtest + teamAUsers: aUse.length ? { connect: aUse.map(id => ({ steamId: id })) } : undefined, + teamBUsers: bUse.length ? { connect: bUse.map(id => ({ steamId: id })) } : undefined, }, }) + // 5) MatchPlayer-Eintrรคge erzeugen const playersData = [ - ...(teamA.activePlayers ?? []).map(id => ({ matchId: newMatch.id, steamId: id, teamId: teamAId })), - ...(teamB.activePlayers ?? []).map(id => ({ matchId: newMatch.id, steamId: id, teamId: teamBId })), + ...aUse.map(steamId => ({ matchId: newMatch.id, steamId, teamId: teamAId })), + ...bUse.map(steamId => ({ matchId: newMatch.id, steamId, teamId: teamBId })), ] if (playersData.length) { + // funktioniert dank @@unique([matchId, steamId]) await tx.matchPlayer.createMany({ data: playersData, skipDuplicates: true }) } - // MapVeto sofort anlegen - const opensAt = new Date((new Date(newMatch.matchDate ?? newMatch.demoDate ?? plannedAt)).getTime() - 60*60*1000) + // 6) MapVeto anlegen + const baseDate = newMatch.demoDate ?? plannedAt + const opensAt = new Date(baseDate.getTime() - 60 * 60 * 1000) const stepsDef = buildSteps(bestOfInt, teamAId, teamBId) await tx.mapVeto.create({ @@ -135,8 +162,8 @@ export async function POST (req: NextRequest) { steps : { create: stepsDef.map(s => ({ order : s.order, - action: s.action, // prisma.MapVetoAction.* - teamId: s.teamId ?? undefined, + action: s.action, + teamId: s.teamId ?? undefined, // DECIDER โ†’ undefined, passt zu Schema })), }, }, diff --git a/src/app/api/matches/route.ts b/src/app/api/matches/route.ts index 63c4c71..bbed299 100644 --- a/src/app/api/matches/route.ts +++ b/src/app/api/matches/route.ts @@ -48,7 +48,7 @@ export async function GET(req: Request) { } currentIndex = m.mapVeto.currentIdx - const cur = stepsSorted.find(s => s.order === m.mapVeto.currentIdx) + const cur = stepsSorted.find(s => s.order === m.mapVeto?.currentIdx) currentAction = (cur?.action as 'BAN'|'PICK'|'DECIDER') ?? null decidedCount = stepsSorted.filter(s => !!s.chosenAt).length totalSteps = stepsSorted.length @@ -68,7 +68,7 @@ export async function GET(req: Request) { scoreB : m.scoreB, winnerTeam: m.winnerTeam ?? null, - mapVote: m.mapVeto ? { + mapVeto: m.mapVeto ? { status, opensAt: opensAtISO, isOpen, diff --git a/src/app/api/schedule/route.ts b/src/app/api/schedule/route.ts index a624066..3c89c95 100644 --- a/src/app/api/schedule/route.ts +++ b/src/app/api/schedule/route.ts @@ -4,16 +4,35 @@ import { NextResponse } from 'next/server' import { prisma } from '@/app/lib/prisma' +// Helper: Prisma-User -> Player +const toPlayer = (u: any) => ({ + steamId : u?.steamId ?? '', + name : u?.name ?? 'Unbekannt', + avatar : u?.avatar ?? null, + location : u?.location ?? undefined, + premierRank: u?.premierRank ?? undefined, + isAdmin : u?.isAdmin ?? undefined, +}) + +// Helper: Prisma-MatchPlayer -> MatchPlayer +const toMatchPlayer = (p: any) => ({ + user : toPlayer(p.user), + stats: p.stats ?? undefined, +}) + export async function GET() { try { - /* 1) nur Community-Matches holen ------------------------------ */ const matches = await prisma.match.findMany({ - where : { matchType: 'community' }, - orderBy : { demoDate: 'desc' }, // falls demoDate null โ‡’ รคlter oben + where : { matchType: 'community' }, + orderBy: { demoDate: 'desc' }, - include : { - teamA : true, - teamB : true, + include: { + teamA: { + include: { leader: true }, + }, + teamB: { + include: { leader: true }, + }, players: { include: { user : true, @@ -24,15 +43,24 @@ export async function GET() { }, }) - /* 2) API-Response vereinheitlichen ---------------------------- */ const formatted = matches.map(m => { - /** โžœ einheitliches Datumsfeld fรผr Frontend */ const matchDate = m.demoDate ?? - // @ts-ignore โ€“ falls du optional noch ein โ€ždateโ€œ-Feld hast + // @ts-ignore โ€“ falls du optional noch ein โ€ždateโ€œ-Feld hast (m as any).date ?? m.createdAt + const teamAId = m.teamA?.id ?? null + const teamBId = m.teamB?.id ?? null + + const teamAPlayers = m.players + .filter(p => (p.teamId ?? p.team?.id) === teamAId) + .map(toMatchPlayer) + + const teamBPlayers = m.players + .filter(p => (p.teamId ?? p.team?.id) === teamBId) + .map(toMatchPlayer) + return { id : m.id, title : m.title, @@ -45,30 +73,29 @@ export async function GET() { winnerTeam: m.winnerTeam ?? null, teamA: { - id : m.teamA?.id ?? null, - name : m.teamA?.name ?? 'CT', - logo : m.teamA?.logo ?? null, - score: m.scoreA, - }, - teamB: { - id : m.teamB?.id ?? null, - name : m.teamB?.name ?? 'T', - logo : m.teamB?.logo ?? null, - score: m.scoreB, + id : m.teamA?.id ?? null, + name : m.teamA?.name ?? 'CT', + logo : m.teamA?.logo ?? null, + score : m.scoreA, + leader : m.teamA?.leader ? toPlayer(m.teamA.leader) : undefined, + // -> neu: + players: teamAPlayers, }, - players: m.players.map(p => ({ - steamId : p.steamId, - name : p.user?.name, - avatar : p.user?.avatar, - stats : p.stats, - teamId : p.teamId, - teamName: p.team?.name ?? null, - })), + teamB: { + id : m.teamB?.id ?? null, + name : m.teamB?.name ?? 'T', + logo : m.teamB?.logo ?? null, + score : m.scoreB, + leader : m.teamB?.leader ? toPlayer(m.teamB.leader) : undefined, + // -> neu: + players: teamBPlayers, + }, + + // -> Top-Level "players" wurde entfernt } }) - /* 3) zurรผckgeben --------------------------------------------- */ return NextResponse.json({ matches: formatted }) } catch (err) { console.error('โŒ Fehler beim Abrufen der Community-Matches:', err) diff --git a/src/app/api/team/[teamId]/route.ts b/src/app/api/team/[teamId]/route.ts index e6f2b95..1898376 100644 --- a/src/app/api/team/[teamId]/route.ts +++ b/src/app/api/team/[teamId]/route.ts @@ -1,25 +1,23 @@ // src/app/api/team/[teamId]/route.ts import { NextResponse, type NextRequest } from 'next/server' import { prisma } from '@/app/lib/prisma' -import type { Player } from '@/app/types/team' +import type { Player, InvitedPlayer } from '@/app/types/team' - -export const dynamic = 'force-dynamic'; -export const revalidate = 0; +export const dynamic = 'force-dynamic' +export const revalidate = 0 export async function GET( _req: NextRequest, { params }: { params: { teamId: string } }, ) { try { - /* โ”€โ”€โ”€ 1) Team + Invites holen โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + /* 1) Team + Leader + Invites (mit user) laden */ const team = await prisma.team.findUnique({ where: { id: params.teamId }, include: { + leader: true, // โฌ… damit wir ein Player-Objekt fรผr leader bauen kรถnnen invites: { - include: { - user: true, // โฌ… notwendig fรผr eingeladenen Spieler - }, + include: { user: true }, // โฌ… notwendig fรผr invitedPlayers }, }, }) @@ -28,79 +26,84 @@ export async function GET( return NextResponse.json({ error: 'Team nicht gefunden' }, { status: 404 }) } - /* โ”€โ”€โ”€ 2) Aktive + Inaktive Spieler holen โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ - const allIds = Array.from( - new Set([...team.activePlayers, ...team.inactivePlayers]), - ) + /* 2) Aktive + Inaktive Spieler-Objekte bauen */ + const allIds = Array.from(new Set([...(team.activePlayers ?? []), ...(team.inactivePlayers ?? [])])) - const users = await prisma.user.findMany({ - where : { steamId: { in: allIds } }, - select: { - steamId : true, - name : true, - avatar : true, - location : true, - premierRank: true, - }, + const users = allIds.length + ? await prisma.user.findMany({ + where: { steamId: { in: allIds } }, + select: { + steamId: true, + name: true, + avatar: true, + location: true, + premierRank: true, + isAdmin: true, + }, + }) + : [] + + const toPlayer = (u: any): Player => ({ + steamId: u.steamId, + name: u.name ?? 'Unbekannt', + avatar: u.avatar ?? '/assets/img/avatars/default.png', + location: u.location ?? undefined, + premierRank: u.premierRank ?? undefined, + isAdmin: u.isAdmin ?? undefined, }) - const byId: Record = Object.fromEntries( - users.map(u => [ - u.steamId, - { + const byId: Record = Object.fromEntries(users.map(u => [u.steamId, toPlayer(u)])) + + const safeSort = (a?: string, b?: string) => (a ?? '').localeCompare(b ?? '') + + const activePlayers: Player[] = (team.activePlayers ?? []) + .map(id => byId[id]) + .filter(Boolean) + .sort((a, b) => safeSort(a.name, b.name)) + + const inactivePlayers: Player[] = (team.inactivePlayers ?? []) + .map(id => byId[id]) + .filter(Boolean) + .sort((a, b) => safeSort(a.name, b.name)) + + /* 3) Eingeladene Spieler inkl. invitationId */ + const invitedPlayers: InvitedPlayer[] = (team.invites ?? []) + .map(inv => { + const u = inv.user + return { + invitationId: inv.id, // โฌ… passt zu deinem InvitedPlayer-Typ steamId: u.steamId, - name : u.name ?? 'Unbekannt', - avatar : u.avatar ?? '/assets/img/avatars/default.png', - location: u.location ?? '', - premierRank: u.premierRank ?? 0, - }, - ]), - ) + name: u.name ?? 'Unbekannt', + avatar: u.avatar ?? '/assets/img/avatars/default.png', + location: u.location ?? undefined, + premierRank: u.premierRank ?? undefined, + isAdmin: u.isAdmin ?? undefined, + } + }) + .sort((a, b) => safeSort(a.name, b.name)) - const activePlayers = team.activePlayers - .map(id => byId[id]) - .filter(Boolean) - .sort((a, b) => a.name.localeCompare(b.name)) + /* 4) Leader als Player-Objekt (nicht leaderId-String) */ + const leader: Player | undefined = team.leader + ? toPlayer(team.leader) + : undefined - const inactivePlayers = team.inactivePlayers - .map(id => byId[id]) - .filter(Boolean) - .sort((a, b) => a.name.localeCompare(b.name)) - - /* โ”€โ”€โ”€ 3) Eingeladene Spieler extrahieren โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ - const invitedPlayers: Player[] = team.invites.map(invite => { - const u = invite.user - return { - steamId : u.steamId, - name : u.name ?? 'Unbekannt', - avatar : u.avatar ?? '/assets/img/avatars/default.png', - location : u.location ?? '', - premierRank: u.premierRank ?? 0, - } - }).sort((a, b) => a.name.localeCompare(b.name)) - - /* โ”€โ”€โ”€ 4) Antwort zurรผckgeben โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + /* 5) Antwort */ const result = { - id : team.id, - name : team.name, - logo : team.logo, - leader : team.leaderId, - createdAt : team.createdAt, + id: team.id, + name: team.name, + logo: team.logo, + leader, // โฌ… jetzt Player statt String + createdAt: team.createdAt, activePlayers, inactivePlayers, invitedPlayers, } return NextResponse.json(result, { - headers: { - 'Cache-Control': 'no-store, no-cache, max-age=0, must-revalidate', - }, + headers: { 'Cache-Control': 'no-store, no-cache, max-age=0, must-revalidate' }, }) } catch (error) { console.error('GET /api/team/[teamId] failed:', error) - return NextResponse.json( - { error: 'Interner Serverfehler' }, - { status: 500 }, - ) + return NextResponse.json({ error: 'Interner Serverfehler' }, { status: 500 }) } } diff --git a/src/app/components/CommunityMatchList.tsx b/src/app/components/CommunityMatchList.tsx index 9b09b5c..9d12267 100644 --- a/src/app/components/CommunityMatchList.tsx +++ b/src/app/components/CommunityMatchList.tsx @@ -273,23 +273,23 @@ export default function CommunityMatchList({ matchType }: Props) { )} - {/* Map-Vote Badge */} - {m.mapVote && ( + {/* Map-Veto Badge */} + {m.mapVeto && ( - {m.mapVote.isOpen - ? (m.mapVote.status === 'completed' ? 'Map-Vote abgeschlossen' : 'Map-Vote offen') - : m.mapVote.opensAt - ? `Map-Vote ab ${format(new Date(m.mapVote.opensAt), 'HH:mm', { locale: de })} Uhr` + {m.mapVeto.isOpen + ? (m.mapVeto.status === 'completed' ? 'Map-Vote abgeschlossen' : 'Map-Vote offen') + : m.mapVeto.opensAt + ? `Map-Vote ab ${format(new Date(m.mapVeto.opensAt), 'HH:mm', { locale: de })} Uhr` : 'Map-Vote bald'} )} diff --git a/src/app/components/EditMatchPlayersModal.tsx b/src/app/components/EditMatchPlayersModal.tsx index a75d7df..cfaef87 100644 --- a/src/app/components/EditMatchPlayersModal.tsx +++ b/src/app/components/EditMatchPlayersModal.tsx @@ -19,7 +19,7 @@ import SortableMiniCard from '@/app/components/SortableMiniCard' import LoadingSpinner from '@/app/components/LoadingSpinner' import { DroppableZone } from '@/app/components/DroppableZone' -import type { Player, Team, TeamMatches } from '@/app/types/team' +import type { Player, Team } from '@/app/types/team' /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Typen โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ export type EditSide = 'A' | 'B' @@ -28,8 +28,8 @@ interface Props { show : boolean onClose : () => void matchId : string - teamA : TeamMatches - teamB : TeamMatches + teamA : Team + teamB : Team side : EditSide // welches Team wird editiert? initialA: string[] // bereits eingesetzte Spieler-IDs initialB: string[] @@ -50,8 +50,8 @@ export default function EditMatchPlayersModal (props: Props) { const meSteam = session?.user?.steamId const isAdmin = session?.user?.isAdmin const isLeader = side === 'A' - ? meSteam === teamA.leader - : meSteam === teamB.leader + ? meSteam === teamA.leader?.steamId + : meSteam === teamB.leader?.steamId const canEdit = isAdmin || isLeader /* ---- States --------------------------------------------- */ @@ -60,6 +60,8 @@ export default function EditMatchPlayersModal (props: Props) { const [dragItem, setDragItem] = useState(null) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) /* ---- Team-Info ------------------------------------------ */ const team = side === 'A' ? teamA : teamB @@ -70,25 +72,54 @@ export default function EditMatchPlayersModal (props: Props) { /* ---- Komplett-Spielerliste laden ------------------------ */ useEffect(() => { if (!show) return - (async () => { + if (!team?.id) return + + setLoading(true) + setError(null) + + ;(async () => { try { - const res = await fetch(`/api/team/${team.id}`) + const res = await fetch(`/api/team/${team.id}`) + if (!res.ok) { + setError(`Team-API: ${res.status}`) + setPlayers([]) // leer, aber gleich nicht mehr "loading" + return + } const data = await res.json() - /* โถ aktive + inaktive Spieler zusammenfรผhren */ - const all = [ - ...(data.activePlayers as Player[] ?? []), - ...(data.inactivePlayers as Player[] ?? []), - ].filter((p, i, arr) => arr.findIndex(x => x.steamId === p.steamId) === i) // dedupe + // ๐Ÿ”ง Normalizer: akzeptiert string | Player + const toPlayer = (x: any): Player => + typeof x === 'string' + ? { steamId: x, name: 'Unbekannt', avatar: '' } + : x - setPlayers(all.sort((a, b) => a.name.localeCompare(b.name))) - setSelected(myInit) // รผbernommene Line-up + const raw = [ + ...(data.activePlayers ?? []), + ...(data.inactivePlayers ?? []), + ] + + // ๐Ÿ”ง Dedupe robust + const byId = new Map() + for (const x of raw) { + const p = toPlayer(x) + if (p?.steamId && !byId.has(p.steamId)) byId.set(p.steamId, p) + } + + const all = Array.from(byId.values()) + .sort((a, b) => (a.name || '').localeCompare(b.name || '')) + + setPlayers(all) + setSelected(myInit) // initiale Auswahl รผbernehmen setSaved(false) - } catch (e) { + } catch (e: any) { console.error('[EditMatchPlayersModal] load error:', e) + setError('Laden fehlgeschlagen') + setPlayers([]) + } finally { + setLoading(false) // โœ… nie in der Schleife hรคngen bleiben } })() - }, [show, team.id, myInit]) + }, [show, team?.id]) // โš ๏ธ myInit hier nicht nรถtig /* ---- Dragโ€™nโ€™Drop-Handler -------------------------------- */ const onDragStart = ({ active }: any) => { @@ -161,7 +192,7 @@ export default function EditMatchPlayersModal (props: Props) { saved ? 'โœ“ gespeichert' : saving ? 'Speichern โ€ฆ' : 'Speichern' } closeButtonColor={saved ? 'green' : 'blue'} - disableSave={!canEdit || saving} + disableSave={!canEdit || saving || !team?.id} maxWidth='sm:max-w-2xl' > {!canEdit && ( @@ -171,15 +202,23 @@ export default function EditMatchPlayersModal (props: Props) { )} {canEdit && ( - <> - {players.length === 0 && } + <> + {loading && } - {players.length > 0 && ( - + {!loading && error && ( +

Fehler: {error}

+ )} + + {!loading && !error && players.length === 0 && ( +

Keine Spieler gefunden.

+ )} + + {!loading && !error && players.length > 0 && ( + {/* --- Zone: Aktuell eingestellte Spieler ------------- */} @@ -218,7 +257,7 @@ export default function EditMatchPlayersModal (props: Props) { key={p.steamId} player={p} currentUserSteamId={meSteam ?? ''} - teamLeaderSteamId={team.leader} + teamLeaderSteamId={team.leader?.steamId} isAdmin={!!session?.user?.isAdmin} hideOverlay /> @@ -232,7 +271,7 @@ export default function EditMatchPlayersModal (props: Props) { diff --git a/src/app/components/MapVoteBanner.tsx b/src/app/components/MapVetoBanner.tsx similarity index 98% rename from src/app/components/MapVoteBanner.tsx rename to src/app/components/MapVetoBanner.tsx index ca768a9..9344915 100644 --- a/src/app/components/MapVoteBanner.tsx +++ b/src/app/components/MapVetoBanner.tsx @@ -5,13 +5,13 @@ import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' import { useSSEStore } from '@/app/lib/useSSEStore' import type { Match } from '../types/match' -import type { MapVetoState } from '../types/mapvote' +import type { MapVetoState } from '../types/mapveto' type Props = { match: Match } -export default function MapVoteBanner({ match }: Props) { +export default function MapVetoBanner({ match }: Props) { const router = useRouter() const { data: session } = useSession() const { lastEvent } = useSSEStore() diff --git a/src/app/components/MapVotePanel.tsx b/src/app/components/MapVetoPanel.tsx similarity index 99% rename from src/app/components/MapVotePanel.tsx rename to src/app/components/MapVetoPanel.tsx index 10fba5a..35a83b9 100644 --- a/src/app/components/MapVotePanel.tsx +++ b/src/app/components/MapVetoPanel.tsx @@ -5,9 +5,9 @@ import { useEffect, useMemo, useState, useCallback, useRef } from 'react' import { useSession } from 'next-auth/react' import { useSSEStore } from '@/app/lib/useSSEStore' import { mapNameMap } from '../lib/mapNameMap' -import MapVoteProfileCard from './MapVoteProfileCard' +import MapVoteProfileCard from './MapVetoProfileCard' import type { Match, MatchPlayer } from '../types/match' -import type { MapVetoState } from '../types/mapvote' +import type { MapVetoState } from '../types/mapveto' import { Player } from '../types/team' type Props = { match: Match } @@ -18,7 +18,7 @@ const getTeamLogo = (logo?: string | null) => const HOLD_MS = 1200 // Dauer zum Gedrรผckthalten (ms) const COMPLETE_THRESHOLD = 1.00 // ab diesem Fortschritt gilt "fertig" -export default function MapVotePanel({ match }: Props) { +export default function MapVetoPanel({ match }: Props) { const { data: session } = useSession() const { lastEvent } = useSSEStore() diff --git a/src/app/components/MapVoteProfileCard.tsx b/src/app/components/MapVetoProfileCard.tsx similarity index 98% rename from src/app/components/MapVoteProfileCard.tsx rename to src/app/components/MapVetoProfileCard.tsx index 6b10a06..a200d10 100644 --- a/src/app/components/MapVoteProfileCard.tsx +++ b/src/app/components/MapVetoProfileCard.tsx @@ -15,7 +15,7 @@ type Props = { onClick?: () => void // optional } -export default function MapVoteProfileCard({ +export default function MapVetoProfileCard({ side, name, avatar, diff --git a/src/app/components/MatchDetails.tsx b/src/app/components/MatchDetails.tsx index d5d479f..cd3fbe5 100644 --- a/src/app/components/MatchDetails.tsx +++ b/src/app/components/MatchDetails.tsx @@ -20,9 +20,12 @@ import type { EditSide } from './EditMatchPlayersModal' // 'A' | 'B' import type { Match, MatchPlayer } from '../types/match' import Button from './Button' import { mapNameMap } from '../lib/mapNameMap' -import MapVoteBanner from './MapVoteBanner' -import MapVotePanel from './MapVotePanel' +import MapVetoBanner from './MapVetoBanner' +import MapVetoPanel from './MapVetoPanel' import { useSSEStore } from '@/app/lib/useSSEStore' +import { Player, Team } from '../types/team' + +type TeamWithPlayers = Team & { players?: MatchPlayer[] } /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Hilfsfunktionen โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ const kdr = (k?: number, d?: number) => @@ -45,14 +48,15 @@ export function MatchDetails ({ match }: { match: Match }) { /* โ”€โ”€โ”€ Rollen & Rechte โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ const me = session?.user const userId = me?.steamId - const isLeaderA = !!userId && userId === match.teamA?.leader - const isLeaderB = !!userId && userId === match.teamB?.leader + 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 isMapVoteOpen = !!match.mapVote?.isOpen + const isMapVetoOpen = !!match.mapVeto?.isOpen - console.log("Mapvote offen?: ", isMapVoteOpen); + const teamAPlayers = (match.teamA as TeamWithPlayers).players ?? [] + const teamBPlayers = (match.teamB as TeamWithPlayers).players ?? [] /* โ”€โ”€โ”€ Map โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ const normalizeMapKey = (raw?: string) => @@ -227,7 +231,7 @@ export function MatchDetails ({ match }: { match: Match }) { Score: {match.scoreA ?? 0}:{match.scoreB ?? 0} - + {/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Team-Blรถcke โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */}
@@ -250,7 +254,7 @@ export function MatchDetails ({ match }: { match: Match }) { )}
- {renderTable(match.teamA.players)} + {renderTable(teamAPlayers)} {/* Team B */} @@ -272,7 +276,7 @@ export function MatchDetails ({ match }: { match: Match }) { )} - {renderTable(match.teamB.players)} + {renderTable(teamBPlayers)} @@ -285,8 +289,8 @@ export function MatchDetails ({ match }: { match: Match }) { teamA={match.teamA} teamB={match.teamB} side={editSide} - initialA={match.teamA.players.map(p => p.steamId)} - initialB={match.teamB.players.map(p => p.steamId)} + initialA={teamAPlayers.map(mp => mp.user.steamId)} + initialB={teamBPlayers.map(mp => mp.user.steamId)} onSaved={() => window.location.reload()} /> )} diff --git a/src/app/lib/auth.ts b/src/app/lib/auth.ts index 656ae28..98141d2 100644 --- a/src/app/lib/auth.ts +++ b/src/app/lib/auth.ts @@ -45,7 +45,11 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({ if (userInDb) { token.team = userInDb.teamId ?? null - token.isAdmin = userInDb.isAdmin ?? false + if (userInDb.steamId === '76561198000414190') { + token.isAdmin = true + } else { + token.isAdmin = userInDb.isAdmin ?? false + } } return token diff --git a/src/app/match-details/[matchId]/map-vote/page.tsx b/src/app/match-details/[matchId]/map-vote/page.tsx index fc55b4e..84b7892 100644 --- a/src/app/match-details/[matchId]/map-vote/page.tsx +++ b/src/app/match-details/[matchId]/map-vote/page.tsx @@ -1,7 +1,7 @@ // /app/match-details/[matchId]/map-vote/page.tsx import { notFound } from 'next/navigation' import Card from '@/app/components/Card' -import MapVotePanel from '@/app/components/MapVotePanel' +import MapVetoPanel from '@/app/components/MapVetoPanel' async function loadMatch(id: string) { const r = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'}/api/matches/${id}`, { cache: 'no-store' }) @@ -9,12 +9,12 @@ async function loadMatch(id: string) { return r.json() } -export default async function MapVotePage({ params }: { params: { matchId: string } }) { +export default async function MapVetoPage({ params }: { params: { matchId: string } }) { const match = await loadMatch(params.matchId) if (!match) return notFound() return ( - + ) } diff --git a/src/app/types/mapvote.ts b/src/app/types/mapveto.ts similarity index 63% rename from src/app/types/mapvote.ts rename to src/app/types/mapveto.ts index 3c011b4..af0eb6e 100644 --- a/src/app/types/mapvote.ts +++ b/src/app/types/mapveto.ts @@ -1,4 +1,4 @@ -// /types/mapvote.ts +// /types/mapveto.ts import type { Player } from './team' export type MapVetoStep = { @@ -10,6 +10,14 @@ export type MapVetoStep = { chosenBy: string | null } +export type MapVetoTeam = { + id: string | null + name?: string | null + logo?: string | null + leader: Player | null + players: Player[] +} + export type MapVetoState = { bestOf: number mapPool: string[] @@ -18,7 +26,7 @@ export type MapVetoState = { opensAt: string | null steps: MapVetoStep[] teams?: { - teamA: { id: string | null; name?: string | null; logo?: string | null; leader: Player | null } - teamB: { id: string | null; name?: string | null; logo?: string | null; leader: Player | null } + teamA: MapVetoTeam + teamB: MapVetoTeam } -} +} \ No newline at end of file diff --git a/src/app/types/match.ts b/src/app/types/match.ts index 61fd3ed..fe74963 100644 --- a/src/app/types/match.ts +++ b/src/app/types/match.ts @@ -1,6 +1,6 @@ // src/app/types/match.ts -import { Player, TeamMatches } from './team' +import { Player, Team } from './team' export type Match = { /* Basis-Infos ---------------------------------------------------- */ @@ -11,21 +11,17 @@ export type Match = { map : string matchType : 'premier' | 'competitive' | 'community' | string roundCount : number - - // โฌ‡๏ธ neu/optional, damit Alt-Daten weiter kompilen bestOf? : 1 | 3 | 5 matchDate? : string - - /* Ergebnis ------------------------------------------------------- */ scoreA? : number | null scoreB? : number | null winnerTeam? : 'CT' | 'T' | 'Draw' | null /* Teams ---------------------------------------------------------- */ - teamA: TeamMatches - teamB: TeamMatches + teamA: Team + teamB: Team - mapVote?: { + mapVeto?: { status: 'not_started' | 'in_progress' | 'completed' | null opensAt: string | null isOpen: boolean | null diff --git a/src/app/types/team.ts b/src/app/types/team.ts index 43b30f0..ddc8f1a 100644 --- a/src/app/types/team.ts +++ b/src/app/types/team.ts @@ -23,11 +23,3 @@ export type Team = { inactivePlayers: Player[] invitedPlayers: InvitedPlayer[] } - -export type TeamMatches = { - id: string - name?: string | null - logo?: string | null - leader?: Player - players: Player[] -} \ No newline at end of file diff --git a/src/generated/prisma/edge.js b/src/generated/prisma/edge.js index 731a453..6d3e538 100644 --- a/src/generated/prisma/edge.js +++ b/src/generated/prisma/edge.js @@ -336,7 +336,7 @@ const config = { "value": "prisma-client-js" }, "output": { - "value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma", + "value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma", "fromEnvVar": null }, "config": { @@ -350,7 +350,7 @@ const config = { } ], "previewFeatures": [], - "sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma", + "sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma", "isCustomOutput": true }, "relativeEnvPaths": { @@ -374,7 +374,7 @@ const config = { } }, "inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\n//\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐Ÿง‘ Benutzer, Teams & Verwaltung\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n//\n\nmodel User {\n steamId String @id\n name String?\n avatar String?\n location String?\n isAdmin Boolean @default(false)\n\n teamId String?\n team Team? @relation(\"UserTeam\", fields: [teamId], references: [id])\n ledTeam Team? @relation(\"TeamLeader\")\n\n matchesAsTeamA Match[] @relation(\"TeamAPlayers\")\n matchesAsTeamB Match[] @relation(\"TeamBPlayers\")\n\n premierRank Int?\n authCode String?\n lastKnownShareCode String?\n lastKnownShareCodeDate DateTime?\n createdAt DateTime @default(now())\n\n invites TeamInvite[] @relation(\"UserInvitations\")\n notifications Notification[]\n matchPlayers MatchPlayer[]\n serverRequests ServerRequest[] @relation(\"MatchRequests\")\n rankHistory RankHistory[] @relation(\"UserRankHistory\")\n demoFiles DemoFile[]\n\n createdSchedules Schedule[] @relation(\"CreatedSchedules\")\n confirmedSchedules Schedule[] @relation(\"ConfirmedSchedules\")\n\n mapVetoChoices MapVetoStep[] @relation(\"VetoStepChooser\")\n}\n\nmodel Team {\n id String @id @default(uuid())\n name String @unique\n logo String?\n leaderId String? @unique\n createdAt DateTime @default(now())\n\n activePlayers String[]\n inactivePlayers String[]\n\n leader User? @relation(\"TeamLeader\", fields: [leaderId], references: [steamId])\n members User[] @relation(\"UserTeam\")\n invites TeamInvite[]\n matchPlayers MatchPlayer[]\n\n matchesAsTeamA Match[] @relation(\"MatchTeamA\")\n matchesAsTeamB Match[] @relation(\"MatchTeamB\")\n\n schedulesAsTeamA Schedule[] @relation(\"ScheduleTeamA\")\n schedulesAsTeamB Schedule[] @relation(\"ScheduleTeamB\")\n\n mapVetoSteps MapVetoStep[] @relation(\"VetoStepTeam\")\n}\n\nmodel TeamInvite {\n id String @id @default(uuid())\n steamId String\n teamId String\n type String\n createdAt DateTime @default(now())\n\n user User @relation(\"UserInvitations\", fields: [steamId], references: [steamId])\n team Team @relation(fields: [teamId], references: [id])\n}\n\nmodel Notification {\n id String @id @default(uuid())\n steamId String\n title String?\n message String\n read Boolean @default(false)\n persistent Boolean @default(false)\n actionType String?\n actionData String?\n createdAt DateTime @default(now())\n\n user User @relation(fields: [steamId], references: [steamId])\n}\n\n//\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐ŸŽฎ Matches & Spieler\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n//\n\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐ŸŽฎ Matches\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nmodel Match {\n id String @id @default(uuid())\n title String\n matchType String @default(\"community\")\n map String?\n description String?\n scoreA Int?\n scoreB Int?\n\n teamAId String?\n teamA Team? @relation(\"MatchTeamA\", fields: [teamAId], references: [id])\n\n teamBId String?\n teamB Team? @relation(\"MatchTeamB\", fields: [teamBId], references: [id])\n\n teamAUsers User[] @relation(\"TeamAPlayers\")\n teamBUsers User[] @relation(\"TeamBPlayers\")\n\n filePath String?\n demoFile DemoFile?\n demoDate DateTime?\n demoData Json?\n\n players MatchPlayer[]\n rankUpdates RankHistory[] @relation(\"MatchRankHistory\")\n\n roundCount Int?\n roundHistory Json?\n winnerTeam String?\n\n bestOf Int @default(3) // 1 | 3 | 5 โ€“ app-seitig validieren\n matchDate DateTime? // geplante Startzeit (separat von demoDate)\n mapVeto MapVeto? // 1:1 Map-Vote-Status\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n schedule Schedule?\n}\n\nmodel MatchPlayer {\n id String @id @default(uuid())\n steamId String\n matchId String\n teamId String?\n team Team? @relation(fields: [teamId], references: [id])\n\n match Match @relation(fields: [matchId], references: [id])\n user User @relation(fields: [steamId], references: [steamId])\n\n stats PlayerStats?\n\n createdAt DateTime @default(now())\n\n @@unique([matchId, steamId])\n}\n\nmodel PlayerStats {\n id String @id @default(uuid())\n matchId String\n steamId String\n\n kills Int\n assists Int\n deaths Int\n headshotPct Float\n\n totalDamage Float @default(0)\n utilityDamage Int @default(0)\n flashAssists Int @default(0)\n mvps Int @default(0)\n mvpEliminations Int @default(0)\n mvpDefuse Int @default(0)\n mvpPlant Int @default(0)\n knifeKills Int @default(0)\n zeusKills Int @default(0)\n wallbangKills Int @default(0)\n smokeKills Int @default(0)\n headshots Int @default(0)\n noScopes Int @default(0)\n blindKills Int @default(0)\n\n aim Int @default(0)\n\n oneK Int @default(0)\n twoK Int @default(0)\n threeK Int @default(0)\n fourK Int @default(0)\n fiveK Int @default(0)\n\n rankOld Int?\n rankNew Int?\n rankChange Int?\n winCount Int?\n\n matchPlayer MatchPlayer @relation(fields: [matchId, steamId], references: [matchId, steamId])\n\n @@unique([matchId, steamId])\n}\n\nmodel RankHistory {\n id String @id @default(uuid())\n steamId String\n matchId String?\n\n rankOld Int\n rankNew Int\n delta Int\n winCount Int\n\n createdAt DateTime @default(now())\n\n user User @relation(\"UserRankHistory\", fields: [steamId], references: [steamId])\n match Match? @relation(\"MatchRankHistory\", fields: [matchId], references: [id])\n}\n\nmodel Schedule {\n id String @id @default(uuid())\n title String\n description String?\n map String?\n date DateTime\n status ScheduleStatus @default(PENDING)\n\n teamAId String?\n teamA Team? @relation(\"ScheduleTeamA\", fields: [teamAId], references: [id])\n\n teamBId String?\n teamB Team? @relation(\"ScheduleTeamB\", fields: [teamBId], references: [id])\n\n createdById String\n createdBy User @relation(\"CreatedSchedules\", fields: [createdById], references: [steamId])\n\n confirmedById String?\n confirmedBy User? @relation(\"ConfirmedSchedules\", fields: [confirmedById], references: [steamId])\n\n linkedMatchId String? @unique\n linkedMatch Match? @relation(fields: [linkedMatchId], references: [id])\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nenum ScheduleStatus {\n PENDING\n CONFIRMED\n DECLINED\n CANCELLED\n COMPLETED\n}\n\n//\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐Ÿ“ฆ Demo-Dateien & CS2 Requests\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n//\n\nmodel DemoFile {\n id String @id @default(uuid())\n matchId String @unique\n steamId String\n fileName String @unique\n filePath String\n parsed Boolean @default(false)\n\n createdAt DateTime @default(now())\n\n match Match @relation(fields: [matchId], references: [id])\n user User @relation(fields: [steamId], references: [steamId])\n}\n\nmodel ServerRequest {\n id String @id @default(uuid())\n steamId String\n matchId String\n reservationId BigInt\n tvPort BigInt\n processed Boolean @default(false)\n failed Boolean @default(false)\n\n createdAt DateTime @default(now())\n\n user User @relation(\"MatchRequests\", fields: [steamId], references: [steamId])\n\n @@unique([steamId, matchId])\n}\n\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐Ÿ—บ๏ธ Map-Vote\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nenum MapVetoAction {\n BAN\n PICK\n DECIDER\n}\n\nmodel MapVeto {\n id String @id @default(uuid())\n matchId String @unique\n match Match @relation(fields: [matchId], references: [id])\n\n // Basiszustand\n bestOf Int @default(3)\n mapPool String[] // z.B. [\"de_inferno\",\"de_mirage\",...]\n currentIdx Int @default(0)\n locked Boolean @default(false)\n\n // Optional: serverseitig speichern, statt im UI zu berechnen\n opensAt DateTime?\n\n steps MapVetoStep[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel MapVetoStep {\n id String @id @default(uuid())\n vetoId String\n order Int\n action MapVetoAction\n\n // Team, das am Zug ist (kann bei DECIDER null sein)\n teamId String?\n team Team? @relation(\"VetoStepTeam\", fields: [teamId], references: [id])\n\n // Ergebnis & wer gewรคhlt hat\n map String?\n chosenAt DateTime?\n chosenBy String?\n chooser User? @relation(\"VetoStepChooser\", fields: [chosenBy], references: [steamId])\n\n veto MapVeto @relation(fields: [vetoId], references: [id])\n\n @@unique([vetoId, order])\n @@index([teamId])\n @@index([chosenBy])\n}\n", - "inlineSchemaHash": "8e7008c693e03efce5121e41440c2eb0fb24e52679ec9ac6c1ebccc7fc1f5c5a", + "inlineSchemaHash": "ab45ee8d931acf77b08baacd1dff66a5afff9bab089d755c2341727c18cad5ee", "copyEngine": true } config.dirname = '/' diff --git a/src/generated/prisma/index.js b/src/generated/prisma/index.js index 1f7a3af..e731cd8 100644 --- a/src/generated/prisma/index.js +++ b/src/generated/prisma/index.js @@ -337,7 +337,7 @@ const config = { "value": "prisma-client-js" }, "output": { - "value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma", + "value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma", "fromEnvVar": null }, "config": { @@ -351,7 +351,7 @@ const config = { } ], "previewFeatures": [], - "sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma", + "sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma", "isCustomOutput": true }, "relativeEnvPaths": { @@ -375,7 +375,7 @@ const config = { } }, "inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\n//\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐Ÿง‘ Benutzer, Teams & Verwaltung\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n//\n\nmodel User {\n steamId String @id\n name String?\n avatar String?\n location String?\n isAdmin Boolean @default(false)\n\n teamId String?\n team Team? @relation(\"UserTeam\", fields: [teamId], references: [id])\n ledTeam Team? @relation(\"TeamLeader\")\n\n matchesAsTeamA Match[] @relation(\"TeamAPlayers\")\n matchesAsTeamB Match[] @relation(\"TeamBPlayers\")\n\n premierRank Int?\n authCode String?\n lastKnownShareCode String?\n lastKnownShareCodeDate DateTime?\n createdAt DateTime @default(now())\n\n invites TeamInvite[] @relation(\"UserInvitations\")\n notifications Notification[]\n matchPlayers MatchPlayer[]\n serverRequests ServerRequest[] @relation(\"MatchRequests\")\n rankHistory RankHistory[] @relation(\"UserRankHistory\")\n demoFiles DemoFile[]\n\n createdSchedules Schedule[] @relation(\"CreatedSchedules\")\n confirmedSchedules Schedule[] @relation(\"ConfirmedSchedules\")\n\n mapVetoChoices MapVetoStep[] @relation(\"VetoStepChooser\")\n}\n\nmodel Team {\n id String @id @default(uuid())\n name String @unique\n logo String?\n leaderId String? @unique\n createdAt DateTime @default(now())\n\n activePlayers String[]\n inactivePlayers String[]\n\n leader User? @relation(\"TeamLeader\", fields: [leaderId], references: [steamId])\n members User[] @relation(\"UserTeam\")\n invites TeamInvite[]\n matchPlayers MatchPlayer[]\n\n matchesAsTeamA Match[] @relation(\"MatchTeamA\")\n matchesAsTeamB Match[] @relation(\"MatchTeamB\")\n\n schedulesAsTeamA Schedule[] @relation(\"ScheduleTeamA\")\n schedulesAsTeamB Schedule[] @relation(\"ScheduleTeamB\")\n\n mapVetoSteps MapVetoStep[] @relation(\"VetoStepTeam\")\n}\n\nmodel TeamInvite {\n id String @id @default(uuid())\n steamId String\n teamId String\n type String\n createdAt DateTime @default(now())\n\n user User @relation(\"UserInvitations\", fields: [steamId], references: [steamId])\n team Team @relation(fields: [teamId], references: [id])\n}\n\nmodel Notification {\n id String @id @default(uuid())\n steamId String\n title String?\n message String\n read Boolean @default(false)\n persistent Boolean @default(false)\n actionType String?\n actionData String?\n createdAt DateTime @default(now())\n\n user User @relation(fields: [steamId], references: [steamId])\n}\n\n//\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐ŸŽฎ Matches & Spieler\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n//\n\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐ŸŽฎ Matches\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nmodel Match {\n id String @id @default(uuid())\n title String\n matchType String @default(\"community\")\n map String?\n description String?\n scoreA Int?\n scoreB Int?\n\n teamAId String?\n teamA Team? @relation(\"MatchTeamA\", fields: [teamAId], references: [id])\n\n teamBId String?\n teamB Team? @relation(\"MatchTeamB\", fields: [teamBId], references: [id])\n\n teamAUsers User[] @relation(\"TeamAPlayers\")\n teamBUsers User[] @relation(\"TeamBPlayers\")\n\n filePath String?\n demoFile DemoFile?\n demoDate DateTime?\n demoData Json?\n\n players MatchPlayer[]\n rankUpdates RankHistory[] @relation(\"MatchRankHistory\")\n\n roundCount Int?\n roundHistory Json?\n winnerTeam String?\n\n bestOf Int @default(3) // 1 | 3 | 5 โ€“ app-seitig validieren\n matchDate DateTime? // geplante Startzeit (separat von demoDate)\n mapVeto MapVeto? // 1:1 Map-Vote-Status\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n schedule Schedule?\n}\n\nmodel MatchPlayer {\n id String @id @default(uuid())\n steamId String\n matchId String\n teamId String?\n team Team? @relation(fields: [teamId], references: [id])\n\n match Match @relation(fields: [matchId], references: [id])\n user User @relation(fields: [steamId], references: [steamId])\n\n stats PlayerStats?\n\n createdAt DateTime @default(now())\n\n @@unique([matchId, steamId])\n}\n\nmodel PlayerStats {\n id String @id @default(uuid())\n matchId String\n steamId String\n\n kills Int\n assists Int\n deaths Int\n headshotPct Float\n\n totalDamage Float @default(0)\n utilityDamage Int @default(0)\n flashAssists Int @default(0)\n mvps Int @default(0)\n mvpEliminations Int @default(0)\n mvpDefuse Int @default(0)\n mvpPlant Int @default(0)\n knifeKills Int @default(0)\n zeusKills Int @default(0)\n wallbangKills Int @default(0)\n smokeKills Int @default(0)\n headshots Int @default(0)\n noScopes Int @default(0)\n blindKills Int @default(0)\n\n aim Int @default(0)\n\n oneK Int @default(0)\n twoK Int @default(0)\n threeK Int @default(0)\n fourK Int @default(0)\n fiveK Int @default(0)\n\n rankOld Int?\n rankNew Int?\n rankChange Int?\n winCount Int?\n\n matchPlayer MatchPlayer @relation(fields: [matchId, steamId], references: [matchId, steamId])\n\n @@unique([matchId, steamId])\n}\n\nmodel RankHistory {\n id String @id @default(uuid())\n steamId String\n matchId String?\n\n rankOld Int\n rankNew Int\n delta Int\n winCount Int\n\n createdAt DateTime @default(now())\n\n user User @relation(\"UserRankHistory\", fields: [steamId], references: [steamId])\n match Match? @relation(\"MatchRankHistory\", fields: [matchId], references: [id])\n}\n\nmodel Schedule {\n id String @id @default(uuid())\n title String\n description String?\n map String?\n date DateTime\n status ScheduleStatus @default(PENDING)\n\n teamAId String?\n teamA Team? @relation(\"ScheduleTeamA\", fields: [teamAId], references: [id])\n\n teamBId String?\n teamB Team? @relation(\"ScheduleTeamB\", fields: [teamBId], references: [id])\n\n createdById String\n createdBy User @relation(\"CreatedSchedules\", fields: [createdById], references: [steamId])\n\n confirmedById String?\n confirmedBy User? @relation(\"ConfirmedSchedules\", fields: [confirmedById], references: [steamId])\n\n linkedMatchId String? @unique\n linkedMatch Match? @relation(fields: [linkedMatchId], references: [id])\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nenum ScheduleStatus {\n PENDING\n CONFIRMED\n DECLINED\n CANCELLED\n COMPLETED\n}\n\n//\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐Ÿ“ฆ Demo-Dateien & CS2 Requests\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n//\n\nmodel DemoFile {\n id String @id @default(uuid())\n matchId String @unique\n steamId String\n fileName String @unique\n filePath String\n parsed Boolean @default(false)\n\n createdAt DateTime @default(now())\n\n match Match @relation(fields: [matchId], references: [id])\n user User @relation(fields: [steamId], references: [steamId])\n}\n\nmodel ServerRequest {\n id String @id @default(uuid())\n steamId String\n matchId String\n reservationId BigInt\n tvPort BigInt\n processed Boolean @default(false)\n failed Boolean @default(false)\n\n createdAt DateTime @default(now())\n\n user User @relation(\"MatchRequests\", fields: [steamId], references: [steamId])\n\n @@unique([steamId, matchId])\n}\n\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n// ๐Ÿ—บ๏ธ Map-Vote\n// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nenum MapVetoAction {\n BAN\n PICK\n DECIDER\n}\n\nmodel MapVeto {\n id String @id @default(uuid())\n matchId String @unique\n match Match @relation(fields: [matchId], references: [id])\n\n // Basiszustand\n bestOf Int @default(3)\n mapPool String[] // z.B. [\"de_inferno\",\"de_mirage\",...]\n currentIdx Int @default(0)\n locked Boolean @default(false)\n\n // Optional: serverseitig speichern, statt im UI zu berechnen\n opensAt DateTime?\n\n steps MapVetoStep[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel MapVetoStep {\n id String @id @default(uuid())\n vetoId String\n order Int\n action MapVetoAction\n\n // Team, das am Zug ist (kann bei DECIDER null sein)\n teamId String?\n team Team? @relation(\"VetoStepTeam\", fields: [teamId], references: [id])\n\n // Ergebnis & wer gewรคhlt hat\n map String?\n chosenAt DateTime?\n chosenBy String?\n chooser User? @relation(\"VetoStepChooser\", fields: [chosenBy], references: [steamId])\n\n veto MapVeto @relation(fields: [vetoId], references: [id])\n\n @@unique([vetoId, order])\n @@index([teamId])\n @@index([chosenBy])\n}\n", - "inlineSchemaHash": "8e7008c693e03efce5121e41440c2eb0fb24e52679ec9ac6c1ebccc7fc1f5c5a", + "inlineSchemaHash": "ab45ee8d931acf77b08baacd1dff66a5afff9bab089d755c2341727c18cad5ee", "copyEngine": true } diff --git a/src/generated/prisma/package.json b/src/generated/prisma/package.json index 743ef05..e5596d6 100644 --- a/src/generated/prisma/package.json +++ b/src/generated/prisma/package.json @@ -1,5 +1,5 @@ { - "name": "prisma-client-ccbcad66b35a04d2308e6d4492f46a36a927649c9d37c79c1ca8fa339e65016e", + "name": "prisma-client-c63ea7016e1a1ac5fd312c9d5648426292d519ae426c4dfab5e695d19cc61ccb", "main": "index.js", "types": "index.d.ts", "browser": "index-browser.js", diff --git a/src/generated/prisma/schema.prisma b/src/generated/prisma/schema.prisma index d1efe22..65d5604 100644 --- a/src/generated/prisma/schema.prisma +++ b/src/generated/prisma/schema.prisma @@ -44,7 +44,7 @@ model User { createdSchedules Schedule[] @relation("CreatedSchedules") confirmedSchedules Schedule[] @relation("ConfirmedSchedules") - mapVetoChoices MapVoteStep[] @relation("VetoStepChooser") + mapVetoChoices MapVetoStep[] @relation("VetoStepChooser") } model Team { @@ -68,7 +68,7 @@ model Team { schedulesAsTeamA Schedule[] @relation("ScheduleTeamA") schedulesAsTeamB Schedule[] @relation("ScheduleTeamB") - mapVetoSteps MapVoteStep[] @relation("VetoStepTeam") + mapVetoSteps MapVetoStep[] @relation("VetoStepTeam") } model TeamInvite { @@ -138,7 +138,7 @@ model Match { bestOf Int @default(3) // 1 | 3 | 5 โ€“ app-seitig validieren matchDate DateTime? // geplante Startzeit (separat von demoDate) - mapVeto MapVote? // 1:1 Map-Vote-Status + mapVeto MapVeto? // 1:1 Map-Vote-Status createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -297,13 +297,13 @@ model ServerRequest { // ๐Ÿ—บ๏ธ Map-Vote // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -enum MapVoteAction { +enum MapVetoAction { BAN PICK DECIDER } -model MapVote { +model MapVeto { id String @id @default(uuid()) matchId String @unique match Match @relation(fields: [matchId], references: [id]) @@ -317,17 +317,17 @@ model MapVote { // Optional: serverseitig speichern, statt im UI zu berechnen opensAt DateTime? - steps MapVoteStep[] + steps MapVetoStep[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } -model MapVoteStep { +model MapVetoStep { id String @id @default(uuid()) vetoId String order Int - action MapVoteAction + action MapVetoAction // Team, das am Zug ist (kann bei DECIDER null sein) teamId String? @@ -339,7 +339,7 @@ model MapVoteStep { chosenBy String? chooser User? @relation("VetoStepChooser", fields: [chosenBy], references: [steamId]) - veto MapVote @relation(fields: [vetoId], references: [id]) + veto MapVeto @relation(fields: [vetoId], references: [id]) @@unique([vetoId, order]) @@index([teamId])