mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
185 lines
4.8 KiB
TypeScript
185 lines
4.8 KiB
TypeScript
import { useMemo } from "react";
|
|
import { Check, ShieldCheck } from "lucide-react";
|
|
import { Stack, Group, Text, Badge, Button, Box } from "@mantine/core";
|
|
import type { Freight } from "@edr/types";
|
|
|
|
import { formatContractApprovalProgress } from "@/features/contracts/contract-approval-progress";
|
|
import { SectionCard } from "@/components/bookings/detail/SectionCard";
|
|
import type { useContractMutations } from "@/hooks/contracts/useContracts";
|
|
|
|
type Mutations = ReturnType<typeof useContractMutations>;
|
|
|
|
interface ContractApprovalStepsCardProps {
|
|
contract: Freight.IContract;
|
|
mutations: Mutations;
|
|
}
|
|
|
|
/** Approval chain with inline approve on the next pending step. */
|
|
export function ContractApprovalStepsCard({
|
|
contract,
|
|
mutations,
|
|
}: ContractApprovalStepsCardProps) {
|
|
const steps = useMemo(
|
|
() =>
|
|
[...(contract.approvalSteps ?? [])].sort(
|
|
(a, b) => a.stepOrder - b.stepOrder,
|
|
),
|
|
[contract.approvalSteps],
|
|
);
|
|
|
|
const nextPending = steps.find((s) => s.status === "PENDING");
|
|
const summary = formatContractApprovalProgress(contract.status, steps);
|
|
|
|
const subtitle =
|
|
summary.detail ||
|
|
(nextPending
|
|
? `Next: ${nextPending.requiredRole} · step ${nextPending.stepOrder}`
|
|
: steps.length
|
|
? "All steps complete"
|
|
: "Accept submission to begin");
|
|
|
|
return (
|
|
<SectionCard
|
|
icon={ShieldCheck}
|
|
title="Approval chain"
|
|
extra={
|
|
<Badge color="edr-green" variant="light" radius="sm">
|
|
{steps.filter((s) => s.status === "APPROVED").length}/{steps.length}
|
|
</Badge>
|
|
}
|
|
>
|
|
<Text size="xs" c="dimmed" mb="sm">
|
|
{subtitle}
|
|
</Text>
|
|
|
|
{steps.length === 0 ? (
|
|
<Text
|
|
size="sm"
|
|
c="dimmed"
|
|
ta="center"
|
|
py="lg"
|
|
px="md"
|
|
style={{
|
|
borderRadius: 8,
|
|
border: "1px dashed var(--mantine-color-gray-3)",
|
|
background: "var(--mantine-color-gray-0)",
|
|
}}
|
|
>
|
|
Use <strong>Accept for approval</strong> in staff actions to
|
|
instantiate steps.
|
|
</Text>
|
|
) : (
|
|
<Stack gap="xs">
|
|
{steps.map((step) => (
|
|
<StepRow
|
|
key={step.id}
|
|
step={step}
|
|
isNext={nextPending?.id === step.id}
|
|
isPending={mutations.approveStep.isPending}
|
|
onApprove={() =>
|
|
mutations.approveStep.mutate({
|
|
stepId: step.id,
|
|
requiredRole: step.requiredRole,
|
|
})
|
|
}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
)}
|
|
</SectionCard>
|
|
);
|
|
}
|
|
|
|
function StepRow({
|
|
step,
|
|
isNext,
|
|
isPending,
|
|
onApprove,
|
|
}: {
|
|
step: Freight.IContractApprovalStep;
|
|
isNext: boolean;
|
|
isPending: boolean;
|
|
onApprove: () => void;
|
|
}) {
|
|
const statusColor =
|
|
step.status === "APPROVED"
|
|
? "edr-green"
|
|
: step.status === "REJECTED"
|
|
? "red"
|
|
: isNext
|
|
? "edr-green"
|
|
: "gray";
|
|
|
|
return (
|
|
<Group
|
|
justify="space-between"
|
|
wrap="nowrap"
|
|
gap="sm"
|
|
px="sm"
|
|
py="xs"
|
|
style={{
|
|
borderRadius: 8,
|
|
border: "1px solid var(--mantine-color-gray-2)",
|
|
borderLeft: isNext
|
|
? "3px solid var(--freight-brand)"
|
|
: "1px solid var(--mantine-color-gray-2)",
|
|
background: isNext ? "var(--mantine-color-gray-0)" : "white",
|
|
}}
|
|
>
|
|
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
|
<Box
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: 8,
|
|
flexShrink: 0,
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
background: "var(--mantine-color-gray-1)",
|
|
color: isNext
|
|
? "var(--mantine-color-gray-7)"
|
|
: "var(--mantine-color-gray-6)",
|
|
}}
|
|
>
|
|
{step.stepOrder}
|
|
</Box>
|
|
<Box style={{ minWidth: 0 }}>
|
|
<Text size="sm" fw={600}>
|
|
{step.requiredRole}
|
|
</Text>
|
|
{step.note && (
|
|
<Text size="xs" c="dimmed" truncate>
|
|
{step.note}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
</Group>
|
|
<Group gap="xs" wrap="nowrap" style={{ flexShrink: 0 }}>
|
|
{isNext && step.status === "PENDING" && (
|
|
<Button
|
|
size="compact-sm"
|
|
color="edr-green"
|
|
leftSection={<Check size={14} />}
|
|
disabled={isPending}
|
|
onClick={onApprove}
|
|
>
|
|
Approve
|
|
</Button>
|
|
)}
|
|
<Badge
|
|
variant="light"
|
|
color={statusColor}
|
|
size="sm"
|
|
radius="sm"
|
|
tt="uppercase"
|
|
>
|
|
{step.status}
|
|
</Badge>
|
|
</Group>
|
|
</Group>
|
|
);
|
|
}
|