bugfix: all the toasts unfied inide appointments feature. DateSelector component updated to an expandable one.

This commit is contained in:
2026-05-17 16:48:42 +03:30
parent 2025a868ea
commit 375fdc60b4
2 changed files with 290 additions and 84 deletions

View File

@@ -14,7 +14,8 @@ import { AppointmentScheduleGrid } from '@/components/ui/appointments/Appointmen
import { AppointmentsPatientSearch } from '@/components/ui/appointments/AppointmentsPatientSearch';
import { AppointmentScheduleLegend } from '@/components/ui/appointments/AppointmentScheduleLegend';
import { ScheduleDayPicker } from '@/components/ui/common/ScheduleDayPicker';
import { Toast } from '@/components/ui/common/Toast';
import { ToastStack } from '@/components/ui/common/Toast';
import { useToast } from '@/lib/hooks/useToast';
import type { AppointmentPurpose } from '@/types/appointment';
import { formatApiErrorMessage } from '@/lib/formatApiError';
import { compareLocalDayStart, getLocalDayIsoRange, startOfLocalDay } from '@/lib/appointmentTime';
@@ -33,7 +34,7 @@ export default function AppointmentsPage() {
const [providers, setProviders] = useState<AppointmentColumnProvider[]>([]);
const [appointments, setAppointments] = useState<AppointmentRecord[]>([]);
const [loadingSchedule, setLoadingSchedule] = useState(false);
const [scheduleError, setScheduleError] = useState('');
const toast = useToast();
const [search, setSearch] = useState('');
const [patients, setPatients] = useState<Patient[]>([]);
@@ -51,9 +52,6 @@ export default function AppointmentsPage() {
const [editingAppointmentId, setEditingAppointmentId] = useState<string | null>(null);
const [savingAppointment, setSavingAppointment] = useState(false);
const [toastError, setToastError] = useState('');
const [toastSuccess, setToastSuccess] = useState('');
const [toastInfo, setToastInfo] = useState('');
const canManageAppointments = canEditAppointments(currentOrganization);
const canEditPatients = hasPermission(currentOrganization, 'TAB_PATIENTS_EDIT');
@@ -84,7 +82,7 @@ export default function AppointmentsPage() {
}
const gen = ++scheduleLoadGen.current;
setLoadingSchedule(true);
setScheduleError('');
toast.setError('');
try {
const range = getLocalDayIsoRange(scheduleDate);
const [pRes, aRes] = await Promise.all([
@@ -100,7 +98,7 @@ export default function AppointmentsPage() {
if (gen !== scheduleLoadGen.current) {
return;
}
setScheduleError(formatApiErrorMessage(err, 'Failed to load schedule.'));
toast.showError(formatApiErrorMessage(err, 'Failed to load schedule.'));
} finally {
if (gen === scheduleLoadGen.current) {
setLoadingSchedule(false);
@@ -143,21 +141,20 @@ export default function AppointmentsPage() {
async function handleCreatePatient() {
setSavingPatient(true);
setToastError('');
setToastSuccess('');
toast.setError('');
try {
const response = await patientsApi.create(patientForm);
setIsCreateOpen(false);
setPatientForm(EMPTY_PATIENT_FORM);
await loadPatientsSearch(search);
setSelectedPatient(response.data);
setToastSuccess(`Patient ${response.data.firstName} ${response.data.lastName} was saved.`);
toast.showSuccess(`Patient ${response.data.firstName} ${response.data.lastName} was saved.`);
} catch (err: unknown) {
const message =
err && typeof err === 'object' && 'message' in err
? String((err as { message: unknown }).message)
: 'Failed to save patient.';
setToastError(message);
toast.showError(message);
} finally {
setSavingPatient(false);
}
@@ -165,15 +162,11 @@ export default function AppointmentsPage() {
function handleSlotClick(hour: number, providerUserId: string, providerName: string) {
if (isViewingPastDay) {
setToastSuccess('');
setToastError('');
setToastInfo('Past appointments are view-only.');
toast.showInfo('Past appointments are view-only.');
return;
}
if (!selectedPatient) {
setToastSuccess('');
setToastError('');
setToastInfo('Select a patient before booking.');
toast.showInfo('Select a patient before booking.');
return;
}
setBookingHour(hour);
@@ -185,9 +178,7 @@ export default function AppointmentsPage() {
function handleAppointmentClick(appointment: AppointmentRecord) {
if (isViewingPastDay) {
setToastSuccess('');
setToastError('');
setToastInfo('Past appointments are view-only.');
toast.showInfo('Past appointments are view-only.');
return;
}
const provider = providers.find((p) => p.userId === appointment.providerUserId);
@@ -206,9 +197,7 @@ export default function AppointmentsPage() {
purpose: AppointmentPurpose;
}) {
setSavingAppointment(true);
setToastError('');
setToastSuccess('');
setToastInfo('');
toast.setError('');
try {
if (activeEditingAppointment) {
await appointmentsApi.update(activeEditingAppointment.id, payload);
@@ -217,7 +206,7 @@ export default function AppointmentsPage() {
}
setBookingOpen(false);
setEditingAppointmentId(null);
setToastSuccess(activeEditingAppointment ? 'Appointment updated.' : 'Appointment saved.');
toast.showSuccess(activeEditingAppointment ? 'Appointment updated.' : 'Appointment saved.');
await loadSchedule();
} catch (err: unknown) {
const message =
@@ -226,7 +215,7 @@ export default function AppointmentsPage() {
: activeEditingAppointment
? 'Could not update appointment.'
: 'Could not save appointment.';
setToastError(message);
toast.showError(message);
} finally {
setSavingAppointment(false);
}
@@ -236,57 +225,33 @@ export default function AppointmentsPage() {
if (!window.confirm('Remove this appointment?')) {
return;
}
setToastError('');
setToastSuccess('');
setToastInfo('');
toast.setError('');
try {
await appointmentsApi.remove(id);
setToastSuccess('Appointment removed.');
toast.showSuccess('Appointment removed.');
await loadSchedule();
} catch (err: unknown) {
const message =
err && typeof err === 'object' && 'message' in err
? String((err as { message: unknown }).message)
: 'Could not delete appointment.';
setToastError(message);
toast.showError(message);
}
}
useEffect(() => {
if (!toastSuccess) {
return;
}
const id = setTimeout(() => setToastSuccess(''), 3200);
return () => clearTimeout(id);
}, [toastSuccess]);
useEffect(() => {
if (!toastError) {
return;
}
const id = setTimeout(() => setToastError(''), 4000);
return () => clearTimeout(id);
}, [toastError]);
useEffect(() => {
if (!toastInfo) {
return;
}
const id = setTimeout(() => setToastInfo(''), 4000);
return () => clearTimeout(id);
}, [toastInfo]);
return (
<div className="relative space-y-6 pb-24">
<div className="space-y-6">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-text-primary">Appointments</h1>
<p className="text-sm text-text-secondary">
Search a patient, pick a date, then click a time slot under a provider to book.
</p>
</div>
<ToastStack {...toast.messages} />
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
<div className="xl:col-span-1 space-y-4">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-text-primary">Appointments</h1>
<p className="text-sm text-text-secondary">
Search a patient, pick a date, then click a time slot under a provider to book.
</p>
</div>
<AppointmentsPatientSearch
search={search}
onSearchChange={setSearch}
@@ -361,16 +326,6 @@ export default function AppointmentsPage() {
</div>
)}
{(scheduleError || toastError || toastSuccess || toastInfo) && (
<div className="fixed bottom-4 left-0 right-0 z-[70] px-4 pointer-events-none">
<div className="pointer-events-auto w-full space-y-2">
{scheduleError && <Toast variant="danger">{scheduleError}</Toast>}
{toastError && <Toast variant="danger">{toastError}</Toast>}
{toastInfo && <Toast variant="warning">{toastInfo}</Toast>}
{toastSuccess && <Toast variant="success">{toastSuccess}</Toast>}
</div>
</div>
)}
</div>
);
}