Files
duty-teller/webapp/js/dutyList.js
Nikolay Tatarinov c9cf86a8f6 refactor: restructure web application with modular JavaScript and remove legacy code
- Deleted the `app.js` file and migrated its functionality to a modular structure with multiple JavaScript files for better organization and maintainability.
- Updated `index.html` to reference the new `main.js` module, ensuring proper loading of the application.
- Introduced new utility modules for API requests, authentication, calendar handling, and DOM manipulation to enhance code clarity and separation of concerns.
- Enhanced CSS styles for improved layout and theming consistency across the application.
- Added comprehensive comments and documentation to new modules to facilitate future development and understanding.
2026-02-19 15:24:52 +03:00

188 lines
6.6 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 { 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 (
'<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 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 (
'<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">В этом месяце дежурств нет.</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">Сегодня</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 = isToday && 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 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" });
}
}
}