поправил капитанов на расстановки
This commit is contained in:
16
app.py
16
app.py
@@ -1711,6 +1711,22 @@ def api_save_squad_editor_data(
|
|||||||
home_team_id = session_row[13]
|
home_team_id = session_row[13]
|
||||||
away_team_id = session_row[17]
|
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(
|
save_match_lineup_for_editor(
|
||||||
match_id=match_id,
|
match_id=match_id,
|
||||||
home_team_id=home_team_id,
|
home_team_id=home_team_id,
|
||||||
|
|||||||
@@ -471,6 +471,52 @@ def save_match_lineup_for_editor(
|
|||||||
insert_players("away", "starting", away_starting)
|
insert_players("away", "starting", away_starting)
|
||||||
insert_players("away", "bench", away_bench)
|
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("home", home_coaches)
|
||||||
insert_coaches("away", away_coaches)
|
insert_coaches("away", away_coaches)
|
||||||
|
|
||||||
|
|||||||
109
static/script.js
109
static/script.js
@@ -2580,6 +2580,13 @@ function makeDraggable(el) {
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
renderRefereesEditor();
|
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() {
|
async function openSquadEditor() {
|
||||||
if (!editModeEnabled) return;
|
if (!editModeEnabled) return;
|
||||||
|
|
||||||
@@ -3370,6 +3440,13 @@ async function openSquadEditor() {
|
|||||||
renderSquadEditorSide("away");
|
renderSquadEditorSide("away");
|
||||||
|
|
||||||
document.getElementById("squadEditorModal")?.classList.add("show");
|
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 availablePlayers = getAvailableSquadPool(side);
|
||||||
const coachOptions = state.coachPool;
|
const coachOptions = state.coachPool;
|
||||||
|
const captainWarning = sideNeedsCaptain(state)
|
||||||
|
? `<div class="squad-editor-captain-warning">⚠ Капитан не выбран. Назначьте капитана из основного состава.</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
mount.innerHTML = `
|
mount.innerHTML = `
|
||||||
|
${captainWarning}
|
||||||
<div class="squad-editor-section">
|
<div class="squad-editor-section">
|
||||||
<div class="squad-editor-section-title">Добавить игрока</div>
|
<div class="squad-editor-section-title">Добавить игрока</div>
|
||||||
<div class="squad-editor-box">
|
<div class="squad-editor-box">
|
||||||
@@ -3560,6 +3641,9 @@ function removePlayerFromEditor(side, playerId, role) {
|
|||||||
const state = getSideEditorState(side);
|
const state = getSideEditorState(side);
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
|
const removedPlayer = state[role]?.find(p => String(p.player_id) === String(playerId));
|
||||||
|
const removedCaptain = role === "starting" && !!removedPlayer?.is_captain;
|
||||||
|
|
||||||
const hasLinkedEvents = matchEvents.some(event =>
|
const hasLinkedEvents = matchEvents.some(event =>
|
||||||
String(event.side) === String(side) &&
|
String(event.side) === String(side) &&
|
||||||
(
|
(
|
||||||
@@ -3576,6 +3660,10 @@ function removePlayerFromEditor(side, playerId, role) {
|
|||||||
|
|
||||||
state[role] = state[role].filter(p => String(p.player_id) !== String(playerId));
|
state[role] = state[role].filter(p => String(p.player_id) !== String(playerId));
|
||||||
renderSquadEditorSide(side);
|
renderSquadEditorSide(side);
|
||||||
|
|
||||||
|
if (removedCaptain && sideNeedsCaptain(state)) {
|
||||||
|
showCaptainRequiredWarning([side]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function movePlayerBetweenZones(side, playerId, fromRole, toRole) {
|
function movePlayerBetweenZones(side, playerId, fromRole, toRole) {
|
||||||
@@ -3586,19 +3674,18 @@ function movePlayerBetweenZones(side, playerId, fromRole, toRole) {
|
|||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
|
|
||||||
const [player] = state[fromRole].splice(idx, 1);
|
const [player] = state[fromRole].splice(idx, 1);
|
||||||
|
const movedCaptainOutOfStarting = fromRole === "starting" && toRole === "bench" && !!player.is_captain;
|
||||||
|
|
||||||
if (toRole === "bench") {
|
if (toRole === "bench") {
|
||||||
player.is_captain = false;
|
player.is_captain = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state[toRole].push(player);
|
state[toRole].push(player);
|
||||||
|
|
||||||
const hasCaptainInStarting = state.starting.some(p => p.is_captain);
|
|
||||||
if (!hasCaptainInStarting && state.starting.length) {
|
|
||||||
state.starting[0].is_captain = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSquadEditorSide(side);
|
renderSquadEditorSide(side);
|
||||||
|
|
||||||
|
if (movedCaptainOutOfStarting && sideNeedsCaptain(state)) {
|
||||||
|
showCaptainRequiredWarning([side]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3620,6 +3707,10 @@ function setCaptainInEditor(side, playerId, role) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderSquadEditorSide(side);
|
renderSquadEditorSide(side);
|
||||||
|
|
||||||
|
if (!getMissingCaptainSidesFromEditor().length) {
|
||||||
|
closeCaptainRequiredWarning();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCoachInEditor(side, index, coachId) {
|
function updateCoachInEditor(side, index, coachId) {
|
||||||
@@ -3757,6 +3848,12 @@ function rebuildFormationFromMatchData(side) {
|
|||||||
async function applySquadEditorChanges() {
|
async function applySquadEditorChanges() {
|
||||||
if (!squadEditorState.home || !squadEditorState.away) return;
|
if (!squadEditorState.home || !squadEditorState.away) return;
|
||||||
|
|
||||||
|
const missingCaptainSides = getMissingCaptainSidesFromEditor();
|
||||||
|
if (missingCaptainSides.length) {
|
||||||
|
showCaptainRequiredWarning(missingCaptainSides);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = buildSquadSavePayload();
|
const payload = buildSquadSavePayload();
|
||||||
const result = await saveSquadEditorChangesToServer(payload);
|
const result = await saveSquadEditorChangesToServer(payload);
|
||||||
|
|
||||||
|
|||||||
@@ -1897,6 +1897,63 @@ body.edit-mode-on .player-node.dragging {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.squad-editor-captain-warning {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #f1b9b9;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff1f1;
|
||||||
|
color: #9f1d1d;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
#captainRequiredModal {
|
||||||
|
z-index: 3400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captain-required-modal-content {
|
||||||
|
width: min(560px, 92vw);
|
||||||
|
margin-top: 16vh;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: visible;
|
||||||
|
border: 1px solid #f0c7c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captain-required-icon {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff0f0;
|
||||||
|
color: #c62828;
|
||||||
|
border: 2px solid #ef9a9a;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captain-required-copy {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captain-required-text {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #4d4d4d;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captain-required-actions {
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.squad-editor-grid {
|
.squad-editor-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -1387,6 +1387,27 @@ data-role="{{ p.position or '' }}"
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="formation-modal captain-required-modal" id="captainRequiredModal">
|
||||||
|
<div class="formation-modal-backdrop" onclick="closeCaptainRequiredWarning()"></div>
|
||||||
|
|
||||||
|
<div class="formation-modal-content captain-required-modal-content">
|
||||||
|
<div class="captain-required-icon">!</div>
|
||||||
|
<div class="captain-required-copy">
|
||||||
|
<div class="formation-modal-title">Не выбран капитан</div>
|
||||||
|
<div class="captain-required-text" id="captainRequiredText"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formation-modal-footer captain-required-actions">
|
||||||
|
<button type="button" class="btn btn-cancel" onclick="closeCaptainRequiredWarning()">
|
||||||
|
Закрыть
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-confirm" onclick="openCaptainEditorFromWarning()">
|
||||||
|
Выбрать капитана
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="formation-modal" id="squadEditorModal">
|
<div class="formation-modal" id="squadEditorModal">
|
||||||
<div class="formation-modal-backdrop" onclick="closeSquadEditor()"></div>
|
<div class="formation-modal-backdrop" onclick="closeSquadEditor()"></div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user