fix(errors) : humanize backend error messages for end users

Add centralized error translation layer (humanizeError) that converts
raw Symfony/Doctrine/API Platform messages into user-friendly French.
Fix useApi to extract errors from all backend response formats
(violations, error, message, hydra:description, detail).
Add toast deduplication to prevent double display. Replace error toast
icon (X → CircleX) to distinguish from the dismiss button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-04 09:48:51 +01:00
parent 55e2a4fafe
commit c7fd8328d6
15 changed files with 227 additions and 49 deletions

View File

@@ -370,6 +370,7 @@ import { useProducts } from '~/composables/useProducts'
import { useProductTypes } from '~/composables/useProductTypes'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import { useCustomFields } from '~/composables/useCustomFields'
import { useDocuments } from '~/composables/useDocuments'
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
@@ -998,7 +999,7 @@ const submitCreation = async () => {
toast.showError(result.error)
}
} catch (error: any) {
toast.showError(error?.message || 'Erreur lors de la création du composant')
toast.showError(humanizeError(error?.message) || 'Impossible de créer le composant')
} finally {
submitting.value = false
uploadingDocuments.value = false

View File

@@ -472,6 +472,7 @@ import { useSites } from '~/composables/useSites'
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useMachines } from '~/composables/useMachines'
import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import IconLucideFactory from '~icons/lucide/factory'
import IconLucideMapPin from '~icons/lucide/map-pin'
import IconLucideUser from '~icons/lucide/user'
@@ -731,10 +732,10 @@ const confirmDeleteMachine = async (machine) => {
showSuccess(`Machine "${machine.name}" supprimée avec succès`)
await loadMachines()
} else {
showError(`Erreur lors de la suppression: ${result.error}`)
showError(`Impossible de supprimer la machine : ${result.error}`)
}
} catch (error) {
showError(`Erreur lors de la suppression: ${error.message}`)
showError(`Impossible de supprimer la machine : ${humanizeError(error.message)}`)
}
}
}

View File

@@ -104,6 +104,7 @@
import { ref, computed, onMounted } from "vue";
import { useMachineTypesApi } from "~/composables/useMachineTypesApi";
import { useToast } from "~/composables/useToast";
import { humanizeError } from "~/shared/utils/errorMessages";
import IconLucidePlus from "~icons/lucide/plus";
import IconLucidePackage from "~icons/lucide/package";
import IconLucideLayoutGrid from "~icons/lucide/layout-grid";
@@ -148,10 +149,10 @@ const confirmDeleteType = async (type) => {
if (result.success) {
showSuccess(`Type "${type.name}" supprimé avec succès`);
} else {
showError(`Erreur lors de la suppression: ${result.error}`);
showError(`Impossible de supprimer le type : ${result.error}`);
}
} catch (error) {
showError(`Erreur lors de la suppression: ${error.message}`);
showError(`Impossible de supprimer le type : ${humanizeError(error.message)}`);
}
}
};

View File

@@ -138,6 +138,7 @@ import { useMachines } from '~/composables/useMachines'
import { useSites } from '~/composables/useSites'
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideFactory from '~icons/lucide/factory'
import IconLucideMapPin from '~icons/lucide/map-pin'
@@ -214,10 +215,10 @@ const confirmDeleteMachine = async (machine) => {
if (result.success) {
showSuccess(`Machine "${machine.name}" supprimée avec succès`)
} else {
showError(`Erreur lors de la suppression: ${result.error}`)
showError(`Impossible de supprimer la machine : ${result.error}`)
}
} catch (error) {
showError(`Erreur lors de la suppression: ${error.message}`)
showError(`Impossible de supprimer la machine : ${humanizeError(error.message)}`)
}
}
}

View File

@@ -315,6 +315,7 @@ import SearchSelect from '~/components/common/SearchSelect.vue'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { usePieces } from '~/composables/usePieces'
import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import { useCustomFields } from '~/composables/useCustomFields'
import { useDocuments } from '~/composables/useDocuments'
import { formatPieceStructurePreview } from '~/shared/modelUtils'
@@ -599,7 +600,7 @@ const submitCreation = async () => {
toast.showError(result.error)
}
} catch (error: any) {
toast.showError(error?.message || 'Erreur lors de la création de la pièce')
toast.showError(humanizeError(error?.message) || 'Impossible de créer la pièce')
} finally {
submitting.value = false
uploadingDocuments.value = false

View File

@@ -407,6 +407,7 @@ import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
import { useProducts } from '~/composables/useProducts'
import { useCustomFields } from '~/composables/useCustomFields'
import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import { useDocuments } from '~/composables/useDocuments'
import { useConstructeurs } from '~/composables/useConstructeurs'
import { useProductHistory, type ProductHistoryEntry } from '~/composables/useProductHistory'
@@ -700,7 +701,7 @@ const submitEdition = async () => {
await router.push('/product-catalog')
}
} catch (error: any) {
toast.showError(error?.message || 'Erreur lors de la mise à jour du produit')
toast.showError(humanizeError(error?.message) || 'Impossible de mettre à jour le produit')
} finally {
saving.value = false
}