mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
- Add ShippingLineBookingsPage for listing and managing shipping line bookings. - Create ShippingLineDocumentsModal for document uploads related to bookings. - Introduce ShippingLineInitiateModal for initiating new shipping line bookings. - Implement booking document state management with booking-doc-state utility. - Add shipping line bookings service for API interactions. - Update index to export new components and services. - Enhance types for freight to include shipping line credits.
439 lines
14 KiB
TypeScript
439 lines
14 KiB
TypeScript
import { api as client } from "@/auth/http";
|
|
import { URL_CONSTANTS } from "@/constants/URLS";
|
|
import type {
|
|
RuleEngineListMeta,
|
|
RuleEngineListResult,
|
|
RuleEngineRecord,
|
|
RuleEngineResourceSlug,
|
|
} from "@/types/rule-engine";
|
|
import { unwrap } from "@/utils/endpoint";
|
|
|
|
export interface RuleEngineListParams {
|
|
search?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
isActive?: boolean;
|
|
status?: string;
|
|
sortBy?: string;
|
|
sortOrder?: "ASC" | "DESC";
|
|
requiresDirectorApproval?: boolean;
|
|
/** Rates category tabs — comma-separated appliesTo / trigger filters. */
|
|
appliesTo?: string;
|
|
trigger?: string;
|
|
/**
|
|
* Rates only: "true" lists shipping-line rates, "false" standard customer
|
|
* ones. Omitted lists both.
|
|
*/
|
|
isShippingLineRate?: string;
|
|
}
|
|
|
|
export interface RuleEngineReorderPayload {
|
|
ids: string[];
|
|
requiresDirectorApproval?: boolean;
|
|
}
|
|
|
|
/** Priority-rule approval workflow (all priority-config changes go through it). */
|
|
const PRIORITY_RULE_CHANGES_BASE = "/priority-rule-change-requests";
|
|
|
|
export interface PriorityRuleChangeRequest {
|
|
id: string;
|
|
action: "CREATE" | "UPDATE" | "DELETE";
|
|
priorityConfigId: string | null;
|
|
priorityConfig?: RuleEngineRecord | null;
|
|
payload: Record<string, unknown> | null;
|
|
status: "PENDING" | "APPROVED" | "REJECTED";
|
|
requestedByUserId: string | null;
|
|
decidedByUserId: string | null;
|
|
decidedAt: string | null;
|
|
decisionNote: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface SubmitPriorityRuleChangePayload {
|
|
action: "CREATE" | "UPDATE" | "DELETE";
|
|
priorityConfigId?: string;
|
|
create?: Record<string, unknown>;
|
|
update?: Record<string, unknown>;
|
|
}
|
|
|
|
/** Approval workflow for edits to LIVE rates — a DRAFT rate still edits directly. */
|
|
const RATE_CHANGES_BASE = "/rate-change-requests";
|
|
|
|
export interface RateChangeRequest {
|
|
id: string;
|
|
rateId: string;
|
|
rate?: RuleEngineRecord | null;
|
|
/** Changed fields only. */
|
|
payload: Record<string, unknown>;
|
|
/** What those same fields were when the change was filed. */
|
|
previousValues: Record<string, unknown>;
|
|
status: "PENDING" | "APPROVED" | "REJECTED";
|
|
requestedByUserId: string | null;
|
|
decidedByUserId: string | null;
|
|
decidedAt: string | null;
|
|
decisionNote: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface SubmitRateChangePayload {
|
|
rateId: string;
|
|
update: Record<string, unknown>;
|
|
}
|
|
|
|
/** One selectable IAM position type, as returned by /approval-rules/position-types. */
|
|
export interface ApprovalPositionType {
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
const RESOURCE_BASE: Record<RuleEngineResourceSlug, string> = {
|
|
"cargo-types": URL_CONSTANTS.RULE_ENGINE.CARGO_TYPES,
|
|
"container-types": URL_CONSTANTS.RULE_ENGINE.CONTAINER_TYPES,
|
|
"wagon-types": URL_CONSTANTS.RULE_ENGINE.WAGON_TYPES,
|
|
"truck-types": URL_CONSTANTS.RULE_ENGINE.TRUCK_TYPES,
|
|
"priority-configs": URL_CONSTANTS.RULE_ENGINE.PRIORITY_CONFIGS,
|
|
"service-types": URL_CONSTANTS.RULE_ENGINE.SERVICE_TYPES,
|
|
"weight-limit-rules": URL_CONSTANTS.RULE_ENGINE.WEIGHT_LIMIT_RULES,
|
|
yards: URL_CONSTANTS.RULE_ENGINE.YARDS,
|
|
"yard-distances": URL_CONSTANTS.RULE_ENGINE.YARD_DISTANCES,
|
|
"shipping-lines": URL_CONSTANTS.RULE_ENGINE.SHIPPING_LINES,
|
|
rates: URL_CONSTANTS.RULE_ENGINE.RATES,
|
|
"approval-rules": URL_CONSTANTS.RULE_ENGINE.APPROVAL_RULES,
|
|
"transit-agents": URL_CONSTANTS.RULE_ENGINE.TRANSIT_AGENTS,
|
|
};
|
|
|
|
const byIdPath = (resource: RuleEngineResourceSlug, id: string): string => {
|
|
switch (resource) {
|
|
case "cargo-types":
|
|
return URL_CONSTANTS.RULE_ENGINE.CARGO_TYPE_BY_ID(id);
|
|
case "container-types":
|
|
return URL_CONSTANTS.RULE_ENGINE.CONTAINER_TYPE_BY_ID(id);
|
|
case "wagon-types":
|
|
return URL_CONSTANTS.RULE_ENGINE.WAGON_TYPE_BY_ID(id);
|
|
case "truck-types":
|
|
return URL_CONSTANTS.RULE_ENGINE.TRUCK_TYPE_BY_ID(id);
|
|
case "priority-configs":
|
|
return URL_CONSTANTS.RULE_ENGINE.PRIORITY_CONFIG_BY_ID(id);
|
|
case "service-types":
|
|
return URL_CONSTANTS.RULE_ENGINE.SERVICE_TYPE_BY_ID(id);
|
|
case "weight-limit-rules":
|
|
return URL_CONSTANTS.RULE_ENGINE.WEIGHT_LIMIT_RULE_BY_ID(id);
|
|
case "yards":
|
|
return URL_CONSTANTS.RULE_ENGINE.YARD_BY_ID(id);
|
|
case "yard-distances":
|
|
return URL_CONSTANTS.RULE_ENGINE.YARD_DISTANCE_BY_ID(id);
|
|
case "shipping-lines":
|
|
return URL_CONSTANTS.RULE_ENGINE.SHIPPING_LINE_BY_ID(id);
|
|
case "rates":
|
|
return URL_CONSTANTS.RULE_ENGINE.RATE_BY_ID(id);
|
|
case "approval-rules":
|
|
return URL_CONSTANTS.RULE_ENGINE.APPROVAL_RULE_BY_ID(id);
|
|
default:
|
|
return `${RESOURCE_BASE[resource]}/${id}`;
|
|
}
|
|
};
|
|
|
|
const defaultMeta = (
|
|
dataLength: number,
|
|
page = 1,
|
|
pageSize = 10,
|
|
): RuleEngineListMeta => {
|
|
const totalPages = Math.max(1, Math.ceil(dataLength / pageSize));
|
|
return {
|
|
total: dataLength,
|
|
page,
|
|
pageSize,
|
|
totalPages,
|
|
hasNextPage: page < totalPages,
|
|
hasPreviousPage: page > 1,
|
|
};
|
|
};
|
|
|
|
/** Standard envelope from the shared pagination toolkit: `{ items, meta }`. */
|
|
const isItemsEnvelope = <T extends RuleEngineRecord>(
|
|
value: unknown,
|
|
): value is RuleEngineListResult<T> =>
|
|
Boolean(value) &&
|
|
typeof value === "object" &&
|
|
Array.isArray((value as { items?: unknown }).items);
|
|
|
|
/** Legacy envelope (`{ data, meta }`) — still returned by wagon-types. */
|
|
const isLegacyEnvelope = <T extends RuleEngineRecord>(
|
|
value: unknown,
|
|
): value is { data: T[]; meta?: RuleEngineListMeta } =>
|
|
Boolean(value) &&
|
|
typeof value === "object" &&
|
|
Array.isArray((value as { data?: unknown }).data);
|
|
|
|
const normalizeList = <T extends RuleEngineRecord>(
|
|
payload: unknown,
|
|
page = 1,
|
|
pageSize = 10,
|
|
): RuleEngineListResult<T> => {
|
|
const candidates: unknown[] = [payload, unwrap(payload as { data: unknown })];
|
|
|
|
for (const body of candidates) {
|
|
if (isItemsEnvelope<T>(body)) {
|
|
return {
|
|
items: body.items,
|
|
meta: body.meta ?? defaultMeta(body.items.length, page, pageSize),
|
|
};
|
|
}
|
|
if (isLegacyEnvelope<T>(body)) {
|
|
return {
|
|
items: body.data,
|
|
meta: body.meta ?? defaultMeta(body.data.length, page, pageSize),
|
|
};
|
|
}
|
|
if (Array.isArray(body)) {
|
|
return {
|
|
items: body as T[],
|
|
meta: defaultMeta(body.length, page, pageSize),
|
|
};
|
|
}
|
|
}
|
|
|
|
return { items: [], meta: defaultMeta(0, page, pageSize) };
|
|
};
|
|
|
|
const normalizeEntity = <T extends RuleEngineRecord>(payload: unknown): T => {
|
|
return unwrap(payload as { data: T }) as T;
|
|
};
|
|
|
|
export const ruleEngineService = {
|
|
list: async <T extends RuleEngineRecord>(
|
|
resource: RuleEngineResourceSlug,
|
|
params?: RuleEngineListParams,
|
|
): Promise<RuleEngineListResult<T>> => {
|
|
const page = params?.page ?? 1;
|
|
const pageSize = params?.pageSize ?? 10;
|
|
const response = await client.get(RESOURCE_BASE[resource], {
|
|
params: {
|
|
page,
|
|
pageSize,
|
|
search: params?.search,
|
|
isActive: params?.isActive,
|
|
status: params?.status,
|
|
sortBy: params?.sortBy,
|
|
sortOrder: params?.sortOrder,
|
|
requiresDirectorApproval: params?.requiresDirectorApproval,
|
|
appliesTo: params?.appliesTo,
|
|
trigger: params?.trigger,
|
|
isShippingLineRate: params?.isShippingLineRate,
|
|
},
|
|
});
|
|
return normalizeList<T>(response.data, page, pageSize);
|
|
},
|
|
|
|
/**
|
|
* Fetch every row of a resource by walking the pages. The API caps pageSize
|
|
* at 100, so option/dropdown consumers that used to ask for 200-500 rows in
|
|
* one shot go through here instead of getting silently capped (or a 400).
|
|
*/
|
|
listAll: async <T extends RuleEngineRecord>(
|
|
resource: RuleEngineResourceSlug,
|
|
params?: Omit<RuleEngineListParams, "page" | "pageSize">,
|
|
): Promise<T[]> => {
|
|
const pageSize = 100;
|
|
const first = await ruleEngineService.list<T>(resource, {
|
|
...params,
|
|
page: 1,
|
|
pageSize,
|
|
});
|
|
const items = [...first.items];
|
|
const totalPages = first.meta.totalPages ?? 1;
|
|
for (let page = 2; page <= totalPages; page += 1) {
|
|
const next = await ruleEngineService.list<T>(resource, {
|
|
...params,
|
|
page,
|
|
pageSize,
|
|
});
|
|
items.push(...next.items);
|
|
}
|
|
return items;
|
|
},
|
|
|
|
getById: async <T extends RuleEngineRecord>(
|
|
resource: RuleEngineResourceSlug,
|
|
id: string,
|
|
): Promise<T> => {
|
|
const response = await client.get(byIdPath(resource, id));
|
|
return normalizeEntity<T>(response.data);
|
|
},
|
|
|
|
create: async <T extends RuleEngineRecord>(
|
|
resource: RuleEngineResourceSlug,
|
|
payload: Record<string, unknown>,
|
|
): Promise<T> => {
|
|
const response = await client.post(RESOURCE_BASE[resource], payload);
|
|
return normalizeEntity<T>(response.data);
|
|
},
|
|
|
|
update: async <T extends RuleEngineRecord>(
|
|
resource: RuleEngineResourceSlug,
|
|
id: string,
|
|
payload: Record<string, unknown>,
|
|
): Promise<T> => {
|
|
const response = await client.patch(byIdPath(resource, id), payload);
|
|
return normalizeEntity<T>(response.data);
|
|
},
|
|
|
|
remove: async (
|
|
resource: RuleEngineResourceSlug,
|
|
id: string,
|
|
): Promise<void> => {
|
|
await client.delete(byIdPath(resource, id));
|
|
},
|
|
|
|
reorder: async (
|
|
resource: RuleEngineResourceSlug,
|
|
payload: RuleEngineReorderPayload,
|
|
): Promise<void> => {
|
|
await client.post(`${RESOURCE_BASE[resource]}/reorder`, payload);
|
|
},
|
|
|
|
moveOrder: async (
|
|
resource: RuleEngineResourceSlug,
|
|
id: string,
|
|
direction: "up" | "down",
|
|
): Promise<void> => {
|
|
await client.post(`${byIdPath(resource, id)}/move-order`, { direction });
|
|
},
|
|
|
|
submitRate: async <T extends RuleEngineRecord>(id: string): Promise<T> => {
|
|
const response = await client.post(
|
|
URL_CONSTANTS.RULE_ENGINE.RATE_SUBMIT(id),
|
|
);
|
|
return normalizeEntity<T>(response.data);
|
|
},
|
|
|
|
approveRate: async <T extends RuleEngineRecord>(id: string): Promise<T> => {
|
|
const response = await client.post(
|
|
URL_CONSTANTS.RULE_ENGINE.RATE_APPROVE(id),
|
|
);
|
|
return normalizeEntity<T>(response.data);
|
|
},
|
|
|
|
/** File a priority-rule change (create/update/delete) for approval. */
|
|
submitPriorityRuleChange: async (
|
|
payload: SubmitPriorityRuleChangePayload,
|
|
): Promise<PriorityRuleChangeRequest> => {
|
|
const response = await client.post(PRIORITY_RULE_CHANGES_BASE, payload);
|
|
return unwrap(response.data) as PriorityRuleChangeRequest;
|
|
},
|
|
|
|
listPriorityRuleChanges: async (
|
|
status?: PriorityRuleChangeRequest["status"],
|
|
): Promise<PriorityRuleChangeRequest[]> => {
|
|
const response = await client.get(PRIORITY_RULE_CHANGES_BASE, {
|
|
params: status ? { status } : undefined,
|
|
});
|
|
const body = unwrap(response.data) as unknown;
|
|
return Array.isArray(body) ? (body as PriorityRuleChangeRequest[]) : [];
|
|
},
|
|
|
|
approvePriorityRuleChange: async (
|
|
id: string,
|
|
decisionNote?: string,
|
|
): Promise<PriorityRuleChangeRequest> => {
|
|
const response = await client.post(
|
|
`${PRIORITY_RULE_CHANGES_BASE}/${id}/approve`,
|
|
{ decisionNote },
|
|
);
|
|
return unwrap(response.data) as PriorityRuleChangeRequest;
|
|
},
|
|
|
|
rejectPriorityRuleChange: async (
|
|
id: string,
|
|
decisionNote?: string,
|
|
): Promise<PriorityRuleChangeRequest> => {
|
|
const response = await client.post(
|
|
`${PRIORITY_RULE_CHANGES_BASE}/${id}/reject`,
|
|
{ decisionNote },
|
|
);
|
|
return unwrap(response.data) as PriorityRuleChangeRequest;
|
|
},
|
|
|
|
/** Propose a change to a LIVE rate — it stays at its current value until approved. */
|
|
submitRateChange: async (
|
|
payload: SubmitRateChangePayload,
|
|
): Promise<RateChangeRequest> => {
|
|
const response = await client.post(RATE_CHANGES_BASE, payload);
|
|
return unwrap(response.data) as RateChangeRequest;
|
|
},
|
|
|
|
listRateChanges: async (
|
|
status?: RateChangeRequest["status"],
|
|
): Promise<RateChangeRequest[]> => {
|
|
const response = await client.get(RATE_CHANGES_BASE, {
|
|
params: status ? { status } : undefined,
|
|
});
|
|
const body = unwrap(response.data) as unknown;
|
|
return Array.isArray(body) ? (body as RateChangeRequest[]) : [];
|
|
},
|
|
|
|
approveRateChange: async (
|
|
id: string,
|
|
decisionNote?: string,
|
|
): Promise<RateChangeRequest> => {
|
|
const response = await client.post(`${RATE_CHANGES_BASE}/${id}/approve`, {
|
|
decisionNote,
|
|
});
|
|
return unwrap(response.data) as RateChangeRequest;
|
|
},
|
|
|
|
rejectRateChange: async (
|
|
id: string,
|
|
decisionNote?: string,
|
|
): Promise<RateChangeRequest> => {
|
|
const response = await client.post(`${RATE_CHANGES_BASE}/${id}/reject`, {
|
|
decisionNote,
|
|
});
|
|
return unwrap(response.data) as RateChangeRequest;
|
|
},
|
|
|
|
/**
|
|
* IAM position types that an approval step can require/block. Replaces the
|
|
* old hardcoded LINE_STAFF/DIRECTOR/CEO triple — the chain is configured from
|
|
* whatever positions IAM actually defines.
|
|
*/
|
|
getApprovalPositionTypes: async (): Promise<ApprovalPositionType[]> => {
|
|
const response = await client.get(
|
|
URL_CONSTANTS.RULE_ENGINE.APPROVAL_RULES_POSITION_TYPES,
|
|
);
|
|
const body = unwrap(response.data) as unknown;
|
|
if (Array.isArray(body)) return body as ApprovalPositionType[];
|
|
if (
|
|
body &&
|
|
typeof body === "object" &&
|
|
"data" in body &&
|
|
Array.isArray((body as { data: unknown }).data)
|
|
) {
|
|
return (body as { data: ApprovalPositionType[] }).data;
|
|
}
|
|
return [];
|
|
},
|
|
|
|
getApprovalChain: async (
|
|
requiresDirectorApproval = true,
|
|
): Promise<RuleEngineRecord[]> => {
|
|
const response = await client.get(
|
|
URL_CONSTANTS.RULE_ENGINE.APPROVAL_RULES_CHAIN,
|
|
{
|
|
params: { requiresDirectorApproval },
|
|
},
|
|
);
|
|
const body = unwrap(response.data) as unknown;
|
|
if (Array.isArray(body)) return body as RuleEngineRecord[];
|
|
if (
|
|
body &&
|
|
typeof body === "object" &&
|
|
"data" in body &&
|
|
Array.isArray((body as { data: unknown }).data)
|
|
) {
|
|
return (body as { data: RuleEngineRecord[] }).data;
|
|
}
|
|
return [];
|
|
},
|
|
};
|