diff --git a/apps/edr-freight-api/src/modules/exports/datasets/contracts.dataset.ts b/apps/edr-freight-api/src/modules/exports/datasets/contracts.dataset.ts
index 854604c45..51e99614a 100644
--- a/apps/edr-freight-api/src/modules/exports/datasets/contracts.dataset.ts
+++ b/apps/edr-freight-api/src/modules/exports/datasets/contracts.dataset.ts
@@ -109,6 +109,15 @@ export const contractsDataset: ExportDataset = {
{ key: 'contractKind', label: 'Kind', type: 'text' },
{ key: 'tradeDirection', label: 'Direction', type: 'select', options: ['IMPORT', 'EXPORT', 'DOMESTIC'].map((v) => ({ value: v, label: v })) },
{ key: 'freightType', label: 'Freight type', type: 'select', options: ['CONTAINER', 'BULK'].map((v) => ({ value: v, label: v })) },
+ { key: 'paymentCurrency', label: 'Currency', type: 'select', options: [
+ { value: 'ETB', label: 'ETB' },
+ { value: 'USD', label: 'USD' },
+ ] },
+ { key: 'serviceTypeId', label: 'Service type', type: 'text' },
+ // Routes are one-to-many on contract_routes, so these filter via EXISTS
+ // rather than a column comparison.
+ { key: 'originYardId', label: 'Origin', type: 'text' },
+ { key: 'destinationYardId', label: 'Destination', type: 'text' },
{ key: 'companyId', label: 'Customer', type: 'text' },
{ key: 'search', label: 'Search reference or customer', type: 'text' },
],
@@ -125,6 +134,24 @@ export const contractsDataset: ExportDataset = {
if (params.contractKind) qb.andWhere('ct.contract_kind = :contractKind', { contractKind: params.contractKind });
if (params.tradeDirection) qb.andWhere('ct.trade_direction = :tradeDirection', { tradeDirection: params.tradeDirection });
if (params.freightType) qb.andWhere('ct.freight_type = :freightType', { freightType: params.freightType });
+ if (params.paymentCurrency) qb.andWhere('ct.payment_currency = :paymentCurrency', { paymentCurrency: params.paymentCurrency });
+ if (params.serviceTypeId) qb.andWhere('ct.service_type_id = :serviceTypeId', { serviceTypeId: params.serviceTypeId });
+ if (params.originYardId) {
+ qb.andWhere(
+ `EXISTS (SELECT 1 FROM freight.contract_routes cr
+ WHERE cr.contract_id = ct.id AND cr.deleted_at IS NULL
+ AND cr.origin_yard_id = :originYardId)`,
+ { originYardId: params.originYardId },
+ );
+ }
+ if (params.destinationYardId) {
+ qb.andWhere(
+ `EXISTS (SELECT 1 FROM freight.contract_routes cr2
+ WHERE cr2.contract_id = ct.id AND cr2.deleted_at IS NULL
+ AND cr2.destination_yard_id = :destinationYardId)`,
+ { destinationYardId: params.destinationYardId },
+ );
+ }
if (params.companyId) qb.andWhere('ct.company_id = :companyId', { companyId: params.companyId });
if (params.search) {
qb.andWhere('(ct.reference ILIKE :search OR c.name ILIKE :search)', { search: `%${params.search as string}%` });
diff --git a/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts b/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts
index c98ca684f..5eb0986b5 100644
--- a/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts
+++ b/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts
@@ -98,6 +98,9 @@ export const invoicesDataset: ExportDataset = {
filters: [
{ key: 'issued', label: 'Issued', type: 'daterange' },
{ key: 'statuses', label: 'Status', type: 'multiselect' },
+ // The invoices list page sends a single `status`; accept both so its
+ // on-screen filter actually carries into the export.
+ { key: 'status', label: 'Status (single)', type: 'text' },
{ key: 'currency', label: 'Currency', type: 'select', options: [
{ value: 'ETB', label: 'ETB' },
{ value: 'USD', label: 'USD' },
@@ -115,6 +118,7 @@ export const invoicesDataset: ExportDataset = {
if (params.issuedTo) qb.andWhere('i.issued_at < :issuedTo', { issuedTo: params.issuedTo });
const statuses = params.statuses as string[] | null;
if (statuses?.length) qb.andWhere('i.status IN (:...statuses)', { statuses });
+ if (params.status) qb.andWhere('i.status = :status', { status: params.status });
if (params.currency) qb.andWhere('i.currency = :currency', { currency: params.currency });
if (params.companyId) qb.andWhere('i.company_id = :companyId', { companyId: params.companyId });
if (params.search) {
diff --git a/apps/edr-freight-api/src/modules/exports/datasets/train-schedules.dataset.ts b/apps/edr-freight-api/src/modules/exports/datasets/train-schedules.dataset.ts
index 5402e7917..bb31591da 100644
--- a/apps/edr-freight-api/src/modules/exports/datasets/train-schedules.dataset.ts
+++ b/apps/edr-freight-api/src/modules/exports/datasets/train-schedules.dataset.ts
@@ -101,6 +101,9 @@ export const trainSchedulesDataset: ExportDataset = {
{ key: 'departure', label: 'Departure', type: 'daterange' },
{ key: 'status', label: 'Status', type: 'text' },
{ key: 'direction', label: 'Direction', type: 'select', options: ['IMPORT', 'EXPORT', 'DOMESTIC'].map((v) => ({ value: v, label: v })) },
+ // freightType is derived from the bookings aboard, so it filters via
+ // EXISTS — the same shape the list service's scheduleFreightTypeFilter uses.
+ { key: 'freightType', label: 'Freight type', type: 'select', options: ['CONTAINER', 'BULK'].map((v) => ({ value: v, label: v })) },
{ key: 'originStationId', label: 'Origin', type: 'text' },
{ key: 'destinationStationId', label: 'Destination', type: 'text' },
{ key: 'search', label: 'Search reference or train number', type: 'text' },
@@ -115,6 +118,14 @@ export const trainSchedulesDataset: ExportDataset = {
if (params.departureTo) qb.andWhere('sch.scheduled_departure_date < :departureTo', { departureTo: params.departureTo });
if (params.status) qb.andWhere('sch.status = :status', { status: params.status });
if (params.direction) qb.andWhere('sch.direction = :direction', { direction: params.direction });
+ if (params.freightType) {
+ qb.andWhere(
+ `EXISTS (SELECT 1 FROM freight.bookings fb
+ WHERE fb.train_schedule_id = sch.id AND fb.deleted_at IS NULL
+ AND fb.freight_type = :freightType)`,
+ { freightType: params.freightType },
+ );
+ }
if (params.originStationId) qb.andWhere('sch.origin_station_id = :originStationId', { originStationId: params.originStationId });
if (params.destinationStationId) qb.andWhere('sch.destination_station_id = :destinationStationId', { destinationStationId: params.destinationStationId });
if (params.search) {
diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx
index aa8b5ed8f..4f761c6f0 100644
--- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx
@@ -53,6 +53,7 @@ import {
type ColumnDef,
} from "@edr/ui-common";
import { FilterBar, dateRangeParams, useFilters, type FilterDef } from "@/components/filters";
+import { ExportButton } from "@/components/export/ExportButton";
/** Every filterable status — the pill tabs are gone, so the select carries them all. */
const STATUS_OPTIONS = CONTRACT_LIST_TABS.flatMap((t) => t.statuses ?? []).map(
@@ -468,7 +469,9 @@ export default function ContractRequestsPage() {
searchPlaceholder="Search reference or customer…"
sortOptions={SORT_OPTIONS}
viewId="contract-requests"
- />
+ >
+
+
{showEmpty ? (
diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx
index 91bb72fdc..552d4c61b 100644
--- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx
@@ -37,6 +37,7 @@ import type { Company, CompanyStatus } from "@/types/customer";
import { isOnboardingDraft } from "@/types/customer";
import { DataTable, DataTableFooter, type ColumnDef } from "@edr/ui-common";
import { FilterBar, useFilters, type FilterDef } from "@/components/filters";
+import { ExportButton } from "@/components/export/ExportButton";
/**
* The list's segmented views. "Pending approval" means submitted-and-awaiting-
@@ -313,6 +314,7 @@ export default function CustomersPage() {
{ label: "Active", value: "active" },
]}
/>
+
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
index 23dc6b76b..50f3fe26c 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
@@ -42,6 +42,7 @@ import {
} from "@/services/fleet/fleet.service";
import { DataTable, DataTableFooter } from "@edr/ui-common";
import { dateRangeParams, FilterBar, useFilters, type FilterDef, type FilterOption } from "@/components/filters";
+import { ExportButton } from "@/components/export/ExportButton";
const DEFAULT_SLUG: FleetResourceSlug = "locomotives";
@@ -621,6 +622,9 @@ const FleetResourcePage = () => {
]}
styles={{ root: { background: "var(--mantine-color-gray-1)" } }}
/>
+ {config.exportKey ? (
+
+ ) : null}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
index 526181d44..e73ebe8f1 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
@@ -119,6 +119,11 @@ export interface FleetResourceConfig {
supportsSearch: boolean;
/** Server-side list filters (e.g. wagon status / readiness). */
listFilters?: FleetListFilterDef[];
+ /**
+ * Export dataset key for this resource. Omitted where no dataset exists yet,
+ * in which case the page renders no export button.
+ */
+ exportKey?: string;
columns: FleetResourceColumn[];
formFields: FleetFormFieldDef[];
emptyValues: Record;
@@ -182,6 +187,7 @@ const WAGON_EDITABLE_STATUS_OPTIONS = WAGON_STATUS_OPTIONS.filter(
export const FLEET_RESOURCES: FleetResourceConfig[] = [
{
slug: "locomotives",
+ exportKey: "locomotives",
label: "Locomotives",
subtitle: "Manage locomotive master data used by train scheduling and fleet operations",
basePath: "/dashboard/locomotives",
@@ -249,6 +255,7 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
},
{
slug: "trains",
+ exportKey: "trains",
label: "Trains",
subtitle: "Manage train master data independently from train scheduling",
basePath: "/dashboard/trains",
@@ -292,6 +299,7 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
},
{
slug: "wagons",
+ exportKey: "wagons",
label: "Wagons",
subtitle: "Manage wagon master data. Operational scheduling uses train schedules separately",
basePath: "/dashboard/wagons",
diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx
index 159157e2f..a8211f73f 100644
--- a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx
@@ -30,6 +30,7 @@ import {
} from "@/components/customers";
import { KpiStrip } from "@/components/page";
import CreditInvoiceActions from "@/components/shipping-lines/CreditInvoiceActions";
+import { ExportButton } from "@/components/export/ExportButton";
import { useExchangeSettingsQuery } from "@/hooks/useExchangeSettings";
import { api } from "@/services/api";
import type { Invoice } from "@/types/invoice";
@@ -247,6 +248,7 @@ export default function InvoicesPanel() {
style={{ flex: 1, minWidth: "240px" }}
radius="lg"
/>
+
+
+
>
}
/>