From c3c3d08a41293be0bb1db057b61630dd48ad30ad Mon Sep 17 00:00:00 2001 From: Marshal Date: Mon, 17 Aug 2026 07:55:52 +0000 Subject: [PATCH] fix issue --- .../booking-clearance.service.spec.ts | 1 + .../contracts/booking-clearance.service.ts | 39 +++++++- .../contracts/ContractClearanceListPage.tsx | 58 ++++++++++-- .../contracts/GlDjiboutiClearanceListPage.tsx | 32 ++++--- .../contracts/contract-clearance-table.css | 94 +++++++++++++++++++ .../src/pages/invoices/InvoicesPage.tsx | 70 +++++++------- .../components/WagonCancellationCard.tsx | 4 +- 7 files changed, 238 insertions(+), 60 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/pages/contracts/contract-clearance-table.css diff --git a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts index 8c127e840..b29a4cfa5 100644 --- a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts +++ b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts @@ -110,6 +110,7 @@ function makeService(overrides?: { .fn() .mockResolvedValue({ id: 'ta-1', name: 'Ahmed Bourhan' }), } as never, // transit agents + { findAll: jest.fn().mockResolvedValue([]) } as never, // contracts repository ); return { diff --git a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts index e0d30a2be..43176da75 100644 --- a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts @@ -1,4 +1,5 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; import { ContractDocPhase, isDeliveryOrderFileCode, @@ -29,6 +30,7 @@ import { ClearanceMilestoneService } from './clearance-milestone.service'; import { GlOperationsService } from './gl-operations.service'; import { GlExchangeService } from './gl-exchange.service'; import { TransitAgentsService } from '../transit-agents/transit-agents.service'; +import { ContractsRepository } from './contracts.repository'; import { AdviseContractDutyDto } from './dto/phased-clearance.dto'; import { buildWorkflowFiles, belongsOnDjClearanceQueue, belongsOnEtClearanceQueue, DJ_BOOKING_QUEUE_STATUSES, persistDeclarationUploads, persistDeliveryOrderUploads, persistDraftDeclarationUploads, persistReleaseOrderUploads, persistTransitPermitUploads, PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES } from './phased-clearance.util'; @@ -155,6 +157,7 @@ export class BookingClearanceService { private readonly notifier: BookingLifecycleNotifierService, private readonly glExchangeService: GlExchangeService, private readonly transitAgentsService: TransitAgentsService, + private readonly contractsRepository: ContractsRepository, ) {} private async assertPhasedCustoms(booking: Booking): Promise { @@ -988,7 +991,39 @@ export class BookingClearanceService { const milestones = await this.workflowService.listMilestonesForBooking(b.id); if (belongsOnEtClearanceQueue(milestones)) filtered.push(b); } - return filtered; + return this.attachContractSummary(filtered); + } + + /** + * Queue rows show the parent contract's reference and lane. Booking has no + * contract relation, and a bare initiated instance may not carry yards yet — + * so batch-load the contracts (with routes) and fill in what's missing: + * `contractReference` always, origin/destination yards only when the booking + * lacks them (its own route wins). + */ + private async attachContractSummary(bookings: Booking[]): Promise { + const ids = [...new Set(bookings.map((b) => b.contractId).filter(Boolean))] as string[]; + if (!ids.length) return bookings; + const contracts = await this.contractsRepository.findAll({ + where: { id: In(ids) }, + relations: { routes: { originYard: true, destinationYard: true } }, + }); + const byId = new Map(contracts.map((c) => [c.id, c])); + for (const b of bookings) { + const contract = b.contractId ? byId.get(b.contractId) : undefined; + if (!contract) continue; + const row = b as Booking & { contractReference?: string | null }; + row.contractReference = contract.reference ?? null; + if (b.originYard && b.destinationYard) continue; + const routes = contract.routes ?? []; + const route = + routes.find((r) => r.id === b.contractRouteId) ?? + (routes.length === 1 ? routes[0] : undefined); + if (!route) continue; + b.originYard = b.originYard ?? route.originYard; + b.destinationYard = b.destinationYard ?? route.destinationYard; + } + return bookings; } async djQueue(): Promise { @@ -1008,6 +1043,6 @@ export class BookingClearanceService { filtered.push(b); } } - return filtered; + return this.attachContractSummary(filtered); } } diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx index 8719f644a..503e6dc60 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx @@ -52,6 +52,47 @@ import { summarizeRequestedCargo, } from "@/features/clearance/requestedCargo"; import { contractsService } from "@/services/contracts.service"; +import "./contract-clearance-table.css"; + +/** Yards carry `label` (API) — older shapes used `name`/`code`. */ +function yardLabel( + yard?: { label?: string; code?: string; name?: string } | null, +): string { + if (!yard) return "—"; + return yard.label ?? yard.name ?? yard.code ?? "—"; +} + +/** + * "Origin → Destination", wrapping past 120px as "Addis Ababa" / + * "→ Djibouti": the arrow is glued to the destination with an nbsp, and + * text wraps normally (the table's cells are otherwise nowrap) so a long + * lane never spills into the next column. + */ +function RouteLabel({ + origin, + destination, +}: { + origin: string; + destination: string; +}) { + return ( + + {origin}{" "} + + {"\u00A0"} + {destination} + + ); +} function CustomsBadge({ customs }: { customs: boolean }) { return customs ? ( @@ -118,8 +159,8 @@ export default function ContractClearanceListPage() { id: b.id, reference: b.reference, customerLabel: b.company?.name ?? b.governmentInstitution ?? "—", - originLabel: b.originYard?.name ?? "—", - destinationLabel: b.destinationYard?.name ?? "—", + originLabel: yardLabel(b.originYard), + destinationLabel: yardLabel(b.destinationYard), tradeDirection: b.tradeDirection ?? "—", freightType: b.freightType ?? "—", status: b.status, @@ -430,11 +471,10 @@ function ShipmentBookingsTable({ id: "route", header: () => Route, cell: ({ row }) => ( - - {row.original.originLabel} - - {row.original.destinationLabel} - + ), }, { @@ -600,13 +640,13 @@ function ShipmentBookingsTable({ } return ( - + columns={columns} data={rows} status={loading ? "loading" : error ? "error" : "success"} onRowClick={(row) => onOpen(row.id)} - containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words" + containerClassName="edr-clearance-table border-0 shadow-none rounded-none bg-transparent" /> ); diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx index 76f2bc5b0..6e34da29a 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx @@ -45,6 +45,7 @@ import { KpiStrip } from "@/components/page/KpiStrip"; import { bookingTable } from "@/components/bookings/booking-ui.styles"; import { useBookingDjClearanceQueue } from "@/hooks/bookings/useBookings"; import type { BookingDetail } from "@/types/booking"; +import "./contract-clearance-table.css"; const prettyStatus = (s?: string | null) => (s ?? "") @@ -214,15 +215,24 @@ function RouteCell({ }) { return ( - - - {origin} - - - - {destination} - - + {/* Wraps past 120px as "Addis Ababa" / "→ Djibouti"; text wraps + normally (cells are otherwise nowrap) so it never spills over. */} + + {origin}{" "} + + {"\u00A0"} + {destination} + @@ -676,7 +686,7 @@ export default function GlDjiboutiClearanceListPage() { ) : null} ) : ( - + columns={shipmentColumns} data={pagedShipmentRows} @@ -694,7 +704,7 @@ export default function GlDjiboutiClearanceListPage() { manualPagination: true, pageCount, }} - containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words" + containerClassName="edr-clearance-table border-0 shadow-none rounded-none bg-transparent" footer={DataTableFooter} /> diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/contract-clearance-table.css b/apps/edr-freight-web/backoffice/src/pages/contracts/contract-clearance-table.css new file mode 100644 index 000000000..bdc615fb8 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/contract-clearance-table.css @@ -0,0 +1,94 @@ +/* + * Scoped to .edr-clearance-table — the DataTable container div on the + * Document Clearance hubs (GL Ethiopia + GL Djibouti). Mirrors the portal's /bookings table + * (bookings-table.css): content-sized columns with a 100px floor, no + * truncation, horizontal scroll when the table outgrows the card, sticky + * header row and a sticky shadowed action column. + */ +.edr-clearance-table { + overflow-x: auto; + max-width: 100%; + min-width: 0; +} + +/* + * width: max-content — the table is exactly as wide as its columns' content + * needs, never squeezed to fit the viewport; the container scrolls instead. + * min-width: 100% keeps it filling the card when content is narrow. + */ +.edr-clearance-table table { + table-layout: auto; + width: max-content; + min-width: 100%; +} + +/* 100px floor, no ceiling: cells grow to fit their text, nothing is clipped. */ +.edr-clearance-table th, +.edr-clearance-table td:not([colspan]) { + min-width: 100px; + max-width: none; + overflow: visible; + text-overflow: clip; + white-space: nowrap; +} + +/* + * Mantine Badge caps itself at max-width: 100%; inside an auto-layout table + * cell that resolves against min-content and clips the label. Let badges size + * to their text so the column grows to fit them. + */ +.edr-clearance-table .mantine-Badge-root { + max-width: none; +} + +/* + * Mantine Group's preventGrowOverflow caps every child at 100%/N of the cell. + * In an auto-width table cell that resolves against min-content and collapses + * the badges/text in the Type, Route and Status columns to nothing. Let group + * children size to their content; the column grows and the container scrolls. + */ +.edr-clearance-table .mantine-Group-root > * { + max-width: none; + flex-shrink: 0; +} + +/* Sticky header row. */ +.edr-clearance-table thead th { + position: sticky; + top: 0; + z-index: 1; +} + +/* + * Sticky action column, shrunk to its content. The width overrides the inline + * width DataTable stamps from tanstack's column size — hence !important. + * `:not([colspan])` keeps the full-width error/empty rows out. + */ +.edr-clearance-table th:last-child, +.edr-clearance-table td:last-child:not([colspan]) { + width: 1% !important; + min-width: 0; + position: sticky; + right: 0; + box-shadow: -12px 0 16px -6px rgba(16, 32, 47, 0.3); +} + +/* + * Sticky cells sit above the scrolling ones, so they need their own opaque + * background or the columns underneath show through. + */ +.edr-clearance-table td:last-child:not([colspan]) { + background: #f5f8fb; + z-index: 2; +} + +/* Row hover uses the tailwind `hover:bg-accent` class on the . */ +.edr-clearance-table tbody tr:hover td:last-child:not([colspan]) { + background: var(--accent, #f4fbf8); +} + +/* Header cell is sticky on both axes — it must outrank the body's sticky column. */ +.edr-clearance-table th:last-child { + background: #f4f7fa; + z-index: 3; +} 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 99f3c5d23..159157e2f 100644 --- a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx @@ -5,14 +5,20 @@ import { Card, Group, SegmentedControl, - SimpleGrid, Stack, Text, TextInput, } from "@mantine/core"; import { useDebouncedValue } from "@mantine/hooks"; import { useQuery } from "@tanstack/react-query"; -import { RefreshCw, Search, X } from "lucide-react"; +import { + Banknote, + CircleDollarSign, + Landmark, + RefreshCw, + Search, + X, +} from "lucide-react"; import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -22,6 +28,7 @@ import { formatMoney, humanize, } from "@/components/customers"; +import { KpiStrip } from "@/components/page"; import CreditInvoiceActions from "@/components/shipping-lines/CreditInvoiceActions"; import { useExchangeSettingsQuery } from "@/hooks/useExchangeSettings"; import { api } from "@/services/api"; @@ -83,7 +90,7 @@ export default function InvoicesPanel() { // Summary card: total collected (paidAmount) across every invoice matching // the current search/status filters, not just the visible page. - const { data: summary } = useQuery( + const { data: summary, isLoading: summaryLoading } = useQuery( api.invoices.collectedSummary.queryOptions({ input: { filter: { search: debouncedQuery, status: statusFilter || undefined }, @@ -190,39 +197,30 @@ export default function InvoicesPanel() { return ( - - - - Total collected - - - {etbFromUsd !== null - ? formatMoney(etbCollected + etbFromUsd, "ETB") - : formatMoney(etbCollected, "ETB")} - - - {etbFromUsd !== null - ? `Includes ${formatMoney(usdCollected, "USD")} converted @ ${rate} ETB/USD` - : "USD rate unavailable — ETB collected only"} - - - - - Collected — ETB only - - - {formatMoney(etbCollected, "ETB")} - - - - - Collected — USD only - - - {formatMoney(usdCollected, "USD")} - - - + diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/WagonCancellationCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/WagonCancellationCard.tsx index f75972f28..29ec6705d 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/WagonCancellationCard.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/WagonCancellationCard.tsx @@ -235,7 +235,7 @@ export function WagonCancellationCard({ Wagon Cancellation - {canRequest && !openRow && !creditRow && ( + {/* {canRequest && !openRow && !creditRow && ( - )} + )} */} {openRow ? (