feat: integrate error handling across various components

- Added `useErrorHandler` hook to centralize error handling logic.
- Updated components in the backoffice and portal applications to utilize the new error handling mechanism, replacing direct notify calls with `handleError` for improved error messaging.
- Enhanced localization files to include generic error messages for better user feedback.
- Refactored error handling in forms and API interactions to ensure consistent user experience across the application.
This commit is contained in:
estifanos
2026-07-24 09:08:41 +00:00
parent 4ccf27c044
commit 8f7c163b9e
28 changed files with 229 additions and 121 deletions

View File

@@ -19,7 +19,7 @@ import {
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import { IconEdit, IconTrash, IconPlus, IconInfoCircle, IconCertificate } from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
import {
useGetCertificationsQuery,
useCreateCertificationMutation,
@@ -75,6 +75,7 @@ function CertificationForm({
export function CertificationPage() {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const { data, isLoading, isError } = useGetCertificationsQuery();
const [createCert, { isLoading: isCreating }] = useCreateCertificationMutation();
const [updateCert, { isLoading: isUpdating }] = useUpdateCertificationMutation();
@@ -104,8 +105,8 @@ export function CertificationPage() {
notify.success(t('certification.created'));
}
resetForm();
} catch {
notify.error(t('certification.error'));
} catch (e) {
handleError(e);
}
};
@@ -116,8 +117,8 @@ export function CertificationPage() {
notify.success(t('certification.deleted'));
closeDelete();
setDeleteTarget(null);
} catch {
notify.error(t('certification.error'));
} catch (e) {
handleError(e);
}
};

View File

@@ -21,7 +21,7 @@ import { useForm } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { IconEdit, IconTrash, IconPlus, IconBriefcase, IconMap, IconCertificate, IconInfoCircle } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
import { LocationPage } from '../../location/pages/LocationPage';
import { CertificationPage } from '../../certification/pages/CertificationPage';
import {
@@ -131,6 +131,7 @@ function ProfessionForm({ editingProf, deptOptions, isSubmitting, onSubmit, onCa
function ProfessionTab() {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const { data: deptRes } = useGetOrganizationsQuery();
const { data: profRes, isLoading, isError } = useGetProfessionsQuery();
const [createProfession, { isLoading: isCreating }] = useCreateProfessionMutation();
@@ -172,10 +173,10 @@ function ProfessionTab() {
notify.success(t('configuration.deleted'));
closeDelete();
setDeleteTarget(null);
} catch {
notify.error(t('configuration.error'));
} catch (e) {
handleError(e);
}
}, [deleteTarget, deleteProfession, closeDelete, t]);
}, [deleteTarget, deleteProfession, closeDelete, handleError]);
const handleProfSubmit = useCallback(async (values: ProfFormValues) => {
const name = { en: values.nameEn, am: values.nameAm };
@@ -199,10 +200,10 @@ function ProfessionTab() {
notify.success(t('configuration.created'));
}
resetProfForm();
} catch {
notify.error(t('configuration.error'));
} catch (e) {
handleError(e);
}
}, [editingProf, createProfession, updateProfession, resetProfForm, t]);
}, [editingProf, createProfession, updateProfession, resetProfForm, handleError]);
const getDeptName = useCallback((deptId: string) => {
const dept = departments.find((d) => d.id === deptId);

View File

@@ -39,7 +39,7 @@ import {
IconCheck,
IconX,
} from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
import { useGetExamQuery, useUpdateExamMutation, useAssignQuestionsMutation } from '../api/exam-api';
import { useGetQuestionsQuery } from '../../question/api/question-api';
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
@@ -69,6 +69,7 @@ function InfoRow({ label, value }: { label: string; value: string }) {
export function ExamDetailPage() {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const printRef = useRef<HTMLDivElement>(null);
@@ -154,8 +155,8 @@ export function ExamDetailPage() {
await assignQuestions({ examId: exam.id, questionIds, remark: undefined }).unwrap();
notify.success('Questions assigned');
closeAssign();
} catch {
notify.error('Failed to assign questions');
} catch (e) {
handleError(e);
}
};

View File

@@ -25,7 +25,7 @@ import {
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import { IconEdit, IconTrash, IconPlus, IconInfoCircle, IconCalendar, IconClipboardList } from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
import {
useGetExamsQuery,
@@ -149,6 +149,7 @@ function ExamForm({
export function ExamPage() {
const navigate = useNavigate();
const { t, i18n } = useTranslation();
const { handleError } = useErrorHandler();
const locale = i18n.language as 'en' | 'am';
const { data: certRes } = useGetCertificationsQuery();
const { data, isLoading, isError } = useGetExamsQuery();
@@ -195,8 +196,8 @@ export function ExamPage() {
notify.success(t('exam.created'));
}
resetForm();
} catch {
notify.error(t('exam.error'));
} catch (e) {
handleError(e);
}
};
@@ -207,8 +208,8 @@ export function ExamPage() {
notify.success(t('exam.deleted'));
closeDelete();
setDeleteTarget(null);
} catch {
notify.error(t('exam.error'));
} catch (e) {
handleError(e);
}
};

View File

@@ -1,7 +1,7 @@
import { Table, Badge, ActionIcon, Text } from '@mantine/core';
import { IconTrash } from '@tabler/icons-react';
import { useGetItemsQuery, useDeleteItemMutation, type Item } from '../api/item-api';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
const STATUS_COLORS: Record<Item['status'], string> = {
DRAFT: 'gray',
@@ -12,13 +12,14 @@ const STATUS_COLORS: Record<Item['status'], string> = {
export function ItemTable() {
const { data, isLoading } = useGetItemsQuery({});
const [deleteItem] = useDeleteItemMutation();
const { handleError } = useErrorHandler();
const handleDelete = async (id: string) => {
try {
await deleteItem(id).unwrap();
notify.success('Item deleted');
} catch {
notify.error('Failed to delete item');
} catch (e) {
handleError(e);
}
};

View File

@@ -22,7 +22,7 @@ import {
useUpdateLocationTypeMutation,
useDeleteLocationTypeMutation,
} from '../api/location-api';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
interface LocationTypeFormValues {
code: string;
@@ -34,6 +34,7 @@ interface LocationTypeFormValues {
export function LocationTypeModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const { data: locationTypes, isLoading } = useGetLocationTypesQuery();
const [createType] = useCreateLocationTypeMutation();
const [updateType] = useUpdateLocationTypeMutation();
@@ -84,8 +85,8 @@ export function LocationTypeModal({ opened, onClose }: { opened: boolean; onClos
try {
await deleteType(id).unwrap();
notify.success(t('location.typeDeleted'));
} catch {
notify.error(t('location.deleteError'));
} catch (e) {
handleError(e);
}
};
@@ -99,8 +100,8 @@ export function LocationTypeModal({ opened, onClose }: { opened: boolean; onClos
notify.success(t('location.typeCreated'));
}
resetForm();
} catch {
notify.error(t('location.typeError'));
} catch (e) {
handleError(e);
}
});

View File

@@ -17,7 +17,7 @@ import {
import { useDisclosure } from '@mantine/hooks';
import { IconSettings, IconMap, IconInfoCircle, IconPlus } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
import { LocationTree } from '../components/LocationTree';
import { LocationDetail } from '../components/LocationDetail';
import { LocationForm } from '../components/LocationForm';
@@ -33,6 +33,7 @@ import type { Location } from '../types/location';
export function LocationPage() {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const { data: locationTypesRes, isLoading: typesLoading } = useGetLocationTypesQuery();
const [createLocation, { isLoading: isCreating }] = useCreateLocationMutation();
const [updateLocation, { isLoading: isUpdating }] = useUpdateLocationMutation();
@@ -82,11 +83,11 @@ export function LocationPage() {
closeFormModal();
setEditingLocation(null);
setParentLocation(null);
} catch {
notify.error(t('location.error'));
} catch (e) {
handleError(e);
}
},
[editingLocation, createLocation, updateLocation, closeFormModal, t],
[editingLocation, createLocation, updateLocation, closeFormModal, handleError],
);
const handleDeleteConfirm = useCallback(async () => {
@@ -96,10 +97,10 @@ export function LocationPage() {
notify.success(t('location.deleted'));
setSelectedLocation(null);
closeDeleteModal();
} catch {
notify.error(t('location.deleteError'));
} catch (e) {
handleError(e);
}
}, [selectedLocation, deleteLocation, closeDeleteModal, t]);
}, [selectedLocation, deleteLocation, closeDeleteModal, handleError]);
if (typesLoading) {
return (

View File

@@ -42,7 +42,7 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { notify, PageHeader } from '@ema-platform/ui';
import { notify, PageHeader, useErrorHandler } from '@ema-platform/ui';
import { useApiMutation } from '@ema-platform/api';
import { setUser } from '@ema-platform/auth';
import type { AuthUser } from '@ema-platform/auth';
@@ -77,6 +77,7 @@ export function ProfilePage() {
const user = useAppSelector((state) => state.auth.user);
const { colorScheme, setColorScheme } = useMantineColorScheme();
const layoutMode = useAppSelector((state) => state.preferences.layoutMode);
const { handleError } = useErrorHandler();
const [updateTrigger] = useApiMutation<AuthUser>();
const [meTrigger] = useApiMutation<AuthUser>();
@@ -156,8 +157,8 @@ export function ProfilePage() {
dispatch(setUser(me));
notify.success(t('profile.profileUpdated'));
} catch {
notify.error(t('profile.updateFailed'));
} catch (e) {
handleError(e);
} finally {
setIsSavingProfile(false);
}
@@ -208,8 +209,8 @@ export function ProfilePage() {
notify.success(t('profile.passwordChanged'));
resetPassword();
} catch {
notify.error(t('profile.passwordFailed'));
} catch (e) {
handleError(e);
} finally {
setIsSavingPassword(false);
}

View File

@@ -20,7 +20,7 @@ import {
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import { IconEdit, IconTrash, IconPlus, IconInfoCircle } from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
import {
useGetQuestionsQuery,
@@ -102,6 +102,7 @@ function QuestionForm({
export function QuestionPage() {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const { data: certRes } = useGetCertificationsQuery();
const { data, isLoading, isError } = useGetQuestionsQuery();
const [createQ, { isLoading: isCreating }] = useCreateQuestionMutation();
@@ -140,8 +141,8 @@ export function QuestionPage() {
notify.success(t('question.created'));
}
resetForm();
} catch {
notify.error(t('question.error'));
} catch (e) {
handleError(e);
}
};
@@ -152,8 +153,8 @@ export function QuestionPage() {
notify.success(t('question.deleted'));
closeDelete();
setDeleteTarget(null);
} catch {
notify.error(t('question.error'));
} catch (e) {
handleError(e);
}
};

View File

@@ -17,7 +17,7 @@ import {
Alert,
} from '@mantine/core';
import { IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
import { useApiQuery } from '@ema-platform/api';
import { useCreateResultMutation, useUpdateResultMutation } from '../api/result-api';
import type { Exam } from '../../exam/types/exam';
@@ -42,6 +42,7 @@ export function RecordResultModal({
}) {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const [seafarerSearch, setSeafarerSearch] = useState('');
const [selectedSeafarerId, setSelectedSeafarerId] = useState<string | null>(null);
const [scores, setScores] = useState<Record<string, number>>({});
@@ -106,8 +107,8 @@ export function RecordResultModal({
setRemark('');
setSeafarerSearch('');
onClose();
} catch {
notify.error(t('result.recordModal.saveError'));
} catch (e) {
handleError(e);
}
};

View File

@@ -35,7 +35,7 @@ import {
IconChartBar,
IconSearch,
} from '@tabler/icons-react';
import { notify, BilingualInput } from '@ema-platform/ui';
import { notify, BilingualInput, useErrorHandler } from '@ema-platform/ui';
import type { BilingualValue } from '@ema-platform/ui';
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation, useUpdateResultMutation } from '../api/result-api';
import { useGetExamsQuery } from '../../exam/api/exam-api';
@@ -95,6 +95,7 @@ function InfoRow({ label, value }: { label: string; value: string }) {
export function ResultPage() {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const { data: examRes } = useGetExamsQuery();
const { data, isLoading, isError } = useGetResultsQuery();
const [fetchDetail, { data: detailResult, isFetching: isDetailLoading }] = useLazyGetResultQuery();
@@ -180,8 +181,8 @@ export function ResultPage() {
}).unwrap();
notify.success(t('result.updated'));
closeDetail();
} catch {
notify.error(t('result.error'));
} catch (e) {
handleError(e);
} finally {
setDetailSaving(false);
}
@@ -201,8 +202,8 @@ export function ResultPage() {
notify.success(t('result.deleted'));
closeDelete();
setDeleteTarget(null);
} catch {
notify.error(t('result.error'));
} catch (e) {
handleError(e);
}
};

View File

@@ -8,6 +8,17 @@ export const am: Translations = {
tagline: 'የቁጥጥር ማዕከል',
},
msg: {
genericError: 'የሆነ ስህተት ተፈጥሯል። እባክዎ እንደገና ይሞክሩ።',
serverError: 'የሰርቨር ስህተት። እባክዎ ቆየት ብለው እንደገና ይሞክሩ።',
validationError: 'እባክዎ ያስገቡትን መረጃ ያረጋግጡና እንደገና ይሞክሩ።',
authError: 'ክፍለ ጊዜዎ አልቋል። እባክዎ እንደገና ይግቡ።',
permissionError: 'ይህን ድርጊት ለመፈጸም ፈቃድ የለዎትም።',
notFoundError: 'የተጠየቀው ንጥል አልተገኘም።',
fileTooLarge: 'ፋይሉ ለመስቀል በጣም ትልቅ ነው።',
networkError: 'የአውታረ መረብ ስህተት። ግንኙነትዎን አረጋግጠው እንደገና ይሞክሩ።',
},
language: {
label: 'ቋንቋ',
en: 'English',

View File

@@ -6,6 +6,17 @@ export const en = {
tagline: 'Control Center',
},
msg: {
genericError: 'Something went wrong. Please try again.',
serverError: 'Server error. Please try again later.',
validationError: 'Please check your input and try again.',
authError: 'Your session has expired. Please sign in again.',
permissionError: "You don't have permission to perform this action.",
notFoundError: 'The requested item was not found.',
fileTooLarge: 'The file is too large to upload.',
networkError: 'Network error. Check your connection and try again.',
},
language: {
label: 'Language',
en: 'English',