(function () { const MONTHS = [ "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь" ]; let current = new Date(); const calendarEl = document.getElementById("calendar"); const monthTitleEl = document.getElementById("monthTitle"); const dutyListEl = document.getElementById("dutyList"); const loadingEl = document.getElementById("loading"); const errorEl = document.getElementById("error"); const accessDeniedEl = document.getElementById("accessDenied"); const headerEl = document.querySelector(".header"); const weekdaysEl = document.querySelector(".weekdays"); const prevBtn = document.getElementById("prevMonth"); const nextBtn = document.getElementById("nextMonth"); function isoDate(d) { return d.toISOString().slice(0, 10); } function firstDayOfMonth(d) { return new Date(d.getFullYear(), d.getMonth(), 1); } function lastDayOfMonth(d) { return new Date(d.getFullYear(), d.getMonth() + 1, 0); } function getMonday(d) { const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); return new Date(d.getFullYear(), d.getMonth(), diff); } function getInitData() { var fromSdk = (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData) || ""; if (fromSdk) return fromSdk; var hash = window.location.hash ? window.location.hash.slice(1) : ""; if (hash.indexOf("tgWebAppData=") === 0) { try { return decodeURIComponent(hash.substring("tgWebAppData=".length)); } catch (e) { return hash.substring("tgWebAppData=".length); } } var q = window.location.search ? new URLSearchParams(window.location.search).get("tgWebAppData") : null; if (q) { try { return decodeURIComponent(q); } catch (e) { return q; } } return ""; } function getInitDataDebug() { var sdk = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData); var hash = window.location.hash ? window.location.hash.slice(1) : ""; var hashHasData = hash.indexOf("tgWebAppData=") === 0; var queryHasData = !!(window.location.search && new URLSearchParams(window.location.search).get("tgWebAppData")); return "SDK: " + (sdk ? "да" : "нет") + ", hash: " + (hashHasData ? hash.length + " симв." : "нет") + ", query: " + (queryHasData ? "да" : "нет"); } function isLocalhost() { var h = window.location.hostname; return h === "localhost" || h === "127.0.0.1" || h === ""; } function showAccessDenied() { if (headerEl) headerEl.hidden = true; if (weekdaysEl) weekdaysEl.hidden = true; calendarEl.hidden = true; dutyListEl.hidden = true; loadingEl.classList.add("hidden"); errorEl.hidden = true; accessDeniedEl.hidden = false; var debugEl = document.getElementById("accessDeniedDebug"); if (debugEl) debugEl.textContent = getInitDataDebug(); } function hideAccessDenied() { accessDeniedEl.hidden = true; if (headerEl) headerEl.hidden = false; if (weekdaysEl) weekdaysEl.hidden = false; calendarEl.hidden = false; dutyListEl.hidden = false; } async function fetchDuties(from, to) { const base = window.location.origin; const url = base + "/api/duties?from=" + encodeURIComponent(from) + "&to=" + encodeURIComponent(to); const initData = getInitData(); const headers = {}; if (initData) headers["X-Telegram-Init-Data"] = initData; var controller = new AbortController(); var timeoutId = setTimeout(function () { controller.abort(); }, 15000); try { var res = await fetch(url, { headers: headers, signal: controller.signal }); clearTimeout(timeoutId); if (res.status === 403) { throw new Error("ACCESS_DENIED"); } if (!res.ok) throw new Error("Ошибка загрузки"); return res.json(); } catch (e) { clearTimeout(timeoutId); if (e.name === "AbortError") { throw new Error("Не удалось загрузить данные. Проверьте интернет."); } throw e; } } function renderCalendar(year, month, dutiesByDate) { const first = firstDayOfMonth(new Date(year, month, 1)); const last = lastDayOfMonth(new Date(year, month, 1)); const start = getMonday(first); const today = isoDate(new Date()); calendarEl.innerHTML = ""; let d = new Date(start); const cells = 42; for (let i = 0; i < cells; i++) { const key = isoDate(d); const isOther = d.getMonth() !== month; const dayDuties = dutiesByDate[key] || []; const isToday = key === today; const cell = document.createElement("div"); cell.className = "day" + (isOther ? " other-month" : "") + (isToday ? " today" : "") + (dayDuties.length ? " has-duty" : ""); cell.innerHTML = "" + d.getDate() + "" + (dayDuties.length ? "" + dayDuties.map(function (x) { return escapeHtml(x.full_name); }).join(", ") + "" : ""); calendarEl.appendChild(cell); d.setDate(d.getDate() + 1); } monthTitleEl.textContent = MONTHS[month] + " " + year; } function renderDutyList(duties) { if (duties.length === 0) { dutyListEl.innerHTML = "
В этом месяце дежурств нет.
"; return; } const grouped = {}; duties.forEach(function (d) { const date = d.start_at.slice(0, 10); if (!grouped[date]) grouped[date] = []; grouped[date].push(d); }); const dates = Object.keys(grouped).sort(); let html = ""; dates.forEach(function (date) { const list = grouped[date]; html += "