style: ui fixes

This commit is contained in:
Nathnael
2026-08-13 13:46:46 +00:00
parent 0df4be1820
commit 89cdc0ad06
46 changed files with 417 additions and 648 deletions

View File

@@ -26,7 +26,7 @@ interface OverviewActivityHeatmapProps {
* selected range. The bright cells (and the peak badge) are the hours the
* intake team needs to be staffed for.
*/
export function OverviewActivityHeatmap({ cells }: OverviewActivityHeatmapProps) {
export function OverviewActivityHeatmap({ cells = [] }: OverviewActivityHeatmapProps) {
const countByCell = new Map(cells.map((c) => [`${c.dow}-${c.block}`, c.count]));
const max = Math.max(0, ...cells.map((c) => c.count));
const peak = cells.reduce<IOverviewHeatmapCell | null>(

View File

@@ -30,35 +30,35 @@ export function OverviewAttentionCard({ bookings, contracts, billing }: Overview
{
key: "needsAction",
label: "Bookings needing action",
count: bookings.needsAction,
count: bookings.needsAction ?? 0,
icon: AlertCircle,
href: "/dashboard/booking-requests",
},
{
key: "urgent",
label: "Urgent bookings",
count: bookings.urgent,
count: bookings.urgent ?? 0,
icon: Clock,
href: "/dashboard/booking-requests",
},
{
key: "contractsApproval",
label: "Contracts in approval",
count: contracts.inApproval,
count: contracts.inApproval ?? 0,
icon: FileSignature,
href: "/dashboard/contract-requests",
},
{
key: "contractsClearance",
label: "Contracts in clearance",
count: contracts.inClearance,
count: contracts.inClearance ?? 0,
icon: ShieldCheck,
href: "/dashboard/contracts/clearance",
},
{
key: "pendingPayments",
label: "Pending payments",
count: billing.pendingPayments,
count: billing.pendingPayments ?? 0,
icon: Banknote,
href: "/dashboard/payments",
},

View File

@@ -2,6 +2,7 @@ import { RefreshCw } from "lucide-react";
import { ActionIcon, Badge, Group, SegmentedControl, Stack, Text } from "@mantine/core";
import { useAuth } from "@/auth/useAuth";
import { formatDateTime } from "@/lib/format";
import { freightBrand } from "@/theme/freight-brand";
import type { OverviewRange } from "@/types/overview";
import "@/components/overview/overview.css";
@@ -20,7 +21,7 @@ function formatRelativeTime(iso: string | undefined) {
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return new Date(iso).toLocaleString();
return formatDateTime(iso);
}
function greeting(hour: number) {

View File

@@ -34,21 +34,27 @@ interface OverviewHeroKpisProps {
*/
export function OverviewHeroKpis({ kpis, current, previous, rangeLabel }: OverviewHeroKpisProps) {
const items: KpiItem[] = [
{
label: `Revenue (${rangeLabel})`,
value: <CountUp value={current.revenueEtb} format={(n) => formatCurrency(n, "ETB")} />,
hint: formatCurrency(current.revenueUsd, "USD"),
icon: Banknote,
color: "yellow",
delta: pctDelta(current.revenueEtb, previous.revenueEtb),
},
{
label: "Cargo moved",
value: <CountUp value={current.tons} format={(n) => `${Math.round(n).toLocaleString()} t`} />,
icon: Package,
color: "edr-green",
delta: pctDelta(current.tons, previous.tons),
},
// An API deployed before the overview revamp omits the period totals —
// drop the two tiles that need them rather than crash (or hide the strip).
...(current
? [
{
label: `Revenue (${rangeLabel})`,
value: <CountUp value={current.revenueEtb} format={(n) => formatCurrency(n, "ETB")} />,
hint: formatCurrency(current.revenueUsd, "USD"),
icon: Banknote,
color: "yellow",
delta: pctDelta(current.revenueEtb, previous?.revenueEtb ?? 0),
},
{
label: "Cargo moved",
value: <CountUp value={current.tons} format={(n) => `${Math.round(n).toLocaleString()} t`} />,
icon: Package,
color: "edr-green",
delta: pctDelta(current.tons, previous?.tons ?? 0),
},
]
: []),
{
label: "Active bookings",
value: <CountUp value={kpis.bookings.totalActive} />,

View File

@@ -25,7 +25,10 @@ const STATS: Array<{
/** Network snapshot: four operational stats plus real wagon-utilization (available / total), not a decorative gauge. */
export function OverviewNetworkCard({ kpis }: { kpis: IOverviewOperationsKpis }) {
const utilizationPct = kpis.wagonsTotal > 0 ? (kpis.wagonsAvailable / kpis.wagonsTotal) * 100 : null;
// An API deployed before the overview revamp omits the wagon counters.
const wagonsTotal = kpis.wagonsTotal ?? 0;
const wagonsAvailable = kpis.wagonsAvailable ?? 0;
const utilizationPct = wagonsTotal > 0 ? (wagonsAvailable / wagonsTotal) * 100 : null;
return (
<SummaryCard
@@ -52,10 +55,10 @@ export function OverviewNetworkCard({ kpis }: { kpis: IOverviewOperationsKpis })
Wagons available
</Text>
<Text fw={800} fz={22} lh={1.1}>
{kpis.wagonsAvailable.toLocaleString()}
{wagonsAvailable.toLocaleString()}
<Text component="span" fz="sm" fw={600} c="dimmed">
{" "}
/ {kpis.wagonsTotal.toLocaleString()}
/ {wagonsTotal.toLocaleString()}
</Text>
</Text>
</Stack>
@@ -72,7 +75,7 @@ export function OverviewNetworkCard({ kpis }: { kpis: IOverviewOperationsKpis })
/>
<Stack gap={0} style={{ minWidth: 0 }}>
<Text fw={800} fz="lg" lh={1.15}>
<CountUp value={kpis[stat.key]} />
<CountUp value={kpis[stat.key] ?? 0} />
</Text>
<Text size="xs" c="dimmed" truncate>
{stat.label}

View File

@@ -14,7 +14,7 @@ interface OverviewPipelineFunnelProps {
}
/** Booking pipeline by stage, in workflow order. Each row deep-links to the exact statuses it represents. */
export function OverviewPipelineFunnel({ data }: OverviewPipelineFunnelProps) {
export function OverviewPipelineFunnel({ data = [] }: OverviewPipelineFunnelProps) {
const rows = data
.map((item) => ({
...item,

View File

@@ -111,7 +111,7 @@ interface OverviewRevenueMixProps {
}
/** ETB revenue split two ways — trade direction and freight type — anchored by the range total. */
export function OverviewRevenueMix({ byDirection, byFreightType }: OverviewRevenueMixProps) {
export function OverviewRevenueMix({ byDirection = [], byFreightType = [] }: OverviewRevenueMixProps) {
const total = byDirection.reduce((sum, s) => sum + s.amountEtb, 0);
return (

View File

@@ -67,9 +67,9 @@ interface OverviewRevenueVolumeChartProps {
* period" at a glance.
*/
export function OverviewRevenueVolumeChart({
bookingTrend,
paymentTrend,
previousPaymentTrend,
bookingTrend = [],
paymentTrend = [],
previousPaymentTrend = [],
rangeDays,
}: OverviewRevenueVolumeChartProps) {
const data = mergeTrend(bookingTrend, paymentTrend, previousPaymentTrend, rangeDays);

View File

@@ -135,7 +135,7 @@ interface OverviewSankeyFlowProps {
* freight type. Ribbon thickness is proportional to revenue, so the biggest
* corridor is unmissable.
*/
export function OverviewSankeyFlow({ flows }: OverviewSankeyFlowProps) {
export function OverviewSankeyFlow({ flows = [] }: OverviewSankeyFlowProps) {
const data = toSankeyData(flows);
return (