Партии: «выбывшие» игроки (красный крестик) = последнее место

Выбор места при завершении партии получает опцию «✕ выбыл»; выбывшему
авто-проставляется общее последнее место (ничья последнего ранга), плюс флаг
eliminated для отрисовки красным крестиком. В результатах и списке партий
выбывшие показываются крестиком и идут в конец. Подсчёт очков не меняется
(используются обычные числовые места).

Бэкенд: модель MatchParticipant.eliminated + миграция 0009 (идемпотентная);
схемы (place стал необязательным, +eliminated); match_service (хелпер
_resolve_finish_places, finish/update); проброс eliminated в роутерах и stats.
Фронт: выбор места с крестиком в MatchDetailPage и AdminMatchEdit, отрисовка в
результатах и MatchList, стиль .place-badge.eliminated; регенерация schema.d.ts.
Тесты: завершение с выбывшим (последнее место, засчитан), все-выбывшие → 422.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 16:54:37 +03:00
co-authored by Claude Opus 4.8
parent cd7e5d16b4
commit 1438a17441
13 changed files with 226 additions and 17 deletions
+14 -1
View File
@@ -19,6 +19,7 @@ interface Row {
nickname: string;
faction_id: number;
place: number;
eliminated: boolean;
was_random: boolean;
comment: string;
}
@@ -46,6 +47,7 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
nickname: p.nickname,
faction_id: p.faction_id,
place: p.place ?? 1,
eliminated: p.eliminated,
was_random: p.was_random,
comment: p.comment ?? "",
})),
@@ -71,7 +73,8 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
participants: rows.map((r) => ({
user_id: r.user_id,
faction_id: r.faction_id,
place: r.place,
place: r.eliminated ? null : r.place,
eliminated: r.eliminated,
was_random: r.was_random,
comment: r.comment.trim() || null,
})),
@@ -133,9 +136,19 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
type="number"
min={1}
value={r.place}
disabled={r.eliminated}
onChange={(e) => upd(i, { place: Math.max(1, Number(e.target.value) || 1) })}
/>
</div>
<label className="row small" style={{ flex: "0 0 auto", gap: 4 }}>
<input
type="checkbox"
style={{ width: "auto" }}
checked={r.eliminated}
onChange={(e) => upd(i, { eliminated: e.target.checked })}
/>
выбыл
</label>
</div>
<input
placeholder="Комментарий об игроке"