From 059e555dc9cf59d5d44af072a1e34fb7d725191f Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Wed, 20 May 2026 12:04:07 +0300 Subject: [PATCH] style: update the pages to use global theme --- .../src/pages/admin/DropdownSettingsPage.tsx | 460 ++++++----- .../portal/src/pages/billing/BillingPage.tsx | 512 +++++------- .../src/pages/bookings/BookingDetailPage.tsx | 107 ++- .../src/pages/bookings/BookingsPage.tsx | 503 +++++------- .../consignments/ConsignmentDetailPage.tsx | 92 +-- .../pages/consignments/ConsignmentsPage.tsx | 584 ++++++------- .../src/pages/customers/CustomersPage.tsx | 20 +- .../pages/customers/DeleteCustomerDialog.tsx | 2 +- .../src/pages/customers/NewCustomerPage.tsx | 13 +- .../src/pages/dashboard/DashboardPage.tsx | 183 ++--- .../src/pages/documents/DocumentsPage.tsx | 775 ++++++++---------- .../src/pages/tracking/TrackingPage.tsx | 586 +++++++------ .../portal/src/pages/trains/TrainsPage.tsx | 490 +++++------ 13 files changed, 1897 insertions(+), 2430 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/admin/DropdownSettingsPage.tsx b/apps/edr-freight-web/portal/src/pages/admin/DropdownSettingsPage.tsx index 1b50ca786..93d466006 100644 --- a/apps/edr-freight-web/portal/src/pages/admin/DropdownSettingsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/admin/DropdownSettingsPage.tsx @@ -18,8 +18,22 @@ import EditDropdownSettingDialog from "./EditDropdownSettingDialog"; import ManageDropdownOptionsDialog from "./ManageDropdownOptionsDialog"; import DeleteDropdownSettingDialog from "./DeleteDropdownSettingDialog"; import { dropdownSettings } from "./dropdownSettings.mock"; +import { + DataTable, + DataTableFooter, + type ColumnDef, + usePagination, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Input, +} from "@edr/ui-common"; export default function DropdownSettingsPage() { + const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); const filtered = useMemo(() => { @@ -33,6 +47,16 @@ export default function DropdownSettingsPage() { ); }, [query]); + const total = filtered.length; + const pageCount = Math.ceil(total / pagination.pageSize); + const start = pagination.pageIndex * pagination.pageSize; + const end = Math.min(start + pagination.pageSize, total); + + const paginatedData = useMemo( + () => filtered.slice(start, end), + [start, end, filtered], + ); + const totalOptions = dropdownSettings.reduce( (sum, s) => sum + s.children.length, 0, @@ -42,9 +66,132 @@ export default function DropdownSettingsPage() { (s) => s.meta?.searchable, ).length; + const columns: ColumnDef<(typeof dropdownSettings)[number]>[] = [ + { + id: "setting", + header: "Setting", + cell: ({ row }) => { + const s = row.original; + return ( +
+
+ +
+
+

{s.label}

+

+ {s.description ?? "No description"} +

+
+
+ ); + }, + }, + { + id: "code", + header: "Code", + cell: ({ row }) => ( + + {row.original.code} + + ), + }, + { + id: "options", + header: "Options", + cell: ({ row }) => { + const s = row.original; + return ( +
+ + {s.children.length} +
+ ); + }, + }, + { + id: "behavior", + header: "Behavior", + cell: ({ row }) => { + const s = row.original; + return ( +
+ {s.multiple ? ( + + ) : ( + + )} + {s.meta?.searchable ? ( + + ) : null} + {s.meta?.clearable ? ( + + ) : null} +
+ ); + }, + }, + { + id: "permissions", + header: "Permissions", + cell: ({ row }) => { + const s = row.original; + return ( +
+ {(s.meta?.permissions ?? []).length === 0 ? ( + + ) : ( + (s.meta?.permissions ?? []).map((p) => ( + + + {p} + + )) + )} +
+ ); + }, + }, + { + id: "actions", + header: () =>
Actions
, + cell: ({ row }) => { + const setting = row.original; + return ( +
+ + + + + + + + + + + +
+ ); + }, + }, + ]; + return ( -
-
+
+
- {/* Header */} -
+

Dropdown Settings

-

+

Manage every dynamic dropdown across the platform — labels, options, ordering, and permissions.

@@ -66,208 +212,122 @@ export default function DropdownSettingsPage() {
- - + setQuery(e.target.value)} + onChange={(e) => { + setQuery(e.target.value); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + }} placeholder="Search by code, label, description..." - className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20" + className="pl-8!" />
- +
-
+
- {/* Stats */}
- } - /> - } - /> - } - /> - } - /> + + +
+

Settings

+

+ {dropdownSettings.length} +

+
+
+ +
+
+
+ + + +
+

Total Options

+

+ {totalOptions} +

+
+
+ +
+
+
+ + + +
+

Multi-select

+

+ {multipleCount} +

+
+
+ +
+
+
+ + + +
+

Searchable

+

+ {searchableCount} +

+
+
+ +
+
+
- {/* Table */} -
-
+ +
-

- Registered Dropdowns -

-

+ Registered Dropdowns + Every dynamic dropdown the platform reads from. -

+
- -
+ + -
- - - - - - - - - - - - - - {filtered.length === 0 ? ( - - - - ) : ( - filtered.map((setting) => ( - - - - - - - - - - - - - - )) - )} - -
SettingCodeOptionsBehaviorPermissionsActions
- No dropdown settings match your search. -
-
-
- -
-
-

- {setting.label} -

-

- {setting.description ?? "No description"} -

