Files
duty-teller/webapp/js/dutyList.js
Nikolay Tatarinov a81103e90d
All checks were successful
CI / lint-and-test (push) Successful in 25s
fix: correct duty list scrolling behavior and simplify current duty check
- Removed unnecessary condition for determining if a duty is current, streamlining the logic.
- Refactored the scrolling functionality to ensure the view correctly focuses on the current duty or today's block.
- Improved code clarity by consolidating the scroll target logic into a reusable function.
2026-02-21 01:08:48 +03:00

185 lines
6.5 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.

/**
* Duty list (timeline) rendering.
*/
import { dutyListEl, state } from "./dom.js";
import { t } from "./i18n.js";
import { escapeHtml } from "./utils.js";
import {
localDateString,
firstDayOfMonth,
lastDayOfMonth,
dateKeyToDDMM,
formatTimeLocal,
formatDateKey
} from "./dateUtils.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 lang = state.lang;
const typeLabel = isCurrent
? t(lang, "duty.now_on_duty")
: (t(lang, "event_type." + (d.event_type || "duty")));
const extraClass = isCurrent ? " duty-item--current" : "";
return (
'<div class="duty-item duty-item--duty duty-timeline-card' +
extraClass +
'"><span class="duty-item-type">' +
escapeHtml(typeLabel) +
'</span> <span class="name">' +
escapeHtml(d.full_name) +
'</span><div class="time">' +
escapeHtml(timeStr) +
"</div></div>"
);
}
/**
* 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 lang = state.lang;
const typeLabel =
typeLabelOverride != null
? typeLabelOverride
: (t(lang, "event_type." + (d.event_type || "duty")));
let itemClass = "duty-item duty-item--" + (d.event_type || "duty");
if (extraClass) itemClass += " " + extraClass;
let timeOrRange = "";
if (showUntilEnd && d.event_type === "duty") {
timeOrRange = t(lang, "duty.until", { time: formatTimeLocal(d.end_at) });
} 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 {
timeOrRange =
formatTimeLocal(d.start_at) + " " + formatTimeLocal(d.end_at);
}
return (
'<div class="' +
itemClass +
'"><span class="duty-item-type">' +
escapeHtml(typeLabel) +
'</span> <span class="name">' +
escapeHtml(d.full_name) +
'</span><div class="time">' +
timeOrRange +
"</div></div>"
);
}
/**
* 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 = '<p class="muted">' + t(state.lang, "duty.none_this_month") + "</p>";
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
? '<span class="duty-timeline-date"><span class="duty-timeline-date-label">' +
escapeHtml(t(state.lang, "duty.today")) +
'</span><span class="duty-timeline-date-day">' +
escapeHtml(dateLabel) +
'</span><span class="duty-timeline-date-dot" aria-hidden="true"></span></span>'
: '<span class="duty-timeline-date">' + escapeHtml(dateLabel) + "</span>";
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 = start <= now && now < end;
dayHtml +=
'<div class="duty-timeline-row">' +
dateCellHtml +
'<span class="duty-timeline-track" aria-hidden="true"></span><div class="duty-timeline-card-wrap">' +
dutyTimelineCardHtml(d, isCurrent) +
"</div></div>";
});
if (dayDuties.length === 0 && isToday) {
dayHtml +=
'<div class="duty-timeline-row duty-timeline-row--empty">' +
dateCellHtml +
'<span class="duty-timeline-track" aria-hidden="true"></span><div class="duty-timeline-card-wrap"></div></div>';
}
fullHtml +=
'<div class="' +
dayClass +
'" data-date="' +
escapeHtml(date) +
'">' +
dayHtml +
"</div>";
});
dutyListEl.innerHTML = fullHtml;
const calendarSticky = document.getElementById("calendarSticky");
const scrollToEl = (el) => {
if (!el) return;
if (calendarSticky) {
requestAnimationFrame(() => {
const calendarHeight = calendarSticky.offsetHeight;
const top = el.getBoundingClientRect().top + window.scrollY;
const scrollTop = Math.max(0, top - calendarHeight);
window.scrollTo({ top: scrollTop, behavior: "auto" });
});
} else {
el.scrollIntoView({ behavior: "auto", block: "start" });
}
};
const currentDutyCard = dutyListEl.querySelector(".duty-item--current");
const todayBlock = dutyListEl.querySelector(".duty-timeline-day--today");
if (currentDutyCard) {
scrollToEl(currentDutyCard);
} else if (todayBlock) {
scrollToEl(todayBlock);
}
}