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 (