f2c06aed43
« Tout format » n'est plus un champ libre total : masque maska charset (lettres/chiffres/espace/tiret, MAJ, longueur libre) pour les plaques anciennes ou étrangères, filtrant accents/ponctuation/symboles. Format autoritaire côté serveur.
176 lines
8.0 KiB
Vue
176 lines
8.0 KiB
Vue
<template>
|
|
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
|
<!-- 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>
|
|
<div class="flex items-center gap-8">
|
|
<MalioButton
|
|
variant="secondary"
|
|
icon-name="mdi:weight"
|
|
icon-position="left"
|
|
:label="t('logistique.weighingTickets.form.weighbridge.auto')"
|
|
:disabled="disabled"
|
|
@click="$emit('request-auto')"
|
|
/>
|
|
<MalioButton
|
|
variant="primary"
|
|
icon-name="mdi:weight"
|
|
icon-position="left"
|
|
:label="t('logistique.weighingTickets.form.weighbridge.manual')"
|
|
:disabled="disabled"
|
|
@click="$emit('request-manual')"
|
|
/>
|
|
</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 2 : Date, Poids, DSD, Immatriculation. -->
|
|
<div class="grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
|
<!-- Date de la pesée — jour par défaut (RG-5.07). MalioDate (composant
|
|
projet pour le type date, exception tolérée @.claude/rules/frontend.md). -->
|
|
<MalioDate
|
|
: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). 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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { WeighingBlockState } from '~/modules/logistique/composables/useWeighingTicketForm'
|
|
|
|
/**
|
|
* 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).
|
|
*/
|
|
|
|
// Masque plaque FR SIV `XX-000-XX` (maska) : 2 lettres, 3 chiffres, 2 lettres,
|
|
// majuscules forcées. Désactivé quand « Tout format » est coché (RG-5.01).
|
|
const PLATE_MASK = {
|
|
mask: 'AA-###-AA',
|
|
tokens: { A: { pattern: /[A-Za-z]/, transform: (c: string) => c.toUpperCase() } },
|
|
}
|
|
|
|
// Masque « Tout format » (RG-5.01) : plaques anciennes / étrangères / engins. On
|
|
// autorise lettres, chiffres, espace et tiret, en MAJUSCULES, longueur libre —
|
|
// mais on filtre tout le reste (accents, ponctuation, symboles : « &é"'(_ç… »).
|
|
// Pattern maska charset du projet (cf. shared/utils/textSanitize) : `preProcess`
|
|
// retire d'abord les caractères hors charset (le token `multiple` glouton
|
|
// s'arrêterait sinon au 1er invalide), puis le token laisse passer le reste.
|
|
const FREE_PLATE_MASK = {
|
|
mask: 'P',
|
|
tokens: { P: { pattern: /[A-Z0-9 -]/, multiple: true } },
|
|
preProcess: (value: string) => value.toUpperCase().replace(/[^A-Z0-9 -]/g, ''),
|
|
}
|
|
|
|
// Masque « chiffres uniquement » (maska, longueur libre) pour Poids et DSD :
|
|
// ces champs texte sont verrouillés sur des entiers, et de toute façon désactivés
|
|
// (remplis par la pesée).
|
|
const NUMERIC_MASK = {
|
|
mask: 'D',
|
|
tokens: { D: { pattern: /[0-9]/, multiple: true } },
|
|
}
|
|
|
|
const props = 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
|
|
}>()
|
|
|
|
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)))
|
|
const dsdDisplay = computed(() => (props.block.dsd === null ? '' : String(props.block.dsd)))
|
|
|
|
/** Remonte la mutation d'un champ du bloc au parent (état des pesées centralisé). */
|
|
function emitBlock(field: keyof WeighingBlockState, value: unknown): void {
|
|
emit('update:block', field, value)
|
|
}
|
|
</script>
|