update
This commit is contained in:
parent
bdb5cbb4e1
commit
61c75b1c8c
@ -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])
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<string, Player> = Object.fromEntries(
|
||||
users.map(u => [
|
||||
u.steamId,
|
||||
{
|
||||
const byId: Record<string, Player> = 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 })
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,23 +273,23 @@ export default function CommunityMatchList({ matchType }: Props) {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Map-Vote Badge */}
|
||||
{m.mapVote && (
|
||||
{/* Map-Veto Badge */}
|
||||
{m.mapVeto && (
|
||||
<span
|
||||
className={`
|
||||
px-2 py-0.5 rounded-full text-[11px] font-semibold
|
||||
${m.mapVote.isOpen ? 'bg-green-300 dark:bg-green-600 text-white' : 'bg-neutral-200 dark:bg-neutral-700'}
|
||||
${m.mapVeto.isOpen ? 'bg-green-300 dark:bg-green-600 text-white' : 'bg-neutral-200 dark:bg-neutral-700'}
|
||||
`}
|
||||
title={
|
||||
m.mapVote.opensAt
|
||||
? `Öffnet ${format(new Date(m.mapVote.opensAt), 'dd.MM.yyyy HH:mm', { locale: de })} Uhr`
|
||||
m.mapVeto.opensAt
|
||||
? `Öffnet ${format(new Date(m.mapVeto.opensAt), 'dd.MM.yyyy HH:mm', { locale: de })} Uhr`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{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'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@ -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<Player | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(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<string, Player>()
|
||||
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 && <LoadingSpinner />}
|
||||
<>
|
||||
{loading && <LoadingSpinner />}
|
||||
|
||||
{players.length > 0 && (
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
{!loading && error && (
|
||||
<p className="text-sm text-red-600">Fehler: {error}</p>
|
||||
)}
|
||||
|
||||
{!loading && !error && players.length === 0 && (
|
||||
<p className="text-sm text-gray-500">Keine Spieler gefunden.</p>
|
||||
)}
|
||||
|
||||
{!loading && !error && players.length > 0 && (
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
{/* --- Zone: Aktuell eingestellte Spieler ------------- */}
|
||||
<DroppableZone
|
||||
id="active"
|
||||
@ -195,7 +234,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
|
||||
/>
|
||||
@ -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) {
|
||||
<SortableMiniCard
|
||||
player={dragItem}
|
||||
currentUserSteamId={meSteam ?? ''}
|
||||
teamLeaderSteamId={team.leader}
|
||||
teamLeaderSteamId={team.leader?.steamId}
|
||||
isAdmin={!!session?.user?.isAdmin}
|
||||
hideOverlay
|
||||
/>
|
||||
|
||||
@ -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()
|
||||
@ -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()
|
||||
|
||||
@ -15,7 +15,7 @@ type Props = {
|
||||
onClick?: () => void // optional
|
||||
}
|
||||
|
||||
export default function MapVoteProfileCard({
|
||||
export default function MapVetoProfileCard({
|
||||
side,
|
||||
name,
|
||||
avatar,
|
||||
@ -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 }) {
|
||||
<strong>Score:</strong> {match.scoreA ?? 0}:{match.scoreB ?? 0}
|
||||
</div>
|
||||
|
||||
<MapVoteBanner match={match} />
|
||||
<MapVetoBanner match={match} />
|
||||
|
||||
{/* ───────── Team-Blöcke ───────── */}
|
||||
<div className="border-t pt-4 mt-4 space-y-10">
|
||||
@ -250,7 +254,7 @@ export function MatchDetails ({ match }: { match: Match }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderTable(match.teamA.players)}
|
||||
{renderTable(teamAPlayers)}
|
||||
</div>
|
||||
|
||||
{/* Team B */}
|
||||
@ -272,7 +276,7 @@ export function MatchDetails ({ match }: { match: Match }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderTable(match.teamB.players)}
|
||||
{renderTable(teamBPlayers)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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()}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
<Card maxWidth="auto">
|
||||
<MapVotePanel match={match} />
|
||||
<MapVetoPanel match={match} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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[]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-ccbcad66b35a04d2308e6d4492f46a36a927649c9d37c79c1ca8fa339e65016e",
|
||||
"name": "prisma-client-c63ea7016e1a1ac5fd312c9d5648426292d519ae426c4dfab5e695d19cc61ccb",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
||||
@ -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])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user