/**
* Duty list (timeline) rendering.
*/
import { dutyListEl, state } from "./dom.js";
import { EVENT_TYPE_LABELS } from "./constants.js";
import { escapeHtml } from "./utils.js";
import {
localDateString,
firstDayOfMonth,
lastDayOfMonth,
dateKeyToDDMM,
formatTimeLocal,
formatDateKey
} from "./dateUtils.js";
import { dutiesByDate } from "./calendar.js";
/**
* Build HTML for one timeline duty card: one-day "DD.MM, HH:MM – HH:MM" or multi-day.
* @param {object} d - Duty
* @param {boolean} isCurrent - Whether this is "current" duty
* @returns {string}
*/
export function dutyTimelineCardHtml(d, isCurrent) {
const startLocal = localDateString(new Date(d.start_at));
const endLocal = localDateString(new Date(d.end_at));
const startDDMM = dateKeyToDDMM(startLocal);
const endDDMM = dateKeyToDDMM(endLocal);
const startTime = formatTimeLocal(d.start_at);
const endTime = formatTimeLocal(d.end_at);
let timeStr;
if (startLocal === endLocal) {
timeStr = startDDMM + ", " + startTime + " – " + endTime;
} else {
timeStr = startDDMM + " " + startTime + " – " + endDDMM + " " + endTime;
}
const typeLabel = isCurrent
? "Сейчас дежурит"
: (EVENT_TYPE_LABELS[d.event_type] || "Дежурство");
const extraClass = isCurrent ? " duty-item--current" : "";
return (
'
"
);
}
/**
* Build HTML for one duty card.
* @param {object} d - Duty
* @param {string} [typeLabelOverride] - e.g. "Сейчас дежурит"
* @param {boolean} [showUntilEnd] - Show "до HH:MM" instead of range
* @param {string} [extraClass] - e.g. "duty-item--current"
* @returns {string}
*/
export function dutyItemHtml(d, typeLabelOverride, showUntilEnd, extraClass) {
const startDate = new Date(d.start_at);
const endDate = new Date(d.end_at);
const typeLabel =
typeLabelOverride != null
? typeLabelOverride
: (EVENT_TYPE_LABELS[d.event_type] || d.event_type);
let itemClass = "duty-item duty-item--" + (d.event_type || "duty");
if (extraClass) itemClass += " " + extraClass;
let timeOrRange = "";
if (showUntilEnd && d.event_type === "duty") {
const end =
String(endDate.getHours()).padStart(2, "0") +
":" +
String(endDate.getMinutes()).padStart(2, "0");
timeOrRange = "до " + end;
} else if (d.event_type === "vacation" || d.event_type === "unavailable") {
const startStr = formatDateKey(d.start_at);
const endStr = formatDateKey(d.end_at);
timeOrRange = startStr === endStr ? startStr : startStr + " – " + endStr;
} else {
const start =
String(startDate.getHours()).padStart(2, "0") +
":" +
String(startDate.getMinutes()).padStart(2, "0");
const end =
String(endDate.getHours()).padStart(2, "0") +
":" +
String(endDate.getMinutes()).padStart(2, "0");
timeOrRange = start + " – " + end;
}
return (
'' +
escapeHtml(typeLabel) +
' ' +
escapeHtml(d.full_name) +
'' +
timeOrRange +
"
"
);
}
/**
* Render duty list (timeline) for current month; scroll to today if visible.
* @param {object[]} duties - Duties (only duty type used for timeline)
*/
export function renderDutyList(duties) {
if (!dutyListEl) return;
const filtered = duties.filter((d) => d.event_type === "duty");
if (filtered.length === 0) {
dutyListEl.classList.remove("duty-timeline");
dutyListEl.innerHTML = 'В этом месяце дежурств нет.
';
return;
}
dutyListEl.classList.add("duty-timeline");
const current = state.current;
const todayKey = localDateString(new Date());
const firstKey = localDateString(firstDayOfMonth(current));
const lastKey = localDateString(lastDayOfMonth(current));
const showTodayInMonth = todayKey >= firstKey && todayKey <= lastKey;
const dateSet = new Set();
filtered.forEach((d) => {
dateSet.add(localDateString(new Date(d.start_at)));
});
if (showTodayInMonth) dateSet.add(todayKey);
const dates = Array.from(dateSet).sort();
const now = new Date();
let fullHtml = "";
dates.forEach((date) => {
const isToday = date === todayKey;
const dayClass =
"duty-timeline-day" + (isToday ? " duty-timeline-day--today" : "");
const dateLabel = dateKeyToDDMM(date);
const dateCellHtml = isToday
? 'Сегодня' +
escapeHtml(dateLabel) +
''
: '' + escapeHtml(dateLabel) + "";
const dayDuties = filtered
.filter((d) => localDateString(new Date(d.start_at)) === date)
.sort((a, b) => new Date(a.start_at) - new Date(b.start_at));
let dayHtml = "";
dayDuties.forEach((d) => {
const start = new Date(d.start_at);
const end = new Date(d.end_at);
const isCurrent = isToday && start <= now && now < end;
dayHtml +=
'' +
dateCellHtml +
'
' +
dutyTimelineCardHtml(d, isCurrent) +
"
";
});
if (dayDuties.length === 0 && isToday) {
dayHtml +=
'';
}
fullHtml +=
'' +
dayHtml +
"
";
});
dutyListEl.innerHTML = fullHtml;
const scrollTarget = dutyListEl.querySelector(".duty-timeline-day--today");
if (scrollTarget) {
const calendarSticky = document.getElementById("calendarSticky");
if (calendarSticky) {
requestAnimationFrame(() => {
const calendarHeight = calendarSticky.offsetHeight;
const todayTop = scrollTarget.getBoundingClientRect().top + window.scrollY;
const scrollTop = Math.max(0, todayTop - calendarHeight);
window.scrollTo({ top: scrollTop, behavior: "auto" });
});
} else {
scrollTarget.scrollIntoView({ behavior: "auto", block: "start" });
}
}
}