Ajout de la génération du bon de reception et correction des retours #4

Merged
tristan merged 3 commits from feat/reception-generation-bon into develop 2026-01-16 14:48:55 +00:00
6 changed files with 162 additions and 58 deletions
Showing only changes of commit cc83242883 - Show all commits

24
.idea/workspace.xml generated
View File

@@ -4,15 +4,13 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global"> <list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/AGENTS.md" beforeDir="false" afterPath="$PROJECT_DIR$/AGENTS.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/composables/useApi.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/useApi.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/nuxt.config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/nuxt.config.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package-lock.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/reception.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/reception.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -47,7 +45,6 @@
<commands /> <commands />
<urls /> <urls />
</component> </component>
<component name="PhpDebugGeneral" listening_started="true" />
<component name="PhpServers"> <component name="PhpServers">
<servers> <servers>
<server host="localhost" id="36c0c232-9151-4654-a36c-e0f5fd99da91" name="ferme-docker" port="8080" use_path_mappings="true"> <server host="localhost" id="36c0c232-9151-4654-a36c-e0f5fd99da91" name="ferme-docker" port="8080" use_path_mappings="true">
@@ -260,7 +257,7 @@
<workItem from="1768287908317" duration="28058000" /> <workItem from="1768287908317" duration="28058000" />
<workItem from="1768374298711" duration="12403000" /> <workItem from="1768374298711" duration="12403000" />
<workItem from="1768460547451" duration="26946000" /> <workItem from="1768460547451" duration="26946000" />
<workItem from="1768547023783" duration="7809000" /> <workItem from="1768547023783" duration="11371000" />
</task> </task>
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)"> <task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -326,7 +323,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1768498751836</updated> <updated>1768498751836</updated>
</task> </task>
<option name="localTasksCounter" value="9" /> <task id="LOCAL-00009" summary="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
<option name="closed" value="true" />
<created>1768555180530</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1768555180530</updated>
</task>
<option name="localTasksCounter" value="10" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -381,6 +386,7 @@
<MESSAGE value="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple" /> <MESSAGE value="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple" />
<MESSAGE value="test : ajout de TU sur les services et providers" /> <MESSAGE value="test : ajout de TU sur les services et providers" />
<MESSAGE value="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global" /> <MESSAGE value="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global" />
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global" /> <MESSAGE value="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur" />
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur" />
</component> </component>
</project> </project>

View File

