feature: localization's first implmentation done. all frontend hardcoded text is now localized.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppointmentsStrip } from '@/components/ui/treatment/AppointmentsStrip';
|
||||
import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart';
|
||||
import { PastTreatmentsPanel } from '@/components/ui/treatment/PastTreatmentsPanel';
|
||||
@@ -34,6 +35,14 @@ import type {
|
||||
TreatmentCaseDraft,
|
||||
} from '@/types/treatment';
|
||||
|
||||
const TREATMENT_TYPE_KEYS = {
|
||||
consultation: 'typeConsultation',
|
||||
filling: 'typeFilling',
|
||||
endo: 'typeEndo',
|
||||
visit: 'typeVisit',
|
||||
hygiene: 'typeHygiene',
|
||||
} as const;
|
||||
|
||||
function newCase(): TreatmentCaseDraft {
|
||||
return {
|
||||
clientId:
|
||||
@@ -121,6 +130,7 @@ interface TreatmentWorkspaceProps {
|
||||
}
|
||||
|
||||
export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWorkspaceProps) {
|
||||
const t = useTranslations('treatment');
|
||||
const { showError, showSuccess, messages: toastMessages } = useToast();
|
||||
const canView = canViewTreatment(currentOrganization);
|
||||
const canEdit = canEditTreatment(currentOrganization);
|
||||
@@ -185,12 +195,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
const currentDraftPreview = useMemo<PastTreatment | null>(() => {
|
||||
if (!selectedAppointment) return null;
|
||||
return casesToPreviewTreatment(cases, {
|
||||
title: `Draft · ${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`,
|
||||
title: t('draftTitle', {
|
||||
patientName: `${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`,
|
||||
}),
|
||||
patientId: selectedAppointment.patientId,
|
||||
treatmentAt: new Date().toISOString(),
|
||||
status: 'draft',
|
||||
});
|
||||
}, [cases, selectedAppointment]);
|
||||
}, [cases, selectedAppointment, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectionLocked(false);
|
||||
@@ -217,7 +229,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (!cancelled) {
|
||||
showError(formatApiErrorMessage(error, 'Failed to load appointments.'));
|
||||
showError(formatApiErrorMessage(error, t('errorLoadAppointments')));
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setApptsLoading(false);
|
||||
@@ -226,7 +238,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [userId, selectedDay, showError]);
|
||||
}, [userId, selectedDay, showError, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const today = startOfLocalDay(new Date());
|
||||
@@ -250,14 +262,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
if (!cancelled) setOrgs(list.data);
|
||||
} catch (error: unknown) {
|
||||
if (!cancelled) {
|
||||
showError(formatApiErrorMessage(error, 'Failed to load linked organizations.'));
|
||||
showError(formatApiErrorMessage(error, t('errorLoadOrgs')));
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [showError]);
|
||||
}, [showError, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAppointment) {
|
||||
@@ -272,7 +284,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
if (!cancelled) setHistory(response.data);
|
||||
} catch (error: unknown) {
|
||||
if (!cancelled) {
|
||||
showError(formatApiErrorMessage(error, 'Failed to load treatment history.'));
|
||||
showError(formatApiErrorMessage(error, t('errorLoadHistory')));
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setHistoryLoading(false);
|
||||
@@ -281,7 +293,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedAppointment?.patientId, showError]);
|
||||
}, [selectedAppointment?.patientId, showError, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const appointmentId = selectedAppointment?.id;
|
||||
@@ -310,19 +322,19 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
setOrganizationSearch('');
|
||||
} catch (error: unknown) {
|
||||
if (!cancelled) {
|
||||
showError(formatApiErrorMessage(error, 'Failed to load treatment draft.'));
|
||||
showError(formatApiErrorMessage(error, t('errorLoadDraft')));
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedAppointment?.id, showError]);
|
||||
}, [selectedAppointment?.id, showError, t]);
|
||||
|
||||
const confirmDiscardIfDirty = useCallback(() => {
|
||||
if (!isDirty) return true;
|
||||
return window.confirm('You have unsaved changes. Discard them and continue?');
|
||||
}, [isDirty]);
|
||||
return window.confirm(t('confirmDiscard'));
|
||||
}, [isDirty, t]);
|
||||
|
||||
const onPickAppointment = useCallback(
|
||||
(id: string) => {
|
||||
@@ -361,16 +373,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
: c,
|
||||
),
|
||||
);
|
||||
showSuccess(
|
||||
`${uploaded.data.length} file${uploaded.data.length === 1 ? '' : 's'} uploaded successfully.`,
|
||||
);
|
||||
showSuccess(t('successFilesUploaded', { count: uploaded.data.length }));
|
||||
} catch (error: unknown) {
|
||||
showError(formatApiErrorMessage(error, 'Failed to upload attachments.'));
|
||||
showError(formatApiErrorMessage(error, t('errorUpload')));
|
||||
} finally {
|
||||
setUploadBusyCaseId(null);
|
||||
}
|
||||
},
|
||||
[canEditTreatmentForDay, selectedAppointment, showSuccess, showError],
|
||||
[canEditTreatmentForDay, selectedAppointment, showSuccess, showError, t],
|
||||
);
|
||||
|
||||
const persistDraft = useCallback(async () => {
|
||||
@@ -401,13 +411,13 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
setSaveBusy(true);
|
||||
try {
|
||||
await persistDraft();
|
||||
showSuccess('Treatment draft saved.');
|
||||
showSuccess(t('successDraftSaved'));
|
||||
} catch (error: unknown) {
|
||||
showError(formatApiErrorMessage(error, 'Failed to save treatment draft.'));
|
||||
showError(formatApiErrorMessage(error, t('errorSaveDraft')));
|
||||
} finally {
|
||||
setSaveBusy(false);
|
||||
}
|
||||
}, [canEditTreatmentForDay, selectedAppointment, persistDraft, showSuccess, showError]);
|
||||
}, [canEditTreatmentForDay, selectedAppointment, persistDraft, showSuccess, showError, t]);
|
||||
|
||||
const handleSendCase = useCallback(
|
||||
async (treatmentCase: TreatmentCaseDraft) => {
|
||||
@@ -416,14 +426,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
orgs.some((o) => o.id === id && o.active),
|
||||
);
|
||||
if (targets.length === 0) {
|
||||
showError('Choose at least one active organization to send this case.');
|
||||
showError(t('errorChooseOrg'));
|
||||
return;
|
||||
}
|
||||
setSendBusyId(treatmentCase.clientId);
|
||||
try {
|
||||
const saved = await persistDraft();
|
||||
const serverCase = saved.cases.find((c) => c.clientId === treatmentCase.clientId);
|
||||
if (!serverCase?.id) throw new Error('Case must be saved before sending.');
|
||||
if (!serverCase?.id) throw new Error(t('errorCaseMustSave'));
|
||||
|
||||
const response = await treatmentsApi.sendCase(serverCase.id, { organizationIds: targets });
|
||||
setCases((prev) => {
|
||||
@@ -445,14 +455,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
const next = [...targets.filter((id) => !prev.includes(id)), ...prev];
|
||||
return next.slice(0, 10);
|
||||
});
|
||||
showSuccess('Case sent to selected organizations.');
|
||||
showSuccess(t('successCaseSent'));
|
||||
} catch (error: unknown) {
|
||||
showError(formatApiErrorMessage(error, 'Failed to send case.'));
|
||||
showError(formatApiErrorMessage(error, t('errorSendCase')));
|
||||
} finally {
|
||||
setSendBusyId(null);
|
||||
}
|
||||
},
|
||||
[canEditTreatmentForDay, selectedAppointment, orgs, persistDraft, showSuccess, showError],
|
||||
[canEditTreatmentForDay, selectedAppointment, orgs, persistDraft, showSuccess, showError, t],
|
||||
);
|
||||
|
||||
const openPreview = useCallback(
|
||||
@@ -489,9 +499,9 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
if (!canView) {
|
||||
return (
|
||||
<div className="surface-card p-6 max-w-xl">
|
||||
<h2 className="text-lg font-semibold text-text-primary">Treatment workspace</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary">{t('noPermissionTitle')}</h2>
|
||||
<p className="text-sm text-text-secondary mt-2">
|
||||
You do not have permission to view the Treatment tab for this organization.
|
||||
{t('noPermissionBody')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -500,11 +510,9 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<header className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold text-text-primary">Treatment</h1>
|
||||
<h1 className="text-2xl font-semibold text-text-primary">{t('title')}</h1>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{canEdit
|
||||
? 'Document cases for your appointments, save drafts, and send work to linked organizations.'
|
||||
: 'View-only access — you can review appointments and treatment history but cannot edit.'}
|
||||
{canEdit ? t('subtitleEdit') : t('subtitleReadOnly')}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -523,8 +531,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
|
||||
{isViewingPastDay && (
|
||||
<p className="text-sm text-text-secondary rounded-[var(--radius-md)] border border-border/60 bg-background-secondary/50 px-3 py-2">
|
||||
Past days are view-only. You can review appointments and history, but treatment cases
|
||||
cannot be added or changed.
|
||||
{t('pastDayNotice')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -532,18 +539,20 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
<div className="space-y-3 min-w-0 xl:max-w-[380px]">
|
||||
{selectedAppointment ? (
|
||||
<div className="surface-card p-3 space-y-0.5">
|
||||
<p className="text-[10px] uppercase tracking-wide text-text-muted">Selected patient</p>
|
||||
<p className="text-[10px] uppercase tracking-wide text-text-muted">{t('selectedPatient')}</p>
|
||||
<p className="text-base font-semibold text-text-primary">
|
||||
{selectedAppointment.patientFirstName} {selectedAppointment.patientLastName}
|
||||
</p>
|
||||
<p className="text-[11px] text-text-secondary">
|
||||
Purpose:{' '}
|
||||
<span className="capitalize text-text-primary">{selectedAppointment.purpose}</span>
|
||||
{t('purposeLabel')}{' '}
|
||||
<span className="capitalize text-text-primary">
|
||||
{t(TREATMENT_TYPE_KEYS[selectedAppointment.purpose as keyof typeof TREATMENT_TYPE_KEYS] ?? selectedAppointment.purpose)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="surface-card p-3 text-sm text-text-muted">
|
||||
{apptsLoading ? 'Loading appointments…' : 'Select a day with at least one appointment.'}
|
||||
{apptsLoading ? t('loadingAppointments') : t('selectDayWithAppointment')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user