diff --git a/app.py b/app.py index e06b8e7..a060a7a 100644 --- a/app.py +++ b/app.py @@ -1711,6 +1711,22 @@ def api_save_squad_editor_data( home_team_id = session_row[13] away_team_id = session_row[17] + missing_captains = [] + if payload.home_starting and not any(player.is_captain for player in payload.home_starting): + missing_captains.append(str(session_row[14] or "Домашняя команда")) + if payload.away_starting and not any(player.is_captain for player in payload.away_starting): + missing_captains.append(str(session_row[18] or "Гостевая команда")) + + if missing_captains: + teams_text = ", ".join(missing_captains) + return JSONResponse( + { + "success": False, + "error": f"Не выбран капитан: {teams_text}. Назначьте капитана из основного состава.", + }, + status_code=400, + ) + save_match_lineup_for_editor( match_id=match_id, home_team_id=home_team_id, diff --git a/repositories/match_lineup_repository.py b/repositories/match_lineup_repository.py index 850ee24..2448b68 100644 --- a/repositories/match_lineup_repository.py +++ b/repositories/match_lineup_repository.py @@ -471,6 +471,52 @@ def save_match_lineup_for_editor( insert_players("away", "starting", away_starting) insert_players("away", "bench", away_bench) + # Синхронизируем капитана с уже сохранённой расстановкой. + # /home-formations и /away-formations читают is_captain из + # match_formations, поэтому ручная смена капитана в редакторе + # состава должна сразу попадать и туда без пересохранения + # вкладки «Расстановки». + def sync_formation_captain(side: str, team_id: int) -> None: + cur.execute( + """ + UPDATE match_formations mf + SET + is_captain = COALESCE(( + SELECT mlp.is_captain + FROM match_lineup_players mlp + WHERE mlp.match_id = mf.match_id + AND mlp.side = %s + AND mlp.role = 'starting' + AND ( + ( + mf.player_id IS NOT NULL + AND mlp.player_id = mf.player_id + ) + OR ( + COALESCE(NULLIF(TRIM(mf.number), ''), '') <> '' + AND COALESCE(mlp.number::text, '') = COALESCE(mf.number, '') + ) + ) + ORDER BY + CASE + WHEN mf.player_id IS NOT NULL + AND mlp.player_id = mf.player_id + THEN 0 + ELSE 1 + END, + mlp.sort_order + LIMIT 1 + ), FALSE), + updated_at = NOW() + WHERE mf.match_id = %s + AND mf.team_id = %s + """, + (side, match_id, team_id), + ) + + sync_formation_captain("home", home_team_id) + sync_formation_captain("away", away_team_id) + insert_coaches("home", home_coaches) insert_coaches("away", away_coaches) diff --git a/static/script.js b/static/script.js index efb752b..8149cd1 100644 --- a/static/script.js +++ b/static/script.js @@ -2580,6 +2580,13 @@ function makeDraggable(el) { document.addEventListener("DOMContentLoaded", () => { setEditMode(false); renderRefereesEditor(); + + if (isGameTabActive()) { + const missingCaptainSides = getMissingCaptainSidesFromMatchData(); + if (missingCaptainSides.length) { + window.setTimeout(() => showCaptainRequiredWarning(missingCaptainSides), 150); + } + } }); @@ -3354,6 +3361,69 @@ function cloneSideState(side) { }; } +let suppressNextCaptainWarning = false; + +function sideNeedsCaptain(state) { + const starting = Array.isArray(state?.starting) ? state.starting : []; + return starting.length > 0 && !starting.some(player => !!player.is_captain); +} + +function getMissingCaptainSidesFromEditor() { + const missing = []; + if (sideNeedsCaptain(squadEditorState.home)) missing.push("home"); + if (sideNeedsCaptain(squadEditorState.away)) missing.push("away"); + return missing; +} + +function getMissingCaptainSidesFromMatchData() { + const missing = []; + const homeState = { starting: window.MATCH_DATA?.homeStarting || [] }; + const awayState = { starting: window.MATCH_DATA?.awayStarting || [] }; + if (sideNeedsCaptain(homeState)) missing.push("home"); + if (sideNeedsCaptain(awayState)) missing.push("away"); + return missing; +} + +function getCaptainWarningTeamNames(sides) { + return sides.map(side => { + const fallback = side === "home" ? "Домашняя команда" : "Гостевая команда"; + return String(window.MATCH_DATA?.teamNames?.[side] || fallback).trim() || fallback; + }); +} + +function showCaptainRequiredWarning(sides) { + const uniqueSides = Array.from(new Set((sides || []).filter(side => side === "home" || side === "away"))); + if (!uniqueSides.length) { + closeCaptainRequiredWarning(); + return; + } + + const modal = document.getElementById("captainRequiredModal"); + const text = document.getElementById("captainRequiredText"); + if (!modal || !text) return; + + const names = getCaptainWarningTeamNames(uniqueSides); + if (names.length === 1) { + text.textContent = `У команды «${names[0]}» не выбран капитан. Обязательно назначьте капитана из основного состава перед сохранением.`; + } else { + text.textContent = `У команд «${names[0]}» и «${names[1]}» не выбраны капитаны. Обязательно назначьте капитана каждой команды из основного состава перед сохранением.`; + } + + modal.classList.add("show"); +} + +function closeCaptainRequiredWarning() { + document.getElementById("captainRequiredModal")?.classList.remove("show"); +} + +function openCaptainEditorFromWarning() { + closeCaptainRequiredWarning(); + if (!editModeEnabled) { + suppressNextCaptainWarning = true; + setEditMode(true); + } +} + async function openSquadEditor() { if (!editModeEnabled) return; @@ -3370,6 +3440,13 @@ async function openSquadEditor() { renderSquadEditorSide("away"); document.getElementById("squadEditorModal")?.classList.add("show"); + + const missingCaptainSides = getMissingCaptainSidesFromEditor(); + if (suppressNextCaptainWarning) { + suppressNextCaptainWarning = false; + } else if (missingCaptainSides.length) { + showCaptainRequiredWarning(missingCaptainSides); + } } @@ -3410,8 +3487,12 @@ function renderSquadEditorSide(side) { const availablePlayers = getAvailableSquadPool(side); const coachOptions = state.coachPool; + const captainWarning = sideNeedsCaptain(state) + ? `