import { computed, reactive, ref } from 'vue' import { todayIso } from '~/shared/utils/date' import type { WeighbridgeMode } from '~/modules/logistique/composables/useWeighbridge' /** * État et logique du formulaire « Ajouter / Modifier un ticket de pesée » (M5, * ERP-189). L'écran est composé de DEUX blocs empilés — pesée à vide puis pesée * à plein — qui partagent un même véhicule. * * Points clés (spec-front § Écran Ajouter, spec-back § 2.4 / 2.9 / 2.10) : * - **Contrepartie conditionnelle (RG-5.03)** : `counterpartyType` (CLIENT / * FOURNISSEUR / AUTRE) pilote le champ requis (client / supplier / otherLabel). * Changer de type purge les champs des autres types — aucune donnée fantôme. * - **Immatriculation + « Tout format » partagés entre les 2 blocs (RG-5.01)** : * une seule valeur (refs uniques) — modifier l'un met à jour l'autre puisque * les 2 blocs bindent la même ref. * - **Workflow 2 temps** : `buildCreatePayload()` (POST à l'« Enregistrer » du * bloc vide) crée le ticket avec la pesée à vide ; `buildFullPayload()` (PATCH * au « Valider ») ajoute la pesée à plein (net recalculé serveur, RG-5.05). * * Composable UI-agnostique et testable : aucune dépendance API ici (les appels * vivent dans l'écran via `useApi`). Instancié PAR écran (refs locales). */ /** Type de contrepartie — miroir de l'enum back (spec-back § 2.9). */ export type CounterpartyType = 'CLIENT' | 'FOURNISSEUR' | 'AUTRE' /** Saisie d'une pesée (bloc vide OU bloc plein). */ export interface WeighingBlockState { /** Date de la pesée (ISO `YYYY-MM-DD`) — jour par défaut (RG-5.07). */ date: string | null /** Poids en kg — readonly, rempli par la pesée (bascule ou manuelle). */ weight: number | null /** DSD — readonly, rempli par la pesée (RG-5.04). */ dsd: number | null /** Mode de la dernière pesée appliquée au bloc. */ mode: WeighbridgeMode | null /** Numéro de pesée (rempli uniquement en pesée manuelle). */ manualNumber: string | null } /** Forme minimale d'un détail de ticket consommée par `hydrate` (cf. useWeighingTicket). */ export interface WeighingTicketHydration { id: number counterpartyType: CounterpartyType client?: { '@id': string } | null supplier?: { '@id': string } | null otherLabel?: string | null immatriculation?: string | null plateFreeFormat?: boolean emptyDate?: string | null emptyWeight?: number | null emptyDsd?: number | null emptyMode?: WeighbridgeMode | null emptyManualNumber?: string | null fullDate?: string | null fullWeight?: number | null fullDsd?: number | null fullMode?: WeighbridgeMode | null fullManualNumber?: string | null } /** Extrait la partie date `YYYY-MM-DD` d'une chaîne ISO (datetime back) — null si absente. */ function isoDateOnly(value: string | null | undefined): string | null { return value ? value.slice(0, 10) : null } /** Crée l'état initial d'un bloc de pesée (date = aujourd'hui, RG-5.07). */ function emptyBlock(today: string): WeighingBlockState { return { date: today, weight: null, dsd: null, mode: null, manualNumber: null, } } export function useWeighingTicketForm() { const today = todayIso() // ── Contrepartie (RG-5.03) ─────────────────────────────────────────────── const counterpartyType = ref(null) const clientIri = ref(null) const supplierIri = ref(null) const otherLabel = ref(null) /** * Change le type de contrepartie et purge les champs devenus hors-sujet : * un seul de client / supplier / otherLabel est conservé selon le type * (RG-5.03 — pas de FK fantôme envoyée au back). */ function setCounterpartyType(type: CounterpartyType | null): void { counterpartyType.value = type if (type !== 'CLIENT') clientIri.value = null if (type !== 'FOURNISSEUR') supplierIri.value = null if (type !== 'AUTRE') otherLabel.value = null } // ── Véhicule : partagé entre les 2 blocs (RG-5.01) ──────────────────────── // Refs UNIQUES : les 2 blocs bindent la même valeur → connexion automatique. const immatriculation = ref(null) const plateFreeFormat = ref(false) // ── Les deux pesées ─────────────────────────────────────────────────────── const empty = reactive(emptyBlock(today)) const full = reactive(emptyBlock(today)) // Id du ticket créé (POST du bloc vide) — pilote le PATCH du bloc plein. const ticketId = ref(null) /** * Champ de contrepartie attendu selon le type courant — utilisé par l'écran * pour afficher conditionnellement le bon champ (RG-5.03). */ const counterpartyField = computed<'client' | 'supplier' | 'other' | null>(() => { switch (counterpartyType.value) { case 'CLIENT': return 'client' case 'FOURNISSEUR': return 'supplier' case 'AUTRE': return 'other' default: return null } }) /** Applique une lecture de pesée (bascule/manuelle) à un bloc. */ function applyReading( block: WeighingBlockState, reading: { weight: number, dsd: number, mode: WeighbridgeMode, manualNumber?: string }, ): void { block.weight = reading.weight block.dsd = reading.dsd block.mode = reading.mode block.manualNumber = reading.manualNumber ?? null } /** Partie « contrepartie » du payload (FK en IRI ou libellé libre). */ function counterpartyPayload(): Record { switch (counterpartyType.value) { case 'CLIENT': return { client: clientIri.value } case 'FOURNISSEUR': return { supplier: supplierIri.value } case 'AUTRE': return { otherLabel: otherLabel.value || null } default: return {} } } /** * Payload de CRÉATION (POST /weighing_tickets, spec-back § 4.3) : contrepartie * + véhicule + pesée à VIDE. Le numéro, le site et le net sont attribués * serveur (rien à envoyer). Les noms de champs miroir des `propertyPath` back * pour que `useFormErrors` mappe les 422 inline. */ function buildCreatePayload(): Record { return { counterpartyType: counterpartyType.value, ...counterpartyPayload(), immatriculation: immatriculation.value || null, plateFreeFormat: plateFreeFormat.value, emptyDate: empty.date || null, emptyWeight: empty.weight, emptyDsd: empty.dsd, emptyMode: empty.mode, emptyManualNumber: empty.manualNumber || null, } } /** * Pré-remplit le formulaire à partir du détail d'un ticket existant (écran * Modification, ERP-190). Le numéro et le site sont immuables (RG-5.09) → * non repris dans l'état éditable (affichés en lecture seule par l'écran). * Les dates ISO du back (datetime) sont ramenées à `YYYY-MM-DD` pour MalioDate. */ function hydrate(detail: WeighingTicketHydration): void { ticketId.value = detail.id counterpartyType.value = detail.counterpartyType ?? null clientIri.value = detail.client?.['@id'] ?? null supplierIri.value = detail.supplier?.['@id'] ?? null otherLabel.value = detail.otherLabel ?? null immatriculation.value = detail.immatriculation ?? null plateFreeFormat.value = detail.plateFreeFormat ?? false empty.date = isoDateOnly(detail.emptyDate) ?? today empty.weight = detail.emptyWeight ?? null empty.dsd = detail.emptyDsd ?? null empty.mode = detail.emptyMode ?? null empty.manualNumber = detail.emptyManualNumber ?? null full.date = isoDateOnly(detail.fullDate) ?? today full.weight = detail.fullWeight ?? null full.dsd = detail.fullDsd ?? null full.mode = detail.fullMode ?? null full.manualNumber = detail.fullManualNumber ?? null } /** * Payload de MODIFICATION (PATCH /weighing_tickets/{id}, ERP-190) : tous les * champs éditables (contrepartie + véhicule + les 2 pesées). Le numéro et le * site sont immuables (RG-5.09, ignorés par le back même si envoyés). Le net * est recalculé serveur (RG-5.05). */ function buildUpdatePayload(): Record { return { ...buildCreatePayload(), ...buildFullPayload() } } /** * Payload de FINALISATION (PATCH /weighing_tickets/{id}, spec-back § 4.4) : * pesée à PLEIN. Le véhicule (immat / tout format) peut avoir été ajusté entre * les 2 blocs → on le repousse aussi (valeur partagée, RG-5.01). Le net est * recalculé serveur (RG-5.05). */ function buildFullPayload(): Record { return { immatriculation: immatriculation.value || null, plateFreeFormat: plateFreeFormat.value, fullDate: full.date || null, fullWeight: full.weight, fullDsd: full.dsd, fullMode: full.mode, fullManualNumber: full.manualNumber || null, } } return { // contrepartie counterpartyType, counterpartyField, clientIri, supplierIri, otherLabel, setCounterpartyType, // véhicule partagé immatriculation, plateFreeFormat, // pesées empty, full, applyReading, // workflow ticketId, hydrate, buildCreatePayload, buildFullPayload, buildUpdatePayload, } }