feat(WIP): filtering, exporting and more reports

This commit is contained in:
Nathnael
2026-08-19 13:51:18 +00:00
parent e964a9b8f4
commit fb21ad1541
56 changed files with 3750 additions and 27 deletions

View File

@@ -4,6 +4,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EDR Freight Backoffice</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>

View File

@@ -85,6 +85,7 @@ import TrainScheduleV2DetailPage from "./pages/trainScheduling/TrainScheduleV2De
import TrainScheduleTrackPage from "./pages/trainScheduling/TrainScheduleTrackPage";
import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
import TradeAccessPage from "./pages/configuration/TradeAccessPage";
import OperationsStandardsPage from "./pages/settings/OperationsStandardsPage";
import ExchangeRateSettingsCard from "./pages/settings/ExchangeRateSettingsCard";
import FirstMilePage from "./pages/operations/FirstMilePage";
import LastMilePage from "./pages/operations/LastMilePage";
@@ -1167,6 +1168,16 @@ const App = () => {
</RequirePermission>
}
/> */}
<Route
path="configuration/operations-standards"
element={
<RequirePermission
permission={FREIGHT_PERMS.settings.operationsStandards.view}
>
<OperationsStandardsPage />
</RequirePermission>
}
/>
<Route path="configuration/cargo-types" element={<CargoTypesPage />} />
<Route
path="configuration/cargo-types/:id"

View File

