update joins in repository services to use entity classes and enhance booking form with hazardous/reefer toggles

This commit is contained in:
Marshal
2026-07-04 04:59:51 +00:00
parent c650a2dcbd
commit f37078d51d
21 changed files with 478 additions and 86 deletions

View File

@@ -21,6 +21,8 @@ interface AuthEmployeePosition {
isDelegate?: boolean;
parentPositionId?: string | null;
permissions?: AuthPermission[];
/** Some IAM payloads nest the position record instead of flattening its key. */
position?: { id?: string; key?: string; name?: LocaleText };
}
interface AuthEmployeeRecord {

View File

@@ -174,6 +174,23 @@ export const useContainerTypeOptions = (
buildContainerTypeSelectOptions(result.data ?? [], includeNone),
});
/**
* Active wagon-type options for the cargo-type / container-type "Wagon type"
* picker. The FK the selection sets drives train-scheduling wagon resolution.
*/
export const useWagonTypeOptions = (enabled = true) =>
useQuery({
...api.wagonTypes.list.queryOptions(),
enabled,
select: (rows: { id: string; code: string; name: string; isActive?: boolean }[]) =>
rows
.filter((wt) => wt.isActive !== false)
.map((wt) => ({
label: wt.name ? `${wt.name} (${wt.code})` : wt.code,
value: wt.id,
})),
});
const LIVE_RATE_PAGE_SIZE = 500;
export const useLiveRateOptions = (enabled = true) =>

View File

@@ -73,15 +73,23 @@ export function getPermissionKeys(user: AuthUser | null | undefined): string[] {
return [...keys];
}
/** Position keys held by the user (e.g. "ethiopian_gl", "djibouti_gl"). */
/**
* Position keys held by the user (e.g. "ethiopian_gl", "djibouti_gl").
* Tolerates IAM payload shape variants: the key flat on the employee position,
* nested under `position.key`, or the GL modeled as a role instead.
*/
export function getPositionKeys(user: AuthUser | null | undefined): string[] {
if (!user) return [];
const keys = new Set<string>();
for (const emp of user.employee ?? []) {
for (const pos of emp.positions ?? []) {
if (pos.key) keys.add(pos.key);
if (pos.position?.key) keys.add(pos.position.key);
}
}
for (const role of user.roles ?? []) {
if (role.key) keys.add(role.key);
}
return [...keys];
}

View File