-
-
-
- - {setting.code} - - -
- - - {setting.children.length} - -
-
-
- {setting.multiple ? ( - - ) : ( - - )} - {setting.meta?.searchable ? ( - - ) : null} - {setting.meta?.clearable ? ( - - ) : null} -
-
-
- {(setting.meta?.permissions ?? []).length === 0 ? ( - - ) : ( - (setting.meta?.permissions ?? []).map((p) => ( - - - {p} - - )) - )} -
-
-
- - - - - - - - - - - -
-
-
-
+ + {}} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount: pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-b shadow-none" + footer={DataTableFooter} + /> + +
); @@ -285,34 +345,10 @@ function BehaviorChip({ className={ muted ? "rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-600" - : "rounded-full bg-[#10B981]/10 px-2 py-0.5 text-xs font-medium text-[#10B981]" + : "rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary" } > {label} ); } - -function StatCard({ - title, - value, - icon, -}: { - title: string; - value: string; - icon: React.ReactNode; -}) { - return ( -
-
-
-

{title}

-

{value}

-
-
- {icon} -
-
-
- ); -} diff --git a/apps/edr-freight-web/portal/src/pages/billing/BillingPage.tsx b/apps/edr-freight-web/portal/src/pages/billing/BillingPage.tsx index a1f594e40..49b314dd4 100644 --- a/apps/edr-freight-web/portal/src/pages/billing/BillingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/billing/BillingPage.tsx @@ -1,8 +1,6 @@ import { useMemo, useState } from "react"; import { AlertCircle, - ChevronLeft, - ChevronRight, Clock, DollarSign, Download, @@ -22,8 +20,19 @@ import { invoices, type InvoiceStatus, } from "./invoices.mock"; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; +import { + DataTable, + DataTableFooter, + type ColumnDef, + usePagination, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Input, +} from "@edr/ui-common"; type FilterValue = "All" | InvoiceStatus; @@ -37,8 +46,7 @@ const FILTERS: FilterValue[] = [ ]; export default function BillingPage() { - const [pageSize, setPageSize] = useState(10); - const [page, setPage] = useState(1); + const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [filter, setFilter] = useState("All"); const [query, setQuery] = useState(""); @@ -56,13 +64,13 @@ export default function BillingPage() { }, [filter, query]); const total = filtered.length; - const totalPages = Math.max(1, Math.ceil(total / pageSize)); - const safePage = Math.min(page, totalPages); - const start = (safePage - 1) * pageSize; - const end = Math.min(start + pageSize, total); - const paginated = useMemo( + const pageCount = Math.ceil(total / pagination.pageSize); + const start = pagination.pageIndex * pagination.pageSize; + const end = Math.min(start + pagination.pageSize, total); + + const paginatedData = useMemo( () => filtered.slice(start, end), - [filtered, start, end], + [start, end, filtered], ); const totalRevenue = invoices @@ -77,73 +85,179 @@ export default function BillingPage() { .reduce((sum, inv) => sum + inv.amount, 0); const overdueCount = invoices.filter((inv) => inv.status === "Overdue").length; + const columns: ColumnDef<(typeof invoices)[number]>[] = [ + { + id: "invoice", + header: "Invoice", + cell: ({ row }) => { + const inv = row.original; + return ( +
+
+ +
+
+

{inv.number}

+

Issued {inv.issueDate}

+
+
+ ); + }, + }, + { + accessorKey: "customer", + header: "Customer", + }, + { + accessorKey: "bookingReference", + header: "Booking", + }, + { + id: "amount", + header: "Amount", + cell: ({ row }) => { + const inv = row.original; + return ( + + {formatCurrency(inv.amount, inv.currency)} + + ); + }, + }, + { + accessorKey: "dueDate", + header: "Due Date", + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "actions", + header: () =>
Actions
, + cell: ({ row }) => { + const invoice = row.original; + return ( +
+ + + + + + + + + +
+ ); + }, + }, + ]; + return ( -
-
+
+
- {/* Header */} -
+

Billing

-

+

Manage invoices, payments, and financial records.

- - + { setQuery(e.target.value); - setPage(1); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); }} placeholder="Search invoices..." - className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20" + className="pl-8!" />
- +
-
+ - {/* Stats */}
- } - tone="brand" - /> - } - tone="brand" - /> - } - tone="danger" - /> + + +
+

Total Revenue (USD)

+

+ {formatCurrency(totalRevenue, "USD")} +

+
+
+ +
+
+
+ + + +
+

Outstanding (USD)

+

+ {formatCurrency(outstanding, "USD")} +

+
+
+ +
+
+
+ + + +
+

Overdue Invoices

+

+ {overdueCount} +

+
+
+ +
+
+
- {/* Filter tabs */} -
+
{FILTERS.map((f) => { const isActive = f === filter; @@ -157,12 +271,12 @@ export default function BillingPage() { type="button" onClick={() => { setFilter(f); - setPage(1); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); }} className={ isActive - ? "inline-flex items-center gap-2 rounded-2xl bg-[#10B981] px-4 py-2 text-sm font-medium text-white" - : "inline-flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-[#10B981]/10 hover:text-[#10B981]" + ? "inline-flex items-center gap-2 rounded-2xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground" + : "inline-flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-primary/10 hover:text-primary" } > {f} @@ -179,276 +293,44 @@ export default function BillingPage() { ); })}
-
+ - {/* Invoices Table */} -
-
+ +
-

- Invoices -

-

+ Invoices + Issued invoices and their payment status. -

+
- -
+ + -
- - - - - - - - - - - - - - - {paginated.length === 0 ? ( - - - - ) : ( - paginated.map((invoice) => ( - - - - - - - - - - - - - - - - )) - )} - -
InvoiceCustomerBookingAmountDue DateStatusActions
- No invoices match your filters. -
-
-
- -
-
-

- {invoice.number} -

-

- Issued {invoice.issueDate} -

