Files
duty-teller/webapp/js/calendar.js
Nikolay Tatarinov c9cf86a8f6 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.
2026-02-19 15:24:52 +03:00

158 lines
4.9 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.

/**
* 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();
}