287 lines
9.5 KiB
TypeScript
287 lines
9.5 KiB
TypeScript
/* ------------------------------------------------------------------
|
||
/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])
|
||
|
||
/* ---- Drag’n’Drop-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>
|
||
)
|
||
}
|