"use client"; import { CircleCheck, Clock, Pill, Search, Users } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; 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 { Input } from "@/components/ui/input"; import { type Prescription, formatPrescribedAt, listPrescriptions, updatePrescription, } from "@/lib/prescriptions"; import { notify } from "@/lib/toast"; // "3 days" / "14 days" / "1 month" → a course length in days; null for // open-ended values ("Ongoing", "As needed", free text we can't parse). function parseDurationDays(duration: string | null): number | null { if (!duration) return null; const match = duration .trim() .toLowerCase() .match(/^(\d+)\s*(day|week|month)s?$/); if (!match) return null; const n = Number(match[1]); const unit = match[2] === "day" ? 1 : match[2] === "week" ? 7 : 30; return n * unit; } // When an active course runs out, or null if it has no parseable end. function expiresAt(rx: Prescription): Date | null { const days = parseDurationDays(rx.duration); if (days == null) return null; const end = new Date(`${rx.prescribedAt}T00:00:00`); end.setDate(end.getDate() + days); return end; } function Kpi({ label, value, icon: Icon, }: { label: string; value: string; icon: typeof Pill; }) { return (
{label} {value}
); } function QueueRow({ rx, expiring, onOpen, onComplete, }: { rx: Prescription; expiring: boolean; onOpen: () => void; onComplete: () => 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)}
{expiring && ( {t("pharmacy.expiringBadge")} )}
); } // The pharmacy department home: a dispensing work queue over the clinic's // prescriptions. Pharmacy holds prescription read/write (no delete and no // create UI — prescribing stays with clinicians), so the only action here is // completing a course. export function PharmacyView() { const { t } = useTranslation(); const [list, setList] = useState([]); const [selected, setSelected] = useState(null); const [sheetOpen, setSheetOpen] = 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 now = new Date(); const soon = new Date(now); soon.setDate(soon.getDate() + 7); const isExpiringSoon = (rx: Prescription) => { if (rx.status !== "active") return false; const end = expiresAt(rx); return end != null && end <= soon; }; const active = useMemo( () => list.filter((rx) => rx.status === "active"), [list], ); // The dispensing queue: active prescriptions, oldest first. const search = query.trim().toLowerCase(); const queue = useMemo(() => { const base = [...active].sort((a, b) => a.prescribedAt.localeCompare(b.prescribedAt), ); if (!search) return base; return base.filter( (rx) => rx.name.toLowerCase().includes(search) || rx.fileNumber.includes(search) || rx.medication.toLowerCase().includes(search) || rx.prescriber.toLowerCase().includes(search), ); }, [active, search]); const kpis = [ { label: t("pharmacy.kpi.active"), value: String(active.length), icon: Pill, }, { label: t("pharmacy.kpi.expiring"), value: String(active.filter(isExpiringSoon).length), icon: Clock, }, { label: t("pharmacy.kpi.patients"), value: String(new Set(active.map((rx) => rx.fileNumber)).size), icon: Users, }, ]; const openRx = (rx: Prescription) => { setSelected(rx); setSheetOpen(true); }; // PUT requires the full record — resend the row with the new status. const markCompleted = async (rx: Prescription) => { try { const updated = await updatePrescription(rx.id, { fileNumber: rx.fileNumber, name: rx.name, initials: rx.initials, medication: rx.medication, dose: rx.dose, frequency: rx.frequency, prescriber: rx.prescriber, prescribedAt: rx.prescribedAt, status: "completed", duration: rx.duration, notes: rx.notes, }); setList((prev) => prev.map((p) => (p.id === updated.id ? updated : p))); notify.success( t("pharmacy.completedTitle"), t("pharmacy.completedBody", { medication: rx.medication, name: rx.name }), ); } catch { notify.error(t("pharmacy.completeFailedTitle"), t("pharmacy.completeFailedBody")); } }; return (

{t("pharmacy.title")}

{t("pharmacy.subtitle")}

setQuery(event.target.value)} placeholder={t("pharmacy.searchPlaceholder")} value={query} />
{kpis.map((k) => ( ))}

{t("pharmacy.queue.title")}

{t("pharmacy.queue.description")}

{queue.map((rx) => ( markCompleted(rx)} onOpen={() => openRx(rx)} rx={rx} /> ))} {queue.length === 0 && (

{search ? t("pharmacy.queue.noMatches") : t("pharmacy.queue.empty")}

)}
); }