frontend: block past dates in appointment/invoice pickers; drop stale Settings Features

- Appointment date picker disables days before today.
- Invoice issue-date picker disables past days by default, with an opt-in
  "Back-date" checkbox for recording older invoices (edit mode keeps past
  dates). Added invoices.dialog.backdate to all locales.
- Removed the inert, outdated "Features" section (patient-owned storage /
  require signed toggles) from Settings and its now-unused i18n keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-09 20:41:58 +03:00
parent 143ffc39f1
commit 35f07c508d
35 changed files with 4028 additions and 157 deletions
@@ -51,6 +51,14 @@ const TYPES = [
const controlClass =
"h-9 w-full rounded-3xl border border-transparent bg-input/50 px-3 text-sm text-foreground outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30";
// Local start-of-day so the calendar can disable days strictly before today
// (appointments can't be scheduled in the past).
const startOfToday = () => {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d;
};
function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<label className="flex flex-col gap-1.5">
@@ -251,6 +259,7 @@ export function AddAppointmentDialog({
/>
<PopoverPopup>
<Calendar
disabled={{ before: startOfToday() }}
mode="single"
onSelect={(d) => {
if (d) {
@@ -59,12 +59,23 @@ function Field({ label, children }: { label: string; children: ReactNode }) {
);
}
// Local start-of-day, used to disable days strictly before today.
const startOfToday = () => {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d;
};
function DatePicker({
value,
onChange,
allowPast = true,
}: {
value: Date;
onChange: (d: Date) => void;
// When false, days before today are disabled (used for the issue date unless
// the clinician opts into back-dating an older invoice).
allowPast?: boolean;
}) {
const [open, setOpen] = useState(false);
return (
@@ -87,6 +98,7 @@ function DatePicker({
/>
<PopoverPopup>
<Calendar
disabled={allowPast ? undefined : { before: startOfToday() }}
mode="single"
onSelect={(d) => {
if (d) {
@@ -131,6 +143,9 @@ export function InvoiceFormDialog({
: null;
const [issuedAt, setIssuedAt] = useState<Date>(() => new Date());
// Off by default: the issue date can't be back-dated unless the clinician
// opts in (for recording an older, pre-existing invoice).
const [allowBackdate, setAllowBackdate] = useState(false);
const [hasDue, setHasDue] = useState(false);
const [dueAt, setDueAt] = useState<Date>(() => new Date());
const [status, setStatus] = useState<InvoiceStatus>("draft");
@@ -143,6 +158,8 @@ export function InvoiceFormDialog({
if (!open) return;
if (mode === "edit" && invoice) {
setIssuedAt(new Date(`${invoice.issuedAt}T00:00:00`));
// Existing invoices legitimately carry past issue dates.
setAllowBackdate(true);
setHasDue(Boolean(invoice.dueAt));
setDueAt(new Date(`${invoice.dueAt ?? invoice.issuedAt}T00:00:00`));
setStatus(invoice.status);
@@ -153,6 +170,7 @@ export function InvoiceFormDialog({
} else {
setSelected(null);
setIssuedAt(new Date());
setAllowBackdate(false);
setHasDue(false);
setDueAt(new Date());
setStatus("draft");
@@ -310,9 +328,24 @@ export function InvoiceFormDialog({
</Field>
<div className="grid grid-cols-2 gap-3">
<Field label={t("invoices.dialog.issued")}>
<DatePicker onChange={setIssuedAt} value={issuedAt} />
</Field>
<div className="flex flex-col gap-1.5">
<span className="flex items-center justify-between gap-2 text-muted-foreground text-xs">
{t("invoices.dialog.issued")}
<label className="flex items-center gap-1 text-[11px]">
<input
checked={allowBackdate}
onChange={(e) => setAllowBackdate(e.target.checked)}
type="checkbox"
/>
{t("invoices.dialog.backdate")}
</label>
</span>
<DatePicker
allowPast={allowBackdate}
onChange={setIssuedAt}
value={issuedAt}
/>
</div>
<div className="flex flex-col gap-1.5">
<span className="flex items-center justify-between text-muted-foreground text-xs">
{t("invoices.dialog.due")}
@@ -293,23 +293,6 @@ export function ProfilePanel() {
</div>
</SettingsSection>
{/* Future features — intentionally not wired to persistence yet. */}
<SettingsSection
description={t("settings.profile.featuresDescription")}
title={t("settings.profile.features")}
>
<div className="space-y-3">
<ToggleRow
description={t("settings.profile.notif.patientStorageDesc")}
title={t("settings.profile.notif.patientStorage")}
/>
<ToggleRow
description={t("settings.profile.notif.requireSignedDesc")}
title={t("settings.profile.notif.requireSigned")}
/>
</div>
</SettingsSection>
<SettingsSection
description={t("settings.profile.dangerZoneDescription")}
title={t("settings.profile.dangerZone")}
@@ -482,6 +482,7 @@
"fileNumber": "ملف رقم {{number}}",
"change": "تغيير",
"issued": "تاريخ الإصدار",
"backdate": "تأريخ رجعي",
"due": "تاريخ الاستحقاق",
"status": "الحالة",
"notes": "ملاحظات",
@@ -1846,8 +1847,6 @@
"patientNotificationsDescription": "رسائل تُرسل إلى المرضى بشأن سجلاتهم ونتائجهم والموافقات المعلّقة",
"accountNotifications": "إشعارات الحساب",
"accountNotificationsDescription": "إشعارات تُرسل إليك بشأن مرضاك وفريق الرعاية",
"features": "الميزات",
"featuresDescription": "أدر ميزات alpha و beta لحسابك",
"dangerZone": "منطقة الخطر",
"dangerZoneDescription": "إجراءات لا رجعة فيها لحسابك",
"deleteAccount": "حذف الحساب",
@@ -1886,11 +1885,7 @@
"pendingApprovals": "الموافقات المعلّقة",
"pendingApprovalsDesc": "أبلغني عند موافقة مريض على تغيير معلّق أو رفضه",
"recordsShared": "سجلات شُوركت معي",
"recordsSharedDesc": "أبلغني عند مشاركة مريض سجلًا معي",
"patientStorage": "تخزين مملوك للمريض (beta)",
"patientStorageDesc": "اكتب السجلات على جهاز المريض الخاص بدلًا من قاعدة بياناتك",
"requireSigned": "طلب سجلات موقّعة",
"requireSignedDesc": "طلب توقيع على كل تغيير تجريه على سجل مريض"
"recordsSharedDesc": "أبلغني عند مشاركة مريض سجلًا معي"
}
},
"careTeam": {
@@ -466,6 +466,7 @@
"fileNumber": "Aktennr. {{number}}",
"change": "Ändern",
"issued": "Ausgestellt",
"backdate": "Rückdatieren",
"due": "Fälligkeitsdatum",
"status": "Status",
"notes": "Notizen",
@@ -1826,8 +1827,6 @@
"patientNotificationsDescription": "E-Mails an Patienten über ihre Datensätze, Ergebnisse und ausstehenden Genehmigungen",
"accountNotifications": "Kontobenachrichtigungen",
"accountNotificationsDescription": "Benachrichtigungen an Sie über Ihre Patienten und das Behandlungsteam",
"features": "Funktionen",
"featuresDescription": "Verwalten Sie Alpha- und Beta-Funktionen für Ihr Konto",
"dangerZone": "Gefahrenzone",
"dangerZoneDescription": "Unumkehrbare Aktionen für Ihr Konto",
"deleteAccount": "Konto löschen",
@@ -1866,11 +1865,7 @@
"pendingApprovals": "Ausstehende Genehmigungen",
"pendingApprovalsDesc": "Benachrichtigen Sie mich, wenn ein Patient eine ausstehende Änderung genehmigt oder ablehnt",
"recordsShared": "Mit mir geteilte Datensätze",
"recordsSharedDesc": "Benachrichtigen Sie mich, wenn ein Patient einen Datensatz mit mir teilt",
"patientStorage": "Patienteneigener Speicher (Beta)",
"patientStorageDesc": "Schreiben Sie Datensätze auf das eigene Gerät des Patienten statt in Ihre Datenbank",
"requireSigned": "Signierte Datensätze erforderlich",
"requireSignedDesc": "Erfordern Sie eine Signatur für jede Änderung, die Sie an einer Patientenakte vornehmen"
"recordsSharedDesc": "Benachrichtigen Sie mich, wenn ein Patient einen Datensatz mit mir teilt"
}
},
"careTeam": {
@@ -466,6 +466,7 @@
"fileNumber": "File #{{number}}",
"change": "Change",
"issued": "Issued",
"backdate": "Back-date",
"due": "Due date",
"status": "Status",
"notes": "Notes",
@@ -1826,8 +1827,6 @@
"patientNotificationsDescription": "Emails sent to patients about their records, results, and pending approvals",
"accountNotifications": "Account notifications",
"accountNotificationsDescription": "Notifications sent to you about your patients and the care team",
"features": "Features",
"featuresDescription": "Manage alpha & beta features for your account",
"dangerZone": "Danger Zone",
"dangerZoneDescription": "Irreversible actions for your account",
"deleteAccount": "Delete account",
@@ -1866,11 +1865,7 @@
"pendingApprovals": "Pending approvals",
"pendingApprovalsDesc": "Notify me when a patient approves or rejects a pending change",
"recordsShared": "Records shared with me",
"recordsSharedDesc": "Notify me when a patient shares a record with me",
"patientStorage": "Patient-owned storage (beta)",
"patientStorageDesc": "Write records to the patient's own device instead of your database",
"requireSigned": "Require signed records",
"requireSignedDesc": "Require a signature on every change you make to a patient record"
"recordsSharedDesc": "Notify me when a patient shares a record with me"
}
},
"careTeam": {
@@ -466,6 +466,7 @@
"fileNumber": "Dossier n° {{number}}",
"change": "Changer",
"issued": "Émise",
"backdate": "Antidater",
"due": "Date d'échéance",
"status": "Statut",
"notes": "Notes",
@@ -1826,8 +1827,6 @@
"patientNotificationsDescription": "E-mails envoyés aux patients à propos de leurs dossiers, résultats et approbations en attente",
"accountNotifications": "Notifications du compte",
"accountNotificationsDescription": "Notifications qui vous sont envoyées à propos de vos patients et de l'équipe soignante",
"features": "Fonctionnalités",
"featuresDescription": "Gérez les fonctionnalités alpha & bêta de votre compte",
"dangerZone": "Zone de danger",
"dangerZoneDescription": "Actions irréversibles pour votre compte",
"deleteAccount": "Supprimer le compte",
@@ -1866,11 +1865,7 @@
"pendingApprovals": "Approbations en attente",
"pendingApprovalsDesc": "Me notifier lorsqu'un patient approuve ou rejette une modification en attente",
"recordsShared": "Dossiers partagés avec moi",
"recordsSharedDesc": "Me notifier lorsqu'un patient partage un dossier avec moi",
"patientStorage": "Stockage appartenant au patient (bêta)",
"patientStorageDesc": "Écrivez les dossiers sur l'appareil du patient plutôt que dans votre base de données",
"requireSigned": "Exiger des dossiers signés",
"requireSignedDesc": "Exiger une signature sur chaque modification que vous apportez à un dossier patient"
"recordsSharedDesc": "Me notifier lorsqu'un patient partage un dossier avec moi"
}
},
"careTeam": {
@@ -466,6 +466,7 @@
"fileNumber": "Faylka #{{number}}",
"change": "Beddel",
"issued": "La bixiyay",
"backdate": "Taariikh hore",
"due": "Taariikhda la sugayo",
"status": "Xaalad",
"notes": "Qoraallo",
@@ -1826,8 +1827,6 @@
"patientNotificationsDescription": "Iimaylada loo diro bukaannada oo ku saabsan diiwaannadooda, natiijooyinka, iyo ansixinta la sugayo",
"accountNotifications": "Ogeysiisyada akoonka",
"accountNotificationsDescription": "Ogeysiisyada laguu soo diro oo ku saabsan bukaannadaada iyo kooxda daryeelka",
"features": "Astaamaha",
"featuresDescription": "Maaree astaamaha alpha & beta ee akoonkaaga",
"dangerZone": "Aagga khatarta",
"dangerZoneDescription": "Ficillo aan dib loo celin karin oo akoonkaaga",
"deleteAccount": "Tirtir akoonka",
@@ -1866,11 +1865,7 @@
"pendingApprovals": "Ansixinta la sugayo",
"pendingApprovalsDesc": "I ogeysii marka bukaan ansixiyo ama diido isbeddel la sugayo",
"recordsShared": "Diiwaanno la ila wadaagay",
"recordsSharedDesc": "I ogeysii marka bukaan diiwaan ila wadaago",
"patientStorage": "Kaydka bukaanka (beta)",
"patientStorageDesc": "Ku qor diiwaannada qalabka gaarka ah ee bukaanka halkii xogtaada",
"requireSigned": "U baahan diiwaanno la saxiixay",
"requireSignedDesc": "U baahan saxiix isbeddel kasta oo aad ku sameyso diiwaanka bukaanka"
"recordsSharedDesc": "I ogeysii marka bukaan diiwaan ila wadaago"
}
},
"careTeam": {