From f39ae774019c23f57ae63313c03a986048c62d2f Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 28 Jul 2026 12:28:11 +0000 Subject: [PATCH] fix(backoffice): map raw IAM error codes to friendly messages (EDRFREIGHT-223, EDRFREIGHT-225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @tria-plc/iamapi-common throws BadRequestException with a raw, untranslated code as the message (e.g. "user_role_not_found", "unit_employee_limit_reached:5") — useErrorHandler's extractMessage returned that string verbatim, so removing an org/unit admin whose role row didn't match, or inviting past a unit's employee cap, toasted the raw backend code instead of an explanation. Adds a small code→i18n map checked before the raw-message fallback, shared by both the async and sync error handlers so every caller (org-admin removal, employee invites, etc.) picks it up for free. Co-Authored-By: Claude Sonnet 5 --- .../src/locales/am/translation.json | 2 + .../src/locales/en/translation.json | 2 + .../src/locales/fr/translation.json | 2 + .../src/shared/hooks/useErrorHandler.ts | 38 +++++++++++++++---- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/locales/am/translation.json b/apps/edr-freight-web/backoffice/src/locales/am/translation.json index 09acb8c62..f8e26a07a 100644 --- a/apps/edr-freight-web/backoffice/src/locales/am/translation.json +++ b/apps/edr-freight-web/backoffice/src/locales/am/translation.json @@ -1649,6 +1649,8 @@ "notFoundError": "የፈለጉትን መረጃ አልተገኘም።", "fileTooLarge": "ፋይሉ በጣም ትልቅ ነው። እባክዎ ፋይሉን አሳንሰው ዳግም ይሞክሩ።", "serverError": "ከአገልጋይ በኩል ችግር አለ። እባክዎ ዳግመኛ ይሞክሩ።", + "userRoleNotFound": "ይህ የአስተዳዳሪ ሚና ምደባ አልተገኘም — ቀደም ብሎ ተወግዶ ሊሆን ይችላል።", + "unitEmployeeLimitReached": "ይህ ክፍል የ{{limit}} ሰራተኞች ገደብ ላይ ደርሷል።", "attachmentDeleted": "አባሪው በተሳካ ሁኔታ ተሰርዟል።", "replyAdded": "ምላሹ በተሳካ ሁኔታ ታክሏል!", "replyError": "ምላሹን በመጨመር ላይ ስህተት አጋጥሟል።", diff --git a/apps/edr-freight-web/backoffice/src/locales/en/translation.json b/apps/edr-freight-web/backoffice/src/locales/en/translation.json index 3fa0f0ee3..66deb4395 100644 --- a/apps/edr-freight-web/backoffice/src/locales/en/translation.json +++ b/apps/edr-freight-web/backoffice/src/locales/en/translation.json @@ -1669,6 +1669,8 @@ "notFoundError": "We couldn't find what you were looking for.", "fileTooLarge": "The file is too large. Please reduce the file size and try again.", "serverError": "Something went wrong on our side. Please try again in a moment.", + "userRoleNotFound": "This admin role assignment could not be found — it may have already been removed.", + "unitEmployeeLimitReached": "This unit has reached its limit of {{limit}} employees.", "attachmentDeleted": "Attachment deleted successfully.", "replyAdded": "Reply added successfully!", "replyError": "An error occurred while adding the reply.", diff --git a/apps/edr-freight-web/backoffice/src/locales/fr/translation.json b/apps/edr-freight-web/backoffice/src/locales/fr/translation.json index 5019a2fb8..b888a2419 100644 --- a/apps/edr-freight-web/backoffice/src/locales/fr/translation.json +++ b/apps/edr-freight-web/backoffice/src/locales/fr/translation.json @@ -1169,6 +1169,8 @@ "notFoundError": "Nous n’avons pas trouvé ce que vous cherchiez.", "fileTooLarge": "Le fichier est trop volumineux. Veuillez réduire sa taille et réessayer.", "serverError": "Un problème est survenu de notre côté. Veuillez réessayer dans un instant.", + "userRoleNotFound": "Cette attribution de rôle d'administrateur est introuvable — elle a peut-être déjà été supprimée.", + "unitEmployeeLimitReached": "Cette unité a atteint sa limite de {{limit}} employés.", "attachmentDeleted": "Pièce jointe supprimée avec succès.", "replyAdded": "Réponse ajoutée avec succès !", "replyError": "Une erreur s’est produite lors de l’ajout de la réponse.", diff --git a/apps/edr-freight-web/backoffice/src/shared/hooks/useErrorHandler.ts b/apps/edr-freight-web/backoffice/src/shared/hooks/useErrorHandler.ts index f5d37d18d..9c42868ba 100644 --- a/apps/edr-freight-web/backoffice/src/shared/hooks/useErrorHandler.ts +++ b/apps/edr-freight-web/backoffice/src/shared/hooks/useErrorHandler.ts @@ -54,6 +54,28 @@ const extractMessage = (value: unknown): string | null => { } }; +// IAM (@tria-plc/iamapi-common) throws BadRequestException with a raw, +// untranslated code string as the message — no i18n on that side — so it +// would otherwise reach the UI verbatim (e.g. "user_role_not_found"). Map +// known codes to a friendly, translated message before falling back to the +// raw text. `unit_employee_limit_reached` carries its configured limit after +// a colon (e.g. "unit_employee_limit_reached:5"). +const UNIT_EMPLOYEE_LIMIT_PREFIX = "unit_employee_limit_reached:"; + +const mapIamErrorCode = ( + raw: string | null, + t: (key: string, options?: Record) => string, +): string | null => { + if (!raw) return null; + if (raw.startsWith(UNIT_EMPLOYEE_LIMIT_PREFIX)) { + return t("msg.unitEmployeeLimitReached", { + limit: raw.slice(UNIT_EMPLOYEE_LIMIT_PREFIX.length), + }); + } + if (raw === "user_role_not_found") return t("msg.userRoleNotFound"); + return null; +}; + // Maps an HTTP status code to the i18n key used when no backend message is available. const statusKeyFor = (status: number | undefined): string => { if (status === 400 || status === 422) return "msg.validationError"; @@ -111,7 +133,7 @@ const parseBlobBody = async (blob: Blob): Promise => { }; export const useErrorHandler = ( - t: (key: string) => string, + t: (key: string, options?: Record) => string, ) => { const getErrorMessage = useCallback( async (err: unknown): Promise => { @@ -134,15 +156,15 @@ export const useErrorHandler = ( const fromException = extractMessage((data as any)?.exception?.response) ?? extractMessage((data as any)?.exception); - if (fromException) return fromException; + if (fromException) return mapIamErrorCode(fromException, t) ?? fromException; const fromData = extractMessage(data); - if (fromData) return fromData; + if (fromData) return mapIamErrorCode(fromData, t) ?? fromData; } if (err instanceof Error) { const fromError = extractMessage(err.message); - if (fromError) return fromError; + if (fromError) return mapIamErrorCode(fromError, t) ?? fromError; } return t(statusKeyFor(status)); @@ -186,7 +208,7 @@ export const useErrorHandler = ( }; export const useClientErrorHandler = ( - t: (key: string) => string, + t: (key: string, options?: Record) => string, ) => { const getErrorMessage = useCallback( (err: unknown): string => { @@ -205,13 +227,13 @@ export const useClientErrorHandler = ( const fromException = extractMessage(data?.exception?.response) ?? extractMessage(data?.exception); - if (fromException) return fromException; + if (fromException) return mapIamErrorCode(fromException, t) ?? fromException; const fromData = extractMessage(data); - if (fromData) return fromData; + if (fromData) return mapIamErrorCode(fromData, t) ?? fromData; const fromError = extractMessage((err as any).message); - if (fromError) return fromError; + if (fromError) return mapIamErrorCode(fromError, t) ?? fromError; } return t(statusKeyFor(status));