"use client";
import { CircleCheck, Clock, Pill, Plus, Search } from "lucide-react";
import { type ReactNode, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { AiBadge } from "@/components/ai-badge";
import {
AddPrescriptionDialog,
type NewPrescription,
} from "@/components/prescriptions/add-prescription-dialog";
import { PrescriptionDetailSheet } from "@/components/prescriptions/prescription-detail-sheet";
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 { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { Input } from "@/components/ui/input";
import {
type Prescription,
type RxStatus,
createPrescription,
deletePrescription,
formatPrescribedAt,
listPrescriptions,
} from "@/lib/prescriptions";
import { notify } from "@/lib/toast";
export type { Prescription, RxStatus } from "@/lib/prescriptions";
const statusVariant: Record<
RxStatus,
"default" | "secondary" | "destructive" | "outline"
> = {
active: "default",
completed: "outline",
expired: "destructive",
};
function Kpi({
label,
value,
icon: Icon,
}: {
label: string;
value: string;
icon: typeof Pill;
}) {
return (
{label}
{value}
);
}
function RxRow({ rx, onOpen }: { rx: Prescription; onOpen: () => void }) {
const { t } = useTranslation();
return (
{
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onOpen();
}
}}
role="button"
tabIndex={0}
>
{rx.initials}
{rx.medication}
{rx.dose && (
· {rx.dose}
)}
{rx.name} · #{rx.fileNumber} · {rx.frequency}
{rx.prescriber}
{formatPrescribedAt(rx.prescribedAt)}
{t(`prescriptions.status.${rx.status}`)}
);
}
function Section({
title,
description,
children,
}: {
title: string;
description?: string;
children: ReactNode;
}) {
return (
{title}
{description && (
{description}
)}
{children}
);
}
export function PrescriptionsView() {
const { t } = useTranslation();
const [addOpen, setAddOpen] = useState(false);
const [list, setList] = useState([]);
const [selected, setSelected] = useState(null);
const [sheetOpen, setSheetOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [query, setQuery] = useState("");
useEffect(() => {
let active = true;
listPrescriptions()
.then((data) => {
if (active) setList(data);
})
.catch(() => {
/* api-client redirects on 401; otherwise leave the list empty */
});
return () => {
active = false;
};
}, []);
const openRx = (rx: Prescription) => {
setSelected(rx);
setSheetOpen(true);
};
const removeRx = async () => {
if (!selected) return;
const id = selected.id;
try {
await deletePrescription(id);
setList((prev) => prev.filter((r) => r.id !== id));
setSheetOpen(false);
notify.success(t("prescriptions.delete.doneTitle"), selected.medication);
} catch {
notify.error(
t("prescriptions.delete.failedTitle"),
t("prescriptions.delete.failedBody"),
);
} finally {
setConfirmOpen(false);
}
};
// Persist a new prescription, then add the saved record to the top of the list.
const addPrescription = async (rx: NewPrescription) => {
try {
const created = await createPrescription({
fileNumber: rx.fileNumber,
name: rx.name,
initials: rx.initials,
medication: rx.medication,
dose: rx.dose,
frequency: rx.frequency,
duration: rx.duration || null,
startDate: rx.startDate || null,
endDate: rx.endDate || null,
notes: rx.notes || null,
});
setList((prev) => [created, ...prev]);
} catch {
notify.error(
t("prescriptions.addFailedTitle"),
t("prescriptions.addFailedBody"),
);
}
};
// Case-insensitive substring match, same pattern as the Patients page.
const search = query.trim().toLowerCase();
const filtered = useMemo(() => {
if (!search) return list;
return list.filter(
(rx) =>
rx.name.toLowerCase().includes(search) ||
rx.fileNumber.includes(search) ||
rx.medication.toLowerCase().includes(search) ||
rx.prescriber.toLowerCase().includes(search) ||
rx.status.toLowerCase().includes(search),
);
}, [list, search]);
const kpis = useMemo(
() => [
{
label: t("prescriptions.kpi.active"),
value: String(list.filter((r) => r.status === "active").length),
icon: Pill,
},
{
label: t("prescriptions.kpi.completed"),
value: String(list.filter((r) => r.status === "completed").length),
icon: CircleCheck,
},
{
label: t("prescriptions.kpi.expired"),
value: String(list.filter((r) => r.status === "expired").length),
icon: Clock,
},
],
[list, t],
);
return (
{t("prescriptions.title")}
{t("prescriptions.subtitle")}
{kpis.map((k) => (
))}
{filtered.map((rx) => (
openRx(rx)} rx={rx} />
))}
{filtered.length === 0 && (
{search
? t("prescriptions.noMatches")
: t("prescriptions.emptyList")}
)}
setConfirmOpen(true)}
onOpenChange={setSheetOpen}
open={sheetOpen}
rx={selected}
/>
);
}