feat: implement day detail panel for calendar

- Introduced a new `dayDetail.js` module to manage the day detail panel functionality, allowing users to view detailed information about duties and events for each calendar day.
- Enhanced the calendar rendering in `calendar.js` to include visual indicators for duties and events, improving user interaction and experience.
- Updated CSS in `style.css` to style the day detail panel and its components, ensuring a responsive design for both desktop and mobile views.
- Refactored `hints.js` to export the `getDutyMarkerRows` function, facilitating better integration with the new day detail features.
- Added localization support for the day detail panel in `i18n.js`, including new translations for close button and event titles.
- Enhanced the initialization process in `main.js` to set up the day detail panel on application load.
This commit is contained in:
2026-02-19 16:23:46 +03:00
parent 4c2d95e776
commit b60111462a
6 changed files with 501 additions and 61 deletions

View File

@@ -9,10 +9,9 @@ import {
localDateString,
firstDayOfMonth,
lastDayOfMonth,
getMonday
getMonday,
dateKeyToDDMM
} 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.
@@ -88,71 +87,65 @@ export function renderCalendar(
const eventSummaries = calendarEventsByDate[key] || [];
const hasEvent = eventSummaries.length > 0;
const showMarkers = !isOther;
const showIndicator = !isOther;
const cell = document.createElement("div");
cell.className =
"day" +
(isOther ? " other-month" : "") +
(isToday ? " today" : "") +
(showMarkers && hasAny ? " has-duty" : "") +
(showMarkers && hasEvent ? " holiday" : "");
(showIndicator && hasAny ? " has-duty" : "") +
(showIndicator && 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) {
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>';
}
cell.setAttribute("data-date", key);
if (showIndicator) {
const dayPayload = dayDuties.map((x) => ({
event_type: x.event_type,
full_name: x.full_name,
start_at: x.start_at,
end_at: x.end_at
}));
cell.setAttribute(
"data-day-duties",
JSON.stringify(dayPayload).replace(/"/g, "&quot;")
);
cell.setAttribute(
"data-day-events",
JSON.stringify(eventSummaries).replace(/"/g, "&quot;")
);
}
html += "</div>";
cell.innerHTML = html;
const ariaParts = [];
ariaParts.push(dateKeyToDDMM(key));
if (hasAny || hasEvent) {
const counts = [];
const lang = state.lang;
if (dutyList.length) counts.push(dutyList.length + " " + t(lang, "event_type.duty"));
if (unavailableList.length) counts.push(unavailableList.length + " " + t(lang, "event_type.unavailable"));
if (vacationList.length) counts.push(vacationList.length + " " + t(lang, "event_type.vacation"));
if (hasEvent) counts.push(t(lang, "hint.events"));
ariaParts.push(counts.join(", "));
} else {
ariaParts.push(t(state.lang, "aria.day_info"));
}
cell.setAttribute("role", "button");
cell.setAttribute("tabindex", "0");
cell.setAttribute("aria-label", ariaParts.join("; "));
let indicatorHtml = "";
if (showIndicator && (hasAny || hasEvent)) {
indicatorHtml = '<div class="day-indicator">';
if (dutyList.length) indicatorHtml += '<span class="day-indicator-dot duty"></span>';
if (unavailableList.length) indicatorHtml += '<span class="day-indicator-dot unavailable"></span>';
if (vacationList.length) indicatorHtml += '<span class="day-indicator-dot vacation"></span>';
if (hasEvent) indicatorHtml += '<span class="day-indicator-dot events"></span>';
indicatorHtml += "</div>";
}
cell.innerHTML =
'<span class="num">' + d.getDate() + "</span>" + indicatorHtml;
calendarEl.appendChild(cell);
d.setDate(d.getDate() + 1);
}
monthTitleEl.textContent = monthName(state.lang, month) + " " + year;
bindInfoButtonTooltips();
bindDutyMarkerTooltips();
}