feat: enhance duty information handling with contact details and current duty view
- Added `bot_username` to settings for dynamic retrieval of the bot's username. - Implemented `_resolve_bot_username` function to fetch the bot's username if not set, improving user experience in group chats. - Updated `DutyWithUser` schema to include optional `phone` and `username` fields for enhanced duty information. - Enhanced API responses to include contact details for users, ensuring better communication. - Introduced a new current duty view in the web app, displaying active duty information along with contact options. - Updated CSS styles for better presentation of contact information in duty cards. - Added unit tests to verify the inclusion of contact details in API responses and the functionality of the current duty view.
This commit is contained in:
@@ -35,6 +35,43 @@ function parseDataAttr(raw) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build HTML for contact info (phone link, Telegram username link) for a duty entry.
|
||||
* @param {'ru'|'en'} lang
|
||||
* @param {string|null|undefined} phone
|
||||
* @param {string|null|undefined} username - Telegram username with or without leading @
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildContactHtml(lang, phone, username) {
|
||||
const parts = [];
|
||||
if (phone && String(phone).trim()) {
|
||||
const p = String(phone).trim();
|
||||
const label = t(lang, "contact.phone");
|
||||
const safeHref = "tel:" + p.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
||||
parts.push(
|
||||
'<span class="day-detail-contact">' +
|
||||
escapeHtml(label) + ": " +
|
||||
'<a href="' + safeHref + '" class="day-detail-contact-link day-detail-contact-phone">' +
|
||||
escapeHtml(p) + "</a></span>"
|
||||
);
|
||||
}
|
||||
if (username && String(username).trim()) {
|
||||
const u = String(username).trim().replace(/^@+/, "");
|
||||
if (u) {
|
||||
const label = t(lang, "contact.telegram");
|
||||
const display = "@" + u;
|
||||
const href = "https://t.me/" + encodeURIComponent(u);
|
||||
parts.push(
|
||||
'<span class="day-detail-contact">' +
|
||||
escapeHtml(label) + ": " +
|
||||
'<a href="' + escapeHtml(href) + '" class="day-detail-contact-link day-detail-contact-username" target="_blank" rel="noopener noreferrer">' +
|
||||
escapeHtml(display) + "</a></span>"
|
||||
);
|
||||
}
|
||||
}
|
||||
return parts.length ? '<div class="day-detail-contact-row">' + parts.join(" ") + "</div>" : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build HTML content for the day detail panel.
|
||||
* @param {string} dateKey - YYYY-MM-DD
|
||||
@@ -72,7 +109,12 @@ export function buildDayDetailContent(dateKey, duties, eventSummaries) {
|
||||
);
|
||||
const rows = hasTimes
|
||||
? getDutyMarkerRows(dutyList, dateKey, nbsp, fromLabel, toLabel)
|
||||
: dutyList.map((it) => ({ timePrefix: "", fullName: it.full_name || "" }));
|
||||
: dutyList.map((it) => ({
|
||||
timePrefix: "",
|
||||
fullName: it.full_name || "",
|
||||
phone: it.phone,
|
||||
username: it.username
|
||||
}));
|
||||
|
||||
html +=
|
||||
'<section class="day-detail-section day-detail-section--duty">' +
|
||||
@@ -80,12 +122,17 @@ export function buildDayDetailContent(dateKey, duties, eventSummaries) {
|
||||
escapeHtml(t(lang, "event_type.duty")) +
|
||||
"</h3><ul class=" +
|
||||
'"day-detail-list">';
|
||||
rows.forEach((r) => {
|
||||
rows.forEach((r, i) => {
|
||||
const duty = hasTimes ? dutyList[i] : null;
|
||||
const phone = r.phone != null ? r.phone : (duty && duty.phone);
|
||||
const username = r.username != null ? r.username : (duty && duty.username);
|
||||
const timeHtml = r.timePrefix ? escapeHtml(r.timePrefix) + " — " : "";
|
||||
const contactHtml = buildContactHtml(lang, phone, username);
|
||||
html +=
|
||||
"<li>" +
|
||||
(timeHtml ? '<span class="day-detail-time">' + timeHtml + "</span>" : "") +
|
||||
escapeHtml(r.fullName) +
|
||||
(contactHtml ? contactHtml : "") +
|
||||
"</li>";
|
||||
});
|
||||
html += "</ul></section>";
|
||||
|
||||
Reference in New Issue
Block a user