This commit is contained in:
Marshal
2026-07-15 14:27:00 +00:00
parent 19c9da28ae
commit d7d9db9c3a
10 changed files with 554 additions and 25 deletions

View File

@@ -4,12 +4,12 @@ import { useQuery } from "@tanstack/react-query";
import { Button, Modal, Stack, Text, Textarea } from "@mantine/core";
import {
Check,
FileCheck,
FilePen,
FileSignature,
MessageSquareWarning,
RefreshCw,
ShieldCheck,
Sparkles,
XCircle,
Zap,
} from "lucide-react";
@@ -180,7 +180,7 @@ export function ContractActionsToolbar({
documentGenerated ? (
<RefreshCw size={16} />
) : (
<Sparkles size={16} />
<FileCheck size={16} />
)
}
loading={mutations.generateContract.isPending}
@@ -196,7 +196,7 @@ export function ContractActionsToolbar({
fullWidth
variant="light"
color="orange"
leftSection={<Sparkles size={16} />}
leftSection={<FileCheck size={16} />}
loading={mutations.generateContract.isPending}
onClick={() => mutations.generateContract.mutate()}
>

View File

@@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
import { Check, ShieldCheck, X } from "lucide-react";
import { AlertTriangle, Check, FileCheck, ShieldCheck, X } from "lucide-react";
import {
Stack,
Group,
@@ -31,6 +31,7 @@ export function ContractApprovalStepsCard({
const [confirmOpen, setConfirmOpen] = useState(false);
const [pendingStep, setPendingStep] =
useState<Freight.IContractApprovalStep | null>(null);
const [needsGenerateOpen, setNeedsGenerateOpen] = useState(false);
const [rejectOpen, setRejectOpen] = useState(false);
const [rejectStepRow, setRejectStepRow] =
useState<Freight.IContractApprovalStep | null>(null);
@@ -47,7 +48,17 @@ export function ContractApprovalStepsCard({
const nextPending = steps.find((s) => s.status === "PENDING");
const summary = formatContractApprovalProgress(contract.status, steps);
// Approvers must review the GENERATED contract document before approving. If
// it has not been generated yet, block the approval and tell staff to generate
// it first (via "Generate contract" in Staff actions) — mirrors the server
// guard so the user sees a clear reason, not a generic failure toast.
const documentGenerated = Boolean(contract.contractGeneratedAt);
const openApprove = (step: Freight.IContractApprovalStep) => {
if (contract.status === "PENDING_APPROVAL" && !documentGenerated) {
setNeedsGenerateOpen(true);
return;
}
setPendingStep(step);
setConfirmOpen(true);
};
@@ -181,6 +192,47 @@ export function ContractApprovalStepsCard({
</Stack>
</Modal>
<Modal
opened={needsGenerateOpen}
onClose={() => setNeedsGenerateOpen(false)}
title={
<Group gap="xs">
<AlertTriangle size={18} color="var(--mantine-color-orange-6)" />
<Text fw={700}>Generate the contract first</Text>
</Group>
}
radius="md"
centered
>
<Stack gap="md">
<Text size="sm" c="dimmed">
The contract document for{" "}
<Text span fw={600} c="dark">
{contract.reference}
</Text>{" "}
has not been generated yet. Approvers must review the generated
document before it can be approved.
</Text>
<Text size="sm" c="dimmed">
Use{" "}
<Text span fw={600} c="dark">
Generate contract
</Text>{" "}
in the Staff actions panel edit the articles first if needed then
return here to approve.
</Text>
<Group justify="flex-end">
<Button
color="edr-green"
leftSection={<FileCheck size={16} />}
onClick={() => setNeedsGenerateOpen(false)}
>
Got it
</Button>
</Group>
</Stack>
</Modal>
<Modal
opened={rejectOpen}
onClose={closeReject}

View File

@@ -201,7 +201,10 @@ const RuleEngineFormDialog = ({
const payload: Record<string, unknown> = {};
for (const field of visibleFields) {
const raw = values[field.name];
// Derived fields always submit their computed value — never stale state.
const raw = field.computeValue
? (field.computeValue(values) ?? "")
: values[field.name];
if (field.type === "multiselect") {
// Always the full replacement list — the API syncs the relation to it.
payload[field.name] = Array.isArray(raw) ? raw : [];
@@ -348,6 +351,7 @@ const RuleEngineFormDialog = ({
}
const isNumber = field.type === "number";
const computed = field.computeValue ? field.computeValue(values) : undefined;
return (
<TextInput
@@ -359,7 +363,8 @@ const RuleEngineFormDialog = ({
// display order) is a non-negative magnitude — reject negatives outright
// rather than letting a typed "-" reach the API.
min={isNumber ? 0 : undefined}
value={String(values[field.name] ?? "")}
disabled={field.disabled || computed !== undefined}
value={String((computed !== undefined ? computed : values[field.name]) ?? "")}
onChange={(e) => {
const next = e.currentTarget.value;
if (isNumber && next.trim().startsWith("-")) return;