Files
duty-teller/webapp/js/currentDuty.js
Nikolay Tatarinov 2fb553567f
Some checks failed
CI / lint-and-test (push) Failing after 45s
feat: enhance CI workflow and update webapp styles
- Added Node.js setup and webapp testing steps to the CI workflow for improved integration.
- Updated HTML to link multiple CSS files for better modularity and organization of styles.
- Removed deprecated `style.css` and introduced new CSS files for base styles, calendar, day detail, hints, markers, states, and duty list to enhance maintainability and readability.
- Implemented new styles for improved presentation of duty information and user interactions.
- Added unit tests for new API functions and contact link rendering to ensure functionality and reliability.
2026-03-02 17:20:33 +03:00

202 lines
6.5 KiB
JavaScript

/**
* Current duty view: full-screen card when opened via Mini App deep link (startapp=duty).
* Fetches today's duties, finds the active one (start <= now < end), shows name, shift, contacts.
*/
import { getCurrentDutyViewEl, state, getLoadingEl } from "./dom.js";
import { t } from "./i18n.js";
import { escapeHtml } from "./utils.js";
import { buildContactLinksHtml } from "./contactHtml.js";
import { fetchDuties } from "./api.js";
import {
localDateString,
dateKeyToDDMM,
formatHHMM
} from "./dateUtils.js";
/** @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;
/**
* 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
* @returns {object|null}
*/
export function findCurrentDuty(duties) {
const now = Date.now();
const dutyType = (duties || []).filter((d) => d.event_type === "duty");
const candidates = dutyType.length ? dutyType : duties || [];
for (const d of candidates) {
const start = new Date(d.start_at).getTime();
const end = new Date(d.end_at).getTime();
if (start <= now && now < end) return d;
}
return null;
}
/**
* Render the current duty view content (card with duty or no-duty message).
* @param {object|null} duty - Active duty or null
* @param {string} lang
* @returns {string}
*/
export function renderCurrentDutyContent(duty, lang) {
const backLabel = t(lang, "current_duty.back");
const title = t(lang, "current_duty.title");
if (!duty) {
const noDuty = t(lang, "current_duty.no_duty");
return (
'<div class="current-duty-card">' +
'<h2 class="current-duty-title">' +
escapeHtml(title) +
"</h2>" +
'<p class="current-duty-no-duty">' +
escapeHtml(noDuty) +
"</p>" +
'<button type="button" class="current-duty-back-btn" data-action="back">' +
escapeHtml(backLabel) +
"</button>" +
"</div>"
);
}
const startLocal = localDateString(new Date(duty.start_at));
const endLocal = localDateString(new Date(duty.end_at));
const startDDMM = dateKeyToDDMM(startLocal);
const endDDMM = dateKeyToDDMM(endLocal);
const startTime = formatHHMM(duty.start_at);
const endTime = formatHHMM(duty.end_at);
const shiftStr =
startDDMM +
" " +
startTime +
" — " +
endDDMM +
" " +
endTime;
const shiftLabel = t(lang, "current_duty.shift");
const contactHtml = buildContactLinksHtml(lang, duty.phone, duty.username, {
classPrefix: "current-duty-contact",
showLabels: true,
separator: " "
});
return (
'<div class="current-duty-card">' +
'<h2 class="current-duty-title">' +
escapeHtml(title) +
"</h2>" +
'<p class="current-duty-name">' +
escapeHtml(duty.full_name) +
"</p>" +
'<div class="current-duty-shift">' +
escapeHtml(shiftLabel) +
": " +
escapeHtml(shiftStr) +
"</div>" +
contactHtml +
'<button type="button" class="current-duty-back-btn" data-action="back">' +
escapeHtml(backLabel) +
"</button>" +
"</div>"
);
}
/**
* Show the current duty view: fetch today's duties, render card or no-duty, show back button.
* Hides calendar/duty list and shows #currentDutyView. Optionally shows Telegram BackButton.
* @param {() => void} onBack - Callback when user taps "Back to calendar"
*/
export async function showCurrentDutyView(onBack) {
const currentDutyViewEl = getCurrentDutyViewEl();
const container = currentDutyViewEl && currentDutyViewEl.closest(".container");
const calendarSticky = document.getElementById("calendarSticky");
const dutyList = document.getElementById("dutyList");
if (!currentDutyViewEl) return;
onBackCallback = onBack;
currentDutyViewEl.classList.remove("hidden");
if (container) container.setAttribute("data-view", "currentDuty");
if (calendarSticky) calendarSticky.hidden = true;
if (dutyList) dutyList.hidden = true;
const loadingEl = getLoadingEl();
if (loadingEl) loadingEl.classList.add("hidden");
const lang = state.lang;
currentDutyViewEl.innerHTML =
'<div class="current-duty-loading">' +
escapeHtml(t(lang, "loading")) +
"</div>";
if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.BackButton) {
window.Telegram.WebApp.BackButton.show();
const handler = () => {
if (onBackCallback) onBackCallback();
};
backButtonHandler = handler;
window.Telegram.WebApp.BackButton.onClick(handler);
}
const today = new Date();
const from = localDateString(today);
const to = from;
try {
const duties = await fetchDuties(from, to);
const duty = findCurrentDuty(duties);
currentDutyViewEl.innerHTML = renderCurrentDutyContent(duty, lang);
} catch (e) {
currentDutyViewEl.innerHTML =
'<div class="current-duty-card">' +
'<p class="current-duty-error">' +
escapeHtml(e.message || t(lang, "error_generic")) +
"</p>" +
'<button type="button" class="current-duty-back-btn" data-action="back">' +
escapeHtml(t(lang, "current_duty.back")) +
"</button>" +
"</div>";
}
currentDutyViewEl.addEventListener("click", handleCurrentDutyClick);
}
/**
* Delegate click for back button.
* @param {MouseEvent} e
*/
function handleCurrentDutyClick(e) {
const btn = e.target && e.target.closest("[data-action='back']");
if (!btn) return;
if (onBackCallback) onBackCallback();
}
/**
* Hide the current duty view and show calendar/duty list again.
* Hides Telegram BackButton and calls loadMonth so calendar is populated.
*/
export function hideCurrentDutyView() {
const currentDutyViewEl = getCurrentDutyViewEl();
const container = currentDutyViewEl && currentDutyViewEl.closest(".container");
const calendarSticky = document.getElementById("calendarSticky");
const dutyList = document.getElementById("dutyList");
if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.BackButton) {
if (backButtonHandler) {
window.Telegram.WebApp.BackButton.offClick(backButtonHandler);
}
window.Telegram.WebApp.BackButton.hide();
}
if (currentDutyViewEl) {
currentDutyViewEl.removeEventListener("click", handleCurrentDutyClick);
currentDutyViewEl.classList.add("hidden");
currentDutyViewEl.innerHTML = "";
}
onBackCallback = null;
backButtonHandler = null;
if (container) container.removeAttribute("data-view");
if (calendarSticky) calendarSticky.hidden = false;
if (dutyList) dutyList.hidden = false;
}