fix(front) : ajustements du formulaire ticket de pesée (ERP-189/190)

- Poids/DSD en champs texte verrouillés sur les chiffres et désactivés.
- Boutons de pesée : icône mdi:weight à gauche + gap-8.
- Bloc « Poids à vide » réagencé en 3 lignes (contrepartie / Date-Poids-DSD-Immat / Tout format).
- Omission des clés null dans les payloads (compact) : requis vides → message NotBlank métier au lieu d'une erreur de type.
- Pesée obligatoire (RG-5.07) signalée inline sous Poids/DSD ; toutes les violations affichées d'un seul aller-retour.
- Erreur d'immatriculation affichée uniquement sur le bloc « Poids à vide » (plus de doublon sur le bloc plein).
This commit is contained in:
2026-06-23 14:03:32 +02:00
parent 68e7205793
commit 5349c3c4d5
7 changed files with 200 additions and 66 deletions
@@ -15,6 +15,31 @@ describe('useWeighingTicketForm', () => {
expect(form.counterpartyType.value).toBeNull()
})
// ── Omission des requis vides (compact) ──────────────────────────────────
it('buildCreatePayload omet les clés null (requis vides absents, pas envoyés à null)', () => {
const form = useWeighingTicketForm()
// Formulaire vierge : counterpartyType / immatriculation non remplis.
const payload = form.buildCreatePayload()
// Absents (et non null) → le back applique NotBlank (message métier) plutôt
// qu'une erreur de type opaque (« doit être de type string »).
expect(payload).not.toHaveProperty('counterpartyType')
expect(payload).not.toHaveProperty('immatriculation')
expect(payload).not.toHaveProperty('emptyWeight')
// Les non-null restent : date du jour + booléen Tout format.
expect(payload.emptyDate).toBe('2026-06-22')
expect(payload.plateFreeFormat).toBe(false)
})
// ── Pesée obligatoire front-only (RG-5.07) ───────────────────────────────
it('missingWeighingFields liste Poids/DSD manquants, puis vide après pesée', () => {
const form = useWeighingTicketForm()
expect(form.missingWeighingFields('empty')).toEqual(['emptyWeight', 'emptyDsd'])
expect(form.missingWeighingFields('full')).toEqual(['fullWeight', 'fullDsd'])
form.applyReading(form.empty, { weight: 7150, dsd: 1, mode: 'AUTO' })
expect(form.missingWeighingFields('empty')).toEqual([])
})
// ── Contrepartie conditionnelle (RG-5.03) ────────────────────────────────
it('CLIENT : ne conserve que le client, purge supplier et otherLabel', () => {
const form = useWeighingTicketForm()
@@ -65,6 +65,19 @@ function isoDateOnly(value: string | null | undefined): string | null {
return value ? value.slice(0, 10) : null
}
/**
* Retire les clés à valeur `null` d'un payload (pattern « omission des requis
* vides » M1). Avec `collectDenormalizationErrors` côté back, envoyer `null` sur
* un scalaire requis (ex. `counterpartyType`) produit une violation de TYPE
* opaque (« Cette valeur doit être de type string. ») au lieu du message métier
* `NotBlank` : une clé ABSENTE laisse au contraire jouer la contrainte `NotBlank`
* et son message FR. On omet donc les null ; les champs réellement requis non
* remplis déclenchent leur vrai message, les optionnels restent simplement absents.
*/
function compact(payload: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== null))
}
/** Crée l'état initial d'un bloc de pesée (date = aujourd'hui, RG-5.07). */
function emptyBlock(today: string): WeighingBlockState {
return {
@@ -122,6 +135,21 @@ export function useWeighingTicketForm() {
}
})
/**
* Champs de pesée manquants d'un bloc (Poids / DSD), RG-5.07. Le back rend ces
* colonnes nullable (workflow 2 temps) : l'obligation « une pesée a été
* effectuée » est donc portée côté front (règle front-only, ERP-101). Renvoie
* les `propertyPath` manquants (ex. `['emptyWeight', 'emptyDsd']`), prêts à
* être posés en erreur inline via `useFormErrors.setError`.
*/
function missingWeighingFields(which: 'empty' | 'full'): string[] {
const block = which === 'empty' ? empty : full
const missing: string[] = []
if (block.weight === null) missing.push(`${which}Weight`)
if (block.dsd === null) missing.push(`${which}Dsd`)
return missing
}
/** Applique une lecture de pesée (bascule/manuelle) à un bloc. */
function applyReading(
block: WeighingBlockState,
@@ -150,7 +178,7 @@ export function useWeighingTicketForm() {
* pour que `useFormErrors` mappe les 422 inline.
*/
function buildCreatePayload(): Record<string, unknown> {
return {
return compact({
counterpartyType: counterpartyType.value,
...counterpartyPayload(),
immatriculation: immatriculation.value || null,
@@ -160,7 +188,7 @@ export function useWeighingTicketForm() {
emptyDsd: empty.dsd,
emptyMode: empty.mode,
emptyManualNumber: empty.manualNumber || null,
}
})
}
/**
@@ -208,7 +236,7 @@ export function useWeighingTicketForm() {
* recalculé serveur (RG-5.05).
*/
function buildFullPayload(): Record<string, unknown> {
return {
return compact({
immatriculation: immatriculation.value || null,
plateFreeFormat: plateFreeFormat.value,
fullDate: full.date || null,
@@ -216,7 +244,7 @@ export function useWeighingTicketForm() {
fullDsd: full.dsd,
fullMode: full.mode,
fullManualNumber: full.manualNumber || null,
}
})
}
return {
@@ -234,6 +262,7 @@ export function useWeighingTicketForm() {
empty,
full,
applyReading,
missingWeighingFields,
// workflow
ticketId,
hydrate,