mirror of
https://github.com/temetro/temetro.git
synced 2026-08-20 15:12:18 +00:00
feat: clinic-wide AI (analytics/earnings/inventory + invoice-from-file),
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>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import type { TemetroUIMessage } from "@/lib/ai-chat";
|
||||
import { apiFetch } from "@/lib/api-client";
|
||||
|
||||
// Persisted AI-chat threads (Claude-style history). Threads are per-user within
|
||||
// the active clinic; the thread id is generated on the client.
|
||||
export type ThreadSummary = { id: string; title: string; updatedAt: string };
|
||||
|
||||
type StoredMessage = { role: string; parts: unknown };
|
||||
|
||||
export function listThreads(): Promise<ThreadSummary[]> {
|
||||
return apiFetch<ThreadSummary[]>("/api/chat/threads");
|
||||
}
|
||||
|
||||
export function getThread(
|
||||
id: string,
|
||||
): Promise<{ id: string; title: string; messages: StoredMessage[] }> {
|
||||
return apiFetch(`/api/chat/threads/${id}`);
|
||||
}
|
||||
|
||||
export function saveThread(
|
||||
id: string,
|
||||
messages: TemetroUIMessage[],
|
||||
title: string,
|
||||
): Promise<{ ok: boolean }> {
|
||||
return apiFetch(`/api/chat/threads/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
messages: messages.map((m) => ({ role: m.role, parts: m.parts })),
|
||||
title,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteThread(id: string): Promise<void> {
|
||||
return apiFetch<void>(`/api/chat/threads/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
// Fired after a thread is saved/deleted so the sidebar history can refresh.
|
||||
export const THREADS_CHANGED_EVENT = "temetro:threads-changed";
|
||||
export function notifyThreadsChanged() {
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent(THREADS_CHANGED_EVENT));
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -1,6 +1,8 @@
|
||||
import type { UIMessage } from "ai";
|
||||
|
||||
import type { Analytics } from "@/lib/analytics";
|
||||
import type { Appointment } from "@/lib/appointments";
|
||||
import type { InventoryItem } from "@/lib/inventory";
|
||||
import type { Lab, Patient, Trend } from "@/lib/patients";
|
||||
import type { Prescription } from "@/lib/prescriptions";
|
||||
import type { Task } from "@/lib/tasks";
|
||||
@@ -39,10 +41,22 @@ export type StepData = {
|
||||
export type AppointmentListData = { appointments: Appointment[] };
|
||||
export type TaskListData = { tasks: Task[] };
|
||||
export type PrescriptionListData = { prescriptions: Prescription[] };
|
||||
export type InventoryListData = { items: InventoryItem[] };
|
||||
|
||||
// Clinic-wide read cards.
|
||||
export type ClinicCardData = {
|
||||
name: string;
|
||||
slug: string | null;
|
||||
createdAt: string | null;
|
||||
};
|
||||
|
||||
// An add proposed by the agent, awaiting one-click clinician approval. `record`
|
||||
// is the validated, ready-to-commit input for the matching create endpoint.
|
||||
export type ActionPreviewKind = "appointment" | "task" | "prescription";
|
||||
export type ActionPreviewKind =
|
||||
| "appointment"
|
||||
| "task"
|
||||
| "prescription"
|
||||
| "invoice";
|
||||
export type ActionPreviewData = {
|
||||
token: string;
|
||||
kind: ActionPreviewKind;
|
||||
@@ -61,6 +75,9 @@ export type TemetroDataParts = {
|
||||
appointmentList: AppointmentListData;
|
||||
taskList: TaskListData;
|
||||
prescriptionList: PrescriptionListData;
|
||||
inventoryList: InventoryListData;
|
||||
clinicCard: ClinicCardData;
|
||||
analyticsCard: Analytics;
|
||||
actionPreview: ActionPreviewData;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { apiFetch } from "@/lib/api-client";
|
||||
// Server-computed clinic analytics. Mirrors the backend `src/types/analytics.ts`.
|
||||
// All figures are aggregates over the active clinic's real data.
|
||||
export type TrendPoint = { label: string; count: number };
|
||||
export type EarningsPoint = { label: string; billed: number; paid: number };
|
||||
|
||||
export type Analytics = {
|
||||
patients: {
|
||||
@@ -24,6 +25,12 @@ export type Analytics = {
|
||||
open: number;
|
||||
done: number;
|
||||
};
|
||||
earnings: {
|
||||
totalBilled: number;
|
||||
totalPaid: number;
|
||||
totalOutstanding: number;
|
||||
byMonth: EarningsPoint[];
|
||||
};
|
||||
trends: {
|
||||
patientsByMonth: TrendPoint[];
|
||||
appointmentsByWeekday: TrendPoint[];
|
||||
@@ -33,3 +40,9 @@ export type Analytics = {
|
||||
export function getAnalytics(): Promise<Analytics> {
|
||||
return apiFetch<Analytics>("/api/analytics");
|
||||
}
|
||||
|
||||
// Current "in the building now" count — checked-in appointments today. Polled by
|
||||
// the Analysis Live card.
|
||||
export function getLiveMetric(): Promise<{ value: number }> {
|
||||
return apiFetch<{ value: number }>("/api/analytics/live");
|
||||
}
|
||||
|
||||
@@ -646,6 +646,14 @@
|
||||
"newThisMonth": "New this month",
|
||||
"active": "Active patients"
|
||||
},
|
||||
"earnings": {
|
||||
"title": "Earnings",
|
||||
"description": "Billed, paid and outstanding — from invoices",
|
||||
"billed": "Total billed",
|
||||
"paid": "Total paid",
|
||||
"outstanding": "Outstanding",
|
||||
"byMonth": "Billed vs paid, by month"
|
||||
},
|
||||
"appointments": {
|
||||
"title": "Appointments & schedule",
|
||||
"description": "Bookings, attendance and what's coming up",
|
||||
@@ -773,6 +781,12 @@
|
||||
"thinking": "Thinking…",
|
||||
"steps": "Steps",
|
||||
"reasoning": "Reasoning",
|
||||
"history": {
|
||||
"title": "Chats",
|
||||
"untitled": "New chat",
|
||||
"empty": "No saved chats yet.",
|
||||
"delete": "Delete chat"
|
||||
},
|
||||
"suggestions": {
|
||||
"schedule": "Show today's schedule",
|
||||
"tasks": "List open tasks",
|
||||
@@ -795,12 +809,14 @@
|
||||
"title": {
|
||||
"appointment": "Proposed appointment",
|
||||
"task": "Proposed task",
|
||||
"prescription": "Proposed prescription"
|
||||
"prescription": "Proposed prescription",
|
||||
"invoice": "Proposed invoice"
|
||||
},
|
||||
"kind": {
|
||||
"appointment": "Appointment added.",
|
||||
"task": "Task added.",
|
||||
"prescription": "Prescription added."
|
||||
"prescription": "Prescription added.",
|
||||
"invoice": "Invoice added."
|
||||
},
|
||||
"approve": "Add",
|
||||
"adding": "Adding…",
|
||||
@@ -832,7 +848,24 @@
|
||||
"done": "Done",
|
||||
"noAppointments": "No appointments.",
|
||||
"noTasks": "No tasks.",
|
||||
"noPrescriptions": "No prescriptions."
|
||||
"noPrescriptions": "No prescriptions.",
|
||||
"inventory": "Inventory",
|
||||
"noInventory": "No inventory items."
|
||||
},
|
||||
"clinicCard": {
|
||||
"title": "Clinic",
|
||||
"since": "Since {{date}}"
|
||||
},
|
||||
"analyticsCard": {
|
||||
"title": "Clinic analytics",
|
||||
"patients": "Patients",
|
||||
"appointmentsThisWeek": "Appts this week",
|
||||
"activePrescriptions": "Active Rx",
|
||||
"openTasks": "Open tasks",
|
||||
"billed": "Billed",
|
||||
"paid": "Paid",
|
||||
"outstanding": "Outstanding",
|
||||
"byMonth": "Billed vs paid, by month"
|
||||
},
|
||||
"labCard": {
|
||||
"flags": {
|
||||
|
||||
Reference in New Issue
Block a user