ironie-nextjs/src/app/components/EditMatchPlayersModal.tsx
2025-08-14 15:06:48 +02:00

287 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/* ------------------------------------------------------------------
/app/components/EditMatchPlayersModal.tsx
zeigt ALLE Spieler des gewählten Teams & nutzt DroppableZone-IDs
"active" / "inactive" analog zur TeamMemberView.
------------------------------------------------------------------- */
'use client'
import { useEffect, useState } from 'react'
import { useSession } from 'next-auth/react'
import {
DndContext, closestCenter, DragOverlay,
} from '@dnd-kit/core'
import {
SortableContext, verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import Modal from '@/app/components/Modal'
import SortableMiniCard from '@/app/components/SortableMiniCard'
import LoadingSpinner from '@/app/components/LoadingSpinner'
import { DroppableZone } from '@/app/components/DroppableZone'
import type { Player, Team } from '@/app/types/team'
/* ───────────────────────── Typen ────────────────────────── */
export type EditSide = 'A' | 'B'
interface Props {
show : boolean
onClose : () => void
matchId : string
teamA : Team
teamB : Team
side : EditSide // welches Team wird editiert?
initialA: string[] // bereits eingesetzte Spieler-IDs
initialB: string[]
onSaved?: () => void
}
/* ───────────────────── Komponente ──────────────────────── */
export default function EditMatchPlayersModal (props: Props) {
const {
show, onClose, matchId,
teamA, teamB, side,
initialA, initialB,
onSaved,
} = props
/* ---- Rollen-Check --------------------------------------- */
const { data: session } = useSession()
const meSteam = session?.user?.steamId
const isAdmin = session?.user?.isAdmin
const isLeader = side === 'A'
? meSteam === teamA.leader?.steamId
: meSteam === teamB.leader?.steamId
const canEdit = isAdmin || isLeader
/* ---- States --------------------------------------------- */
const [players, setPlayers] = useState<Player[]>([])
const [selected, setSelected] = useState<string[]>([])
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
const other = side === 'A' ? teamB : teamA
const otherInit = side === 'A' ? initialB : initialA
const myInit = side === 'A' ? initialA : initialB
/* ---- Komplett-Spielerliste laden ------------------------ */
useEffect(() => {
if (!show) return
if (!team?.id) {
// ❗ Kein verknüpftes Team zeig einen klaren Hinweis
setPlayers([])
setSelected([])
setError('Kein Team mit diesem Match verknüpft (fehlende Team-ID).')
setLoading(false)
return
}
setLoading(true)
setError(null)
;(async () => {
try {
const res = await fetch(`/api/team/${encodeURIComponent(team.id)}`, {
cache: 'no-store',
})
if (!res.ok) {
setError(`Team-API: ${res.status}`)
setPlayers([])
return
}
const data = await res.json()
// 👉 Hier brauchst du KEIN Normalizer mehr, wenn deine /api/team-Route
// (wie zuletzt angepasst) bereits Player-Objekte liefert.
const all = [
...(data.activePlayers ?? []),
...(data.inactivePlayers ?? []),
]
.filter((p: Player) => !!p?.steamId)
.filter((p: Player, i: number, arr: Player[]) => arr.findIndex(x => x.steamId === p.steamId) === i)
.sort((a: Player, b: Player) => (a.name || '').localeCompare(b.name || ''))
setPlayers(all)
setSelected(myInit) // initiale Auswahl aus Props
setSaved(false)
} catch (e) {
console.error('[EditMatchPlayersModal] load error:', e)
setError('Laden fehlgeschlagen')
setPlayers([])
} finally {
setLoading(false)
}
})()
}, [show, team?.id])
/* ---- DragnDrop-Handler -------------------------------- */
const onDragStart = ({ active }: any) => {
setDragItem(players.find(p => p.steamId === active.id) ?? null)
}
const onDragEnd = ({ active, over }: any) => {
setDragItem(null)
if (!over) return
const id = active.id as string
const dropZone = over.id as string // "active" | "inactive"
const already = selected.includes(id)
const toActive = dropZone === 'active'
if ( (toActive && already) || (!toActive && !already) ) return
setSelected(sel =>
toActive
? [...sel, id].slice(0, 5) // max 5 einsatzfähig
: sel.filter(x => x !== id),
)
}
/* ---- Speichern ------------------------------------------ */
const handleSave = async () => {
setSaving(true)
try {
const body = {
players: [
/* akt. Auswahl für die bearbeitete Seite */
...selected.map(steamId => ({ steamId, teamId: team.id })),
/* unveränderte Gegenseite unbedingt mitschicken! */
...otherInit.map(steamId => ({ steamId, teamId: other.id })),
],
}
const res = await fetch(`/api/matches/${matchId}`, {
method : 'PUT',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify(body),
})
if (!res.ok) throw new Error()
setSaved(true)
onSaved?.()
} catch (e) {
console.error('[EditMatchPlayersModal] save error:', e)
} finally {
setSaving(false)
}
}
/* ---- Listen trennen ------------------------------------- */
const active = players.filter(p => selected.includes(p.steamId))
const inactive = players.filter(p => !selected.includes(p.steamId))
/* ---- UI -------------------------------------------------- */
if (!show) return null
return (
<Modal
id="edit-match-players"
title={`Spieler bearbeiten ${team.name ?? 'Team'}`}
show
onClose={onClose}
onSave={handleSave}
closeButtonTitle={
saved ? '✓ Gespeichert' : saving ? 'Speichern …' : 'Speichern'
}
closeButtonColor={saved ? 'green' : 'blue'}
disableSave={!canEdit || saving || !team?.id}
maxWidth='sm:max-w-2xl'
>
{!canEdit && (
<p className="text-sm text-gray-700 dark:text-neutral-300">
Du darfst dieses Team nicht bearbeiten.
</p>
)}
{canEdit && (
<>
{loading && <LoadingSpinner />}
{!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"
className="mb-4"
label={`Eingesetzte Spieler (${active.length} / 5)`}
activeDragItem={dragItem}
>
<SortableContext
items={active.map(p => p.steamId)}
strategy={verticalListSortingStrategy}
>
{active.map(p => (
<SortableMiniCard
key={p.steamId}
player={p}
currentUserSteamId={meSteam ?? ''}
teamLeaderSteamId={team.leader?.steamId}
isAdmin={!!session?.user?.isAdmin}
hideOverlay
/>
))}
</SortableContext>
</DroppableZone>
{/* --- Zone: Verfügbar (restliche) ------------------- */}
<DroppableZone
id="inactive"
label="Verfügbare Spieler"
activeDragItem={dragItem}
>
<SortableContext
items={inactive.map(p => p.steamId)}
strategy={verticalListSortingStrategy}
>
{inactive.map(p => (
<SortableMiniCard
key={p.steamId}
player={p}
currentUserSteamId={meSteam ?? ''}
teamLeaderSteamId={team.leader?.steamId}
isAdmin={!!session?.user?.isAdmin}
hideOverlay
/>
))}
</SortableContext>
</DroppableZone>
{/* Drag-Overlay */}
<DragOverlay>
{dragItem && (
<SortableMiniCard
player={dragItem}
currentUserSteamId={meSteam ?? ''}
teamLeaderSteamId={team.leader?.steamId}
isAdmin={!!session?.user?.isAdmin}
hideOverlay
/>
)}
</DragOverlay>
</DndContext>
)}
</>
)}
</Modal>
)
}