Files
duty-teller/webapp/js/calendar.js
Nikolay Tatarinov 4c2d95e776
All checks were successful
CI / lint-and-test (push) Successful in 14s
feat: implement internationalization for duty calendar
- Introduced a new `i18n.js` module for handling translations and language detection, supporting both Russian and English.
- Updated various components to utilize the new translation functions, enhancing user experience by providing localized messages for errors, hints, and UI elements.
- Refactored existing code in `api.js`, `calendar.js`, `dutyList.js`, `hints.js`, and `ui.js` to replace hardcoded strings with translated messages, improving maintainability and accessibility.
- Enhanced the initialization process in `main.js` to set the document language and update UI elements based on the user's language preference.
2026-02-19 16:00:00 +03:00

159 lines
5.0 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, state } from "./dom.js";
import { monthName, t } from "./i18n.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) {
const lang = state.lang;
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=\"" + escapeHtml(t(lang, "aria.duty")) + "\">Д</button>";
}
if (unavailableList.length) {
html +=
'<button type="button" class="unavailable-marker" data-event-type="unavailable" data-names="' +
namesAttr(unavailableList) +
'" aria-label="' + escapeHtml(t(lang, "aria.unavailable")) + '">Н</button>';
}
if (vacationList.length) {
html +=
'<button type="button" class="vacation-marker" data-event-type="vacation" data-names="' +
namesAttr(vacationList) +
'" aria-label="' + escapeHtml(t(lang, "aria.vacation")) + '">О</button>';
}
if (hasEvent) {
html +=
'<button type="button" class="info-btn" aria-label="' + escapeHtml(t(lang, "aria.day_info")) + '" data-summary="' +
escapeHtml(eventSummaries.join("\n")) +
'">i</button>';
}
}
html += "</div>";
cell.innerHTML = html;
calendarEl.appendChild(cell);
d.setDate(d.getDate() + 1);
}
monthTitleEl.textContent = monthName(state.lang, month) + " " + year;
bindInfoButtonTooltips();
bindDutyMarkerTooltips();
}