import { useEffect, useState } from 'react'; import Modal from './Modal'; import Notice from './Notice'; import { ApiError, api } from '../lib/api'; import { CLASSIFICATION_LABEL } from '../lib/format'; import type { Character, ForumClassification, ForumThread, PlayerDetail, PlayerSummary, } from '../lib/types'; export function ClassifyModal({ thread, onClose, onDone, onError, }: { thread: ForumThread; onClose: () => void; onDone: (message: string) => void; onError: (message: string) => void; }) { const [choice, setChoice] = useState('NEW_CC'); const [name, setName] = useState(thread.title); const [predecessorId, setPredecessorId] = useState(null); const [reviveId, setReviveId] = useState(null); // Der Regelfall ist das bezahlte Redesign. Kulanz ist die Ausnahme und // wird bewusst vergeben - sonst entstehen Kulanz-Buchungen, die niemand // gewollt hat. const [useKind, setUseKind] = useState<'REDESIGN_KULANZ' | 'REDESIGN_PAID'>('REDESIGN_PAID'); const [characters, setCharacters] = useState([]); const [inactive, setInactive] = useState([]); const [busy, setBusy] = useState(false); // Für ein Redesign braucht der Admin die Liste der aktiven CCs des Spielers. useEffect(() => { if (!thread.playerId) return; void (async () => { try { const detail = await api(`/api/players/${thread.playerId}`); setCharacters(detail.characters.filter((c) => c.status === 'ACTIVE')); setInactive(detail.characters.filter((c) => c.status === 'INACTIVE')); } catch { // Ohne Liste bleibt nur die Ablehnung - das Formular sagt es. } })(); }, [thread.playerId]); const needsPlayer = choice === 'NEW_CC' || choice === 'REDESIGN' || choice === 'REACTIVATION'; const needsName = choice === 'NEW_CC' || choice === 'REDESIGN'; // Ein redesignter CC existiert im Original nicht mehr und kostet deshalb. const gewaehlterCC = inactive.find((c) => c.id === reviveId); const reaktivierungKostet = gewaehlterCC?.inactiveReason === 'REWORKED'; const submit = async (event: React.FormEvent) => { event.preventDefault(); setBusy(true); try { await api(`/api/forum/threads/${thread.threadId}/classify`, { method: 'POST', body: { classification: choice, ...(needsName ? { characterName: name.trim() } : {}), ...(choice === 'REDESIGN' ? { predecessorId, useKind } : {}), ...(choice === 'REACTIVATION' ? { characterId: reviveId, ...(reaktivierungKostet ? { useKind } : {}) } : {}), }, }); onDone(`Antrag entschieden: ${CLASSIFICATION_LABEL[choice]}.`); } catch (err) { onError(err instanceof ApiError ? err.message : 'Entscheidung fehlgeschlagen'); } finally { setBusy(false); } }; return (
{thread.title}
von {thread.authorName ?? 'unbekannt'} {thread.player ? ` · ${thread.player.displayName}` : ' · kein Spieler zugeordnet'}
{( [ ['NEW_CC', 'Angenommen - neuer CC', 'verbraucht 1 CC-Guthaben'], ['REDESIGN', 'Angenommen - Redesign', 'verbraucht 1 Redesign-Guthaben'], ['REACTIVATION', 'Angenommen - Reaktivierung', 'ein inaktiver CC kehrt zurück'], ['REJECTED', 'Abgelehnt', 'keine Buchung'], ['IGNORE', 'Kein Antrag', 'keine Buchung'], ] as const ).map(([value, label, hint]) => ( ))}
{needsPlayer && !thread.playerId && ( Ohne zugeordneten Spieler lässt sich nichts buchen. Bitte erst „Spieler zuordnen". )} {needsName && thread.playerId && ( <> setName(event.target.value)} required />

Vorbelegt mit dem Thread-Titel - bitte anpassen.

)} {choice === 'REACTIVATION' && thread.playerId && ( <> {inactive.length === 0 && (

Dieser Spieler hat keine inaktiven CCs.

)} {gewaehlterCC && !reaktivierungKostet && ( Kommt gratis zurück - der CC existiert unverändert weiter. )} {reaktivierungKostet && ( <> Dieser CC wurde redesignt und existiert im Original nicht mehr. Die Rückkehr kostet ein Guthaben.
)} )} {choice === 'REDESIGN' && thread.playerId && ( <> {characters.length === 0 && (

Dieser Spieler hat keine aktiven CCs.

)}
)}
); } export function AssignModal({ thread, onClose, onDone, onError, }: { thread: ForumThread; onClose: () => void; onDone: () => void; onError: (message: string) => void; }) { const [query, setQuery] = useState(thread.authorName ?? ''); const [results, setResults] = useState([]); useEffect(() => { const timer = setTimeout(async () => { try { const params = new URLSearchParams({ q: query, take: '20' }); const data = await api<{ items: PlayerSummary[] }>(`/api/players?${params}`); setResults(data.items); } catch { // Suchfehler blockieren den Dialog nicht. } }, 250); return () => clearTimeout(timer); }, [query]); const assign = async (playerId: number) => { try { await api(`/api/forum/threads/${thread.threadId}/player`, { method: 'POST', body: { playerId }, }); onDone(); } catch (err) { onError(err instanceof ApiError ? err.message : 'Zuordnen fehlgeschlagen'); } }; return ( Forumsname des Antragstellers: {thread.authorName ?? 'unbekannt'}. Passt kein Eintrag, trag den Forumsnamen beim Spieler nach - dann findet der nächste Abgleich ihn von allein. setQuery(event.target.value)} autoFocus />
    {results.map((player) => (
  • {player.displayName} {player.forumName && · Forum: {player.forumName}}
  • ))} {results.length === 0 &&

    Kein Treffer.

    }
); }