feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur

This commit is contained in:
AUTIN Tristan
2026-01-16 10:18:12 +01:00
parent 94ea49587a
commit 2d3ce2ca43
9 changed files with 2612 additions and 53 deletions
+36 -37
View File
@@ -4,27 +4,15 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="test : ajout de TU sur les services et providers">
<change afterPath="$PROJECT_DIR$/templates/reception_voucher.html.twig" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/ferme.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/ferme.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/php.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/php.xml" afterDir="false" />
<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">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/composer.json" beforeDir="false" afterPath="$PROJECT_DIR$/composer.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/composer.lock" beforeDir="false" afterPath="$PROJECT_DIR$/composer.lock" 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/components/reception/reception-form.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-unloading.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-unloading.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" 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/composables/useWeighing.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/useWeighing.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/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/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" 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/services/weight.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/weight.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/stores/reception.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/stores/reception.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Entity/Reception.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Reception.php" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -36,7 +24,7 @@
<execution />
</component>
<component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="147" />
<option name="cachedIndexableFilesCount" value="151" />
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
</component>
<component name="FileTemplateManagerImpl">
@@ -59,6 +47,7 @@
<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">
@@ -221,28 +210,28 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.MCP Project settings loaded": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"git-widget-placeholder": "feat/reception-generation-bon",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "reference.webide.settings.project.settings.php.debug",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;feat/reception-generation-bon&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;reference.webide.settings.project.settings.php.debug&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
"keyToStringList": {
"vue.recent.templates": [
"Vue Composition API Component"
&quot;keyToStringList&quot;: {
&quot;vue.recent.templates&quot;: [
&quot;Vue Composition API Component&quot;
]
}
}]]></component>
}</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
@@ -270,7 +259,8 @@
<workItem from="1768201706520" duration="13383000" />
<workItem from="1768287908317" duration="28058000" />
<workItem from="1768374298711" duration="12403000" />
<workItem from="1768460547451" duration="26867000" />
<workItem from="1768460547451" duration="26946000" />
<workItem from="1768547023783" duration="7809000" />
</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" />
@@ -328,7 +318,15 @@
<option name="project" value="LOCAL" />
<updated>1768318921478</updated>
</task>
<option name="localTasksCounter" value="8" />
<task id="LOCAL-00008" summary="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="closed" value="true" />
<created>1768498751836</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1768498751836</updated>
</task>
<option name="localTasksCounter" value="9" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -382,6 +380,7 @@
<MESSAGE value="feat : update du fichier README.md et CHANGELOG.md" />
<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" />
<option name="LAST_COMMIT_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" />
</component>
</project>
+7
View File
@@ -22,17 +22,24 @@ Frontend conventions
- Layout in `frontend/layouts/default.vue`: max width `1050px`, header full width.
- Tailwind custom color palette is `primary` (e.g. `bg-primary-500`).
- API composable in `frontend/composables/useApi.ts` with `get/post/put/patch/delete` and default JSON/PATCH content types.
- API errors/success toasts can be customized via `toastErrorMessage`/`toastSuccessMessage` or i18n keys `toastErrorKey`/`toastSuccessKey`. Global method fallbacks use `errors.http.*` keys.
- `useApi` uses `useNuxtApp().$i18n` (not `useI18n`) to avoid setup-only constraint in service calls.
- Pinia store: `frontend/stores/reception.ts` is the source of truth for the current reception.
- Zod is used for form validation (e.g. `frontend/components/reception/reception-form.vue`); shared helpers live in `frontend/utils/zod-errors.ts`.
- Weighing logic is shared via `frontend/composables/useWeighing.ts`.
- Reception step UI uses store state (`currentStep`) in `frontend/pages/reception/[[id]].vue`.
- Active nav styles in header use `NuxtLink` with `custom` slot.
- Reusable UI components live under `frontend/components/ui/` and are auto-imported with `Ui` prefix (e.g. `UiLoadingDots`).
- Service layer lives in `frontend/services/` with typed DTOs in `frontend/services/dto/`.
- Reception service uses `receptions`, `receptions/{id}`, `receptions/weigh` and supports success/error toast keys.
- Reception receipt endpoint is `receptions/{id}/receipt` (PDF) via `frontend/composables/usePdfPrinter.ts`.
Environment & routing
- Frontend dev server: `npm run dev` in `frontend/`.
- API base for local dev: `http://localhost:8080/api` (set in `frontend/.env` via `NUXT_PUBLIC_API_BASE`).
- CORS handled by Nelmio; `.env` includes `CORS_ALLOW_ORIGIN` regex for localhost.
- Nuxt i18n locales live in `frontend/i18n/locales` (configured via `langDir: 'locales'`).
- Default locale is `fr`; translations in `frontend/i18n/locales/fr.json`.
Notes
- Do not add a GET that creates resources; use POST + PATCH.
+18
View File
@@ -0,0 +1,18 @@
.iziToast {
font-size: 16px;
min-height: 72px;
}
.iziToast > .iziToast-body {
padding: 18px 24px;
}
.iziToast > .iziToast-body .iziToast-title {
font-size: 18px;
line-height: 1.3;
}
.iziToast > .iziToast-body .iziToast-message {
font-size: 16px;
line-height: 1.5;
}
+52 -1
View File
@@ -16,12 +16,25 @@ export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
FetchOptions<ResponseType> & {
toast?: boolean
toastTitle?: string
toastErrorMessage?: string
toastSuccessMessage?: string
toastErrorKey?: string
toastSuccessKey?: string
}
export const useApi = (): ApiClient => {
const config = useRuntimeConfig()
const baseURL = config.public.apiBase ?? '/api'
const toast = useToast()
const nuxtApp = useNuxtApp()
const i18n = nuxtApp.$i18n as
| {
t: (key: string) => string
te?: (key: string) => boolean
}
| undefined
const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
const te = (key: string) => (i18n?.te ? i18n.te(key) : false)
const extractErrorMessage = (error: unknown, responseData?: unknown): string => {
const data = responseData ?? (error as FetchError)?.data
@@ -46,17 +59,55 @@ export const useApi = (): ApiClient => {
return (error as FetchError)?.message ?? 'Erreur inconnue.'
}
const methodErrorKeys: Record<string, string> = {
GET: 'errors.http.get',
POST: 'errors.http.post',
PUT: 'errors.http.put',
PATCH: 'errors.http.patch',
DELETE: 'errors.http.delete'
}
const client = $fetch.create({
baseURL,
retry: 0,
onResponse({ options }) {
const apiOptions = options as ApiFetchOptions<'json'>
if (apiOptions?.toast === false) {
return
}
const successKey = apiOptions?.toastSuccessKey
const successMessage =
apiOptions?.toastSuccessMessage ||
(successKey ? (te(successKey) ? t(successKey) : successKey) : '')
if (successMessage) {
toast.success({
title: 'Succès',
message: successMessage
})
}
},
onResponseError({ response, error, options }) {
const apiOptions = options as ApiFetchOptions<'json'>
if (apiOptions?.toast === false) {
return
}
const method =
typeof options?.method === 'string' ? options.method.toUpperCase() : 'GET'
const defaultKey = methodErrorKeys[method]
const defaultMessage =
defaultKey && te(defaultKey) ? t(defaultKey) : ''
const errorKey = apiOptions?.toastErrorKey
const errorMessage =
errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
const extractedMessage = extractErrorMessage(error, response?._data)
const message =
extractErrorMessage(error, response?._data) ||
apiOptions?.toastErrorMessage ||
errorMessage ||
defaultMessage ||
extractedMessage ||
'Une erreur est survenue.'
toast.error({
+23
View File
@@ -0,0 +1,23 @@
{
"errors": {
"http": {
"get": "Impossible de récupérer les données.",
"post": "Impossible de créer la ressource.",
"put": "Impossible de mettre à jour la ressource.",
"patch": "Impossible de mettre à jour la ressource.",
"delete": "Impossible de supprimer la ressource."
},
"reception": {
"list": "Impossible de récupérer la liste des réceptions.",
"fetch": "Impossible de récupérer la réception.",
"create": "Impossible de créer la réception.",
"update": "Impossible de mettre à jour la réception.",
"weigh": "Impossible de récupérer la pesée."
}
},
"success": {
"reception": {
"update": "Réception mise à jour avec succès."
}
}
}
+23 -2
View File
@@ -2,13 +2,34 @@ export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
ssr: false,
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt', 'nuxt-toast'],
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'nuxt-toast',
'@nuxtjs/i18n'
],
css: ['~/assets/css/toast.css'],
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE
}
},
toast: {
settings: {
timeout: 0,
closeOnClick: true,
progressBar: false
}
},
i18n: {
strategy: 'no_prefix',
defaultLocale: 'fr',
langDir: 'locales',
locales: [
{ code: 'fr', file: 'fr.json', name: 'Français' }
]
},
typescript: {
strict: true
}
})
})
+2433 -5
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -11,6 +11,7 @@
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
},
"dependencies": {
"@nuxtjs/i18n": "^10.2.1",
"@pinia/nuxt": "^0.11.3",
"izitoast": "^1.4.0",
"nuxt": "^4.2.2",
+19 -8
View File
@@ -1,28 +1,39 @@
import { useApi } from '~/composables/useApi'
import type { ReceptionData } from '~/services/dto/reception-data'
import type { WeightData } from '~/services/dto/weight-data'
import {useApi} from '~/composables/useApi'
import type {ReceptionData} from '~/services/dto/reception-data'
import type {WeightData} from '~/services/dto/weight-data'
export async function getReceptionList() {
const api = useApi()
return api.get<ReceptionData>(`receptions`)
return api.get<ReceptionData>(`receptions`, {}, {
toastErrorKey: 'errors.reception.list'
})
}
export async function getReception(id: number) {
const api = useApi()
return api.get<ReceptionData>(`receptions/${id}`)
return api.get<ReceptionData>(`receptions/${id}`, {}, {
toastErrorKey: 'errors.reception.fetch'
})
}
export async function createReception(payload: Partial<ReceptionData> = {}) {
const api = useApi()
return api.post<ReceptionData>('receptions', payload)
return api.post<ReceptionData>('receptions', payload, {
toastErrorKey: 'errors.reception.create'
})
}
export async function updateReception(id: number, payload: Partial<ReceptionData>) {
const api = useApi()
return api.patch<ReceptionData>(`receptions/${id}`, payload)
return api.patch<ReceptionData>(`receptions/${id}`, payload, {
toastErrorKey: 'errors.reception.update',
toastSuccessKey: 'success.reception.update'
})
}
export async function getWeight(): Promise<WeightData> {
const api = useApi()
return api.get<WeightData>('receptions/weigh')
return api.get<WeightData>('receptions/weigh', {}, {
toastErrorKey: 'errors.reception.weigh'
})
}