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.
This commit is contained in:
187
webapp/js/dutyList.js
Normal file
187
webapp/js/dutyList.js
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user