diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts index 3f696bc6b..846fd2c9a 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts @@ -35,6 +35,7 @@ export interface BookingListFilterOptions { serviceTypeId?: string; cargoTypeId?: string; freightType?: string; + bookingType?: string; tradeDirection?: string; paymentCurrency?: string; paymentStatus?: string; @@ -737,6 +738,11 @@ export class BookingsRepository extends BaseRepository { freightType: options.freightType, }); } + if (options.bookingType) { + qb.andWhere('booking.bookingType = :bookingType', { + bookingType: options.bookingType, + }); + } if (options.createdFrom) { qb.andWhere('booking.created_at >= :createdFrom', { createdFrom: options.createdFrom, diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index ac639a4bd..e3d882baa 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -1001,6 +1001,7 @@ export class BookingsService { serviceTypeId: filter.serviceTypeId, cargoTypeId: filter.cargoTypeId, freightType: filter.freightType, + bookingType: filter.bookingType, tradeDirection: filter.tradeDirection, paymentCurrency: filter.paymentCurrency, paymentStatus: filter.paymentStatus, diff --git a/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts b/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts index 4b033bab5..cf0ca76f6 100644 --- a/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts +++ b/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts @@ -157,6 +157,10 @@ export class Booking extends BaseEntity { @Column({ name: 'contract_route_id', type: 'uuid', nullable: true }) contractRouteId?: string | null; + /** Booking origin: ONE_TIME (single-shipment) or GENERAL_CONTRACT (drawdown). */ + @Column({ name: 'booking_type', type: 'varchar', length: 20, default: 'ONE_TIME' }) + bookingType!: string; + /** Denormalized contract kind (ONE_TIME | GENERAL) for the single-active-booking index. */ @Column({ name: 'contract_kind', type: 'varchar', length: 20, nullable: true }) contractKind?: string | null; diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index e7e567c7a..1e7d1e280 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -52,8 +52,9 @@ import ContractClearanceListPage from "./pages/contracts/ContractClearanceListPa import ContractClearanceDetailPage from "./pages/contracts/ContractClearanceDetailPage"; import GlDjiboutiClearanceListPage from "./pages/contracts/GlDjiboutiClearanceListPage"; import GlClearanceDetailPage from "./pages/contracts/GlClearanceDetailPage"; -import ShipmentRequestsPage from "./pages/contracts/ShipmentRequestsPage"; -import ShipmentRequestDetailPage from "./pages/contracts/ShipmentRequestDetailPage"; +// Hidden for now — Shipment Requests pages disabled (imports kept commented). +// import ShipmentRequestsPage from "./pages/contracts/ShipmentRequestsPage"; +// import ShipmentRequestDetailPage from "./pages/contracts/ShipmentRequestDetailPage"; import GlCreateBookingForm from "./components/contracts/GlCreateBookingForm"; import DocumentClearanceDetailPage from "./pages/bookings/DocumentClearanceDetailPage"; import CustomerDetailPage from "./pages/customers/CustomerDetailPage"; @@ -178,12 +179,13 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ FREIGHT_PERMS.contracts.clearanceEtActions, ], }, - { - label: "Shipment Requests", - href: "/dashboard/shipment-requests", - icon: , - permission: FREIGHT_PERMS.contracts.createBooking, - }, + // Hidden for now — Shipment Requests nav item disabled. + // { + // label: "Shipment Requests", + // href: "/dashboard/shipment-requests", + // icon: , + // permission: FREIGHT_PERMS.contracts.createBooking, + // }, { label: "GL Djibouti Clearance", href: "/dashboard/gl-djibouti/clearance", @@ -672,6 +674,7 @@ const App = () => { } /> + {/* Hidden for now — Shipment Requests pages disabled. { } /> + */} {/* GL (Path B) contract clearance review hub */} t.key === tab); - if (!match?.statuses?.length) return undefined; - return match.statuses.join(","); -} +/** The two booking-kind tabs: one-time vs general-contract bookings. */ +type BookingKindTab = "ONE_TIME" | "GENERAL_CONTRACT"; + +const BOOKING_KIND_TABS: { value: BookingKindTab; label: string }[] = [ + { value: "ONE_TIME", label: "One-time booking" }, + { value: "GENERAL_CONTRACT", label: "General booking" }, +]; + +/** Status options for the filter select — built from the shared status styles. */ +const STATUS_OPTIONS = Object.entries(BOOKING_STATUS_STYLES).map( + ([value, { label }]) => ({ value, label }), +); + +const TRADE_DIRECTION_OPTIONS = [ + { value: "IMPORT", label: "Import" }, + { value: "EXPORT", label: "Export" }, + { value: "DOMESTIC", label: "Domestic" }, +]; + +const FREIGHT_TYPE_OPTIONS = [ + { value: "CONTAINER", label: "Container" }, + { value: "BULK", label: "Bulk" }, +]; function formatDate(value: string | null | undefined): string { if (!value) return "—"; @@ -75,14 +89,16 @@ function formatDate(value: string | null | undefined): string { }); } -type OperationsSubTab = "ready" | "scheduled"; - export default function BookingRequestsPage() { const navigate = useNavigate(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); - const [activeTab, setActiveTab] = useState("all"); - const [operationsSubTab, setOperationsSubTab] = useState("ready"); + // Booking-kind tabs (one-time vs general contract) replace the old status tabs. + const [kindTab, setKindTab] = useState("ONE_TIME"); + // Per-tab filter selects (each nullable = "all"). + const [statusFilter, setStatusFilter] = useState(null); + const [directionFilter, setDirectionFilter] = useState(null); + const [freightTypeFilter, setFreightTypeFilter] = useState(null); const [allocateOpen, setAllocateOpen] = useState(false); const [allocateIds, setAllocateIds] = useState([]); const suppressRowClickRef = useRef(false); @@ -93,47 +109,26 @@ export default function BookingRequestsPage() { }, 400); }, []); - const tabStatuses = getStatusesForTab(activeTab); - const isOperationsTab = activeTab === "operations"; - const filter: BookingListFilter = useMemo(() => { - if (isOperationsTab) { - if (operationsSubTab === "ready") { - return { - page: 1, - pageSize: 100, - statuses: "PAID", - assignedToSchedule: "false", - sortBy: "createdAt", - sortOrder: "DESC", - tab: activeTab, - }; - } - return { - page: 1, - pageSize: 100, - statuses: "PAID", - schedulingStatuses: "SCHEDULED,DISPATCHED", - sortBy: "scheduledDate", - sortOrder: "ASC", - tab: activeTab, - }; - } return { page: pagination.pageIndex + 1, pageSize: pagination.pageSize, sortBy: "createdAt", sortOrder: "DESC", - tab: activeTab, - ...(tabStatuses ? { statuses: tabStatuses } : {}), + // React Query cache key per kind tab. + tab: kindTab, + bookingType: kindTab, + ...(statusFilter ? { statuses: statusFilter } : {}), + ...(directionFilter ? { tradeDirection: directionFilter } : {}), + ...(freightTypeFilter ? { freightType: freightTypeFilter } : {}), }; }, [ - isOperationsTab, - operationsSubTab, pagination.pageIndex, pagination.pageSize, - activeTab, - tabStatuses, + kindTab, + statusFilter, + directionFilter, + freightTypeFilter, ]); const { data, isLoading, isError, refetch, isFetching } = useBookingList(filter); @@ -171,18 +166,6 @@ export default function BookingRequestsPage() { void refetchSummary(); }, [refetch, refetchSummary]); - const handleAllocateFromQueue = useCallback( - (ids: string[]) => { - const selected = rows.filter((b) => ids.includes(b.id)); - const sorted = [...selected].sort( - (a, b) => (b.priorityScore ?? 0) - (a.priorityScore ?? 0), - ); - setAllocateIds(sorted.map((b) => b.id)); - setAllocateOpen(true); - }, - [rows], - ); - const handleRowClick = useCallback( (row: BookingListRow) => { if (suppressRowClickRef.current) return; @@ -356,6 +339,8 @@ export default function BookingRequestsPage() { ]} /> + {/* Status tabs replaced by booking-kind tabs (one-time / general). The + old BookingStatusTabs is commented out — status is now a filter select. { @@ -364,73 +349,97 @@ export default function BookingRequestsPage() { }} counts={tabCounts} /> + */} + + { + setKindTab((value as BookingKindTab) ?? "ONE_TIME"); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + }} + > + + {BOOKING_KIND_TABS.map((t) => ( + + {t.label} + + ))} + + - - } - value={query} - onChange={(e) => setQuery(e.target.value)} - rightSection={ - query && ( - setQuery("")} - > - - - ) - } - style={{ flex: 1, minWidth: "200px" }} - radius="lg" - /> - - {total} record{total !== 1 ? "s" : ""} - - + + + } + value={query} + onChange={(e) => setQuery(e.target.value)} + rightSection={ + query && ( + setQuery("")} + > + + + ) + } + style={{ flex: 1, minWidth: "200px" }} + radius="lg" + /> + + {total} record{total !== 1 ? "s" : ""} + + + + { + setDirectionFilter(v); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + }} + clearable + radius="lg" + style={{ minWidth: 170 }} + /> +