-
-
-
- {invoice.customer} - - {invoice.bookingReference} - - {formatCurrency(invoice.amount, invoice.currency)} - - {invoice.dueDate} - - - -
- - - - - - - - - -
-
-
- - { - setPageSize(size); - setPage(1); - }} - /> -
-
-
- ); -} - -function Pagination({ - page, - pageSize, - total, - totalPages, - start, - end, - onPageChange, - onPageSizeChange, -}: { - page: number; - pageSize: number; - total: number; - totalPages: number; - start: number; - end: number; - onPageChange: (page: number) => void; - onPageSizeChange: (size: number) => void; -}) { - const showingFrom = total === 0 ? 0 : start + 1; - - return ( -
-
- - - - Showing {showingFrom}–{end} of {total} - -
- -
- - - {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => { - const isActive = p === page; - return ( - - ); - })} - - -
-
- ); -} - -function StatCard({ - title, - value, - icon, - tone, -}: { - title: string; - value: string; - icon: React.ReactNode; - tone?: "brand" | "danger"; -}) { - const iconWrap = - tone === "danger" - ? "bg-red-100 text-red-600" - : "bg-[#10B981] text-white"; - - return ( -
-
-
-

{title}

-

{value}

-
-
- {icon} -
+ + {}} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount: pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-b shadow-none" + footer={DataTableFooter} + /> + +
); 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 0fa42a826..cd1bd1e99 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx @@ -17,6 +17,11 @@ import Breadcrumbs from "@/components/Breadcrumbs"; import NewBookingPage from "./NewBookingPage"; import DeleteBookingDialog from "./DeleteBookingDialog"; import { getBookingById, type BookingStatus } from "./bookings.mock"; +import { + Button, + Card, + CardContent, +} from "@edr/ui-common"; export default function BookingDetailPage() { const { id } = useParams<{ id: string }>(); @@ -25,37 +30,37 @@ export default function BookingDetailPage() { if (!booking) { return ( -
-
+
+
-
+

Booking not found

-

+

The booking you're looking for doesn't exist or has been removed.

- + Back to Bookings -
+
); } return ( -
-
+
+
- {/* Header */} -
+
-
- +
+

{booking.reference}

-
+
{booking.customer} {booking.requestedDate} @@ -103,59 +107,49 @@ export default function BookingDetailPage() { specialInstructions: booking.specialInstructions, }} > - + navigate("/bookings")} > - +
-
+ - {/* Route banner */} -
+
} + icon={} /> -
- - +
+ +
} + icon={} />
-
+
- {/* Transport Legs (multimodal only) */} {booking.transportMode === "Multimodal" && booking.legs && booking.legs.length > 0 ? ( -
+
- +

Transport Legs

- + {booking.legs.length} legs
@@ -163,10 +157,10 @@ export default function BookingDetailPage() { {booking.legs.map((leg, i) => (
-
+
{i + 1}
@@ -175,7 +169,7 @@ export default function BookingDetailPage() {

{leg.from || "—"} - + {leg.to || "—"}

@@ -183,29 +177,28 @@ export default function BookingDetailPage() {
))}
-
+
) : null} - {/* Detail grid */}
} + icon={} label="Customer" value={booking.customer} /> } + icon={} label="Cargo Type" value={booking.cargoType} /> } + icon={} label="Container" value={`${booking.containerCount} × ${booking.containerType}`} /> } + icon={} label="Weight" value={`${booking.weightTons} tons`} /> @@ -213,17 +206,17 @@ export default function BookingDetailPage() { } + icon={} label="Transport Mode" value={booking.transportMode} /> } + icon={} label="Requested Date" value={booking.requestedDate} /> } + icon={} label="Priority" value={booking.priority} /> @@ -231,14 +224,14 @@ export default function BookingDetailPage() {
- +

{booking.cargoDescription}

- +

{booking.specialInstructions}

@@ -259,7 +252,7 @@ function RouteEndpoint({ }) { return (
-
+
{icon}
@@ -280,10 +273,10 @@ function DetailCard({ children: React.ReactNode; }) { return ( -
+

{title}

{children}
-
+ ); } @@ -298,7 +291,7 @@ function DetailRow({ }) { return (
-
{icon}
+
{icon}

{label}

{value}

diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingsPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingsPage.tsx index c9e356ae2..3febdeff5 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingsPage.tsx @@ -1,9 +1,7 @@ -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { Link } from "react-router-dom"; import { ArrowRight, - ChevronLeft, - ChevronRight, Clock, Eye, Filter, @@ -19,342 +17,248 @@ import Breadcrumbs from "@/components/Breadcrumbs"; import NewBookingPage from "./NewBookingPage"; import DeleteBookingDialog from "./DeleteBookingDialog"; import { bookings, type BookingStatus } from "./bookings.mock"; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; +import { + DataTable, + DataTableFooter, + type ColumnDef, + usePagination, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Input, +} from "@edr/ui-common"; export default function BookingsPage() { - const [pageSize, setPageSize] = useState(10); - const [page, setPage] = useState(1); + const { pagination, setPagination } = usePagination({ pageSize: 10 }); const total = bookings.length; - const totalPages = Math.max(1, Math.ceil(total / pageSize)); - const safePage = Math.min(page, totalPages); - const start = (safePage - 1) * pageSize; - const end = Math.min(start + pageSize, total); - const paginated = useMemo( + const pageCount = Math.ceil(total / pagination.pageSize); + const start = pagination.pageIndex * pagination.pageSize; + const end = Math.min(start + pagination.pageSize, total); + + const paginatedData = useMemo( () => bookings.slice(start, end), [start, end], ); + const columns: ColumnDef<(typeof bookings)[number]>[] = [ + { + accessorKey: "reference", + header: "Reference", + cell: ({ row }) => { + const booking = row.original; + return ( +
+
+ +
+
+

{booking.reference}

+

{booking.requestedDate}

+
+
+ ); + }, + }, + { + accessorKey: "customer", + header: "Customer", + }, + { + id: "route", + header: "Route", + cell: ({ row }) => ( +
+ {row.original.originStation} + + {row.original.destinationStation} +
+ ), + }, + { + id: "cargo", + header: "Cargo", + cell: ({ row }) => { + const b = row.original; + return ( +
+

{b.cargoType}

+

+ {b.containerCount} × {b.containerType} · {b.weightTons}t +

+
+ ); + }, + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "actions", + header: () =>
Actions
, + cell: ({ row }) => { + const booking = row.original; + return ( +
+ + + + + + + + + + + +
+ ); + }, + }, + ]; + return ( -
-
+
+
- {/* Header */} -
+

Bookings

-

+

Manage and monitor your freight bookings.

- - +
- +
-
+ - {/* Stats */}
- } - /> - b.status === "In Transit").length, - )} - icon={} - /> - b.status === "Pending").length, - )} - icon={} - /> + + +
+

Total Bookings

+

+ {total} +

+
+
+ +
+
+
+ + + +
+

In Transit

+

+ {bookings.filter((b) => b.status === "In Transit").length} +

+
+
+ +
+
+
+ + + +
+

Pending

+

+ {bookings.filter((b) => b.status === "Pending").length} +

+
+
+ +
+
+
- {/* Booking Table */} -
-
+ +
-

- Booking List -

-

+ Booking List + Recent freight bookings and their status. -

+
- -
+ + -
- - - - - - - - - - - - - - {paginated.map((booking) => ( - - - - - - - - - - - - - - ))} - -
ReferenceCustomerRouteCargoStatusActions
-
-
- -
-
-

- {booking.reference} -

-

- {booking.requestedDate} -

-
-
-
- {booking.customer} - -
- {booking.originStation} - - {booking.destinationStation} -
-
-
-

{booking.cargoType}

-

- {booking.containerCount} × {booking.containerType} ·{" "} - {booking.weightTons}t -

-
-
- - -
- - - - - - - - - - - -
-
-
- - { - setPageSize(size); - setPage(1); - }} - /> -
-
-
- ); -} - -function Pagination({ - page, - pageSize, - total, - totalPages, - start, - end, - onPageChange, - onPageSizeChange, -}: { - page: number; - pageSize: number; - total: number; - totalPages: number; - start: number; - end: number; - onPageChange: (page: number) => void; - onPageSizeChange: (size: number) => void; -}) { - const showingFrom = total === 0 ? 0 : start + 1; - - return ( -
-
- - - - Showing {showingFrom}–{end} of {total} - -
- -
- - - {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => { - const isActive = p === page; - return ( - - ); - })} - - -
-
- ); -} - -function StatCard({ - title, - value, - icon, -}: { - title: string; - value: string; - icon: React.ReactNode; -}) { - return ( -
-
-
-

{title}

-

{value}

-
-
- {icon} -
+ + {}} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount: pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-b shadow-none" + footer={DataTableFooter} + /> + +
); @@ -377,4 +281,3 @@ function StatusBadge({ status }: { status: BookingStatus }) { ); } - diff --git a/apps/edr-freight-web/portal/src/pages/consignments/ConsignmentDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/consignments/ConsignmentDetailPage.tsx index ab7b0bdaf..e554a9c11 100644 --- a/apps/edr-freight-web/portal/src/pages/consignments/ConsignmentDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/consignments/ConsignmentDetailPage.tsx @@ -21,6 +21,10 @@ import { getConsignmentById, type ConsignmentStatus, } from "./consignments.mock"; +import { + Button, + Card, +} from "@edr/ui-common"; export default function ConsignmentDetailPage() { const { id } = useParams<{ id: string }>(); @@ -29,38 +33,37 @@ export default function ConsignmentDetailPage() { if (!consignment) { return ( -
-
+
+
-
+

Consignment not found

-

- The consignment you're looking for doesn't exist or has been - removed. +

+ The consignment you're looking for doesn't exist or has been removed.

- + Back to Consignments -
+
); } return ( -
-
+
+
- {/* Header */} -
+
-
- +
+

{consignment.trackingNumber} {consignment.hazardous ? ( - + Hazardous ) : null}

-
+
{consignment.customer} Booking {consignment.bookingReference} @@ -113,65 +115,55 @@ export default function ConsignmentDetailPage() { estimatedDelivery: consignment.estimatedDelivery, }} > - + navigate("/consignments")} > - +
-
+ - {/* Route banner */} -
+
- +
-
+ - {/* Detail grid */}
} + icon={} label="Cargo Type" value={consignment.cargoType} /> } + icon={} label="Pieces" value={String(consignment.pieces)} /> } + icon={} label="Weight" value={`${consignment.weightKg.toLocaleString()} kg`} /> } + icon={} label="Volume" value={`${consignment.volumeM3} m³`} /> @@ -179,22 +171,22 @@ export default function ConsignmentDetailPage() { } + icon={} label="Customer" value={consignment.customer} /> } + icon={} label="Booking" value={consignment.bookingReference} /> } + icon={} label="Created" value={consignment.createdAt} /> } + icon={} label="Estimated Delivery" value={consignment.estimatedDelivery} /> @@ -202,14 +194,14 @@ export default function ConsignmentDetailPage() {
- +

{consignment.description}

- +

{consignment.specialHandling}

@@ -228,8 +220,8 @@ function RouteEndpoint({ }) { return (
-
- +
+

@@ -249,10 +241,10 @@ function DetailCard({ children: React.ReactNode; }) { return ( -

+

{title}

{children}
-
+ ); } @@ -267,7 +259,7 @@ function DetailRow({ }) { return (
-
{icon}
+
{icon}

{label}

{value}

diff --git a/apps/edr-freight-web/portal/src/pages/consignments/ConsignmentsPage.tsx b/apps/edr-freight-web/portal/src/pages/consignments/ConsignmentsPage.tsx index 157ad7c23..93376c1e3 100644 --- a/apps/edr-freight-web/portal/src/pages/consignments/ConsignmentsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/consignments/ConsignmentsPage.tsx @@ -4,8 +4,6 @@ import { AlertTriangle, ArrowRight, CheckCircle2, - ChevronLeft, - ChevronRight, Eye, Filter, Package, @@ -23,8 +21,19 @@ import { consignments, type ConsignmentStatus, } from "./consignments.mock"; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; +import { + DataTable, + DataTableFooter, + type ColumnDef, + usePagination, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Input, +} from "@edr/ui-common"; type FilterValue = "All" | ConsignmentStatus; @@ -38,8 +47,7 @@ const FILTERS: FilterValue[] = [ ]; export default function ConsignmentsPage() { - const [pageSize, setPageSize] = useState(10); - const [page, setPage] = useState(1); + const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [filter, setFilter] = useState("All"); const [query, setQuery] = useState(""); @@ -60,13 +68,13 @@ export default function ConsignmentsPage() { }, [filter, query]); const total = filtered.length; - const totalPages = Math.max(1, Math.ceil(total / pageSize)); - const safePage = Math.min(page, totalPages); - const start = (safePage - 1) * pageSize; - const end = Math.min(start + pageSize, total); - const paginated = useMemo( + const pageCount = Math.ceil(total / pagination.pageSize); + const start = pagination.pageIndex * pagination.pageSize; + const end = Math.min(start + pagination.pageSize, total); + + const paginatedData = useMemo( () => filtered.slice(start, end), - [filtered, start, end], + [start, end, filtered], ); const inTransitCount = consignments.filter( @@ -77,76 +85,221 @@ export default function ConsignmentsPage() { ).length; const hazmatCount = consignments.filter((c) => c.hazardous).length; + const columns: ColumnDef<(typeof consignments)[number]>[] = [ + { + id: "trackingNumber", + header: "Tracking #", + cell: ({ row }) => { + const c = row.original; + return ( +
+
+ +
+
+

+ {c.trackingNumber} + {c.hazardous ? ( + + + DG + + ) : null} +

+

{c.createdAt}

+
+
+ ); + }, + }, + { + accessorKey: "bookingReference", + header: "Booking", + }, + { + accessorKey: "customer", + header: "Customer", + }, + { + id: "route", + header: "Route", + cell: ({ row }) => ( +
+ {row.original.originStation} + + {row.original.destinationStation} +
+ ), + }, + { + id: "cargo", + header: "Cargo", + cell: ({ row }) => { + const c = row.original; + return ( +
+

{c.cargoType}

+

+ {c.pieces} pcs · {c.weightKg.toLocaleString()} kg · {c.volumeM3} m³ +

+
+ ); + }, + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "actions", + header: () =>
Actions
, + cell: ({ row }) => { + const consignment = row.original; + return ( +
+ + + + + + + + + + + +
+ ); + }, + }, + ]; + return ( -
-
+
+
- {/* Header */} -
+

Consignments

-

+

Track cargo units and their handling status.

- - + { setQuery(e.target.value); - setPage(1); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); }} placeholder="Search consignments..." - className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20" + className="pl-8!" />
- +
-
+ - {/* Stats */}
- } - /> - } - /> - } - /> - } - tone="danger" - /> + + +
+

Total Consignments

+

+ {consignments.length} +

+
+
+ +
+
+
+ + + +
+

In Transit

+

+ {inTransitCount} +

+
+
+ +
+
+
+ + + +
+

Delivered

+

+ {deliveredCount} +

+
+
+ +
+
+
+ + + +
+

Hazardous

+

+ {hazmatCount} +

+
+
+ +
+
+
- {/* Filter tabs */} -
+
{FILTERS.map((f) => { const isActive = f === filter; @@ -160,12 +313,12 @@ export default function ConsignmentsPage() { type="button" onClick={() => { setFilter(f); - setPage(1); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); }} className={ isActive - ? "inline-flex items-center gap-2 rounded-2xl bg-[#10B981] px-4 py-2 text-sm font-medium text-white" - : "inline-flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-[#10B981]/10 hover:text-[#10B981]" + ? "inline-flex items-center gap-2 rounded-2xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground" + : "inline-flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-primary/10 hover:text-primary" } > {f} @@ -182,303 +335,44 @@ export default function ConsignmentsPage() { ); })}
-
+ - {/* Table */} -
-
+ +
-

- Consignment List -

-

+ Consignment List + Cargo units and their current handling status. -

+
- -
+ + -
- - - - - - - - - - - - - - - {paginated.length === 0 ? ( - - - - ) : ( - paginated.map((consignment) => ( - - - - - - - - - - - - - - - - )) - )} - -
Tracking #BookingCustomerRouteCargoStatusActions
- No consignments match your filters. -
-
-
- -
-
-

- {consignment.trackingNumber} - {consignment.hazardous ? ( - - - DG - - ) : null} -

-

- {consignment.createdAt} -

-
-
-
- {consignment.bookingReference} - - {consignment.customer} - -
- {consignment.originStation} - - {consignment.destinationStation} -
-
-
-

{consignment.cargoType}

-

- {consignment.pieces} pcs ·{" "} - {consignment.weightKg.toLocaleString()} kg ·{" "} - {consignment.volumeM3} m³ -

-
-
- - -
- - - - - - - - - - - -
-
-
- - { - setPageSize(size); - setPage(1); - }} - /> -
-
-
- ); -} - -function Pagination({ - page, - pageSize, - total, - totalPages, - start, - end, - onPageChange, - onPageSizeChange, -}: { - page: number; - pageSize: number; - total: number; - totalPages: number; - start: number; - end: number; - onPageChange: (page: number) => void; - onPageSizeChange: (size: number) => void; -}) { - const showingFrom = total === 0 ? 0 : start + 1; - - return ( -
-
- - - - Showing {showingFrom}–{end} of {total} - -
- -
- - - {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => { - const isActive = p === page; - return ( - - ); - })} - - -
-
- ); -} - -function StatCard({ - title, - value, - icon, - tone, -}: { - title: string; - value: string; - icon: React.ReactNode; - tone?: "brand" | "danger"; -}) { - const iconWrap = - tone === "danger" - ? "bg-red-100 text-red-600" - : "bg-[#10B981] text-white"; - - return ( -
-
-
-

{title}

-

{value}

-
-
- {icon} -
+ + {}} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount: pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-b shadow-none" + footer={DataTableFooter} + /> + +
); diff --git a/apps/edr-freight-web/portal/src/pages/customers/CustomersPage.tsx b/apps/edr-freight-web/portal/src/pages/customers/CustomersPage.tsx index e912f8b4f..e0e1e807a 100644 --- a/apps/edr-freight-web/portal/src/pages/customers/CustomersPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/customers/CustomersPage.tsx @@ -90,11 +90,10 @@ export default function CustomerPage() { const customer = row.original; return (
- - + + - - +
); diff --git a/apps/edr-freight-web/portal/src/pages/customers/DeleteCustomerDialog.tsx b/apps/edr-freight-web/portal/src/pages/customers/DeleteCustomerDialog.tsx index d0d766c0c..859e415e9 100644 --- a/apps/edr-freight-web/portal/src/pages/customers/DeleteCustomerDialog.tsx +++ b/apps/edr-freight-web/portal/src/pages/customers/DeleteCustomerDialog.tsx @@ -28,7 +28,7 @@ export default function DeleteCustomerDialog({ {children} - + Delete customer? diff --git a/apps/edr-freight-web/portal/src/pages/customers/NewCustomerPage.tsx b/apps/edr-freight-web/portal/src/pages/customers/NewCustomerPage.tsx index 459732ad8..6cfd61b50 100644 --- a/apps/edr-freight-web/portal/src/pages/customers/NewCustomerPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/customers/NewCustomerPage.tsx @@ -7,12 +7,11 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; + Input, + Label, + Button, + Textarea, +} from "@edr/ui-common"; import { Building2, @@ -61,7 +60,7 @@ export default function NewCustomerPage({ {children ?? } - + {title} diff --git a/apps/edr-freight-web/portal/src/pages/dashboard/DashboardPage.tsx b/apps/edr-freight-web/portal/src/pages/dashboard/DashboardPage.tsx index d0bdd3faa..9add083c1 100644 --- a/apps/edr-freight-web/portal/src/pages/dashboard/DashboardPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/dashboard/DashboardPage.tsx @@ -36,12 +36,17 @@ import { customers } from "../customers/customers.mock"; import { invoices } from "../billing/invoices.mock"; import { shipments } from "../tracking/shipments.mock"; import { trains } from "../trains/trains.mock"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@edr/ui-common"; -// Main brand color: #10B981 (emerald-500). Lighter variants for chart -// hierarchy so multi-series lines/areas/bars stay visually distinct. -const BRAND = "#10B981"; // emerald-500 -const BRAND_LIGHT = "#6EE7B7"; // emerald-300 -const BRAND_LIGHTER = "#D1FAE5"; // emerald-100 +const BRAND = "#10B981"; +const BRAND_LIGHT = "#6EE7B7"; +const BRAND_LIGHTER = "#D1FAE5"; const STATUS_PALETTE: Record = { Pending: "#d97706", @@ -52,7 +57,6 @@ const STATUS_PALETTE: Record = { }; export default function DashboardPage() { - /** Top KPI numbers. */ const totalRevenue = useMemo( () => invoices @@ -70,7 +74,6 @@ export default function DashboardPage() { 100, ); - /** Bookings per month (last 6 mock months). */ const bookingsTrend = useMemo(() => { const months = ["Dec", "Jan", "Feb", "Mar", "Apr", "May"]; const total = bookings.length; @@ -81,7 +84,6 @@ export default function DashboardPage() { })); }, []); - /** Revenue by currency. */ const revenueByCurrency = useMemo(() => { const buckets = new Map(); invoices.forEach((inv) => { @@ -97,7 +99,6 @@ export default function DashboardPage() { })); }, []); - /** Booking status distribution (pie). */ const bookingStatusData = useMemo(() => { const buckets = new Map(); bookings.forEach((b) => { @@ -110,7 +111,6 @@ export default function DashboardPage() { })); }, []); - /** Cargo type distribution (horizontal bar). */ const cargoTypeData = useMemo(() => { const buckets = new Map(); bookings.forEach((b) => { @@ -121,7 +121,6 @@ export default function DashboardPage() { .sort((a, b) => b.count - a.count); }, []); - /** Active corridor performance (multi-line). */ const corridorPerformance = useMemo(() => { const weeks = ["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8"]; return weeks.map((label, i) => ({ @@ -132,7 +131,6 @@ export default function DashboardPage() { })); }, []); - /** Fleet utilization. */ const fleetUtilization = useMemo(() => { const total = trains.length; return [ @@ -159,38 +157,35 @@ export default function DashboardPage() { ].filter((entry) => entry.value > 0); }, []); - /** Recent shipments (top 5 by id). */ const recentShipments = useMemo( () => [...shipments].slice(-5).reverse(), [], ); return ( -
-
+
+
- {/* Header */} -
+
-

+

Freight Dashboard

-

+

Operational overview · today · all corridors

- + ● Live - + Last 30 days
-
+ - {/* KPIs */}
} + icon={} /> } + icon={} /> } + icon={} /> } + icon={} />
- {/* Trend + Cargo mix */}
- {/* Bookings trend (area chart) */} - {/* Booking status (donut) */} @@ -333,7 +325,6 @@ export default function DashboardPage() {
- {/* Corridor + Cargo */}
- {/* Revenue + Fleet + Recent */}
- {/* Recent shipments */} -
-
+ +
-

- Recent Shipments -

-

Latest activity

+ Recent Shipments + Latest activity
View all - + -
+ -
    - {recentShipments.map((s) => ( -
  • -
    - -
    -

    - {s.reference} -

    -

    - {s.originStation} → {s.destinationStation} -

    + +
      + {recentShipments.map((s) => ( +
    • +
      + +
      +

      + {s.reference} +

      +

      + {s.originStation} → {s.destinationStation} +

      +
      -
    - - {s.progress}% - -
  • - ))} -
-
+ + {s.progress}% + + + ))} + + +
- {/* Bottom summary cards */}
} + icon={} /> } + icon={} /> } + icon={} /> } + icon={} />
@@ -615,24 +602,24 @@ function KpiCard({ const TrendIcon = trend === "up" ? ArrowUpRight : ArrowDownRight; return ( -
-
+ +

{label}

{value}

- + {delta} vs last period
-
+
{icon}
-
-
+ + ); } @@ -648,17 +635,13 @@ function ChartCard({ className?: string; }) { return ( -
-
-

{title}

- {subtitle ? ( -

{subtitle}

- ) : null} -
- {children} -
+ + + {title} + {subtitle ? {subtitle} : null} + + {children} + ); } @@ -676,10 +659,10 @@ function SummaryCard({ return (
-
+
{icon}
@@ -687,7 +670,7 @@ function SummaryCard({

{value}

- + ); } diff --git a/apps/edr-freight-web/portal/src/pages/documents/DocumentsPage.tsx b/apps/edr-freight-web/portal/src/pages/documents/DocumentsPage.tsx index ed454739a..bd7976499 100644 --- a/apps/edr-freight-web/portal/src/pages/documents/DocumentsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/documents/DocumentsPage.tsx @@ -1,8 +1,6 @@ import { useMemo, useState } from "react"; import { CheckCircle2, - ChevronLeft, - ChevronRight, Clock, Download, Eye, @@ -30,8 +28,19 @@ import { type DocumentRecord, type DocumentStatus, } from "./documents.mock"; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; +import { + DataTable, + DataTableFooter, + type ColumnDef, + usePagination, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Input, +} from "@edr/ui-common"; type FilterValue = "All" | DocumentStatus; type ViewMode = "grid" | "table"; @@ -46,8 +55,7 @@ const FILTERS: FilterValue[] = [ ]; export default function DocumentsPage() { - const [pageSize, setPageSize] = useState(10); - const [page, setPage] = useState(1); + const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [filter, setFilter] = useState("All"); const [query, setQuery] = useState(""); const [view, setView] = useState("table"); @@ -67,13 +75,13 @@ export default function DocumentsPage() { }, [filter, query]); const total = filtered.length; - const totalPages = Math.max(1, Math.ceil(total / pageSize)); - const safePage = Math.min(page, totalPages); - const start = (safePage - 1) * pageSize; - const end = Math.min(start + pageSize, total); - const paginated = useMemo( + const pageCount = Math.ceil(total / pagination.pageSize); + const start = pagination.pageIndex * pagination.pageSize; + const end = Math.min(start + pagination.pageSize, total); + + const paginatedData = useMemo( () => filtered.slice(start, end), - [filtered, start, end], + [start, end, filtered], ); const totalSize = documents.reduce((sum, d) => sum + d.sizeBytes, 0); @@ -82,18 +90,109 @@ export default function DocumentsPage() { (d) => d.status === "Pending Review", ).length; + const columns: ColumnDef[] = [ + { + id: "document", + header: "Document", + cell: ({ row }) => { + const doc = row.original; + return ( +
+
+ +
+
+

{doc.name}

+

+ {doc.format} · By {doc.uploadedBy} +

+
+
+ ); + }, + }, + { + accessorKey: "type", + header: "Type", + }, + { + id: "linkedTo", + header: "Linked To", + cell: ({ row }) => ( +
+

{row.original.linkedReference}

+

{row.original.linkedType}

+
+ ), + }, + { + id: "size", + header: "Size", + cell: ({ row }) => ( + + {formatBytes(row.original.sizeBytes)} + + ), + }, + { + accessorKey: "uploadedAt", + header: "Uploaded", + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "actions", + header: () =>
Actions
, + cell: ({ row }) => { + const doc = row.original; + return ( +
+ + + + + + + + +
+ ); + }, + }, + ]; + return ( -
-
+
+
- {/* Header */} -
+

Documents

-

+

Manage freight documents linked to bookings, consignments, and invoices.

@@ -101,147 +200,200 @@ export default function DocumentsPage() {
- - + { setQuery(e.target.value); - setPage(1); + setPagination({ + pageIndex: 0, + pageSize: pagination.pageSize, + }); }} placeholder="Search documents..." - className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20" + className="pl-8!" />
- +
-
+
- {/* Stats */}
- } - /> - } - /> - } - /> - } - /> + + +
+

Total Documents

+

+ {documents.length} +

+
+
+ +
+
+
+ + + +
+

Approved

+

+ {approvedCount} +

+
+
+ +
+
+
+ + + +
+

Pending Review

+

+ {pendingCount} +

+
+
+ +
+
+
+ + + +
+

Storage Used

+

+ {formatBytes(totalSize)} +

+
+
+ +
+
+
- {/* Filter tabs + view toggle */} -
-
- {FILTERS.map((f) => { - const isActive = f === filter; - const count = - f === "All" - ? documents.length - : documents.filter((d) => d.status === f).length; - return ( - - ); - })} -
+ {f} + + {count} + + + ); + })} +
-
- setView("grid")} - label="Grid view" - > - - Grid - - setView("table")} - label="Table view" - > - - Table - +
+ setView("grid")} + label="Grid view" + > + + Grid + + setView("table")} + label="Table view" + > + + Table + +
-
+ - {/* Empty / Grid / Table */} - {paginated.length === 0 ? ( -
-

+ {paginatedData.length === 0 ? ( + +

No documents match your filters.

-
+ ) : view === "grid" ? (
- {paginated.map((doc) => ( + {paginatedData.map((doc) => ( ))}
) : ( - - )} + + +
+ Document Library + + All freight documents stored in the system. + +
+ +
- {filtered.length > 0 ? ( -
- { - setPageSize(size); - setPage(1); - }} - /> -
- ) : null} + + { }} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount: pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-b shadow-none" + footer={DataTableFooter} + /> + +
+ )}
); @@ -266,8 +418,8 @@ function ViewToggleButton({ aria-pressed={active} className={ active - ? "inline-flex items-center gap-2 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-[#10B981] shadow-sm" - : "inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 transition hover:text-[#10B981]" + ? "inline-flex items-center gap-2 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-primary shadow-sm" + : "inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 transition hover:text-primary" } > {children} @@ -276,85 +428,74 @@ function ViewToggleButton({ } function FormatIcon({ format }: { format: DocumentFormat }) { - const className = "h-5 w-5 text-[#10B981]"; - if (format === "PDF") return ; - if (format === "DOCX") return ; - if (format === "XLSX") return ; - if (format === "PNG" || format === "JPG") - return ; - return ; + if (format === "PDF") return ; + if (format === "DOCX") return ; + if (format === "XLSX") return ; + if (format === "PNG" || format === "JPG") return ; + return ; } function DocumentCard({ doc }: { doc: DocumentRecord }) { return ( -
-
-
-
- -
-
-

- {doc.name} -

-

{doc.type}

+ +
+
+
+
+ +
+
+

+ {doc.name} +

+

{doc.type}

+
+
- -
-
- - - - -
+
+ + + + +
-

By {doc.uploadedBy}

+

By {doc.uploadedBy}

-
- - - - + + - - - - - - + + + + + +
-
+ ); } @@ -367,250 +508,6 @@ function MetaRow({ label, value }: { label: string; value: string }) { ); } -function DocumentTable({ documents: rows }: { documents: DocumentRecord[] }) { - return ( -
-
-
-

- Document Library -

-

- All freight documents stored in the system. -

-
- - -
- -
- - - - - - - - - - - - - - - {rows.map((doc) => ( - - - - - - - - - - - - - - - - ))} - -
DocumentTypeLinked ToSizeUploadedStatusActions
-
-
- -
-
-

- {doc.name} -

-

- {doc.format} · By {doc.uploadedBy} -

-
-
-
{doc.type} -
-

{doc.linkedReference}

-

{doc.linkedType}

-
-
- {formatBytes(doc.sizeBytes)} - - {doc.uploadedAt} - - - -
- - - - - - - - -
-
-
-
- ); -} - -function Pagination({ - page, - pageSize, - total, - totalPages, - start, - end, - onPageChange, - onPageSizeChange, -}: { - page: number; - pageSize: number; - total: number; - totalPages: number; - start: number; - end: number; - onPageChange: (page: number) => void; - onPageSizeChange: (size: number) => void; -}) { - const showingFrom = total === 0 ? 0 : start + 1; - - return ( -
-
- - - - Showing {showingFrom}–{end} of {total} - -
- -
- - - {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => { - const isActive = p === page; - return ( - - ); - })} - - -
-
- ); -} - -function StatCard({ - title, - value, - icon, -}: { - title: string; - value: string; - icon: React.ReactNode; -}) { - return ( -
-
-
-

{title}

-

{value}

-
-
- {icon} -
-
-
- ); -} - function StatusBadge({ status }: { status: DocumentStatus }) { const styles: Record = { Draft: "bg-slate-100 text-slate-600", diff --git a/apps/edr-freight-web/portal/src/pages/tracking/TrackingPage.tsx b/apps/edr-freight-web/portal/src/pages/tracking/TrackingPage.tsx index 47b37d952..ce277ff9c 100644 --- a/apps/edr-freight-web/portal/src/pages/tracking/TrackingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/tracking/TrackingPage.tsx @@ -23,6 +23,15 @@ import { type ShipmentMode, type ShipmentStatus, } from "./shipments.mock"; +import { + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Input, +} from "@edr/ui-common"; type FilterValue = "All" | ShipmentStatus; type ViewMode = "grid" | "table"; @@ -52,107 +61,103 @@ export default function TrackingPage() { }, [filter, query]); return ( -
-
+
+
- {/* Header */} -
+

Shipment Tracking

-

+

Real-time cargo and shipment monitoring.

- - + setQuery(e.target.value)} placeholder="Search shipments..." - className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20" + className="pl-8!" />
- +
-
+ - {/* Filter tabs + view toggle */} -
-
- {FILTERS.map((f) => { - const isActive = f === filter; - const count = - f === "All" - ? shipments.length - : shipments.filter((s) => s.status === f).length; - return ( - - ); - })} -
+ {f} + + {count} + + + ); + })} +
-
- setView("grid")} - label="Grid view" - > - - Grid - - setView("table")} - label="Table view" - > - - Table - +
+ setView("grid")} + label="Grid view" + > + + Grid + + setView("table")} + label="Table view" + > + + Table + +
-
+ - {/* Empty / Grid / Table */} {filtered.length === 0 ? ( -
-

+ +

No shipments match your filters.

-
+ ) : view === "grid" ? (
{filtered.map((shipment) => ( @@ -186,8 +191,8 @@ function ViewToggleButton({ aria-pressed={active} className={ active - ? "inline-flex items-center gap-2 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-[#10B981] shadow-sm" - : "inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 transition hover:text-[#10B981]" + ? "inline-flex items-center gap-2 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-primary shadow-sm" + : "inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 transition hover:text-primary" } > {children} @@ -197,253 +202,246 @@ function ViewToggleButton({ function ShipmentCard({ shipment }: { shipment: Shipment }) { return ( -
-
-
- - {shipment.reference} - - - Consignment {shipment.consignmentReference} - + +
+
+
+ + {shipment.reference} + + + Consignment {shipment.consignmentReference} + +
+
- -
- {/* Route */} -
-
-

- From -

-

- {shipment.originStation} -

+
+
+

+ From +

+

+ {shipment.originStation} +

+
+ +
+

+ To +

+

+ {shipment.destinationStation} +

+
- -
-

- To -

-

- {shipment.destinationStation} -

-
-
- {/* Progress */} -
-
- Progress - - {shipment.progress}% - +
+
+ Progress + + {shipment.progress}% + +
+
+
+
-
-
-
-
- {/* Meta */} -
-
- - {shipment.customer} +
+
+ + {shipment.customer} +
+
+ + {shipment.mode} +
+
+ + {shipment.currentLocation} +
+
+ + + Updated {shipment.lastUpdate} · ETA {shipment.eta} + +
-
- - {shipment.mode} -
-
- - {shipment.currentLocation} -
-
- - - Updated {shipment.lastUpdate} · ETA {shipment.eta} - -
-
- {/* Actions */} -
- - - + + - - - + + + +
-
+ ); } function ShipmentTable({ shipments: rows }: { shipments: Shipment[] }) { return ( -
-
- - - - - - - - - - - - - - + + +
+ Shipment List + All shipments and their current status. +
+
-
- {rows.map((shipment) => ( - - - - - - - - - - - - - - - - - - + +
+
ShipmentBookingRouteModeStatusProgressCurrent LocationETAActions
-
-
- -
-
-

- {shipment.reference} -

-

- {shipment.customer} -

-
-
-
- {shipment.bookingReference} - -
- {shipment.originStation} - - {shipment.destinationStation} -
-
-
- - {shipment.mode} -
-
- - -
-
-
-
- - {shipment.progress}% - -
-
-
- - {shipment.currentLocation} -
-
- {shipment.eta} - -
- - - - - - - -
-
+ + + + + + + + + + + - ))} - -
ShipmentBookingRouteModeStatusProgressCurrent LocationETAActions
-
-
+ + + + {rows.map((shipment) => ( + + +
+
+ +
+
+

+ {shipment.reference} +

+

+ {shipment.customer} +

+
+
+ + + + {shipment.bookingReference} + + + +
+ {shipment.originStation} + + {shipment.destinationStation} +
+ + + +
+ + {shipment.mode} +
+ + + + + + + +
+
+
+
+ + {shipment.progress}% + +
+ + + +
+ + {shipment.currentLocation} +
+ + + + {shipment.eta} + + + +
+ + + + + + + +
+ + + ))} + + +
+ + ); } function ModeIcon({ mode }: { mode: ShipmentMode }) { - if (mode === "rail") return ; - if (mode === "truck") return ; - return ; + if (mode === "rail") return ; + if (mode === "truck") return ; + return ; } function StatusBadge({ status }: { status: ShipmentStatus }) { diff --git a/apps/edr-freight-web/portal/src/pages/trains/TrainsPage.tsx b/apps/edr-freight-web/portal/src/pages/trains/TrainsPage.tsx index 63447360d..42447f5b5 100644 --- a/apps/edr-freight-web/portal/src/pages/trains/TrainsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/trains/TrainsPage.tsx @@ -1,7 +1,5 @@ import { useMemo, useState } from "react"; import { - ChevronLeft, - ChevronRight, Filter, Gauge, Pencil, @@ -16,8 +14,19 @@ import Breadcrumbs from "@/components/Breadcrumbs"; import NewTrainPage from "./NewTrainPage"; import DeleteTrainDialog from "./DeleteTrainDialog"; import { trains, type TrainStatus } from "./trains.mock"; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; +import { + DataTable, + DataTableFooter, + type ColumnDef, + usePagination, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Input, +} from "@edr/ui-common"; type FilterValue = "All" | TrainStatus; @@ -30,8 +39,7 @@ const FILTERS: FilterValue[] = [ ]; export default function TrainsPage() { - const [pageSize, setPageSize] = useState(10); - const [page, setPage] = useState(1); + const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [filter, setFilter] = useState("All"); const [query, setQuery] = useState(""); @@ -50,13 +58,13 @@ export default function TrainsPage() { }, [filter, query]); const total = filtered.length; - const totalPages = Math.max(1, Math.ceil(total / pageSize)); - const safePage = Math.min(page, totalPages); - const start = (safePage - 1) * pageSize; - const end = Math.min(start + pageSize, total); - const paginated = useMemo( + const pageCount = Math.ceil(total / pagination.pageSize); + const start = pagination.pageIndex * pagination.pageSize; + const end = Math.min(start + pagination.pageSize, total); + + const paginatedData = useMemo( () => filtered.slice(start, end), - [filtered, start, end], + [start, end, filtered], ); const operationalCount = trains.filter( @@ -66,70 +74,174 @@ export default function TrainsPage() { (t) => t.status === "In Maintenance", ).length; + const columns: ColumnDef<(typeof trains)[number]>[] = [ + { + id: "train", + header: "Train", + cell: ({ row }) => { + const t = row.original; + return ( +
+
+ +
+
+

{t.code}

+

{t.name}

+
+
+ ); + }, + }, + { + accessorKey: "type", + header: "Type", + }, + { + accessorKey: "capacityTons", + header: "Capacity", + cell: ({ row }) => {row.original.capacityTons}t, + }, + { + accessorKey: "depot", + header: "Depot", + }, + { + accessorKey: "mileageKm", + header: "Mileage", + cell: ({ row }) => ( + {row.original.mileageKm.toLocaleString()} km + ), + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "actions", + header: () =>
Actions
, + cell: ({ row }) => { + const train = row.original; + return ( +
+ + + + + + + +
+ ); + }, + }, + ]; + return ( -
-
+
+
- {/* Header */} -
+

Trains

-

+

Fleet roster, capacity, and maintenance status.

- - + { setQuery(e.target.value); - setPage(1); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); }} placeholder="Search trains..." - className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20" + className="pl-8!" />
- +
-
+ - {/* Stats */}
- } - /> - } - /> - } - /> + + +
+

Total Trains

+

+ {trains.length} +

+
+
+ +
+
+
+ + + +
+

Operational

+

+ {operationalCount} +

+
+
+ +
+
+
+ + + +
+

In Maintenance

+

+ {maintenanceCount} +

+
+
+ +
+
+
- {/* Filter tabs */} -
+
{FILTERS.map((f) => { const isActive = f === filter; @@ -143,12 +255,12 @@ export default function TrainsPage() { type="button" onClick={() => { setFilter(f); - setPage(1); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); }} className={ isActive - ? "inline-flex items-center gap-2 rounded-2xl bg-[#10B981] px-4 py-2 text-sm font-medium text-white" - : "inline-flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-[#10B981]/10 hover:text-[#10B981]" + ? "inline-flex items-center gap-2 rounded-2xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground" + : "inline-flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-primary/10 hover:text-primary" } > {f} @@ -165,262 +277,44 @@ export default function TrainsPage() { ); })}
-
+ - {/* Train Table */} -
-
+ +
-

- Fleet Roster -

-

+ Fleet Roster + All locomotives and wagons in service. -

+
- -
+ + -
- - - - - - - - - - - - - - - {paginated.length === 0 ? ( - - - - ) : ( - paginated.map((train) => ( - - - - - - - - - - - - - - - - )) - )} - -
TrainTypeCapacityDepotMileageStatusActions
- No trains match your filters. -
-
-
- -
-
-

- {train.code} -

-

- {train.name} -

-
-
-
- {train.type} - - {train.capacityTons}t - - {train.depot} - - {train.mileageKm.toLocaleString()} km - - - -
- - - - - - - -
-
-
- - { - setPageSize(size); - setPage(1); - }} - /> -
-
-
- ); -} - -function Pagination({ - page, - pageSize, - total, - totalPages, - start, - end, - onPageChange, - onPageSizeChange, -}: { - page: number; - pageSize: number; - total: number; - totalPages: number; - start: number; - end: number; - onPageChange: (page: number) => void; - onPageSizeChange: (size: number) => void; -}) { - const showingFrom = total === 0 ? 0 : start + 1; - - return ( -
-
- - - - Showing {showingFrom}–{end} of {total} - -
- -
- - - {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => { - const isActive = p === page; - return ( - - ); - })} - - -
-
- ); -} - -function StatCard({ - title, - value, - icon, -}: { - title: string; - value: string; - icon: React.ReactNode; -}) { - return ( -
-
-
-

{title}

-

{value}

-
-
- {icon} -
+ + {}} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount: pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-b shadow-none" + footer={DataTableFooter} + /> + +
);