mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
style: ui fixes
This commit is contained in:
@@ -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>(
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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} />,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user