mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +00:00
feat: Stripe-style filter bar for freight backoffice (pilot: contracts)
Replace the ad-hoc filter controls with a URL-linkable pill filter bar: each filter is a pill that opens a type-aware popover (text/enum/date/ number/boolean, each with the right operator set), overflow filters live behind a searchable "More filters" menu, sorting is a separate control, and filter state round-trips through the URL query string (shareable, back/forward-safe, backward compatible with existing ?statuses=A,B links). Frontend (apps/edr-freight-web/backoffice/src/components/filters/): - FilterDef schema + a pure url.ts codec (parse/serialize/toApiParams), with a 24-case round-trip + malformed-input test suite - useFilters hook driving react-query params straight from useSearchParams, debounced search, saved views in localStorage (@mantine/hooks useLocalStorage), page-reset-on-filter-change baked into one setSearchParams call instead of a separate effect - FilterBar/FilterPill/OperatorSelect/MoreFiltersMenu/SortControl + per-type popover bodies (Mantine) - ContractRequestsPage migrated end to end as the pilot Backend (apps/edr-freight-api): - pagination.util: applySort() — whitelisted sortBy resolved against a per-module column map (never interpolated), with a mandatory `id ASC` tiebreaker so paginating a non-unique sort can't drop/duplicate rows - facets.util: computeFacets() — one GROUP BY per enum column, each omitting its own predicate, so picking a value doesn't hide its siblings - contracts/bookings: list-summary now returns real filter-scoped facet counts (contracts' getStatusCounts was unfiltered/global; superseded) - deleted drivers/vehicles findAllWithFilters — dead code that interpolated an unwhitelisted sortBy straight into orderBy() - migration: missing bookings(status)/wagons(status) indexes + (created_at DESC, id ASC) partials on the hot list tables UI polish pass: inactive pill uses the opaque "default" variant instead of a faint tinted outline, active pill uses "light" not "filled", larger X hit target, applied filters sort first, sort control separated behind a divider on the right and wraps independently from the filter row, popover option rows are fully clickable (count moved inside the native label) with bigger hit area and font, fixed a real date-filter bug where the calendar's own portal falsely registered as an "outside click" and closed the popover, and fixed a timezone bug where bare YYYY-MM-DD strings were parsed as UTC instead of local time (shifts a day for EAT). Not in this commit: rollout to the other ~59 list pages, the Ethiopian- calendar DateBody branch, and the Family-B (client-side) bridge mode — tracked in the filter-bar plan.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Supports the Stripe-style pill filter bar: every list it lands on filters
|
||||
* and sorts server-side now. `@Index` decorators alone do nothing —
|
||||
* `synchronize: false` means an index exists only if a migration created it
|
||||
* (see the `RepairSynchronizeDrift`-style gaps this closes).
|
||||
*
|
||||
* `idx_warehouse_inventory_status` already exists (FreightBaseline). Bookings
|
||||
* and wagons have no plain `status` index — `idx_bookings_route_day` and
|
||||
* `idx_wagons_readiness` only cover `status` as a trailing/partial column,
|
||||
* not a standalone `WHERE status = $1`, and `status` is the single
|
||||
* most-filtered column on both lists (bookings: 37 values).
|
||||
*
|
||||
* `(created_at DESC, id ASC)` partials match the default sort + id
|
||||
* tiebreaker `applySort` now appends everywhere, and none of these tables
|
||||
* had a created_at index at all.
|
||||
*/
|
||||
export class FilterableListIndexes3540000000000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_status
|
||||
ON freight.bookings USING btree (status)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_wagons_status
|
||||
ON freight.wagons USING btree (status)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_contracts_created_at_id
|
||||
ON freight.contracts (created_at DESC, id ASC) WHERE deleted_at IS NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_created_at_id
|
||||
ON freight.bookings (created_at DESC, id ASC) WHERE deleted_at IS NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_created_at_id
|
||||
ON freight.warehouse_inventory (created_at DESC, id ASC) WHERE deleted_at IS NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_wagons_created_at_id
|
||||
ON freight.wagons (created_at DESC, id ASC) WHERE deleted_at IS NULL
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_wagons_created_at_id`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_warehouse_inventory_created_at_id`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_created_at_id`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_contracts_created_at_id`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_wagons_status`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_status`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user