diff --git a/backend/src/routes/activity.ts b/backend/src/routes/activity.ts index 88dae62..9dfd738 100644 --- a/backend/src/routes/activity.ts +++ b/backend/src/routes/activity.ts @@ -25,3 +25,18 @@ activityRouter.get("/", async (req, res, next) => { next(err); } }); + +// A single patient's record history (who added/changed what, when). Any clinic +// member can read it — it's the audit trail for that chart. +activityRouter.get("/patient/:fileNumber", async (req, res, next) => { + try { + res.json( + await service.listPatientActivity( + req.organizationId!, + req.params.fileNumber as string, + ), + ); + } catch (err) { + next(err); + } +}); diff --git a/backend/src/services/activity.ts b/backend/src/services/activity.ts index 2f91069..7271f12 100644 --- a/backend/src/services/activity.ts +++ b/backend/src/services/activity.ts @@ -54,6 +54,28 @@ export async function recordActivity(params: { } } +// Lists every audit entry tied to a single patient (by file number), newest +// first. Unlike the clinic feed this is NOT scoped to one actor: a patient's +// record history should show every clinician who added or changed data on it. +export async function listPatientActivity( + orgId: string, + fileNumber: string, + limit = 100, +): Promise { + const rows = await db + .select() + .from(activityLog) + .where( + and( + eq(activityLog.organizationId, orgId), + eq(activityLog.patientFileNumber, fileNumber), + ), + ) + .orderBy(desc(activityLog.createdAt)) + .limit(limit); + return rows.map(toEntry); +} + // Lists the clinic's audit feed. When `actorId` is given, only that user's own // actions are returned (each employee sees their own activity); admins/owners // call without it to see the whole clinic. diff --git a/frontend/components/patients/patient-detail.tsx b/frontend/components/patients/patient-detail.tsx index fa8e403..c46d84b 100644 --- a/frontend/components/patients/patient-detail.tsx +++ b/frontend/components/patients/patient-detail.tsx @@ -1,12 +1,14 @@ "use client"; -import { ArrowLeftRight, Network, Pencil, Trash2 } from "lucide-react"; +import { ArrowLeftRight, FileDown, Network, Pencil, Trash2 } from "lucide-react"; import { type ReactNode, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Sparkline } from "@/components/chat/sparkline"; import { AttachmentsSection } from "@/components/patients/patient-files"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { type ActivityEntry, listPatientActivity } from "@/lib/activity"; +import { printPatientSummary } from "@/lib/patient-pdf"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -39,7 +41,14 @@ type RecordFile = { rows: { label: string; value: string }[]; }; -type BadgeVariant = "default" | "secondary" | "destructive" | "outline"; +type BadgeVariant = + | "default" + | "secondary" + | "destructive" + | "outline" + | "success" + | "info" + | "warning"; const severityVariant: Record = { mild: "outline", @@ -53,8 +62,8 @@ const labFlagVariant: Record = { critical: "destructive", }; const statusVariant: Record = { - active: "secondary", - inpatient: "destructive", + active: "success", + inpatient: "info", discharged: "outline", }; @@ -105,6 +114,60 @@ function TrendBlock({ trend }: { trend: Trend }) { ); } +// The patient's record history: every audited add/change on this chart, newest +// first. Reuses the clinic activity log scoped to this file number. +function RecordHistory({ fileNumber }: { fileNumber: string }) { + const { t } = useTranslation(); + const [entries, setEntries] = useState(null); + const [error, setError] = useState(false); + + useEffect(() => { + let active = true; + listPatientActivity(fileNumber) + .then((e) => active && setEntries(e)) + .catch(() => active && setError(true)); + return () => { + active = false; + }; + }, [fileNumber]); + + return ( +
+ {error ? ( +

+ {t("patientCard.history.loadError")} +

+ ) : entries === null ? ( +

{t("patients.loading")}

+ ) : entries.length === 0 ? ( +

+ {t("patientCard.history.empty")} +

+ ) : ( +
    + {entries.map((e) => ( +
  1. + + + {e.actorInitials} + + +
    + + {e.actorName} {e.action} + + + {new Date(e.createdAt).toLocaleString()} + +
    +
  2. + ))} +
+ )} +
+ ); +} + // Full patient record laid out vertically for the side Sheet — plain full-width // sections (no fixed-width cards, no nested click-to-expand dialogs). export function PatientDetail({ @@ -200,6 +263,15 @@ export function PatientDetail({ )}
+ {onTransfer && (