refactor: restructure web application with modular JavaScript and remove legacy code

- Deleted the `app.js` file and migrated its functionality to a modular structure with multiple JavaScript files for better organization and maintainability.
- Updated `index.html` to reference the new `main.js` module, ensuring proper loading of the application.
- Introduced new utility modules for API requests, authentication, calendar handling, and DOM manipulation to enhance code clarity and separation of concerns.
- Enhanced CSS styles for improved layout and theming consistency across the application.
- Added comprehensive comments and documentation to new modules to facilitate future development and understanding.
This commit is contained in:
2026-02-19 15:24:52 +03:00
parent 6d91274a4e
commit c9cf86a8f6
15 changed files with 1432 additions and 921 deletions

157
webapp/js/calendar.js Normal file
View File

@@ -0,0 +1,157 @@
/**
* Calendar grid and events-by-date mapping.
*/
import { calendarEl, monthTitleEl } from "./dom.js";
import { MONTHS } from "./constants.js";
import { escapeHtml } from "./utils.js";
import {
localDateString,
firstDayOfMonth,
lastDayOfMonth,
getMonday
} from "./dateUtils.js";
import { bindInfoButtonTooltips } from "./hints.js";
import { bindDutyMarkerTooltips } from "./hints.js";
/**
* Build { localDateKey -> array of summary strings }. API date is UTC YYYY-MM-DD; map to local.
* @param {object[]} events - Calendar events with date, summary
* @returns {Record<string, string[]>}
*/
export function calendarEventsByDate(events) {
const byDate = {};
(events || []).forEach((e) => {
const utcMidnight = new Date(e.date + "T00:00:00Z");
const key = localDateString(utcMidnight);
if (!byDate[key]) byDate[key] = [];
if (e.summary) byDate[key].push(e.summary);
});
return byDate;
}
/**
* Group duties by local date (start_at/end_at are UTC).
* @param {object[]} duties - Duties with start_at, end_at
* @returns {Record<string, object[]>}
*/
export function dutiesByDate(duties) {
const byDate = {};
duties.forEach((d) => {
const start = new Date(d.start_at);
const end = new Date(d.end_at);
const endLocal = localDateString(end);
let t = new Date(start);
while (true) {
const key = localDateString(t);
if (!byDate[key]) byDate[key] = [];
byDate[key].push(d);
if (key === endLocal) break;
t.setDate(t.getDate() + 1);
}
});
return byDate;
}
/**
* Render calendar grid for given month with duties and calendar events.
* @param {number} year
* @param {number} month - 0-based
* @param {Record<string, object[]>} dutiesByDateMap
* @param {Record<string, string[]>} calendarEventsByDateMap
*/
export function renderCalendar(
year,
month,
dutiesByDateMap,
calendarEventsByDateMap
) {
if (!calendarEl || !monthTitleEl) return;
const first = firstDayOfMonth(new Date(year, month, 1));
const last = lastDayOfMonth(new Date(year, month, 1));
const start = getMonday(first);
const today = localDateString(new Date());
const calendarEventsByDate = calendarEventsByDateMap || {};
calendarEl.innerHTML = "";
let d = new Date(start);
const cells = 42;
for (let i = 0; i < cells; i++) {
const key = localDateString(d);
const isOther = d.getMonth() !== month;
const dayDuties = dutiesByDateMap[key] || [];
const dutyList = dayDuties.filter((x) => x.event_type === "duty");
const unavailableList = dayDuties.filter((x) => x.event_type === "unavailable");
const vacationList = dayDuties.filter((x) => x.event_type === "vacation");
const hasAny = dayDuties.length > 0;
const isToday = key === today;
const eventSummaries = calendarEventsByDate[key] || [];
const hasEvent = eventSummaries.length > 0;
const showMarkers = !isOther;
const cell = document.createElement("div");
cell.className =
"day" +
(isOther ? " other-month" : "") +
(isToday ? " today" : "") +
(showMarkers && hasAny ? " has-duty" : "") +
(showMarkers && hasEvent ? " holiday" : "");
const namesAttr = (list) =>
list.length
? escapeHtml(list.map((x) => x.full_name).join("\n"))
: "";
const dutyItemsJson = dutyList.length
? JSON.stringify(
dutyList.map((x) => ({
full_name: x.full_name,
start_at: x.start_at,
end_at: x.end_at
}))
).replace(/'/g, "&#39;")
: "";
let html =
'<span class="num">' +
d.getDate() +
'</span><div class="day-markers">';
if (showMarkers) {
if (dutyList.length) {
html +=
'<button type="button" class="duty-marker" data-event-type="duty" data-date="' +
escapeHtml(key) +
'" data-names="' +
namesAttr(dutyList) +
'" data-duty-items=\'' +
dutyItemsJson +
"' aria-label=\"Дежурные\">Д</button>";
}
if (unavailableList.length) {
html +=
'<button type="button" class="unavailable-marker" data-event-type="unavailable" data-names="' +
namesAttr(unavailableList) +
'" aria-label="Недоступен">Н</button>';
}
if (vacationList.length) {
html +=
'<button type="button" class="vacation-marker" data-event-type="vacation" data-names="' +
namesAttr(vacationList) +
'" aria-label="Отпуск">О</button>';
}
if (hasEvent) {
html +=
'<button type="button" class="info-btn" aria-label="Информация о дне" data-summary="' +
escapeHtml(eventSummaries.join("\n")) +
'">i</button>';
}
}
html += "</div>";
cell.innerHTML = html;
calendarEl.appendChild(cell);
d.setDate(d.getDate() + 1);
}
monthTitleEl.textContent = MONTHS[month] + " " + year;
bindInfoButtonTooltips();
bindDutyMarkerTooltips();
}