mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 16:00:56 +00:00
feat(rates): multi-tier distance band entry in last-mile rate form
- tierList field type in rule-engine form dialog (add/remove rows, overlap + open-ended validation, From km auto-continues) - create submits one rate row per tier sequentially - editing a band row keeps the single From/To/value form
This commit is contained in:
@@ -306,7 +306,34 @@ const RuleEngineResourcePage = () => {
|
||||
|
||||
const formFields = useMemo(() => {
|
||||
if (!config) return [];
|
||||
return config.formFields.map((field) => {
|
||||
// Last-mile container bands: creating uses the multi-row tier list (one
|
||||
// rate per tier); editing an existing band row keeps the single
|
||||
// From/To/value fields (a rate row IS one band).
|
||||
const bandFields = config.formFields.filter((field) => {
|
||||
if (config.slug !== "rates") return true;
|
||||
if (field.type === "tierList") return !editing;
|
||||
if (editing) return true;
|
||||
return field.name !== "minKm" && field.name !== "maxKm";
|
||||
});
|
||||
return bandFields.map((field) => {
|
||||
// On create, the tier rows carry the per-band rate values — the single
|
||||
// last-mile "Rate value" field then only applies to bulk mode.
|
||||
if (
|
||||
config.slug === "rates" &&
|
||||
!editing &&
|
||||
field.name === "rateValue" &&
|
||||
field.showWhen?.field === "appliesTo" &&
|
||||
field.showWhen.equals.includes("LAST_MILE")
|
||||
) {
|
||||
return {
|
||||
...field,
|
||||
showWhen: undefined,
|
||||
showIf: (values: Record<string, unknown>) =>
|
||||
values.appliesTo === "LAST_MILE" && values.lastMileMode === "BULK",
|
||||
};
|
||||
}
|
||||
return field;
|
||||
}).map((field) => {
|
||||
if (isPriorityRules && field.name === "minWagonCount") {
|
||||
return {
|
||||
...field,
|
||||
@@ -624,6 +651,31 @@ const RuleEngineResourcePage = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Container-mode create: the tier list becomes one rate row per tier,
|
||||
// created sequentially so an overlap/duplicate rejection stops the batch
|
||||
// with its own toast instead of half-failing in parallel.
|
||||
const tiers = (
|
||||
payload as {
|
||||
tiers?: Array<{ minKm: number; maxKm: number | null; rateValue: number }>;
|
||||
}
|
||||
).tiers;
|
||||
if (!editing?.id && Array.isArray(tiers)) {
|
||||
const { tiers: _omitted, ...base } = payload as Record<string, unknown>;
|
||||
void _omitted;
|
||||
void (async () => {
|
||||
try {
|
||||
for (const tier of tiers) {
|
||||
await create.mutateAsync({ ...base, ...tier });
|
||||
}
|
||||
setFormOpen(false);
|
||||
setEditing(null);
|
||||
} catch {
|
||||
// The create mutation already toasted the failure; keep the dialog
|
||||
// open so the admin can fix the tier set and retry.
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
} else if (isPriorityRules) {
|
||||
// Label is required by the backend but hidden in the UI for now.
|
||||
payload = { ...values, label: String(Date.now()) };
|
||||
|
||||
@@ -17,7 +17,7 @@ export type ColumnFormat =
|
||||
| "entityLabel"
|
||||
| "rateLabel";
|
||||
|
||||
export type FormFieldType = "text" | "number" | "boolean" | "date" | "email" | "select" | "multiselect" | "textarea" | "radio";
|
||||
export type FormFieldType = "text" | "number" | "boolean" | "date" | "email" | "select" | "multiselect" | "textarea" | "radio" | "tierList";
|
||||
|
||||
export interface ResourceColumn {
|
||||
id: string;
|
||||
@@ -1022,6 +1022,19 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
showWhen: { field: "appliesTo", equals: ["LAST_MILE"] },
|
||||
getInitialValue: (record) => String(record.currency ?? "ETB"),
|
||||
},
|
||||
// ── Distance tiers (create only — the page swaps this for the single
|
||||
// From/To/value fields when editing an existing band row). Each tier
|
||||
// becomes its own rate row, so every band keeps edit/delete/approval. ──
|
||||
{
|
||||
name: "tiers",
|
||||
label: "Distance tiers",
|
||||
type: "tierList",
|
||||
required: true,
|
||||
description:
|
||||
"One rate per distance range. To km is exclusive (0–30 then 30+); leave the last tier's To km empty for no upper limit.",
|
||||
showIf: (v) =>
|
||||
v.appliesTo === "LAST_MILE" && v.lastMileMode === "CONTAINER",
|
||||
},
|
||||
// ── Container type — Container freight, container-kind intercity, and
|
||||
// the empty-container return surcharge (20ft vs 40ft price differently) ─
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user