feat(freight): non-terminal change-request review + unified customer timeline

Backoffice can now "Request changes" on a pending settings change
request without rejecting it outright: a new ChangesRequested status
keeps the row open so the customer's next edit appends into the same
request instead of starting a fresh cycle, and the reviewer's note
persists across that round instead of being cleared on resubmit.

Version History and Review History (previously two separate,
differently-shaped lists) are merged into one chronological timeline
under a new History tab, including document changes shown as a real
previous-vs-current diff (both files openable).

Bug fixes surfaced while wiring this up:
- Replacing a single-file document slot left the old file live
  alongside the new one instead of retiring it (customer settings +
  onboarding uploads).
- The "previous" file in a document diff 404'd once superseded —
  the preview route now also matches soft-deleted records.
- A document replace was recorded twice in the timeline (once at
  upload, once again at change-request approval).
This commit is contained in:
Nathnael
2026-07-31 14:12:30 +00:00
parent 3952e8bbdf
commit 4f81a0bbb8
23 changed files with 795 additions and 166 deletions

View File

@@ -51,7 +51,11 @@ export class FilesController {
@Query("download") download: string | undefined,
@Res() res: Response,
) {
const record = await this.filesService.findById(fileId);
// Includes soft-deleted records: a superseded document (replaced via a
// single-file document slot, or resolved as part of a license/PoA swap)
// is only reachable by UUID through the change-request/version-history
// diff, where reviewers need to open the "previous" file to compare it.
const record = await this.filesService.findByIdIncludingDeleted(fileId);
// Chat attachments are cross-tenant sensitive and this route has no
// ownership check, so a leaked/guessed UUID would hand one company's file to
@@ -63,7 +67,9 @@ export class FilesController {
);
}
const { stream } = await this.filesService.streamById(fileId);
const { stream } = await this.filesService.streamById(fileId, {
includeDeleted: true,
});
const forceDownload = download === "1" || download === "true";
const disposition = forceDownload ? "attachment" : "inline";