/** * 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 { Calendar } from "lucide-react"; import { useTranslation } from "@/i18n/use-translation"; import { translate } from "@/i18n/messages"; 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 { triggerHapticLight } from "@/lib/telegram-haptic"; import { ContactLinks } from "@/components/contact/ContactLinks"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen"; import type { DutyWithUser } from "@/types"; import { useTelegramBackButton, useTelegramCloseAction } from "@/hooks/telegram"; import { useScreenReady } from "@/hooks/use-screen-ready"; import { useRequestState } from "@/hooks/use-request-state"; 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; } /** * 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 { initDataRaw } = useTelegramAuth(); const [duty, setDuty] = useState(null); const [remaining, setRemaining] = useState<{ hours: number; minutes: number } | null>(null); const { state: requestState, setLoading, setSuccess, setError, setAccessDenied, isLoading, isError, isAccessDenied, } = useRequestState("loading"); 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); setSuccess(); if (active) { setRemaining(getRemainingTime(active.end_at)); } else { setRemaining(null); } } catch (e) { if (signal?.aborted) return; if (e instanceof AccessDeniedError) { setAccessDenied(e.serverDetail ?? null); setDuty(null); setRemaining(null); } else { setError(translate(lang, "error_generic")); setDuty(null); setRemaining(null); } } }, [initDataRaw, lang, setSuccess, setAccessDenied, setError] ); // 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]); useScreenReady(!isLoading); // 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]); useTelegramBackButton({ enabled: true, onClick: onBack, }); const handleBack = () => { triggerHapticLight(); onBack(); }; const closeMiniAppOrFallback = useTelegramCloseAction(onBack); const handleClose = () => { triggerHapticLight(); closeMiniAppOrFallback(); }; 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 (isLoading) { return (
); } if (isAccessDenied) { return ( ); } if (isError) { const handleRetry = () => { triggerHapticLight(); setLoading(); loadTodayDuties(); }; return (

{requestState.error}

); } 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 remainingValueStr = t("current_duty.remaining_value", { hours: String(rem.hours), minutes: String(rem.minutes), }); 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")} {t("duty.now_on_duty")}

{duty.full_name}

{shiftLabel}

{shiftStr}

{t("current_duty.remaining_label")} {remainingValueStr} {t("current_duty.ends_at", { time: formatHHMM(duty.end_at) })}
{hasContacts ? ( ) : (

{t("current_duty.contact_info_not_set")}

)}
); }