"use client"; import { Activity as ActivityIcon, CalendarClock, CalendarDays, FileText, Hash, ListChecks, type LucideIcon, NotebookPen, Pill, Stethoscope, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Card } from "@/components/ui/card"; import { Dialog, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { ListPagination } from "@/components/ui/list-pagination"; import { type ActivityEntityType, type ActivityEntry, listActivity, } from "@/lib/activity"; import { cn } from "@/lib/utils"; // A plain, tamper-evident audit log of record changes in the active clinic. (The // blockchain-style signing / patient-approval flow from the product vision is // separate and not built yet.) const entityIcon: Record = { patient: Stethoscope, note: NotebookPen, appointment: CalendarClock, prescription: Pill, task: ListChecks, }; // ISO timestamp -> "Today, 10:24" / "Yesterday, 16:05" / "Jun 3, 14:30". function formatTime(iso: string): string { const d = new Date(iso); const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false, }); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); const sameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); if (sameDay(d, today)) return `Today, ${time}`; if (sameDay(d, yesterday)) return `Yesterday, ${time}`; return `${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })}, ${time}`; } // Full, unambiguous timestamp for the detail dialog. function formatFullTime(iso: string): string { return new Date(iso).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short", }); } function Kpi({ label, value, icon: Icon, }: { label: string; value: string; icon: LucideIcon; }) { return (
{label} {value}
); } function DetailRow({ label, value }: { label: string; value: string }) { return (
{label} {value}
); } // Entries shown per page in the activity feed before paginating. const PAGE_SIZE = 10; export function ActivityView() { const { t } = useTranslation(); const [entries, setEntries] = useState([]); const [selected, setSelected] = useState(null); const [page, setPage] = useState(1); useEffect(() => { let active = true; listActivity() .then((data) => { if (active) setEntries(data); }) .catch(() => { /* api-client redirects on 401; otherwise leave the feed empty */ }); return () => { active = false; }; }, []); const kpis = useMemo(() => { const now = new Date(); const startOfToday = new Date( now.getFullYear(), now.getMonth(), now.getDate(), ); const startOfWeek = new Date(startOfToday); startOfWeek.setDate(startOfToday.getDate() - now.getDay()); const today = entries.filter((e) => new Date(e.createdAt) >= startOfToday); const week = entries.filter((e) => new Date(e.createdAt) >= startOfWeek); return [ { label: t("activity.changesToday"), value: String(today.length), icon: ActivityIcon, }, { label: t("activity.thisWeek"), value: String(week.length), icon: CalendarDays }, { label: t("activity.totalRecorded"), value: String(entries.length), icon: Hash, }, ]; }, [entries, t]); // Client-side pagination over the feed (10/page). `page` is clamped at render // so a shrinking feed never leaves us past the last page. const totalPages = Math.max(1, Math.ceil(entries.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const pageRows = entries.slice( (safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE, ); return (

{t("activity.title")}

{t("activity.subtitle")}

{kpis.map((k) => ( ))}
{entries.length === 0 ? (
{t("activity.empty")}
) : (
    {pageRows.map((entry, i) => { const Icon = entityIcon[entry.entityType] ?? FileText; const isLast = i === pageRows.length - 1; const context = [ entry.actorName, entry.patientName && `${entry.patientName}${ entry.patientFileNumber ? ` (#${entry.patientFileNumber})` : "" }`, ] .filter(Boolean) .join(" ยท "); return (
  1. {!isLast &&
    }
  2. ); })}
)} !o && setSelected(null)} open={selected !== null} > {t("activity.detail.title")} {selected?.action} {selected?.patientName && ( )} {selected?.entityId && !selected?.patientFileNumber && ( )} }> {t("activity.detail.close")}
); }