Merge branch 'freight_feature/profile' of github.com:Tria-plc/edr-platform into freight_feature/profile

This commit is contained in:
marshal
2026-06-25 09:29:32 +03:00

View File

@@ -19,7 +19,7 @@ import {
Snowflake,
Trash2,
} from "lucide-react";
import { useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo } from "react";
import {
Controller,
useFieldArray,
@@ -80,29 +80,39 @@ export function Step4Route({
remove: removeRoute,
} = useFieldArray({ control: form.control, name: "extraRoutes" });
// useFieldArray's `fields` don't re-render on value change, so watch the live
// route values to filter each row's yard options by what it has selected.
const watchedExtraRoutes = form.watch("extraRoutes") ?? [];
const yardOptions = useMemo(() => {
if (!referenceData?.yard) return [];
return referenceData.yard.map((y) => ({ value: y.id, label: y.name }));
}, [referenceData]);
const originData = useMemo(() => {
return yardOptions
.filter((o) => o.value !== destinationYard)
.filter((o) => {
if (!originCountry) return true;
const yard = referenceData?.yard.find((y) => y.id === o.value);
return yard?.country === originCountry;
});
}, [yardOptions, destinationYard, originCountry, referenceData]);
const destData = useMemo(() => {
return yardOptions
.filter((o) => o.value !== originYard)
.filter((o) => {
if (!destinationCountry) return true;
const yard = referenceData?.yard.find((y) => y.id === o.value);
return yard?.country === destinationCountry;
});
}, [yardOptions, originYard, destinationCountry, referenceData]);
// Filter the yard list to one side of a route: the yards in `country` (the
// operation type fixes which country each end must be in — see originCountry /
// destinationCountry above), excluding the yard already chosen on the other
// end of the same route so origin and destination can never match.
const yardsForSide = useCallback(
(country: string | null, excludeYardId: string) =>
yardOptions
.filter((o) => o.value !== excludeYardId)
.filter((o) => {
if (!country) return true;
const yard = referenceData?.yard.find((y) => y.id === o.value);
return yard?.country === country;
}),
[yardOptions, referenceData],
);
const originData = useMemo(
() => yardsForSide(originCountry, destinationYard),
[yardsForSide, originCountry, destinationYard],
);
const destData = useMemo(
() => yardsForSide(destinationCountry, originYard),
[yardsForSide, destinationCountry, originYard],
);
const origin = referenceData?.yard.find((y) => y.id === originYard);
const dest = referenceData?.yard.find((y) => y.id === destinationYard);
@@ -123,6 +133,25 @@ export function Step4Route({
}
}, [destinationCountry, dest, form]);
// Same cleanup for the extra contract routes: when the operation type changes,
// clear any extra-route yard whose country no longer matches the required side
// so an added route can't contradict the operation either.
useEffect(() => {
watchedExtraRoutes.forEach((route, i) => {
const ro = referenceData?.yard.find((y) => y.id === route?.originYard);
if (originCountry && ro && ro.country !== originCountry) {
form.setValue(`extraRoutes.${i}.originYard`, "");
}
const rd = referenceData?.yard.find(
(y) => y.id === route?.destinationYard,
);
if (destinationCountry && rd && rd.country !== destinationCountry) {
form.setValue(`extraRoutes.${i}.destinationYard`, "");
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [originCountry, destinationCountry, referenceData, form]);
const directionStyle: Record<string, string> = {
EXPORT: "bg-sky-50 text-sky-800 border-sky-200",
IMPORT: "bg-amber-50 text-amber-800 border-amber-200",
@@ -263,57 +292,70 @@ export function Step4Route({
cover.
</Text>
<Stack gap={12}>
{extraRoutes.map((rf, i) => (
<Group
key={rf.id}
gap={10}
align="flex-start"
wrap="nowrap"
className="rounded-xl"
style={{ border: "1px solid #E6ECF2", padding: 12 }}
>
<Box style={{ flex: 1 }}>
<Controller
name={`extraRoutes.${i}.originYard`}
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Origin"
placeholder="Origin..."
data={yardOptions}
/>
)}
/>
</Box>
<Box style={{ flex: 1 }}>
<Controller
name={`extraRoutes.${i}.destinationYard`}
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Destination"
placeholder="Destination..."
data={yardOptions}
/>
)}
/>
</Box>
<Button
variant="subtle"
color="red"
size="xs"
mt={24}
px={6}
onClick={() => removeRoute(i)}
{extraRoutes.map((rf, i) => {
// Each extra route is constrained by the SAME operation type as the
// primary route: its origin must sit in originCountry and its
// destination in destinationCountry. Watch this row's current values
// so each side also excludes the yard picked on the other side.
const rowOrigin = watchedExtraRoutes[i]?.originYard ?? "";
const rowDestination =
watchedExtraRoutes[i]?.destinationYard ?? "";
const rowOriginData = yardsForSide(originCountry, rowDestination);
const rowDestData = yardsForSide(destinationCountry, rowOrigin);
return (
<Group
key={rf.id}
gap={10}
align="flex-start"
wrap="nowrap"
className="rounded-xl"
style={{ border: "1px solid #E6ECF2", padding: 12 }}
>
<Trash2 size={16} />
</Button>
</Group>
))}
<Box style={{ flex: 1 }}>
<Controller
name={`extraRoutes.${i}.originYard`}
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Origin"
placeholder="Origin..."
disabled={stationSelectDisabled}
data={rowOriginData}
/>
)}
/>
</Box>
<Box style={{ flex: 1 }}>
<Controller
name={`extraRoutes.${i}.destinationYard`}
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Destination"
placeholder="Destination..."
disabled={stationSelectDisabled}
data={rowDestData}
/>
)}
/>
</Box>
<Button
variant="subtle"
color="red"
size="xs"
mt={24}
px={6}
onClick={() => removeRoute(i)}
>
<Trash2 size={16} />
</Button>
</Group>
);
})}
</Stack>
</Box>
)}