fix(front) : poids en champ texte chiffré dans la pesée manuelle + retrait numéro/site sur la modification (ERP-189/190)
- Modale « Pesée manuelle » : champ Poids passé en MalioInputText verrouillé sur les chiffres (NUMERIC_MASK), comme le formulaire. - Masques de pesée factorisés dans utils/weighingMasks (NUMERIC / PLATE / FREE_PLATE). - Écran Modification : suppression des champs lecture seule « Numéro » et « Site » en tête (le numéro reste rappelé dans le titre de l'écran).
This commit is contained in:
@@ -710,8 +710,6 @@
|
|||||||
"addTitle": "Ajouter un ticket de pesée",
|
"addTitle": "Ajouter un ticket de pesée",
|
||||||
"emptyBlock": "Poids à vide",
|
"emptyBlock": "Poids à vide",
|
||||||
"fullBlock": "Poids à plein",
|
"fullBlock": "Poids à plein",
|
||||||
"number": "Numéro",
|
|
||||||
"site": "Site",
|
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"weight": "Poids (Kg)",
|
"weight": "Poids (Kg)",
|
||||||
"dsd": "DSD",
|
"dsd": "DSD",
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { WeighingBlockState } from '~/modules/logistique/composables/useWeighingTicketForm'
|
import type { WeighingBlockState } from '~/modules/logistique/composables/useWeighingTicketForm'
|
||||||
|
import { NUMERIC_MASK, PLATE_MASK, FREE_PLATE_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bloc de pesée (« Poids à vide » ou « Poids à plein ») de l'écran Ticket de pesée.
|
* Bloc de pesée (« Poids à vide » ou « Poids à plein ») de l'écran Ticket de pesée.
|
||||||
@@ -108,35 +109,9 @@ import type { WeighingBlockState } from '~/modules/logistique/composables/useWei
|
|||||||
* L'immatriculation et « Tout format » sont PARTAGÉS entre les 2 blocs (RG-5.01) :
|
* 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`
|
* 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).
|
* permet au parent d'injecter la contrepartie sur le seul bloc vide (RG-5.03).
|
||||||
|
* Masques de saisie factorisés dans `utils/weighingMasks`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 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<{
|
const props = defineProps<{
|
||||||
/** Identifiant technique du bloc (pour les `id` de champs uniques). */
|
/** Identifiant technique du bloc (pour les `id` de champs uniques). */
|
||||||
blockId: string
|
blockId: string
|
||||||
|
|||||||
@@ -100,11 +100,9 @@ describe('Écran Modification ticket de pesée (page /weighing-tickets/{id}/edit
|
|||||||
mockOpen.mockReset()
|
mockOpen.mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pré-remplit le numéro et le site en lecture seule (RG-5.09)', async () => {
|
it('charge le ticket au montage (pré-remplissage via hydrate)', async () => {
|
||||||
const wrapper = await mountPage()
|
await mountPage()
|
||||||
expect(mockFetchTicket).toHaveBeenCalledWith('9')
|
expect(mockFetchTicket).toHaveBeenCalledWith('9')
|
||||||
expect(wrapper.find('[data-label="logistique.weighingTickets.form.number"]').attributes('value')).toBe('86-TP-0001')
|
|
||||||
expect(wrapper.find('[data-label="logistique.weighingTickets.form.site"]').attributes('value')).toBe('Chatellerault')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('bascule des boutons : « Enregistrer » + « Imprimer » présents, pas de « Valider »', async () => {
|
it('bascule des boutons : « Enregistrer » + « Imprimer » présents, pas de « Valider »', async () => {
|
||||||
|
|||||||
@@ -18,23 +18,8 @@
|
|||||||
<p v-else-if="error" class="mt-12 text-center text-m-danger">{{ t('logistique.weighingTickets.edit.notFound') }}</p>
|
<p v-else-if="error" class="mt-12 text-center text-m-danger">{{ t('logistique.weighingTickets.edit.notFound') }}</p>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Numéro + site : lecture seule, immuables (RG-5.09). -->
|
<!-- Numéro + site : immuables (RG-5.09), rappelés dans le titre de l'écran. -->
|
||||||
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="mt-[48px] flex flex-col gap-8">
|
||||||
<MalioInputText
|
|
||||||
:model-value="ticketNumber"
|
|
||||||
:label="t('logistique.weighingTickets.form.number')"
|
|
||||||
:readonly="true"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
<MalioInputText
|
|
||||||
:model-value="siteName"
|
|
||||||
:label="t('logistique.weighingTickets.form.site')"
|
|
||||||
:readonly="true"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8 flex flex-col gap-8">
|
|
||||||
<!-- ── Bloc « Poids à vide » (porte la contrepartie, RG-5.03) ──── -->
|
<!-- ── Bloc « Poids à vide » (porte la contrepartie, RG-5.03) ──── -->
|
||||||
<WeighingBlock
|
<WeighingBlock
|
||||||
block-id="empty"
|
block-id="empty"
|
||||||
@@ -156,11 +141,12 @@
|
|||||||
<h2 class="text-[24px] font-bold">{{ t('logistique.weighingTickets.form.manual.title') }}</h2>
|
<h2 class="text-[24px] font-bold">{{ t('logistique.weighingTickets.form.manual.title') }}</h2>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<MalioInputNumber
|
<!-- Poids : champ texte verrouillé sur les chiffres (comme le formulaire). -->
|
||||||
|
<MalioInputText
|
||||||
v-model="manualModal.weight"
|
v-model="manualModal.weight"
|
||||||
|
:mask="NUMERIC_MASK"
|
||||||
:label="t('logistique.weighingTickets.form.manual.weight')"
|
:label="t('logistique.weighingTickets.form.manual.weight')"
|
||||||
:required="true"
|
:required="true"
|
||||||
:min="0"
|
|
||||||
:error="manualModal.errors.weight"
|
:error="manualModal.errors.weight"
|
||||||
/>
|
/>
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
@@ -195,6 +181,7 @@ import { useWeighingTicketForm, type WeighingBlockState } from '~/modules/logist
|
|||||||
import { useWeighbridge } from '~/modules/logistique/composables/useWeighbridge'
|
import { useWeighbridge } from '~/modules/logistique/composables/useWeighbridge'
|
||||||
import { useWeighingTicket } from '~/modules/logistique/composables/useWeighingTicket'
|
import { useWeighingTicket } from '~/modules/logistique/composables/useWeighingTicket'
|
||||||
import { useWeighingTicketReferentials, type RefOption } from '~/modules/logistique/composables/useWeighingTicketReferentials'
|
import { useWeighingTicketReferentials, type RefOption } from '~/modules/logistique/composables/useWeighingTicketReferentials'
|
||||||
|
import { NUMERIC_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
@@ -236,9 +223,8 @@ const loading = ref(true)
|
|||||||
const error = ref(false)
|
const error = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
|
||||||
// Numéro + site immuables (RG-5.09), affichés en lecture seule.
|
// Numéro immuable (RG-5.09), rappelé dans le titre de l'écran.
|
||||||
const ticketNumber = ref<string>('')
|
const ticketNumber = ref<string>('')
|
||||||
const siteName = ref<string>('')
|
|
||||||
|
|
||||||
const headerTitle = computed(() =>
|
const headerTitle = computed(() =>
|
||||||
ticketNumber.value
|
ticketNumber.value
|
||||||
@@ -322,7 +308,7 @@ const manualModal = reactive({
|
|||||||
open: false,
|
open: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
target: 'empty' as 'empty' | 'full',
|
target: 'empty' as 'empty' | 'full',
|
||||||
weight: null as string | number | null,
|
weight: null as string | null,
|
||||||
manualNumber: null as string | null,
|
manualNumber: null as string | null,
|
||||||
errors: {} as Record<string, string>,
|
errors: {} as Record<string, string>,
|
||||||
})
|
})
|
||||||
@@ -399,7 +385,6 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
const detail = await fetchTicket(ticketId)
|
const detail = await fetchTicket(ticketId)
|
||||||
ticketNumber.value = detail.number
|
ticketNumber.value = detail.number
|
||||||
siteName.value = detail.site?.name ?? ''
|
|
||||||
form.hydrate(detail)
|
form.hydrate(detail)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
@@ -145,11 +145,12 @@
|
|||||||
<h2 class="text-[24px] font-bold">{{ t('logistique.weighingTickets.form.manual.title') }}</h2>
|
<h2 class="text-[24px] font-bold">{{ t('logistique.weighingTickets.form.manual.title') }}</h2>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<MalioInputNumber
|
<!-- Poids : champ texte verrouillé sur les chiffres (comme le formulaire). -->
|
||||||
|
<MalioInputText
|
||||||
v-model="manualModal.weight"
|
v-model="manualModal.weight"
|
||||||
|
:mask="NUMERIC_MASK"
|
||||||
:label="t('logistique.weighingTickets.form.manual.weight')"
|
:label="t('logistique.weighingTickets.form.manual.weight')"
|
||||||
:required="true"
|
:required="true"
|
||||||
:min="0"
|
|
||||||
:error="manualModal.errors.weight"
|
:error="manualModal.errors.weight"
|
||||||
/>
|
/>
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
@@ -183,6 +184,7 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
|||||||
import { useWeighingTicketForm, type WeighingBlockState } from '~/modules/logistique/composables/useWeighingTicketForm'
|
import { useWeighingTicketForm, type WeighingBlockState } from '~/modules/logistique/composables/useWeighingTicketForm'
|
||||||
import { useWeighbridge } from '~/modules/logistique/composables/useWeighbridge'
|
import { useWeighbridge } from '~/modules/logistique/composables/useWeighbridge'
|
||||||
import { useWeighingTicketReferentials, type RefOption } from '~/modules/logistique/composables/useWeighingTicketReferentials'
|
import { useWeighingTicketReferentials, type RefOption } from '~/modules/logistique/composables/useWeighingTicketReferentials'
|
||||||
|
import { NUMERIC_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
@@ -302,7 +304,7 @@ const manualModal = reactive({
|
|||||||
open: false,
|
open: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
target: 'empty' as 'empty' | 'full',
|
target: 'empty' as 'empty' | 'full',
|
||||||
weight: null as string | number | null,
|
weight: null as string | null,
|
||||||
manualNumber: null as string | null,
|
manualNumber: null as string | null,
|
||||||
errors: {} as Record<string, string>,
|
errors: {} as Record<string, string>,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { MaskInputOptions } from 'maska'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Masques de saisie du module « Tickets de pesée » (M5). Partagés entre le
|
||||||
|
* composant de bloc (`WeighingBlock`) et les modales de pesée (écrans Ajouter /
|
||||||
|
* Modifier). La validation de format reste autoritaire côté serveur (RG-5.01).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Masque « chiffres uniquement » (longueur libre) — Poids et DSD. Verrouille la
|
||||||
|
* saisie sur des entiers.
|
||||||
|
*/
|
||||||
|
export const NUMERIC_MASK: MaskInputOptions = {
|
||||||
|
mask: 'D',
|
||||||
|
tokens: { D: { pattern: /[0-9]/, multiple: true } },
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Masque plaque FR SIV `XX-000-XX` : 2 lettres, 3 chiffres, 2 lettres, majuscules
|
||||||
|
* forcées. Utilisé quand « Tout format » n'est pas coché (RG-5.01).
|
||||||
|
*/
|
||||||
|
export const PLATE_MASK: MaskInputOptions = {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
export const FREE_PLATE_MASK: MaskInputOptions = {
|
||||||
|
mask: 'P',
|
||||||
|
tokens: { P: { pattern: /[A-Z0-9 -]/, multiple: true } },
|
||||||
|
preProcess: (value: string) => value.toUpperCase().replace(/[^A-Z0-9 -]/g, ''),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user