- 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.
158 lines
4.9 KiB
JavaScript
158 lines
4.9 KiB
JavaScript
/**
|
||
* 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, "'")
|
||
: "";
|
||
|
||
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();
|
||
}
|