mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 14:20:58 +00:00
fix: onboarding back and contract blocking
This commit is contained in:
@@ -286,9 +286,13 @@ export default function OnboardingWizardDialog({
|
||||
});
|
||||
}, [roles, nationality, startMutation]);
|
||||
|
||||
// Note: no "back to role selection" — once the draft is created the role(s)
|
||||
// are fixed; the form's first-step Back is a no-op so progress never resets.
|
||||
const handleBackToRoles = useCallback(() => { }, []);
|
||||
// Back from the form's first step returns to nationality/role selection.
|
||||
// Safe to re-enter: startOnboarding is idempotent — it reuses the existing
|
||||
// draft, refreshes the nationality and creates only roles that don't exist yet.
|
||||
const handleBackToRoles = useCallback(() => {
|
||||
setStartError(null);
|
||||
setPhase("nationality-role");
|
||||
}, []);
|
||||
|
||||
// Save the current step's fields to the draft (PATCH /profile). Returns the
|
||||
// server error message on failure so the form can show it (e.g. duplicate TIN).
|
||||
@@ -359,7 +363,6 @@ export default function OnboardingWizardDialog({
|
||||
// The active step across the whole journey, driving the header + progress pill.
|
||||
const activeStep: WizardStep = phase === "form" ? formStep : phase;
|
||||
const stepMeta = STEP_META[activeStep];
|
||||
console.log({ stepMeta, activeStep, STEP_META });
|
||||
const activeIdx = WIZARD_STEPS.indexOf(activeStep);
|
||||
|
||||
// Closing from the congratulations panel also clears the completed flag so a
|
||||
@@ -403,7 +406,6 @@ export default function OnboardingWizardDialog({
|
||||
onSubmit: handleSubmit,
|
||||
isPending: finishMutation.isPending,
|
||||
onBack: handleBackToRoles,
|
||||
hideFirstStepBack: true,
|
||||
initialStep: effectiveResumeStep,
|
||||
resyncOpen: opened,
|
||||
onStepChange: handleStepChange,
|
||||
|
||||
@@ -51,7 +51,6 @@ export default function CompanyProfileForm({
|
||||
onBack,
|
||||
initialStep,
|
||||
resyncOpen,
|
||||
hideFirstStepBack,
|
||||
onStepChange,
|
||||
onSaveStep,
|
||||
rehydrate,
|
||||
@@ -73,8 +72,6 @@ export default function CompanyProfileForm({
|
||||
initialStep?: CompanyStep;
|
||||
/** When this flips true (dialog reopened), jump back to initialStep (furthest reached). */
|
||||
resyncOpen?: boolean;
|
||||
/** Hide the Back button on the first step (onboarding can't go back to role pick). */
|
||||
hideFirstStepBack?: boolean;
|
||||
/** Reports the active step so the parent can persist resume progress. */
|
||||
onStepChange?: (step: CompanyStep) => void;
|
||||
/** Persist the current step's data before advancing; returns an error to show. */
|
||||
@@ -514,10 +511,6 @@ export default function CompanyProfileForm({
|
||||
else setStep(stepOrder[currentIdx - 1]);
|
||||
};
|
||||
|
||||
// Back is hidden on the first step during onboarding (can't return to role
|
||||
// selection); otherwise always available.
|
||||
const showBack = !(hideFirstStepBack && step === "company");
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
@@ -851,17 +844,13 @@ export default function CompanyProfileForm({
|
||||
)}
|
||||
|
||||
<Group justify="space-between" pt="xs">
|
||||
{showBack ? (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={prevStep}
|
||||
leftSection={<ArrowLeft size={16} />}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={prevStep}
|
||||
leftSection={<ArrowLeft size={16} />}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
onClick={nextStep}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Popover,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
@@ -31,6 +33,8 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
import { PROFILE_TYPE_LABELS } from "@/constants/profileMode";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import { api } from "@/services/api";
|
||||
import { ContractCustomerAction } from "@/components/customer-actions/ContractCustomerAction";
|
||||
import type { ContractListFilter } from "@/services/contracts.service";
|
||||
@@ -58,14 +62,26 @@ function primaryRoute(contract: Freight.IContract) {
|
||||
|
||||
export default function ContractsList() {
|
||||
const navigate = useNavigate();
|
||||
const { company } = useAuth();
|
||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||
const [query, setQuery] = useState("");
|
||||
const [disclaimerOpen, setDisclaimerOpen] = useState(false);
|
||||
const [freightFilter, setFreightFilter] = useState<string | null>(null);
|
||||
const [kindFilter, setKindFilter] = useState<string | null>(null);
|
||||
const [createdFrom, setCreatedFrom] = useState<string>("");
|
||||
const [createdTo, setCreatedTo] = useState<string>("");
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
|
||||
// A contract can only be created under an approved profile — NewContractPage
|
||||
// blocks every operation whose profile isn't "active". With none approved the
|
||||
// page is reachable but unusable, so warn before sending the user there.
|
||||
const profiles = company?.company?.companyProfiles ?? [];
|
||||
const noActiveProfile =
|
||||
profiles.length > 0 && !profiles.some((p) => p.status === "active");
|
||||
|
||||
const openNewContract = () =>
|
||||
navigate("/contracts/new", { state: { fresh: true } });
|
||||
|
||||
const toggleExpanded = (id: string) =>
|
||||
setExpanded((prev) => {
|
||||
const nextSet = new Set(prev);
|
||||
@@ -137,9 +153,11 @@ export default function ContractsList() {
|
||||
const stats = useMemo(() => {
|
||||
const items = data?.items ?? [];
|
||||
const active = items.filter((c) =>
|
||||
["CONTRACT_ACTIVE", "FULLY_EXECUTED", "ACTIVE_SHIPMENT_IN_PROGRESS"].includes(
|
||||
c.status,
|
||||
),
|
||||
[
|
||||
"CONTRACT_ACTIVE",
|
||||
"FULLY_EXECUTED",
|
||||
"ACTIVE_SHIPMENT_IN_PROGRESS",
|
||||
].includes(c.status),
|
||||
).length;
|
||||
const pending = items.filter((c) =>
|
||||
[
|
||||
@@ -158,7 +176,7 @@ export default function ContractsList() {
|
||||
return { active, pending, total };
|
||||
}, [data]);
|
||||
|
||||
const total = data?.meta?.total ?? (data?.items?.length ?? 0);
|
||||
const total = data?.meta?.total ?? data?.items?.length ?? 0;
|
||||
const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize));
|
||||
const pageIndex = pagination.pageIndex;
|
||||
const start = total === 0 ? 0 : pageIndex * pagination.pageSize + 1;
|
||||
@@ -175,19 +193,63 @@ export default function ContractsList() {
|
||||
<Stack gap="lg">
|
||||
{/* Header */}
|
||||
<Group justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
||||
<Title
|
||||
order={1}
|
||||
fw={800}
|
||||
fz={26}
|
||||
style={{ letterSpacing: "-0.01em" }}
|
||||
>
|
||||
Contracts
|
||||
</Title>
|
||||
<Button
|
||||
color="edr-green"
|
||||
<Popover
|
||||
opened={disclaimerOpen}
|
||||
onChange={setDisclaimerOpen}
|
||||
position="bottom-end"
|
||||
width={340}
|
||||
radius="md"
|
||||
size="md"
|
||||
leftSection={<Plus size={16} />}
|
||||
onClick={() => navigate("/contracts/new", { state: { fresh: true } })}
|
||||
styles={{ root: { fontWeight: 600, height: 42, paddingInline: 18 } }}
|
||||
shadow="md"
|
||||
withArrow
|
||||
trapFocus
|
||||
>
|
||||
New Contract
|
||||
</Button>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
size="md"
|
||||
leftSection={<Plus size={16} />}
|
||||
onClick={() =>
|
||||
noActiveProfile
|
||||
? setDisclaimerOpen((o) => !o)
|
||||
: openNewContract()
|
||||
}
|
||||
styles={{
|
||||
root: { fontWeight: 600, height: 42, paddingInline: 18 },
|
||||
}}
|
||||
>
|
||||
New Contract
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Stack gap="sm">
|
||||
<Group gap={8} wrap="nowrap" align="flex-start">
|
||||
<AlertTriangle
|
||||
size={18}
|
||||
color="var(--mantine-color-edr-accent-6)"
|
||||
style={{ flexShrink: 0, marginTop: 1 }}
|
||||
/>
|
||||
<Text fz={14} fw={700} style={{ color: INK }}>
|
||||
None of your profiles are active yet
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Text fz={13} c="dimmed">
|
||||
Contracts can only be created under a profile EDR has
|
||||
approved. You can continue, but every operation stays locked
|
||||
until at least one profile is approved.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Group>
|
||||
|
||||
{/* Summary strip */}
|
||||
@@ -378,7 +440,11 @@ export default function ContractsList() {
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={11}>
|
||||
<Stack align="center" gap={8} py={48}>
|
||||
<Inbox size={26} color={MUTED} style={{ opacity: 0.5 }} />
|
||||
<Inbox
|
||||
size={26}
|
||||
color={MUTED}
|
||||
style={{ opacity: 0.5 }}
|
||||
/>
|
||||
<Text fz={13} c="dimmed">
|
||||
No contracts yet. Create one from New Contract.
|
||||
</Text>
|
||||
@@ -400,154 +466,159 @@ export default function ContractsList() {
|
||||
const isOpen = expanded.has(c.id);
|
||||
return (
|
||||
<Fragment key={c.id}>
|
||||
<Table.Tr
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
background: isOpen ? "#F4FBF8" : undefined,
|
||||
}}
|
||||
onClick={() => navigate(`/contracts/${c.id}`)}
|
||||
>
|
||||
<Table.Td>
|
||||
<Box
|
||||
component="button"
|
||||
aria-label={isOpen ? "Hide progress" : "Show progress"}
|
||||
aria-expanded={isOpen}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpanded(c.id);
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${BORDER}`,
|
||||
background: isOpen ? GREEN : "#FFFFFF",
|
||||
color: isOpen ? "#FFFFFF" : MUTED,
|
||||
cursor: "pointer",
|
||||
transition: "all 140ms ease",
|
||||
}}
|
||||
>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
style={{
|
||||
transform: isOpen ? "rotate(180deg)" : "none",
|
||||
transition: "transform 160ms ease",
|
||||
<Table.Tr
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
background: isOpen ? "#F4FBF8" : undefined,
|
||||
}}
|
||||
onClick={() => navigate(`/contracts/${c.id}`)}
|
||||
>
|
||||
<Table.Td>
|
||||
<Box
|
||||
component="button"
|
||||
aria-label={
|
||||
isOpen ? "Hide progress" : "Show progress"
|
||||
}
|
||||
aria-expanded={isOpen}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpanded(c.id);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={14} fw={700} style={{ color: INK }}>
|
||||
{c.reference}
|
||||
</Text>
|
||||
<Text fz={12} c="dimmed">
|
||||
{isContainer ? "Containerised" : "Bulk"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={isGeneral ? "edr-green" : "gray"}
|
||||
radius="sm"
|
||||
>
|
||||
{isGeneral ? "General" : "One-Time"}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap={7} wrap="nowrap" align="center">
|
||||
{isContainer ? (
|
||||
<Package size={15} color={MUTED} />
|
||||
) : (
|
||||
<Weight size={15} color={MUTED} />
|
||||
)}
|
||||
<Text fz={13} style={{ color: INK }}>
|
||||
{isContainer ? "Container" : "Bulk"}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${BORDER}`,
|
||||
background: isOpen ? GREEN : "#FFFFFF",
|
||||
color: isOpen ? "#FFFFFF" : MUTED,
|
||||
cursor: "pointer",
|
||||
transition: "all 140ms ease",
|
||||
}}
|
||||
>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
style={{
|
||||
transform: isOpen ? "rotate(180deg)" : "none",
|
||||
transition: "transform 160ms ease",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={14} fw={700} style={{ color: INK }}>
|
||||
{c.reference}
|
||||
</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={13} style={{ color: INK }}>
|
||||
{origin}{" "}
|
||||
<Text span c="dimmed">
|
||||
→
|
||||
</Text>{" "}
|
||||
{destination}
|
||||
{count > 1 && (
|
||||
<Text span c="dimmed" fz={12}>
|
||||
{" "}
|
||||
+{count - 1}
|
||||
<Text fz={12} c="dimmed">
|
||||
{isContainer ? "Containerised" : "Bulk"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={isGeneral ? "edr-green" : "gray"}
|
||||
radius="sm"
|
||||
>
|
||||
{isGeneral ? "General" : "One-Time"}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap={7} wrap="nowrap" align="center">
|
||||
{isContainer ? (
|
||||
<Package size={15} color={MUTED} />
|
||||
) : (
|
||||
<Weight size={15} color={MUTED} />
|
||||
)}
|
||||
<Text fz={13} style={{ color: INK }}>
|
||||
{isContainer ? "Container" : "Bulk"}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text
|
||||
fz={13}
|
||||
c={dir ? undefined : "dimmed"}
|
||||
style={{ color: dir ? INK : undefined }}
|
||||
>
|
||||
{tradeLabel}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={13} style={{ color: INK }}>
|
||||
{c.paymentCurrency ?? "—"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text
|
||||
fz={13}
|
||||
c={c.createdAt ? undefined : "dimmed"}
|
||||
style={{ color: c.createdAt ? INK : undefined }}
|
||||
>
|
||||
{c.createdAt
|
||||
? new Date(c.createdAt).toLocaleDateString()
|
||||
: "—"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text
|
||||
fz={13}
|
||||
c={c.contractValidUntil ? undefined : "dimmed"}
|
||||
style={{
|
||||
color: c.contractValidUntil ? INK : undefined,
|
||||
}}
|
||||
>
|
||||
{c.contractValidUntil
|
||||
? new Date(
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={13} style={{ color: INK }}>
|
||||
{origin}{" "}
|
||||
<Text span c="dimmed">
|
||||
→
|
||||
</Text>{" "}
|
||||
{destination}
|
||||
{count > 1 && (
|
||||
<Text span c="dimmed" fz={12}>
|
||||
{" "}
|
||||
+{count - 1}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text
|
||||
fz={13}
|
||||
c={dir ? undefined : "dimmed"}
|
||||
style={{ color: dir ? INK : undefined }}
|
||||
>
|
||||
{tradeLabel}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={13} style={{ color: INK }}>
|
||||
{c.paymentCurrency ?? "—"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text
|
||||
fz={13}
|
||||
c={c.createdAt ? undefined : "dimmed"}
|
||||
style={{ color: c.createdAt ? INK : undefined }}
|
||||
>
|
||||
{c.createdAt
|
||||
? new Date(c.createdAt).toLocaleDateString()
|
||||
: "—"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text
|
||||
fz={13}
|
||||
c={c.contractValidUntil ? undefined : "dimmed"}
|
||||
style={{
|
||||
color: c.contractValidUntil ? INK : undefined,
|
||||
}}
|
||||
>
|
||||
{c.contractValidUntil
|
||||
? new Date(
|
||||
c.contractValidUntil,
|
||||
).toLocaleDateString()
|
||||
: "—"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ContractStatusBadge status={c.status} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group justify="flex-end" gap={8} wrap="nowrap">
|
||||
<ContractDocButton
|
||||
contract={c}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<ContractCustomerAction
|
||||
contract={c}
|
||||
bookings={bookings}
|
||||
size="sm"
|
||||
listStyle
|
||||
/>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{isOpen && (
|
||||
<Table.Tr style={{ background: "#F4FBF8" }}>
|
||||
<Table.Td colSpan={11} style={{ padding: "6px 20px 18px" }}>
|
||||
<ContractStepBanner contract={c} />
|
||||
: "—"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ContractStatusBadge status={c.status} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group justify="flex-end" gap={8} wrap="nowrap">
|
||||
<ContractDocButton
|
||||
contract={c}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<ContractCustomerAction
|
||||
contract={c}
|
||||
bookings={bookings}
|
||||
size="sm"
|
||||
listStyle
|
||||
/>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
{isOpen && (
|
||||
<Table.Tr style={{ background: "#F4FBF8" }}>
|
||||
<Table.Td
|
||||
colSpan={11}
|
||||
style={{ padding: "6px 20px 18px" }}
|
||||
>
|
||||
<ContractStepBanner contract={c} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
@@ -564,7 +635,10 @@ export default function ContractsList() {
|
||||
gap="md"
|
||||
px={20}
|
||||
py={14}
|
||||
style={{ borderTop: `1px solid ${BORDER}`, background: "#FCFDFE" }}
|
||||
style={{
|
||||
borderTop: `1px solid ${BORDER}`,
|
||||
background: "#FCFDFE",
|
||||
}}
|
||||
>
|
||||
<Group gap={10} align="center">
|
||||
<Text fz={13} c="dimmed">
|
||||
@@ -574,8 +648,7 @@ export default function ContractsList() {
|
||||
data={["10", "25", "50"]}
|
||||
value={String(pagination.pageSize)}
|
||||
onChange={(v) =>
|
||||
v &&
|
||||
setPagination({ pageIndex: 0, pageSize: Number(v) })
|
||||
v && setPagination({ pageIndex: 0, pageSize: Number(v) })
|
||||
}
|
||||
radius="md"
|
||||
size="xs"
|
||||
|
||||
Reference in New Issue
Block a user