fix: chat input clipping, AI appointments create patients, batched approval

1. Chat input toolbar was clipped in the empty state: the form's overflow-hidden
   made its flex min-height resolve to 0, so the vertically-centered layout
   squeezed it and hid the bottom toolbar (attach/add-patient/model/mic/send).
   Add `shrink-0` to the form; let the empty-state column scroll.
2. AI-imported appointments now create/link a patient: services.ensurePatient
   reuses a same-name patient or creates one (auto file number, source "ai");
   appointments.createAppointment calls it when the booking has no file number,
   so imported people appear on the Patients page.
3. Many proposals collapse into one BatchActionPreviewCard → a review dialog
   with per-row remove and "Add all" (commits sequentially), instead of one
   Add/Discard card per record.

Plus: widen the Live chart right margin so the value pill stays inside the card.
Verified with `next build`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-14 21:23:25 +03:00
parent 8c1f520047
commit b946dc9226
8 changed files with 304 additions and 12 deletions
+9 -1
View File
@@ -4,6 +4,7 @@ import { db } from "../db/index.js";
import { appointments } from "../db/schema/appointments.js";
import type { AppointmentInput } from "../lib/appointment-validation.js";
import type { Appointment } from "../types/appointment.js";
import * as patients from "./patients.js";
type AppointmentRow = typeof appointments.$inferSelect;
@@ -58,9 +59,16 @@ export async function createAppointment(
userId: string,
input: AppointmentInput,
): Promise<Appointment> {
// Link to a patient — creating one when the booking has no file number (e.g.
// an AI-imported appointment), so the person shows up on the Patients page.
const fileNumber = await patients.ensurePatient(orgId, userId, {
fileNumber: input.fileNumber,
name: input.name,
initials: input.initials,
});
const [row] = await db
.insert(appointments)
.values(columns(orgId, input, userId))
.values(columns(orgId, { ...input, fileNumber }, userId))
.returning();
return toAppointment(row!);
}
+38 -1
View File
@@ -11,7 +11,10 @@ import {
problems,
} from "../db/schema/patients.js";
import { HttpError } from "../lib/http-error.js";
import type { PatientInput } from "../lib/patient-validation.js";
import {
patientInputSchema,
type PatientInput,
} from "../lib/patient-validation.js";
import type {
Allergy,
Encounter,
@@ -314,6 +317,40 @@ export async function generateFileNumber(orgId: string): Promise<string> {
return String(Number(r?.max ?? 9999) + 1);
}
// Resolve the file number to attach a denormalized record (e.g. an appointment)
// to a patient. With a file number, use it as-is. Without one (AI-imported rows),
// reuse an existing same-name patient when present — deduping repeat rows and
// re-imports — otherwise create a minimal patient (auto file number, source "ai")
// so they appear on the Patients page. Returns the file number to link.
export async function ensurePatient(
orgId: string,
userId: string,
patient: { fileNumber: string; name: string; initials: string },
): Promise<string> {
if (patient.fileNumber) return patient.fileNumber;
const [existing] = await db
.select({ fileNumber: patients.fileNumber })
.from(patients)
.where(
and(
eq(patients.organizationId, orgId),
eq(patients.name, patient.name),
),
)
.limit(1);
if (existing) return existing.fileNumber;
const created = await createPatient(
orgId,
userId,
patientInputSchema.parse({
name: patient.name,
initials: patient.initials,
source: "ai",
}),
);
return created.fileNumber;
}
export async function listPatients(
orgId: string,
demographicsOnly = false,