From 67137c722d7fb0df803ad068d50185955870bbef Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Fri, 5 Jun 2026 01:28:32 +0300 Subject: [PATCH] Polish footer menu, Notes spacing, and Appointments add flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - User menu: rename "Quick nav" to "Search"; add spacing to the clinic submenu so it no longer touches the parent menu (sideOffset). - Notes: lay the list, title, toolbar, and writing area out as separate rounded panels with gaps so they're no longer stuck together. - Sidebar: shorten the "Appointments & Schedule" label to "Appointments". - Appointments: replace the oversized patient form with a compact "New appointment" dialog — pick a patient via quick search by name/file number, set time/type/provider; the entry is added to today's schedule. Co-Authored-By: Claude Opus 4.8 --- .../appointments/add-appointment-dialog.tsx | 252 ++++++++++++++++++ .../appointments/appointments-view.tsx | 30 ++- frontend/components/notes/notes-editor.tsx | 4 +- frontend/components/notes/notes-view.tsx | 58 ++-- frontend/components/sidebar-02/nav-user.tsx | 4 +- frontend/lib/i18n/locales/en/translation.json | 2 +- 6 files changed, 304 insertions(+), 46 deletions(-) create mode 100644 frontend/components/appointments/add-appointment-dialog.tsx diff --git a/frontend/components/appointments/add-appointment-dialog.tsx b/frontend/components/appointments/add-appointment-dialog.tsx new file mode 100644 index 0000000..7bce241 --- /dev/null +++ b/frontend/components/appointments/add-appointment-dialog.tsx @@ -0,0 +1,252 @@ +"use client"; + +import { Search } from "lucide-react"; +import { type FormEvent, type ReactNode, useEffect, useMemo, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogDescription, + DialogFooter, + DialogHeader, + DialogPanel, + DialogPopup, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { listPatients, type Patient } from "@/lib/patients"; +import { notify } from "@/lib/toast"; + +export type NewAppointment = { + fileNumber: string; + name: string; + initials: string; + time: string; + type: string; + provider: string; +}; + +const TYPES = [ + "Follow-up", + "New patient", + "Consultation", + "Lab review", + "Vaccination", +]; + +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"; + +function Field({ label, children }: { label: string; children: ReactNode }) { + return ( + + ); +} + +// Compact "New appointment" dialog. The patient is chosen via a quick search by +// name or file number; the rest is the slot. Appointments are mock-only, so a +// confirmed entry is handed back to the page via onAdd. +export function AddAppointmentDialog({ + open, + onOpenChange, + onAdd, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + onAdd: (appt: NewAppointment) => void; +}) { + const [patients, setPatients] = useState([]); + const [query, setQuery] = useState(""); + const [selected, setSelected] = useState(null); + const [time, setTime] = useState("09:00"); + const [type, setType] = useState(TYPES[0]); + const [provider, setProvider] = useState(""); + + // Load patients lazily when the dialog opens (for the quick search). + useEffect(() => { + if (!open) return; + let active = true; + listPatients() + .then((data) => { + if (active) setPatients(data); + }) + .catch(() => { + /* search just stays empty */ + }); + return () => { + active = false; + }; + }, [open]); + + const matches = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return []; + return patients + .filter( + (p) => + p.name.toLowerCase().includes(q) || p.fileNumber.includes(q), + ) + .slice(0, 6); + }, [patients, query]); + + const reset = () => { + setQuery(""); + setSelected(null); + setTime("09:00"); + setType(TYPES[0]); + setProvider(""); + }; + + const submit = (event: FormEvent) => { + event.preventDefault(); + if (!selected) { + notify.error("Pick a patient", "Search and select a patient first."); + return; + } + onAdd({ + fileNumber: selected.fileNumber, + name: selected.name, + initials: selected.initials, + time, + type, + provider: provider.trim() || selected.pcp, + }); + notify.success("Appointment added", `${selected.name} at ${time}`); + reset(); + onOpenChange(false); + }; + + return ( + { + onOpenChange(o); + if (!o) reset(); + }} + open={open} + > + + + New appointment + + Search for a patient by name or file number, then set the slot. + + + +
+ + + {selected ? ( +
+
+ + {selected.name} + + + File #{selected.fileNumber} + +
+ +
+ ) : ( +
+
+ + setQuery(event.target.value)} + placeholder="Search name or file number" + value={query} + /> +
+ {query.trim() && ( +
+ {matches.length === 0 ? ( +

+ No patients found. +

+ ) : ( + matches.map((p) => ( + + )) + )} +
+ )} +
+ )} +
+ +
+ + setTime(event.target.value)} + type="time" + value={time} + /> + + + + +
+ + + setProvider(event.target.value)} + placeholder="e.g. Dr. Okafor" + value={provider} + /> + +
+ + + }> + Cancel + + + +
+
+
+ ); +} diff --git a/frontend/components/appointments/appointments-view.tsx b/frontend/components/appointments/appointments-view.tsx index 9215bf8..f71e6ba 100644 --- a/frontend/components/appointments/appointments-view.tsx +++ b/frontend/components/appointments/appointments-view.tsx @@ -3,7 +3,10 @@ import { CalendarClock, Clock, Plus, Stethoscope, Users } from "lucide-react"; import { type ReactNode, useState } from "react"; -import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; +import { + AddAppointmentDialog, + type NewAppointment, +} from "@/components/appointments/add-appointment-dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -202,8 +205,16 @@ function Section({ export function AppointmentsView() { const [addOpen, setAddOpen] = useState(false); - // Bumped on open so the create dialog remounts with a fresh file # / form. - const [addKey, setAddKey] = useState(0); + const [schedule, setSchedule] = useState(today); + + // Insert a new (mock) appointment into today's schedule, kept time-sorted. + const addAppointment = (appt: NewAppointment) => { + setSchedule((prev) => + [...prev, { ...appt, status: "confirmed" as const }].sort((a, b) => + a.time.localeCompare(b.time), + ), + ); + }; return (
@@ -218,10 +229,7 @@ export function AppointmentsView() {
- - +
+ + + + + + No note selected + + Select a note from the list, or create a new one to start + writing. + + + + + + +
)} diff --git a/frontend/components/sidebar-02/nav-user.tsx b/frontend/components/sidebar-02/nav-user.tsx index d39d985..c1208dd 100644 --- a/frontend/components/sidebar-02/nav-user.tsx +++ b/frontend/components/sidebar-02/nav-user.tsx @@ -182,7 +182,7 @@ export function NavUser() { {/* Command palette: hint + shortcut, sits below Theme. */} - Quick nav + Search K @@ -195,7 +195,7 @@ export function NavUser() { {activeName} - +

{activeOrg?.name ?? "No clinic selected"} diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index daa1552..d7e0adc 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -47,7 +47,7 @@ "nav": { "newChat": "New chat", "patients": "Patients", - "appointments": "Appointments & Schedule", + "appointments": "Appointments", "analysis": "Analysis", "notes": "Notes", "settings": "Settings",