mirror of
https://github.com/temetro/temetro.git
synced 2026-09-04 23:05:35 +00:00
feat: AI-added records save with placeholders + "Added by AI" provenance
Stop blocking AI imports/proposals on missing non-critical fields. Records the chat agent drafts now save with safe placeholders, auto-generated file numbers, and a source="ai" marker that surfaces an "Added by AI" badge so a clinician can review/edit them later. Backend: - add `source` (manual|ai) column to patients/appointments/prescriptions (migration 0014) + canonical types, services, validation schemas - relax patient/appointment validation: empty file number allowed, demographic + type/provider/initials fall back to placeholders (initials derived from name) - patients.generateFileNumber() auto-assigns an MRN when one is missing - proposeAppointment accepts a name when no file number resolves; AI commits + /api/ai/import stamp source="ai" Frontend: - `source` on Appointment/Patient/Prescription types; AI commits send source="ai" - reusable <AiBadge> shown on the Patients table/detail and prescriptions list Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { initialsFromName } from "./initials.js";
|
||||
|
||||
const nonEmpty = z.string().trim().min(1);
|
||||
|
||||
const EMPTY_VITALS = { bp: "", hr: "", temp: "", spo2: "", takenAt: "" };
|
||||
const EMPTY_TREND = { label: "", unit: "", points: [] as number[] };
|
||||
|
||||
export const allergySchema = z.object({
|
||||
substance: nonEmpty,
|
||||
reaction: nonEmpty,
|
||||
@@ -49,28 +54,44 @@ export const trendSchema = z.object({
|
||||
|
||||
// A full patient payload — the frontend form sends the entire record on both
|
||||
// create and edit, so the same schema covers both.
|
||||
export const patientInputSchema = z.object({
|
||||
fileNumber: z.string().trim().regex(/^\d+$/, "File number must be digits"),
|
||||
name: nonEmpty,
|
||||
age: z.number().int().min(0).max(150),
|
||||
sex: z.enum(["M", "F"]),
|
||||
pcp: z.string(),
|
||||
// Optional link to the responsible clinician (user id). Empty string ⇒ null.
|
||||
primaryProviderId: z.preprocess(
|
||||
(v) => (v === "" ? null : v),
|
||||
z.string().nullable().optional(),
|
||||
),
|
||||
status: z.enum(["active", "inpatient", "discharged"]),
|
||||
initials: z.string().trim().min(1).max(4),
|
||||
allergies: z.array(allergySchema).default([]),
|
||||
alerts: z.array(z.string()).default([]),
|
||||
medications: z.array(medicationSchema).default([]),
|
||||
problems: z.array(problemSchema).default([]),
|
||||
vitals: vitalsSchema,
|
||||
vitalsTrend: trendSchema,
|
||||
labs: z.array(labSchema).default([]),
|
||||
labTrend: trendSchema,
|
||||
encounters: z.array(encounterSchema).default([]),
|
||||
});
|
||||
//
|
||||
// Tolerant by design: an AI import from a sparse export (e.g. just a name) still
|
||||
// validates. The file number may be empty (the patient service auto-generates
|
||||
// one), demographics fall back to safe placeholders, initials are derived from
|
||||
// the name, and the clinical sections default to empty. Such rows are stamped
|
||||
// `source: "ai"` and surfaced with an "Added by AI" badge for later editing.
|
||||
export const patientInputSchema = z
|
||||
.object({
|
||||
fileNumber: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^\d*$/, "File number must be digits")
|
||||
.default(""),
|
||||
name: nonEmpty,
|
||||
age: z.coerce.number().int().min(0).max(150).default(0),
|
||||
sex: z.enum(["M", "F"]).default("M"),
|
||||
pcp: z.string().default(""),
|
||||
// Optional link to the responsible clinician (user id). Empty string ⇒ null.
|
||||
primaryProviderId: z.preprocess(
|
||||
(v) => (v === "" ? null : v),
|
||||
z.string().nullable().optional(),
|
||||
),
|
||||
status: z.enum(["active", "inpatient", "discharged"]).default("active"),
|
||||
initials: z.string().trim().max(4).default(""),
|
||||
allergies: z.array(allergySchema).default([]),
|
||||
alerts: z.array(z.string()).default([]),
|
||||
medications: z.array(medicationSchema).default([]),
|
||||
problems: z.array(problemSchema).default([]),
|
||||
vitals: vitalsSchema.default(EMPTY_VITALS),
|
||||
vitalsTrend: trendSchema.default(EMPTY_TREND),
|
||||
labs: z.array(labSchema).default([]),
|
||||
labTrend: trendSchema.default(EMPTY_TREND),
|
||||
encounters: z.array(encounterSchema).default([]),
|
||||
source: z.enum(["manual", "ai"]).default("manual"),
|
||||
})
|
||||
.transform((v) => ({
|
||||
...v,
|
||||
initials: v.initials || initialsFromName(v.name),
|
||||
}));
|
||||
|
||||
export type PatientInput = z.infer<typeof patientInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user