Files
duty-teller/webapp/js/dutyList.js
Nikolay Tatarinov 4c2d95e776
All checks were successful
CI / lint-and-test (push) Successful in 14s
feat: implement internationalization for duty calendar
- Introduced a new `i18n.js` module for handling translations and language detection, supporting both Russian and English.
- Updated various components to utilize the new translation functions, enhancing user experience by providing localized messages for errors, hints, and UI elements.
- Refactored existing code in `api.js`, `calendar.js`, `dutyList.js`, `hints.js`, and `ui.js` to replace hardcoded strings with translated messages, improving maintainability and accessibility.
- Enhanced the initialization process in `main.js` to set the document language and update UI elements based on the user's language preference.
2026-02-19 16:00:00 +03:00

178 lines
6.3 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 = 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" });
}
}
}