diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx index da19b7599..111df497b 100644 --- a/apps/edr-freight-web/portal/src/App.tsx +++ b/apps/edr-freight-web/portal/src/App.tsx @@ -22,6 +22,7 @@ import { } from "lucide-react"; import BookingsPage from "./pages/bookings/BookingsPage"; +import MyBookings from "./pages/bookings/MyBookings"; import BookingDetailPage from "./pages/bookings/BookingDetailPage"; import NewBookingPage from "./pages/bookings/NewBookingPage"; import ConsignmentsPage from "./pages/consignments/ConsignmentsPage"; @@ -49,7 +50,7 @@ const sidebarItems: SidebarItem[] = [ { label: "My Portal", href: "/portal", icon: }, { label: "Dashboard", href: "/", icon: }, { label: "Customers", href: "/customers", icon: }, - { label: "Bookings", href: "/bookings", icon: }, + { label: "My Bookings", href: "/bookings", icon: }, { label: "Consignments", href: "/consignments", icon: }, { label: "Tracking", href: "/tracking", icon: }, { label: "Stations", href: "/stations", icon: }, @@ -115,7 +116,8 @@ const App = () => { } /> } /> - } /> + } /> + } /> } /> } /> } /> @@ -128,10 +130,7 @@ const App = () => { } /> } /> } /> - } - /> + } /> } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx index 7eb038196..1ce8f76d5 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx @@ -15,7 +15,7 @@ import { import Breadcrumbs from "@/components/Breadcrumbs"; import DeleteBookingDialog from "./DeleteBookingDialog"; -import { getBookingById, type BookingStatus } from "./bookings.mock"; +import { getBookingById, deleteBooking, type BookingStatus } from "./bookings.mock"; import { Button, Card } from "@edr/ui-common"; export default function BookingDetailPage() { @@ -88,7 +88,10 @@ export default function BookingDetailPage() { navigate("/bookings")} + onConfirm={() => { + deleteBooking(booking.id); + navigate("/bookings"); + }} > + + + navigate(`/bookings/${booking.id}`)} + > + + View + + + handleDeleteConfirm(booking.id)} + > + e.preventDefault()} + variant="destructive" + > + + Delete + + + + + + ); + }, + }, + ]; + + return ( +
+
+ + + {/* Header Section Card */} + +
+

+ My Bookings +

+

+ View and manage your freight booking requests. +

+
+ +
+
+ + setSearchTerm(e.target.value)} + className="pl-8!" + /> +
+ + + + +
+
+ + {/* Stat Cards */} +
+ + +
+

Total Bookings

+

+ {myBookings.length} +

+
+
+ +
+
+
+ + + +
+

Active Bookings

+

+ {activeCount} +

+
+
+ +
+
+
+ + + +
+

Pending Approval

+

+ {pendingCount} +

