все остальные апдейты на Кубок России
This commit is contained in:
314
static/script.js
314
static/script.js
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user