Files
duty-teller/webapp/js/contactHtml.js
Nikolay Tatarinov 0d28123d0b
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
feat: enhance current duty display with remaining time and improved contact links
- 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.
2026-03-02 19:04:30 +03:00

151 lines
5.9 KiB
JavaScript

/**
* Shared HTML builder for contact links (phone, Telegram) used by day detail,
* current duty, and duty list.
*/
import { t } from "./i18n.js";
import { escapeHtml } from "./utils.js";
/** Phone icon SVG for block layout (inline, same style as dutyList). */
const ICON_PHONE =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>';
/** Telegram / send icon SVG for block layout. */
const ICON_TELEGRAM =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
/**
* Format Russian phone number for display: 79146522209 -> +7 914 652-22-09.
* Accepts 10 digits (9XXXXXXXXX), 11 digits (79XXXXXXXXX or 89XXXXXXXXX).
* Other lengths are returned as-is (digits only).
*
* @param {string} phone - Raw phone string (digits, optional leading 7/8)
* @returns {string} Formatted display string, e.g. "+7 914 652-22-09"
*/
export function formatPhoneDisplay(phone) {
if (phone == null || String(phone).trim() === "") return "";
const digits = String(phone).replace(/\D/g, "");
if (digits.length === 10) {
return "+7 " + digits.slice(0, 3) + " " + digits.slice(3, 6) + "-" + digits.slice(6, 8) + "-" + digits.slice(8);
}
if (digits.length === 11 && (digits[0] === "7" || digits[0] === "8")) {
const rest = digits.slice(1);
return "+7 " + rest.slice(0, 3) + " " + rest.slice(3, 6) + "-" + rest.slice(6, 8) + "-" + rest.slice(8);
}
return digits;
}
/**
* Build HTML for contact links (phone, Telegram username).
* Validates phone/username, builds tel: and t.me hrefs, wraps in spans/links.
*
* @param {'ru'|'en'} lang - UI language for labels (when showLabels is true)
* @param {string|null|undefined} phone - Phone number
* @param {string|null|undefined} username - Telegram username with or without leading @
* @param {object} options - Rendering options
* @param {string} options.classPrefix - CSS class prefix (e.g. "day-detail-contact", "duty-contact")
* @param {boolean} [options.showLabels=true] - Whether to show "Phone:" / "Telegram:" labels (ignored when layout is "block")
* @param {string} [options.separator=' '] - Separator between contact parts (e.g. " ", " · ")
* @param {'inline'|'block'} [options.layout='inline'] - "block" = full-width button blocks with SVG icons (for current duty card)
* @returns {string} HTML string or "" if no valid contact
*/
export function buildContactLinksHtml(lang, phone, username, options) {
const { classPrefix, showLabels = true, separator = " ", layout = "inline" } = options || {};
const parts = [];
if (phone && String(phone).trim()) {
const p = String(phone).trim();
const safeHref =
"tel:" +
p.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
const displayPhone = formatPhoneDisplay(p);
if (layout === "block") {
const blockClass = classPrefix + "-block " + classPrefix + "-block--phone";
parts.push(
'<a href="' +
safeHref +
'" class="' +
escapeHtml(blockClass) +
'">' +
ICON_PHONE +
"<span>" +
escapeHtml(displayPhone) +
"</span></a>"
);
} else {
const linkHtml =
'<a href="' +
safeHref +
'" class="' +
escapeHtml(classPrefix + "-link " + classPrefix + "-phone") +
'">' +
escapeHtml(displayPhone) +
"</a>";
if (showLabels) {
const label = t(lang, "contact.phone");
parts.push(
'<span class="' +
escapeHtml(classPrefix) +
'">' +
escapeHtml(label) +
": " +
linkHtml +
"</span>"
);
} else {
parts.push(linkHtml);
}
}
}
if (username && String(username).trim()) {
const u = String(username).trim().replace(/^@+/, "");
if (u) {
const display = "@" + u;
const href = "https://t.me/" + encodeURIComponent(u);
if (layout === "block") {
const blockClass = classPrefix + "-block " + classPrefix + "-block--telegram";
parts.push(
'<a href="' +
escapeHtml(href) +
'" class="' +
escapeHtml(blockClass) +
'" target="_blank" rel="noopener noreferrer">' +
ICON_TELEGRAM +
"<span>" +
escapeHtml(display) +
"</span></a>"
);
} else {
const linkHtml =
'<a href="' +
escapeHtml(href) +
'" class="' +
escapeHtml(classPrefix + "-link " + classPrefix + "-username") +
'" target="_blank" rel="noopener noreferrer">' +
escapeHtml(display) +
"</a>";
if (showLabels) {
const label = t(lang, "contact.telegram");
parts.push(
'<span class="' +
escapeHtml(classPrefix) +
'">' +
escapeHtml(label) +
": " +
linkHtml +
"</span>"
);
} else {
parts.push(linkHtml);
}
}
}
}
if (parts.length === 0) return "";
const rowClass = classPrefix + "-row" + (layout === "block" ? " " + classPrefix + "-row--blocks" : "");
const inner = layout === "block" ? parts.join("") : parts.join(separator);
return '<div class="' + escapeHtml(rowClass) + '">' + inner + "</div>";
}