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:
Khalid Abdi
2026-06-14 22:25:15 +03:00
parent b946dc9226
commit 3aa699aefe
27 changed files with 4274 additions and 61 deletions
+50
View File
@@ -0,0 +1,50 @@
import {
index,
integer,
jsonb,
pgTable,
text,
timestamp,
} from "drizzle-orm/pg-core";
import { organization, user } from "./auth.js";
// Persisted AI-chat threads (Claude-style history), per user within a clinic.
// The thread id is client-generated (nanoid) so the frontend can start saving a
// fresh chat without a round-trip. Messages store the full UIMessage `parts`
// (text + custom record cards) as JSONB so a reopened thread renders exactly as
// it did live.
export const aiChatThreads = pgTable(
"ai_chat_threads",
{
id: text("id").primaryKey(),
organizationId: text("organization_id")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
title: text("title").notNull().default("New chat"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(t) => [index("ai_threads_org_user_idx").on(t.organizationId, t.userId)],
);
export const aiChatMessages = pgTable(
"ai_chat_messages",
{
id: text("id").primaryKey(),
threadId: text("thread_id")
.notNull()
.references(() => aiChatThreads.id, { onDelete: "cascade" }),
position: integer("position").notNull(),
role: text("role").notNull(),
parts: jsonb("parts").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(t) => [index("ai_messages_thread_idx").on(t.threadId, t.position)],
);
+1
View File
@@ -11,3 +11,4 @@ export * from "./messaging.js";
export * from "./notifications.js";
export * from "./settings.js";
export * from "./ai.js";
export * from "./ai-chat.js";