modify booking request page

This commit is contained in:
Marshal
2026-07-03 14:01:50 +00:00
parent 57d3752ea5
commit c977deb460
6 changed files with 156 additions and 128 deletions

View File

@@ -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<Booking> {
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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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: <Send />,
permission: FREIGHT_PERMS.contracts.createBooking,
},
// Hidden for now — Shipment Requests nav item disabled.
// {
// label: "Shipment Requests",
// href: "/dashboard/shipment-requests",
// icon: <Send />,
// permission: FREIGHT_PERMS.contracts.createBooking,
// },
{
label: "GL Djibouti Clearance",
href: "/dashboard/gl-djibouti/clearance",
@@ -672,6 +674,7 @@ const App = () => {
</RequirePermission>
}
/>
{/* Hidden for now — Shipment Requests pages disabled.
<Route
path="shipment-requests"
element={
@@ -692,6 +695,7 @@ const App = () => {
</RequirePermission>
}
/>
*/}
{/* GL (Path B) contract clearance review hub */}
<Route
path="contracts/clearance"

View File

@@ -4,6 +4,7 @@ import {
Button,
Card,
Group,
Select,
Stack,
Tabs,
Text,
@@ -30,17 +31,12 @@ import { BookingActionsMenu } from "@/components/bookings/BookingActionsMenu";
import { BookingApprovalProgressCell } from "@/components/bookings/BookingApprovalProgressCell";
import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge";
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
import {
BookingStatusTabs,
type BookingStatusTabKey,
} from "@/components/bookings/BookingStatusTabs";
// BookingStatusTabs / Operations* queues removed — replaced by booking-kind tabs.
import { BookingTableEmpty } from "@/components/bookings/BookingTableEmpty";
import { OperationsBookingQueue } from "@/components/bookings/OperationsBookingQueue";
import { OperationsScheduledBookings } from "@/components/bookings/OperationsScheduledBookings";
import { bookingTable } from "@/components/bookings/booking-ui.styles";
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import { AllocateBookingWizard } from "@/components/trainScheduling/AllocateBookingWizard";
import { BOOKING_LIST_TABS } from "@/features/bookings/booking-status.config";
import { BOOKING_STATUS_STYLES } from "@/features/bookings/booking-status.config";
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
import {
useBookingDetail,
@@ -57,11 +53,29 @@ import {
type ColumnDef,
} from "@edr/ui-common";
function getStatusesForTab(tab: BookingStatusTabKey): string | undefined {
const match = BOOKING_LIST_TABS.find((t) => 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<BookingStatusTabKey>("all");
const [operationsSubTab, setOperationsSubTab] = useState<OperationsSubTab>("ready");
// Booking-kind tabs (one-time vs general contract) replace the old status tabs.
const [kindTab, setKindTab] = useState<BookingKindTab>("ONE_TIME");
// Per-tab filter selects (each nullable = "all").
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [directionFilter, setDirectionFilter] = useState<string | null>(null);
const [freightTypeFilter, setFreightTypeFilter] = useState<string | null>(null);
const [allocateOpen, setAllocateOpen] = useState(false);
const [allocateIds, setAllocateIds] = useState<string[]>([]);
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.
<BookingStatusTabs
active={activeTab}
onChange={(tab) => {
@@ -364,73 +349,97 @@ export default function BookingRequestsPage() {
}}
counts={tabCounts}
/>
*/}
<Tabs
value={kindTab}
onChange={(value) => {
setKindTab((value as BookingKindTab) ?? "ONE_TIME");
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}}
>
<Tabs.List>
{BOOKING_KIND_TABS.map((t) => (
<Tabs.Tab key={t.value} value={t.value}>
{t.label}
</Tabs.Tab>
))}
</Tabs.List>
</Tabs>
<Card p={0}>
<Stack gap={0}>
<Box px="md" pt="md" pb="sm" w="100%">
<Group justify="space-between" gap="md" wrap="wrap">
<TextInput
placeholder="Search reference or customer…"
leftSection={<Search size={18} />}
value={query}
onChange={(e) => setQuery(e.target.value)}
rightSection={
query && (
<ActionIcon
size="sm"
color="gray"
radius="md"
variant="transparent"
onClick={() => setQuery("")}
>
<X size={16} />
</ActionIcon>
)
}
style={{ flex: 1, minWidth: "200px" }}
radius="lg"
/>
<Text size="sm" c="dimmed">
{total} record{total !== 1 ? "s" : ""}
</Text>
</Group>
<Stack gap="sm">
<Group justify="space-between" gap="md" wrap="wrap">
<TextInput
placeholder="Search reference or customer…"
leftSection={<Search size={18} />}
value={query}
onChange={(e) => setQuery(e.target.value)}
rightSection={
query && (
<ActionIcon
size="sm"
color="gray"
radius="md"
variant="transparent"
onClick={() => setQuery("")}
>
<X size={16} />
</ActionIcon>
)
}
style={{ flex: 1, minWidth: "200px" }}
radius="lg"
/>
<Text size="sm" c="dimmed">
{total} record{total !== 1 ? "s" : ""}
</Text>
</Group>
<Group gap="sm" wrap="wrap">
<Select
placeholder="All statuses"
data={STATUS_OPTIONS}
value={statusFilter}
onChange={(v) => {
setStatusFilter(v);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}}
clearable
searchable
radius="lg"
style={{ minWidth: 200 }}
/>
<Select
placeholder="All directions"
data={TRADE_DIRECTION_OPTIONS}
value={directionFilter}
onChange={(v) => {
setDirectionFilter(v);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}}
clearable
radius="lg"
style={{ minWidth: 170 }}
/>
<Select
placeholder="All freight types"
data={FREIGHT_TYPE_OPTIONS}
value={freightTypeFilter}
onChange={(v) => {
setFreightTypeFilter(v);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}}
clearable
radius="lg"
style={{ minWidth: 170 }}
/>
</Group>
</Stack>
</Box>
{isOperationsTab ? (
<Box px="md" pb="md">
<Stack gap="md">
<Tabs
value={operationsSubTab}
onChange={(value) =>
setOperationsSubTab((value as OperationsSubTab) ?? "ready")
}
>
<Tabs.List>
<Tabs.Tab value="ready">Ready to allocate</Tabs.Tab>
<Tabs.Tab value="scheduled">On train / scheduled</Tabs.Tab>
</Tabs.List>
</Tabs>
{isError ? (
<BookingTableEmpty
isError
hasSearch={false}
onRetry={handleRefresh}
/>
) : operationsSubTab === "ready" ? (
<OperationsBookingQueue
bookings={rows}
isLoading={isLoading}
onAllocate={handleAllocateFromQueue}
/>
) : (
<OperationsScheduledBookings
bookings={rows}
isLoading={isLoading}
/>
)}
</Stack>
</Box>
) : showEmpty ? (
{showEmpty ? (
<Box px="md" pb="md">
<BookingTableEmpty
isError={isError}

View File

@@ -17,6 +17,8 @@ export interface BookingListFilter {
// customerId?: string;
companyId?: string;
freightType?: string;
/** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */
bookingType?: string;
tradeDirection?: string;
paymentCurrency?: string;
page?: number;
@@ -125,6 +127,7 @@ export const bookingsService = {
if (filter.pageSize != null) params.pageSize = filter.pageSize;
if (filter.companyId) params.companyId = filter.companyId;
if (filter.freightType) params.freightType = filter.freightType;
if (filter.bookingType) params.bookingType = filter.bookingType;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency;
}
@@ -148,6 +151,7 @@ export const bookingsService = {
if (filter.assignedToSchedule) params.assignedToSchedule = filter.assignedToSchedule;
if (filter.companyId) params.companyId = filter.companyId;
if (filter.freightType) params.freightType = filter.freightType;
if (filter.bookingType) params.bookingType = filter.bookingType;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency;
}