mirror of
https://github.com/temetro/temetro.git
synced 2026-08-12 03:26:53 +00:00
3aa699aefe
real Live card, persistent chat history Analytics & earnings: - Analytics now carries real money computed from invoices (billed/paid/ outstanding + by-month); new Earnings section on the Analysis page drawn with the project's Bklit chart components (shared EarningsChart). AI agent reaches the whole clinic: - new read tools getClinicInfo / getAnalytics / listInventory render clinic, analytics (with a Bklit earnings chart) and inventory cards in chat - proposeInvoice turns an uploaded purchase/medication list into an invoice (new "invoice" action-preview kind → createInvoice); invoices/appointments auto-create/link a patient (ensurePatient) so they hit the Patients page Live card: - plots real data — patients checked in today — via GET /api/analytics/live (polled); value pill clamped and margins widened so nothing spills the card Persistent AI chat history (Claude-style): - ai_chat_threads + ai_chat_messages (migration 0016); per-user, org-scoped thread CRUD under /api/chat/threads - chat panel owns a thread id, loads /?thread=<id>, and auto-saves after each exchange; sidebar lists past chats (open/delete), "New chat" starts fresh Verified with backend typecheck + frontend tsc + next build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import { Boxes } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card } from "@/components/ui/card";
|
|
import type { InventoryItem } from "@/lib/inventory";
|
|
|
|
// Read-only inventory card the agent shows for listInventory; low-stock items
|
|
// (at or below their reorder threshold) get a destructive badge.
|
|
export function InventoryListCard({ items }: { items: InventoryItem[] }) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<Card className="w-full gap-0 overflow-hidden p-0">
|
|
<div className="flex items-center gap-2 border-b px-4 py-3">
|
|
<Boxes className="size-4 text-muted-foreground" />
|
|
<span className="font-medium text-sm">{t("chat.lists.inventory")}</span>
|
|
<Badge className="ml-auto" variant="secondary">
|
|
{items.length}
|
|
</Badge>
|
|
</div>
|
|
{items.length === 0 ? (
|
|
<p className="px-4 py-6 text-center text-muted-foreground text-sm">
|
|
{t("chat.lists.noInventory")}
|
|
</p>
|
|
) : (
|
|
<div className="max-h-72 divide-y divide-border overflow-y-auto">
|
|
{items.map((it) => {
|
|
const low = it.stockQuantity <= it.reorderThreshold;
|
|
return (
|
|
<div className="flex items-center gap-3 px-4 py-2.5" key={it.id}>
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate font-medium text-foreground text-sm">
|
|
{it.name}
|
|
</span>
|
|
<span className="truncate text-muted-foreground text-xs">
|
|
{[it.strength, it.form].filter(Boolean).join(" · ")}
|
|
</span>
|
|
</div>
|
|
<Badge variant={low ? "destructive" : "outline"}>
|
|
{it.stockQuantity} {it.unit}
|
|
</Badge>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|