Enhance contract clearance features with review timestamps and staff identifiers; improve file handling in controllers and views for better user experience.

This commit is contained in:
Marshal
2026-06-27 19:30:42 +00:00
parent 0ab553bf48
commit 7a2a55d94b
8 changed files with 282 additions and 123 deletions

View File

@@ -18,6 +18,10 @@ export interface ContractClearanceDocument {
file: { id: string; name: string; url: string } | null;
reviewStatus: ContractDocReviewStatus | null;
note: string | null;
/** When the review decision (approve/query) was recorded. */
reviewedAt: string | null;
/** Staff id that recorded the decision (no user directory to resolve names). */
reviewedByStaffId: string | null;
}
export interface ContractClearanceView {
@@ -77,6 +81,8 @@ export class ContractClearanceService {
file: file ? { id: file.id, name: file.name, url: file.url } : null,
reviewStatus: review?.status ?? null,
note: review?.note ?? null,
reviewedAt: review?.reviewedAt ? review.reviewedAt.toISOString() : null,
reviewedByStaffId: review?.reviewedByStaffId ?? null,
});
}
};
@@ -97,6 +103,8 @@ export class ContractClearanceService {
file: { id: f.id, name: f.name, url: f.url },
reviewStatus: review?.status ?? null,
note: review?.note ?? null,
reviewedAt: review?.reviewedAt ? review.reviewedAt.toISOString() : null,
reviewedByStaffId: review?.reviewedByStaffId ?? null,
});
}

View File

@@ -1,5 +1,12 @@
import { Controller, Get, Param, ParseUUIDPipe, Res } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import {
Controller,
Get,
Param,
ParseUUIDPipe,
Query,
Res,
} from "@nestjs/common";
import { ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger";
import { Response } from "express";
import { FilesService } from "./files.service";
@@ -11,18 +18,34 @@ export class FilesController {
@Get(":fileId")
@ApiOperation({
summary: "Download a file by ID",
summary: "Stream a file by ID",
description:
"Global endpoint — streams any uploaded file directly from MinIO by its UUID. " +
"No resource context (e.g. booking ID) required.",
"No resource context (e.g. booking ID) required. Serves inline by default so " +
"the browser can preview it; pass ?download=1 to force a download.",
})
@ApiQuery({
name: "download",
required: false,
description: "Set to 1/true to force a download instead of inline preview.",
})
async download(
@Param("fileId", ParseUUIDPipe) fileId: string,
@Query("download") download: string | undefined,
@Res() res: Response,
) {
const { stream, record } = await this.filesService.streamById(fileId);
const forceDownload = download === "1" || download === "true";
const disposition = forceDownload ? "attachment" : "inline";
res.setHeader("Content-Type", record.mimeType);
res.setHeader("Content-Disposition", `attachment; filename="${record.name}"`);
res.setHeader(
"Content-Disposition",
`${disposition}; filename="${record.name}"`,
);
// Allow the browser to cache the streamed bytes briefly for smoother
// in-page previews (re-opening the viewer shouldn't re-hit MinIO).
res.setHeader("Cache-Control", "private, max-age=300");
stream.pipe(res);
}
}