/* global flatpickr, Chart */ (function () { function qs(sel) { return document.querySelector(sel); } function isoFromDate(d) { // d is Date const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; } function nearestSunday(d) { // JS Date: Sunday=0 ... Saturday=6 const day = d.getDay(); const delta = day === 0 ? 0 : day; // go back to Sunday const out = new Date(d); out.setDate(d.getDate() - delta); out.setHours(12, 0, 0, 0); return out; } function initDatePicker() { const el = qs("#datePicker"); if (!el || typeof flatpickr === "undefined") return; flatpickr.localize(flatpickr.l10ns.ru); flatpickr(el, { dateFormat: "Y-m-d", defaultDate: el.value || null, disableMobile: false, disable: [ function (date) { // allow only Sundays (0) return date.getDay() !== 0; }, ], onChange: function (selectedDates) { if (!selectedDates || !selectedDates.length) return; const sunday = nearestSunday(selectedDates[0]); const iso = isoFromDate(sunday); const url = new URL(window.location.href); url.searchParams.set("date", iso); // сохраняем текущий view графика, если есть if (!url.searchParams.get("view") && window.__VOLEY__ && window.__VOLEY__.chartView) { url.searchParams.set("view", window.__VOLEY__.chartView); } window.location.href = url.toString(); }, }); } async function handleAttendanceSubmit(ev) { const form = ev.target.closest("form.attendance-form"); if (!form) return; ev.preventDefault(); const fd = new FormData(form); const row = form.closest(".player-row"); if (!row) return; // небольшая блокировка кнопки const btn = form.querySelector("button[type='submit']"); if (btn) { btn.disabled = true; btn.classList.add("opacity-70"); } try { const resp = await fetch(form.action, { method: "POST", body: fd }); if (!resp.ok) throw new Error("network"); const html = await resp.text(); // заменяем целиком строку игрока const tmp = document.createElement("div"); tmp.innerHTML = html; const newRow = tmp.firstElementChild; if (newRow) row.replaceWith(newRow); // после отметки обновим команды (если блок есть) refreshTeams(); } catch (e) { alert("Не удалось сохранить отметку. Попробуйте ещё раз."); if (btn) { btn.disabled = false; btn.classList.remove("opacity-70"); } } } async function refreshTeams() { const box = qs("#teamsBox"); if (!box || !window.__VOLEY__ || !window.__VOLEY__.selectedDate) return; try { const fd = new FormData(); fd.set("session_date", window.__VOLEY__.selectedDate); const resp = await fetch("/api/teams/random", { method: "POST", body: fd }); if (!resp.ok) return; box.innerHTML = await resp.text(); } catch (_e) { // молча } } function initAttendance() { document.addEventListener("submit", function (ev) { const t = ev.target; if (t && t.matches && t.matches("form.attendance-form")) { handleAttendanceSubmit(ev); } }); } function initTeams() { document.addEventListener("submit", function (ev) { const t = ev.target; if (t && t.matches && t.matches("form.teams-form")) { ev.preventDefault(); const fd = new FormData(t); // защита от отсутствующего hidden input (или пустого значения) if ((!fd.get("session_date") || String(fd.get("session_date")).trim() === "") && window.__VOLEY__ && window.__VOLEY__.selectedDate) { fd.set("session_date", window.__VOLEY__.selectedDate); } fetch(t.action, { method: "POST", body: fd }) .then((r) => r.text()) .then((html) => { const box = qs("#teamsBox"); if (box) box.innerHTML = html; }) .catch(function () {}); } }); } function initChart() { const canvas = qs("#attendanceChart"); if (!canvas || !window.__VOLEY__) return; const labels = window.__VOLEY__.chartLabels || []; const values = window.__VOLEY__.chartValues || []; const ctx = canvas.getContext("2d"); function formatLabel(s) { // month: YYYY-MM-DD -> DD.MM // year: YYYY-MM -> MM if (typeof s !== "string") return String(s); if (s.length === 10 && s[4] === "-" && s[7] === "-") return s.slice(8, 10) + "." + s.slice(5, 7); if (s.length === 7 && s[4] === "-") return s.slice(5, 7); return s; } function renderVanillaBarChart() { // Простая отрисовка без зависимостей (если CDN недоступен) const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); const w = Math.max(240, Math.floor(rect.width)); const h = Math.max(160, Math.floor(rect.height)); canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr); canvas.style.width = w + "px"; canvas.style.height = h + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // clear ctx.clearRect(0, 0, w, h); // palette const grid = "rgba(148, 163, 184, 0.15)"; const text = "rgba(226, 232, 240, 0.85)"; const bar = "rgba(99, 102, 241, 0.78)"; const barStroke = "rgba(99, 102, 241, 1)"; const padL = 40; const padR = 10; const padT = 10; const padB = 28; const plotW = w - padL - padR; const plotH = h - padT - padB; const maxV = Math.max(1, ...values.map((x) => (Number.isFinite(+x) ? +x : 0))); // grid lines + y labels (0..max) ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Arial"; ctx.fillStyle = text; ctx.strokeStyle = grid; ctx.lineWidth = 1; const steps = Math.min(5, maxV); for (let i = 0; i <= steps; i++) { const yVal = Math.round((maxV * i) / steps); const y = padT + plotH - (plotH * i) / steps; ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(w - padR, y); ctx.stroke(); ctx.fillText(String(yVal), 6, y + 4); } const n = Math.max(0, labels.length); if (n === 0) { ctx.fillText("Нет данных для графика", padL, padT + 18); return; } const gap = Math.min(10, Math.max(4, Math.floor(plotW / (n * 6)))); const barW = Math.max(6, Math.floor((plotW - gap * (n - 1)) / n)); for (let i = 0; i < n; i++) { const v = Number.isFinite(+values[i]) ? +values[i] : 0; const bh = Math.round((v / maxV) * plotH); const x = padL + i * (barW + gap); const y = padT + plotH - bh; // bar ctx.fillStyle = bar; ctx.strokeStyle = barStroke; ctx.beginPath(); ctx.rect(x, y, barW, bh); ctx.fill(); ctx.stroke(); // x label (every Nth to avoid overlap) const every = n <= 8 ? 1 : n <= 16 ? 2 : 3; if (i % every === 0) { ctx.fillStyle = text; const lab = formatLabel(labels[i]); ctx.fillText(lab, x, h - 10); } } } // Пытаемся Chart.js, но если не загрузился — рисуем fallback. try { if (typeof Chart === "undefined") { renderVanillaBarChart(); } else { // eslint-disable-next-line no-new new Chart(ctx, { type: "bar", data: { labels: labels, datasets: [ { label: "Посещений", data: values, borderWidth: 1, backgroundColor: "rgba(99, 102, 241, 0.75)", borderColor: "rgba(99, 102, 241, 1)", }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { enabled: true }, }, scales: { x: { ticks: { color: "rgba(226, 232, 240, 0.8)" }, grid: { color: "rgba(148, 163, 184, 0.15)" }, }, y: { beginAtZero: true, ticks: { color: "rgba(226, 232, 240, 0.8)", precision: 0 }, grid: { color: "rgba(148, 163, 184, 0.15)" }, }, }, }, }); } } catch (e) { renderVanillaBarChart(); } // на ресайзе перерисуем только fallback (Chart.js сам умеет) if (typeof Chart === "undefined") { window.addEventListener("resize", function () { renderVanillaBarChart(); }); } } initDatePicker(); initAttendance(); initTeams(); initChart(); })();