feat : cycle de vie brouillon/validé du ticket de pesée (ERP-193)
Une pesée (bascule ou manuelle) s'enregistre désormais dès la validation de sa
modale, sans exiger la contrepartie ni l'immatriculation : le ticket naît
« brouillon » (status DRAFT, sans numéro). Le bouton « Valider » finalise quand
les 3 champs du haut (contrepartie + champ associé + immatriculation) ET les 2
pesées sont renseignés : attribution du numéro {siteCode}-TP-{NNNN} et passage
en VALIDATED, puis ouverture du bon de pesée PDF.
Back : counterparty_type/immatriculation/number nullables + colonne status
(migration racine), contraintes strictes déplacées en groupe de validation
finalize, opération PATCH /weighing_tickets/{id}/validate, numéro attribué à la
validation. Front : 4 champs en haut hors blocs, persistance immédiate des
pesées, écrans Ajouter/Modifier refondus, colonne Statut dans la liste, form à
plat pleine largeur. Tests back (lifecycle brouillon/validate) + front à jour.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
||||
<!-- Padding vertical piloté par la page (1er bloc sans pt, dernier sans pb). -->
|
||||
<div>
|
||||
<!-- En-tête du bloc : titre + boutons de pesée (bascule / manuelle). -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-[20px] font-semibold text-m-primary">{{ title }}</h2>
|
||||
@@ -23,122 +24,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-4">
|
||||
<!-- Ligne 1 : contrepartie (type en col 1 + champ conditionnel en col 2),
|
||||
rendue par le parent (bloc vide uniquement) via le slot. -->
|
||||
<div v-if="$slots.counterparty" class="grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<slot name="counterparty" />
|
||||
</div>
|
||||
<!-- Ligne : Date/heure, Poids, DSD. L'immatriculation et « Tout format »
|
||||
vivent désormais dans les 4 champs du haut, hors des blocs (ERP-193). -->
|
||||
<div class="mt-6 grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<!-- Date/heure de la pesée — date du jour + heure courante par défaut
|
||||
(RG-5.07), ré-horodatée à la validation de la pesée. -->
|
||||
<MalioDateTime
|
||||
:model-value="block.date"
|
||||
:label="t('logistique.weighingTickets.form.date')"
|
||||
:required="true"
|
||||
:editable="true"
|
||||
:disabled="disabled"
|
||||
:error="errors.date"
|
||||
@update:model-value="(v: string | null) => emitBlock('date', v)"
|
||||
/>
|
||||
|
||||
<!-- Ligne 2 : Date, Poids, DSD, Immatriculation. -->
|
||||
<div class="grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<!-- Date/heure de la pesée — date du jour + heure courante par défaut
|
||||
(RG-5.07). MalioDateTime : on enregistre l'instant réel de la pesée
|
||||
(jamais 00:00:00), le back stocke un TIMESTAMP. -->
|
||||
<MalioDateTime
|
||||
:model-value="block.date"
|
||||
:label="t('logistique.weighingTickets.form.date')"
|
||||
:required="true"
|
||||
:editable="true"
|
||||
:disabled="disabled"
|
||||
:error="errors.date"
|
||||
@update:model-value="(v: string | null) => emitBlock('date', v)"
|
||||
/>
|
||||
<!-- Poids : champ texte verrouillé sur les chiffres, toujours désactivé
|
||||
(rempli par la pesée, jamais saisi à la main — RG-5.07). -->
|
||||
<MalioInputText
|
||||
:model-value="weightDisplay"
|
||||
:mask="NUMERIC_MASK"
|
||||
:label="t('logistique.weighingTickets.form.weight')"
|
||||
:required="true"
|
||||
:disabled="true"
|
||||
:error="errors.weight"
|
||||
/>
|
||||
|
||||
<!-- Poids : champ texte verrouillé sur les chiffres, toujours désactivé
|
||||
(rempli par la pesée, jamais saisi à la main — RG-5.07). Unité Kg
|
||||
dans le label. -->
|
||||
<MalioInputText
|
||||
:model-value="weightDisplay"
|
||||
:mask="NUMERIC_MASK"
|
||||
:label="t('logistique.weighingTickets.form.weight')"
|
||||
:required="true"
|
||||
:disabled="true"
|
||||
:error="errors.weight"
|
||||
/>
|
||||
|
||||
<!-- DSD : champ texte verrouillé sur les chiffres, toujours désactivé
|
||||
(rempli par la pesée — RG-5.04 / RG-5.07). -->
|
||||
<MalioInputText
|
||||
:model-value="dsdDisplay"
|
||||
:mask="NUMERIC_MASK"
|
||||
:label="t('logistique.weighingTickets.form.dsd')"
|
||||
:required="true"
|
||||
:disabled="true"
|
||||
:error="errors.dsd"
|
||||
/>
|
||||
|
||||
<!-- Immatriculation : masque XX-000-XX (plaque FR SIV) ; en « Tout format »,
|
||||
masque ÉLARGI (lettres/chiffres/espace/tiret, MAJ) pour les plaques
|
||||
anciennes/étrangères, mais sans laisser passer n'importe quoi.
|
||||
PARTAGÉE entre les 2 blocs (RG-5.01) — v-model remonté au form parent.
|
||||
TODO migrer le masque plaque quand @malio/layer-ui couvrira le format. -->
|
||||
<MalioInputText
|
||||
:model-value="immatriculation"
|
||||
:mask="plateFreeFormat ? FREE_PLATE_MASK : PLATE_MASK"
|
||||
:label="t('logistique.weighingTickets.form.immatriculation')"
|
||||
:required="true"
|
||||
:disabled="disabled"
|
||||
:error="errors.immatriculation"
|
||||
@update:model-value="(v: string | null) => $emit('update:immatriculation', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Ligne 3 : « Tout format » (désactive le masque plaque). Partagé entre
|
||||
blocs (RG-5.01). Sur sa propre ligne. -->
|
||||
<div class="grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioCheckbox
|
||||
:id="`${blockId}-plate-free-format`"
|
||||
:model-value="plateFreeFormat"
|
||||
:label="t('logistique.weighingTickets.form.plateFreeFormat')"
|
||||
group-class="self-center"
|
||||
:disabled="disabled"
|
||||
@update:model-value="(v: boolean) => $emit('update:plateFreeFormat', v)"
|
||||
/>
|
||||
</div>
|
||||
<!-- DSD : champ texte verrouillé sur les chiffres, toujours désactivé
|
||||
(rempli par la pesée — RG-5.04 / RG-5.07). -->
|
||||
<MalioInputText
|
||||
:model-value="dsdDisplay"
|
||||
:mask="NUMERIC_MASK"
|
||||
:label="t('logistique.weighingTickets.form.dsd')"
|
||||
:required="true"
|
||||
:disabled="true"
|
||||
:error="errors.dsd"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { WeighingBlockState } from '~/modules/logistique/composables/useWeighingTicketForm'
|
||||
import { NUMERIC_MASK, PLATE_MASK, FREE_PLATE_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||
import { NUMERIC_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||
|
||||
/**
|
||||
* Bloc de pesée (« Poids à vide » ou « Poids à plein ») de l'écran Ticket de pesée.
|
||||
* Champs Date / Poids / DSD / Immatriculation / « Tout format » + boutons de pesée.
|
||||
* L'immatriculation et « Tout format » sont PARTAGÉS entre les 2 blocs (RG-5.01) :
|
||||
* portés par le form parent et remontés en `update:*`. Le slot `counterparty`
|
||||
* permet au parent d'injecter la contrepartie sur le seul bloc vide (RG-5.03).
|
||||
* Masques de saisie factorisés dans `utils/weighingMasks`.
|
||||
* Champs Date/heure / Poids / DSD + boutons de pesée (bascule / manuelle). Depuis
|
||||
* ERP-193, la contrepartie, l'immatriculation et « Tout format » sont remontés dans
|
||||
* les 4 champs du haut de page (hors blocs). Masque numérique factorisé dans
|
||||
* `utils/weighingMasks`.
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
/** Identifiant technique du bloc (pour les `id` de champs uniques). */
|
||||
blockId: string
|
||||
title: string
|
||||
block: WeighingBlockState
|
||||
/** Immatriculation partagée (RG-5.01) — portée par le form parent. */
|
||||
immatriculation: string | null
|
||||
/** « Tout format » partagé (RG-5.01) — porté par le form parent. */
|
||||
plateFreeFormat: boolean
|
||||
/** Erreurs 422 par champ (propertyPath → message). */
|
||||
errors?: Record<string, string>
|
||||
disabled?: boolean
|
||||
}>()
|
||||
}>(), {
|
||||
errors: () => ({}),
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:block': [field: keyof WeighingBlockState, value: unknown]
|
||||
'update:immatriculation': [value: string | null]
|
||||
'update:plateFreeFormat': [value: boolean]
|
||||
'request-auto': []
|
||||
'request-manual': []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const errors = computed(() => props.errors ?? {})
|
||||
|
||||
// Poids / DSD : champs texte → on présente l'entier sous forme de chaîne (vide
|
||||
// tant que la pesée n'a pas rempli la valeur).
|
||||
const weightDisplay = computed(() => (props.block.weight === null ? '' : String(props.block.weight)))
|
||||
|
||||
Reference in New Issue
Block a user