Merge pull request #1096 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-08-03 22:53:30 +03:00
committed by GitHub
21 changed files with 615 additions and 28 deletions

View File

@@ -2,10 +2,6 @@ import { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import {
useAllExternalUsers,
userTypeEnum,
} from "@/super-admin/hooks/useExternalUsers";
import {
ALL_TRADE_DIRECTIONS,
TRADE_DIRECTION_LABELS,
@@ -39,9 +35,9 @@ export default function TradeAccessPage() {
const queryClient = useQueryClient();
const [search, setSearch] = useState("");
const { data: usersResponse, isLoading: usersLoading } = useAllExternalUsers({
userType: userTypeEnum.employee,
take: 3000,
const { data: usersResponse, isLoading: usersLoading } = useQuery({
queryKey: ["staff-users", "employees"],
queryFn: userTradeAccessService.employees,
});
const { data: configs, isLoading: configsLoading } = useQuery({

View File

@@ -60,12 +60,16 @@ interface CargoNode extends RuleEngineRecord {
wagonTypes?: { id: string; code?: string; name?: string }[];
/** PER_ITEM only: whole items that physically fit one wagon, keyed by wagon-type id. */
itemsPerWagonMap?: Record<string, number> | null;
/** PER_TON only: max tons of this cargo per wagon, keyed by wagon-type id. */
tonsPerWagonMap?: Record<string, number> | null;
isActive?: boolean;
displayOrder?: number;
}
/** Form-value prefix for the per-wagon-type items-fit inputs (PER_ITEM cargo). */
const ITEMS_FIT_PREFIX = "itemsFit__";
/** Form-value prefix for the per-wagon-type tonnage-cap inputs (PER_TON cargo). */
const TONS_CAP_PREFIX = "tonsCap__";
const str = (v: unknown): string => (v == null ? "" : String(v));
const orderOf = (n: CargoNode): number => Number(n.displayOrder ?? 0);
@@ -159,8 +163,26 @@ const CargoTypesPage = () => {
getInitialValue: (record) =>
(record as CargoNode).itemsPerWagonMap?.[opt.value],
}));
// PER_TON cargo: an OPTIONAL "max tons per wagon" per selected wagon type —
// how much of this commodity actually rides one wagon, which can be less
// than its rating (sugar 50T on a 70T wagon, so 200T takes 4 wagons not 3).
// Left blank the wagon's full rated capacity applies, so existing cargo is
// unaffected; the API rejects a value above the rating.
const tonsCapFields: FormFieldDef[] = (wagonTypeOptions ?? []).map((opt) => ({
name: `${TONS_CAP_PREFIX}${opt.value}`,
label: `Max tons per ${opt.label} wagon`,
type: "number",
optional: true,
placeholder: "Blank = full wagon capacity",
showIf: (values) =>
values.unitOfMeasure === "PER_TON" &&
Array.isArray(values.wagonTypeIds) &&
(values.wagonTypeIds as string[]).includes(opt.value),
getInitialValue: (record) =>
(record as CargoNode).tonsPerWagonMap?.[opt.value],
}));
const wagonTypesAt = base.findIndex((field) => field.name === "wagonTypeIds");
base.splice(wagonTypesAt + 1, 0, ...fitFields);
base.splice(wagonTypesAt + 1, 0, ...fitFields, ...tonsCapFields);
return base;
}, [wagonTypeOptions]);
@@ -230,14 +252,22 @@ const CargoTypesPage = () => {
// none are visible (not PER_ITEM) so an update clears stale fits.
const payload: Record<string, unknown> = {};
const itemsPerWagonMap: Record<string, number> = {};
const tonsPerWagonMap: Record<string, number> = {};
for (const [key, value] of Object.entries(values)) {
if (key.startsWith(ITEMS_FIT_PREFIX)) {
itemsPerWagonMap[key.slice(ITEMS_FIT_PREFIX.length)] = Number(value);
} else if (key.startsWith(TONS_CAP_PREFIX)) {
// Blank means "no cap" (use the full rated capacity), so an empty input
// must stay OUT of the map — sending 0 would be a zero-ton wagon.
if (value !== "" && value !== null && value !== undefined) {
tonsPerWagonMap[key.slice(TONS_CAP_PREFIX.length)] = Number(value);
}
} else {
payload[key] = value;
}
}
payload.itemsPerWagonMap = Object.keys(itemsPerWagonMap).length ? itemsPerWagonMap : null;
payload.tonsPerWagonMap = Object.keys(tonsPerWagonMap).length ? tonsPerWagonMap : null;
// Add always attaches to the page we're on; edit keeps the node's parent.
if (formMode?.kind === "create" && current) {
payload.parentGroupId = current.id;

View File

@@ -25,7 +25,27 @@ export interface MyTradeAccess {
directions: TradeDirection[];
}
export interface StaffUser {
id: string;
/** Localized jsonb on iam.users — not a plain string. */
name: { en?: string; am?: string } | null;
username: string;
email: string | null;
}
export const userTradeAccessService = {
/**
* Employees to assign scopes to. Served by the freight API rather than IAM's
* `/users/filter`, which 400s on its own pagination params (its @Query() DTO
* omits skip/take/orderBy while the global whitelist pipe rejects them).
*/
employees: async (): Promise<{ items: StaffUser[] }> =>
(
await client.get("/staff/users", {
params: { userType: "employee", pageSize: 100 },
})
).data,
/** All configured per-user scopes (admin only). */
list: async (): Promise<UserTradeAccessRow[]> =>
(await client.get("/user-trade-access")).data,