diff --git a/src/app/(dashboard)/jobs/[jobId]/components/job-files-panel.tsx b/src/app/(dashboard)/jobs/[jobId]/components/job-files-panel.tsx index 646a3fb..79a1718 100644 --- a/src/app/(dashboard)/jobs/[jobId]/components/job-files-panel.tsx +++ b/src/app/(dashboard)/jobs/[jobId]/components/job-files-panel.tsx @@ -254,7 +254,8 @@ function FileRow({ file }: { file: JobFileWithUrl }) { const [open, setOpen] = useState(false); const [downloadOpen, setDownloadOpen] = useState(false); const [viewerOpen, setViewerOpen] = useState(false); - const isViewable = VIEWABLE_RE.test(file.name); + const isArchived = Boolean(file.archivedAt); + const isViewable = !isArchived && VIEWABLE_RE.test(file.name); useEffect(() => { if (state.ok) { @@ -281,12 +282,20 @@ function FileRow({ file }: { file: JobFileWithUrl }) { } return ( -
  • +
  • {kindIcon(file.kind)}

    {file.name}

    {JOB_FILE_KIND_LABELS[file.kind] ?? file.kind} · {formatSize(file.size)} + {isArchived && ( + <> + {" · "} + + Arşivlendi {new Date(file.archivedAt!).toLocaleDateString("tr-TR")} + + + )}

    @@ -315,7 +324,13 @@ function FileRow({ file }: { file: JobFileWithUrl }) { )} - diff --git a/src/app/api/jobs/[jobId]/files/[fileId]/download/route.ts b/src/app/api/jobs/[jobId]/files/[fileId]/download/route.ts index e2ad199..44dd11d 100644 --- a/src/app/api/jobs/[jobId]/files/[fileId]/download/route.ts +++ b/src/app/api/jobs/[jobId]/files/[fileId]/download/route.ts @@ -46,6 +46,12 @@ export async function GET( if (ctx.tenantId !== job.clinicTenantId && ctx.tenantId !== job.labTenantId) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } + if (file.archivedAt) { + return NextResponse.json( + { error: "Dosya arşivlendi, indirilebilir kopya yok." }, + { status: 410 }, + ); + } const buf = (await storage.getFileDownload(BUCKETS.jobFiles, file.fileId)) as | ArrayBuffer diff --git a/src/lib/appwrite/job-actions.ts b/src/lib/appwrite/job-actions.ts index ebea74c..b013168 100644 --- a/src/lib/appwrite/job-actions.ts +++ b/src/lib/appwrite/job-actions.ts @@ -6,6 +6,7 @@ import { z } from "zod"; import { logAudit } from "./audit"; import { syncFinanceForJob } from "./finance-sync"; +import { archiveJobFiles } from "./job-file-archive"; import { createNotification } from "./notification-helpers"; import { calculateJobPriceForProsthetic } from "./pricing"; import { @@ -564,6 +565,9 @@ export async function markDeliveredAction( jobId, message: `Hasta ${job.patientCode} işi teslim alındı.`, }); + // Free up Storage now that the case is closed. Metadata rows stay for + // the audit trail; only the binaries go. + void archiveJobFiles(jobId); } catch (e) { return { ok: false, error: appwriteError(e, "Teslim alınamadı.") }; } diff --git a/src/lib/appwrite/job-file-archive.ts b/src/lib/appwrite/job-file-archive.ts new file mode 100644 index 0000000..a69bcc8 --- /dev/null +++ b/src/lib/appwrite/job-file-archive.ts @@ -0,0 +1,48 @@ +import "server-only"; + +import { Query } from "node-appwrite"; + +import { BUCKETS, DATABASE_ID, TABLES, type JobFile } from "./schema"; +import { createAdminClient } from "./server"; + +/** + * Purge the binary scan/image/document objects backing a finished job from + * Appwrite Storage and stamp archivedAt on the corresponding rows. The row + * itself stays — the lab and clinic still need the audit trail (which file + * was uploaded, by whom, when) long after delivery. + * + * Best-effort: a single Storage error must not block the calling action. + * The function never throws. + */ +export async function archiveJobFiles(jobId: string): Promise { + const { tablesDB, storage } = createAdminClient(); + try { + const result = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.jobFiles, + queries: [Query.equal("jobId", jobId), Query.limit(500)], + }); + const rows = result.rows as unknown as JobFile[]; + const now = new Date().toISOString(); + await Promise.all( + rows.map(async (r) => { + if (r.archivedAt) return; + try { + await storage.deleteFile(BUCKETS.jobFiles, r.fileId); + } catch { + // file already gone, or storage unreachable — still flip archivedAt + // so the UI doesn't keep teasing a download button. + } + try { + await tablesDB.updateRow(DATABASE_ID, TABLES.jobFiles, r.$id, { + archivedAt: now, + }); + } catch { + // row update failed; leave it for the next call to retry. + } + }), + ); + } catch { + // List itself failed — nothing to do. + } +} diff --git a/src/lib/appwrite/schema.ts b/src/lib/appwrite/schema.ts index ade81f0..76902b0 100644 --- a/src/lib/appwrite/schema.ts +++ b/src/lib/appwrite/schema.ts @@ -137,6 +137,9 @@ export interface JobFile extends Row { name: string; size: number; mimeType?: string; + /** Set when the binary is purged from object storage after a job closes. + * The row stays for audit; downloads/previews are disabled past this point. */ + archivedAt?: string; } export interface JobStatusHistory extends Row {