feature: localization's first implmentation done. all frontend hardcoded text is now localized.
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { DialogCloseButton } from '@/components/ui/shared/DialogCloseButton';
|
||||
import { Dropdown } from '@/components/ui/shared/Dropdown';
|
||||
import type { AppointmentPurpose, AppointmentRecord } from '@/types/appointment';
|
||||
import { APPOINTMENT_PURPOSE_LABEL } from '@/components/ui/appointments/appointmentPurposeStyles';
|
||||
import { APPOINTMENT_PURPOSES } from '@/types/appointment';
|
||||
import { getPurposeLabel } from '@/components/ui/appointments/appointmentPurposeStyles';
|
||||
import type { Patient } from '@/types/patient';
|
||||
import {
|
||||
combineLocalDateAndTime,
|
||||
@@ -36,6 +38,14 @@ interface AppointmentBookingModalProps {
|
||||
deleting?: boolean;
|
||||
}
|
||||
|
||||
const PURPOSE_OPTION_COLORS: Record<AppointmentPurpose, string> = {
|
||||
consultation: '#ddd6fe',
|
||||
filling: '#fed7aa',
|
||||
endo: '#fecaca',
|
||||
visit: '#bae6fd',
|
||||
hygiene: '#d9f99d',
|
||||
};
|
||||
|
||||
export function AppointmentBookingModal({
|
||||
open,
|
||||
scheduleDate,
|
||||
@@ -51,20 +61,15 @@ export function AppointmentBookingModal({
|
||||
onDelete,
|
||||
deleting = false,
|
||||
}: AppointmentBookingModalProps) {
|
||||
const t = useTranslations('appointments');
|
||||
const tCommon = useTranslations('common');
|
||||
const tPatients = useTranslations('patients');
|
||||
|
||||
const [startTime, setStartTime] = useState('09:00');
|
||||
const [endTime, setEndTime] = useState('10:00');
|
||||
const [purpose, setPurpose] = useState<AppointmentPurpose>('consultation');
|
||||
const [error, setError] = useState('');
|
||||
const purposeTextColor =
|
||||
purpose === 'consultation'
|
||||
? '#ddd6fe'
|
||||
: purpose === 'filling'
|
||||
? '#fed7aa'
|
||||
: purpose === 'endo'
|
||||
? '#fecaca'
|
||||
: purpose === 'visit'
|
||||
? '#bae6fd'
|
||||
: '#d9f99d';
|
||||
const purposeTextColor = PURPOSE_OPTION_COLORS[purpose];
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -116,7 +121,7 @@ export function AppointmentBookingModal({
|
||||
return;
|
||||
}
|
||||
if (!editingAppointment && !patient) {
|
||||
setError('Select a patient first.');
|
||||
setError(t('errorSelectPatient'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,26 +129,26 @@ export function AppointmentBookingModal({
|
||||
const endAt = combineLocalDateAndTime(scheduleDate, endTime);
|
||||
|
||||
if (endAt <= startAt) {
|
||||
setError('End time must be after start time.');
|
||||
setError(t('errorEndAfterStart'));
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
if (isSameLocalCalendarDay(scheduleDate, now) && startAt.getTime() < now.getTime()) {
|
||||
setError('Cannot schedule in the past.');
|
||||
setError(t('errorPastSchedule'));
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
if (compareLocalDayStart(scheduleDate, today) < 0) {
|
||||
setError('Past appointments are view-only.');
|
||||
setError(t('errorPastViewOnly'));
|
||||
return;
|
||||
}
|
||||
|
||||
const effectivePatientId = editingAppointment?.patientId ?? patient?.id;
|
||||
const effectiveProviderId = editingAppointment?.providerUserId ?? providerUserId;
|
||||
if (!effectivePatientId || !effectiveProviderId) {
|
||||
setError('Missing appointment details.');
|
||||
setError(t('errorMissingDetails'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,29 +171,34 @@ export function AppointmentBookingModal({
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h2 id="appointment-modal-title" className="text-lg font-semibold text-text-primary pr-2">
|
||||
{editingAppointment ? 'Edit appointment' : 'New appointment'}
|
||||
{editingAppointment ? t('editTitle') : t('newTitle')}
|
||||
</h2>
|
||||
<DialogCloseButton onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-text-secondary">
|
||||
Provider: <span className="text-text-primary font-medium">{providerName}</span>
|
||||
{t('providerLabel')}{' '}
|
||||
<span className="text-text-primary font-medium">{providerName}</span>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">Patient</label>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
{t('patientLabel')}
|
||||
</label>
|
||||
<p className="text-sm text-text-primary rounded-[var(--radius-md)] border border-border bg-background-secondary/60 px-3 py-2">
|
||||
{editingAppointment
|
||||
? `${editingAppointment.patient.firstName} ${editingAppointment.patient.lastName}`
|
||||
: patient
|
||||
? `${patient.firstName} ${patient.lastName}`
|
||||
: '—'}
|
||||
: tPatients('emptyValue')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">Start</label>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
{t('startLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
step={60}
|
||||
@@ -198,7 +208,9 @@ export function AppointmentBookingModal({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">End</label>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
{t('endLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
step={60}
|
||||
@@ -210,26 +222,20 @@ export function AppointmentBookingModal({
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
label="Purpose"
|
||||
label={t('purposeLabel')}
|
||||
value={purpose}
|
||||
onChange={(e) => setPurpose(e.target.value as AppointmentPurpose)}
|
||||
style={{ color: purposeTextColor }}
|
||||
>
|
||||
<option value="consultation" style={{ color: '#ddd6fe', backgroundColor: '#14253d' }}>
|
||||
{APPOINTMENT_PURPOSE_LABEL.consultation}
|
||||
</option>
|
||||
<option value="filling" style={{ color: '#fed7aa', backgroundColor: '#14253d' }}>
|
||||
{APPOINTMENT_PURPOSE_LABEL.filling}
|
||||
</option>
|
||||
<option value="endo" style={{ color: '#fecaca', backgroundColor: '#14253d' }}>
|
||||
{APPOINTMENT_PURPOSE_LABEL.endo}
|
||||
</option>
|
||||
<option value="visit" style={{ color: '#bae6fd', backgroundColor: '#14253d' }}>
|
||||
{APPOINTMENT_PURPOSE_LABEL.visit}
|
||||
</option>
|
||||
<option value="hygiene" style={{ color: '#d9f99d', backgroundColor: '#14253d' }}>
|
||||
{APPOINTMENT_PURPOSE_LABEL.hygiene}
|
||||
</option>
|
||||
{APPOINTMENT_PURPOSES.map((purposeOption) => (
|
||||
<option
|
||||
key={purposeOption}
|
||||
value={purposeOption}
|
||||
style={{ color: PURPOSE_OPTION_COLORS[purposeOption], backgroundColor: '#14253d' }}
|
||||
>
|
||||
{getPurposeLabel(purposeOption, t)}
|
||||
</option>
|
||||
))}
|
||||
</Dropdown>
|
||||
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
@@ -243,14 +249,14 @@ export function AppointmentBookingModal({
|
||||
disabled={loading || deleting}
|
||||
isLoading={deleting}
|
||||
>
|
||||
Delete
|
||||
{tCommon('delete')}
|
||||
</Button>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<Button type="button" variant="ghost" onClick={onClose} disabled={loading || deleting}>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -259,7 +265,7 @@ export function AppointmentBookingModal({
|
||||
isLoading={loading}
|
||||
disabled={deleting}
|
||||
>
|
||||
Save
|
||||
{tCommon('save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user