(() => { "use strict"; const boot = window.UI_BUILDER_BOOT || {}; if (boot.mode !== "runtime") return; const state = { root: null, signature: "", timerId: "hockey_game_timer", dashboardId: "hockey_penalty_dashboard", interval: null, resizeObserver: null, control: null, controlGameId: "", controlLoading: false, controlFetchedAt: 0, periodEditing: false, }; const escapeHtml = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); function formatDate(value, language) { const raw = String(value || "").trim(); if (!raw) return "Дата не указана"; const parsed = new Date(`${raw}T12:00:00`); if (Number.isNaN(parsed.getTime())) return raw; return parsed.toLocaleDateString(language === "en" ? "en-GB" : "ru-RU", { day: "2-digit", month: "short", year: "numeric", }); } function formatPenaltyTime(milliseconds) { const totalSeconds = Math.max(0, Math.ceil(Number(milliseconds || 0) / 1000)); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}:${String(seconds).padStart(2, "0")}`; } function discoverActionIds() { const components = window.UIBuilderRuntime?.getConfig?.()?.components || []; const timer = components.find((item) => item.action_id === "hockey_game_timer") || components.find((item) => item.type === "timer"); const dashboard = components.find((item) => item.type === "hockey_penalty_dashboard"); state.timerId = timer?.action_id || "hockey_game_timer"; state.dashboardId = dashboard?.action_id || "hockey_penalty_dashboard"; } function snapshot() { const data = window.UIBuilderRuntime?.getData?.() || {}; const game = data.hockey?.selected_game || null; const tournament = data.hockey?.selected_tournament || null; const language = data.hockey?.language?.display || localStorage.getItem("hockey.language") || "ru"; const timer = window.UIBuilderRuntime?.getTimer?.(state.timerId) || null; const board = window.UIBuilderRuntime?.getHockeyPenaltyBoard?.(state.dashboardId) || {}; const penalties = (Array.isArray(board.penalties) ? board.penalties : []) .filter((event) => ( event && !event.finished && (event.player || (event.teamPenalty && ["home", "away"].includes(event.side))) && event.infraction && event.preset )) .sort((left, right) => { const sideOrder = { home: 0, away: 1 }; return (sideOrder[left.player?.side || left.side] ?? 2) - (sideOrder[right.player?.side || right.side] ?? 2) || Number(left.createdAt || 0) - Number(right.createdAt || 0); }); return { data, game, tournament, language, timer, penalties, control: state.control }; } async function controlRequest(path, options = {}) { const response = await fetch(`/api/hockey${path}`, { cache: "no-store", credentials: "same-origin", ...options, headers: { ...(options.body ? { "Content-Type": "application/json" } : {}), ...(options.headers || {}), }, }); let payload = {}; try { payload = await response.json(); } catch (_) {} if (!response.ok) { const detail = typeof payload.detail === "string" ? payload.detail : `HTTP ${response.status}`; throw new Error(detail); } return payload; } async function loadControl(force = false) { const data = window.UIBuilderRuntime?.getData?.() || {}; const gameId = String(data.hockey?.selected_game?.external_id || data.hockey?.selected_game?.id || "").trim(); const language = data.hockey?.language?.display === "en" ? "en" : "ru"; if (!gameId) { state.control = null; state.controlGameId = ""; return; } if (state.controlGameId !== gameId) { state.control = null; state.controlGameId = gameId; force = true; } if (state.controlLoading) return; if (!force && Date.now() - state.controlFetchedAt < 1800) return; state.controlLoading = true; try { const previousPeriod = String(state.control?.current_period || ""); state.control = await controlRequest(`/games/${encodeURIComponent(gameId)}/control?language=${language}`); state.controlFetchedAt = Date.now(); const nextPeriod = String(state.control?.current_period || ""); if (previousPeriod && nextPeriod && previousPeriod !== nextPeriod) { window.UIBuilderRuntime?.patchData?.({ hockey: { game_control: state.control } }); window.dispatchEvent(new CustomEvent("hockey:game-control-updated", { detail: { game_id: gameId, control: state.control, apply_timers: true }, })); } render(true); } catch (error) { console.error("Could not load hockey period state", error); } finally { state.controlLoading = false; } } async function updatePeriod(gameId, value, language) { try { state.control = await controlRequest(`/games/${encodeURIComponent(gameId)}/control/period`, { method: "PUT", body: JSON.stringify({ current_period: value, language }), }); state.controlFetchedAt = Date.now(); window.UIBuilderRuntime?.patchData?.({ hockey: { game_control: state.control } }); window.dispatchEvent(new CustomEvent("hockey:game-control-updated", { detail: { game_id: gameId, control: state.control, apply_timers: true }, })); render(true); } catch (error) { console.error("Could not update hockey period", error); window.alert(error.message || "Не удалось изменить период"); await loadControl(true); } } function timerStatus(timer, language) { if (!timer) return language === "en" ? "not configured" : "не настроен"; if (timer.finished) return language === "en" ? "finished" : "завершён"; if (timer.running) return language === "en" ? "running" : "идёт"; if (timer.paused) return language === "en" ? "paused" : "пауза"; return language === "en" ? "stopped" : "остановлен"; } function penaltyMarkup(event, language, teamNames = {}) { const side = event.player?.side || event.side || "neutral"; const teamPenalty = Boolean(event.teamPenalty); const number = String(event.player?.number || "").replace(/^#/, ""); const playerName = teamPenalty ? (teamNames[side] || (language === "en" ? "Team penalty" : "Командное удаление")) : (event.player?.name || (language === "en" ? "Player" : "Игрок")); const infraction = event.infraction?.label || event.infraction?.name || event.infraction?.id || ""; const accessibleLabel = [playerName, infraction, formatPenaltyTime(event.remainingMs)] .filter(Boolean) .join(" · "); return `
${teamPenalty ? (language === "en" ? "TEAM" : "КОМ") : number ? `#${escapeHtml(number)}` : "—"}
`; } function render(force = false) { if (!state.root || !window.UIBuilderRuntime) return; // The header refreshes frequently because the timer changes. Replacing the // ${periodOptions.map((item) => ``).join("")}
${language === "en" ? "MAIN TIMER" : "ОСНОВНОЙ ТАЙМЕР"} ${escapeHtml(clock)} ${escapeHtml(timerStatus(value.timer, language))}
${strengthMarkup} ${penalties}
`; const currentPeriodSelect = state.root.querySelector("[data-hockey-current-period]"); currentPeriodSelect?.addEventListener("pointerdown", () => { state.periodEditing = true; }); currentPeriodSelect?.addEventListener("focus", () => { state.periodEditing = true; }); currentPeriodSelect?.addEventListener("change", (event) => { if (!game?.external_id) return; updatePeriod(String(game.external_id), event.target.value, language); }); currentPeriodSelect?.addEventListener("blur", () => { state.periodEditing = false; window.setTimeout(() => render(true), 0); }); } function mount() { if (state.root) return; const topbar = document.querySelector(".runtime-topbar"); if (!topbar) return; const root = document.createElement("section"); root.id = "hockeyMatchStatusHeader"; root.className = "hockey-match-status"; root.setAttribute("aria-label", "Краткое состояние матча"); root.tabIndex = 0; root.title = "Нажмите, чтобы выбрать турнир или матч"; root.addEventListener("click", (event) => { if (!event.target.closest(".hockey-status-match")) return; document.querySelector(".runtime-actions .hockey-selected-chip")?.click(); }); root.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); document.querySelector(".runtime-actions .hockey-selected-chip")?.click(); }); topbar.appendChild(root); state.root = root; discoverActionIds(); render(true); if (typeof ResizeObserver === "function") { state.resizeObserver = new ResizeObserver(() => { window.UIBuilderRuntime?.refreshLayout?.(); }); state.resizeObserver.observe(topbar); } requestAnimationFrame(() => window.UIBuilderRuntime?.refreshLayout?.()); state.interval = window.setInterval(() => { if (!document.hidden) { render(); loadControl(false); } }, 250); } window.addEventListener("hockey:game-selected", () => { loadControl(true); render(true); }); window.addEventListener("hockey:settings-updated", () => { loadControl(true); render(true); }); window.addEventListener("hockey:game-control-updated", (event) => { const payload = event.detail?.control; const gameId = String(event.detail?.game_id || payload?.game_id || ""); if (!payload || (state.controlGameId && gameId && state.controlGameId !== gameId)) return; state.control = payload; state.controlFetchedAt = Date.now(); render(true); }); window.addEventListener("ui-builder:data-patched", () => render(true)); document.addEventListener("visibilitychange", () => { if (!document.hidden) render(true); }); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", mount, { once: true }); } else { mount(); } loadControl(true); })();