import { useState, type FormEvent } from 'react'; import { Link, useParams } from 'react-router-dom'; import { BILAN_FIELD_LABELS, REQUIRED_BILAN_FIELDS, WORK_ORDER_TYPE_LABELS, type BilanField, type WorkOrderDetail, type WorkOrderStatus, } from '@siop/shared'; import { useCommentWorkOrder, usePatchChecklist, useReferenceValues, useSetAssignees, useTransitionWorkOrder, useUpsertReport, useWorkOrder, } from '@/api/exploitation'; import { useUsers } from '@/api/referentiel'; import { usePermissions } from '@/auth/use-permissions'; import { Avatar } from '@/components/avatar'; import { PrioriteOT, StatutOT } from '@/components/chips-ot'; import { Button } from '@/components/ui/button'; import { formatDateFr } from '@/lib/lieux'; const BOUTONS_TRANSITION: Record = { IN_PROGRESS: { libelle: 'Démarrer', variant: 'prim' }, ON_HOLD: { libelle: 'En attente', variant: 'ghost' }, DONE: { libelle: "Clôturer l'intervention", variant: 'succes' }, CANCELLED: { libelle: 'Annuler', variant: 'dangerLeger' }, OPEN: { libelle: 'Rouvrir' }, }; /** Écran 4 validé R0 (+ checklist maquette R2) : tout le carnet sur un écran. */ export default function PageFicheOT() { const { id } = useParams<{ id: string }>(); const { data: ot } = useWorkOrder(id); const { can } = usePermissions(); const transition = useTransitionWorkOrder(id ?? ''); if (!ot) return null; const peutEditer = can('WORK_ORDERS', 'edit'); const cloturable = ot.closureBlockers.length === 0; return ( <>
Ordres de travail / {ot.reference}

{ot.title}

{peutEditer ? ot.allowedTransitions.map((cible) => { const bouton = BOUTONS_TRANSITION[cible]; const bloquee = cible === 'DONE' && !cloturable; return ( ); }) : null}
{transition.isError ? (

{transition.error.message}

) : null}
{ot.checklist.length > 0 ? : null}

Pièces & main-d'œuvre

Consommations et coûts arrivent avec la gestion (R3).

Documents

Photos et pièces jointes arrivent avec la bibliothèque (R3).

); } function CarteIntervention({ ot, peutEditer }: { ot: WorkOrderDetail; peutEditer: boolean }) { const { data: users } = useUsers(); const assignation = useSetAssignees(ot.id); const [enAssignation, setEnAssignation] = useState(false); const techniciens = (users ?? []).filter( (u) => u.status === 'active' && u.role.name.startsWith('Technicien'), ); return (

Intervention

{ot.reference} · {WORK_ORDER_TYPE_LABELS[ot.type]}
Équipement
Ascenseur {ot.assetReference} {' '} — {ot.siteName}
Priorité
Échéance
{formatDateFr(ot.dueDate)}
Assignés
{ot.assignees.length ? ( <> {ot.assignees.map((a) => ( ))} {ot.assignees.map((a) => a.displayName).join(', ')} ) : ( '—' )} {peutEditer ? ( ) : null}
{ot.request ? ( <>
Demande liée
{ot.request.reference} — {ot.request.requesterLabel}
) : null}
{enAssignation ? (
{ot.assignees.length ? ( ) : null}
) : null}
); } const KIND_LABELS: Record = { CREATED: 'OT créé', GENERATED: 'Généré', FROM_REQUEST: 'Créé depuis une demande', STATUS_CHANGED: 'Changement d’état', ASSIGNED: 'Assignation', COMMENT: 'Commentaire', }; function CarteActivite({ ot, peutEditer }: { ot: WorkOrderDetail; peutEditer: boolean }) { const commentaire = useCommentWorkOrder(ot.id); const surEnvoi = (e: FormEvent) => { e.preventDefault(); const form = e.currentTarget; const message = String(new FormData(form).get('message') ?? '').trim(); if (!message) return; commentaire.mutate(message, { onSuccess: () => form.reset() }); }; return (

Activité

{peutEditer ? (
) : null}
    {ot.events.map((e) => (
  • {e.kind === 'COMMENT' ? ( <>Commentaire {e.by ? {e.by.displayName} : null} : « {e.message} » ) : ( <> {KIND_LABELS[e.kind] ?? e.kind} {e.message ? <> — {e.message} : null} )}
    {new Intl.DateTimeFormat('fr-FR', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(e.createdAt))} {e.by && e.kind !== 'COMMENT' ? ` · ${e.by.displayName}` : ''}
  • ))}
); } function CarteChecklist({ ot, peutEditer }: { ot: WorkOrderDetail; peutEditer: boolean }) { const patch = usePatchChecklist(ot.id); const traitees = ot.checklist.filter((c) => c.state !== 'PENDING').length; return (

Checklist du mois{' '} — {traitees} / {ot.checklist.length} traitées

{ot.checklist.map((item) => (
{item.label} {item.doneBy ? `${item.doneBy.displayName.split(' ')[0]} · ${new Intl.DateTimeFormat('fr-FR', { timeStyle: 'short' }).format(new Date(item.doneAt!))}` : '—'}
))} {ot.checklist.some((c) => c.state === 'PENDING') ? (

⚠ La clôture reste bloquée : chaque tâche doit être Fait ou N-A.

) : null}
); } const CHAMPS_BILAN: { champ: BilanField; cle: keyof NonNullable & string; dto: string }[] = [ { champ: 'DOOR_STATE', cle: 'doorState', dto: 'doorStateId' }, { champ: 'CABIN_POSITION', cle: 'cabinPosition', dto: 'cabinPositionId' }, { champ: 'ANOMALY', cle: 'anomaly', dto: 'anomalyId' }, { champ: 'EXTERNAL_CAUSE', cle: 'externalCause', dto: 'externalCauseId' }, { champ: 'ACTION_TAKEN', cle: 'actionTaken', dto: 'actionTakenId' }, { champ: 'COMPONENT_CONCERNED', cle: 'componentConcerned', dto: 'componentConcernedId' }, ]; function CarteBilan({ ot, peutEditer }: { ot: WorkOrderDetail; peutEditer: boolean }) { const { data: valeurs } = useReferenceValues(); const maj = useUpsertReport(ot.id); const bloqueurBilan = ot.closureBlockers.find((b) => b.includes('bilan')); return (

Bilan d'intervention{' '} — requis pour clôturer

{CHAMPS_BILAN.map(({ champ, cle, dto }) => { const options = (valeurs ?? []).filter((v) => v.field === champ && v.isActive); const valeur = ot.report?.[cle as 'doorState'] ?? null; const requis = REQUIRED_BILAN_FIELDS.includes(champ); return (
); })}
{bloqueurBilan && ot.status !== 'DONE' && ot.status !== 'CANCELLED' ? (

⚠ {bloqueurBilan} — la clôture restera bloquée.

) : null} {maj.isError ?

{maj.error.message}

: null}
); }