@@ -2,15 +2,11 @@
<form @submit.prevent="validate"> <form @submit.prevent="validate">
<div class="grid grid-cols-1 items-start gap-8 mb-16"> <div class="grid grid-cols-1 items-start gap-8 mb-16">
<h1 class="font-bold text-5xl uppercase">Réception</h1> <h1 class="font-bold text-5xl uppercase">Réception</h1>
<div class="flex flex-col"> <div>
<label for="license-plate" class="font-bold uppercase text-xl mb-4">Immatriculation</label> <UiLicensePlateInput
<input
id="license-plate"
v-model="form.licensePlate" v-model="form.licensePlate"
type="text" v-model:allowAny="allowAnyLicensePlate"
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
/> />
<p v-if="fieldErrors.licensePlate" class="text-red-600 text-sm">{{ fieldErrors.licensePlate }}</p>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<label for="reception-date" class="font-bold uppercase text-xl mb-4">Date de reception</label> <label for="reception-date" class="font-bold uppercase text-xl mb-4">Date de reception</label>
@@ -20,7 +16,6 @@
type="date" type="date"
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase" 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> </div>
<div class="flex justify-center"> <div class="flex justify-center">
@@ -34,8 +29,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { z } from 'zod'
import { mapZodErrors } from '~/utils/zod-errors'
import { useReceptionStore } from '~/stores/reception' import { useReceptionStore } from '~/stores/reception'
type ReceptionFormData = { type ReceptionFormData = {
@@ -49,20 +42,7 @@ const form = reactive<ReceptionFormData>({
licensePlate: '', licensePlate: '',
receptionDate: new Date().toISOString().slice(0, 10) receptionDate: new Date().toISOString().slice(0, 10)
}) })
const fieldErrors = reactive<Partial<Record<keyof ReceptionFormData, string>>>({ const allowAnyLicensePlate = ref(false)
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.')
})
watch( watch(
() => receptionStore.current, () => receptionStore.current,
@@ -74,26 +54,14 @@ watch(
) )
async function validate() { async function validate() {
fieldErrors.licensePlate = undefined
fieldErrors.receptionDate = undefined
const normalizedLicensePlate = form.licensePlate.trim() const normalizedLicensePlate = form.licensePlate.trim()
const normalizedReceptionDate = form.receptionDate.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) { if (!receptionStore.current) {
const created = await receptionStore.createReception({ const created = await receptionStore.createReception({
currentStep: 1, currentStep: 1,
licensePlate: normalizedLicensePlate || null, licensePlate: normalizedLicensePlate,
receptionDate: normalizedReceptionDate || null receptionDate: normalizedReceptionDate
}) })
if (created) { if (created) {
await router.push(`/reception/${created.id}`) await router.push(`/reception/${created.id}`)
@@ -104,8 +72,8 @@ async function validate() {
const nextStep = receptionStore.current.currentStep + 1 const nextStep = receptionStore.current.currentStep + 1
await receptionStore.updateReception(receptionStore.current.id, { await receptionStore.updateReception(receptionStore.current.id, {
currentStep: nextStep, currentStep: nextStep,
licensePlate: normalizedLicensePlate || null, licensePlate: normalizedLicensePlate,
receptionDate: normalizedReceptionDate || null receptionDate: normalizedReceptionDate
}) })
} }

View File

@@ -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>

View File

@@ -10,6 +10,7 @@
"@nuxtjs/i18n": "^10.2.1", "@nuxtjs/i18n": "^10.2.1",
"@pinia/nuxt": "^0.11.3", "@pinia/nuxt": "^0.11.3",
"izitoast": "^1.4.0", "izitoast": "^1.4.0",
"maska": "^3.2.0",
"nuxt": "^4.2.2", "nuxt": "^4.2.2",
"nuxt-toast": "^1.4.0", "nuxt-toast": "^1.4.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
@@ -9195,6 +9196,11 @@
"source-map-js": "^1.2.1" "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": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

View File

@@ -14,6 +14,7 @@
"@nuxtjs/i18n": "^10.2.1", "@nuxtjs/i18n": "^10.2.1",
"@pinia/nuxt": "^0.11.3", "@pinia/nuxt": "^0.11.3",
"izitoast": "^1.4.0", "izitoast": "^1.4.0",
"maska": "^3.2.0",
"nuxt": "^4.2.2", "nuxt": "^4.2.2",
"nuxt-toast": "^1.4.0", "nuxt-toast": "^1.4.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",

View File

@@ -2,7 +2,11 @@
<div> <div>
<div class="flex justify-between h-[52px] mb-[90px]"> <div class="flex justify-between h-[52px] mb-[90px]">
<p class="self-center">Indicateur détapes</p> <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> </div>
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/> <ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/> <ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
@@ -21,13 +25,39 @@ const router = useRouter()
const receptionStore = useReceptionStore() const receptionStore = useReceptionStore()
const { current: storeReception } = storeToRefs(receptionStore) const { current: storeReception } = storeToRefs(receptionStore)
onMounted(async () => { const resolveReceptionId = (param: unknown) => {
const raw = route.params.id const idStr = Array.isArray(param) ? param[0] : param
const idStr = Array.isArray(raw) ? raw[0] : raw if (!idStr) {
const id = idStr ? Number(idStr) : null return null
if (id !== null) {
await receptionStore.loadReception(id)
} }
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> </script>