/** * Current duty view: full-screen card when opened via Mini App deep link (startapp=duty). * Fetches today's duties, finds the active one, shows name, shift, auto-updating remaining time, * and contact links. Integrates with Telegram BackButton. * Ported from webapp/js/currentDuty.js. */ "use client"; import { useEffect, useState, useCallback } from "react"; import { backButton, closeMiniApp } from "@telegram-apps/sdk-react"; import { Calendar } from "lucide-react"; import { useTranslation } from "@/i18n/use-translation"; import { useAppStore } from "@/store/app-store"; import { useTelegramAuth } from "@/hooks/use-telegram-auth"; import { fetchDuties, AccessDeniedError } from "@/lib/api"; import { localDateString, dateKeyToDDMM, formatHHMM, } from "@/lib/date-utils"; import { getRemainingTime, findCurrentDuty } from "@/lib/current-duty"; import { ContactLinks } from "@/components/contact/ContactLinks"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import type { DutyWithUser } from "@/types"; export interface CurrentDutyViewProps { /** Called when user taps Back (in-app button or Telegram BackButton). */ onBack: () => void; /** True when opened via pin button (startParam=duty). Shows Close instead of Back to calendar. */ openedFromPin?: boolean; } type ViewState = "loading" | "error" | "ready"; /** * Full-screen current duty view with Telegram BackButton and auto-updating remaining time. */ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyViewProps) { const { t } = useTranslation(); const lang = useAppStore((s) => s.lang); const setAppContentReady = useAppStore((s) => s.setAppContentReady); const { initDataRaw } = useTelegramAuth(); const [state, setState] = useState("loading"); const [duty, setDuty] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [remaining, setRemaining] = useState<{ hours: number; minutes: number } | null>(null); const loadTodayDuties = useCallback( async (signal?: AbortSignal | null) => { const today = new Date(); const from = localDateString(today); const to = from; const initData = initDataRaw ?? ""; try { const duties = await fetchDuties(from, to, initData, lang, signal); if (signal?.aborted) return; const active = findCurrentDuty(duties); setDuty(active); setState("ready"); if (active) { setRemaining(getRemainingTime(active.end_at)); } else { setRemaining(null); } } catch (e) { if (signal?.aborted) return; setState("error"); const msg = e instanceof AccessDeniedError && e.serverDetail ? e.serverDetail : t("error_generic"); setErrorMessage(msg); setDuty(null); setRemaining(null); } }, [initDataRaw, lang, t] ); // Fetch today's duties on mount; abort on unmount to avoid setState after unmount. useEffect(() => { const controller = new AbortController(); loadTodayDuties(controller.signal); return () => controller.abort(); }, [loadTodayDuties]); // Mark content ready when data is loaded or error, so page can call ready() and show content. useEffect(() => { if (state !== "loading") { setAppContentReady(true); } }, [state, setAppContentReady]); // Auto-update remaining time every second when there is an active duty. useEffect(() => { if (!duty) return; const interval = setInterval(() => { setRemaining(getRemainingTime(duty.end_at)); }, 1000); return () => clearInterval(interval); }, [duty]); // Telegram BackButton: show on mount, hide on unmount, handle click. useEffect(() => { let offClick: (() => void) | undefined; try { if (backButton.mount.isAvailable()) { backButton.mount(); } if (backButton.show.isAvailable()) { backButton.show(); } if (backButton.onClick.isAvailable()) { offClick = backButton.onClick(onBack); } } catch { // Non-Telegram environment; BackButton not available. } return () => { try { if (typeof offClick === "function") offClick(); if (backButton.hide.isAvailable()) { backButton.hide(); } } catch { // Ignore cleanup errors in non-Telegram environment. } }; }, [onBack]); const handleBack = () => { onBack(); }; const handleClose = () => { if (closeMiniApp.isAvailable()) { closeMiniApp(); } else { onBack(); } }; const primaryButtonLabel = openedFromPin ? t("current_duty.close") : t("current_duty.back"); const primaryButtonAriaLabel = openedFromPin ? t("current_duty.close") : t("current_duty.back"); const handlePrimaryAction = openedFromPin ? handleClose : handleBack; if (state === "loading") { return (

{t("loading")}

); } if (state === "error") { const handleRetry = () => { setState("loading"); loadTodayDuties(); }; return (

{errorMessage}

); } if (!duty) { return (
{t("current_duty.title")}

{t("current_duty.no_duty")}

); } 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 rem = remaining ?? getRemainingTime(duty.end_at); const remainingStr = t("current_duty.remaining", { hours: String(rem.hours), minutes: String(rem.minutes), }); const endsAtStr = t("current_duty.ends_at", { time: endTime }); const displayTz = typeof window !== "undefined" && (window as unknown as { __DT_TZ?: string }).__DT_TZ; const shiftLabel = displayTz ? t("current_duty.shift_tz", { tz: displayTz }) : t("current_duty.shift_local"); const hasContacts = Boolean(duty.phone && String(duty.phone).trim()) || Boolean(duty.username && String(duty.username).trim()); return (
{t("current_duty.title")}

{duty.full_name}

{shiftLabel} {shiftStr}

{remainingStr}

{endsAtStr}

{hasContacts ? ( ) : (

{t("current_duty.contact_info_not_set")}

)}
); }