Merge pull request #1461 from Tria-plc/freight_feature/usermanagement

feat(contracts): implement staff cancellation of contracts with reaso…
This commit is contained in:
marshal
2026-08-29 14:36:08 +03:00
committed by GitHub
13 changed files with 352 additions and 5 deletions

View File

@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { Button, Group, Modal, Stack, Text, Textarea } from "@mantine/core";
import {
Ban,
Check,
Eye,
// FilePen, // ponytail: back with the "Edit contract articles" button
@@ -81,6 +82,8 @@ export function ContractActionsToolbar({
const mayReject = hasPermission(user, FREIGHT_PERMS.contracts.reject[arm]);
// One key both ways — whoever can freeze a contract can unfreeze it.
const maySuspend = hasPermission(user, FREIGHT_PERMS.contracts.suspend);
// Cancel is its own key — it is terminal, so it is NOT implied by suspend.
const mayCancel = hasPermission(user, FREIGHT_PERMS.contracts.cancel);
const [editorOpen, setEditorOpen] = useState(false);
const [editorMode, setEditorMode] = useState<"accept" | "edit">("accept");
@@ -93,6 +96,67 @@ export function ContractActionsToolbar({
const [suspendReason, setSuspendReason] = useState("");
const [resumeOpen, setResumeOpen] = useState(false);
const [resumeNote, setResumeNote] = useState("");
const [cancelOpen, setCancelOpen] = useState(false);
const [cancelReason, setCancelReason] = useState("");
// Shared by the suspended branch and the normal toolbar — both can cancel.
const cancelModal = (
<Modal
opened={cancelOpen}
onClose={() => setCancelOpen(false)}
title="Cancel this contract?"
centered
>
<Stack gap="md">
<Text size="sm">
Contract <b>{contract.reference}</b> will be cancelled permanently.
This cannot be undone there is no way to reactivate it. A new
contract with the same details can be submitted afterwards. The
customer is notified.
</Text>
<Textarea
label="Reason for cancellation"
placeholder="Explain why this contract is being cancelled…"
autosize
minRows={3}
value={cancelReason}
onChange={(e) => setCancelReason(e.currentTarget.value)}
/>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setCancelOpen(false)}>
Keep contract
</Button>
<Button
color="red"
disabled={!cancelReason.trim()}
loading={mutations.cancelByStaff.isPending}
onClick={() =>
mutations.cancelByStaff.mutate(cancelReason, {
onSuccess: () => {
setCancelOpen(false);
setCancelReason("");
},
})
}
>
Cancel contract
</Button>
</Group>
</Stack>
</Modal>
);
const cancelButton = mayCancel ? (
<Button
fullWidth
variant="light"
color="red"
leftSection={<Ban size={16} />}
onClick={() => setCancelOpen(true)}
>
Cancel contract
</Button>
) : null;
// Whether the document is editable depends on WHO is viewing — only the
// approver whose turn it is may edit — so the server decides, not the client.
@@ -126,9 +190,13 @@ export function ContractActionsToolbar({
if (status === "CHANGES_REQUESTED") {
return (
<SectionCard icon={Zap} title="Awaiting customer">
<Text size="sm" c="dimmed">
No staff actions until the customer resubmits the contract.
</Text>
<Stack gap="sm">
<Text size="sm" c="dimmed">
No staff actions until the customer resubmits the contract.
</Text>
{cancelButton}
</Stack>
{cancelModal}
</SectionCard>
);
}
@@ -166,6 +234,7 @@ export function ContractActionsToolbar({
You do not have permission to lift a suspension.
</Text>
)}
{cancelButton}
</Stack>
<Modal
@@ -209,6 +278,8 @@ export function ContractActionsToolbar({
</Group>
</Stack>
</Modal>
{cancelModal}
</SectionCard>
);
}
@@ -362,11 +433,16 @@ export function ContractActionsToolbar({
</Button>
)}
{/* Available at every non-terminal status — the early returns above
already cover the statuses where cancelling makes no sense. */}
{cancelButton}
{!canAccept &&
!inApproval &&
!canViewContract &&
!canReviewClearance &&
!canSuspend && (
!canSuspend &&
!mayCancel && (
<Text size="sm" c="dimmed">
No staff actions available for this status. Monitor until the
workflow advances.
@@ -515,6 +591,8 @@ export function ContractActionsToolbar({
</Button>
</Stack>
</Modal>
{cancelModal}
</SectionCard>
);
}