Ajout de la génération du bon de reception et correction des retours #4
24
.idea/workspace.xml
generated
24
.idea/workspace.xml
generated
@@ -4,15 +4,13 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<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$/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$/frontend/composables/useApi.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/useApi.ts" 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/components/reception/reception-form.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" 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/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>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -47,7 +45,6 @@
|
||||
<commands />
|
||||
<urls />
|
||||
</component>
|
||||
<component name="PhpDebugGeneral" listening_started="true" />
|
||||
<component name="PhpServers">
|
||||
<servers>
|
||||
<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="1768374298711" duration="12403000" />
|
||||
<workItem from="1768460547451" duration="26946000" />
|
||||
<workItem from="1768547023783" duration="7809000" />
|
||||
<workItem from="1768547023783" duration="11371000" />
|
||||
</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)">
|
||||
<option name="closed" value="true" />
|
||||
@@ -326,7 +323,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768498751836</updated>
|
||||
</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 />
|
||||
</component>
|
||||
<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="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" />
|
||||
<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>
|
||||
</project>
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
93
frontend/components/ui/license-plate-input.vue
Normal file
93
frontend/components/ui/license-plate-input.vue
Normal 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>
|
||||
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@@ -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