mirror of
https://github.com/temetro/temetro.git
synced 2026-08-09 18:20:00 +00:00
b1abb29108
Add a real file-storage layer to the backend and wire upload UI into the frontend. backend: - new `attachments` table (org-scoped, links to a patient file number and optionally a lab result) + Drizzle migration - `/api/attachments` route: upload (multer → disk under UPLOAD_DIR), list, stream/download, delete; gated by patient:write OR lab:write via a new requireAnyPermission helper so lab staff can attach analyses - UPLOAD_DIR env (default ./uploads) + a persistent docker volume frontend: - lib/attachments.ts client (multipart upload, list, delete, preview URL) - staged file picker in the patient Add/Edit dialog (uploaded after save) and the lab Add-result dialog (linked to the result) - a Files section in the patient sheet that lists attachments and opens them in a preview dialog (images inline, others via download) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
43 lines
1.3 KiB
TypeScript
43 lines
1.3 KiB
TypeScript
import {
|
|
index,
|
|
integer,
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
uuid,
|
|
} from "drizzle-orm/pg-core";
|
|
|
|
import { organization, user } from "./auth.js";
|
|
|
|
// Files uploaded against a patient record (and optionally a specific lab
|
|
// result). Scoped to a clinic (organization). The bytes live on disk under
|
|
// UPLOAD_DIR (see src/services/attachments.ts); this table only holds metadata
|
|
// plus the relative `storagePath`.
|
|
// fileNumber → the patient's MRN this file belongs to.
|
|
// labKey → set when the file documents a specific lab result.
|
|
export const attachments = pgTable(
|
|
"attachments",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
organizationId: text("organization_id")
|
|
.notNull()
|
|
.references(() => organization.id, { onDelete: "cascade" }),
|
|
fileNumber: text("file_number"),
|
|
labKey: text("lab_key"),
|
|
filename: text("filename").notNull(),
|
|
mimeType: text("mime_type").notNull(),
|
|
sizeBytes: integer("size_bytes").notNull(),
|
|
storagePath: text("storage_path").notNull(),
|
|
uploadedByUserId: text("uploaded_by_user_id").references(() => user.id, {
|
|
onDelete: "set null",
|
|
}),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
},
|
|
(table) => [
|
|
index("attachments_org_file_idx").on(
|
|
table.organizationId,
|
|
table.fileNumber,
|
|
),
|
|
],
|
|
);
|