- 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.
188 lines
6.6 KiB
JavaScript
188 lines
6.6 KiB
JavaScript
/**
|
||
* 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" });
|
||
}
|
||
}
|
||
}
|