@@ -41,6 +41,7 @@ import {
import {
useRuleEngineList,
useRuleEngineMutations,
useWagonTypeOptions,
} from "@/hooks/rule-engine/useRuleEngine";
import type { RuleEngineRecord } from "@/types/rule-engine";
@@ -54,6 +55,8 @@ interface CargoNode extends RuleEngineRecord {
requiresDirectorApproval?: boolean;
/** How this cargo is measured (PER_TON / PER_ITEM); null for groups/unset. */
unitOfMeasure?: string | null;
/** Wagon type FK used to carry this bulk cargo during scheduling; null if unset. */
wagonTypeId?: string | null;
isActive?: boolean;
displayOrder?: number;
}
@@ -79,6 +82,18 @@ const FORM_FIELDS: FormFieldDef[] = [
{ label: "Per item (break-bulk)", value: "PER_ITEM" },
],
},
{
// Wagon type that carries this (bulk) commodity — drives train-scheduling
// wagon resolution. Optional: leave "None" for grouping categories and
// container/legacy cargo; set it on scheduled bulk commodities.
// Options injected at render from useWagonTypeOptions.
name: "wagonTypeId",
label: "Wagon type",
type: "select",
optional: true,
placeholder: "Select wagon type (bulk cargo)",
options: [{ label: "None", value: RULE_ENGINE_SELECT_NONE }],
},
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
];
@@ -105,6 +120,24 @@ const CargoTypesPage = () => {
const { create, update, remove } = useRuleEngineMutations(CARGO_SLUG);
// Wagon-type options for the "Wagon type" picker (bulk cargo → wagon FK).
const { data: wagonTypeOptions } = useWagonTypeOptions(canManage);
const formFields = useMemo<FormFieldDef[]>(
() =>
FORM_FIELDS.map((field) =>
field.name === "wagonTypeId"
? {
...field,
options: [
{ label: "None", value: RULE_ENGINE_SELECT_NONE },
...(wagonTypeOptions ?? []),
],
}
: field,
),
[wagonTypeOptions],
);
const [search, setSearch] = useState("");
const [formMode, setFormMode] = useState<FormMode | null>(null);
const [deleteTarget, setDeleteTarget] = useState<CargoNode | null>(null);
@@ -350,7 +383,7 @@ const CargoTypesPage = () => {
? "Create a top-level cargo category."
: "Create a cargo type inside this category. It's attached here automatically."
}
fields={FORM_FIELDS}
fields={formFields}
initialRecord={formMode?.kind === "edit" ? formMode.record : null}
isSubmitting={create.isPending || update.isPending}
onSubmit={handleSubmit}

View File

@@ -33,6 +33,7 @@ import {
useCargoTypeParentOptions,
useContainerTypeOptions,
useLiveRateOptions,
useWagonTypeOptions,
useRateWorkflow,
useRuleEngineList,
useRuleEngineMutations,
@@ -151,6 +152,9 @@ const RuleEngineResourcePage = () => {
const usesLiveRateField = Boolean(
config?.formFields.some((f) => f.name === "rateId"),
);
const usesWagonTypeField = Boolean(
config?.formFields.some((f) => f.name === "wagonTypeId"),
);
const { data: cargoParentOptions, isLoading: cargoParentOptionsLoading } =
useCargoTypeParentOptions(editingId, config?.slug === "cargo-types");
@@ -160,6 +164,8 @@ const RuleEngineResourcePage = () => {
useContainerTypeOptions(config?.slug === "rates", usesContainerTypeField);
const { data: liveRateOptions, isLoading: liveRateOptionsLoading } =
useLiveRateOptions(usesLiveRateField);
const { data: wagonTypeOptions, isLoading: wagonTypeOptionsLoading } =
useWagonTypeOptions(usesWagonTypeField);
const formFields = useMemo(() => {
if (!config) return [];
@@ -193,9 +199,16 @@ const RuleEngineResourcePage = () => {
options: liveRateOptions ?? [],
};
}
if (field.name === "wagonTypeId") {
return {
...field,
type: "select" as const,
options: wagonTypeOptions ?? [],
};
}
return field;
});
}, [config, cargoParentOptions, cargoLeafOptions, containerTypeOptions, liveRateOptions]);
}, [config, cargoParentOptions, cargoLeafOptions, containerTypeOptions, liveRateOptions, wagonTypeOptions]);
const rows = data?.data ?? [];
const meta = data?.meta;
@@ -502,7 +515,8 @@ const RuleEngineResourcePage = () => {
(config.slug === "cargo-types" && cargoParentOptionsLoading) ||
(usesContainerTypeField && containerTypeOptionsLoading) ||
(usesCargoTypeField && cargoLeafOptionsLoading) ||
(usesLiveRateField && liveRateOptionsLoading)
(usesLiveRateField && liveRateOptionsLoading) ||
(usesWagonTypeField && wagonTypeOptionsLoading)
}
positionOptions={!editing ? createPositionOptions : undefined}
positionLoading={createPositionLoading}

View File

@@ -248,6 +248,14 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
formFields: [
{ name: "label", label: "Label", type: "text", required: true },
{ name: "sizeFt", label: "Size (ft)", type: "number", required: true },
// Options injected at render from useWagonTypeOptions (RuleEngineResourcePage).
{
name: "wagonTypeId",
label: "Wagon type",
type: "select",
required: true,
description: "Wagon type used to carry this container during train scheduling.",
},
{ name: "isOpenTop", label: "Open top", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
],