'use client'; import { useMemo, useState } from 'react'; import Link from 'next/link'; import { ArrowLeft, Printer, AlertCircle } from 'lucide-react'; import { trpc } from '@/lib/trpc/client'; import { SHIFTS, shiftWindow, todayWindow, type ShiftKey } from '@/lib/shifts'; // ── Duration helper ───────────────────────────────────────────────────────── function formatDuration(ms: number | null): string { if (ms === null) return '—'; const totalMin = Math.round(ms / 60_000); if (totalMin < 1) return '< 1 min'; if (totalMin < 60) return `${totalMin} min`; const h = Math.floor(totalMin / 60); const m = totalMin % 60; return m > 0 ? `${h} h ${m} min` : `${h} h`; } function formatDateTime(d: Date | string): string { const dt = new Date(d); return dt.toLocaleString('pt-PT', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', }); } function formatDate(d: Date | string): string { return new Date(d).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit' }); } // ── Window label ───────────────────────────────────────────────────────────── function windowLabel(from: Date, to: Date): string { return `${formatDateTime(from)} → ${formatDateTime(to)}`; } // ── Metric card ────────────────────────────────────────────────────────────── function MetricCard({ label, value, sub }: { label: string; value: string; sub?: string }) { return (

{label}

{value}

{sub &&

{sub}

}
); } const STATUS_LABEL: Record<'OPEN' | 'CLAIMED', string> = { OPEN: 'Aberto', CLAIMED: 'Em curso', }; const STATUS_CLASS: Record<'OPEN' | 'CLAIMED', string> = { OPEN: 'bg-orange-100 text-orange-700', CLAIMED: 'bg-blue-100 text-blue-700', }; // ── 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')}`; } export function ReportView() { 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 "Hoje" 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; return (
{/* ── Header (hidden in print) ── */}
Fila /

Relatório de turno

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

FieldOps — Relatório de manutenção

{windowLabel(win.from, win.to)}

{/* ── 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" /> até setCustomPending((p) => ({ ...p, to: e.target.value }))} className="rounded-lg border border-border bg-card px-2 py-1 text-sm" />
)} {/* Active window label */}

{windowState.type === 'shift' ? `Turno d${windowState.key === 'manha' ? 'a Manhã' : windowState.key === 'tarde' ? 'a Tarde' : 'a Noite'} — ` : windowState.type === 'today' ? 'Hoje — ' : 'Personalizado — '} {windowLabel(win.from, win.to)}

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

A carregar…

)} {error && (

{error.message}

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

Sem pedidos nesta janela.

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

Resumo

0 || data.totals.claimed > 0 ? `${data.totals.open} aberto · ${data.totals.claimed} em curso` : undefined } /> 0 ? `sobre ${data.responseMs.count} pedido${data.responseMs.count > 1 ? 's' : ''}` : 'sem dados' } /> 0 ? `sobre ${data.resolutionMs.count} pedido${data.resolutionMs.count > 1 ? 's' : ''}` : 'sem dados' } />
{/* By workstation */} {data.byWorkstation.length > 0 && (

Por posto

{data.byWorkstation.map((ws, i) => ( ))}
Código Nome Área Pedidos
{ws.code} {ws.name} {ws.area} {ws.count}
)} {/* By area */} {data.byArea.length > 1 && (

Por área

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

Em aberto à hora do relatório

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

Nada em aberto neste turno. ✓

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

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

{r.description}

Reportado por {r.reportedByEmail} · {formatDate(r.createdAt)}

{STATUS_LABEL[r.status]}
))}
)}
)}
); }