mirror of
https://github.com/temetro/temetro.git
synced 2026-08-19 22:56:17 +00:00
backend: org-scoped pharmacy inventory model
Add an `inventory` resource mirroring the prescriptions feature end-to-end: Drizzle table (org-scoped, indexed), domain type, zod validation, service (list/get/create/update/delete), and an RBAC-gated CRUD router mounted at /api/inventory. Grant the new `inventory` statement to roles — pharmacy gets read/write, full clinicians read/write/delete, reception/lab none — in both the backend access control and (mirrored) the frontend. Record writes in the activity log via a new `inventory` entity type. Includes the migration and an idempotent demo seed script. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ export const statements = {
|
||||
patient: ["read", "write", "delete"],
|
||||
appointment: ["read", "write", "delete"],
|
||||
prescription: ["read", "write", "delete"],
|
||||
inventory: ["read", "write", "delete"],
|
||||
task: ["read", "write", "delete"],
|
||||
lab: ["read", "write"],
|
||||
} as const;
|
||||
@@ -39,6 +40,7 @@ export const owner = ac.newRole({
|
||||
patient: ["read", "write", "delete"],
|
||||
appointment: ["read", "write", "delete"],
|
||||
prescription: ["read", "write", "delete"],
|
||||
inventory: ["read", "write", "delete"],
|
||||
task: ["read", "write", "delete"],
|
||||
lab: ["read", "write"],
|
||||
});
|
||||
@@ -48,6 +50,7 @@ export const admin = ac.newRole({
|
||||
patient: ["read", "write", "delete"],
|
||||
appointment: ["read", "write", "delete"],
|
||||
prescription: ["read", "write", "delete"],
|
||||
inventory: ["read", "write", "delete"],
|
||||
task: ["read", "write", "delete"],
|
||||
lab: ["read", "write"],
|
||||
});
|
||||
@@ -58,6 +61,7 @@ export const member = ac.newRole({
|
||||
patient: ["read", "write"],
|
||||
appointment: ["read", "write", "delete"],
|
||||
prescription: ["read", "write", "delete"],
|
||||
inventory: ["read", "write", "delete"],
|
||||
task: ["read", "write", "delete"],
|
||||
lab: ["read", "write"],
|
||||
});
|
||||
@@ -70,6 +74,7 @@ export const doctor = ac.newRole({
|
||||
patient: ["read", "write"],
|
||||
appointment: ["read", "write", "delete"],
|
||||
prescription: ["read", "write", "delete"],
|
||||
inventory: ["read", "write", "delete"],
|
||||
task: ["read", "write", "delete"],
|
||||
lab: ["read", "write"],
|
||||
});
|
||||
@@ -96,6 +101,7 @@ export const pharmacy = ac.newRole({
|
||||
patient: ["read"],
|
||||
appointment: ["read"],
|
||||
prescription: ["read", "write"],
|
||||
inventory: ["read", "write"],
|
||||
task: ["read", "write"],
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const nonEmpty = z.string().trim().min(1);
|
||||
|
||||
// Payload accepted by POST/PUT /api/inventory. Only the medication name is
|
||||
// required; quantities default to 0 and the descriptive fields to empty strings
|
||||
// so a quick add (name only) is valid.
|
||||
export const inventoryInputSchema = z.object({
|
||||
name: nonEmpty.max(200),
|
||||
form: z.string().trim().max(120).default(""),
|
||||
strength: z.string().trim().max(120).default(""),
|
||||
unit: z.string().trim().max(60).default(""),
|
||||
stockQuantity: z.number().int().min(0).max(1_000_000).default(0),
|
||||
reorderThreshold: z.number().int().min(0).max(1_000_000).default(0),
|
||||
location: z.string().trim().max(200).default(""),
|
||||
expiresAt: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD.")
|
||||
.nullish(),
|
||||
notes: z.string().max(5000).nullish(),
|
||||
});
|
||||
|
||||
export type InventoryInput = z.infer<typeof inventoryInputSchema>;
|
||||
Reference in New Issue
Block a user