style: enhance layout and functionality of duty markers and calendar

- Updated CSS for `.day` and `.access-denied` to improve layout and visual consistency.
- Introduced a new function `dutyOverlapsLocalRange` in `dateUtils.js` to check duty overlaps within a specified date range.
- Refactored `dutyItemHtml` in `dutyList.js` to utilize `formatTimeLocal` for time formatting, enhancing readability.
- Added utility functions in `hints.js` for parsing duty marker data and building time prefixes, streamlining hint rendering logic.
- Improved the `showAccessDenied` function in `ui.js` to display detailed server messages when access is denied.
This commit is contained in:
2026-02-19 15:40:34 +03:00
parent c9cf86a8f6
commit dd14d48824
6 changed files with 156 additions and 147 deletions

View File

@@ -48,6 +48,100 @@ export function positionHint(hintEl, btnRect) {
});
}
/**
* Parse data-duty-items and data-date from marker; detect if any item has times.
* @param {HTMLElement} marker
* @returns {{ dutyItems: object[], hasTimes: boolean, hintDay: string }}
*/
function parseDutyMarkerData(marker) {
const dutyItemsRaw =
marker.getAttribute("data-duty-items") ||
(marker.dataset && marker.dataset.dutyItems) ||
"";
let dutyItems = [];
try {
if (dutyItemsRaw) dutyItems = JSON.parse(dutyItemsRaw);
} catch (e) {
/* ignore */
}
const hasTimes =
dutyItems.length > 0 &&
dutyItems.some((it) => {
const start = it.start_at != null ? it.start_at : it.startAt;
const end = it.end_at != null ? it.end_at : it.endAt;
return start || end;
});
const hintDay = marker.getAttribute("data-date") || "";
return { dutyItems, hasTimes, hintDay };
}
/**
* Get fullName from duty item (supports snake_case and camelCase).
* @param {object} item
* @returns {string}
*/
function getItemFullName(item) {
return item.full_name != null ? item.full_name : item.fullName;
}
/**
* Build timePrefix for one duty item in the list (shared logic for text and HTML).
* @param {object} item - duty item with start_at/startAt, end_at/endAt
* @param {number} idx - index in dutyItems
* @param {number} total - dutyItems.length
* @param {string} hintDay - YYYY-MM-DD
* @param {string} sep - separator after "с"/"до" (e.g. " - " for text, "\u00a0" for HTML)
* @returns {string}
*/
function buildDutyItemTimePrefix(item, idx, total, hintDay, sep) {
const startAt = item.start_at != null ? item.start_at : item.startAt;
const endAt = item.end_at != null ? item.end_at : item.endAt;
const endHHMM = endAt ? formatHHMM(endAt) : "";
const startHHMM = startAt ? formatHHMM(startAt) : "";
const startSameDay =
hintDay && startAt && localDateString(new Date(startAt)) === hintDay;
const endSameDay =
hintDay && endAt && localDateString(new Date(endAt)) === hintDay;
let timePrefix = "";
if (idx === 0) {
if (total === 1 && startSameDay && startHHMM) {
timePrefix = "с" + sep + startHHMM;
if (endSameDay && endHHMM && endHHMM !== startHHMM) {
timePrefix += " до" + sep + endHHMM;
}
} else if (endHHMM) {
timePrefix = "до" + sep + endHHMM;
}
} else if (idx > 0) {
if (startHHMM) timePrefix = "с" + sep + startHHMM;
if (endHHMM && endSameDay && endHHMM !== startHHMM) {
timePrefix += (timePrefix ? " " : "") + "до" + sep + endHHMM;
}
}
return timePrefix;
}
/**
* Get array of { timePrefix, fullName } for duty items (single source of time rules).
* @param {object[]} dutyItems
* @param {string} hintDay
* @param {string} timeSep - e.g. " - " for text, "\u00a0" for HTML
* @returns {{ timePrefix: string, fullName: string }[]}
*/
function getDutyMarkerRows(dutyItems, hintDay, timeSep) {
return dutyItems.map((item, idx) => {
const timePrefix = buildDutyItemTimePrefix(
item,
idx,
dutyItems.length,
hintDay,
timeSep
);
const fullName = getItemFullName(item);
return { timePrefix, fullName };
});
}
/**
* Get plain text content for duty marker tooltip.
* @param {HTMLElement} marker - .duty-marker, .unavailable-marker or .vacation-marker
@@ -59,63 +153,13 @@ export function getDutyMarkerHintContent(marker) {
const names = marker.getAttribute("data-names") || "";
let body;
if (type === "duty") {
const dutyItemsRaw =
marker.getAttribute("data-duty-items") ||
(marker.dataset && marker.dataset.dutyItems) ||
"";
let dutyItems = [];
try {
if (dutyItemsRaw) dutyItems = JSON.parse(dutyItemsRaw);
} catch (e) {
/* ignore */
}
const hasTimes =
dutyItems.length > 0 &&
dutyItems.some((it) => {
const start = it.start_at != null ? it.start_at : it.startAt;
const end = it.end_at != null ? it.end_at : it.endAt;
return start || end;
});
const { dutyItems, hasTimes, hintDay } = parseDutyMarkerData(marker);
if (dutyItems.length >= 1 && hasTimes) {
const hintDay = marker.getAttribute("data-date") || "";
body = dutyItems
.map((item, idx) => {
const startAt = item.start_at != null ? item.start_at : item.startAt;
const endAt = item.end_at != null ? item.end_at : item.endAt;
const endHHMM = endAt ? formatHHMM(endAt) : "";
const startHHMM = startAt ? formatHHMM(startAt) : "";
const startSameDay =
hintDay && startAt && localDateString(new Date(startAt)) === hintDay;
const endSameDay =
hintDay && endAt && localDateString(new Date(endAt)) === hintDay;
const fullName =
item.full_name != null ? item.full_name : item.fullName;
let timePrefix = "";
if (idx === 0) {
if (
dutyItems.length === 1 &&
startSameDay &&
startHHMM
) {
timePrefix = "с - " + startHHMM;
if (endSameDay && endHHMM && endHHMM !== startHHMM) {
timePrefix += " до - " + endHHMM;
}
} else if (endHHMM) {
timePrefix = "до -" + endHHMM;
}
} else if (idx > 0) {
if (startHHMM) timePrefix = "с - " + startHHMM;
if (
endHHMM &&
endSameDay &&
endHHMM !== startHHMM
) {
timePrefix += (timePrefix ? " " : "") + "до - " + endHHMM;
}
}
return timePrefix ? timePrefix + " — " + fullName : fullName;
})
const rows = getDutyMarkerRows(dutyItems, hintDay, " - ");
body = rows
.map((r) =>
r.timePrefix ? r.timePrefix + " — " + r.fullName : r.fullName
)
.join("\n");
} else {
body = names;
@@ -135,65 +179,16 @@ export function getDutyMarkerHintHtml(marker) {
const type = marker.getAttribute("data-event-type") || "duty";
if (type !== "duty") return null;
const names = marker.getAttribute("data-names") || "";
const dutyItemsRaw =
marker.getAttribute("data-duty-items") ||
(marker.dataset && marker.dataset.dutyItems) ||
"";
let dutyItems = [];
try {
if (dutyItemsRaw) dutyItems = JSON.parse(dutyItemsRaw);
} catch (e) {
/* ignore */
}
const hasTimes =
dutyItems.length > 0 &&
dutyItems.some((it) => {
const start = it.start_at != null ? it.start_at : it.startAt;
const end = it.end_at != null ? it.end_at : it.endAt;
return start || end;
});
const hintDay = marker.getAttribute("data-date") || "";
const { dutyItems, hasTimes, hintDay } = parseDutyMarkerData(marker);
const nbsp = "\u00a0";
let rows;
let rowHtmls;
if (dutyItems.length >= 1 && hasTimes) {
rows = dutyItems.map((item, idx) => {
const startAt = item.start_at != null ? item.start_at : item.startAt;
const endAt = item.end_at != null ? item.end_at : item.endAt;
const endHHMM = endAt ? formatHHMM(endAt) : "";
const startHHMM = startAt ? formatHHMM(startAt) : "";
const startSameDay =
hintDay && startAt && localDateString(new Date(startAt)) === hintDay;
const endSameDay =
hintDay && endAt && localDateString(new Date(endAt)) === hintDay;
const fullName =
item.full_name != null ? item.full_name : item.fullName;
let timePrefix = "";
if (idx === 0) {
if (
dutyItems.length === 1 &&
startSameDay &&
startHHMM
) {
timePrefix = "с" + nbsp + startHHMM;
if (endSameDay && endHHMM && endHHMM !== startHHMM) {
timePrefix += " до" + nbsp + endHHMM;
}
} else if (endHHMM) {
timePrefix = "до" + nbsp + endHHMM;
}
} else if (idx > 0) {
if (startHHMM) timePrefix = "с" + nbsp + startHHMM;
if (
endHHMM &&
endSameDay &&
endHHMM !== startHHMM
) {
timePrefix += (timePrefix ? " " : "") + "до" + nbsp + endHHMM;
}
}
const timeHtml = timePrefix ? escapeHtml(timePrefix) : "";
const nameHtml = escapeHtml(fullName);
const namePart = timePrefix ? " — " + nameHtml : nameHtml;
const rows = getDutyMarkerRows(dutyItems, hintDay, nbsp);
rowHtmls = rows.map((r) => {
const timeHtml = r.timePrefix ? escapeHtml(r.timePrefix) : "";
const namePart = r.timePrefix
? " — " + escapeHtml(r.fullName)
: escapeHtml(r.fullName);
return (
'<span class="calendar-event-hint-time">' +
timeHtml +
@@ -203,7 +198,7 @@ export function getDutyMarkerHintHtml(marker) {
);
});
} else {
rows = (names ? names.split("\n") : []).map((fullName) => {
rowHtmls = (names ? names.split("\n") : []).map((fullName) => {
const nameHtml = escapeHtml(fullName.trim());
return (
'<span class="calendar-event-hint-time"></span><span class="calendar-event-hint-name">' +
@@ -212,10 +207,10 @@ export function getDutyMarkerHintHtml(marker) {
);
});
}
if (rows.length === 0) return null;
if (rowHtmls.length === 0) return null;
return (
'<div class="calendar-event-hint-title">Дежурство:</div><div class="calendar-event-hint-rows">' +
rows
rowHtmls
.map((r) => '<div class="calendar-event-hint-row">' + r + "</div>")
.join("") +
"</div>"