Refactor calendar rendering and duty list display

- Updated the calendar cell rendering to conditionally show duty and event markers based on the month view.
- Introduced a new HTML structure for day markers to improve layout and styling.
- Enhanced the duty list rendering to filter duties based on the current month, ensuring only relevant duties are displayed.
- Added CSS styles for day markers to improve visual presentation and alignment.
This commit is contained in:
2026-02-17 23:26:41 +03:00
parent 3f4c7bf66c
commit e23d2247e0
2 changed files with 60 additions and 22 deletions

View File

@@ -207,25 +207,29 @@
const hasEvent = eventSummaries.length > 0; const hasEvent = eventSummaries.length > 0;
const cell = document.createElement("div"); const cell = document.createElement("div");
cell.className = "day" + (isOther ? " other-month" : "") + (isToday ? " today" : "") + (hasAny ? " has-duty" : "") + (hasEvent ? " holiday" : ""); const showMarkers = !isOther;
cell.className = "day" + (isOther ? " other-month" : "") + (isToday ? " today" : "") + (showMarkers && hasAny ? " has-duty" : "") + (showMarkers && hasEvent ? " holiday" : "");
function namesAttr(list) { return list.length ? escapeHtml(list.map(function (x) { return x.full_name; }).join("\n")) : ""; } function namesAttr(list) { return list.length ? escapeHtml(list.map(function (x) { return x.full_name; }).join("\n")) : ""; }
function titleAttr(list) { return list.length ? escapeHtml(list.map(function (x) { return x.full_name; }).join(", ")) : ""; } function titleAttr(list) { return list.length ? escapeHtml(list.map(function (x) { return x.full_name; }).join(", ")) : ""; }
let markers = "<span class=\"num\">" + d.getDate() + "</span>"; let html = "<span class=\"num\">" + d.getDate() + "</span><div class=\"day-markers\">";
if (dutyList.length) { if (showMarkers) {
markers += "<span class=\"duty-marker\" data-names=\"" + namesAttr(dutyList) + "\" title=\"" + titleAttr(dutyList) + "\" aria-label=\"Дежурные\">Д</span>"; if (dutyList.length) {
html += "<span class=\"duty-marker\" data-names=\"" + namesAttr(dutyList) + "\" title=\"" + titleAttr(dutyList) + "\" aria-label=\"Дежурные\">Д</span>";
}
if (unavailableList.length) {
html += "<span class=\"unavailable-marker\" data-names=\"" + namesAttr(unavailableList) + "\" title=\"" + titleAttr(unavailableList) + "\" aria-label=\"Недоступен\">Н</span>";
}
if (vacationList.length) {
html += "<span class=\"vacation-marker\" data-names=\"" + namesAttr(vacationList) + "\" title=\"" + titleAttr(vacationList) + "\" aria-label=\"Отпуск\">О</span>";
}
if (hasEvent) {
html += "<button type=\"button\" class=\"info-btn\" aria-label=\"Информация о дне\" data-summary=\"" + escapeHtml(eventSummaries.join("\n")) + "\">i</button>";
}
} }
if (unavailableList.length) { html += "</div>";
markers += "<span class=\"unavailable-marker\" data-names=\"" + namesAttr(unavailableList) + "\" title=\"" + titleAttr(unavailableList) + "\" aria-label=\"Недоступен\">Н</span>"; cell.innerHTML = html;
}
if (vacationList.length) {
markers += "<span class=\"vacation-marker\" data-names=\"" + namesAttr(vacationList) + "\" title=\"" + titleAttr(vacationList) + "\" aria-label=\"Отпуск\">О</span>";
}
if (hasEvent) {
markers += "<button type=\"button\" class=\"info-btn\" aria-label=\"Информация о дне\" data-summary=\"" + escapeHtml(eventSummaries.join("\n")) + "\">i</button>";
}
cell.innerHTML = markers;
calendarEl.appendChild(cell); calendarEl.appendChild(cell);
d.setDate(d.getDate() + 1); d.setDate(d.getDate() + 1);
} }
@@ -470,8 +474,12 @@
setNavEnabled(false); setNavEnabled(false);
loadingEl.classList.remove("hidden"); loadingEl.classList.remove("hidden");
errorEl.hidden = true; errorEl.hidden = true;
const from = localDateString(firstDayOfMonth(current)); const first = firstDayOfMonth(current);
const to = localDateString(lastDayOfMonth(current)); const start = getMonday(first);
const gridEnd = new Date(start);
gridEnd.setDate(gridEnd.getDate() + 41);
const from = localDateString(start);
const to = localDateString(gridEnd);
try { try {
const dutiesPromise = fetchDuties(from, to); const dutiesPromise = fetchDuties(from, to);
const eventsPromise = fetchCalendarEvents(from, to); const eventsPromise = fetchCalendarEvents(from, to);
@@ -480,7 +488,16 @@
const byDate = dutiesByDate(duties); const byDate = dutiesByDate(duties);
const calendarByDate = calendarEventsByDate(events); const calendarByDate = calendarEventsByDate(events);
renderCalendar(current.getFullYear(), current.getMonth(), byDate, calendarByDate); renderCalendar(current.getFullYear(), current.getMonth(), byDate, calendarByDate);
renderDutyList(duties); const last = lastDayOfMonth(current);
const firstKey = localDateString(first);
const lastKey = localDateString(last);
const dutiesInMonth = duties.filter(function (d) {
const byDateLocal = dutiesByDate([d]);
return Object.keys(byDateLocal).some(function (key) {
return key >= firstKey && key <= lastKey;
});
});
renderDutyList(dutiesInMonth);
} catch (e) { } catch (e) {
if (e.message === "ACCESS_DENIED") { if (e.message === "ACCESS_DENIED") {
showAccessDenied(e.serverDetail); showAccessDenied(e.serverDetail);

View File

@@ -143,6 +143,17 @@ body {
opacity: 0.9; opacity: 0.9;
} }
.day-markers {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 2px;
align-items: center;
margin-top: 2px;
min-width: 0;
}
.calendar-event-hint { .calendar-event-hint {
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;
@@ -170,10 +181,9 @@ body {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 16px; width: 14px;
height: 16px; height: 14px;
margin-top: 2px; font-size: 0.6rem;
font-size: 0.65rem;
font-weight: 700; font-weight: 700;
border-radius: 50%; border-radius: 50%;
flex-shrink: 0; flex-shrink: 0;
@@ -205,6 +215,10 @@ body {
} }
.duty-item { .duty-item {
display: grid;
grid-template-columns: 5.5em 1fr;
gap: 0 8px;
align-items: baseline;
padding: 8px 10px; padding: 8px 10px;
margin-bottom: 6px; margin-bottom: 6px;
border-radius: 8px; border-radius: 8px;
@@ -221,16 +235,23 @@ body {
} }
.duty-item .duty-item-type { .duty-item .duty-item-type {
grid-column: 1;
grid-row: 1;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--muted); color: var(--muted);
margin-right: 4px;
} }
.duty-item .name { .duty-item .name {
grid-column: 2;
grid-row: 1 / -1;
min-width: 0;
font-weight: 600; font-weight: 600;
} }
.duty-item .time { .duty-item .time {
grid-column: 1;
grid-row: 2;
align-self: start;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--muted); color: var(--muted);
} }