feat: enhance current duty display with remaining time and improved contact links
All checks were successful
CI / lint-and-test (push) Successful in 35s
Docker Build and Release / build-and-push (push) Successful in 51s
Docker Build and Release / release (push) Successful in 8s

- Added a new message key for displaying remaining time until the end of the shift in both English and Russian.
- Updated the current duty card to show remaining time with a formatted string.
- Enhanced the contact links to support block layout with icons for phone and Telegram, improving visual presentation.
- Implemented a new utility function to calculate remaining time until the end of the shift.
- Added unit tests for the new functionality, ensuring accurate time calculations and proper rendering of contact links.
This commit is contained in:
2026-03-02 19:04:30 +03:00
parent 2e78b3c1e6
commit 0d28123d0b
7 changed files with 376 additions and 43 deletions

View File

@@ -14,11 +14,29 @@ import {
formatHHMM
} from "./dateUtils.js";
/** Empty calendar icon for "no duty" state (outline, stroke). */
const ICON_NO_DUTY =
'<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
/** @type {(() => void)|null} Callback when user taps "Back to calendar". */
let onBackCallback = null;
/** @type {(() => void)|null} Handler registered with Telegram BackButton.onClick. */
let backButtonHandler = null;
/**
* Compute remaining time until end of shift. Call only when now < end (active duty).
* @param {string|Date} endAt - ISO end time of the shift
* @returns {{ hours: number, minutes: number }}
*/
export function getRemainingTime(endAt) {
const end = new Date(endAt).getTime();
const now = Date.now();
const ms = Math.max(0, end - now);
const hours = Math.floor(ms / (1000 * 60 * 60));
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
return { hours, minutes };
}
/**
* Find the duty that is currently active (start <= now < end). Prefer event_type === "duty".
* @param {object[]} duties - List of duties with start_at, end_at, event_type
@@ -49,13 +67,18 @@ export function renderCurrentDutyContent(duty, lang) {
if (!duty) {
const noDuty = t(lang, "current_duty.no_duty");
return (
'<div class="current-duty-card">' +
'<div class="current-duty-card current-duty-card--no-duty">' +
'<h2 class="current-duty-title">' +
escapeHtml(title) +
"</h2>" +
'<div class="current-duty-no-duty-wrap">' +
'<span class="current-duty-no-duty-icon">' +
ICON_NO_DUTY +
"</span>" +
'<p class="current-duty-no-duty">' +
escapeHtml(noDuty) +
"</p>" +
"</div>" +
'<button type="button" class="current-duty-back-btn" data-action="back">' +
escapeHtml(backLabel) +
"</button>" +
@@ -78,15 +101,22 @@ export function renderCurrentDutyContent(duty, lang) {
" " +
endTime;
const shiftLabel = t(lang, "current_duty.shift");
const { hours: remHours, minutes: remMinutes } = getRemainingTime(duty.end_at);
const remainingStr = t(lang, "current_duty.remaining", {
hours: String(remHours),
minutes: String(remMinutes)
});
const contactHtml = buildContactLinksHtml(lang, duty.phone, duty.username, {
classPrefix: "current-duty-contact",
showLabels: true,
separator: " "
separator: " ",
layout: "block"
});
return (
'<div class="current-duty-card">' +
'<h2 class="current-duty-title">' +
'<span class="current-duty-live-dot"></span> ' +
escapeHtml(title) +
"</h2>" +
'<p class="current-duty-name">' +
@@ -97,6 +127,9 @@ export function renderCurrentDutyContent(duty, lang) {
": " +
escapeHtml(shiftStr) +
"</div>" +
'<div class="current-duty-remaining">' +
escapeHtml(remainingStr) +
"</div>" +
contactHtml +
'<button type="button" class="current-duty-back-btn" data-action="back">' +
escapeHtml(backLabel) +