diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts index 77303d08d..90e1585cf 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts @@ -132,7 +132,22 @@ export class BookingsController { const companyId = await this.bookingsService.resolveCustomerCompanyId(userId); // No linked company yet → no bookings to show (avoids leaking all bookings). - if (!companyId) return { items: [], total: 0 }; + if (!companyId) { + const page = filter.page ?? 1; + const pageSize = filter.pageSize ?? 20; + return { + items: [], + total: 0, + meta: { + page, + pageSize, + total: 0, + totalPages: 0, + hasNextPage: false, + hasPreviousPage: false, + }, + }; + } // Scope to the active operational profile (importer/exporter) when one // resolves; otherwise fall back to company-level scoping. const companyProfileId = 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 6c45b1b48..55fb4d9ae 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,8 @@ export interface BookingListFilterOptions { paymentCurrency?: string; paymentStatus?: string; excludePaymentStatus?: string; + createdFrom?: string; + createdTo?: string; allowConsolidation?: boolean; consolidationPaired?: string; } @@ -436,7 +438,18 @@ export class BookingsRepository extends BaseRepository { pageSize: number; sortBy?: string; sortOrder?: 'ASC' | 'DESC'; - }): Promise<{ items: Booking[]; total: number }> { + }): Promise<{ + items: Booking[]; + total: number; + meta: { + page: number; + pageSize: number; + total: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; + }> { const page = options.page; const pageSize = options.pageSize; @@ -483,7 +496,22 @@ export class BookingsRepository extends BaseRepository { } } - return { items, total }; + const totalPages = pageSize > 0 ? Math.ceil(total / pageSize) : 0; + // Return both the flat `total` (consumed by the backoffice list) and a + // `meta` block (consumed by the portal, matching PaginationMeta) so neither + // app needs to change its read shape. + return { + items, + total, + meta: { + page, + pageSize, + total, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + }, + }; } async getStatusCounts(): Promise> { @@ -591,6 +619,17 @@ export class BookingsRepository extends BaseRepository { bookingType: options.bookingType, }); } + if (options.createdFrom) { + qb.andWhere('booking.created_at >= :createdFrom', { + createdFrom: options.createdFrom, + }); + } + if (options.createdTo) { + // Inclusive end-of-day: callers pass a date; include the whole day. + qb.andWhere('booking.created_at <= :createdTo', { + createdTo: options.createdTo, + }); + } if (options.tradeDirection) { qb.andWhere('booking.trade_direction = :tradeDirection', { tradeDirection: options.tradeDirection, 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 90258e47b..d33e1c169 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -42,6 +42,20 @@ import { import { Booking } from './entities/booking.entity'; import { FileRecord } from '../files/entities/file.entity'; +/** Paginated booking list: flat `total` (backoffice) + `meta` block (portal). */ +export interface PaginatedBookings { + items: Booking[]; + total: number; + meta: { + page: number; + pageSize: number; + total: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; +} + const URGENT_PRIORITY_THRESHOLD = 1000; const NEEDS_ACTION_STATUSES = [ 'SUBMITTED', @@ -628,7 +642,7 @@ export class BookingsService { filter: FilterBookingDto, forceCompanyId?: string, forceCompanyProfileId?: string, - ): Promise<{ items: Booking[]; total: number }> { + ): Promise { const page = filter.page ?? 1; const pageSize = filter.pageSize ?? 20; const statusFilter = this.parseStatusFilter(filter); @@ -654,6 +668,8 @@ export class BookingsService { tradeDirection: filter.tradeDirection, paymentCurrency: filter.paymentCurrency, paymentStatus: filter.paymentStatus, + createdFrom: filter.createdFrom, + createdTo: filter.createdTo, allowConsolidation: filter.allowConsolidation, consolidationPaired: filter.consolidationPaired, sortBy: filter.sortBy, @@ -676,7 +692,7 @@ export class BookingsService { async findMyPayable( userId: string, filter: FilterBookingDto, - ): Promise<{ items: Booking[]; total: number }> { + ): Promise { const { company } = await this.companiesService.getCompanyInfoByUserId(userId); // Scope to the active operational profile when one resolves; fall back to // company-level so not-yet-onboarded customers still see their payables. @@ -823,6 +839,8 @@ export class BookingsService { tradeDirection: filter.tradeDirection, paymentCurrency: filter.paymentCurrency, paymentStatus: filter.paymentStatus, + createdFrom: filter.createdFrom, + createdTo: filter.createdTo, allowConsolidation: filter.allowConsolidation, consolidationPaired: filter.consolidationPaired, }; diff --git a/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts b/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts index deee17cf0..d52473813 100644 --- a/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts +++ b/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts @@ -1,6 +1,6 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsIn, IsOptional, IsUUID } from 'class-validator'; +import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator'; import { BOOKING_STATUSES, BOOKING_TYPES, @@ -62,6 +62,16 @@ export class FilterBookingDto { @IsIn([...BOOKING_TYPES]) bookingType?: string; + @ApiPropertyOptional({ description: 'Filter bookings created on/after this date (ISO)' }) + @IsOptional() + @IsDateString() + createdFrom?: string; + + @ApiPropertyOptional({ description: 'Filter bookings created on/before this date (ISO)' }) + @IsOptional() + @IsDateString() + createdTo?: string; + @ApiPropertyOptional({ enum: TRADE_DIRECTIONS }) @IsOptional() @IsIn([...TRADE_DIRECTIONS]) diff --git a/apps/edr-freight-web/portal/src/components/AppLayout.tsx b/apps/edr-freight-web/portal/src/components/AppLayout.tsx index 5df4ca842..7720f6117 100644 --- a/apps/edr-freight-web/portal/src/components/AppLayout.tsx +++ b/apps/edr-freight-web/portal/src/components/AppLayout.tsx @@ -41,6 +41,7 @@ import { type ReactNode, useState, } from "react"; +import { PROFILE_TYPE_LABELS } from "@/constants/profileMode"; export interface SidebarItem { label: string; @@ -79,14 +80,6 @@ type SwitchResult = | { success: true; data?: unknown } | { success: false; error?: { message?: string } }; -const PROFILE_TYPE_LABELS: Record = { - importer: "Importer", - exporter: "Exporter", - freight_forwarder: "Freight Forwarder", - dj_freight_forwarder: "DJ Freight Forwarder", - transporter: "Transporter", -}; - function getInitials(name: string): string { return name .split(" ") @@ -813,8 +806,7 @@ export function AppLayout({ You don't have an {modeLabel(targetMode).toLowerCase()} profile yet. Add your business license to create one and switch to{" "} - {modeLabel(targetMode).toLowerCase()} mode. A new reference will be - generated automatically. + {modeLabel(targetMode).toLowerCase()} mode. + + ) : ( + + ) + } + styles={{ + root: { textTransform: "none", letterSpacing: 0, fontWeight: 600 }, + }} + > + Viewing: {label} + + + ); +} + +export default ModeIndicator; diff --git a/apps/edr-freight-web/portal/src/constants/profileMode.ts b/apps/edr-freight-web/portal/src/constants/profileMode.ts new file mode 100644 index 000000000..a73901ccb --- /dev/null +++ b/apps/edr-freight-web/portal/src/constants/profileMode.ts @@ -0,0 +1,30 @@ +/** + * Operational-mode (importer/exporter/…) labels and helpers, shared by the app + * header and the per-page mode indicator so there is a single source of truth. + */ + +export const PROFILE_TYPE_LABELS: Record = { + importer: "Importer", + exporter: "Exporter", + freight_forwarder: "Freight Forwarder", + dj_freight_forwarder: "DJ Freight Forwarder", + transporter: "Transporter", +}; + +/** The data-scope label shown to the user (importer ⇒ "Import", exporter ⇒ "Export"). */ +export function modeDataLabel( + activeProfileType?: string | null, +): string | null { + if (activeProfileType === "importer") return "Import"; + if (activeProfileType === "exporter") return "Export"; + return null; +} + +/** Short helper sentence describing what the active mode scopes. */ +export function modeDataDescription( + activeProfileType?: string | null, +): string { + const label = modeDataLabel(activeProfileType); + if (!label) return ""; + return `Showing your ${label.toLowerCase()} data — switch in the header.`; +} diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/HelloSection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/HelloSection.tsx index 9f1d726b0..3d7ca6fa5 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/HelloSection.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/HelloSection.tsx @@ -2,6 +2,7 @@ import { Box, Group, Text } from "@mantine/core"; import { ArrowRight, Truck } from "lucide-react"; import { memo } from "react"; import { Link } from "react-router-dom"; +import { ModeIndicator } from "@/components/ModeIndicator"; import { cv } from "../constants"; interface HelloSectionProps { @@ -19,9 +20,12 @@ export const HelloSection = memo(function HelloSection({ {greeting} - - {companyName} 👋 - + + + {companyName} 👋 + + + diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx index 6fb25516a..4120dec7f 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx @@ -9,8 +9,10 @@ import { paymentsService, type PaymentMethod } from "@/services/payments.service import type { Freight } from "@edr/types"; import { ActivityCard } from "./components/ActivityCard"; +import { ContainersCard } from "./components/ContainersCard"; import { ContractCard } from "./components/ContractCard"; import { DocRow, IconSquare } from "./components/Documents"; +import { KeyFactsStrip } from "./components/KeyFactsStrip"; import { BodyGrid, CardTitle, PageShell, SectionCard } from "./components/layout"; import { CancelledBanner, @@ -113,6 +115,8 @@ export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {showPairedNotice && } + + + + {booking.files && booking.files.length > 0 && ( diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ContainersCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ContainersCard.tsx new file mode 100644 index 000000000..5ec72f48f --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ContainersCard.tsx @@ -0,0 +1,90 @@ +import { Box, Group, Table, Text } from "@mantine/core"; +import { Boxes } from "lucide-react"; + +import type { Freight } from "@edr/types"; + +import { CardTitle, SectionCard } from "./layout"; + +/** + * Per-container-type breakdown for container bookings (count, type, VGM). + * Renders nothing for bulk bookings, which have no container lines. + */ +export function ContainersCard({ booking }: { booking: Freight.IBooking }) { + const containers = booking.containers ?? []; + if (booking.freightType === "BULK" || containers.length === 0) return null; + + const totalUnits = containers.reduce((sum, c) => sum + Number(c.qty || 0), 0); + const totalVgm = containers.reduce( + (sum, c) => sum + Number(c.vgm || 0) * Number(c.qty || 0), + 0, + ); + + return ( + + + + + Containers + + + {totalUnits} unit{totalUnits !== 1 ? "s" : ""} + + + + + + + Type + Qty + VGM / unit + + Total VGM + + + + + {containers.map((c, i) => { + const lineVgm = Number(c.vgm || 0) * Number(c.qty || 0); + return ( + + + + {c.type} + + + + + {c.qty} + + + + + {c.vgm ? `${c.vgm} t` : "—"} + + + + + {lineVgm ? `${lineVgm.toLocaleString()} t` : "—"} + + + + ); + })} + +
+ + + + Total weight (VGM) + + + {totalVgm.toLocaleString()} t + + +
+ ); +} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/KeyFactsStrip.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/KeyFactsStrip.tsx new file mode 100644 index 000000000..5e75d4323 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/KeyFactsStrip.tsx @@ -0,0 +1,107 @@ +import { Box, Group, SimpleGrid, Text } from "@mantine/core"; +import { + CalendarClock, + CreditCard, + MapPin, + Package, + Tag, + Train, +} from "lucide-react"; +import type { ReactNode } from "react"; + +import type { Freight } from "@edr/types"; + +import { fmtDate, yardLabel } from "../utils"; +import { SectionCard } from "./layout"; + +type BookingLike = Freight.IBooking & { + bookingType?: string; + paymentStatus?: string; + trainScheduleId?: string | null; +}; + +function Fact({ + icon, + label, + value, +}: { + icon: ReactNode; + label: string; + value: ReactNode; +}) { + return ( + + + {icon} + + + + {label} + + + {value} + + + + ); +} + +/** + * Compact at-a-glance facts strip at the top of the booking detail page — gives + * a fast scan of the key attributes before the deeper cards below. + */ +export function KeyFactsStrip({ booking }: { booking: BookingLike }) { + const isContract = booking.bookingType === "GENERAL_CONTRACT"; + const freight = booking.freightType === "BULK" ? "Bulk" : "Container"; + const payment = booking.paymentStatus + ? booking.paymentStatus + .replace(/_/g, " ") + .toLowerCase() + .replace(/^\w/, (c) => c.toUpperCase()) + : "—"; + + return ( + + + } + label="Type" + value={isContract ? "General Contract" : "One-Time"} + /> + } label="Cargo" value={freight} /> + } + label="Route" + value={`${yardLabel(booking.originYard)} → ${yardLabel(booking.destinationYard)}`} + /> + } label="Payment" value={payment} /> + } + label="Train" + value={booking.trainScheduleId ? "Assigned" : "Not assigned"} + /> + } + label={isContract ? "Ordering until" : "Scheduled"} + value={ + isContract + ? fmtDate(booking.expiresAt ?? null) + : fmtDate(booking.scheduledDate) + } + /> + + + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx b/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx index 78730084b..d4d8a75ec 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx @@ -34,6 +34,13 @@ import { import { ShipmentTrackingModal } from "./tracking/ShipmentTrackingModal"; import { PayNowButton } from "./payments/PayNowButton"; +import { ModeIndicator } from "@/components/ModeIndicator"; +import { + BookingTypeBadge, + CargoModeCell, + PaymentBadge, + SchedulingCell, +} from "./booking-display"; // Bookings that have left (or are leaving) the yard can be tracked live. const TRACKABLE_STATUSES = new Set([ @@ -320,24 +327,54 @@ export default function MyBookings() { const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [statusFilter, setStatusFilter] = useState("all"); const [query, setQuery] = useState(""); + const [typeFilter, setTypeFilter] = useState(null); + const [freightFilter, setFreightFilter] = useState(null); + const [createdFrom, setCreatedFrom] = useState(""); + const [createdTo, setCreatedTo] = useState(""); const [trackingBooking, setTrackingBooking] = useState( null, ); const statuses = STATUS_FILTERS.find((t) => t.key === statusFilter)?.statuses; + const resetPage = () => + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + const selectFilter = (key: StatusFilterKey) => { setStatusFilter(key); - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + resetPage(); + }; + + const hasExtraFilters = + !!typeFilter || !!freightFilter || !!createdFrom || !!createdTo; + const clearExtraFilters = () => { + setTypeFilter(null); + setFreightFilter(null); + setCreatedFrom(""); + setCreatedTo(""); + resetPage(); }; const filter: BookingListFilter = useMemo( () => ({ statuses, + bookingType: typeFilter ?? undefined, + freightType: freightFilter ?? undefined, + createdFrom: createdFrom || undefined, + // include the whole selected end day + createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined, page: pagination.pageIndex + 1, pageSize: pagination.pageSize, }), - [statuses, pagination.pageIndex, pagination.pageSize], + [ + statuses, + typeFilter, + freightFilter, + createdFrom, + createdTo, + pagination.pageIndex, + pagination.pageSize, + ], ); const { data, isLoading, isError } = useQuery( @@ -358,14 +395,20 @@ export default function MyBookings() { const doneCount = useStatusCount( STATUS_FILTERS.find((f) => f.key === "done")!.statuses, ); + const transitCount = useStatusCount( + STATUS_FILTERS.find((f) => f.key === "transit")!.statuses, + ); + const closedCount = useStatusCount( + STATUS_FILTERS.find((f) => f.key === "closed")!.statuses, + ); const cardCounts: Record = { all: allCount, active: activeCount, payment: paymentCount, draft: draftCount, done: doneCount, - transit: undefined, - closed: undefined, + transit: transitCount, + closed: closedCount, }; const allItems = data?.items ?? []; @@ -424,6 +467,20 @@ export default function MyBookings() { ); }, }, + { + id: "type", + size: 150, + meta: hMeta, + header: () => , + cell: ({ row }) => , + }, + { + id: "cargo", + size: 168, + meta: hMeta, + header: () => , + cell: ({ row }) => , + }, { id: "route", size: 196, @@ -448,6 +505,20 @@ export default function MyBookings() { ); }, }, + { + id: "payment", + size: 130, + meta: hMeta, + header: () => , + cell: ({ row }) => , + }, + { + id: "scheduling", + size: 140, + meta: hMeta, + header: () => , + cell: ({ row }) => , + }, { id: "status", size: 190, @@ -536,9 +607,12 @@ export default function MyBookings() { {/* ── Page header ─────────────────────────────────────────────── */} - - Bookings - + + + Bookings + + + Track every cargo booking — from draft to delivery. @@ -606,9 +680,79 @@ export default function MyBookings() { radius="md" checkIconPosition="right" comboboxProps={{ withinPortal: true }} - style={{ width: 200 }} + style={{ width: 190 }} aria-label="Filter by status" /> + { + setFreightFilter(v); + resetPage(); + }} + clearable + radius="md" + comboboxProps={{ withinPortal: true }} + style={{ width: 150 }} + aria-label="Filter by cargo type" + /> + { + setCreatedFrom(e.currentTarget.value); + resetPage(); + }} + radius="md" + style={{ width: 150 }} + aria-label="Created from" + placeholder="From" + /> + { + setCreatedTo(e.currentTarget.value); + resetPage(); + }} + radius="md" + style={{ width: 150 }} + aria-label="Created to" + placeholder="To" + /> + {hasExtraFilters && ( + + )} {total} booking{total !== 1 ? "s" : ""} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/booking-display.tsx b/apps/edr-freight-web/portal/src/pages/bookings/booking-display.tsx new file mode 100644 index 000000000..02f3c48d6 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/bookings/booking-display.tsx @@ -0,0 +1,109 @@ +import { Badge, Group, Text } from "@mantine/core"; +import type { Freight } from "@edr/types"; + +/** + * Shared presentation helpers for booking-like rows (one-time bookings AND + * general contracts). Kept in one place so the bookings list, contracts list, + * and detail page render type/freight/mode/payment consistently. + */ + +type BookingLike = Freight.IBooking & { + bookingType?: string; + freightType?: string; + tradeDirection?: string; + paymentStatus?: string; +}; + +/** One-Time vs General Contract. */ +export function BookingTypeBadge({ booking }: { booking: BookingLike }) { + const isContract = booking.bookingType === "GENERAL_CONTRACT"; + return ( + + {isContract ? "General Contract" : "One-Time"} + + ); +} + +/** Containerised vs Bulk, plus the trade direction (Import/Export/Domestic). */ +export function CargoModeCell({ booking }: { booking: BookingLike }) { + const freight = + booking.freightType === "BULK" ? "Bulk" : "Container"; + const dir = booking.tradeDirection + ? booking.tradeDirection.charAt(0) + booking.tradeDirection.slice(1).toLowerCase() + : null; + return ( + + + {freight} + + {dir && ( + + {dir} + + )} + + ); +} + +const PAYMENT_COLORS: Record = { + PAID: "green", + PENDING: "gray", + PNR_GENERATED: "blue", + VERIFICATION_IN_PROGRESS: "yellow", + FAILED: "red", +}; + +const PAYMENT_LABELS: Record = { + PAID: "Paid", + PENDING: "Pending", + PNR_GENERATED: "PNR generated", + VERIFICATION_IN_PROGRESS: "Verifying", + FAILED: "Failed", +}; + +/** Payment status pill. */ +export function PaymentBadge({ status }: { status?: string | null }) { + if (!status) return ; + return ( + + {PAYMENT_LABELS[status] ?? status.replace(/_/g, " ")} + + ); +} + +/** Whether a booking is assigned to a train yet (scheduling progress). */ +export function SchedulingCell({ booking }: { booking: BookingLike & { trainScheduleId?: string | null; schedulingStatus?: string } }) { + const assigned = !!booking.trainScheduleId; + const label = assigned + ? "Assigned" + : booking.schedulingStatus === "HOLDING" + ? "Holding" + : booking.schedulingStatus === "ELIGIBLE" + ? "Eligible" + : "Not scheduled"; + return ( + + {label} + + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx index ba6f7ade8..2c766b27d 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx @@ -25,6 +25,7 @@ import { } from "lucide-react"; import { api } from "@/services/api"; +import { ModeIndicator } from "@/components/ModeIndicator"; import { PayNowButton } from "../bookings/payments/PayNowButton"; import { BORDER, @@ -120,6 +121,7 @@ export default function ContractDetailPage() { {contract.reference} +
General contract · {isContainer ? "Containerised" : "Bulk"} diff --git a/apps/edr-freight-web/portal/src/pages/contracts/ContractsList.tsx b/apps/edr-freight-web/portal/src/pages/contracts/ContractsList.tsx index 0940798ec..40ebd4f45 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/ContractsList.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/ContractsList.tsx @@ -7,13 +7,14 @@ import { Card, Group, Paper, + Select, Stack, Text, TextInput, ThemeIcon, Title, } from "@mantine/core"; -import { Layers, Plus, Search } from "lucide-react"; +import { Layers, Plus, Search, X } from "lucide-react"; import { api } from "@/services/api"; import type { BookingListFilter } from "@/services/bookings.service"; @@ -24,22 +25,46 @@ import { type ColumnDef, usePagination, } from "@edr/ui-common"; +import { ModeIndicator } from "@/components/ModeIndicator"; +import { CargoModeCell, PaymentBadge } from "../bookings/booking-display"; import { ContractStatusBadge, GREEN, INK, MUTED } from "./contract-ui"; export default function ContractsList() { const navigate = useNavigate(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); + const [freightFilter, setFreightFilter] = useState(null); + const [createdFrom, setCreatedFrom] = useState(""); + const [createdTo, setCreatedTo] = useState(""); + + const resetPage = () => + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + const hasExtraFilters = !!freightFilter || !!createdFrom || !!createdTo; + const clearExtraFilters = () => { + setFreightFilter(null); + setCreatedFrom(""); + setCreatedTo(""); + resetPage(); + }; const filter: BookingListFilter = useMemo( () => ({ bookingType: "GENERAL_CONTRACT", + freightType: freightFilter ?? undefined, + createdFrom: createdFrom || undefined, + createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined, page: pagination.pageIndex + 1, pageSize: pagination.pageSize, sortBy: "createdAt", sortOrder: "DESC", }), - [pagination.pageIndex, pagination.pageSize], + [ + freightFilter, + createdFrom, + createdTo, + pagination.pageIndex, + pagination.pageSize, + ], ); const { data, isLoading, isError } = useQuery( @@ -93,6 +118,11 @@ export default function ContractsList() { ); }, }, + { + id: "cargo", + header: () => , + cell: ({ row }) => , + }, { id: "route", header: () => , @@ -109,6 +139,11 @@ export default function ContractsList() { ); }, }, + { + id: "payment", + header: () => , + cell: ({ row }) => , + }, { id: "expires", header: () => , @@ -138,9 +173,12 @@ export default function ContractsList() { {/* Header */} - - General Contracts - + + + General Contracts + + + Reserve a quantity once, then place orders against it until the contract runs out or its window closes. @@ -163,16 +201,71 @@ export default function ContractsList() { hint="accepting orders" /> - {/* Search */} - } - value={query} - onChange={(e) => setQuery(e.currentTarget.value)} - radius="md" - styles={{ input: { height: 44 } }} - maw={420} - /> + {/* Search + filters */} + + } + value={query} + onChange={(e) => setQuery(e.currentTarget.value)} + radius="md" + styles={{ input: { height: 44 } }} + style={{ flex: 1, minWidth: 220, maxWidth: 360 }} + /> +