@@ -555,6 +555,11 @@ export const buildSidebarSections = (
href: "/dashboard/configuration/exchange-rate",
permission: FREIGHT_PERMS.settings.exchangeRate.view,
},
{
label: "Operating standards",
href: "/dashboard/configuration/operations-standards",
permission: FREIGHT_PERMS.settings.operationsStandards.view,
},
],
},
{

View File

@@ -57,6 +57,10 @@ export const URL_CONSTANTS = {
BASE: "/exchange-settings",
},
OPERATIONS_STANDARDS: {
BASE: "/operations-standards",
},
AUDIT_LOGS: {
BASE: "/audit",
},
@@ -542,6 +546,7 @@ export const URL_CONSTANTS = {
YARD_BY_ID: (id: string) => `/yards/${id}`,
YARD_DISTANCES: "/yard-distances",
OPERATIONS_TARGETS: "/operations-targets",
YARD_DISTANCE_BY_ID: (id: string) => `/yard-distances/${id}`,
SHIPPING_LINES: "/shipping-lines",

View File

@@ -221,6 +221,8 @@ export interface YardOption {
label: string;
value: string;
country: string;
/** The yard's business code — what config keyed on a station stores. */
code: string;
}
/**
@@ -243,6 +245,7 @@ export const useYardOptions = (enabled = true) =>
label: label && code ? `${label} (${code})` : label || code || String(row.id),
value: String(row.id),
country: String(row.country ?? ""),
code,
};
}),
});

View File

@@ -0,0 +1,35 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
operationsStandardsService,
type OperationsStandardsPatch,
} from "@/services/operationsStandards.service";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
const QUERY_KEY = ["operationsStandards"];
export const useOperationsStandardsQuery = () =>
useQuery({
queryKey: QUERY_KEY,
queryFn: () => operationsStandardsService.get(),
});
export const useUpdateOperationsStandards = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
return useMutation({
mutationFn: (patch: OperationsStandardsPatch) =>
operationsStandardsService.update(patch),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
toast.success(
t("operationsStandards.updated", "Operating standards updated"),
);
},
onError: handleError,
});
};

View File

@@ -372,6 +372,12 @@ export const FREIGHT_PERMS = {
view: "edr_freight_app:settings:exchange_rate:view",
manage: "edr_freight_app:settings:exchange_rate:manage",
},
// Standard station stay, cycle and leg times, and the charged-tonnage
// factors the operations reports measure actual performance against.
operationsStandards: {
view: "edr_freight_app:settings:operations_standards:view",
manage: "edr_freight_app:settings:operations_standards:manage",
},
contractTemplates: {
view: "edr_freight_app:settings:contract_templates:view",
manage: "edr_freight_app:settings:contract_templates:manage",

View File

@@ -1,4 +1,4 @@
import { SimpleGrid, Stack } from "@mantine/core";
import { SimpleGrid, Stack, Title } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import { Navigate } from "react-router-dom";
@@ -7,28 +7,39 @@ import { ReportSection } from "@/components/reports/ReportSection";
import { api } from "@/services/api";
/**
* The revenue dashboard the reporting spec asks for, assembled from reports
* that already exist rather than a second aggregation API: each tile is a
* The dashboards the reporting specs ask for, assembled from reports that
* already exist rather than a second aggregation API: each tile is a
* `ReportSection` opened on its chart, and each one permission-gates itself by
* rendering nothing when the caller's catalog lacks that report.
*/
const TILES = [
const REVENUE_TILES = [
"revenue-by-period",
"revenue-by-category",
"revenue-by-route",
"revenue-top-customers",
];
const OPERATIONS_TILES = [
"cargo-volume-performance",
"teu-performance",
"trainset-performance",
"turnaround-cycle",
];
export default function ReportsLandingPage() {
const { data: catalog, isLoading } = useQuery(api.reports.catalog.queryOptions());
if (isLoading) return null;
const visible = TILES.filter((key) => catalog?.some((r) => r.key === key));
const visible = (keys: string[]) =>
keys.filter((key) => catalog?.some((r) => r.key === key));
// No revenue reports for this user — fall back to the old behaviour and send
// them to the first report they can actually open.
if (!visible.length) {
const revenue = visible(REVENUE_TILES);
const operations = visible(OPERATIONS_TILES);
// No dashboard reports for this user — fall back to the old behaviour and
// send them to the first report they can actually open.
if (!revenue.length && !operations.length) {
const first = catalog?.[0];
return <Navigate to={first ? `/dashboard/reports/${first.key}` : "/dashboard"} replace />;
}
@@ -37,14 +48,31 @@ export default function ReportsLandingPage() {
<PageContainer>
<Stack gap="lg">
<PageHeader
title="Revenue dashboard"
subtitle="Billed rail revenue by period, category, corridor and customer. Pick any report in the sidebar for the full table, filters and export."
title="Reports dashboard"
subtitle="Billed rail revenue and operational performance at a glance. Pick any report in the sidebar for the full table, filters and export."
/>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="lg">
{visible.map((key) => (
<ReportSection key={key} reportKey={key} defaultView="chart" />
))}
</SimpleGrid>
{revenue.length > 0 && (
<Stack gap="sm">
<Title order={3}>Revenue</Title>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="lg">
{revenue.map((key) => (
<ReportSection key={key} reportKey={key} defaultView="chart" />
))}
</SimpleGrid>
</Stack>
)}
{operations.length > 0 && (
<Stack gap="sm">
<Title order={3}>Operations</Title>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="lg">
{operations.map((key) => (
<ReportSection key={key} reportKey={key} defaultView="chart" />
))}
</SimpleGrid>
</Stack>
)}
</Stack>
</PageContainer>
);

View File

@@ -309,7 +309,9 @@ const RuleEngineResourcePage = () => {
(f) =>
f.name === "originYardId" ||
f.name === "fromYardId" ||
f.name === "toYardId",
f.name === "toYardId" ||
// Operational targets pick a station by yard code.
f.name === "dimensionKey",
),
);
const { data: yardOptions, isLoading: yardOptionsLoading } =
@@ -471,6 +473,20 @@ const RuleEngineResourcePage = () => {
.map(({ label, value }) => ({ label, value })),
};
}
// An operational target's key is a category, a container class, or a
// station's YARD CODE — never a yard id, because the reports match it
// against what their classification CASE emits.
if (field.name === "dimensionKey") {
const staticOptions = field.optionsFromValues;
return {
...field,
type: "select" as const,
optionsFromValues: (values: Record<string, unknown>) =>
String(values.dimension ?? "") === "station"
? (yardOptions ?? []).map(({ label, code }) => ({ label, value: code }))
: (staticOptions?.(values) ?? []),
};
}
if (field.name === "originYardId" || field.name === "destinationYardId") {
const end = field.name === "originYardId" ? "origin" : "destination";
return {

View File

@@ -140,6 +140,37 @@ const TRADE_DIRECTIONS = [
{ label: "Both", value: "BOTH" },
];
/**
* The cargo categories and container classes an operational target may be
* keyed on.
*
* Mirrors CARGO_CATEGORIES / CONTAINER_CLASSES in the API's
* `modules/reports/operations-classification.ts`, which is the source of truth:
* a report matches a target by this exact key, so a value here that the API
* does not emit is a plan the report will never find. The API spec
* `operations-classification.spec.ts` guards the API side of the pair.
*/
export const OPERATIONS_CARGO_CATEGORIES = [
{ label: "Multimodal container import", value: "CONTAINER_IMPORT_MULTIMODAL" },
{ label: "Unimodal container import", value: "CONTAINER_IMPORT_UNIMODAL" },
{ label: "Export container", value: "CONTAINER_EXPORT" },
{ label: "Empty container", value: "EMPTY_CONTAINER" },
{ label: "Fertilizer", value: "FERTILIZER" },
{ label: "RoRo", value: "RORO" },
{ label: "Break bulk", value: "BREAK_BULK" },
{ label: "Sand", value: "SAND" },
{ label: "Bulk", value: "BULK" },
{ label: "Other imports", value: "OTHER_IMPORT" },
{ label: "Other export cargo", value: "OTHER_EXPORT" },
];
export const OPERATIONS_CONTAINER_CLASSES = [
{ label: "Multimodal container import", value: "CONTAINER_IMPORT_MULTIMODAL" },
{ label: "Unimodal container import", value: "CONTAINER_IMPORT_UNIMODAL" },
{ label: "Full export container", value: "CONTAINER_EXPORT" },
{ label: "Empty container return", value: "EMPTY_CONTAINER_RETURN" },
];
// Mirrors the YardCountry enum in @edr/types — the only two countries on the line.
const YARD_COUNTRIES = [
{ label: "Ethiopia", value: "Ethiopia" },
@@ -463,6 +494,14 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
optional: true,
placeholder: "Select parent cargo type (optional)",
},
{
name: "fullTrainsetWagons",
label: "Wagons in a full trainset",
type: "number",
optional: true,
description:
"What the Trainset Performance report divides loaded wagons by — 37 for vehicles, 22 for sand. Leave blank to use the default in Operating standards.",
},
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
{ name: "hasLashing", label: "Charge lashing fee", type: "boolean" },
{
@@ -624,6 +663,88 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ name: "isActive", label: "Active", type: "boolean", description: "Off suspends the officer regardless of the validity window" },
],
},
{
slug: "operations-targets",
label: "Operational Targets",
category: "configuration",
subtitle:
"Planned TEU, trainsets and tonnage per period — the Plan column in the operations reports",
searchPlaceholder: "Search by category, station or note...",
supportsSearch: true,
cardTitleKey: "dimensionKey",
cardSubtitleKey: "periodStart",
columns: [
{ id: "periodStart", header: "Period start", accessorKey: "periodStart", format: "date" },
{ id: "periodType", header: "Period", accessorKey: "periodType" },
{ id: "metric", header: "Metric", accessorKey: "metric" },
{ id: "dimension", header: "Dimension", accessorKey: "dimension" },
{ id: "dimensionKey", header: "Applies to", accessorKey: "dimensionKey" },
{ id: "plannedValue", header: "Plan", accessorKey: "plannedValue", format: "number" },
],
formFields: [
{
name: "metric",
label: "Metric",
type: "select",
required: true,
options: [
{ label: "TEU", value: "TEU" },
{ label: "Trainsets", value: "TRAINSET" },
{ label: "Volume (tons)", value: "VOLUME_TONS" },
],
},
{
name: "periodType",
label: "Period",
type: "select",
required: true,
options: [
{ label: "Weekly", value: "week" },
{ label: "Monthly", value: "month" },
{ label: "Quarterly", value: "quarter" },
{ label: "Yearly", value: "year" },
],
},
{
name: "periodStart",
label: "Period start",
type: "date",
required: true,
description:
"Any date inside the period — it is snapped to the start of the week, month, quarter or year on save.",
},
{
name: "dimension",
label: "Applies to",
type: "select",
required: true,
options: [
{ label: "Cargo category", value: "cargo_category" },
{ label: "Station", value: "station" },
{ label: "Container class", value: "container_class" },
],
},
{
name: "dimensionKey",
label: "Category / station code",
type: "select",
required: true,
placeholder: "Select what the target applies to",
// The valid keys depend on the chosen dimension, and must match what the
// reports emit exactly — a typo here is a target the report never finds.
optionsFromValues: (values) => {
const dimension = String(values.dimension ?? "");
if (dimension === "container_class") return OPERATIONS_CONTAINER_CLASSES;
if (dimension === "station") return [];
return OPERATIONS_CARGO_CATEGORIES;
},
description:
"Station targets are keyed on the yard code (KALITY, MOJO, NAGAD…) — type it exactly as it appears on Yards.",
},
{ name: "plannedValue", label: "Planned value", type: "number", required: true },
{ name: "note", label: "Note", type: "text", optional: true },
],
},
{
slug: "yard-distances",
label: "Yard Distances",
@@ -637,6 +758,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ id: "fromYardLabel", header: "From yard", accessorKey: "fromYardLabel" },
{ id: "toYardLabel", header: "To yard", accessorKey: "toYardLabel" },
{ id: "distanceKm", header: "Distance (km)", accessorKey: "distanceKm", format: "number" },
{ id: "standardHours", header: "Standard (hrs)", accessorKey: "standardHours", format: "number" },
],
formFields: [
// Options injected at render from useYardOptions (RuleEngineResourcePage).
@@ -650,6 +772,14 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
description:
"Symmetric — one entry covers both directions. Route segments between these yards use this value.",
},
{
name: "standardHours",
label: "Standard running time (hrs)",
type: "number",
optional: true,
description:
"What the Train Delays report judges this leg against — 21h Negad to GMP, 20h to Adama, 20.5h to Modjo, 22h to Sebeta. Leave blank to use the default in Operating standards.",
},
],
},
{

View File

@@ -0,0 +1,282 @@
import { useState } from "react";
import { Save } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/shared/common/ui/card";
import { Input } from "@/shared/common/ui/input";
import { Button } from "@/shared/common/ui/button";
import {
useOperationsStandardsQuery,
useUpdateOperationsStandards,
} from "@/hooks/useOperationsStandards";
import type { OperationsStandards } from "@/services/operationsStandards.service";
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import { useAuth } from "@/auth/useAuth";
type Field = {
name: keyof Omit<OperationsStandards, "id" | "updatedAt">;
label: string;
hint: string;
unit: string;
integer?: boolean;
};
type Section = { title: string; description: string; fields: Field[] };
/**
* Grouped the way the reporting spec reads, so an operator changing "the
* Djibouti standard" finds it next to the Ethiopian one rather than hunting a
* flat list of fifteen numbers.
*/
const SECTIONS: Section[] = [
{
title: "Station staying time",
description:
"How long a train may stand at a station before the stop needs a reason. Used by Station Staying Time.",
fields: [
{
name: "stationStandardHoursEthiopia",
label: "Ethiopian stations",
hint: "Standard stop on the Ethiopian side",
unit: "hrs",
},
{
name: "stationStandardHoursDjibouti",
label: "Djibouti stations",
hint: "Standard stop on the Djibouti side",
unit: "hrs",
},
],
},
{
title: "Turnaround cycle",
description:
"The full out-and-back a train is expected to complete in. Used by Turnaround Cycle.",
fields: [
{
name: "cycleStandardHoursContainer",
label: "Container",
hint: "10 + 21 + 13 + 21",
unit: "hrs",
},
{
name: "cycleStandardHoursBulkDmp",
label: "Bulk via DMP",
hint: "13 + 21 + 33 + 21",
unit: "hrs",
},
{
name: "cycleStandardHoursBulkNagad",
label: "Bulk via Negad",
hint: "13 + 21 + 41 + 21",
unit: "hrs",
},
{
name: "cycleStandardHoursBulkBcc",
label: "Bulk via BCC",
hint: "13 + 21 + 41 + 21",
unit: "hrs",
},
],
},
{
title: "Delay",
description:
"Used by Train Delays when a yard pair has no standard of its own. Per-corridor times live on Yard Distances.",
fields: [
{
name: "defaultLegStandardHours",
label: "Default leg standard",
hint: "Negad to GMP is 21 hours",
unit: "hrs",
},
{
name: "delayToleranceMinutes",
label: "Tolerance",
hint: "Grace before a leg counts as delayed",
unit: "min",
integer: true,
},
],
},
{
title: "Charged volume",
description:
"The standard weight capacity cargo is charged on, as opposed to what was weighed. Used by Charged and Actual Volumes.",
fields: [
{
name: "chargedTonsFull20ft",
label: "Laden 20ft container",
hint: "Per container",
unit: "t",
},
{
name: "chargedTonsFull40ft",
label: "Laden 40ft container",
hint: "Per container",
unit: "t",
},
{
name: "chargedTonsEmpty20ft",
label: "Empty 20ft container",
hint: "Per container",
unit: "t",
},
{
name: "chargedTonsEmpty40ft",
label: "Empty 40ft container",
hint: "Per container",
unit: "t",
},
{
name: "chargedTonsPerWagonGeneral",
label: "Wagon of steel, fertilizer, rice, sugar",
hint: "Per wagon",
unit: "t",
},
{
name: "chargedTonsPerWagonPerishable",
label: "Wagon of vegetables, milk, meat, livestock",
hint: "Per wagon",
unit: "t",
},
],
},
{
title: "Trainset",
description:
"Used by Trainset Performance when a cargo type has no wagon count of its own — set those on Cargo Types.",
fields: [
{
name: "defaultFullTrainsetWagons",
label: "Wagons in a full trainset",
hint: "37 for vehicles and 22 for sand are set per cargo type",
unit: "wagons",
integer: true,
},
],
},
];
const ALL_FIELDS = SECTIONS.flatMap((s) => s.fields);
/**
* The operating standards the operations reports measure against.
*
* A single settings row rather than constants in the code, because the business
* treats these as tunable — the corridor standard is explicitly described as
* flexible. Every value here changes what a report calls on-time, encouraging,
* or on plan, so the page shows what each one drives.
*/
export default function OperationsStandardsPage() {
const { user } = useAuth();
const { data, isLoading } = useOperationsStandardsQuery();
const update = useUpdateOperationsStandards();
const [draft, setDraft] = useState<Record<string, string>>({});
const canEdit =
hasPermission(user, FREIGHT_PERMS.settings.operationsStandards.manage) ||
hasPermission(user, FREIGHT_PERMS.admin);
const valueOf = (field: Field): string =>
draft[field.name] ?? (data ? String(data[field.name] ?? "") : "");
const invalid = (field: Field): boolean => {
const raw = draft[field.name];
if (raw === undefined) return false;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) return true;
return field.integer ? !Number.isInteger(parsed) : false;
};
const anyInvalid = ALL_FIELDS.some(invalid);
const dirty = Object.keys(draft).length > 0;
const handleSave = async () => {
if (anyInvalid || !dirty) return;
const patch = Object.fromEntries(
Object.entries(draft).map(([key, value]) => [key, Number(value)]),
);
await update.mutateAsync(patch);
setDraft({});
};
return (
<div className="p-4 space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold">Operating standards</h1>
<p className="text-sm text-muted-foreground max-w-3xl">
The figures every operations report measures actual performance
against. Changing one changes what the reports call on time, over
standard, or on plan it does not change any charge a customer
pays.
</p>
</div>
<Button
onClick={handleSave}
disabled={!canEdit || !dirty || anyInvalid || update.isPending}
>
<Save className="h-4 w-4 mr-2" />
{update.isPending ? "Saving..." : "Save changes"}
</Button>
</div>
{SECTIONS.map((section) => (
<Card key={section.title}>
<CardHeader>
<CardTitle>{section.title}</CardTitle>
<CardDescription>{section.description}</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{section.fields.map((field) => (
<div key={field.name} className="space-y-1">
<label
className="text-sm font-medium"
htmlFor={`standard-${field.name}`}
>
{field.label}
</label>
<div className="flex items-center gap-2">
<Input
id={`standard-${field.name}`}
type="number"
step={field.integer ? 1 : 0.01}
min={field.integer ? 1 : 0.01}
value={valueOf(field)}
disabled={isLoading || !canEdit}
aria-invalid={invalid(field)}
onChange={(e) =>
setDraft((d) => ({ ...d, [field.name]: e.target.value }))
}
/>
<span className="text-sm text-muted-foreground w-16">
{field.unit}
</span>
</div>
<p className="text-xs text-muted-foreground">
{invalid(field)
? field.integer
? "Must be a whole number above zero"
: "Must be above zero"
: field.hint}
</p>
</div>
))}
</CardContent>
</Card>
))}
{!canEdit && (
<p className="text-sm text-muted-foreground">
You can view these standards but not change them.
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,51 @@
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
import type { ApiResponse } from "@/types/apiResponse";
const BASE = URL_CONSTANTS.OPERATIONS_STANDARDS.BASE;
/**
* The railway's operating standards — the numbers the operations reports
* measure actual performance against. One row, edited here.
*/
export interface OperationsStandards {
id: string;
stationStandardHoursEthiopia: number;
stationStandardHoursDjibouti: number;
cycleStandardHoursContainer: number;
cycleStandardHoursBulkDmp: number;
cycleStandardHoursBulkNagad: number;
cycleStandardHoursBulkBcc: number;
defaultLegStandardHours: number;
delayToleranceMinutes: number;
chargedTonsFull20ft: number;
chargedTonsFull40ft: number;
chargedTonsEmpty20ft: number;
chargedTonsEmpty40ft: number;
chargedTonsPerWagonGeneral: number;
chargedTonsPerWagonPerishable: number;
defaultFullTrainsetWagons: number;
updatedAt?: string;
}
export type OperationsStandardsPatch = Partial<
Omit<OperationsStandards, "id" | "updatedAt">
>;
export const operationsStandardsService = {
get: async (): Promise<OperationsStandards> => {
const response = await client.get<ApiResponse<OperationsStandards>>(BASE);
return unwrap(response.data);
},
update: async (
patch: OperationsStandardsPatch,
): Promise<OperationsStandards> => {
const response = await client.patch<ApiResponse<OperationsStandards>>(
BASE,
patch,
);
return unwrap(response.data);
},
};

View File

@@ -96,6 +96,7 @@ const RESOURCE_BASE: Record<RuleEngineResourceSlug, string> = {
"weight-limit-rules": URL_CONSTANTS.RULE_ENGINE.WEIGHT_LIMIT_RULES,
yards: URL_CONSTANTS.RULE_ENGINE.YARDS,
"yard-distances": URL_CONSTANTS.RULE_ENGINE.YARD_DISTANCES,
"operations-targets": URL_CONSTANTS.RULE_ENGINE.OPERATIONS_TARGETS,
"shipping-lines": URL_CONSTANTS.RULE_ENGINE.SHIPPING_LINES,
rates: URL_CONSTANTS.RULE_ENGINE.RATES,
"approval-rules": URL_CONSTANTS.RULE_ENGINE.APPROVAL_RULES,

View File

@@ -86,7 +86,7 @@ export const freightMantineTheme = createTheme({
black: "#10202F",
fontFamily:
'"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
'"Space Grotesk", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
defaultRadius: "md",
@@ -123,7 +123,7 @@ export const freightMantineTheme = createTheme({
},
headings: {
fontFamily: '"Inter", var(--mantine-font-family)',
fontFamily: '"Space Grotesk", var(--mantine-font-family)',
fontWeight: "700",
sizes: {
h1: { fontSize: "36px", lineHeight: "1.1", fontWeight: "800" },

View File

@@ -11,7 +11,8 @@ export type RuleEngineResourceSlug =
| "shipping-lines"
| "rates"
| "approval-rules"
| "transit-agents";
| "transit-agents"
| "operations-targets";
/**
* Mirrors the API's shared `PaginationMeta` (@edr/types). The `has*` flags are