From 49c39bd853b70601d8ce42543bbe79b588729742 Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Sat, 6 Jun 2026 18:59:19 +0300 Subject: [PATCH] Tasks/notes/prescriptions/appointments/patients UX fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Task dialog: rename Title→Subject, add a "Details" textarea (notes), widen to sm:max-w-lg. - Tasks & Notes: replace split-panes with full-width lists that open a right-side Sheet on click (TaskDetailSheet, NoteDetailSheet), mirroring the Patients table → detail Sheet pattern. - Prescriptions: rows are now clickable and open PrescriptionDetailSheet with the full Rx details (incl. duration + notes). - New prescription dialog: Duration is a dropdown of presets with an "Other" → custom input, plus a Notes textarea; NewPrescription gains notes and threads duration/notes through to the list. - Appointments: header search filtering by patient/type/provider, grouped by date, with an empty state. - Patients: pressing Enter in the search box opens the top match's record. Co-Authored-By: Claude Opus 4.8 --- .../appointments/appointments-view.tsx | 81 ++++-- .../components/notes/note-detail-sheet.tsx | 55 ++++ frontend/components/notes/notes-view.tsx | 157 ++++++------ .../components/patients/patients-view.tsx | 7 + .../prescriptions/add-prescription-dialog.tsx | 64 ++++- .../prescription-detail-sheet.tsx | 104 ++++++++ .../prescriptions/prescriptions-view.tsx | 43 +++- .../components/tasks/task-detail-sheet.tsx | 102 ++++++++ frontend/components/tasks/tasks-view.tsx | 234 +++++++----------- 9 files changed, 595 insertions(+), 252 deletions(-) create mode 100644 frontend/components/notes/note-detail-sheet.tsx create mode 100644 frontend/components/prescriptions/prescription-detail-sheet.tsx create mode 100644 frontend/components/tasks/task-detail-sheet.tsx diff --git a/frontend/components/appointments/appointments-view.tsx b/frontend/components/appointments/appointments-view.tsx index 2df1555..56ef2ed 100644 --- a/frontend/components/appointments/appointments-view.tsx +++ b/frontend/components/appointments/appointments-view.tsx @@ -5,6 +5,7 @@ import { CalendarDays, Clock, Plus, + Search, Stethoscope, Users, } from "lucide-react"; @@ -19,6 +20,7 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; // All figures here are mock/placeholder data — there is no scheduling backend. // They illustrate the Appointments & Schedule layout. @@ -261,6 +263,7 @@ export function AppointmentsView() { const [addOpen, setAddOpen] = useState(false); const [calendarOpen, setCalendarOpen] = useState(false); const [appointments, setAppointments] = useState(seed); + const [query, setQuery] = useState(""); // Insert a new (mock) appointment at the date/time chosen in the dialog. const addAppointment = (appt: NewAppointment) => { @@ -294,6 +297,25 @@ export function AppointmentsView() { })); }, [appointments]); + const search = query.trim().toLowerCase(); + + // While searching, match name/type/provider across every date and group the + // hits by date (soonest first) so each section keeps its day header. + const results = useMemo(() => { + if (!search) return []; + const hits = appointments.filter( + (a) => + a.name.toLowerCase().includes(search) || + a.type.toLowerCase().includes(search) || + a.provider.toLowerCase().includes(search), + ); + const keys = [...new Set(hits.map((a) => a.date))].sort(); + return keys.map((key) => ({ + key, + items: hits.filter((a) => a.date === key).sort(byTime), + })); + }, [appointments, search]); + return (
@@ -306,6 +328,15 @@ export function AppointmentsView() {

+
+ + setQuery(event.target.value)} + placeholder="Search patient, type, provider" + value={query} + /> +
-
- {kpis.map((k) => ( - - ))} -
- -
- {todayItems.length > 0 ? ( - + {search ? ( + results.length > 0 ? ( + results.map((group) => ( +
+ +
+ )) ) : (

- Nothing scheduled today. + No matching appointments.

- )} -
+ ) + ) : ( + <> +
+ {kpis.map((k) => ( + + ))} +
- {upcoming.map((group) => ( -
- -
- ))} +
+ {todayItems.length > 0 ? ( + + ) : ( +

+ Nothing scheduled today. +

+ )} +
+ + {upcoming.map((group) => ( +
+ +
+ ))} + + )} void; + saving: boolean; + onSave: (data: { title: string; content: string }) => void; + onDelete?: () => void; +}) { + return ( + + + + {note?.id ? "Edit note" : "New note"} + + {/* Plain flex container (not SheetPanel) so the editor gets a bounded + height and scrolls internally rather than nesting two scroll areas. */} +
+ {note && ( + + )} +
+
+
+ ); +} diff --git a/frontend/components/notes/notes-view.tsx b/frontend/components/notes/notes-view.tsx index 0b59df7..e7a6ef0 100644 --- a/frontend/components/notes/notes-view.tsx +++ b/frontend/components/notes/notes-view.tsx @@ -3,7 +3,7 @@ import { NotebookPen, Plus } from "lucide-react"; import { useEffect, useState } from "react"; -import { NotesEditor } from "@/components/notes/notes-editor"; +import { NoteDetailSheet } from "@/components/notes/note-detail-sheet"; import { Button } from "@/components/ui/button"; import { Empty, @@ -21,7 +21,6 @@ import { updateNote, } from "@/lib/notes"; import { notify } from "@/lib/toast"; -import { cn } from "@/lib/utils"; const newDraft = (): Note => ({ id: "", @@ -33,8 +32,9 @@ const newDraft = (): Note => ({ export function NotesView() { const [notes, setNotes] = useState([]); - // No auto-selection: with nothing chosen the right pane shows the Empty state. + // The note shown in the editor Sheet; null when the Sheet is closed. const [selected, setSelected] = useState(null); + const [sheetOpen, setSheetOpen] = useState(false); const [draftKey, setDraftKey] = useState(0); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -64,6 +64,12 @@ export function NotesView() { const startNew = () => { setSelected(newDraft()); setDraftKey((k) => k + 1); + setSheetOpen(true); + }; + + const openNote = (note: Note) => { + setSelected(note); + setSheetOpen(true); }; const save = async (data: { title: string; content: string }) => { @@ -92,6 +98,7 @@ export function NotesView() { const list = await listNotes(); setNotes(list); setSelected(null); + setSheetOpen(false); notify.success("Note deleted"); } catch (err) { notify.error( @@ -102,84 +109,76 @@ export function NotesView() { }; return ( -
- {/* Left: note list */} - - - {/* Right: editor or empty state */} -
- {selected ? ( - remove(selected.id) : undefined} - onSave={save} - saving={saving} - /> - ) : ( -
- - - - - - No note selected - - Select a note from the list, or create a new one to start - writing. - - - - - - -
- )} +
+ + {loading ? ( +
+ Loading… +
+ ) : notes.length === 0 ? ( +
+ + + + + + No notes yet + + Create a note to start writing. + + + + + + +
+ ) : ( +
+ {notes.map((n) => ( + + ))} +
+ )} + + remove(selected.id) : undefined} + onOpenChange={(o) => { + setSheetOpen(o); + if (!o) setSelected(null); + }} + onSave={save} + open={sheetOpen} + saving={saving} + />
); } diff --git a/frontend/components/patients/patients-view.tsx b/frontend/components/patients/patients-view.tsx index a4f0527..5384476 100644 --- a/frontend/components/patients/patients-view.tsx +++ b/frontend/components/patients/patients-view.tsx @@ -83,6 +83,13 @@ export function PatientsView() { setQuery(event.target.value)} + onKeyDown={(event) => { + // Enter opens the top match's record, like picking it from the table. + if (event.key === "Enter" && patients.length > 0) { + event.preventDefault(); + open(patients[0].fileNumber); + } + }} placeholder="Search name or MRN" value={query} /> diff --git a/frontend/components/prescriptions/add-prescription-dialog.tsx b/frontend/components/prescriptions/add-prescription-dialog.tsx index ed8dadd..f7f6e0a 100644 --- a/frontend/components/prescriptions/add-prescription-dialog.tsx +++ b/frontend/components/prescriptions/add-prescription-dialog.tsx @@ -16,6 +16,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { listPatients, type Patient } from "@/lib/patients"; import { notify } from "@/lib/toast"; @@ -27,6 +28,7 @@ export type NewPrescription = { dose: string; frequency: string; duration: string; + notes: string; }; const FREQUENCIES = [ @@ -37,6 +39,20 @@ const FREQUENCIES = [ "As needed", ]; +// Preset courses for the Duration dropdown; "Other" reveals a free-text input. +const DURATIONS = [ + "3 days", + "5 days", + "7 days", + "10 days", + "14 days", + "1 month", + "3 months", + "Ongoing", + "Other", +]; +const OTHER_DURATION = "Other"; + 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"; @@ -112,7 +128,9 @@ export function AddPrescriptionDialog({ const [medication, setMedication] = useState(""); const [dose, setDose] = useState(""); const [frequency, setFrequency] = useState(FREQUENCIES[0]); - const [duration, setDuration] = useState(""); + const [durationChoice, setDurationChoice] = useState(DURATIONS[0]); + const [durationCustom, setDurationCustom] = useState(""); + const [notes, setNotes] = useState(""); // Load patients lazily when the dialog opens (for the quick search). useEffect(() => { @@ -151,7 +169,9 @@ export function AddPrescriptionDialog({ setMedication(""); setDose(""); setFrequency(FREQUENCIES[0]); - setDuration(""); + setDurationChoice(DURATIONS[0]); + setDurationCustom(""); + setNotes(""); }; const submit = (event: FormEvent) => { @@ -164,6 +184,12 @@ export function AddPrescriptionDialog({ notify.error("Add a medication", "Enter the medication name."); return; } + const duration = + durationChoice === OTHER_DURATION ? durationCustom.trim() : durationChoice; + if (durationChoice === OTHER_DURATION && !duration) { + notify.error("Add a duration", "Enter the custom duration."); + return; + } onAdd({ fileNumber: selected.fileNumber, name: selected.name, @@ -171,7 +197,8 @@ export function AddPrescriptionDialog({ medication: medication.trim(), dose: dose.trim(), frequency, - duration: duration.trim(), + duration, + notes: notes.trim(), }); notify.success("Prescription added", `${medication.trim()} for ${selected.name}`); reset(); @@ -295,10 +322,33 @@ export function AddPrescriptionDialog({ - setDuration(event.target.value)} - placeholder="e.g. 7 days" - value={duration} + + {durationChoice === OTHER_DURATION && ( + setDurationCustom(event.target.value)} + placeholder="e.g. 21 days" + value={durationCustom} + /> + )} + + + +