mirror of
https://github.com/temetro/temetro.git
synced 2026-08-28 11:27:40 +00:00
Patients page, clinical Settings re-theme, Polar-style user footer
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,15 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import { SidebarInset } from "@/components/ui/sidebar";
|
import { SidebarInset } from "@/components/ui/sidebar";
|
||||||
import { ChatPanel } from "@/components/chat/chat-panel";
|
import { ChatPanel } from "@/components/chat/chat-panel";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<SidebarInset className="flex flex-1 flex-col overflow-hidden">
|
<SidebarInset className="flex flex-1 flex-col overflow-hidden">
|
||||||
<ChatPanel />
|
{/* ChatPanel reads the `?patient=` search param, so it needs a Suspense boundary. */}
|
||||||
|
<Suspense>
|
||||||
|
<ChatPanel />
|
||||||
|
</Suspense>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { SidebarInset } from "@/components/ui/sidebar";
|
||||||
|
import { PatientsView } from "@/components/patients/patients-view";
|
||||||
|
|
||||||
|
export default function PatientsPage() {
|
||||||
|
return (
|
||||||
|
<SidebarInset className="flex flex-1 flex-col overflow-y-auto">
|
||||||
|
<PatientsView />
|
||||||
|
</SidebarInset>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import type { ChatStatus } from "ai";
|
import type { ChatStatus } from "ai";
|
||||||
import { useCallback, useState } from "react";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Conversation,
|
Conversation,
|
||||||
@@ -106,6 +107,18 @@ export function ChatPanel() {
|
|||||||
|
|
||||||
const handleStop = useCallback(() => setStatus("ready"), []);
|
const handleStop = useCallback(() => setStatus("ready"), []);
|
||||||
|
|
||||||
|
// Opening a patient from the Patients page lands here as `/?patient=<file#>`;
|
||||||
|
// run the lookup once on arrival.
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const requestedPatient = searchParams.get("patient");
|
||||||
|
const handledPatientRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestedPatient && handledPatientRef.current !== requestedPatient) {
|
||||||
|
handledPatientRef.current = requestedPatient;
|
||||||
|
send(`/patient ${requestedPatient}`);
|
||||||
|
}
|
||||||
|
}, [requestedPatient, send]);
|
||||||
|
|
||||||
const promptInput = (
|
const promptInput = (
|
||||||
<ChatInput onStop={handleStop} onSubmit={send} status={status} />
|
<ChatInput onStop={handleStop} onSubmit={send} status={status} />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<Patient["status"], BadgeVariant> = {
|
||||||
|
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 (
|
||||||
|
<div className="mx-auto w-full max-w-4xl px-6 py-10">
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Patients</h1>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
className="w-full pl-9 sm:w-64"
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
placeholder="Search name or MRN"
|
||||||
|
value={query}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="rounded-3xl"
|
||||||
|
onClick={() => {
|
||||||
|
setAddKey((k) => k + 1);
|
||||||
|
setAddOpen(true);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
Add patient
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 overflow-hidden rounded-2xl border border-border bg-card/30">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-border border-b text-left text-xs text-muted-foreground uppercase">
|
||||||
|
<th className="px-4 py-3 font-medium">Name</th>
|
||||||
|
<th className="px-4 py-3 font-medium">MRN</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Age · Sex</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Status</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Last seen</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Allergies</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{patients.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
className="px-4 py-10 text-center text-muted-foreground"
|
||||||
|
colSpan={6}
|
||||||
|
>
|
||||||
|
No patients found.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
patients.map((p) => (
|
||||||
|
<tr
|
||||||
|
className="cursor-pointer border-border/50 border-b transition-colors last:border-0 hover:bg-accent/50"
|
||||||
|
key={p.fileNumber}
|
||||||
|
onClick={() => open(p.fileNumber)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
open(p.fileNumber);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 font-medium text-foreground">
|
||||||
|
{p.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-muted-foreground">
|
||||||
|
{p.fileNumber}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-muted-foreground">
|
||||||
|
{p.age} · {p.sex}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<Badge className="capitalize" variant={statusVariant[p.status]}>
|
||||||
|
{p.status}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-muted-foreground">
|
||||||
|
{p.encounters[0]?.date ?? "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-muted-foreground">
|
||||||
|
{p.allergies.length || "—"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PatientFormDialog
|
||||||
|
key={addKey}
|
||||||
|
mode="create"
|
||||||
|
onCreated={(fileNumber) => open(fileNumber)}
|
||||||
|
onOpenChange={setAddOpen}
|
||||||
|
open={addOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,58 +4,52 @@ import { cn } from "@/lib/utils";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
CopyField,
|
||||||
SettingsCard,
|
SettingsCard,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
whiteButton,
|
whiteButton,
|
||||||
} from "@/components/settings/settings-parts";
|
} from "@/components/settings/settings-parts";
|
||||||
|
|
||||||
export function BillingPanel() {
|
export function SigningPanel() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsCard className="flex flex-col gap-6 p-6 sm:flex-row sm:items-start sm:justify-between">
|
<SettingsCard className="flex flex-col gap-6 p-6 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="text-xl font-semibold tracking-tight">Early Member</h3>
|
<h3 className="text-xl font-semibold tracking-tight">Signing key</h3>
|
||||||
<Badge className="bg-emerald-500/15 text-emerald-400">Active</Badge>
|
<Badge className="bg-emerald-500/15 text-emerald-400">Active</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
<Button className={cn("rounded-lg", whiteButton)}>Change plan</Button>
|
<Button className={cn("rounded-lg", whiteButton)}>Rotate key</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:text-right">
|
<div className="sm:text-right">
|
||||||
<p className="text-3xl font-semibold tracking-tight">Free</p>
|
<p className="text-3xl font-semibold tracking-tight">Ed25519</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">Created May 28, 2026</p>
|
||||||
4.00% + $0.40 per transaction
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
action={
|
description="The public key patients use to verify your signatures"
|
||||||
<Button className="rounded-lg" variant="secondary">
|
title="Signing identity"
|
||||||
Add payment method
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
description="Cards used to pay for your temetro subscription"
|
|
||||||
title="Payment methods"
|
|
||||||
>
|
>
|
||||||
<SettingsCard className="flex items-center justify-center p-12">
|
<SettingsCard className="p-5">
|
||||||
<p className="text-sm text-muted-foreground">No payment methods on file</p>
|
<CopyField
|
||||||
|
description="Share or publish this fingerprint so patients can trust your changes"
|
||||||
|
label="Public key fingerprint"
|
||||||
|
value="ed25519:9f86 d081 884c 7d65 9a2f eaa0 c55a d015"
|
||||||
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
action={
|
description="Changes you've signed that are waiting on the patient's approval"
|
||||||
<Button className="rounded-lg" variant="secondary">
|
title="Signed records"
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
description="Used on invoices for your temetro subscription"
|
|
||||||
title="Billing address"
|
|
||||||
>
|
>
|
||||||
<SettingsCard className="p-5">
|
<SettingsCard className="flex items-center justify-center p-12">
|
||||||
<p className="text-sm">khalid</p>
|
<p className="text-sm text-muted-foreground">No pending signatures</p>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { ChevronDown, Plus } from "lucide-react";
|
import { ChevronDown, Plus } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -12,59 +11,54 @@ import {
|
|||||||
SettingsCard,
|
SettingsCard,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
ToggleRow,
|
ToggleRow,
|
||||||
whiteButton,
|
|
||||||
} from "@/components/settings/settings-parts";
|
} from "@/components/settings/settings-parts";
|
||||||
|
|
||||||
const customerNotifications = [
|
const patientNotifications = [
|
||||||
{
|
{
|
||||||
title: "Order confirmation",
|
title: "New lab result",
|
||||||
description: "Sent when a customer completes a one-time purchase",
|
description: "Sent when a new lab result is available on a patient's chart",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Subscription confirmation",
|
title: "Record updated",
|
||||||
description: "Sent when a customer starts a new subscription",
|
description: "Sent when a patient's record is updated by a member of the care team",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Subscription cycled",
|
title: "Approval requested",
|
||||||
description: "Sent when a subscription automatically renews",
|
description: "Sent when a signed change is awaiting the patient's approval",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Trial converted",
|
title: "Change approved",
|
||||||
description: "Sent when a trial ends and the subscription becomes paid",
|
description: "Sent when a patient approves a pending change to their record",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Renewal reminder",
|
title: "New message",
|
||||||
description: "Sent 7 days before a subscription with a long billing cycle renews",
|
description: "Sent when a patient or another clinician sends a message",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Trial conversion reminder",
|
title: "Visit scheduled",
|
||||||
description: "Sent before a trial ends and converts to a paid subscription",
|
description: "Sent when an upcoming visit is added to a patient's record",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Subscription updated",
|
|
||||||
description: "Sent when a customer changes their subscription to a different product",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function PreferencesPanel() {
|
export function ProfilePanel() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsSection title="Organization">
|
<SettingsSection title="Clinician profile">
|
||||||
<SettingsCard className="space-y-6 p-5">
|
<SettingsCard className="space-y-6 p-5">
|
||||||
<CopyField
|
<CopyField
|
||||||
description="Unique identifier for your organization"
|
description="Your unique clinician identifier, used when signing records"
|
||||||
label="Identifier"
|
label="Clinician ID"
|
||||||
value="62a5278f-91c6-4912-b711-ee1c9c2f0a73"
|
value="62a5278f-91c6-4912-b711-ee1c9c2f0a73"
|
||||||
/>
|
/>
|
||||||
<CopyField
|
<CopyField
|
||||||
description="Used for Customer Portal, Transaction Statements, etc."
|
description="Used in your public profile and the patient portal"
|
||||||
label="Organization Slug"
|
label="Handle"
|
||||||
value="khalid"
|
value="dr-khalid"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-end gap-4">
|
<div className="flex items-end gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<FieldLabel>Logo</FieldLabel>
|
<FieldLabel>Avatar</FieldLabel>
|
||||||
<Avatar className="size-10 rounded-xl" size="lg">
|
<Avatar className="size-10 rounded-xl" size="lg">
|
||||||
<AvatarFallback className="rounded-xl bg-muted text-sm font-medium">
|
<AvatarFallback className="rounded-xl bg-muted text-sm font-medium">
|
||||||
K
|
K
|
||||||
@@ -72,55 +66,55 @@ export function PreferencesPanel() {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 space-y-1.5">
|
<div className="flex-1 space-y-1.5">
|
||||||
<FieldLabel required>Organization Name</FieldLabel>
|
<FieldLabel required>Display name</FieldLabel>
|
||||||
<Input defaultValue="khalid" />
|
<Input defaultValue="Dr. Khalid" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<FieldLabel>Country</FieldLabel>
|
<FieldLabel>Specialty</FieldLabel>
|
||||||
<button
|
<button
|
||||||
className="flex h-9 w-full items-center justify-between rounded-3xl bg-input/50 px-3 text-sm text-muted-foreground transition-colors hover:bg-input/70"
|
className="flex h-9 w-full items-center justify-between rounded-3xl bg-input/50 px-3 text-sm text-muted-foreground transition-colors hover:bg-input/70"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Select country
|
Select specialty
|
||||||
<ChevronDown className="size-4" />
|
<ChevronDown className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<FieldLabel required>Website</FieldLabel>
|
<FieldLabel>Clinic / practice</FieldLabel>
|
||||||
<Input placeholder="https://acme.com" />
|
<Input placeholder="e.g. Main Hospital" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<FieldLabel required>Support Email</FieldLabel>
|
<FieldLabel required>Contact email</FieldLabel>
|
||||||
<Input placeholder="support@acme.com" />
|
<Input placeholder="clinician@example.org" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FieldLabel>Social Media</FieldLabel>
|
<FieldLabel>Professional links</FieldLabel>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Your personal social media links are used for identity verification. They
|
Registry or institutional profiles used to verify your identity. They are
|
||||||
will never be shown publicly.
|
never shown to patients.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button className="rounded-lg" size="sm" variant="outline">
|
<Button className="rounded-lg" size="sm" variant="outline">
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
Add Social
|
Add link
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
description="Emails automatically sent to customers for purchases, renewals, and other subscription lifecycle events"
|
description="Emails sent to patients about their records, results, and pending approvals"
|
||||||
title="Customer notifications"
|
title="Patient notifications"
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{customerNotifications.map((item) => (
|
{patientNotifications.map((item) => (
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
defaultChecked
|
defaultChecked
|
||||||
description={item.description}
|
description={item.description}
|
||||||
@@ -132,63 +126,49 @@ export function PreferencesPanel() {
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
description="Emails sent to members of your organization for account and product activity"
|
description="Notifications sent to you about your patients and the care team"
|
||||||
title="Account notifications"
|
title="Account notifications"
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
defaultChecked
|
defaultChecked
|
||||||
description="Send a notification when new orders are created"
|
description="Notify me when a patient approves or rejects a pending change"
|
||||||
title="New Orders"
|
title="Pending approvals"
|
||||||
/>
|
/>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
defaultChecked
|
defaultChecked
|
||||||
description="Send a notification when new subscriptions are created"
|
description="Notify me when a patient shares a record with me"
|
||||||
title="New Subscriptions"
|
title="Records shared with me"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
description="Manage alpha & beta features for your organization"
|
description="Manage alpha & beta features for your account"
|
||||||
title="Features"
|
title="Features"
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
description="Show translated checkouts to your customers"
|
description="Write records to the patient's own device instead of your database"
|
||||||
title="Localized Checkout"
|
title="Patient-owned storage (beta)"
|
||||||
/>
|
/>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
description="Enable seat-based pricing for subscription products. Requires the member model to be enabled."
|
description="Require a signature on every change you make to a patient record"
|
||||||
title="Seat-Based Billing"
|
title="Require signed records"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
description="Manage access tokens to authenticate with the temetro API"
|
description="Irreversible actions for your account"
|
||||||
title="Developers"
|
|
||||||
>
|
|
||||||
<SettingsCard className="flex flex-col items-start gap-4 p-6">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
You don't have any active organization access tokens.
|
|
||||||
</p>
|
|
||||||
<Button className={cn("rounded-lg", whiteButton)} size="sm">
|
|
||||||
Create token
|
|
||||||
</Button>
|
|
||||||
</SettingsCard>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<SettingsSection
|
|
||||||
description="Irreversible actions for this organization"
|
|
||||||
title="Danger Zone"
|
title="Danger Zone"
|
||||||
>
|
>
|
||||||
<SettingsCard className="flex items-center justify-between gap-4 p-4">
|
<SettingsCard className="flex items-center justify-between gap-4 p-4">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-sm font-medium">Delete Organization</p>
|
<p className="text-sm font-medium">Delete account</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Permanently delete this organization and all associated data. This action
|
Permanently delete your temetro account and any locally stored signing
|
||||||
cannot be undone.
|
keys. This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button className="rounded-lg bg-destructive text-white hover:bg-destructive/90">
|
<Button className="rounded-lg bg-destructive text-white hover:bg-destructive/90">
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import {
|
|||||||
SettingsCard,
|
SettingsCard,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from "@/components/settings/settings-parts";
|
} from "@/components/settings/settings-parts";
|
||||||
import { BillingPanel } from "@/components/settings/settings-billing";
|
import { SigningPanel } from "@/components/settings/settings-billing";
|
||||||
import { PreferencesPanel } from "@/components/settings/settings-preferences";
|
import { ProfilePanel } from "@/components/settings/settings-preferences";
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
"Preferences",
|
"Profile",
|
||||||
"Billing",
|
"Records",
|
||||||
"Members",
|
"Signing",
|
||||||
"Webhooks",
|
"Care team",
|
||||||
"Custom Fields",
|
"Developers",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type Tab = (typeof TABS)[number];
|
type Tab = (typeof TABS)[number];
|
||||||
@@ -37,7 +37,7 @@ function PlaceholderPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsView() {
|
export function SettingsView() {
|
||||||
const [tab, setTab] = useState<Tab>("Preferences");
|
const [tab, setTab] = useState<Tab>("Profile");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-3xl px-6 py-10">
|
<div className="mx-auto w-full max-w-3xl px-6 py-10">
|
||||||
@@ -63,24 +63,24 @@ export function SettingsView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 space-y-12">
|
<div className="mt-10 space-y-12">
|
||||||
{tab === "Preferences" && <PreferencesPanel />}
|
{tab === "Profile" && <ProfilePanel />}
|
||||||
{tab === "Billing" && <BillingPanel />}
|
{tab === "Records" && (
|
||||||
{tab === "Members" && (
|
|
||||||
<PlaceholderPanel
|
<PlaceholderPanel
|
||||||
description="Invite and manage members of your organization"
|
description="How patient records are sourced, stored, and displayed"
|
||||||
title="Members"
|
title="Records"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "Webhooks" && (
|
{tab === "Signing" && <SigningPanel />}
|
||||||
|
{tab === "Care team" && (
|
||||||
<PlaceholderPanel
|
<PlaceholderPanel
|
||||||
description="Send event notifications to your own endpoints"
|
description="Clinicians with access to this workspace"
|
||||||
title="Webhooks"
|
title="Care team"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "Custom Fields" && (
|
{tab === "Developers" && (
|
||||||
<PlaceholderPanel
|
<PlaceholderPanel
|
||||||
description="Collect additional information from your customers"
|
description="Access tokens for the temetro API"
|
||||||
title="Custom Fields"
|
title="Developers"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import Image from "next/image";
|
|||||||
import type { Route } from "./nav-main";
|
import type { Route } from "./nav-main";
|
||||||
import DashboardNavigation from "@/components/sidebar-02/nav-main";
|
import DashboardNavigation from "@/components/sidebar-02/nav-main";
|
||||||
import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications";
|
import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications";
|
||||||
|
import { NavUser } from "@/components/sidebar-02/nav-user";
|
||||||
|
|
||||||
const sampleNotifications = [
|
const sampleNotifications = [
|
||||||
{
|
{
|
||||||
@@ -51,7 +52,7 @@ const dashboardRoutes: Route[] = [
|
|||||||
id: "patients",
|
id: "patients",
|
||||||
title: "Patients",
|
title: "Patients",
|
||||||
icon: <Users className="size-4" />,
|
icon: <Users className="size-4" />,
|
||||||
link: "#",
|
link: "/patients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "settings",
|
id: "settings",
|
||||||
@@ -109,12 +110,7 @@ export function DashboardSidebar() {
|
|||||||
<DashboardNavigation routes={dashboardRoutes} />
|
<DashboardNavigation routes={dashboardRoutes} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter className="px-2">
|
<SidebarFooter className="px-2">
|
||||||
{!isCollapsed && (
|
<NavUser />
|
||||||
<div className="flex items-baseline gap-2 px-2 py-1.5">
|
|
||||||
<span className="font-semibold text-foreground">temetro</span>
|
|
||||||
<span className="text-xs text-muted-foreground">open source</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ChevronsUpDown, LogOut, Settings as SettingsIcon, Sun } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
|
// Placeholder identity — there is no auth backend yet.
|
||||||
|
const user = { name: "Dr. Khalid", role: "Clinician", initials: "K" };
|
||||||
|
// Open-source repo (placeholder).
|
||||||
|
const REPO_URL = "https://github.com/temetro/temetro";
|
||||||
|
|
||||||
|
function GitHubIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className={className}
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M12 .5C5.37.5 0 5.78 0 12.29c0 5.2 3.44 9.6 8.21 11.16.6.11.82-.25.82-.55 0-.27-.01-1.16-.02-2.1-3.34.73-4.04-1.42-4.04-1.42-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.73.08-.73 1.21.09 1.84 1.25 1.84 1.25 1.07 1.84 2.81 1.31 3.5 1 .11-.78.42-1.31.76-1.61-2.67-.31-5.47-1.34-5.47-5.95 0-1.31.47-2.39 1.24-3.23-.12-.31-.54-1.54.12-3.21 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.67.24 2.9.12 3.21.77.84 1.24 1.92 1.24 3.23 0 4.62-2.81 5.64-5.49 5.94.43.37.82 1.1.82 2.22 0 1.6-.02 2.89-.02 3.29 0 .31.21.69.83.57A12.02 12.02 0 0 0 24 12.29C24 5.78 18.63.5 12 .5Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavUser() {
|
||||||
|
const { isMobile, state } = useSidebar();
|
||||||
|
const isCollapsed = state === "collapsed";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<SidebarMenuButton
|
||||||
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
size="lg"
|
||||||
|
tooltip={user.name}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Avatar className="size-8">
|
||||||
|
<AvatarFallback>{user.initials}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-medium">{user.name}</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="ml-auto size-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="min-w-56"
|
||||||
|
side={isMobile ? "bottom" : isCollapsed ? "right" : "top"}
|
||||||
|
sideOffset={8}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="flex items-center gap-2 py-2 text-foreground">
|
||||||
|
<Avatar className="size-8">
|
||||||
|
<AvatarFallback>{user.initials}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-medium">{user.name}</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem render={<Link href="/settings" />}>
|
||||||
|
<SettingsIcon />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
render={<a href={REPO_URL} rel="noreferrer" target="_blank" />}
|
||||||
|
>
|
||||||
|
<GitHubIcon className="size-4" />
|
||||||
|
Docs & GitHub
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Sun />
|
||||||
|
Theme
|
||||||
|
<DropdownMenuShortcut>Dark</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem variant="destructive">
|
||||||
|
<LogOut />
|
||||||
|
Log out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -261,6 +261,11 @@ const PATIENTS: Record<string, Patient> = {
|
|||||||
|
|
||||||
export const SAMPLE_FILE_NUMBERS = Object.keys(PATIENTS);
|
export const SAMPLE_FILE_NUMBERS = Object.keys(PATIENTS);
|
||||||
|
|
||||||
|
// Live list of every patient in the store (includes ones added this session).
|
||||||
|
export function listPatients(): Patient[] {
|
||||||
|
return Object.values(PATIENTS);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPatient(fileNumber: string): Promise<Patient | null> {
|
export async function getPatient(fileNumber: string): Promise<Patient | null> {
|
||||||
// Simulate retrieval latency so the loading (skeleton) state is visible.
|
// Simulate retrieval latency so the loading (skeleton) state is visible.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||||
|
|||||||
Reference in New Issue
Block a user