import { ActionIcon, Badge, Button, Group, Popover, Stack, Text, Tooltip, } from "@mantine/core"; import { DateTimePicker } from "@mantine/dates"; import { useMutation } from "@tanstack/react-query"; import { Pencil, PlayCircle, StopCircle } from "lucide-react"; import { useEffect, useState } from "react"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; import { useToast } from "@/hooks/use-toast"; import { api } from "@/services/api"; import type { StationWorkPhaseLog } from "@/types/trainScheduling"; const parseError = (error: unknown, fallback: string) => { const message = (error as { response?: { data?: { message?: string | string[] } } }) ?.response?.data?.message; if (Array.isArray(message)) return message.join("; "); return message || (error as Error)?.message || fallback; }; const fmtTime = (iso: string) => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? iso : d.toLocaleString(); }; const fmtElapsed = (fromIso: string, toIso?: string | null) => { const from = new Date(fromIso).getTime(); const to = toIso ? new Date(toIso).getTime() : Date.now(); const mins = Math.max(0, Math.round((to - from) / 60_000)); const h = Math.floor(mins / 60); const m = mins % 60; return h > 0 ? `${h}h ${m}m` : `${m}m`; }; /** Pencil-popover to correct an already-recorded start/end timestamp. */ function EditTimeButton({ label, value, disabled, disabledReason, minDate, maxDate, onSave, saving, }: { label: string; value: string; disabled: boolean; disabledReason: string; minDate?: Date; maxDate?: Date; onSave: (at: Date) => void; saving: boolean; }) { const [opened, setOpened] = useState(false); const [draft, setDraft] = useState(null); useEffect(() => { if (opened) setDraft(new Date(value)); }, [opened, value]); return ( setOpened((o) => !o)} > setDraft(v ? new Date(v) : null)} minDate={minDate} maxDate={maxDate ?? new Date()} valueFormat="DD MMM YYYY HH:mm" clearable={false} radius="md" maw={280} /> ); } /** * Start/End buttons + elapsed time for one station's loading OR unloading * window. Booking load/unload at the yard is server-gated on the window having * been started, so these buttons come first in the operator's flow. Each of * the four buttons (start/end × loading/unloading) is its own permission, and * the pencil edits a recorded time under the same permission that set it. */ export function StationWorkControls({ scheduleId, yardId, phase, log, }: { scheduleId: string; yardId: string; phase: "loading" | "unloading"; log?: StationWorkPhaseLog | null; }) { const { user } = useAuth(); const { toast } = useToast(); const canStart = hasPermission( user, phase === "loading" ? FREIGHT_PERMS.trainScheduling.loadingStart : FREIGHT_PERMS.trainScheduling.unloadingStart, ); const canEnd = hasPermission( user, phase === "loading" ? FREIGHT_PERMS.trainScheduling.loadingEnd : FREIGHT_PERMS.trainScheduling.unloadingEnd, ); const record = useMutation(api.trainScheduling.recordStationWork.mutationOptions()); // Re-render each minute so the running elapsed time ticks while unended. const [, setTick] = useState(0); useEffect(() => { if (!log?.startedAt || log?.endedAt) return; const t = setInterval(() => setTick((n) => n + 1), 60_000); return () => clearInterval(t); }, [log?.startedAt, log?.endedAt]); const doRecord = (edge: "start" | "end", at?: Date) => { record.mutate( { scheduleId, yardId, phase, edge, ...(at ? { at: at.toISOString() } : {}) }, { onSuccess: () => toast({ title: `${phase === "loading" ? "Loading" : "Unloading"} ${edge} recorded`, }), onError: (err) => toast({ title: `Could not record ${phase} ${edge}`, description: parseError(err, "Please try again"), variant: "destructive", }), }, ); }; const title = phase === "loading" ? "Loading" : "Unloading"; const started = Boolean(log?.startedAt); const ended = Boolean(log?.endedAt); return ( {title} {ended ? " done" : started ? " in progress" : " not started"} {!started ? ( ) : ( <> {fmtTime(log!.startedAt!)} → {ended ? fmtTime(log!.endedAt!) : "…"} ( {fmtElapsed(log!.startedAt!, log?.endedAt)}) {log?.startedByName || log?.endedByName ? ( {log?.endedByName ?? log?.startedByName} ) : null} doRecord("start", at)} saving={record.isPending} /> {ended ? ( doRecord("end", at)} saving={record.isPending} /> ) : null} {!ended ? ( ) : null} )} ); }