mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
feat(customers): implement customer management page with mock data
- Created CustomersPage component to display a list of companies with search and pagination features. - Added mock data for companies, including various statuses and profiles. - Implemented a service layer to simulate API calls for fetching company data, bookings, documents, and payments. - Defined TypeScript types for company and related entities to ensure type safety. - Integrated Mantine components for UI consistency and improved user experience.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { Box, Card } from "@mantine/core";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface TableCardProps {
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Minimum width (px) the table is forced to occupy. The Mantine `Table` is
|
||||
* always `width: 100%`, so without a floor it can never overflow its
|
||||
* container and the horizontal scroll never engages. Setting a floor lets
|
||||
* columns keep a sensible width and the card scroll horizontally on narrow
|
||||
* viewports instead of squishing.
|
||||
*/
|
||||
minWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush card shell for a `DataTable`: a borderless, padding-less card whose
|
||||
* single child is a horizontally scrollable region. Pair with the table's
|
||||
* `containerClassName="border-0 shadow-none bg-transparent"` so every table on
|
||||
* the customer pages reads identically (same surface, same scroll behaviour).
|
||||
*/
|
||||
export function TableCard({ children, minWidth = 860 }: TableCardProps) {
|
||||
return (
|
||||
<Card p={0}>
|
||||
<Box style={{ overflowX: "auto" }} w="100%">
|
||||
<Box miw={minWidth}>{children}</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableCard;
|
||||
@@ -0,0 +1,209 @@
|
||||
import { Badge, Group, Tooltip } from "@mantine/core";
|
||||
|
||||
import type {
|
||||
CompanyProfile,
|
||||
CompanyStatus,
|
||||
CompanyType,
|
||||
CustomerBookingStatus,
|
||||
CustomerPaymentStatus,
|
||||
ProfileStatus,
|
||||
ProfileType,
|
||||
} from "@/types/customer";
|
||||
|
||||
import { humanize } from "./format";
|
||||
|
||||
const badgeStyle = {
|
||||
fontSize: "0.7rem",
|
||||
letterSpacing: "0.04em",
|
||||
whiteSpace: "nowrap" as const,
|
||||
};
|
||||
|
||||
/** Shared status palette — active/paid green, pending amber, terminal red. */
|
||||
const STATUS_COLOR: Record<CompanyStatus | ProfileStatus, string> = {
|
||||
active: "edr-green",
|
||||
pending: "yellow",
|
||||
suspended: "orange",
|
||||
blacklisted: "red",
|
||||
};
|
||||
|
||||
const COMPANY_TYPE_COLOR: Record<CompanyType, string> = {
|
||||
customer: "edr-green",
|
||||
freight_forwarder: "blue",
|
||||
dj_freight_forwarder: "indigo",
|
||||
transporter: "grape",
|
||||
};
|
||||
|
||||
const PROFILE_TYPE_COLOR: Record<ProfileType, string> = {
|
||||
importer: "teal",
|
||||
exporter: "cyan",
|
||||
freight_forwarder: "blue",
|
||||
dj_freight_forwarder: "indigo",
|
||||
transporter: "grape",
|
||||
};
|
||||
|
||||
export function CompanyStatusBadge({ status }: { status: CompanyStatus }) {
|
||||
return (
|
||||
<Badge
|
||||
color={STATUS_COLOR[status] ?? "gray"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
tt="capitalize"
|
||||
fw={600}
|
||||
style={badgeStyle}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompanyTypeBadge({ type }: { type: CompanyType }) {
|
||||
return (
|
||||
<Badge
|
||||
color={COMPANY_TYPE_COLOR[type] ?? "gray"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
fw={600}
|
||||
style={badgeStyle}
|
||||
>
|
||||
{humanize(type)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile chips for a company row: one chip per role (Importer / Exporter / …)
|
||||
* carrying its reference code. Caps at three (a company has at most three
|
||||
* profiles); any extra collapse into a `+N` chip.
|
||||
*/
|
||||
export function ProfileChips({
|
||||
profiles,
|
||||
max = 3,
|
||||
}: {
|
||||
profiles: CompanyProfile[];
|
||||
max?: number;
|
||||
}) {
|
||||
if (!profiles.length) {
|
||||
return (
|
||||
<Badge color="gray" variant="light" size="sm" radius="md" style={badgeStyle}>
|
||||
No profiles
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const shown = profiles.slice(0, max);
|
||||
const extra = profiles.length - shown.length;
|
||||
|
||||
return (
|
||||
<Group gap={6} wrap="wrap">
|
||||
{shown.map((profile) => (
|
||||
<Tooltip
|
||||
key={profile.id}
|
||||
label={`${humanize(profile.type)} · ${humanize(profile.status)}`}
|
||||
withArrow
|
||||
>
|
||||
<Badge
|
||||
color={PROFILE_TYPE_COLOR[profile.type] ?? "gray"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
fw={600}
|
||||
style={badgeStyle}
|
||||
>
|
||||
{humanize(profile.type)} · {profile.reference}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
))}
|
||||
{extra > 0 ? (
|
||||
<Badge color="gray" variant="light" size="sm" radius="md" style={badgeStyle}>
|
||||
+{extra}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProfileTypeBadge({ type }: { type: ProfileType }) {
|
||||
return (
|
||||
<Badge
|
||||
color={PROFILE_TYPE_COLOR[type] ?? "gray"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
fw={600}
|
||||
style={badgeStyle}
|
||||
>
|
||||
{humanize(type)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProfileStatusBadge({ status }: { status: ProfileStatus }) {
|
||||
return (
|
||||
<Badge
|
||||
color={STATUS_COLOR[status] ?? "gray"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
tt="capitalize"
|
||||
fw={600}
|
||||
style={badgeStyle}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const BOOKING_STATUS_COLOR: Record<CustomerBookingStatus, string> = {
|
||||
DRAFT: "gray",
|
||||
SUBMITTED: "yellow",
|
||||
PENDING_APPROVAL: "yellow",
|
||||
APPROVED: "cyan",
|
||||
PAID: "edr-green",
|
||||
IN_TRANSIT: "blue",
|
||||
COMPLETED: "indigo",
|
||||
REJECTED: "red",
|
||||
CANCELLED: "red",
|
||||
};
|
||||
|
||||
export function BookingStatusBadge({ status }: { status: CustomerBookingStatus }) {
|
||||
return (
|
||||
<Badge
|
||||
color={BOOKING_STATUS_COLOR[status] ?? "gray"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
tt="uppercase"
|
||||
fw={600}
|
||||
style={badgeStyle}
|
||||
>
|
||||
{humanize(status)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const PAYMENT_STATUS_COLOR: Record<CustomerPaymentStatus, string> = {
|
||||
"action-required": "orange",
|
||||
processing: "yellow",
|
||||
success: "edr-green",
|
||||
failed: "red",
|
||||
canceled: "gray",
|
||||
refunded: "grape",
|
||||
};
|
||||
|
||||
export function PaymentStatusBadge({ status }: { status: CustomerPaymentStatus }) {
|
||||
return (
|
||||
<Badge
|
||||
color={PAYMENT_STATUS_COLOR[status] ?? "gray"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
tt="capitalize"
|
||||
fw={600}
|
||||
style={badgeStyle}
|
||||
>
|
||||
{humanize(status)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/** Shared formatting helpers for the customer-management pages. */
|
||||
|
||||
/** snake_case / SCREAMING_CASE → Title Case. */
|
||||
export function humanize(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.split(/[_\s]+/)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function formatDate(value: string | null | undefined): string {
|
||||
if (!value) return "—";
|
||||
const d = new Date(value);
|
||||
return Number.isNaN(d.getTime())
|
||||
? "—"
|
||||
: d.toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export function formatMoney(amount: number, currency: string): string {
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
style: "currency",
|
||||
currency,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (!bytes) return "0 B";
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const value = bytes / Math.pow(1024, i);
|
||||
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export {
|
||||
BookingStatusBadge,
|
||||
CompanyStatusBadge,
|
||||
CompanyTypeBadge,
|
||||
PaymentStatusBadge,
|
||||
ProfileChips,
|
||||
ProfileStatusBadge,
|
||||
ProfileTypeBadge,
|
||||
} from "./badges";
|
||||
export { formatBytes, formatDate, formatMoney, humanize } from "./format";
|
||||
export { TableCard, type TableCardProps } from "./TableCard";
|
||||
Reference in New Issue
Block a user