+
+
+ +
+
+
+
+ + {/* Data Table */} + + +
+ Recent Requests + + A list of your recent freight bookings and their statuses. + +
+ + +
+ + + {total === 0 ? ( +
+ +

No bookings found

+

+ {searchTerm ? "No bookings match your current search filter." : "You haven't requested any bookings yet."} +

+
+ ) : ( + navigate(`/bookings/${(row as Booking).id}`)} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount: pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-b shadow-none" + footer={DataTableFooter} + /> + )} +
+
+
+
+ ); +} + +function StatusBadge({ status }: { status: BookingStatus }) { + const styles: Record = { + Pending: "bg-amber-100 text-amber-700", + Confirmed: "bg-sky-100 text-sky-700", + "In Transit": "bg-indigo-100 text-indigo-700", + Delivered: "bg-emerald-100 text-emerald-700", + Cancelled: "bg-red-100 text-red-700", + }; + + return ( + + {status} + + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx index e23b44cb2..49d233167 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -5,6 +5,10 @@ import { useNavigate } from "react-router-dom"; import { CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react"; import { Button } from "@edr/ui-common"; import Breadcrumbs from "@/components/Breadcrumbs"; +import { addBooking } from "./bookings.mock"; +import { getCurrentCustomer } from "@/lib/currentCustomer"; +import { api } from "@/services/api"; +import type { CreateBookingPayload } from "@/services/bookings.service"; import { MOCK_VALID_CONTRACTS, STEPS, @@ -35,8 +39,8 @@ export default function NewBookingPage() { const [submitted, setSubmitted] = useState(false); const form = useForm({ - resolver: zodResolver(bookingFormSchema), defaultValues: initialBookingFormValues, + resolver: zodResolver(bookingFormSchema), mode: "onChange", }); @@ -119,6 +123,123 @@ export default function NewBookingPage() { return; } + const me = getCurrentCustomer(); + const reference = + data.draftContractId || + data.previousContractRef || + `EDR-DRAFT-${Date.now()}`; + + const qtyCount = + data.cargoType === "container" + ? data.containers.reduce((acc, c) => acc + Number(c.qty || 0), 0) + : 1; + + const totalWeight = + data.cargoType === "container" + ? data.containers.reduce( + (acc, c) => acc + Number(c.vgm || 0) * Number(c.qty || 0), + 0, + ) + : Number(data.cargoWeight || 0); + + const description = + data.cargoType === "container" + ? data.containers.map((c) => `${c.qty} × ${c.type}`).join(", ") + : data.freightType === "bulk" + ? `Bulk - ${data.bulkCommodity === "Others" ? data.bulkCommodityOther : data.bulkCommodity}` + : `Break-Bulk - ${data.breakBulkType === "Others" ? data.breakBulkTypeOther : data.breakBulkType}`; + + const newBooking = { + id: Date.now(), + reference, + customerId: me.id, + customer: me.company, + cargoType: (data.cargoType === "container" + ? "Containerized" + : "Bulk") as any, + originStation: data.originYard, + destinationStation: data.destinationYard, + transportMode: (data.serviceType === "rail" + ? "Rail" + : "Multimodal") as any, + containerType: (data.cargoType === "container" && + data.containers[0]?.type === "40ft" + ? "40FT" + : "20FT") as any, + containerCount: qtyCount, + weightTons: totalWeight, + requestedDate: new Date().toISOString().slice(0, 10), + priority: (data.isHazardous ? "High" : "Normal") as any, + cargoDescription: description, + specialInstructions: data.notes || "Standard handling required", + status: "Pending" as any, + }; + + addBooking(newBooking); + + // Call API using api.bookings.create.call + const apiPayload = { + reference, + customerId: String(me.id), + scheduledDate: new Date().toISOString().slice(0, 10), + totalAmount: 0, + contractType: data.contractType.toUpperCase(), + previousContractId: data.previousContractRef || undefined, + serviceType: + data.serviceType === "rail" ? "RAIL_ONLY" : "RAIL_AND_FORWARDING", + firstMileEnabled: data.firstMileEnabled, + firstMilePickupAddress: data.firstMileEnabled + ? data.pickUpAddress + : undefined, + lastMileEnabled: data.lastMileEnabled, + lastMileDeliveryAddress: data.lastMileEnabled + ? data.deliveryAddress + : undefined, + equipmentReturn: + data.equipmentReturn === "with_return" + ? "WITH_RETURN" + : "WITHOUT_RETURN", + originStation: data.originYard, + destinationStation: data.destinationYard, + cargoTotalWeightVgm: totalWeight, + freightType: data.cargoType === "container" ? "BREAK_BULK" : "BULK", + freightSubtype: + data.cargoType === "container" + ? undefined + : data.freightType === "bulk" + ? data.bulkCommodity + : data.breakBulkType, + isHazardous: data.isHazardous, + isRefrigerated: data.isRefrigerated, + tradeDirection: + getRouteDirection(data.originYard, data.destinationYard) === "export" + ? "EXPORT" + : "IMPORT", + paymentCurrency: "USD", + allowConsolidation: data.consolidationEnabled, + ...(data.cargoType === "container" && data.containers.length > 0 + ? { + containers: data.containers.map((c) => ({ + type: c.type === "40ft" ? "40FT" as const : "20FT" as const, + qty: Number(c.qty || 1), + vgm: Number(c.vgm || 0), + })), + } + : {}), + }; + + api.bookings.create + .call(apiPayload as CreateBookingPayload) + .then((created) => { + console.log("Successfully created booking via API:", created); + }) + .catch((err) => { + console.warn( + "API call failed (expected if API server is offline), falling back to mock storage:", + err, + ); + }); + setSubmitted(true); setTimeout(() => navigate("/bookings"), 2500); } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/bookings.mock.ts b/apps/edr-freight-web/portal/src/pages/bookings/bookings.mock.ts index 7740f7364..11bb4fc1c 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/bookings.mock.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/bookings.mock.ts @@ -83,7 +83,7 @@ function pickStation(i: number, offset: number) { return stations[(i + offset) % stations.length] as string; } -export const bookings: Booking[] = Array.from({ length: 22 }, (_, i) => { +const INITIAL_BOOKINGS: Booking[] = Array.from({ length: 22 }, (_, i) => { const customer = customers[i % customers.length] as (typeof customers)[number]; const id = i + 1; const requested = new Date(2026, 4, 1 + (i % 28)); @@ -125,6 +125,43 @@ export const bookings: Booking[] = Array.from({ length: 22 }, (_, i) => { }; }); +const getStoredBookings = (): Booking[] => { + if (typeof window === "undefined" || !window.localStorage) { + return INITIAL_BOOKINGS; + } + const data = localStorage.getItem("edr_bookings"); + if (!data) { + localStorage.setItem("edr_bookings", JSON.stringify(INITIAL_BOOKINGS)); + return INITIAL_BOOKINGS; + } + try { + return JSON.parse(data); + } catch (e) { + return INITIAL_BOOKINGS; + } +}; + +export const bookings: Booking[] = getStoredBookings(); + +export function saveBookingsToStorage() { + if (typeof window !== "undefined" && window.localStorage) { + localStorage.setItem("edr_bookings", JSON.stringify(bookings)); + } +} + +export function addBooking(booking: Booking) { + bookings.unshift(booking); + saveBookingsToStorage(); +} + +export function deleteBooking(id: number) { + const index = bookings.findIndex((b) => b.id === id); + if (index !== -1) { + bookings.splice(index, 1); + saveBookingsToStorage(); + } +} + export function getBookingById(id: number | string): Booking | undefined { const numericId = typeof id === "string" ? Number(id) : id; return bookings.find((b) => b.id === numericId); diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts index 5d094f059..1549606ed 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts @@ -46,7 +46,7 @@ export const REQUIRED_DOC_KEYS = [ "tin_certificate", "business_license", "business_registration", - "national_id", + // "national_id", ] as const; export const STEPS = [ @@ -162,7 +162,7 @@ export const bookingFormSchema = z destinationYard: z.string(), cargoType: z.enum(["container", "bulk"], "Select a cargo type."), cargoWeight: z.string(), - freightType: z.enum(["bulk", "break_bulk"]), + freightType: z.enum(["bulk", "break_bulk", ""]).default(""), bulkCommodity: z.string(), bulkCommodityOther: z.string(), breakBulkType: z.string(), @@ -480,4 +480,4 @@ export function calcWagons(containers: ContainerConfig[]): WagonCalcResult { ft20Wagons: Ft20Wagons, wagonLayout, }; -} \ No newline at end of file +} diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts index e368ed675..ba2b227c6 100644 --- a/apps/edr-freight-web/portal/src/services/api.ts +++ b/apps/edr-freight-web/portal/src/services/api.ts @@ -8,7 +8,7 @@ import type { UpdateFileUploadFieldDto, UpdateFileUploadSettingDto, } from "@/types/fileUploadSettings"; -import { bookingsService } from "./bookings.service"; +import { bookingsService, CreateBookingPayload } from "./bookings.service"; import { consignmentsService } from "./consignments.service"; import { trackingService } from "./tracking.service"; import { fileUploadSettingsService } from "./fileUploadSettings.service"; @@ -40,16 +40,11 @@ export const api = { ({ id }) => bookingsService.get(id), ), - create: endpoint< - { - reference: string; - customerId: string; - scheduledDate: string; - totalAmount: number; - trainId?: string; - }, - Freight.IBooking - >("bookings", "create", (input) => bookingsService.create(input)), + create: endpoint( + "bookings", + "create", + bookingsService.create, + ), remove: endpoint<{ id: string }, void>("bookings", "remove", ({ id }) => bookingsService.remove(id), diff --git a/apps/edr-freight-web/portal/src/services/bookings.service.ts b/apps/edr-freight-web/portal/src/services/bookings.service.ts index 17ace13e8..45070a6f9 100644 --- a/apps/edr-freight-web/portal/src/services/bookings.service.ts +++ b/apps/edr-freight-web/portal/src/services/bookings.service.ts @@ -14,7 +14,16 @@ export const bookingsService = { return data.data; }, create: async (payload: CreateBookingPayload): Promise => { - const { data } = await api.post("/bookings", payload); + const fd = new FormData(); + for (const [key, value] of Object.entries(payload)) { + if (value === undefined || value === null) continue; + if (Array.isArray(value) || typeof value === "object") { + fd.append(key, JSON.stringify(value)); + } else { + fd.append(key, String(value)); + } + } + const { data } = await api.post("/api/bookings", fd); return data.data; }, remove: async (id: string): Promise => { diff --git a/apps/edr-freight-web/portal/src/services/tracking.service.ts b/apps/edr-freight-web/portal/src/services/tracking.service.ts index 07026cf0a..af3f5f4b7 100644 --- a/apps/edr-freight-web/portal/src/services/tracking.service.ts +++ b/apps/edr-freight-web/portal/src/services/tracking.service.ts @@ -1,12 +1,12 @@ import type { Freight } from "@edr/types"; -import { api } from "../utils/api"; +import { client } from "../utils/api"; export const trackingService = { forConsignment: async ( consignmentId: string, ): Promise => { - const { data } = await api.get(`/tracking/${consignmentId}`); + const { data } = await client.get(`/tracking/${consignmentId}`); return data.data; }, }; diff --git a/packages/types/src/freight/index.ts b/packages/types/src/freight/index.ts index a9f768b1c..b6d1e6dfa 100644 --- a/packages/types/src/freight/index.ts +++ b/packages/types/src/freight/index.ts @@ -155,4 +155,14 @@ export interface CreateBookingDto { tradeDirection: "IMPORT" | "EXPORT"; paymentCurrency: string; allowConsolidation?: boolean; + + startDate?: string; + endDate?: string; + financialTerms?: string; + + containers?: Array<{ + type: "20FT" | "40FT"; + qty: number; + vgm: number; + }>; }