'use client'; import { useMemo, useState } from 'react'; import Link from 'next/link'; import { ArrowLeft, Printer, AlertCircle } from 'lucide-react'; import { useTranslations, useFormatter } from 'next-intl'; import { trpc } from '@/lib/trpc/client'; import { SHIFTS, shiftWindow, todayWindow, type ShiftKey } from '@/lib/shifts'; // ── Duration helper ───────────────────────────────────────────────────────── type TFn = ReturnType>; function formatDuration(ms: number | null, t: TFn): string { if (ms === null) return t('duration.dash'); const totalMin = Math.round(ms / 60_000); if (totalMin < 1) return t('duration.lessThan1Min'); if (totalMin < 60) return t('duration.minutes', { n: totalMin }); const h = Math.floor(totalMin / 60); const m = totalMin % 60; return m > 0 ? t('duration.hoursMinutes', { h, m }) : t('duration.hours', { h }); } // ── Status ─────────────────────────────────────────────────────────────────── const STATUS_CLASS: Record<'OPEN' | 'CLAIMED', string> = { OPEN: 'bg-orange-100 text-orange-700', CLAIMED: 'bg-blue-100 text-blue-700', }; // ── Metric card ────────────────────────────────────────────────────────────── function MetricCard({ label, value, sub }: { label: string; value: string; sub?: string }) { return (

{label}

{value}

{sub &&

{sub}

}
); } // ── Main component ─────────────────────────────────────────────────────────── type WindowState = | { type: 'shift'; key: ShiftKey; day: Date } | { type: 'today' } | { type: 'custom'; from: Date; to: Date }; function computeWindow(state: WindowState): { from: Date; to: Date } { const now = new Date(); if (state.type === 'today') return todayWindow(now); if (state.type === 'shift') return shiftWindow(state.key, state.day); return { from: state.from, to: state.to }; } function localDateStr(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } function localDateTimeStr(d: Date): string { return `${localDateStr(d)}T${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; } const DATE_TIME_FMT = { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' } as const; const DATE_FMT = { day: '2-digit', month: '2-digit' } as const; export function ReportView() { const t = useTranslations('report'); const tc = useTranslations('common'); const format = useFormatter(); const [windowState, setWindowState] = useState({ type: 'today' }); const [dayInput, setDayInput] = useState(() => localDateStr(new Date())); const [customActive, setCustomActive] = useState(false); const [customPending, setCustomPending] = useState(() => { const now = new Date(); const midnight = new Date(now); midnight.setHours(0, 0, 0, 0); return { from: localDateTimeStr(midnight), to: localDateTimeStr(now) }; }); // Stabilise the window so the query key only changes when the user picks a // new window. Without this, the 'today' mode recomputes `to = new Date()` on // every render → new query key → fetch loop. Re-selecting "Today" refreshes. const win = useMemo(() => computeWindow(windowState), [windowState]); const { data, isLoading, error } = trpc.maintenanceRequest.report.useQuery( { from: win.from, to: win.to }, { staleTime: 30_000 }, ); function selectShift(key: ShiftKey) { const day = new Date(dayInput + 'T00:00:00'); setCustomActive(false); setWindowState({ type: 'shift', key, day }); } function selectToday() { setCustomActive(false); setWindowState({ type: 'today' }); } function applyCustom() { const from = new Date(customPending.from); const to = new Date(customPending.to); if (isNaN(from.getTime()) || isNaN(to.getTime()) || to <= from) return; setWindowState({ type: 'custom', from, to }); } const activeShift = windowState.type === 'shift' ? windowState.key : null; const range = `${format.dateTime(win.from, DATE_TIME_FMT)} → ${format.dateTime(win.to, DATE_TIME_FMT)}`; const windowLabelText = windowState.type === 'today' ? t('windowLabel.today', { range }) : windowState.type === 'shift' ? t(`windowLabel.${windowState.key}`, { range }) : t('windowLabel.custom', { range }); return (
{/* ── Header (hidden in print) ── */}
{t('backToQueue')} /

{t('title')}

{/* ── Print header (only in print) ── */}

{t('printHeader')}

{range}

{/* ── Window selector (hidden in print) ── */}
{/* Shift shortcuts + day picker */}
{(Object.keys(SHIFTS) as ShiftKey[]).map((key) => ( ))} { setDayInput(e.target.value); if (windowState.type === 'shift') { const day = new Date(e.target.value + 'T00:00:00'); setWindowState({ type: 'shift', key: windowState.key, day }); } }} className="rounded-lg border border-border bg-card px-2 py-1 text-sm" />
{/* Custom range inputs */} {customActive && (
setCustomPending((p) => ({ ...p, from: e.target.value }))} className="rounded-lg border border-border bg-card px-2 py-1 text-sm" /> {t('customUntil')} setCustomPending((p) => ({ ...p, to: e.target.value }))} className="rounded-lg border border-border bg-card px-2 py-1 text-sm" />
)} {/* Active window label */}

{windowLabelText}

{/* ── Body ── */}
{isLoading && (

{tc('loading')}

)} {error && (

{error.message}

)} {data && data.totals.created === 0 && (

{t('emptyWindow')}

)} {data && data.totals.created > 0 && (
{/* Summary cards */}

{t('sections.summary')}

0 || data.totals.claimed > 0 ? t('metrics.openSub', { open: data.totals.open, claimed: data.totals.claimed }) : undefined } /> 0 ? t('metrics.requestsSub', { count: data.responseMs.count }) : t('metrics.noData') } /> 0 ? t('metrics.requestsSub', { count: data.resolutionMs.count }) : t('metrics.noData') } />
{/* By workstation */} {data.byWorkstation.length > 0 && (

{t('sections.byWorkstation')}

{data.byWorkstation.map((ws, i) => ( ))}
{t('table.code')} {t('table.name')} {t('table.area')} {t('table.requests')}
{ws.code} {ws.name} {ws.area} {ws.count}
)} {/* By area */} {data.byArea.length > 1 && (

{t('sections.byArea')}

{data.byArea.map((a) => (
{a.area} {a.count}
))}
)} {/* Still open */}

{t('sections.stillOpen')}

{data.stillOpen.length === 0 ? (

{t('allClear')}

) : (
{data.stillOpen.map((r) => (

{r.code} — {r.name}{' '} · {r.area}

{r.description}

{t('stillOpenReportedBy', { email: r.reportedByEmail, date: format.dateTime(new Date(r.createdAt), DATE_FMT), })}

{tc(`status.${r.status.toLowerCase() as 'open' | 'claimed'}`)}
))}
)}
)}
); }