mirror of
https://github.com/temetro/temetro.git
synced 2026-08-05 16:37:42 +00:00
Add patient: intake form in the input that creates + opens a new chart
- chat-input: new "Add patient" pill (UserPlus) opens an intake dialog; remounts it via a key each open for a fresh file # / cleared form. On create it runs /patient <new#> so the record's cards open in the chat. - add-patient-dialog: controlled Dialog + form — generated file # (with regenerate), identity (name/age/sex/status/PCP), current vitals, and repeatable allergies & medications. Save builds a Patient and adds it to the store. Save disabled until a name is entered. - lib/patients: generateFileNumber() (unique) + addPatient() (session store; swap point for a real create API). - patient-cards: graceful empty states (no labs / meds / problems / visits / trend) so a brand-new sparse chart renders cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
"use client";
|
||||
|
||||
import { Plus, RefreshCw, X } from "lucide-react";
|
||||
import { type FormEvent, type ReactNode, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
addPatient,
|
||||
type AllergySeverity,
|
||||
generateFileNumber,
|
||||
type Patient,
|
||||
} from "@/lib/patients";
|
||||
|
||||
type AddPatientDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onCreated: (fileNumber: string) => void;
|
||||
};
|
||||
|
||||
type AllergyDraft = { substance: string; reaction: string; severity: AllergySeverity };
|
||||
type MedicationDraft = { name: string; dose: string; frequency: string };
|
||||
|
||||
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 (
|
||||
<label className="flex flex-col gap-1.5">
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({
|
||||
label,
|
||||
onAdd,
|
||||
}: {
|
||||
label: string;
|
||||
onAdd: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{label}
|
||||
</span>
|
||||
<Button onClick={onAdd} size="sm" type="button" variant="ghost">
|
||||
<Plus className="size-4" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function initialsFromName(name: string): string {
|
||||
const parts = name.trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
return "?";
|
||||
}
|
||||
return parts
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0])
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
const today = () =>
|
||||
new Date().toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
export function AddPatientDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onCreated,
|
||||
}: AddPatientDialogProps) {
|
||||
// Lazily generate the file number on mount. The dialog is remounted (via a
|
||||
// `key`) each time it opens, so this gives a fresh number + cleared form
|
||||
// without a reset effect.
|
||||
const [fileNumber, setFileNumber] = useState(generateFileNumber);
|
||||
const [name, setName] = useState("");
|
||||
const [age, setAge] = useState("");
|
||||
const [sex, setSex] = useState<Patient["sex"]>("F");
|
||||
const [status, setStatus] = useState<Patient["status"]>("active");
|
||||
const [pcp, setPcp] = useState("");
|
||||
const [bp, setBp] = useState("");
|
||||
const [hr, setHr] = useState("");
|
||||
const [temp, setTemp] = useState("");
|
||||
const [spo2, setSpo2] = useState("");
|
||||
const [allergies, setAllergies] = useState<AllergyDraft[]>([]);
|
||||
const [medications, setMedications] = useState<MedicationDraft[]>([]);
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!name.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const patient: Patient = {
|
||||
fileNumber,
|
||||
name: name.trim(),
|
||||
age: Number(age) || 0,
|
||||
sex,
|
||||
pcp: pcp.trim() || "—",
|
||||
status,
|
||||
initials: initialsFromName(name),
|
||||
allergies: allergies.filter((a) => a.substance.trim()),
|
||||
alerts: [],
|
||||
medications: medications.filter((m) => m.name.trim()),
|
||||
problems: [],
|
||||
vitals: {
|
||||
bp: bp.trim() || "—",
|
||||
hr: hr.trim() || "—",
|
||||
temp: temp.trim() || "—",
|
||||
spo2: spo2.trim() || "—",
|
||||
takenAt: today(),
|
||||
},
|
||||
vitalsTrend: { label: "Heart rate", unit: "bpm", points: [] },
|
||||
labs: [],
|
||||
labTrend: { label: "—", unit: "", points: [] },
|
||||
encounters: [],
|
||||
};
|
||||
|
||||
addPatient(patient);
|
||||
onCreated(fileNumber);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={onOpenChange} open={open}>
|
||||
<DialogContent className="max-h-[85dvh] overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add patient</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new chart. A file number has been generated for you.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<Field label="File number">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input readOnly value={fileNumber} />
|
||||
<Button
|
||||
aria-label="Regenerate file number"
|
||||
onClick={() => setFileNumber(generateFileNumber())}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Field label="Full name">
|
||||
<Input
|
||||
autoFocus
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
placeholder="e.g. Jordan Pierce"
|
||||
required
|
||||
value={name}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<Field label="Age">
|
||||
<Input
|
||||
inputMode="numeric"
|
||||
onChange={(event) => setAge(event.target.value)}
|
||||
placeholder="—"
|
||||
value={age}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Sex">
|
||||
<select
|
||||
className={controlClass}
|
||||
onChange={(event) => setSex(event.target.value as Patient["sex"])}
|
||||
value={sex}
|
||||
>
|
||||
<option value="F">Female</option>
|
||||
<option value="M">Male</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Status">
|
||||
<select
|
||||
className={controlClass}
|
||||
onChange={(event) =>
|
||||
setStatus(event.target.value as Patient["status"])
|
||||
}
|
||||
value={status}
|
||||
>
|
||||
<option value="active">Active</option>
|
||||
<option value="inpatient">Inpatient</option>
|
||||
<option value="discharged">Discharged</option>
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field label="Primary care">
|
||||
<Input
|
||||
onChange={(event) => setPcp(event.target.value)}
|
||||
placeholder="e.g. Dr. Lena Ortiz"
|
||||
value={pcp}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
Current vitals
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<Input
|
||||
aria-label="Blood pressure"
|
||||
onChange={(event) => setBp(event.target.value)}
|
||||
placeholder="BP"
|
||||
value={bp}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Heart rate"
|
||||
onChange={(event) => setHr(event.target.value)}
|
||||
placeholder="HR"
|
||||
value={hr}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Temperature"
|
||||
onChange={(event) => setTemp(event.target.value)}
|
||||
placeholder="Temp"
|
||||
value={temp}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Oxygen saturation"
|
||||
onChange={(event) => setSpo2(event.target.value)}
|
||||
placeholder="SpO₂"
|
||||
value={spo2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<SectionHeader
|
||||
label="Allergies"
|
||||
onAdd={() =>
|
||||
setAllergies((prev) => [
|
||||
...prev,
|
||||
{ substance: "", reaction: "", severity: "mild" },
|
||||
])
|
||||
}
|
||||
/>
|
||||
{allergies.map((allergy, index) => (
|
||||
<div className="flex items-center gap-2" key={index}>
|
||||
<Input
|
||||
aria-label="Substance"
|
||||
onChange={(event) =>
|
||||
setAllergies((prev) =>
|
||||
prev.map((row, i) =>
|
||||
i === index
|
||||
? { ...row, substance: event.target.value }
|
||||
: row
|
||||
)
|
||||
)
|
||||
}
|
||||
placeholder="Substance"
|
||||
value={allergy.substance}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Reaction"
|
||||
onChange={(event) =>
|
||||
setAllergies((prev) =>
|
||||
prev.map((row, i) =>
|
||||
i === index
|
||||
? { ...row, reaction: event.target.value }
|
||||
: row
|
||||
)
|
||||
)
|
||||
}
|
||||
placeholder="Reaction"
|
||||
value={allergy.reaction}
|
||||
/>
|
||||
<select
|
||||
aria-label="Severity"
|
||||
className={cn(controlClass, "w-auto")}
|
||||
onChange={(event) =>
|
||||
setAllergies((prev) =>
|
||||
prev.map((row, i) =>
|
||||
i === index
|
||||
? {
|
||||
...row,
|
||||
severity: event.target.value as AllergySeverity,
|
||||
}
|
||||
: row
|
||||
)
|
||||
)
|
||||
}
|
||||
value={allergy.severity}
|
||||
>
|
||||
<option value="mild">Mild</option>
|
||||
<option value="moderate">Moderate</option>
|
||||
<option value="severe">Severe</option>
|
||||
</select>
|
||||
<button
|
||||
aria-label="Remove allergy"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
onClick={() =>
|
||||
setAllergies((prev) => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<SectionHeader
|
||||
label="Medications"
|
||||
onAdd={() =>
|
||||
setMedications((prev) => [
|
||||
...prev,
|
||||
{ name: "", dose: "", frequency: "" },
|
||||
])
|
||||
}
|
||||
/>
|
||||
{medications.map((med, index) => (
|
||||
<div className="flex items-center gap-2" key={index}>
|
||||
<Input
|
||||
aria-label="Medication name"
|
||||
onChange={(event) =>
|
||||
setMedications((prev) =>
|
||||
prev.map((row, i) =>
|
||||
i === index ? { ...row, name: event.target.value } : row
|
||||
)
|
||||
)
|
||||
}
|
||||
placeholder="Name"
|
||||
value={med.name}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Dose"
|
||||
onChange={(event) =>
|
||||
setMedications((prev) =>
|
||||
prev.map((row, i) =>
|
||||
i === index ? { ...row, dose: event.target.value } : row
|
||||
)
|
||||
)
|
||||
}
|
||||
placeholder="Dose"
|
||||
value={med.dose}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Frequency"
|
||||
onChange={(event) =>
|
||||
setMedications((prev) =>
|
||||
prev.map((row, i) =>
|
||||
i === index
|
||||
? { ...row, frequency: event.target.value }
|
||||
: row
|
||||
)
|
||||
)
|
||||
}
|
||||
placeholder="Frequency"
|
||||
value={med.frequency}
|
||||
/>
|
||||
<button
|
||||
aria-label="Remove medication"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
onClick={() =>
|
||||
setMedications((prev) => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button type="button" variant="outline" />}>
|
||||
Cancel
|
||||
</DialogClose>
|
||||
<Button disabled={!name.trim()} type="submit">
|
||||
Save patient
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Plus,
|
||||
Square,
|
||||
Stethoscope,
|
||||
UserPlus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { AddPatientDialog } from "@/components/chat/add-patient-dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -133,6 +135,9 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
||||
const [specialty, setSpecialty] = useState("internal-medicine");
|
||||
const [facility, setFacility] = useState("main-hospital");
|
||||
const [timeRange, setTimeRange] = useState("12m");
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
// Bumped on each open so the dialog remounts with a fresh file number + form.
|
||||
const [addKey, setAddKey] = useState(0);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isGenerating = status === "submitted" || status === "streaming";
|
||||
@@ -187,6 +192,7 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
@@ -323,7 +329,26 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
||||
triggerClassName={contextPill}
|
||||
value={timeRange}
|
||||
/>
|
||||
<button
|
||||
className={cn(contextPill, "ml-auto")}
|
||||
onClick={() => {
|
||||
setAddKey((k) => k + 1);
|
||||
setAddOpen(true);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<UserPlus className="size-4" />
|
||||
<span>Add patient</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<AddPatientDialog
|
||||
key={addKey}
|
||||
onCreated={(fileNumber) => onSubmit(`/patient ${fileNumber}`)}
|
||||
onOpenChange={setAddOpen}
|
||||
open={addOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,14 @@ function Row({ label, value }: { label: ReactNode; value: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Empty({ children }: { children: ReactNode }) {
|
||||
return <p className="text-muted-foreground">{children}</p>;
|
||||
}
|
||||
|
||||
function TrendBlock({ trend }: { trend: Trend }) {
|
||||
if (trend.points.length === 0) {
|
||||
return <Empty>No trend data yet.</Empty>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
@@ -101,6 +108,9 @@ function TrendBlock({ trend }: { trend: Trend }) {
|
||||
}
|
||||
|
||||
function TrendDetail({ trend }: { trend: Trend }) {
|
||||
if (trend.points.length === 0) {
|
||||
return <Empty>No trend data yet.</Empty>;
|
||||
}
|
||||
const min = Math.min(...trend.points);
|
||||
const max = Math.max(...trend.points);
|
||||
return (
|
||||
@@ -285,26 +295,30 @@ function LabsCard({ patient }: { patient: Patient }) {
|
||||
<ExpandableCard
|
||||
description={`As of ${patient.labs[0]?.takenAt ?? "—"}`}
|
||||
detail={
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{patient.labs.map((lab) => (
|
||||
<div
|
||||
className="flex items-center justify-between gap-3"
|
||||
key={lab.name}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-foreground">{lab.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{lab.takenAt}
|
||||
</span>
|
||||
patient.labs.length === 0 ? (
|
||||
<Empty>No labs on file.</Empty>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{patient.labs.map((lab) => (
|
||||
<div
|
||||
className="flex items-center justify-between gap-3"
|
||||
key={lab.name}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-foreground">{lab.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{lab.takenAt}
|
||||
</span>
|
||||
</div>
|
||||
{labValue(lab.value, lab.flag)}
|
||||
</div>
|
||||
{labValue(lab.value, lab.flag)}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<TrendDetail trend={patient.labTrend} />
|
||||
</div>
|
||||
<Separator />
|
||||
<TrendDetail trend={patient.labTrend} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
title="Labs"
|
||||
>
|
||||
@@ -313,34 +327,43 @@ function LabsCard({ patient }: { patient: Patient }) {
|
||||
<CardDescription>As of {patient.labs[0]?.takenAt ?? "—"}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{patient.labs.map((lab) => (
|
||||
<Row
|
||||
key={lab.name}
|
||||
label={lab.name}
|
||||
value={labValue(lab.value, lab.flag)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<TrendBlock trend={patient.labTrend} />
|
||||
{patient.labs.length === 0 ? (
|
||||
<Empty>No labs on file.</Empty>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
{patient.labs.map((lab) => (
|
||||
<Row
|
||||
key={lab.name}
|
||||
label={lab.name}
|
||||
value={labValue(lab.value, lab.flag)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<TrendBlock trend={patient.labTrend} />
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</ExpandableCard>
|
||||
);
|
||||
}
|
||||
|
||||
function MedicationsCard({ patient }: { patient: Patient }) {
|
||||
const list = (
|
||||
<div className="flex flex-col gap-2">
|
||||
{patient.medications.map((med) => (
|
||||
<Row
|
||||
key={med.name}
|
||||
label={med.name}
|
||||
value={`${med.dose} · ${med.frequency}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const list =
|
||||
patient.medications.length === 0 ? (
|
||||
<Empty>No active medications.</Empty>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{patient.medications.map((med) => (
|
||||
<Row
|
||||
key={med.name}
|
||||
label={med.name}
|
||||
value={`${med.dose} · ${med.frequency}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<ExpandableCard
|
||||
description={`${patient.medications.length} active`}
|
||||
@@ -357,17 +380,20 @@ function MedicationsCard({ patient }: { patient: Patient }) {
|
||||
}
|
||||
|
||||
function ProblemsCard({ patient }: { patient: Patient }) {
|
||||
const list = (
|
||||
<div className="flex flex-col gap-2">
|
||||
{patient.problems.map((problem) => (
|
||||
<Row
|
||||
key={problem.label}
|
||||
label={problem.label}
|
||||
value={`since ${problem.since}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const list =
|
||||
patient.problems.length === 0 ? (
|
||||
<Empty>No active problems.</Empty>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{patient.problems.map((problem) => (
|
||||
<Row
|
||||
key={problem.label}
|
||||
label={problem.label}
|
||||
value={`since ${problem.since}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<ExpandableCard
|
||||
description={`${patient.problems.length} active`}
|
||||
@@ -437,6 +463,9 @@ function AllergiesCard({ patient }: { patient: Patient }) {
|
||||
}
|
||||
|
||||
function VisitsList({ patient }: { patient: Patient }) {
|
||||
if (patient.encounters.length === 0) {
|
||||
return <Empty>No visits yet.</Empty>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{patient.encounters.map((encounter) => (
|
||||
|
||||
@@ -266,3 +266,18 @@ export async function getPatient(fileNumber: string): Promise<Patient | null> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
return PATIENTS[fileNumber.trim()] ?? null;
|
||||
}
|
||||
|
||||
// Generate a unique 5-digit file number not already in the store.
|
||||
export function generateFileNumber(): string {
|
||||
let candidate: string;
|
||||
do {
|
||||
candidate = String(10000 + Math.floor(Math.random() * 89999));
|
||||
} while (candidate in PATIENTS);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
// Add (or replace) a patient in the in-memory store. Session-only — no backend
|
||||
// yet, so this resets on reload. Swap point for a real "create patient" API.
|
||||
export function addPatient(patient: Patient): void {
|
||||
PATIENTS[patient.fileNumber] = patient;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user