mirror of
https://github.com/temetro/temetro.git
synced 2026-08-17 13:57:48 +00:00
feat: edit parsed records before importing
The import preview only showed counts + skipped-row errors with no way to fix anything. Now every parsed record is editable before import: - backend: shared validatePatientImport() (used by the previewImport tool) + POST /api/ai/import/validate for dry-run re-validation; the import preview payload now carries the original records (and the source record on each invalid row) so the UI can edit them. - frontend: the import card gets a "Review & edit" dialog listing every record with a ready / needs-fixing badge; opening one reuses the full PatientFormDialog in a new non-persisting review mode (editable file number) and re-validates on save, so skipped rows can be fixed and included. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
saveAiConfig,
|
||||
toAiConfig,
|
||||
} from "../services/ai/config.js";
|
||||
import { validatePatientImport } from "../services/ai/import.js";
|
||||
import { getPolicy, savePolicy } from "../services/ai/policy.js";
|
||||
import * as patients from "../services/patients.js";
|
||||
|
||||
@@ -189,3 +190,27 @@ aiRouter.post(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// --- Migration import re-validation (dry run) -------------------------------
|
||||
// Powers the "review & edit before import" UI: the client edits parsed records
|
||||
// and calls this to refresh which are ready vs. need fixing. Writes nothing.
|
||||
aiRouter.post(
|
||||
"/import/validate",
|
||||
requireAuth,
|
||||
requireOrg,
|
||||
requirePermission({ patient: ["write"] }),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const records = (req.body as { records?: unknown[] }).records;
|
||||
if (!Array.isArray(records)) {
|
||||
throw new HttpError(400, "records must be an array.");
|
||||
}
|
||||
if (records.length > 500) {
|
||||
throw new HttpError(400, "Too many records (max 500).");
|
||||
}
|
||||
res.json(validatePatientImport(records));
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { patientInputSchema } from "../../lib/patient-validation.js";
|
||||
|
||||
// Result of a dry-run validation of parsed patient records. `valid` holds the
|
||||
// normalized, ready-to-commit records; `invalid` keeps the *original* record
|
||||
// alongside its errors so the clinician can edit and re-validate it in the UI.
|
||||
export type ImportValidation = {
|
||||
valid: unknown[];
|
||||
invalid: { index: number; errors: string[]; record: unknown }[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
// Validate parsed patient records against the (tolerant) patient schema without
|
||||
// writing anything. Shared by the chat `previewImport` tool and the
|
||||
// re-validation endpoint the edit-before-import UI calls.
|
||||
export function validatePatientImport(records: unknown[]): ImportValidation {
|
||||
const valid: unknown[] = [];
|
||||
const invalid: ImportValidation["invalid"] = [];
|
||||
records.forEach((record, index) => {
|
||||
const parsed = patientInputSchema.safeParse(record);
|
||||
if (parsed.success) {
|
||||
valid.push(parsed.data);
|
||||
} else {
|
||||
invalid.push({
|
||||
index,
|
||||
errors: parsed.error.issues.map(
|
||||
(i) => `${i.path.join(".") || "(root)"}: ${i.message}`,
|
||||
),
|
||||
record,
|
||||
});
|
||||
}
|
||||
});
|
||||
return { valid, invalid, total: records.length };
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { appointmentInputSchema } from "../../lib/appointment-validation.js";
|
||||
import { initialsFromName } from "../../lib/initials.js";
|
||||
import { inventoryInputSchema } from "../../lib/inventory-validation.js";
|
||||
import { invoiceInputSchema } from "../../lib/invoice-validation.js";
|
||||
import { patientInputSchema } from "../../lib/patient-validation.js";
|
||||
import { validatePatientImport } from "./import.js";
|
||||
import { prescriptionInputSchema } from "../../lib/prescription-validation.js";
|
||||
import { taskInputSchema } from "../../lib/task-validation.js";
|
||||
import * as analytics from "../analytics.js";
|
||||
@@ -620,30 +620,18 @@ export function createChatTools(ctx: ToolContext) {
|
||||
}),
|
||||
execute: async ({ records }) => {
|
||||
step(`Validating ${records.length} record(s)`);
|
||||
const valid: unknown[] = [];
|
||||
const invalid: { index: number; errors: string[] }[] = [];
|
||||
records.forEach((rec, index) => {
|
||||
const parsed = patientInputSchema.safeParse(rec);
|
||||
if (parsed.success) {
|
||||
valid.push(parsed.data);
|
||||
} else {
|
||||
invalid.push({
|
||||
index,
|
||||
errors: parsed.error.issues.map(
|
||||
(i) => `${i.path.join(".") || "(root)"}: ${i.message}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
// Hand the validated, ready-to-commit set to the UI for an approval
|
||||
// card. The client posts these back to /api/ai/import on approval.
|
||||
const { valid, invalid, total } = validatePatientImport(records);
|
||||
// Hand the validated set + the raw records to the UI for an approval
|
||||
// card. The client can edit any record, re-validate, and posts the valid
|
||||
// set back to /api/ai/import on approval. `records` carries the originals
|
||||
// so invalid rows are editable.
|
||||
writer.write({
|
||||
type: "data-importPreview",
|
||||
data: { valid, invalid, total: records.length },
|
||||
data: { records, valid, invalid, total },
|
||||
});
|
||||
step(`${valid.length} ready, ${invalid.length} skipped`);
|
||||
return {
|
||||
total: records.length,
|
||||
total,
|
||||
validCount: valid.length,
|
||||
invalidCount: invalid.length,
|
||||
invalid,
|
||||
|
||||
Reference in New Issue
Block a user