feat : ajout d'un composant pour le champ d'immatriculation, ajout de la lib maska pour le format des champs et correction de la gestion des mises en attentes des receptions
This commit is contained in:
@@ -2,15 +2,11 @@
|
||||
<form @submit.prevent="validate">
|
||||
<div class="grid grid-cols-1 items-start gap-8 mb-16">
|
||||
<h1 class="font-bold text-5xl uppercase">Réception</h1>
|
||||
<div class="flex flex-col">
|
||||
<label for="license-plate" class="font-bold uppercase text-xl mb-4">Immatriculation</label>
|
||||
<input
|
||||
id="license-plate"
|
||||
<div>
|
||||
<UiLicensePlateInput
|
||||
v-model="form.licensePlate"
|
||||
type="text"
|
||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
||||
v-model:allowAny="allowAnyLicensePlate"
|
||||
/>
|
||||
<p v-if="fieldErrors.licensePlate" class="text-red-600 text-sm">{{ fieldErrors.licensePlate }}</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label for="reception-date" class="font-bold uppercase text-xl mb-4">Date de reception</label>
|
||||
@@ -20,7 +16,6 @@
|
||||
type="date"
|
||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
||||
/>
|
||||
<p v-if="fieldErrors.receptionDate" class="text-red-600 text-sm">{{ fieldErrors.receptionDate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
@@ -34,8 +29,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import { mapZodErrors } from '~/utils/zod-errors'
|
||||
import { useReceptionStore } from '~/stores/reception'
|
||||
|
||||
type ReceptionFormData = {
|
||||
@@ -49,20 +42,7 @@ const form = reactive<ReceptionFormData>({
|
||||
licensePlate: '',
|
||||
receptionDate: new Date().toISOString().slice(0, 10)
|
||||
})
|
||||
const fieldErrors = reactive<Partial<Record<keyof ReceptionFormData, string>>>({
|
||||
licensePlate: undefined,
|
||||
receptionDate: undefined
|
||||
})
|
||||
const formSchema = z.object({
|
||||
licensePlate: z
|
||||
.string()
|
||||
.min(1, 'Immatriculation requise.')
|
||||
.max(20, 'Immatriculation trop longue (20 caracteres max).'),
|
||||
receptionDate: z
|
||||
.string()
|
||||
.min(1, 'Date de reception requise.')
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Date de reception invalide.')
|
||||
})
|
||||
const allowAnyLicensePlate = ref(false)
|
||||
|
||||
watch(
|
||||
() => receptionStore.current,
|
||||
@@ -74,26 +54,14 @@ watch(
|
||||
)
|
||||
|
||||
async function validate() {
|
||||
fieldErrors.licensePlate = undefined
|
||||
fieldErrors.receptionDate = undefined
|
||||
const normalizedLicensePlate = form.licensePlate.trim()
|
||||
const normalizedReceptionDate = form.receptionDate.trim()
|
||||
const result = formSchema.safeParse({
|
||||
licensePlate: normalizedLicensePlate,
|
||||
receptionDate: normalizedReceptionDate
|
||||
})
|
||||
if (!result.success) {
|
||||
const errors = mapZodErrors<ReceptionFormData>(result.error)
|
||||
fieldErrors.licensePlate = errors.licensePlate ?? 'Formulaire invalide.'
|
||||
fieldErrors.receptionDate = errors.receptionDate ?? 'Formulaire invalide.'
|
||||
return
|
||||
}
|
||||
|
||||
if (!receptionStore.current) {
|
||||
const created = await receptionStore.createReception({
|
||||
currentStep: 1,
|
||||
licensePlate: normalizedLicensePlate || null,
|
||||
receptionDate: normalizedReceptionDate || null
|
||||
licensePlate: normalizedLicensePlate,
|
||||
receptionDate: normalizedReceptionDate
|
||||
})
|
||||
if (created) {
|
||||
await router.push(`/reception/${created.id}`)
|
||||
@@ -104,8 +72,8 @@ async function validate() {
|
||||
const nextStep = receptionStore.current.currentStep + 1
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
currentStep: nextStep,
|
||||
licensePlate: normalizedLicensePlate || null,
|
||||
receptionDate: normalizedReceptionDate || null
|
||||
licensePlate: normalizedLicensePlate,
|
||||
receptionDate: normalizedReceptionDate
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<label :for="inputId" class="font-bold uppercase text-xl mb-4">{{ label }}</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:value="modelValue"
|
||||
v-maska="maskOptions"
|
||||
type="text"
|
||||
:maxlength="maxLength"
|
||||
:placeholder="placeholderText"
|
||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<label :for="checkboxId" class="mt-3 flex items-center gap-3 text-sm">
|
||||
<input
|
||||
:id="checkboxId"
|
||||
:checked="allowAny"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 accent-primary-500"
|
||||
@change="toggleAllowAny"
|
||||
/>
|
||||
Autoriser un format libre
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { vMaska } from 'maska/vue'
|
||||
type Props = {
|
||||
modelValue: string
|
||||
allowAny?: boolean
|
||||
label?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
allowAny: false,
|
||||
label: 'Immatriculation',
|
||||
id: 'license-plate'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
(event: 'update:allowAny', value: boolean): void
|
||||
}>()
|
||||
|
||||
const inputId = computed(() => props.id)
|
||||
const checkboxId = computed(() => `${props.id}-format`)
|
||||
|
||||
const maskOptions = computed(() =>
|
||||
props.allowAny
|
||||
? undefined
|
||||
: {
|
||||
mask: '@@-###-@@',
|
||||
eager: true,
|
||||
tokens: {
|
||||
'@': {
|
||||
pattern: /[A-Za-z]/,
|
||||
transform: (char: string) => char.toUpperCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
const placeholderText = computed(() => (props.allowAny ? '' : 'AA-123-AA'))
|
||||
const maxLength = computed(() => (props.allowAny ? 20 : 9))
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.allowAny) {
|
||||
emit('update:modelValue', target.value)
|
||||
return
|
||||
}
|
||||
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const toggleAllowAny = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextValue = target.checked
|
||||
emit('update:allowAny', nextValue)
|
||||
if (!nextValue) {
|
||||
emit('update:modelValue', props.modelValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Generated
+6
@@ -10,6 +10,7 @@
|
||||
"@nuxtjs/i18n": "^10.2.1",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"izitoast": "^1.4.0",
|
||||
"maska": "^3.2.0",
|
||||
"nuxt": "^4.2.2",
|
||||
"nuxt-toast": "^1.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
@@ -9195,6 +9196,11 @@
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/maska": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/maska/-/maska-3.2.0.tgz",
|
||||
"integrity": "sha512-zSmSgs5/q9vMSmrdZT3rKOv9uLznNWR/niuuAdBZDTvB3SMKOX9vhMtDijFyExz+B4UClu2rvksylUh/ea1bLA=="
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@nuxtjs/i18n": "^10.2.1",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"izitoast": "^1.4.0",
|
||||
"maska": "^3.2.0",
|
||||
"nuxt": "^4.2.2",
|
||||
"nuxt-toast": "^1.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
<div>
|
||||
<div class="flex justify-between h-[52px] mb-[90px]">
|
||||
<p class="self-center">Indicateur d’étapes</p>
|
||||
<NuxtLink to="/" class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center">Mettre en attente</NuxtLink>
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
||||
@click="saveAndHold"
|
||||
>Mettre en attente</button>
|
||||
</div>
|
||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
||||
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
||||
@@ -21,13 +25,39 @@ const router = useRouter()
|
||||
const receptionStore = useReceptionStore()
|
||||
const { current: storeReception } = storeToRefs(receptionStore)
|
||||
|
||||
onMounted(async () => {
|
||||
const raw = route.params.id
|
||||
const idStr = Array.isArray(raw) ? raw[0] : raw
|
||||
const id = idStr ? Number(idStr) : null
|
||||
|
||||
if (id !== null) {
|
||||
await receptionStore.loadReception(id)
|
||||
const resolveReceptionId = (param: unknown) => {
|
||||
const idStr = Array.isArray(param) ? param[0] : param
|
||||
if (!idStr) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
const id = Number(idStr)
|
||||
return Number.isFinite(id) ? id : null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async (param) => {
|
||||
const id = resolveReceptionId(param)
|
||||
if (id === null) {
|
||||
receptionStore.clearCurrent()
|
||||
return
|
||||
}
|
||||
await receptionStore.loadReception(id)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const saveAndHold = async () => {
|
||||
if (!receptionStore.current) {
|
||||
await router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
currentStep: receptionStore.current.currentStep,
|
||||
licensePlate: receptionStore.current.licensePlate,
|
||||
receptionDate: receptionStore.current.receptionDate
|
||||
})
|
||||
await router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user