/** * 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} */ 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} */ 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} dutiesByDateMap * @param {Record} 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 = '' + d.getDate() + '
'; if (showMarkers) { if (dutyList.length) { html += '"; } if (unavailableList.length) { html += ''; } if (vacationList.length) { html += ''; } if (hasEvent) { html += ''; } } html += "
"; cell.innerHTML = html; calendarEl.appendChild(cell); d.setDate(d.getDate() + 1); } monthTitleEl.textContent = MONTHS[month] + " " + year; bindInfoButtonTooltips(); bindDutyMarkerTooltips(); }