mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(export-ui): mount the export button on the eight remaining list pages
customers, contracts, invoices, payments, train schedules, and locomotives / trains / wagons via the fleet page's config. FilterBar pages pass controls.params into the children slot. The four pages still on ad-hoc filtering pass their own hand-built filter object instead, which is why ExportButton takes plain params rather than a UseFilters — it would otherwise have been blocked behind migrating those pages. FleetResource serves seven slugs from config, so it gets an optional exportKey there and renders nothing for the four slugs with no dataset yet. Auditing each page's real filter keys against the dataset declarations turned up three gaps where an on-screen filter would have silently not applied to the export: invoices sends a singular "status" (the dataset only had the multiselect "statuses"), contracts sends paymentCurrency, serviceTypeId and route origin/destination, and train schedules sends freightType. Added all of them — contract routes filter through EXISTS on contract_routes since they are one-to-many, and train-schedule freightType through EXISTS on the bookings aboard, matching the list service. Verified in the browser: the button renders on each page, and the invoices dialog follows that page's own filter object — selecting Paid moves the count from 126 to 100, which matches the database. Filter pass-through checked against the database for invoices, payments, wagons, contracts and train schedules.
This commit is contained in:
@@ -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}%` });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
>
|
||||
<ExportButton datasetKey="contracts" params={controls.params} />
|
||||
</FilterBar>
|
||||
</Box>
|
||||
|
||||
{showEmpty ? (
|
||||
|
||||
@@ -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" },
|
||||
]}
|
||||
/>
|
||||
<ExportButton datasetKey="customers" params={controls.params} />
|
||||
</FilterBar>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -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 ? (
|
||||
<ExportButton datasetKey={config.exportKey} params={controls.params} />
|
||||
) : null}
|
||||
</FilterBar>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<ExportButton datasetKey="invoices" params={filter} size="sm" />
|
||||
<SegmentedControl
|
||||
size="sm"
|
||||
radius="md"
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { KpiStrip } from "@/components/page";
|
||||
import { ExportButton } from "@/components/export/ExportButton";
|
||||
import { formatDate, formatMoney } from "@/lib/format";
|
||||
import { api } from "@/services/api";
|
||||
import type { PaymentMethod, PaymentRow } from "@/services/payments.service";
|
||||
@@ -295,6 +296,7 @@ export default function PaymentsPanel() {
|
||||
}
|
||||
style={{ flex: 1, minWidth: "200px" }}
|
||||
/>
|
||||
<ExportButton datasetKey="payments" params={filter} size="sm" />
|
||||
<Select
|
||||
placeholder="All methods"
|
||||
clearable
|
||||
|
||||
@@ -55,6 +55,7 @@ import CreateScheduleWindowFields, {
|
||||
} from "@/components/trainScheduling/CreateScheduleWindowFields";
|
||||
import EditScheduleDateModal from "@/components/trainScheduling/EditScheduleDateModal";
|
||||
import { showScheduleWarnings } from "@/components/trainScheduling/locomotiveOptions";
|
||||
import { ExportButton } from "@/components/export/ExportButton";
|
||||
import {
|
||||
RouteCorridor,
|
||||
StatusPill,
|
||||
@@ -736,6 +737,7 @@ export default function TrainScheduleV2ListPage() {
|
||||
w={170}
|
||||
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
|
||||
/>
|
||||
<ExportButton datasetKey="train-schedules" params={filters} size="sm" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user