diff --git a/.cursor/rules/appointments.mdc b/.cursor/rules/appointments.mdc index 1a64402..ee9abdf 100644 --- a/.cursor/rules/appointments.mdc +++ b/.cursor/rules/appointments.mdc @@ -9,4 +9,6 @@ alwaysApply: false - List includes `hasTreatment` when a `Treatment` row is linked (`appointmentId`). - **Patient change** blocked while linked → `APPOINTMENT_PATIENT_LOCKED` (UI: `patientLockedHint`). - **Delete** blocked while linked → `APPOINTMENT_HAS_TREATMENT` (hide delete + `deleteBlockedHint`). Empty appointments (no treatment yet) remain deletable. +- **Past days:** new bookings stay blocked. Existing appointments **without** treatment can be edited/deleted; with treatment → toast `infoEditBlockedHasTreatment` (no modal). Banner clicks are not gated by `canBook` (slots still are). +- **Disabled feedback:** use `title` + click `toast.showInfo` with `common.readOnlyAccess` (same key as Staff/Patients) or the domain reason (`infoPastViewOnly`, `infoEditBlockedHasTreatment`). - Prisma: `Treatment.appointmentId` is `onDelete: SetNull` — deleting an appointment does **not** cascade-delete treatments/cases/tasks; do not rely on cascade for cleanup. diff --git a/.cursor/skills/i18n-formatting/SKILL.md b/.cursor/skills/i18n-formatting/SKILL.md index 086c398..59d169a 100644 --- a/.cursor/skills/i18n-formatting/SKILL.md +++ b/.cursor/skills/i18n-formatting/SKILL.md @@ -36,7 +36,7 @@ Use when adding or changing user-visible dates/times/numbers, RTL layout, or loc - `fa`: Jalali display `YYYY/MM/DD` (Persian digits), parse/mask in `persianCalendar.ts` - `en` / `nl`: Gregorian display `YYYY-MM-DD`, parse/mask in `dateInputFormat.ts` - Calendar icon at **`end-3`** (matches select chevron inset); text uses **`text-start`** (logical — right in RTL, left in LTR) -- Popup: **`CalendarDayPartsPanel`** (shared with schedule picker) +- Popup: **`CalendarDayPartsPanel`** (shared with schedule picker); positioned via `useAnchoredPanelPosition` (fixed + viewport clamp / flip) so RTL/LTR edge fields stay on-screen - **Do not** add native `` — one component for all locales **Select chevron** diff --git a/AGENTS.md b/AGENTS.md index 0f24565..0a39188 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ frontend/src/ - **Unread semantics**: Treatment tab badge = count of unread cases **for the user's own treatment plans** (per-case read cursor) and clears when a case is opened/marked read (not on tab visit). - **Lab shipment progress + comments**: shown in **Lab dispatch panel** for the active shipment; expanding activity / opening comments marks that case read. -**Appointments (quick ref):** Do not delete (or change patient) when `hasTreatment`; codes `APPOINTMENT_HAS_TREATMENT` / `APPOINTMENT_PATIENT_LOCKED`. Appointment delete does not cascade-delete treatments. See `.cursor/rules/appointments.mdc`. +**Appointments (quick ref):** Do not delete (or change patient) when `hasTreatment`; codes `APPOINTMENT_HAS_TREATMENT` / `APPOINTMENT_PATIENT_LOCKED`. Past days: no new bookings; edit/delete OK without treatment; with treatment → toast. Appointment delete does not cascade-delete treatments. See `.cursor/rules/appointments.mdc`. **Lab Tasks tab:** Newest case first; steps ordered 1→N; case grouping when sorted by date; `stepCompleted` filter; prosthesis colors from catalog; task assignment in **Cases** (compact row: status + assignee + last update); on **Tasks**, all staff see every task but only assignee (or unassigned pool) can change status — others see “Assigned to {name}” instead of the status dropdown; **case due dates** set/edited in clinic Treatment lab dispatch, shown on lab Cases/Tasks with overdue filter + sort; **mobile:** larger task status controls, sticky case header when grouped; **tab badges:** `LabCaseActivity` + `GET /notifications/tab-counts` (lab Cases/Tasks split, clinic Treatment) — see `.cursor/skills/lab-tasks/SKILL.md` and `.cursor/skills/lab-notifications/SKILL.md`. diff --git a/frontend/messages/en.json b/frontend/messages/en.json index b57d8fc..b858ad8 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -573,6 +573,7 @@ "subtitle": "Search a patient, pick a date, then click a time slot under a provider to book.", "loadingSchedule": "Loading schedule…", "infoPastViewOnly": "Past appointments are view-only.", + "infoEditBlockedHasTreatment": "This appointment cannot be edited because a treatment is linked to it.", "infoSelectPatient": "Select a patient before booking.", "errorOutsideHours": "This appointment falls outside the provider's current working hours and cannot be edited.", "successUpdated": "Appointment updated.", diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index ec8ad15..ef983fa 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -574,6 +574,7 @@ "subtitle": "یک بیمار را جستجو کنید، تاریخ را انتخاب کنید، سپس روی یک زمان در زیر ارائه‌دهنده کلیک کنید تا رزرو کنید.", "loadingSchedule": "در حال بارگذاری برنامه...", "infoPastViewOnly": "نوبت‌های گذشته فقط قابل مشاهده هستند.", + "infoEditBlockedHasTreatment": "این نوبت قابل ویرایش نیست چون یک درمان به آن متصل است.", "infoSelectPatient": "قبل از رزرو، یک بیمار را انتخاب کنید.", "errorOutsideHours": "این نوبت خارج از ساعات کاری فعلی ارائه‌دهنده است و قابل ویرایش نیست.", "successUpdated": "نوبت به‌روزرسانی شد.", diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index b7f32f1..6f23238 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -573,6 +573,7 @@ "subtitle": "Zoek een patiënt, kies een datum en klik vervolgens op een tijdslot onder een aanbieder om te boeken.", "loadingSchedule": "Rooster laden...", "infoPastViewOnly": "Afspraken uit het verleden zijn alleen-lezen.", + "infoEditBlockedHasTreatment": "Deze afspraak kan niet worden bewerkt omdat er een behandeling aan gekoppeld is.", "infoSelectPatient": "Selecteer een patiënt voor het boeken.", "errorOutsideHours": "Deze afspraak valt buiten de huidige werktijden van de aanbieder en kan niet worden bewerkt.", "successUpdated": "Afspraak bijgewerkt.", diff --git a/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx b/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx index b576174..9e2abb3 100644 --- a/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx +++ b/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx @@ -203,13 +203,16 @@ export function AppointmentBookingModal({ } const now = new Date(); - if (isSameLocalCalendarDay(scheduleDate, now) && startAt.getTime() < now.getTime()) { + if ( + !editingAppointment && + isSameLocalCalendarDay(scheduleDate, now) && + startAt.getTime() < now.getTime() + ) { setError(t('errorPastSchedule')); return; } - const today = new Date(); - if (compareLocalDayStart(scheduleDate, today) < 0) { + if (!editingAppointment && compareLocalDayStart(scheduleDate, now) < 0) { setError(t('errorPastViewOnly')); return; } diff --git a/frontend/src/components/ui/appointments/AppointmentScheduleGrid.tsx b/frontend/src/components/ui/appointments/AppointmentScheduleGrid.tsx index bbbae55..76a61a7 100644 --- a/frontend/src/components/ui/appointments/AppointmentScheduleGrid.tsx +++ b/frontend/src/components/ui/appointments/AppointmentScheduleGrid.tsx @@ -81,6 +81,10 @@ interface AppointmentScheduleGridProps { appointments: AppointmentRecord[]; treatmentCatalog: TreatmentCatalogEntry[]; canBook: boolean; + /** Same message used for title + click toast when booking is blocked. */ + slotBlockReason?: string | null; + /** Same message used for title + click toast when editing an appointment is blocked. */ + appointmentEditBlockReason?: (appointment: AppointmentRecord) => string | null; onSlotClick: (startMinute: number, providerUserId: string, providerName: string) => void; onAppointmentClick?: (appointment: AppointmentRecord) => void; onAppointmentOutsideHours?: (appointment: AppointmentRecord) => void; @@ -92,6 +96,8 @@ export function AppointmentScheduleGrid({ appointments, treatmentCatalog, canBook, + slotBlockReason = null, + appointmentEditBlockReason, onSlotClick, onAppointmentClick, onAppointmentOutsideHours, @@ -161,9 +167,6 @@ export function AppointmentScheduleGrid({ }); return; } - if (!canBook) { - return; - } onAppointmentClick?.(apt); } @@ -216,7 +219,7 @@ export function AppointmentScheduleGrid({ return (
{formatMinuteLabel(hour * 60, locale)} @@ -247,13 +250,15 @@ export function AppointmentScheduleGrid({ SCHEDULE_SLOT_MINUTES, p.dayBlocks, ); - const slotDisabled = !canBook || columnFullyDisabled || !slotActive; + const structuralDisabled = columnFullyDisabled || !slotActive; + const bookingBlocked = Boolean(slotBlockReason) || !canBook; + const slotLooksDisabled = structuralDisabled || bookingBlocked; return (