- 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.
151 lines
5.9 KiB
JavaScript
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, "&").replace(/"/g, """).replace(/</g, "<");
|
|
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>";
|
|
}
|