mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(portal): Integrate @edr/ui-common components and centralize styling
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EDR Freight Portal</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<link rel="stylesheet" href="/index.css" />
|
||||
</body>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EDR Freight Portal</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -2,8 +2,9 @@ import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import "../index.css";
|
||||
import "@tria-plc/iamui-common/styles.css";
|
||||
import "@edr/ui-common/styles.css"
|
||||
import "@edr/ui-common/styles.css";
|
||||
|
||||
import App from "./App";
|
||||
import {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
Eye,
|
||||
Filter,
|
||||
@@ -18,63 +16,155 @@ import {
|
||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||
import NewCustomerPage from "./NewCustomerPage";
|
||||
import DeleteCustomerDialog from "./DeleteCustomerDialog";
|
||||
import { customers, type CustomerStatus } from "./customers.mock";
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
|
||||
import {
|
||||
customers,
|
||||
type CustomerStatus,
|
||||
type Customer,
|
||||
} from "./customers.mock";
|
||||
import {
|
||||
DataTable,
|
||||
DataTableFooter,
|
||||
type ColumnDef,
|
||||
usePagination,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
Input,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
export default function CustomerPage() {
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [page, setPage] = useState(1);
|
||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||
|
||||
const total = customers.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(
|
||||
() => customers.slice(start, end),
|
||||
[start, end],
|
||||
);
|
||||
|
||||
const columns: ColumnDef<Customer>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Customer",
|
||||
cell: ({ row }) => {
|
||||
const customer = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-secondary text-secondary-foreground border">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">{customer.name}</p>
|
||||
<p className="text-sm text-slate-500">ID #{customer.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "company",
|
||||
header: "Company",
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-slate-700">{row.original.email}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => <StatusBadge status={row.original.status} />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <div className="text-right">Actions</div>,
|
||||
cell: ({ row }) => {
|
||||
const customer = row.original;
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link
|
||||
to={`/customers/${customer.id}`}
|
||||
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Link>
|
||||
|
||||
<NewCustomerPage
|
||||
mode="edit"
|
||||
customer={{
|
||||
companyName: customer.company,
|
||||
customerType: customer.customerType,
|
||||
contactPerson: customer.name,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
tinNumber: customer.tinNumber,
|
||||
city: customer.city,
|
||||
country: customer.country,
|
||||
address: customer.address,
|
||||
notes: customer.notes,
|
||||
}}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Pencil />
|
||||
</Button>
|
||||
</NewCustomerPage>
|
||||
|
||||
<DeleteCustomerDialog customerName={customer.name}>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-xl border border-red-200 p-2 text-red-600 transition hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</DeleteCustomerDialog>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 p-6">
|
||||
<div className="mx-auto max-w-7xl space-y-6">
|
||||
<div className="min-h-screen p-6">
|
||||
<div className="space-y-6">
|
||||
<Breadcrumbs items={[{ label: "Customers" }]} />
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-4 rounded-3xl bg-white p-6 shadow-sm md:flex-row md:items-center md:justify-between">
|
||||
<Card className="p-6 flex-row justify-between ">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
|
||||
Customers
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
<p className="mt-1 text-sm text-secondary-foreground ">
|
||||
Manage and monitor your customer records.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
|
||||
<div className="relative w-full sm:w-80">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search customers..."
|
||||
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-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20"
|
||||
className="pl-8!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NewCustomerPage>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-[#33578D] px-4 py-2 text-sm font-medium text-white transition hover:bg-[#33578D]/90"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<Button>
|
||||
<Plus />
|
||||
Add Customer
|
||||
</button>
|
||||
</Button>
|
||||
</NewCustomerPage>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<StatCard
|
||||
title="Total Customers"
|
||||
@@ -95,221 +185,42 @@ export default function CustomerPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Customer Table */}
|
||||
<div className="overflow-hidden rounded-3xl bg-white shadow-sm">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
|
||||
<Card className="gap-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b ">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">
|
||||
Customer List
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500">
|
||||
<CardTitle>Customer List</CardTitle>
|
||||
<CardDescription>
|
||||
Recent customer activities and records.
|
||||
</p>
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<button className="inline-flex items-center gap-2 rounded-2xl border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]">
|
||||
<Filter className="h-4 w-4" />
|
||||
<Button variant="secondary" size="sm">
|
||||
<Filter />
|
||||
Filter
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[700px] whitespace-nowrap text-left">
|
||||
<thead className="bg-slate-50 text-sm text-slate-500">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-medium">Customer</th>
|
||||
<th className="px-6 py-4 font-medium">Company</th>
|
||||
<th className="px-6 py-4 font-medium">Email</th>
|
||||
<th className="px-6 py-4 font-medium">Status</th>
|
||||
<th className="px-6 py-4 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{paginated.map((customer) => (
|
||||
<tr
|
||||
key={customer.id}
|
||||
className="border-t border-slate-100 transition hover:bg-[#33578D]/5"
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#33578D]/10 text-[#33578D]">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">
|
||||
{customer.name}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">
|
||||
ID #{customer.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4 text-sm text-slate-700">
|
||||
{customer.company}
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4 text-sm text-slate-700">
|
||||
{customer.email}
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<StatusBadge status={customer.status} />
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link
|
||||
to={`/customers/${customer.id}`}
|
||||
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Link>
|
||||
|
||||
<NewCustomerPage
|
||||
mode="edit"
|
||||
customer={{
|
||||
companyName: customer.company,
|
||||
customerType: customer.customerType,
|
||||
contactPerson: customer.name,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
tinNumber: customer.tinNumber,
|
||||
city: customer.city,
|
||||
country: customer.country,
|
||||
address: customer.address,
|
||||
notes: customer.notes,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
</NewCustomerPage>
|
||||
|
||||
<DeleteCustomerDialog customerName={customer.name}>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-xl border border-red-200 p-2 text-red-600 transition hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</DeleteCustomerDialog>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
page={safePage}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
totalPages={totalPages}
|
||||
start={start}
|
||||
end={end}
|
||||
onPageChange={setPage}
|
||||
onPageSizeChange={(size) => {
|
||||
setPageSize(size);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col gap-3 border-t border-slate-100 px-6 py-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center gap-3 text-sm text-slate-500">
|
||||
<label htmlFor="page-size" className="font-medium text-slate-700">
|
||||
Rows per page
|
||||
</label>
|
||||
<select
|
||||
id="page-size"
|
||||
value={pageSize}
|
||||
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
||||
className="h-9 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-700 outline-none transition focus:border-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20"
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>
|
||||
Showing {showingFrom}–{end} of {total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={page <= 1}
|
||||
className="inline-flex h-9 items-center gap-1 rounded-xl border border-slate-200 px-3 text-sm font-medium text-slate-700 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-slate-700"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Prev
|
||||
</button>
|
||||
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => {
|
||||
const isActive = p === page;
|
||||
return (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
onClick={() => onPageChange(p)}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={
|
||||
isActive
|
||||
? "h-9 w-9 rounded-xl bg-[#33578D] text-sm font-medium text-white"
|
||||
: "h-9 w-9 rounded-xl border border-slate-200 text-sm font-medium text-slate-700 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
|
||||
}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
className="inline-flex h-9 items-center gap-1 rounded-xl border border-slate-200 px-3 text-sm font-medium text-slate-700 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-slate-700"
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
<CardContent className="px-0">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={paginatedData}
|
||||
status="success"
|
||||
onRowClick={() => { }}
|
||||
pagination={{
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
pageCount: pageCount,
|
||||
totalCount: total,
|
||||
}}
|
||||
tableOptions={{
|
||||
state: { pagination },
|
||||
onPaginationChange: setPagination,
|
||||
}}
|
||||
containerClassName="border-b shadow-none"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -325,8 +236,8 @@ function StatCard({
|
||||
icon: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-3xl bg-white p-6 shadow-sm transition hover:shadow-md hover:ring-1 hover:ring-[#33578D]/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<Card className="transition hover:shadow-md hover:ring-1 hover:ring-[#33578D]/20">
|
||||
<CardContent className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">{title}</p>
|
||||
<h3 className="mt-2 text-3xl font-bold text-slate-900">{value}</h3>
|
||||
@@ -335,8 +246,8 @@ function StatCard({
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#33578D]/10 text-[#33578D]">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
} from "@edr/ui-common";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button } from "@edr/ui-common";
|
||||
|
||||
export interface DeleteCustomerDialogProps {
|
||||
customerName: string;
|
||||
|
||||
Reference in New Issue
Block a user