/** * 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"; /** Empty calendar icon for "no duty" state (outline, stroke). */ const ICON_NO_DUTY = ''; /** @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 * @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 ( '
' + '

' + escapeHtml(title) + "

" + '
' + '' + ICON_NO_DUTY + "" + '

' + escapeHtml(noDuty) + "

" + "
" + '" + "
" ); } 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 { 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: " ", layout: "block" }); return ( '
' + '

' + ' ' + escapeHtml(title) + "

" + '

' + escapeHtml(duty.full_name) + "

" + '
' + escapeHtml(shiftLabel) + ": " + escapeHtml(shiftStr) + "
" + '
' + escapeHtml(remainingStr) + "
" + contactHtml + '" + "
" ); } /** * 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 = '
' + escapeHtml(t(lang, "loading")) + "
"; 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 = '
' + '

' + escapeHtml(e.message || t(lang, "error_generic")) + "

" + '" + "
"; } 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; }