245 lines
9.9 KiB
TypeScript
245 lines
9.9 KiB
TypeScript
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<CounterpartyType | null>(null)
|
|
const clientIri = ref<string | null>(null)
|
|
const supplierIri = ref<string | null>(null)
|
|
const otherLabel = ref<string | null>(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<string | null>(null)
|
|
const plateFreeFormat = ref<boolean>(false)
|
|
|
|
// ── Les deux pesées ───────────────────────────────────────────────────────
|
|
const empty = reactive<WeighingBlockState>(emptyBlock(today))
|
|
const full = reactive<WeighingBlockState>(emptyBlock(today))
|
|
|
|
// Id du ticket créé (POST du bloc vide) — pilote le PATCH du bloc plein.
|
|
const ticketId = ref<number | null>(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<string, unknown> {
|
|
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<string, unknown> {
|
|
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<string, unknown> {
|
|
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<string, unknown> {
|
|
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,
|
|
}
|
|
}
|