все остальные апдейты на Кубок России

This commit is contained in:
2026-07-02 17:00:15 +03:00
parent 03a68ee8ca
commit e7b215af5e
11 changed files with 1644 additions and 180 deletions

View File

@@ -1069,6 +1069,8 @@ function setEditMode(enabled) {
el.disabled = !editModeEnabled;
}
});
setTourScheduleGlobalEditMode(editModeEnabled);
renderRefereesEditor();
if (editModeEnabled && isGameTabActive()) {
@@ -1099,6 +1101,8 @@ function updateEditModeUI() {
el.disabled = !editModeEnabled;
}
});
setTourScheduleGlobalEditMode(editModeEnabled);
}
function stepExtraTime(step) {
@@ -4064,6 +4068,113 @@ function buildChannelValue(matchId) {
return "";
}
function resetTourScheduleRowEditValues(matchId) {
document.querySelectorAll(`.tour-score-input[data-match-id="${matchId}"]`).forEach((input) => {
input.value = input.dataset.initialValue || "";
});
const statusSelect = document.querySelector(`.tour-status-select[data-match-id="${matchId}"]`);
if (statusSelect) {
statusSelect.value = statusSelect.dataset.initialStatus || "scheduled";
}
}
function setTourScheduleRowEditMode(matchId, enabled, resetValues = false) {
const display = document.querySelector(`[data-schedule-display="${matchId}"]`);
const edit = document.querySelector(`[data-schedule-edit="${matchId}"]`);
const saveActions = document.querySelector(`[data-schedule-save-actions="${matchId}"]`);
if (resetValues) {
resetTourScheduleRowEditValues(matchId);
}
if (display) display.hidden = enabled;
if (edit) edit.hidden = !enabled;
if (saveActions) saveActions.hidden = !enabled;
document
.querySelectorAll(`.tour-score-input[data-match-id="${matchId}"], .tour-status-select[data-match-id="${matchId}"]`)
.forEach((el) => {
el.disabled = !enabled;
});
}
function setTourScheduleGlobalEditMode(enabled) {
document.querySelectorAll("[data-schedule-row]").forEach((row) => {
const matchId = row.dataset.scheduleRow;
if (!matchId) return;
setTourScheduleRowEditMode(matchId, enabled, !enabled);
});
}
function enableTourScheduleEdit(matchId) {
setTourScheduleRowEditMode(matchId, true);
}
function cancelTourScheduleEdit(matchId) {
resetTourScheduleRowEditValues(matchId);
setTourScheduleRowEditMode(matchId, editModeEnabled);
}
function readTourScoreValue(matchId, side) {
const input = document.querySelector(`.tour-score-input[data-match-id="${matchId}"][data-score-side="${side}"]`);
if (!input) return null;
const value = String(input.value || "").trim();
if (value === "") return null;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error("invalid_score");
}
return parsed;
}
async function saveTourScheduleMatch(matchExternalId) {
let homeScore;
let awayScore;
try {
homeScore = readTourScoreValue(matchExternalId, "home");
awayScore = readTourScoreValue(matchExternalId, "away");
} catch (err) {
alert("Счёт должен быть целым числом 0 или больше");
return;
}
if ((homeScore === null) !== (awayScore === null)) {
alert("Заполни оба значения счёта или очисти оба поля");
return;
}
const status = document.querySelector(`.tour-status-select[data-match-id="${matchExternalId}"]`)?.value || "scheduled";
const res = await fetch(`/admin/session/${MATCH_DATA.sessionToken}/schedule/match`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
match_external_id: String(matchExternalId),
home_score: homeScore,
away_score: awayScore,
status
})
});
if (!res.ok) {
let message = "Не удалось сохранить счёт / статус";
try {
const data = await res.json();
message = data.message || data.error || message;
} catch (_) {}
alert(message);
return;
}
window.location.reload();
}
function openClearMatchModal() {
const modal = document.getElementById("clearMatchModal");
if (modal) modal.classList.add("show");
@@ -4078,3 +4189,206 @@ function confirmClearMatch() {
closeClearMatchModal();
clearMatchEvents();
}
// -------------------- Пенальти Кубка России --------------------
let penaltyState = null;
let penaltyRequestInFlight = false;
function penaltyResultMark(result) {
if (result === "scored") return "●";
if (result === "missed") return "×";
return "";
}
function penaltyResultTitle(result) {
if (result === "scored") return "Забил";
if (result === "missed") return "Не забил";
return "Не выбран";
}
function normalizePenaltyRounds(state) {
const maxRounds = Math.max(5, Number(state?.max_rounds || 5));
const inputRounds = Array.isArray(state?.rounds) ? state.rounds : [];
const byNumber = new Map(inputRounds.map(row => [Number(row.number), row]));
const rounds = [];
for (let number = 1; number <= maxRounds; number += 1) {
const row = byNumber.get(number) || { number, home: "", away: "" };
rounds.push({
number,
home: row.home || "",
away: row.away || ""
});
}
return rounds;
}
function renderPenaltyShootout(state) {
penaltyState = state || penaltyState || { max_rounds: 5, rounds: [], totals: { home: 0, away: 0 } };
penaltyState.rounds = normalizePenaltyRounds(penaltyState);
const homeScore = document.getElementById("penaltyHomeScore");
const awayScore = document.getElementById("penaltyAwayScore");
const homeNext = document.getElementById("penaltyHomeNext");
const awayNext = document.getElementById("penaltyAwayNext");
const body = document.getElementById("penaltyRoundsBody");
if (homeScore) homeScore.textContent = penaltyState?.totals?.home ?? 0;
if (awayScore) awayScore.textContent = penaltyState?.totals?.away ?? 0;
if (homeNext) homeNext.textContent = `Следующий удар: ${getNextPenaltyShotNumber("home")}`;
if (awayNext) awayNext.textContent = `Следующий удар: ${getNextPenaltyShotNumber("away")}`;
if (!body) return;
body.innerHTML = penaltyState.rounds.map(row => {
return `
<tr>
<td class="penalty-round-number">${row.number}</td>
<td>${renderPenaltyCell("home", row.number, row.home)}</td>
<td>${renderPenaltyCell("away", row.number, row.away)}</td>
</tr>
`;
}).join("");
}
function renderPenaltyCell(side, shotNumber, result) {
const safeSide = side === "away" ? "away" : "home";
const currentClass = result ? ` ${result}` : " empty";
const scoredActive = result === "scored" ? " active" : "";
const missedActive = result === "missed" ? " active" : "";
const clearDisabled = result ? "" : " disabled";
const mark = penaltyResultMark(result) || "—";
return `
<div class="penalty-cell${currentClass}">
<div class="penalty-mark" title="${escapeHtml(penaltyResultTitle(result))}">${escapeHtml(mark)}</div>
<div class="penalty-cell-actions">
<button type="button" class="penalty-mini-btn scored${scoredActive}" onclick="setPenaltyShot('${safeSide}', ${shotNumber}, 'scored')">Забил</button>
<button type="button" class="penalty-mini-btn missed${missedActive}" onclick="setPenaltyShot('${safeSide}', ${shotNumber}, 'missed')">Не забил</button>
<button type="button" class="penalty-mini-btn clear" onclick="setPenaltyShot('${safeSide}', ${shotNumber}, 'clear')"${clearDisabled}>Очистить</button>
</div>
</div>
`;
}
async function loadPenaltyShootout() {
if (!SESSION_TOKEN || !MATCH_DATA.isRussianCup) return;
const body = document.getElementById("penaltyRoundsBody");
if (!body) return;
try {
const res = await fetch(`/admin/session/${SESSION_TOKEN}/penalties`, { cache: "no-store" });
if (!res.ok) throw new Error("penalty_load_failed");
const data = await res.json();
renderPenaltyShootout(data);
} catch (err) {
console.error("Не удалось загрузить пенальти", err);
body.innerHTML = `<tr><td colspan="3" class="empty-box">Не удалось загрузить серию пенальти.</td></tr>`;
}
}
async function setPenaltyShot(side, shotNumber, result) {
if (!SESSION_TOKEN || penaltyRequestInFlight) return;
penaltyRequestInFlight = true;
try {
const res = await fetch(`/admin/session/${SESSION_TOKEN}/penalties/shot`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ side, shot_number: shotNumber, result })
});
if (!res.ok) throw new Error("penalty_save_failed");
const data = await res.json();
renderPenaltyShootout(data);
} catch (err) {
console.error("Не удалось сохранить пенальти", err);
alert("Не удалось сохранить пенальти");
} finally {
penaltyRequestInFlight = false;
}
}
function getNextPenaltyShotNumber(side) {
const safeSide = side === "away" ? "away" : "home";
const rounds = normalizePenaltyRounds(penaltyState || { max_rounds: 5, rounds: [] });
const emptyRow = rounds.find(row => !row[safeSide]);
if (emptyRow) return emptyRow.number;
return rounds.length + 1;
}
async function setNextPenaltyShot(side, result) {
const nextNumber = getNextPenaltyShotNumber(side);
const currentMax = Math.max(5, Number(penaltyState?.max_rounds || 5));
if (nextNumber > currentMax) {
await addPenaltyRound(false);
}
await setPenaltyShot(side, nextNumber, result);
}
async function addPenaltyRound(showAlert = true) {
if (!SESSION_TOKEN || penaltyRequestInFlight) return;
penaltyRequestInFlight = true;
try {
const res = await fetch(`/admin/session/${SESSION_TOKEN}/penalties/round`, { method: "POST" });
if (!res.ok) throw new Error("penalty_round_failed");
const data = await res.json();
renderPenaltyShootout(data);
} catch (err) {
console.error("Не удалось добавить серию пенальти", err);
if (showAlert) alert("Не удалось добавить серию пенальти");
} finally {
penaltyRequestInFlight = false;
}
}
async function deleteLastPenaltyRound() {
if (!SESSION_TOKEN || penaltyRequestInFlight) return;
const currentMax = Math.max(5, Number(penaltyState?.max_rounds || 5));
if (currentMax <= 5) {
alert("Первые 5 серий нельзя удалить. Можно только очистить данные.");
return;
}
if (!confirm(`Удалить последнюю серию №${currentMax}? Данные ударов этой серии тоже будут удалены.`)) return;
penaltyRequestInFlight = true;
try {
const res = await fetch(`/admin/session/${SESSION_TOKEN}/penalties/round`, { method: "DELETE" });
if (!res.ok) throw new Error("penalty_delete_round_failed");
const data = await res.json();
renderPenaltyShootout(data);
} catch (err) {
console.error("Не удалось удалить последнюю серию пенальти", err);
alert("Не удалось удалить последнюю серию пенальти");
} finally {
penaltyRequestInFlight = false;
}
}
async function clearPenaltyShootout() {
if (!SESSION_TOKEN || penaltyRequestInFlight) return;
if (!confirm("Очистить всю серию пенальти для этого матча?")) return;
penaltyRequestInFlight = true;
try {
const res = await fetch(`/admin/session/${SESSION_TOKEN}/penalties`, { method: "DELETE" });
if (!res.ok) throw new Error("penalty_clear_failed");
const data = await res.json();
renderPenaltyShootout(data);
} catch (err) {
console.error("Не удалось очистить пенальти", err);
alert("Не удалось очистить пенальти");
} finally {
penaltyRequestInFlight = false;
}
}
document.addEventListener("DOMContentLoaded", () => {
if (document.getElementById("penaltyPanel")) {
loadPenaltyShootout();
}
});