feat(companies): per-document change requests and resubmission review queue

Two review-workflow gaps for freight customer onboarding:

Request for change per document. Backoffice can now flag a single uploaded
document (company document, profile licence, or POA delegation letter) with a
note the customer sees, instead of rejecting the whole role over it. Adds
review_status/review_note/reviewed_by/reviewed_at to freight.files (migration
AddFileReviewStatus, partial index for the gate), a POST
documents/:fileId/request-change endpoint, the backoffice action + modal, and a
portal banner/badge so the customer knows what to re-upload. Re-uploading clears
the flag. Approving a role is blocked while any of its documents has an open
correction; the gate check and the status write share a pessimistic write lock
on the company row (as does the change-request write) so a correction can never
slip in between the check and the profile going Active.

Resubmission is visible to reviewers. When a customer resubmits a rejected role
or amends a change request, backoffice staff are notified (allBackoffice inbox
item, deep-linked to the customer) and the resubmission surfaces in a new
"Pending changes" list view + KPI, since such companies are status = active and
never matched the pending-approval filter.
This commit is contained in:
Nathnael
2026-07-21 07:34:40 +00:00
parent 1cfc0f0fa8
commit fd5aedcec7
22 changed files with 948 additions and 60 deletions

View File

@@ -0,0 +1,101 @@
import {
Alert,
Button,
Group,
Modal,
Stack,
Text,
Textarea,
} from "@mantine/core";
import { useMutation } from "@tanstack/react-query";
import { FilePen } from "lucide-react";
import { useEffect, useState } from "react";
import { api } from "@/services/api";
import type { CustomerDocument } from "@/types/customer";
export interface RequestDocumentChangeModalProps {
/** The document under review; `null` closes the modal. */
document: CustomerDocument | null;
companyId: string;
onClose: () => void;
}
/**
* Ask the customer to correct one uploaded document.
*
* Deliberately narrower than rejecting a whole role: the customer keeps every
* other document and only re-uploads this one. The role cannot be approved
* while the request is open, so the note has to say what is actually wrong —
* it is shown to the customer verbatim.
*/
export function RequestDocumentChangeModal({
document,
companyId,
onClose,
}: RequestDocumentChangeModalProps) {
const requestChange = useMutation(
api.customers.requestDocumentChange.mutationOptions(),
);
const [note, setNote] = useState("");
// Re-opening on a document that already has an open request should show what
// was asked for, so the reviewer edits the reason rather than retyping it.
useEffect(() => {
setNote(document?.reviewNote ?? "");
}, [document?.id, document?.reviewNote]);
const submit = () => {
if (!document) return;
requestChange.mutate(
{ companyId, fileId: document.id, note: note.trim() },
{ onSuccess: onClose },
);
};
return (
<Modal
opened={document !== null}
onClose={onClose}
title="Request a change"
centered
radius="lg"
>
<Stack gap="md">
<Alert color="orange" variant="light" icon={<FilePen size={18} />}>
The customer is notified and sees this note verbatim. This role cannot
be approved until they upload a corrected document.
</Alert>
<Text size="sm" c="dimmed">
Document: <strong>{document?.name}</strong>
</Text>
<Textarea
label="What needs correcting?"
placeholder="e.g. The trade license scan is cut off — please re-upload the full page."
autosize
minRows={3}
value={note}
onChange={(e) => setNote(e.currentTarget.value)}
required
/>
<Group justify="flex-end" gap="sm">
<Button
variant="default"
onClick={onClose}
disabled={requestChange.isPending}
>
Cancel
</Button>
<Button
color="orange"
loading={requestChange.isPending}
disabled={note.trim().length === 0}
onClick={submit}
>
Request change
</Button>
</Group>
</Stack>
</Modal>
);
}

View File

@@ -13,6 +13,10 @@ export {
ChangeRequestReview,
ChangeRequestPendingBadge,
} from "./ChangeRequestReview";
export {
RequestDocumentChangeModal,
type RequestDocumentChangeModalProps,
} from "./RequestDocumentChangeModal";
export {
default as ResetPasswordAction,
type ResetPasswordActionProps,