mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
add migration file when the app is starting
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { Module, OnApplicationBootstrap } from "@nestjs/common";
|
import { Module, OnApplicationBootstrap } from "@nestjs/common";
|
||||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||||
import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
|
import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
|
||||||
|
import { DataSource, DataSourceOptions } from "typeorm";
|
||||||
|
import { ensurePostgresSchemas } from "./config/ensure-postgres-schemas";
|
||||||
import { IamModule, DataSeeder } from "@tria-plc/iamapi-common";
|
import { IamModule, DataSeeder } from "@tria-plc/iamapi-common";
|
||||||
import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module";
|
import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module";
|
||||||
|
|
||||||
@@ -34,6 +36,14 @@ import { DemoUsersSeeder } from "./seed/demo-users.seeder";
|
|||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (config: ConfigService): TypeOrmModuleOptions =>
|
useFactory: (config: ConfigService): TypeOrmModuleOptions =>
|
||||||
config.get<TypeOrmModuleOptions>("database")!,
|
config.get<TypeOrmModuleOptions>("database")!,
|
||||||
|
dataSourceFactory: async (options) => {
|
||||||
|
if (!options) {
|
||||||
|
throw new Error("Missing TypeORM DataSource options");
|
||||||
|
}
|
||||||
|
await ensurePostgresSchemas(options as DataSourceOptions);
|
||||||
|
const dataSource = new DataSource(options as DataSourceOptions);
|
||||||
|
return dataSource.initialize();
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
SharedAuthModule,
|
SharedAuthModule,
|
||||||
IamModule.forRoot(),
|
IamModule.forRoot(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { registerAs } from "@nestjs/config";
|
import { registerAs } from "@nestjs/config";
|
||||||
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
|
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
|
||||||
|
import { join, dirname } from "path";
|
||||||
import {
|
import {
|
||||||
DefaultPosition,
|
DefaultPosition,
|
||||||
DefaultUnit,
|
DefaultUnit,
|
||||||
@@ -44,6 +45,7 @@ import {
|
|||||||
NotificationTemplate,
|
NotificationTemplate,
|
||||||
} from "@tria-plc/iamapi-common";
|
} from "@tria-plc/iamapi-common";
|
||||||
import { OrganizationSetting } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization-setting.entity";
|
import { OrganizationSetting } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization-setting.entity";
|
||||||
|
import { APPLICATION_SEARCH_PATH } from "./ensure-postgres-schemas";
|
||||||
|
|
||||||
const iamEntities = [
|
const iamEntities = [
|
||||||
DefaultPosition,
|
DefaultPosition,
|
||||||
@@ -90,26 +92,37 @@ const iamEntities = [
|
|||||||
NotificationTemplate,
|
NotificationTemplate,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const iamMigrationsGlob = join(
|
||||||
|
dirname(require.resolve("@tria-plc/iamapi-common/package.json")),
|
||||||
|
"dist/db/migrations/*.js",
|
||||||
|
);
|
||||||
|
const freightMigrationsGlob = join(__dirname, "../migrations/*.js");
|
||||||
|
|
||||||
export default registerAs(
|
export default registerAs(
|
||||||
"database",
|
"database",
|
||||||
(): TypeOrmModuleOptions => ({
|
(): TypeOrmModuleOptions => {
|
||||||
|
return {
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
host: process.env.DB_HOST ?? "localhost",
|
host: process.env.DB_HOST ?? "localhost",
|
||||||
port: parseInt(process.env.DB_PORT ?? "5433", 10),
|
port: parseInt(process.env.DB_PORT ?? "5433", 10),
|
||||||
username: process.env.DB_USER ?? "postgres",
|
username: process.env.DB_USER ?? "postgres",
|
||||||
password: process.env.DB_PASSWORD ?? "",
|
password: process.env.DB_PASSWORD ?? "",
|
||||||
database: process.env.DB_NAME ?? "edr_freight",
|
database: process.env.DB_NAME ?? "edr_freight",
|
||||||
|
schema: "public",
|
||||||
|
extra: {
|
||||||
|
options: `-c search_path=${APPLICATION_SEARCH_PATH}`,
|
||||||
|
},
|
||||||
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
|
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
migrations: [
|
migrations: [
|
||||||
// IAM schema + tables must be created before freight migrations
|
// IAM schema + tables must be created before freight migrations
|
||||||
__dirname +
|
iamMigrationsGlob,
|
||||||
"/../../node_modules/@tria-plc/iamapi-common/dist/db/migrations/*.js",
|
freightMigrationsGlob,
|
||||||
__dirname + "/../migrations/*.js",
|
|
||||||
],
|
],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
// Schema changes via migrations only (synchronize breaks ITMLS backfill on existing rows).
|
// Schema changes via migrations only (synchronize breaks ITMLS backfill on existing rows).
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: process.env.NODE_ENV === "development",
|
logging: process.env.NODE_ENV === "development",
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
50
apps/edr-freight-api/src/config/ensure-postgres-schemas.ts
Normal file
50
apps/edr-freight-api/src/config/ensure-postgres-schemas.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { DataSource, DataSourceOptions } from "typeorm";
|
||||||
|
|
||||||
|
/** Schemas required before TypeORM migrations and entity access. */
|
||||||
|
export const APPLICATION_SCHEMAS = [
|
||||||
|
"public",
|
||||||
|
"iam",
|
||||||
|
"freight",
|
||||||
|
"audit",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const APPLICATION_SEARCH_PATH = APPLICATION_SCHEMAS.join(",");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeORM creates the migrations table before any migration runs. If `public` was
|
||||||
|
* dropped, current_schema() is null and CREATE TABLE migrations fails.
|
||||||
|
* IAM/freight migrations assume their schemas already exist.
|
||||||
|
*/
|
||||||
|
export async function ensurePostgresSchemas(
|
||||||
|
options: DataSourceOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const bootstrap = new DataSource({
|
||||||
|
...options,
|
||||||
|
entities: [],
|
||||||
|
migrations: [],
|
||||||
|
migrationsRun: false,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await bootstrap.initialize();
|
||||||
|
|
||||||
|
for (const schema of APPLICATION_SCHEMAS) {
|
||||||
|
if (schema === "public") {
|
||||||
|
await bootstrap.query(`CREATE SCHEMA IF NOT EXISTS public`);
|
||||||
|
await bootstrap.query(`GRANT ALL ON SCHEMA public TO public`);
|
||||||
|
await bootstrap.query(`GRANT CREATE ON SCHEMA public TO public`);
|
||||||
|
} else {
|
||||||
|
await bootstrap.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
|
||||||
|
await bootstrap.query(`GRANT USAGE ON SCHEMA "${schema}" TO public`);
|
||||||
|
await bootstrap.query(
|
||||||
|
`GRANT CREATE ON SCHEMA "${schema}" TO public`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await bootstrap.query(
|
||||||
|
`SET search_path TO ${APPLICATION_SEARCH_PATH}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await bootstrap.destroy();
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-ITMLS baseline for fresh databases. Older environments created `freight.bookings`
|
||||||
|
* via synchronize or manual SQL; ItmlsFullSchemaRewrite only ALTERs that table.
|
||||||
|
*/
|
||||||
|
export class CreateFreightLegacyBaseline1748550000000 implements MigrationInterface {
|
||||||
|
name = 'CreateFreightLegacyBaseline1748550000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS freight`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE freight.train_status AS ENUM (
|
||||||
|
'AVAILABLE', 'SCHEDULED', 'IN_SERVICE', 'UNDER_MAINTENANCE', 'OUT_OF_SERVICE'
|
||||||
|
);
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS freight.trains (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
code VARCHAR(32) NOT NULL UNIQUE,
|
||||||
|
capacity_tons NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
status freight.train_status NOT NULL DEFAULT 'AVAILABLE',
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS freight.bookings (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
reference VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
customer_id UUID NOT NULL,
|
||||||
|
train_id UUID,
|
||||||
|
status VARCHAR(40) NOT NULL DEFAULT 'DRAFT',
|
||||||
|
scheduled_date TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
total_amount NUMERIC(14, 2) NOT NULL DEFAULT 0,
|
||||||
|
payment_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
contract_type VARCHAR(20) NOT NULL DEFAULT 'SPOT',
|
||||||
|
previous_contract_id UUID,
|
||||||
|
trade_direction VARCHAR(10) NOT NULL DEFAULT 'IMPORT',
|
||||||
|
equipment_return VARCHAR(20) NOT NULL DEFAULT 'RETURN',
|
||||||
|
first_mile_pickup_address TEXT,
|
||||||
|
last_mile_delivery_address TEXT,
|
||||||
|
cargo_total_weight_vgm NUMERIC(12, 3) NOT NULL DEFAULT 0,
|
||||||
|
is_hazardous BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
payment_currency VARCHAR(5) NOT NULL DEFAULT 'USD',
|
||||||
|
start_date DATE,
|
||||||
|
end_date DATE,
|
||||||
|
financial_terms TEXT,
|
||||||
|
version_number INT NOT NULL DEFAULT 1,
|
||||||
|
approved_by_staff_id UUID,
|
||||||
|
approved_by_staff_at TIMESTAMPTZ,
|
||||||
|
signed_by_director_id UUID,
|
||||||
|
signed_by_director_at TIMESTAMPTZ,
|
||||||
|
signed_by_ceo_id UUID,
|
||||||
|
signed_by_ceo_at TIMESTAMPTZ,
|
||||||
|
priority_score INT NOT NULL DEFAULT 0,
|
||||||
|
allow_consolidation BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
consolidation_partner_id UUID,
|
||||||
|
origin_station VARCHAR(255),
|
||||||
|
destination_station VARCHAR(255),
|
||||||
|
service_type VARCHAR(100),
|
||||||
|
freight_type VARCHAR(100),
|
||||||
|
freight_subtype VARCHAR(255),
|
||||||
|
containers JSONB,
|
||||||
|
first_mile_enabled BOOLEAN DEFAULT false,
|
||||||
|
last_mile_enabled BOOLEAN DEFAULT false,
|
||||||
|
is_refrigerated BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS freight.bookings CASCADE`);
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS freight.trains CASCADE`);
|
||||||
|
await queryRunner.query(`DROP TYPE IF EXISTS freight.train_status`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,14 @@ export class AddBookingsRemainingForeignKeys1748800000000 implements MigrationIn
|
|||||||
name = 'AddBookingsRemainingForeignKeys1748800000000';
|
name = 'AddBookingsRemainingForeignKeys1748800000000';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const publicCustomersExists = await queryRunner.query(`
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name = 'customers'
|
||||||
|
) AS exists
|
||||||
|
`);
|
||||||
|
const hasPublicCustomers = Boolean(publicCustomersExists[0]?.exists);
|
||||||
|
|
||||||
// ── freight.bookings: nullable FK cleanup ─────────────────────────────
|
// ── freight.bookings: nullable FK cleanup ─────────────────────────────
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
UPDATE freight.bookings b
|
UPDATE freight.bookings b
|
||||||
@@ -24,49 +32,51 @@ export class AddBookingsRemainingForeignKeys1748800000000 implements MigrationIn
|
|||||||
AND NOT EXISTS (SELECT 1 FROM freight.bookings pb WHERE pb.id = b.consolidation_partner_id);
|
AND NOT EXISTS (SELECT 1 FROM freight.bookings pb WHERE pb.id = b.consolidation_partner_id);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Remove bookings with no matching public.customers row
|
// Legacy DBs only: public.customers is created/moved in MoveCustomersToFreightSchema (174890).
|
||||||
await queryRunner.query(`
|
if (hasPublicCustomers) {
|
||||||
DELETE FROM freight.booking_cargo_modifier bcm
|
await queryRunner.query(`
|
||||||
USING freight.bookings b
|
DELETE FROM freight.booking_cargo_modifier bcm
|
||||||
WHERE bcm.booking_id = b.id
|
USING freight.bookings b
|
||||||
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
WHERE bcm.booking_id = b.id
|
||||||
`);
|
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||||
await queryRunner.query(`
|
`);
|
||||||
DELETE FROM freight.booking_approval_step bas
|
await queryRunner.query(`
|
||||||
USING freight.bookings b
|
DELETE FROM freight.booking_approval_step bas
|
||||||
WHERE bas.booking_id = b.id
|
USING freight.bookings b
|
||||||
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
WHERE bas.booking_id = b.id
|
||||||
`);
|
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||||
await queryRunner.query(`
|
`);
|
||||||
DELETE FROM freight.booking_rate_snapshot brs
|
await queryRunner.query(`
|
||||||
USING freight.bookings b
|
DELETE FROM freight.booking_rate_snapshot brs
|
||||||
WHERE brs.booking_id = b.id
|
USING freight.bookings b
|
||||||
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
WHERE brs.booking_id = b.id
|
||||||
`);
|
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||||
await queryRunner.query(`
|
`);
|
||||||
DELETE FROM freight.booking_container bc
|
await queryRunner.query(`
|
||||||
USING freight.bookings b
|
DELETE FROM freight.booking_container bc
|
||||||
WHERE bc.booking_id = b.id
|
USING freight.bookings b
|
||||||
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
WHERE bc.booking_id = b.id
|
||||||
`);
|
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||||
await queryRunner.query(`
|
`);
|
||||||
DELETE FROM freight.bookings b
|
await queryRunner.query(`
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
DELETE FROM freight.bookings b
|
||||||
`);
|
WHERE NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE freight.bookings
|
||||||
|
ADD CONSTRAINT "FK_bookings_customer_id"
|
||||||
|
FOREIGN KEY (customer_id)
|
||||||
|
REFERENCES public.customers(id)
|
||||||
|
ON DELETE RESTRICT;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// ── freight.bookings FKs ────────────────────────────────────────────
|
// ── freight.bookings FKs ────────────────────────────────────────────
|
||||||
await queryRunner.query(`
|
|
||||||
DO $$ BEGIN
|
|
||||||
ALTER TABLE freight.bookings
|
|
||||||
ADD CONSTRAINT "FK_bookings_customer_id"
|
|
||||||
FOREIGN KEY (customer_id)
|
|
||||||
REFERENCES public.customers(id)
|
|
||||||
ON DELETE RESTRICT;
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN NULL;
|
|
||||||
END $$;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
ALTER TABLE freight.bookings
|
ALTER TABLE freight.bookings
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateFreightFilesTable1749100000000 implements MigrationInterface {
|
||||||
|
name = 'CreateFreightFilesTable1749100000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS freight.files (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
resource_id UUID NOT NULL,
|
||||||
|
resource VARCHAR(100) NOT NULL,
|
||||||
|
code VARCHAR(100) NOT NULL,
|
||||||
|
name VARCHAR(500) NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
mime_type VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS "IDX_freight_files_resource"
|
||||||
|
ON freight.files (resource_id, resource);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS "IDX_freight_files_resource_code"
|
||||||
|
ON freight.files (resource_id, resource, code);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS freight.files CASCADE`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
} from "@/types/rule-engine";
|
} from "@/types/rule-engine";
|
||||||
|
|
||||||
const CARGO_TYPE_PARENT_PAGE_SIZE = 500;
|
const CARGO_TYPE_PARENT_PAGE_SIZE = 500;
|
||||||
|
const CONTAINER_TYPE_OPTIONS_PAGE_SIZE = 500;
|
||||||
|
|
||||||
export const useRuleEngineList = (
|
export const useRuleEngineList = (
|
||||||
resource: RuleEngineResourceSlug,
|
resource: RuleEngineResourceSlug,
|
||||||
@@ -52,6 +53,36 @@ export const useCargoTypeParentOptions = (excludeId?: string, enabled = true) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useContainerTypeOptions = (enabled = true) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: [
|
||||||
|
...QUERY_KEYS.RULE_ENGINE.list("container-types"),
|
||||||
|
"select-options",
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
ruleEngineService.list<RuleEngineRecord>("container-types", {
|
||||||
|
page: 1,
|
||||||
|
pageSize: CONTAINER_TYPE_OPTIONS_PAGE_SIZE,
|
||||||
|
}),
|
||||||
|
enabled,
|
||||||
|
select: (result) => {
|
||||||
|
const noneOption = { label: "None", value: RULE_ENGINE_SELECT_NONE };
|
||||||
|
const options = (result.data ?? []).map((row) => {
|
||||||
|
const label = String(row.label ?? "").trim();
|
||||||
|
const code = String(row.code ?? "").trim();
|
||||||
|
const size = row.sizeFt ? `${String(row.sizeFt)}ft` : "";
|
||||||
|
const parts = [label || code || String(row.id), size].filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: parts.join(" - "),
|
||||||
|
value: String(row.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [noneOption, ...options];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const useApprovalChain = (enabled: boolean) =>
|
export const useApprovalChain = (enabled: boolean) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: QUERY_KEYS.RULE_ENGINE.chain,
|
queryKey: QUERY_KEYS.RULE_ENGINE.chain,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
useApprovalChain,
|
useApprovalChain,
|
||||||
useCargoTypeParentOptions,
|
useCargoTypeParentOptions,
|
||||||
|
useContainerTypeOptions,
|
||||||
useRateWorkflow,
|
useRateWorkflow,
|
||||||
useRuleEngineList,
|
useRuleEngineList,
|
||||||
useRuleEngineMutations,
|
useRuleEngineMutations,
|
||||||
@@ -104,16 +105,31 @@ const RuleEngineResourcePage = () => {
|
|||||||
const editingId = editing?.id ? String(editing.id) : undefined;
|
const editingId = editing?.id ? String(editing.id) : undefined;
|
||||||
const { data: cargoParentOptions, isLoading: cargoParentOptionsLoading } =
|
const { data: cargoParentOptions, isLoading: cargoParentOptionsLoading } =
|
||||||
useCargoTypeParentOptions(editingId, config?.slug === "cargo-types");
|
useCargoTypeParentOptions(editingId, config?.slug === "cargo-types");
|
||||||
|
const { data: containerTypeOptions, isLoading: containerTypeOptionsLoading } =
|
||||||
|
useContainerTypeOptions(config?.slug === "rates");
|
||||||
|
|
||||||
const formFields = useMemo(() => {
|
const formFields = useMemo(() => {
|
||||||
if (!config || config.slug !== "cargo-types") return config?.formFields ?? [];
|
if (!config) return [];
|
||||||
const fallback = [{ label: "None", value: RULE_ENGINE_SELECT_NONE }];
|
|
||||||
return config.formFields.map((field) =>
|
return config.formFields.map((field) =>
|
||||||
field.name === "parentGroupId"
|
config.slug === "cargo-types" && field.name === "parentGroupId"
|
||||||
? { ...field, options: cargoParentOptions ?? fallback }
|
? {
|
||||||
: field,
|
...field,
|
||||||
|
options:
|
||||||
|
cargoParentOptions ?? [
|
||||||
|
{ label: "None", value: RULE_ENGINE_SELECT_NONE },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: config.slug === "rates" && field.name === "containerTypeId"
|
||||||
|
? {
|
||||||
|
...field,
|
||||||
|
options:
|
||||||
|
containerTypeOptions ?? [
|
||||||
|
{ label: "None", value: RULE_ENGINE_SELECT_NONE },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: field,
|
||||||
);
|
);
|
||||||
}, [config, cargoParentOptions]);
|
}, [config, cargoParentOptions, containerTypeOptions]);
|
||||||
|
|
||||||
const rows = data?.data ?? [];
|
const rows = data?.data ?? [];
|
||||||
const meta = data?.meta;
|
const meta = data?.meta;
|
||||||
@@ -320,7 +336,8 @@ const RuleEngineResourcePage = () => {
|
|||||||
initialRecord={editing}
|
initialRecord={editing}
|
||||||
isSubmitting={create.isPending || update.isPending}
|
isSubmitting={create.isPending || update.isPending}
|
||||||
selectOptionsLoading={
|
selectOptionsLoading={
|
||||||
config.slug === "cargo-types" && cargoParentOptionsLoading
|
(config.slug === "cargo-types" && cargoParentOptionsLoading) ||
|
||||||
|
(config.slug === "rates" && containerTypeOptionsLoading)
|
||||||
}
|
}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -333,7 +333,13 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
|||||||
],
|
],
|
||||||
formFields: [
|
formFields: [
|
||||||
{ name: "rateType", label: "Rate type", type: "select", required: true, options: RATE_TYPES },
|
{ name: "rateType", label: "Rate type", type: "select", required: true, options: RATE_TYPES },
|
||||||
{ name: "containerTypeId", label: "Container type ID", type: "text", placeholder: "Optional UUID" },
|
{
|
||||||
|
name: "containerTypeId",
|
||||||
|
label: "Container type",
|
||||||
|
type: "select",
|
||||||
|
optional: true,
|
||||||
|
placeholder: "Select container type (optional)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "tradeDirection",
|
name: "tradeDirection",
|
||||||
label: "Trade direction",
|
label: "Trade direction",
|
||||||
|
|||||||
Reference in New Issue
Block a user