mirror of
https://github.com/temetro/temetro.git
synced 2026-08-28 11:27:40 +00:00
feat: patient & lab file attachments (real backend storage)
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>
This commit is contained in:
@@ -4,6 +4,7 @@ import { CalendarIcon, Plus, RefreshCw, X } from "lucide-react";
|
||||
import { type FormEvent, type ReactNode, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { StagedFilesField } from "@/components/patients/patient-files";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
type Patient,
|
||||
updatePatient,
|
||||
} from "@/lib/patients";
|
||||
import { uploadAttachment } from "@/lib/attachments";
|
||||
import { hasClinicalAccess, useActiveRole } from "@/lib/roles";
|
||||
import { listProviders, type Provider } from "@/lib/staff";
|
||||
import { notify } from "@/lib/toast";
|
||||
@@ -217,6 +219,9 @@ export function PatientFormDialog({
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Files staged in the form, uploaded once the patient record is saved (so the
|
||||
// attachment can be linked to the file number).
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
|
||||
const [fileNumber, setFileNumber] = useState(() =>
|
||||
isEdit && patient ? patient.fileNumber : generateFileNumber()
|
||||
@@ -334,6 +339,21 @@ export function PatientFormDialog({
|
||||
const saved = isEdit
|
||||
? await updatePatient(built)
|
||||
: await createPatient(built);
|
||||
// Upload any staged files now that we have a saved file number.
|
||||
if (files.length > 0) {
|
||||
const results = await Promise.allSettled(
|
||||
files.map((file) =>
|
||||
uploadAttachment({ file, fileNumber: saved.fileNumber }),
|
||||
),
|
||||
);
|
||||
if (results.some((r) => r.status === "rejected")) {
|
||||
notify.error(
|
||||
t("patientFiles.uploadFailedTitle"),
|
||||
t("patientFiles.uploadFailedBody"),
|
||||
);
|
||||
}
|
||||
setFiles([]);
|
||||
}
|
||||
if (isEdit) {
|
||||
onSaved?.(saved);
|
||||
notify.success(
|
||||
@@ -669,6 +689,8 @@ export function PatientFormDialog({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<StagedFilesField onChange={setFiles} value={files} />
|
||||
</DialogPanel>
|
||||
|
||||
<DialogFooter className="flex-col items-stretch gap-2 sm:flex-row sm:items-center">
|
||||
|
||||
Reference in New Issue
Block a user