improvement: error handeling structure changed and unified all across the app. user no longer sees inappropriate messages.

This commit is contained in:
2026-07-12 18:27:38 +03:30
parent 901d838a2c
commit fab5111aa8
45 changed files with 977 additions and 241 deletions

View File

@@ -20,7 +20,7 @@ import { ScheduleDayPicker } from '@/components/ui/shared/ScheduleDayPicker';
import { ToastStack } from '@/components/ui/shared/Toast';
import { useToast } from '@/lib/hooks/useToast';
import type { AppointmentPurpose } from '@/types/appointment';
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { compareLocalDayStart, getLocalDayIsoRange, startOfLocalDay } from '@/components/appointments/appointmentTime';
const EMPTY_PATIENT_FORM: CreatePatientInput = {
@@ -32,6 +32,7 @@ const EMPTY_PATIENT_FORM: CreatePatientInput = {
export default function AppointmentsPage() {
const t = useTranslations('appointments');
const tErrors = useTranslations('errors');
const tPatients = useTranslations('patients');
const { currentOrganization } = useAuth();
const [scheduleDate, setScheduleDate] = useState(() => startOfLocalDay(new Date()));
@@ -105,7 +106,7 @@ export default function AppointmentsPage() {
if (gen !== scheduleLoadGen.current) {
return;
}
toast.showError(formatApiErrorMessage(err, t('errorLoadSchedule')));
toast.showError(getUserFacingError(err, tErrors, t('errorLoadSchedule')));
} finally {
if (gen === scheduleLoadGen.current) {
setLoadingSchedule(false);
@@ -178,11 +179,7 @@ export default function AppointmentsPage() {
);
}
} catch (err: unknown) {
const message =
err && typeof err === 'object' && 'message' in err
? String((err as { message: unknown }).message)
: tPatients('errorSavePatient');
toast.showError(message);
toast.showError(getUserFacingError(err, tErrors, tPatients('errorSavePatient')));
} finally {
setSavingPatient(false);
}
@@ -248,13 +245,13 @@ export default function AppointmentsPage() {
toast.showSuccess(activeEditingAppointment ? t('successUpdated') : t('successSaved'));
await loadSchedule();
} catch (err: unknown) {
const message =
err && typeof err === 'object' && 'message' in err
? String((err as { message: unknown }).message)
: activeEditingAppointment
? t('errorUpdate')
: t('errorSave');
toast.showError(message);
toast.showError(
getUserFacingError(
err,
tErrors,
activeEditingAppointment ? t('errorUpdate') : t('errorSave'),
),
);
} finally {
setSavingAppointment(false);
}
@@ -276,11 +273,7 @@ export default function AppointmentsPage() {
toast.showSuccess(t('successRemoved'));
await loadSchedule();
} catch (err: unknown) {
const message =
err && typeof err === 'object' && 'message' in err
? String((err as { message: unknown }).message)
: t('errorDelete');
toast.showError(message);
toast.showError(getUserFacingError(err, tErrors, t('errorDelete')));
} finally {
setDeletingAppointment(false);
}

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { ToastStack } from '@/components/ui/shared/Toast';
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { useAuth } from '@/lib/hooks/useAuth';
import { useToast } from '@/lib/hooks/useToast';
import { canEditCases, canEditTasks } from '@/components/shared/permissions';
@@ -35,6 +35,7 @@ const PAGE_SIZE = 20;
export default function CasesPage() {
const t = useTranslations('cases');
const tErrors = useTranslations('errors');
const tCommon = useTranslations('common');
const { currentOrganization, user } = useAuth();
const toast = useToast();
@@ -112,7 +113,7 @@ export default function CasesPage() {
setCases(response.data.items);
setPagination(response.data.pagination);
} catch (error: unknown) {
toast.showError(formatApiErrorMessage(error, t('errorLoadList')));
toast.showError(getUserFacingError(error, tErrors, t('errorLoadList')));
} finally {
setLoadingList(false);
}
@@ -127,7 +128,7 @@ export default function CasesPage() {
const response = await casesApi.getOne(caseId);
setSelectedCase(response.data);
} catch (error: unknown) {
toast.showError(formatApiErrorMessage(error, t('errorLoadDetail')));
toast.showError(getUserFacingError(error, tErrors, t('errorLoadDetail')));
if (!options?.silent) {
setSelectedCase(null);
}
@@ -218,7 +219,7 @@ export default function CasesPage() {
setSelectedCase(response.data);
} catch (error: unknown) {
setSelectedCase(previousCase);
toast.showError(formatApiErrorMessage(error, t('errorUpdateTask')));
toast.showError(getUserFacingError(error, tErrors, t('errorUpdateTask')));
} finally {
setUpdatingImportant(false);
}

View File

@@ -24,7 +24,7 @@ import { Input } from '@/components/ui/shared/Input';
import { SearchBar } from '@/components/ui/shared/SearchBar';
import { Table } from '@/components/ui/shared/Table';
import { ToastStack } from '@/components/ui/shared/Toast';
import type { ApiError } from '@/types/api';
import { getUserFacingError } from '@/components/shared/formatApiError';
function formatOrganizationStatusLabel(status: string): string {
if (!status) return status;
@@ -42,6 +42,7 @@ type TableMode = 'existing' | 'search';
export default function OrganizationsPage() {
const t = useTranslations('organizations');
const tErrors = useTranslations('errors');
const tNav = useTranslations('nav');
const tCommon = useTranslations('common');
const { currentOrganization } = useAuth();
@@ -49,14 +50,8 @@ export default function OrganizationsPage() {
const toast = useToast();
const formatApiMessage = useCallback(
(err: unknown): string => {
if (!err || typeof err !== 'object') return tCommon('errorGeneric');
const m = (err as ApiError).message;
if (Array.isArray(m)) return m.join(', ');
if (typeof m === 'string') return m;
return tCommon('errorGeneric');
},
[tCommon],
(err: unknown): string => getUserFacingError(err, tErrors, tCommon('errorGeneric')),
[tCommon, tErrors],
);
const formatConnectionStatusLabel = useCallback(

View File

@@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/shared/Button';
import { ToastStack } from '@/components/ui/shared/Toast';
import { patientsApi } from '@/lib/api/patients';
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { useAuth } from '@/lib/hooks/useAuth';
import { useToast } from '@/lib/hooks/useToast';
import { hasPermission } from '@/components/shared/permissions';
@@ -23,6 +23,7 @@ const EMPTY_PATIENT_FORM: CreatePatientInput = {
export default function PatientsPage() {
const t = useTranslations('patients');
const tErrors = useTranslations('errors');
const tCommon = useTranslations('common');
const { currentOrganization } = useAuth();
const toast = useToast();
@@ -67,7 +68,7 @@ export default function PatientsPage() {
setSelectedPatient(freshSelected);
}
} catch (error: unknown) {
toast.showError(formatApiErrorMessage(error, t('errorLoadPatients')));
toast.showError(getUserFacingError(error, tErrors, t('errorLoadPatients')));
} finally {
setLoadingPatients(false);
}
@@ -98,7 +99,7 @@ export default function PatientsPage() {
);
}
} catch (error: unknown) {
toast.showError(formatApiErrorMessage(error, t('errorSavePatient')));
toast.showError(getUserFacingError(error, tErrors, t('errorSavePatient')));
} finally {
setSavingPatient(false);
}

View File

@@ -16,6 +16,7 @@ import { Input } from '@/components/ui/shared/Input';
import { Toast } from '@/components/ui/shared/Toast';
import { Checkbox } from '@/components/ui/shared/Checkbox';
import { DialogCloseButton } from '@/components/ui/shared/DialogCloseButton';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { OwnerWorkingHoursDialog } from '@/components/settings/OwnerWorkingHoursDialog';
type PasswordForm = {
@@ -26,6 +27,7 @@ type PasswordForm = {
export default function AccountSettingsPage() {
const t = useTranslations('settings');
const tErrors = useTranslations('errors');
const tAuth = useTranslations('auth');
const tCommon = useTranslations('common');
const tValidation = useTranslations('validation');
@@ -152,8 +154,7 @@ export default function AccountSettingsPage() {
await syncSessionAfterParticipationChange();
setSuccessMessage(t('participateEnabledTasks'));
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('participateUpdateFailed');
setError(message || t('participateUpdateFailed'));
setError(getUserFacingError(err, tErrors, t('participateUpdateFailed')));
setParticipatesInTasks(false);
} finally {
setParticipationLoading(false);
@@ -180,8 +181,7 @@ export default function AccountSettingsPage() {
setRevokeConfirmOpen(false);
setPendingRevokeType(null);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('participateUpdateFailed');
setError(message || t('participateUpdateFailed'));
setError(getUserFacingError(err, tErrors, t('participateUpdateFailed')));
} finally {
setParticipationLoading(false);
}
@@ -214,8 +214,7 @@ export default function AccountSettingsPage() {
try {
await enableClinicParticipation(options);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('participateUpdateFailed');
setError(message || t('participateUpdateFailed'));
setError(getUserFacingError(err, tErrors, t('participateUpdateFailed')));
throw err;
} finally {
setParticipationLoading(false);
@@ -249,8 +248,7 @@ export default function AccountSettingsPage() {
setSuccessMessage(t('passwordChanged'));
router.replace('/login');
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('passwordChangeFailed');
setError(message || t('passwordChangeFailed'));
setError(getUserFacingError(err, tErrors, t('passwordChangeFailed')));
} finally {
setIsSubmitting(false);
}

View File

@@ -35,7 +35,7 @@ import { Input } from '@/components/ui/shared/Input';
import { Checkbox } from '@/components/ui/shared/Checkbox';
import { Table } from '@/components/ui/shared/Table';
import { ToastStack } from '@/components/ui/shared/Toast';
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { StaffMembersMobileList } from '@/components/staff/StaffMembersMobileList';
import { useToast } from '@/lib/hooks/useToast';
@@ -147,6 +147,7 @@ function PermissionGrid({
export default function StaffPage() {
const router = useRouter();
const t = useTranslations('staff');
const tErrors = useTranslations('errors');
const tCommon = useTranslations('common');
const tFeatures = useTranslations('staff.features');
const tWorkingHours = useTranslations('staff.workingHours');
@@ -229,7 +230,7 @@ export default function StaffPage() {
setMembers(res.data.members);
setSeats(res.data.seats);
} catch (e) {
toast.showError(formatApiErrorMessage(e, t('errorLoadStaff')));
toast.showError(getUserFacingError(e, tErrors, t('errorLoadStaff')));
} finally {
setLoading(false);
}
@@ -304,7 +305,7 @@ export default function StaffPage() {
await load();
}
} catch (e) {
toast.showError(formatApiErrorMessage(e, t('errorCopyInvite')));
toast.showError(getUserFacingError(e, tErrors, t('errorCopyInvite')));
} finally {
setCopyingInviteMembershipId(null);
}
@@ -387,7 +388,7 @@ export default function StaffPage() {
resetInviteForm();
await load();
} catch (e) {
toast.showError(formatApiErrorMessage(e, t('errorSendInvite')));
toast.showError(getUserFacingError(e, tErrors, t('errorSendInvite')));
} finally {
setInviteLoading(false);
}
@@ -410,7 +411,7 @@ export default function StaffPage() {
setEditWorkingHoursDays(state.days);
setEditAutoRepeatWeekly(state.autoRepeatWeekly);
} catch (e) {
toast.showError(formatApiErrorMessage(e, t('errorLoadWorkingHours')));
toast.showError(getUserFacingError(e, tErrors, t('errorLoadWorkingHours')));
} finally {
setEditLoadingWorkingHours(false);
}
@@ -449,7 +450,7 @@ export default function StaffPage() {
setEditStep(1);
await load();
} catch (e) {
toast.showError(formatApiErrorMessage(e, t('errorUpdateMember')));
toast.showError(getUserFacingError(e, tErrors, t('errorUpdateMember')));
} finally {
setEditLoading(false);
}
@@ -470,7 +471,7 @@ export default function StaffPage() {
setDisableTarget(null);
await load();
} catch (e) {
toast.showError(formatApiErrorMessage(e, t('errorDisableMember')));
toast.showError(getUserFacingError(e, tErrors, t('errorDisableMember')));
} finally {
setDisablingMembershipId(null);
}
@@ -487,7 +488,7 @@ export default function StaffPage() {
setEnableTarget(null);
await load();
} catch (e) {
toast.showError(formatApiErrorMessage(e, t('errorEnableMember')));
toast.showError(getUserFacingError(e, tErrors, t('errorEnableMember')));
} finally {
setEnablingMembershipId(null);
}
@@ -600,7 +601,7 @@ export default function StaffPage() {
setCopiedInviteMembershipId(lastInviteInfo.membershipId);
setTimeout(() => setCopiedInviteMembershipId(null), 1500);
} catch (e) {
toast.showError(formatApiErrorMessage(e, t('errorCopyInvite')));
toast.showError(getUserFacingError(e, tErrors, t('errorCopyInvite')));
} finally {
setCopyingInviteMembershipId(null);
}

View File

@@ -17,7 +17,7 @@ import {
formatToothList,
prosthesisTypeBadgeStyle,
} from '@/components/ui/treatment/prosthesisTypeDisplay';
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { canEditTasks, canViewTasks } from '@/components/shared/permissions';
import { useAuth } from '@/lib/hooks/useAuth';
import { useToast } from '@/lib/hooks/useToast';
@@ -38,6 +38,7 @@ function formatPatientName(patient: { firstName: string; lastName: string }) {
export default function TasksPage() {
const t = useTranslations('tasks');
const tErrors = useTranslations('errors');
const { currentOrganization, user, isAuthReady } = useAuth();
const { showError, setError, messages: toastMessages } = useToast();
@@ -107,7 +108,7 @@ export default function TasksPage() {
setTasks(response.data.items);
setPagination(response.data.pagination);
} catch (error: unknown) {
showError(formatApiErrorMessage(error, tRef.current('errorLoadList')));
showError(getUserFacingError(error, tErrors, tRef.current('errorLoadList')));
} finally {
setLoading(false);
}
@@ -127,7 +128,7 @@ export default function TasksPage() {
await tasksApi.updateStatus(taskId, status);
await loadTasks();
} catch (error: unknown) {
showError(formatApiErrorMessage(error, t('errorUpdateTask')));
showError(getUserFacingError(error, tErrors, t('errorUpdateTask')));
} finally {
setUpdatingTaskId(null);
}

View File

@@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { useTranslations } from 'next-intl';
import { Link } from '@/i18n/navigation';
import { useAuth } from '@/lib/hooks/useAuth';
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { TodayDashboard } from '@/components/today/TodayDashboard';
import { TodayLoadErrorBanner } from '@/components/today/TodayLoadErrorBanner';
import { TodaySectionErrorFallback } from '@/components/today/TodaySectionErrorFallback';
@@ -13,6 +13,7 @@ import { useTodaySummary } from '@/lib/hooks/useTodaySummary';
export default function TodayPage() {
const t = useTranslations('today');
const tErrors = useTranslations('errors');
const { currentOrganization } = useAuth();
const orgId = currentOrganization?.id;
const { data, loading, isInitialLoad, error, reload } = useTodaySummary(orgId);
@@ -54,7 +55,7 @@ export default function TodayPage() {
{error ? (
<TodayLoadErrorBanner
message={formatApiErrorMessage(error, t('loadError'))}
message={getUserFacingError(error, tErrors, t('loadError'))}
retryLabel={t('retryLoad')}
onRetry={() => void reload()}
isRetrying={loading && Boolean(data)}