import { Head, router, usePage } from '@inertiajs/react'; import { AlertTriangle, CheckCircle2, ClipboardCheck, PackageCheck, RotateCcw, ScanLine, } from 'lucide-react'; import { useState } from 'react'; import InputError from '@/components/input-error'; import { PageHeading } from '@/components/page-heading'; import { ReservationStatus } from '@/components/reservation-status'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; type HandoverRecord = { id: number; type: 'checkout' | 'checkin'; condition: string | null; notes: string | null; created_at: string; operator: { name: string }; }; type OperationReservation = { id: number; uuid: string; reference: string; purpose: string; usage_location: string; starts_at: string; ends_at: string; status: string; requester: { name: string; email: string; department: string | null; snipe_user_id: number | null; }; items: { id: number; status: string; asset: { snipe_asset_id: number; name: string; asset_tag: string; status_name: string | null; location_name: string | null; }; handover_records: HandoverRecord[]; }[]; }; type FormState = { snipeUserId: string; condition: string; notes: string; }; export default function OperationsIndex({ reservations, operationallyConfigured, }: { reservations: OperationReservation[]; operationallyConfigured: boolean; }) { const { errors } = usePage().props; const [forms, setForms] = useState>({}); const [incidents, setIncidents] = useState< Record >({}); const [search, setSearch] = useState(''); const normalizedSearch = search.trim().toLocaleLowerCase('pt-PT'); const visibleReservations = reservations.filter((reservation) => { if (!normalizedSearch) { return true; } return [ reservation.reference, reservation.requester.name, reservation.requester.email, ...reservation.items.flatMap((item) => [ item.asset.asset_tag, item.asset.snipe_asset_id.toString(), ]), ].some((value) => value.toLocaleLowerCase('pt-PT').includes(normalizedSearch), ); }); function formFor(reservation: OperationReservation): FormState { return ( forms[reservation.id] ?? { snipeUserId: reservation.requester.snipe_user_id?.toString() ?? '', condition: 'Sem danos visíveis', notes: '', } ); } function updateForm( reservation: OperationReservation, key: keyof FormState, value: string, ) { setForms((current) => ({ ...current, [reservation.id]: { ...formFor(reservation), [key]: value }, })); } function post( reservation: OperationReservation, action: string, data = {}, ) { router.post(`/operacoes/${reservation.uuid}/${action}`, data, { preserveScroll: true, }); } return ( <>
{!operationallyConfigured && (

Movimentos externos indisponíveis

Configure SNIPE_IT_OPERATIONAL_TOKEN para ativar entregas e devoluções. A preparação continua disponível.

)}
{visibleReservations.length === 0 && (

{search ? 'Nenhuma operação corresponde à leitura ou pesquisa.' : 'Não há operações pendentes para hoje.'}

)} {visibleReservations.map((reservation) => { const form = formFor(reservation); const canReturn = [ 'delivered', 'in_use', 'overdue', 'incident', ].includes(reservation.status) && reservation.items.some( (item) => item.status === 'delivered', ); const canDeliver = reservation.items.some( (item) => item.status === 'ready', ); const canComplete = reservation.status === 'returned' || (reservation.status === 'incident' && reservation.items.every((item) => ['returned', 'cancelled'].includes( item.status, ), )); return (
{reservation.reference}

{reservation.requester.name} ·{' '} {reservation.usage_location}

{formatPeriod( reservation.starts_at, reservation.ends_at, )}

{reservation.items .filter( (item) => item.status !== 'cancelled', ) .map((item) => (

{ item.asset .name }

{ item.asset .asset_tag }{' '} ·{' '} {item.asset .location_name ?? 'Sem localização'}

{item.status}
{item.handover_records .length > 0 && (
{item.handover_records.map( (record) => (

{record.type === 'checkout' ? 'Entregue' : 'Devolvido'}{' '} por{' '} { record .operator .name }{' '} ·{' '} { record.condition }

), )}
)} {[ 'ready', 'delivered', 'in_use', 'overdue', 'returned', ].includes( reservation.status, ) && ( setIncidents( ( current, ) => ({ ...current, [item.id]: value, }), ) } onSubmit={() => { const incident = incidents[ item.id ] ?? { notes: '', blockedUntil: '', }; router.post( `/operacoes/${reservation.uuid}/itens/${item.id}/incidente`, { notes: incident.notes, blocked_until: incident.blockedUntil || null, }, { preserveScroll: true, }, ); }} /> )}
))}
{reservation.status === 'approved' && ( )} {canDeliver && (
)} {canReturn && (
{[ 'delivered', 'in_use', ].includes( reservation.status, ) && ( )}
)} {canComplete && ( )}
); })}
); } function IncidentForm({ value, onChange, onSubmit, }: { itemId: number; value: { notes: string; blockedUntil: string }; onChange: (value: { notes: string; blockedUntil: string }) => void; onSubmit: () => void; }) { return (
Registar incidente
onChange({ ...value, notes: event.target.value }) } />
); } function formatPeriod(startsAt: string, endsAt: string) { const formatter = new Intl.DateTimeFormat('pt-PT', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', }); return `${formatter.format(new Date(startsAt))} — ${formatter.format(new Date(endsAt))}`; } OperationsIndex.layout = { breadcrumbs: [{ title: 'Operações', href: '/operacoes' }], };