Files
duty-teller/webapp/app.js
Nikolay Tatarinov 7cdf1edc34 Enhance API access control and update Docker configuration
- Added port mapping to docker-compose for local development.
- Modified the API to allow access from localhost without Telegram initData for local development.
- Updated the web application to check for localhost before denying access based on initData.
2026-02-17 14:15:06 +03:00

192 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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");
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() {
return (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData) || "";
}
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;
}
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;
const res = await fetch(url, { headers: headers });
if (res.status === 403) {
throw new Error("ACCESS_DENIED");
}
if (!res.ok) throw new Error("Ошибка загрузки");
return res.json();
}
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 =
"<span class=\"num\">" + d.getDate() + "</span>" +
(dayDuties.length ? "<span class=\"day-duties\">" + dayDuties.map(function (x) { return x.full_name; }).join(", ") + "</span>" : "");
calendarEl.appendChild(cell);
d.setDate(d.getDate() + 1);
}
monthTitleEl.textContent = MONTHS[month] + " " + year;
}
function renderDutyList(duties) {
if (duties.length === 0) {
dutyListEl.innerHTML = "<p class=\"muted\">В этом месяце дежурств нет.</p>";
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 += "<h2>" + date + "</h2>";
list.forEach(function (d) {
const start = d.start_at.slice(11, 16);
const end = d.end_at.slice(11, 16);
html += "<div class=\"duty-item\"><span class=\"name\">" + escapeHtml(d.full_name) + "</span><div class=\"time\">" + start + " " + end + "</div></div>";
});
});
dutyListEl.innerHTML = html;
}
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = s;
return div.innerHTML;
}
function dutiesByDate(duties) {
const byDate = {};
duties.forEach(function (d) {
const start = new Date(d.start_at);
const end = new Date(d.end_at);
for (let t = new Date(start); t <= end; t.setDate(t.getDate() + 1)) {
const key = isoDate(t);
if (!byDate[key]) byDate[key] = [];
byDate[key].push(d);
}
});
return byDate;
}
function showError(msg) {
errorEl.textContent = msg;
errorEl.hidden = false;
loadingEl.classList.add("hidden");
}
async function loadMonth() {
var _initData = getInitData();
if (!_initData && !isLocalhost()) {
showAccessDenied();
return;
}
hideAccessDenied();
loadingEl.classList.remove("hidden");
errorEl.hidden = true;
const from = isoDate(firstDayOfMonth(current));
const to = isoDate(lastDayOfMonth(current));
try {
const duties = await fetchDuties(from, to);
const byDate = dutiesByDate(duties);
renderCalendar(current.getFullYear(), current.getMonth(), byDate);
renderDutyList(duties);
} catch (e) {
if (e.message === "ACCESS_DENIED") {
showAccessDenied();
return;
}
showError(e.message || "Не удалось загрузить данные.");
return;
}
loadingEl.classList.add("hidden");
}
document.getElementById("prevMonth").addEventListener("click", function () {
current.setMonth(current.getMonth() - 1);
loadMonth();
});
document.getElementById("nextMonth").addEventListener("click", function () {
current.setMonth(current.getMonth() + 1);
loadMonth();
});
loadMonth();
})();