From dec150c77da2f419474f6dea061a862b044f1632 Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Mon, 1 Jun 2026 21:55:33 +0300 Subject: [PATCH] Patients page, clinical Settings re-theme, Polar-style user footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Patients: new /patients directory table (name, MRN, age·sex, status, last seen, allergies) with search + Add patient; clicking a row opens the record in the chat via /?patient=<#>. chat-panel reads that search param (Suspense-wrapped) and runs the lookup once on arrival. Sidebar "Patients" now links to /patients; lib/patients gains listPatients(). - Settings: re-themed to clinical tabs (Profile · Records · Signing · Care team · Developers) — clinician profile, patient notifications, patient-owned-storage/signed-records features, and a Signing key panel. Same layout/components, only content changed. - Sidebar footer: Polar-style NavUser (avatar + name + chevron) opening a menu upward — Settings · Docs & GitHub · Theme · Log out. Collapsed ("locked"): avatar only, name on hover (tooltip), menu opens to the right. Placeholder identity (no auth yet). Co-Authored-By: Claude Opus 4.8 --- frontend/app/(app)/page.tsx | 7 +- frontend/app/(app)/patients/page.tsx | 10 ++ frontend/components/chat/chat-panel.tsx | 15 +- .../components/patients/patients-view.tsx | 136 ++++++++++++++++++ .../components/settings/settings-billing.tsx | 46 +++--- .../settings/settings-preferences.tsx | 120 +++++++--------- .../components/settings/settings-view.tsx | 38 ++--- .../components/sidebar-02/app-sidebar.tsx | 10 +- frontend/components/sidebar-02/nav-user.tsx | 116 +++++++++++++++ frontend/lib/patients.ts | 5 + 10 files changed, 379 insertions(+), 124 deletions(-) create mode 100644 frontend/app/(app)/patients/page.tsx create mode 100644 frontend/components/patients/patients-view.tsx create mode 100644 frontend/components/sidebar-02/nav-user.tsx diff --git a/frontend/app/(app)/page.tsx b/frontend/app/(app)/page.tsx index 3169314..7b145a2 100644 --- a/frontend/app/(app)/page.tsx +++ b/frontend/app/(app)/page.tsx @@ -1,10 +1,15 @@ +import { Suspense } from "react"; + import { SidebarInset } from "@/components/ui/sidebar"; import { ChatPanel } from "@/components/chat/chat-panel"; export default function Home() { return ( - + {/* ChatPanel reads the `?patient=` search param, so it needs a Suspense boundary. */} + + + ); } diff --git a/frontend/app/(app)/patients/page.tsx b/frontend/app/(app)/patients/page.tsx new file mode 100644 index 0000000..cee76a5 --- /dev/null +++ b/frontend/app/(app)/patients/page.tsx @@ -0,0 +1,10 @@ +import { SidebarInset } from "@/components/ui/sidebar"; +import { PatientsView } from "@/components/patients/patients-view"; + +export default function PatientsPage() { + return ( + + + + ); +} diff --git a/frontend/components/chat/chat-panel.tsx b/frontend/components/chat/chat-panel.tsx index bae5674..d9b1328 100644 --- a/frontend/components/chat/chat-panel.tsx +++ b/frontend/components/chat/chat-panel.tsx @@ -2,7 +2,8 @@ import { nanoid } from "nanoid"; import type { ChatStatus } from "ai"; -import { useCallback, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Conversation, @@ -106,6 +107,18 @@ export function ChatPanel() { const handleStop = useCallback(() => setStatus("ready"), []); + // Opening a patient from the Patients page lands here as `/?patient=`; + // run the lookup once on arrival. + const searchParams = useSearchParams(); + const requestedPatient = searchParams.get("patient"); + const handledPatientRef = useRef(null); + useEffect(() => { + if (requestedPatient && handledPatientRef.current !== requestedPatient) { + handledPatientRef.current = requestedPatient; + send(`/patient ${requestedPatient}`); + } + }, [requestedPatient, send]); + const promptInput = ( ); diff --git a/frontend/components/patients/patients-view.tsx b/frontend/components/patients/patients-view.tsx new file mode 100644 index 0000000..1419dd4 --- /dev/null +++ b/frontend/components/patients/patients-view.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { Plus, Search } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { listPatients, type Patient } from "@/lib/patients"; + +type BadgeVariant = "secondary" | "destructive" | "outline"; + +const statusVariant: Record = { + active: "secondary", + inpatient: "destructive", + discharged: "outline", +}; + +export function PatientsView() { + const router = useRouter(); + const [query, setQuery] = useState(""); + const [addOpen, setAddOpen] = useState(false); + // Bumped on open so the create dialog remounts with a fresh file # / form. + const [addKey, setAddKey] = useState(0); + + const q = query.trim().toLowerCase(); + const patients = listPatients().filter( + (p) => !q || p.name.toLowerCase().includes(q) || p.fileNumber.includes(q) + ); + + const open = (fileNumber: string) => router.push(`/?patient=${fileNumber}`); + + return ( +
+
+

