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 } /** 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, } } /** * 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, buildCreatePayload, buildFullPayload, } }