mirror of
https://github.com/temetro/temetro.git
synced 2026-08-10 10:37:43 +00:00
feat: delete controls across add-pages (lab, dispense, rx, inventory, tasks)
Backend: - DELETE /api/patients/:fileNumber/labs (remove one lab result, lab:write) - DELETE /api/dispenses/:id (void a ledger entry, inventory:write) Frontend (all guarded by ConfirmDialog): - lab "Recent results" rows: delete result - pharmacy "Recently dispensed" rows: delete record - prescriptions/tasks detail sheets + inventory detail dialog gain a delete action (prescription delete stays off the shared Pharmacy sheet) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import { dispenseInputSchema } from "../lib/dispense-validation.js";
|
||||
import { HttpError } from "../lib/http-error.js";
|
||||
import {
|
||||
requireAuth,
|
||||
requireOrg,
|
||||
@@ -53,3 +54,27 @@ dispensesRouter.post(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
dispensesRouter.delete(
|
||||
"/:id",
|
||||
requirePermission({ inventory: ["write"] }),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const ok = await service.deleteDispense(
|
||||
req.organizationId!,
|
||||
req.params.id as string,
|
||||
);
|
||||
if (!ok) throw new HttpError(404, "Dispense not found.");
|
||||
await recordActivity({
|
||||
orgId: req.organizationId!,
|
||||
actor: { id: req.user!.id, name: req.user!.name },
|
||||
action: "Voided a dispense record",
|
||||
entityType: "dispense",
|
||||
entityId: req.params.id as string,
|
||||
});
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -28,6 +28,12 @@ const labsAppendSchema = z.object({
|
||||
labs: z.array(labSchema).min(1).max(50),
|
||||
});
|
||||
|
||||
const labDeleteSchema = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
value: z.string(),
|
||||
takenAt: z.string(),
|
||||
});
|
||||
|
||||
// Notify the rest of the clinic about a patient record change (best-effort,
|
||||
// pushed live over the socket).
|
||||
async function notifyClinic(
|
||||
@@ -255,6 +261,36 @@ patientsRouter.post(
|
||||
},
|
||||
);
|
||||
|
||||
// Remove one lab result. Gated by `lab:write` like appending, so lab staff can
|
||||
// correct their own submissions without patient-edit rights.
|
||||
patientsRouter.delete(
|
||||
"/:fileNumber/labs",
|
||||
requirePermission({ lab: ["write"] }),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const match = labDeleteSchema.parse(req.body);
|
||||
const updated = await service.deleteLab(
|
||||
req.organizationId!,
|
||||
req.params.fileNumber as string,
|
||||
match,
|
||||
);
|
||||
if (!updated) throw new HttpError(404, "Patient not found.");
|
||||
await recordActivity({
|
||||
orgId: req.organizationId!,
|
||||
actor: { id: req.user!.id, name: req.user!.name },
|
||||
action: `Removed lab result ${match.name} for ${updated.name}`,
|
||||
entityType: "patient",
|
||||
entityId: updated.fileNumber,
|
||||
patientName: updated.name,
|
||||
patientFileNumber: updated.fileNumber,
|
||||
});
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
patientsRouter.delete(
|
||||
"/:fileNumber",
|
||||
requirePermission({ patient: ["delete"] }),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
|
||||
import { db } from "../db/index.js";
|
||||
import { dispenses } from "../db/schema/dispenses.js";
|
||||
@@ -59,3 +59,14 @@ export async function createDispense(
|
||||
.returning();
|
||||
return toDispense(row!);
|
||||
}
|
||||
|
||||
export async function deleteDispense(
|
||||
orgId: string,
|
||||
id: string,
|
||||
): Promise<boolean> {
|
||||
const deleted = await db
|
||||
.delete(dispenses)
|
||||
.where(and(eq(dispenses.organizationId, orgId), eq(dispenses.id, id)))
|
||||
.returning({ id: dispenses.id });
|
||||
return deleted.length > 0;
|
||||
}
|
||||
|
||||
@@ -566,6 +566,41 @@ export async function appendLabs(
|
||||
return getPatient(orgId, fileNumber);
|
||||
}
|
||||
|
||||
// Remove a single lab result from a patient, identified by its
|
||||
// name/value/takenAt (the frontend has no row id). Scoped to the org via the
|
||||
// owning patient. Returns the reloaded patient, or null when the chart is gone.
|
||||
export async function deleteLab(
|
||||
orgId: string,
|
||||
fileNumber: string,
|
||||
match: { name: string; value: string; takenAt: string },
|
||||
): Promise<Patient | null> {
|
||||
const [existing] = await db
|
||||
.select({ id: patients.id })
|
||||
.from(patients)
|
||||
.where(
|
||||
and(
|
||||
eq(patients.organizationId, orgId),
|
||||
eq(patients.fileNumber, fileNumber),
|
||||
),
|
||||
);
|
||||
if (!existing) return null;
|
||||
await db
|
||||
.delete(labs)
|
||||
.where(
|
||||
and(
|
||||
eq(labs.patientId, existing.id),
|
||||
eq(labs.name, match.name),
|
||||
eq(labs.value, match.value),
|
||||
eq(labs.takenAt, match.takenAt),
|
||||
),
|
||||
);
|
||||
await db
|
||||
.update(patients)
|
||||
.set({ updatedAt: new Date() })
|
||||
.where(eq(patients.id, existing.id));
|
||||
return getPatient(orgId, fileNumber);
|
||||
}
|
||||
|
||||
export async function deletePatient(
|
||||
orgId: string,
|
||||
fileNumber: string,
|
||||
|
||||
Reference in New Issue
Block a user