Patients

+
+
+ + setQuery(event.target.value)} + placeholder="Search name or MRN" + value={query} + /> +
+ +
+
+ +
+ + + + + + + + + + + + + {patients.length === 0 ? ( + + + + ) : ( + patients.map((p) => ( + open(p.fileNumber)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + open(p.fileNumber); + } + }} + role="button" + tabIndex={0} + > + + + + + + + + )) + )} + +
NameMRNAge · SexStatusLast seenAllergies
+ No patients found. +
+ {p.name} + + {p.fileNumber} + + {p.age} · {p.sex} + + + {p.status} + + + {p.encounters[0]?.date ?? "—"} + + {p.allergies.length || "—"} +
+
+ + open(fileNumber)} + onOpenChange={setAddOpen} + open={addOpen} + /> +
+ ); +} diff --git a/frontend/components/settings/settings-billing.tsx b/frontend/components/settings/settings-billing.tsx index 43839bc..ecefbe3 100644 --- a/frontend/components/settings/settings-billing.tsx +++ b/frontend/components/settings/settings-billing.tsx @@ -4,58 +4,52 @@ import { cn } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { + CopyField, SettingsCard, SettingsSection, whiteButton, } from "@/components/settings/settings-parts"; -export function BillingPanel() { +export function SigningPanel() { return ( <>
-

Early Member

+

Signing key

Active

- For our founding community of early members. + Every change you make to a patient record is signed with this key, so patients + can verify it came from you before approving it.

- +
-

Free

-

- 4.00% + $0.40 per transaction -

+

Ed25519

+

Created May 28, 2026

- Add payment method - - } - description="Cards used to pay for your temetro subscription" - title="Payment methods" + description="The public key patients use to verify your signatures" + title="Signing identity" > - -

No payment methods on file

+ +
- Edit - - } - description="Used on invoices for your temetro subscription" - title="Billing address" + description="Changes you've signed that are waiting on the patient's approval" + title="Signed records" > - -

khalid

+ +

No pending signatures

diff --git a/frontend/components/settings/settings-preferences.tsx b/frontend/components/settings/settings-preferences.tsx index ffdde7d..ce094b8 100644 --- a/frontend/components/settings/settings-preferences.tsx +++ b/frontend/components/settings/settings-preferences.tsx @@ -2,7 +2,6 @@ import { ChevronDown, Plus } from "lucide-react"; -import { cn } from "@/lib/utils"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -12,59 +11,54 @@ import { SettingsCard, SettingsSection, ToggleRow, - whiteButton, } from "@/components/settings/settings-parts"; -const customerNotifications = [ +const patientNotifications = [ { - title: "Order confirmation", - description: "Sent when a customer completes a one-time purchase", + title: "New lab result", + description: "Sent when a new lab result is available on a patient's chart", }, { - title: "Subscription confirmation", - description: "Sent when a customer starts a new subscription", + title: "Record updated", + description: "Sent when a patient's record is updated by a member of the care team", }, { - title: "Subscription cycled", - description: "Sent when a subscription automatically renews", + title: "Approval requested", + description: "Sent when a signed change is awaiting the patient's approval", }, { - title: "Trial converted", - description: "Sent when a trial ends and the subscription becomes paid", + title: "Change approved", + description: "Sent when a patient approves a pending change to their record", }, { - title: "Renewal reminder", - description: "Sent 7 days before a subscription with a long billing cycle renews", + title: "New message", + description: "Sent when a patient or another clinician sends a message", }, { - title: "Trial conversion reminder", - description: "Sent before a trial ends and converts to a paid subscription", - }, - { - title: "Subscription updated", - description: "Sent when a customer changes their subscription to a different product", + title: "Visit scheduled", + description: "Sent when an upcoming visit is added to a patient's record", }, ]; -export function PreferencesPanel() { +export function ProfilePanel() { return ( <> - +
- Logo + Avatar K @@ -72,55 +66,55 @@ export function PreferencesPanel() {
- Organization Name - + Display name +
- Country + Specialty
- Website - + Clinic / practice +
- Support Email - + Contact email +
- Social Media + Professional links

- Your personal social media links are used for identity verification. They - will never be shown publicly. + Registry or institutional profiles used to verify your identity. They are + never shown to patients.

- {customerNotifications.map((item) => ( + {patientNotifications.map((item) => (
- -

- You don't have any active organization access tokens. -

- -
-
- -
-

Delete Organization

+

Delete account

- Permanently delete this organization and all associated data. This action - cannot be undone. + Permanently delete your temetro account and any locally stored signing + keys. This action cannot be undone.