import { useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ApiError } from "../api/client"; import { ConfirmDialog } from "../components/ConfirmDialog"; import { MatchMedia } from "../components/MatchMedia"; import { Spinner } from "../components/Spinner"; import { formatDuration, formatTime } from "../domain/format"; import { WIN_REASONS, type WinReason, winReasonLabel } from "../domain/winReasons"; import { useToast } from "../context/ToastContext"; import { useDeleteMatch, useDeleteMatchAttachment, useFinishMatch, useMatch, useUploadMatchAttachment, } from "../hooks/matches"; interface FinishRow { user_id: number; nickname: string; faction_name: string; place: number; eliminated: boolean; comment: string; } export function MatchDetailPage() { const { matchId } = useParams(); const id = matchId ? Number(matchId) : null; const { data: match, isLoading, refetch } = useMatch(id); const finish = useFinishMatch(); const del = useDeleteMatch(); const uploadAtt = useUploadMatchAttachment(id ?? 0); const deleteAtt = useDeleteMatchAttachment(id ?? 0); const toast = useToast(); const navigate = useNavigate(); const [rows, setRows] = useState(null); const [winReason, setWinReason] = useState("objectives"); const [overall, setOverall] = useState(""); const [error, setError] = useState(null); const [confirmRemove, setConfirmRemove] = useState(false); if (isLoading) return ; if (!match) return
Партия не найдена.
; const canModify = !!match.can_modify; // авторитетный флаг с бэкенда (создатель/owner/admin) const inProgress = match.status === "in_progress"; // Ленивая инициализация строк завершения из участников. const finishRows: FinishRow[] = rows ?? match.participants.map((p, i) => ({ user_id: p.user_id, nickname: p.nickname, faction_name: p.faction_name, place: i + 1, eliminated: false, comment: p.comment ?? "", })); const upd = (i: number, patch: Partial) => setRows(finishRows.map((r, idx) => (idx === i ? { ...r, ...patch } : r))); // Конфликт версий (кто-то изменил партию с другого устройства) → сообщаем и обновляем. const isStale = (e: unknown) => e instanceof ApiError && e.code === "STALE_WRITE"; const submitFinish = async () => { if (!id || !match) return; setError(null); try { await finish.mutateAsync({ matchId: id, body: { participants: finishRows.map((r) => ({ user_id: r.user_id, place: r.eliminated ? null : r.place, eliminated: r.eliminated, comment: r.comment.trim() || null, })), win_reason: winReason, overall_comment: overall.trim() || null, expected_version: match.version, }, }); toast.show("Партия завершена"); } catch (e) { if (isStale(e)) { toast.show("Партия изменилась на другом устройстве — обновлено"); refetch(); return; } setError(e instanceof ApiError ? e.message : "Не удалось завершить партию"); } }; const remove = async () => { if (!id || !match) return; try { await del.mutateAsync({ matchId: id, expectedVersion: match.version }); toast.show(inProgress ? "Партия отменена" : "Партия удалена"); navigate(-1); } catch (e) { if (isStale(e)) { toast.show("Партия изменилась на другом устройстве — обновлено"); refetch(); return; } toast.show(e instanceof ApiError ? e.message : "Не удалось выполнить"); } }; const sorted = [...match.participants].sort( (a, b) => (a.place ?? 99) - (b.place ?? 99), ); return (

{match.played_at}

{inProgress ? ( идёт ) : ( {formatDuration(match.duration_minutes)} )}
Начало: {formatTime(match.started_at)} {match.finished_at && ` · конец: ${formatTime(match.finished_at)}`}
{!inProgress && (
Победа: {winReasonLabel(match.win_reason)}
)} {match.overall_comment &&

{match.overall_comment}

}
{/* Завершённая партия — результаты */} {!inProgress && (

Результаты

{sorted.map((p) => (
{p.eliminated ? "✕" : (p.place ?? "—")} {p.nickname} · {p.faction_name} {p.was_random && 🎲}
{p.comment &&
{p.comment}
}
))}
)} {/* Медиа (read-only): для завершённых и для зрителей без прав на правку */} {!(inProgress && canModify) && match.attachments.length > 0 && (

Медиа

)} {/* Незавершённая партия — состав + форма завершения */} {inProgress && ( <>

Состав

{match.participants.map((p) => (
{p.nickname} · {p.faction_name} {p.was_random && 🎲}
))}
{canModify && (

Завершение партии

{finishRows.map((r, i) => (
{r.nickname} {r.faction_name}
upd(i, { comment: e.target.value })} />
))}