fix(backoffice): map raw IAM error codes to friendly messages (EDRFREIGHT-223, EDRFREIGHT-225)

@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 <noreply@anthropic.com>
This commit is contained in:
Nathnael
2026-07-28 12:28:11 +00:00
parent b1cb149c44
commit f39ae77401
4 changed files with 36 additions and 8 deletions

View File

@@ -1649,6 +1649,8 @@
"notFoundError": "የፈለጉትን መረጃ አልተገኘም።",
"fileTooLarge": "ፋይሉ በጣም ትልቅ ነው። እባክዎ ፋይሉን አሳንሰው ዳግም ይሞክሩ።",
"serverError": "ከአገልጋይ በኩል ችግር አለ። እባክዎ ዳግመኛ ይሞክሩ።",
"userRoleNotFound": "ይህ የአስተዳዳሪ ሚና ምደባ አልተገኘም — ቀደም ብሎ ተወግዶ ሊሆን ይችላል።",
"unitEmployeeLimitReached": "ይህ ክፍል የ{{limit}} ሰራተኞች ገደብ ላይ ደርሷል።",
"attachmentDeleted": "አባሪው በተሳካ ሁኔታ ተሰርዟል።",
"replyAdded": "ምላሹ በተሳካ ሁኔታ ታክሏል!",
"replyError": "ምላሹን በመጨመር ላይ ስህተት አጋጥሟል።",

View File

@@ -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.",

View File

@@ -1169,6 +1169,8 @@
"notFoundError": "Nous navons 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 sest produite lors de lajout de la réponse.",

View File

@@ -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, unknown>) => 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<unknown> => {
};
export const useErrorHandler = (
t: (key: string) => string,
t: (key: string, options?: Record<string, unknown>) => string,
) => {
const getErrorMessage = useCallback(
async (err: unknown): Promise<string> => {
@@ -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, unknown>) => 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));