Files
edr-platform/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseDashboardCharts.tsx
Hagernesh 2e22b72e27 feat(warehouse): assemble dashboard cockpit + server-side throughput series
- WarehouseDashboardPage now composes the ops KPI strip, lifecycle cards, flow
  charts, zone-occupancy heatmap and demurrage/accrual exceptions into one
  control-tower view; drop the redundant lifecycle donut.
- New GET /warehouse-inventory/throughput (date_trunc time series) replaces the
  client-side buildTrend that downloaded the entire inventory list to bucket it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:27:53 +00:00

153 lines
5.5 KiB
TypeScript

import { useState } from 'react';
import { Card, Group, SegmentedControl, Stack, Text, ThemeIcon } from '@mantine/core';
import { BarChart3, CalendarRange } from 'lucide-react';
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { useWarehouseThroughput } from '@/hooks/useWarehouses';
import type { WarehouseDashboard } from '@/types/warehouse';
interface WarehouseDashboardChartsProps {
data?: WarehouseDashboard;
}
const ORANGE = '#f08c00';
const GREEN = '#22c55e'; // green from bookings
/** Inventory lifecycle status series — one distinct color per status (aligned with status badges). */
const STATUS_SERIES = [
{ key: 'stored', label: 'Stored', color: '#228be6' }, // blue
{ key: 'reserved', label: 'Reserved', color: '#ae3ec9' }, // grape
{ key: 'readyForLoading', label: 'Ready', color: '#f08c00' }, // orange
{ key: 'loaded', label: 'Loaded', color: '#12b886' }, // teal
{ key: 'dispatched', label: 'Dispatched', color: GREEN }, // green (bookings)
] as const;
type Granularity = 'week' | 'month' | 'year';
/** Label a period start according to the selected granularity. */
function formatPeriod(iso: string, granularity: Granularity): string {
const d = new Date(iso);
if (granularity === 'year') return String(d.getFullYear());
if (granularity === 'week') return d.toLocaleDateString('en', { day: 'numeric', month: 'short' });
return d.toLocaleDateString('en', { month: 'short' });
}
export function WarehouseDashboardCharts({ data }: WarehouseDashboardChartsProps) {
const [granularity, setGranularity] = useState<Granularity>('month');
// Server-side time series (replaces downloading the whole inventory to bucket).
const { data: series = [] } = useWarehouseThroughput(granularity);
const trend = series.map((p) => ({
label: formatPeriod(p.periodStart, granularity),
received: p.received,
dispatched: p.dispatched,
}));
const hasTrend = trend.some((b) => b.received > 0 || b.dispatched > 0);
const statusData = STATUS_SERIES.map((s) => ({
name: s.label,
value: data ? Number(data[s.key as keyof WarehouseDashboard] ?? 0) : 0,
color: s.color,
}));
const hasStatus = statusData.some((d) => d.value > 0);
return (
<Stack gap="md">
{/* Time-filtered throughput */}
<Card withBorder radius="lg" padding="lg">
<Group justify="space-between" mb="md" wrap="wrap">
<Group gap="sm">
<ThemeIcon size="lg" radius="md" style={{ backgroundColor: ORANGE, color: '#fff' }}>
<CalendarRange size={20} />
</ThemeIcon>
<div>
<Text fw={700}>Throughput Over Time</Text>
<Text size="xs" c="dimmed">
Received vs dispatched inventory
</Text>
</div>
</Group>
<SegmentedControl
value={granularity}
onChange={(v) => setGranularity(v as Granularity)}
data={[
{ label: 'Weekly', value: 'week' },
{ label: 'Monthly', value: 'month' },
{ label: 'Yearly', value: 'year' },
]}
/>
</Group>
{hasTrend ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={trend} margin={{ top: 8, right: 8, left: -16, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--mantine-color-gray-2)" />
<XAxis dataKey="label" tick={{ fontSize: 12 }} />
<YAxis allowDecimals={false} tick={{ fontSize: 12 }} />
<Tooltip cursor={{ fill: 'var(--mantine-color-gray-1)' }} />
<Legend iconType="circle" />
<Bar dataKey="received" name="Received" fill={ORANGE} radius={[6, 6, 0, 0]} />
<Bar dataKey="dispatched" name="Dispatched" fill={GREEN} radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<EmptyChart />
)}
</Card>
{/* Status distribution bar */}
<Card withBorder radius="lg" padding="lg">
<Group gap="sm" mb="md">
<ThemeIcon size="lg" radius="md" style={{ backgroundColor: GREEN, color: '#fff' }}>
<BarChart3 size={20} />
</ThemeIcon>
<div>
<Text fw={700}>Inventory by Status</Text>
<Text size="xs" c="dimmed">
Items at each lifecycle stage
</Text>
</div>
</Group>
{hasStatus ? (
<ResponsiveContainer width="100%" height={280}>
<BarChart data={statusData} margin={{ top: 8, right: 8, left: -16, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--mantine-color-gray-2)" />
<XAxis dataKey="name" tick={{ fontSize: 12 }} />
<YAxis allowDecimals={false} tick={{ fontSize: 12 }} />
<Tooltip cursor={{ fill: 'var(--mantine-color-gray-1)' }} />
<Bar dataKey="value" name="Items" radius={[6, 6, 0, 0]}>
{statusData.map((entry) => (
<Cell key={entry.name} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
) : (
<EmptyChart />
)}
</Card>
</Stack>
);
}
function EmptyChart() {
return (
<Group justify="center" align="center" h={280}>
<Text c="dimmed" size="sm">
No inventory data to chart yet.
</Text>
</Group>
);
}