Fe changes
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
'use client'
|
||||
|
||||
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Label, PolarAngleAxis, Pie, PieChart, RadialBar, RadialBarChart, XAxis, YAxis } from 'recharts'
|
||||
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from '@/components/ui/chart'
|
||||
|
||||
export interface ChartDatum {
|
||||
name: string
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface MonthlySalesPoint {
|
||||
month: string // yyyy-MM
|
||||
invoiced: number
|
||||
}
|
||||
|
||||
const fmtCompact = (v: number) => v.toLocaleString('en-LK', { notation: 'compact', maximumFractionDigits: 1 })
|
||||
const fmtMonth = (v: string) => new Date(`${v}-01`).toLocaleDateString('en-GB', { month: 'short', year: '2-digit' })
|
||||
|
||||
const SLICE_COLORS = ['var(--chart-2)', 'var(--chart-4)', 'var(--chart-1)', 'var(--chart-3)', 'var(--destructive)']
|
||||
|
||||
function Empty({ text }: { text: string }) {
|
||||
return <p className="text-xs text-muted-foreground">{text}</p>
|
||||
}
|
||||
|
||||
export function DonutChart({ data, centerLabel, emptyText }: { data: ChartDatum[]; centerLabel: string; emptyText: string }) {
|
||||
const total = data.reduce((sum, d) => sum + d.value, 0)
|
||||
if (total === 0) return <Empty text={emptyText} />
|
||||
|
||||
const slices = data.map((d, i) => ({ ...d, fill: SLICE_COLORS[i % SLICE_COLORS.length] }))
|
||||
const config = Object.fromEntries(slices.map(s => [s.name, { label: s.name, color: s.fill }])) as ChartConfig
|
||||
|
||||
return (
|
||||
<ChartContainer config={config} className="h-full min-h-40 w-full">
|
||||
<PieChart>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel nameKey="name" />} />
|
||||
<Pie data={slices} dataKey="value" nameKey="name" innerRadius="55%" strokeWidth={2}>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (!viewBox || !('cx' in viewBox)) return null
|
||||
return (
|
||||
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
|
||||
<tspan x={viewBox.cx} y={(viewBox.cy ?? 0) - 6} className="fill-foreground text-base font-semibold">{fmtCompact(total)}</tspan>
|
||||
<tspan x={viewBox.cx} y={(viewBox.cy ?? 0) + 12} className="fill-muted-foreground text-[11px]">{centerLabel}</tspan>
|
||||
</text>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent nameKey="name" />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export function HorizontalBarChart({ data, emptyText, color = 'var(--chart-2)' }: { data: ChartDatum[]; emptyText: string; color?: string }) {
|
||||
if (data.length === 0 || data.every(d => d.value === 0)) return <Empty text={emptyText} />
|
||||
|
||||
const config = { value: { label: 'Quantity', color } } satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<ChartContainer config={config} className="h-full min-h-40 w-full">
|
||||
<BarChart data={data} layout="vertical" margin={{ left: 0, right: 8 }}>
|
||||
<CartesianGrid horizontal={false} />
|
||||
<XAxis type="number" tickFormatter={fmtCompact} tickLine={false} axisLine={false} />
|
||||
<YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={96} />
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="value" fill="var(--color-value)" radius={4} maxBarSize={22}>
|
||||
{data.map(d => <Cell key={d.name} fill={color} />)}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export function ColumnBarChart({ data, emptyText, label }: { data: ChartDatum[]; emptyText: string; label: string }) {
|
||||
if (data.length === 0 || data.every(d => d.value === 0)) return <Empty text={emptyText} />
|
||||
|
||||
const config = { value: { label, color: 'var(--chart-2)' } } satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<ChartContainer config={config} className="h-full min-h-40 w-full">
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="name" tickLine={false} axisLine={false} interval={0} tick={{ fontSize: 10 }} />
|
||||
<YAxis tickFormatter={fmtCompact} tickLine={false} axisLine={false} width={40} />
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="value" fill="var(--color-value)" radius={4} maxBarSize={36} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export function SalesTrendChart({ data }: { data: MonthlySalesPoint[] }) {
|
||||
if (data.length === 0) return <Empty text="No invoices yet." />
|
||||
|
||||
const config = { invoiced: { label: 'Invoiced', color: 'var(--chart-2)' } } satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<ChartContainer config={config} className="h-full min-h-40 w-full">
|
||||
<AreaChart data={data}>
|
||||
<defs>
|
||||
<linearGradient id="dashFillInvoiced" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-invoiced)" stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor="var(--color-invoiced)" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="month" tickFormatter={fmtMonth} tickLine={false} axisLine={false} />
|
||||
<YAxis tickFormatter={fmtCompact} tickLine={false} axisLine={false} width={48} />
|
||||
<ChartTooltip content={<ChartTooltipContent labelFormatter={(v) => fmtMonth(String(v))} />} />
|
||||
<Area dataKey="invoiced" type="monotone" stroke="var(--color-invoiced)" fill="url(#dashFillInvoiced)" />
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export function RadialGauge({ pct, label, sub }: { pct: number; label: string; sub: string }) {
|
||||
const config = { value: { label, color: 'var(--chart-2)' } } satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-32 items-center gap-4">
|
||||
<div className="relative aspect-square h-full max-h-40 min-h-24">
|
||||
<ChartContainer config={config} className="size-full">
|
||||
<RadialBarChart data={[{ value: pct, fill: 'var(--color-value)' }]} startAngle={90} endAngle={-270} innerRadius="75%" outerRadius="100%">
|
||||
<PolarAngleAxis type="number" domain={[0, 100]} tick={false} />
|
||||
<RadialBar dataKey="value" background cornerRadius={10} />
|
||||
</RadialBarChart>
|
||||
</ChartContainer>
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center text-lg font-semibold text-foreground">{pct}%</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{sub}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
getPaginatedItems,
|
||||
} from '@/lib/api/index'
|
||||
import type { LowStockItemResponse, StockMovementResponse } from '@/types/api'
|
||||
import { ColumnBarChart, DonutChart, HorizontalBarChart, RadialGauge, SalesTrendChart } from './dashboard-charts'
|
||||
import type { ChartDatum, MonthlySalesPoint } from './dashboard-charts'
|
||||
|
||||
const STORAGE_KEY = 'distribution-dashboard-widgets-v3'
|
||||
const ROW_HEIGHT = 36
|
||||
@@ -31,7 +33,7 @@ type Breakpoint = keyof typeof DEFAULT_BREAKPOINTS
|
||||
const BREAKPOINT_ORDER = Object.keys(DEFAULT_BREAKPOINTS) as Breakpoint[]
|
||||
|
||||
type Category = 'sales' | 'ops' | 'stock' | 'finance'
|
||||
type Size = 'sm' | 'md' | 'lg'
|
||||
type Size = 'sm' | 'md' | 'lg' | 'chart' | 'chartWide'
|
||||
|
||||
interface WidgetMeta {
|
||||
id: string
|
||||
@@ -59,11 +61,18 @@ const WIDGET_META: WidgetMeta[] = [
|
||||
{ id: 'warehouseBalance', cat: 'stock', title: 'Warehouse Balances', size: 'md' },
|
||||
{ id: 'outstandingPay', cat: 'finance', title: 'Outstanding Payments', size: 'sm' },
|
||||
{ id: 'invoiceStatus', cat: 'finance', title: 'Invoices by Status', size: 'md' },
|
||||
{ id: 'salesTrendChart', cat: 'finance', title: 'Sales Trend (6 months)', size: 'chartWide' },
|
||||
{ id: 'paymentStatusChart', cat: 'finance', title: 'Payment Status', size: 'chart' },
|
||||
{ id: 'poStatusChart', cat: 'sales', title: 'Shop POs by Status', size: 'chart' },
|
||||
{ id: 'loadingSheetsChart', cat: 'ops', title: 'Loading Sheets Split', size: 'chart' },
|
||||
{ id: 'warehouseChart', cat: 'stock', title: 'Warehouse Stock Chart', size: 'chart' },
|
||||
{ id: 'movementTypeChart', cat: 'stock', title: 'Movements by Type', size: 'chart' },
|
||||
]
|
||||
|
||||
const DEFAULT_ACTIVE = [
|
||||
'pendingPO', 'awaitingConfirm', 'fulfillRate', 'openRides', 'loadingSheetsByStatus',
|
||||
'deliveryTasks', 'lowStock', 'outstandingPay', 'invoiceStatus',
|
||||
'salesTrendChart', 'poStatusChart', 'paymentStatusChart', 'warehouseChart',
|
||||
]
|
||||
|
||||
/** Width (in that breakpoint's own columns), height, and minimums per widget size, per breakpoint. Narrower breakpoints
|
||||
@@ -71,28 +80,38 @@ const DEFAULT_ACTIVE = [
|
||||
const SIZE_CONFIG: Record<Breakpoint, Record<Size, { w: number; h: number; minW: number; minH: number }>> = {
|
||||
lg: {
|
||||
sm: { w: 3, h: 4, minW: 2, minH: 3 },
|
||||
md: { w: 6, h: 5, minW: 3, minH: 3 },
|
||||
md: { w: 6, h: 6, minW: 3, minH: 3 },
|
||||
lg: { w: 12, h: 7, minW: 4, minH: 4 },
|
||||
chart: { w: 4, h: 7, minW: 3, minH: 5 },
|
||||
chartWide: { w: 8, h: 7, minW: 4, minH: 5 },
|
||||
},
|
||||
md: {
|
||||
sm: { w: 5, h: 4, minW: 3, minH: 3 },
|
||||
md: { w: 10, h: 5, minW: 4, minH: 3 },
|
||||
md: { w: 10, h: 6, minW: 4, minH: 3 },
|
||||
lg: { w: 10, h: 7, minW: 4, minH: 4 },
|
||||
chart: { w: 5, h: 7, minW: 3, minH: 5 },
|
||||
chartWide: { w: 10, h: 7, minW: 4, minH: 5 },
|
||||
},
|
||||
sm: {
|
||||
sm: { w: 3, h: 4, minW: 2, minH: 3 },
|
||||
md: { w: 6, h: 5, minW: 3, minH: 3 },
|
||||
md: { w: 6, h: 6, minW: 3, minH: 3 },
|
||||
lg: { w: 6, h: 7, minW: 3, minH: 4 },
|
||||
chart: { w: 6, h: 7, minW: 3, minH: 5 },
|
||||
chartWide: { w: 6, h: 7, minW: 3, minH: 5 },
|
||||
},
|
||||
xs: {
|
||||
sm: { w: 4, h: 4, minW: 2, minH: 3 },
|
||||
md: { w: 4, h: 5, minW: 2, minH: 3 },
|
||||
md: { w: 4, h: 6, minW: 2, minH: 3 },
|
||||
lg: { w: 4, h: 7, minW: 2, minH: 4 },
|
||||
chart: { w: 4, h: 7, minW: 2, minH: 5 },
|
||||
chartWide: { w: 4, h: 7, minW: 2, minH: 5 },
|
||||
},
|
||||
xxs: {
|
||||
sm: { w: 2, h: 4, minW: 1, minH: 3 },
|
||||
md: { w: 2, h: 5, minW: 1, minH: 3 },
|
||||
md: { w: 2, h: 6, minW: 1, minH: 3 },
|
||||
lg: { w: 2, h: 7, minW: 1, minH: 4 },
|
||||
chart: { w: 2, h: 7, minW: 1, minH: 5 },
|
||||
chartWide: { w: 2, h: 7, minW: 1, minH: 5 },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -198,6 +217,32 @@ interface DashboardData {
|
||||
draftInvoiceCount: number
|
||||
printedInvoiceCount: number
|
||||
finalizedInvoiceCount: number
|
||||
poStatusChart: ChartDatum[]
|
||||
paymentStatusChart: ChartDatum[]
|
||||
movementTypeChart: ChartDatum[]
|
||||
monthlySales: MonthlySalesPoint[]
|
||||
}
|
||||
|
||||
const PO_STATUSES = ['Pending', 'PartiallyFulfilled', 'Fulfilled', 'Cancelled'] as const
|
||||
const PAYMENT_STATUSES = ['Pending', 'PartiallyPaid', 'Paid'] as const
|
||||
const SALES_MONTHS = 6
|
||||
/** Recent-activity charts aggregate this many latest records client-side. */
|
||||
const CHART_SAMPLE_SIZE = 200
|
||||
|
||||
const splitWords = (s: string) => s.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
|
||||
function buildMonthlySales(rows: Array<{ createdAt: string; totalAmount: number }>): MonthlySalesPoint[] {
|
||||
const now = new Date()
|
||||
const months: MonthlySalesPoint[] = []
|
||||
for (let i = SALES_MONTHS - 1; i >= 0; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
|
||||
months.push({ month: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`, invoiced: 0 })
|
||||
}
|
||||
for (const r of rows) {
|
||||
const bucket = months.find(m => m.month === r.createdAt.slice(0, 7))
|
||||
if (bucket) bucket.invoiced += r.totalAmount
|
||||
}
|
||||
return months
|
||||
}
|
||||
|
||||
async function loadDashboardData(): Promise<DashboardData> {
|
||||
@@ -211,6 +256,8 @@ async function loadDashboardData(): Promise<DashboardData> {
|
||||
balances,
|
||||
outstanding,
|
||||
draftInv, printedInv, finalizedInv,
|
||||
poStatusCounts, paymentStatusCounts,
|
||||
recentInvoices, recentMovements,
|
||||
] = await Promise.all([
|
||||
shopPos.list({ status: 'Pending', pageSize: 1 }),
|
||||
shopPos.list({ confirmed: true, status: 'Pending', pageSize: 1 }),
|
||||
@@ -228,8 +275,17 @@ async function loadDashboardData(): Promise<DashboardData> {
|
||||
invoices.list({ status: 'Draft', pageSize: 1 }),
|
||||
invoices.list({ status: 'Printed', pageSize: 1 }),
|
||||
invoices.list({ status: 'Finalized', pageSize: 1 }),
|
||||
Promise.all(PO_STATUSES.map(status => shopPos.list({ status, pageSize: 1 }))),
|
||||
Promise.all(PAYMENT_STATUSES.map(paymentStatus => invoices.list({ paymentStatus, pageSize: 1 }))),
|
||||
invoices.list({ pageSize: CHART_SAMPLE_SIZE }),
|
||||
stock.movements({ pageSize: CHART_SAMPLE_SIZE }),
|
||||
])
|
||||
|
||||
const movementCounts = new Map<string, number>()
|
||||
for (const m of getPaginatedItems(recentMovements)) {
|
||||
movementCounts.set(m.type, (movementCounts.get(m.type) ?? 0) + 1)
|
||||
}
|
||||
|
||||
const balanceRows = getPaginatedItems(balances)
|
||||
const byWarehouse = new Map<string, WarehouseBalanceRow>()
|
||||
for (const b of balanceRows) {
|
||||
@@ -259,6 +315,10 @@ async function loadDashboardData(): Promise<DashboardData> {
|
||||
draftInvoiceCount: draftInv.total,
|
||||
printedInvoiceCount: printedInv.total,
|
||||
finalizedInvoiceCount: finalizedInv.total,
|
||||
poStatusChart: PO_STATUSES.map((s, i) => ({ name: splitWords(s), value: poStatusCounts[i].total })),
|
||||
paymentStatusChart: PAYMENT_STATUSES.map((s, i) => ({ name: splitWords(s), value: paymentStatusCounts[i].total })),
|
||||
movementTypeChart: Array.from(movementCounts, ([type, value]) => ({ name: splitWords(type), value })).sort((a, b) => b.value - a.value),
|
||||
monthlySales: buildMonthlySales(getPaginatedItems(recentInvoices)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,14 +331,6 @@ function CategoryPill({ cat }: { cat: Category }) {
|
||||
)
|
||||
}
|
||||
|
||||
function Bar({ pct, colorClass }: { pct: number; colorClass: string }) {
|
||||
return (
|
||||
<div className="h-2 rounded-full bg-muted overflow-hidden">
|
||||
<div className={`h-full rounded-full ${colorClass}`} style={{ width: `${Math.min(100, Math.max(0, pct))}%` }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function WidgetBody({ id, data }: { id: string; data: DashboardData }) {
|
||||
switch (id) {
|
||||
case 'pendingPO':
|
||||
@@ -297,13 +349,7 @@ function WidgetBody({ id, data }: { id: string; data: DashboardData }) {
|
||||
)
|
||||
case 'fulfillRate': {
|
||||
const pct = data.totalPOCount > 0 ? Math.round((data.fulfilledPOCount / data.totalPOCount) * 100) : 0
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2.5 text-2xl font-bold text-foreground">{pct}%</div>
|
||||
<Bar pct={pct} colorClass="bg-blue-600" />
|
||||
<div className="mt-2.5 text-xs text-muted-foreground">{data.fulfilledPOCount} of {data.totalPOCount} orders fulfilled</div>
|
||||
</div>
|
||||
)
|
||||
return <RadialGauge pct={pct} label="Fulfilled" sub={`${data.fulfilledPOCount} of ${data.totalPOCount} orders fulfilled`} />
|
||||
}
|
||||
case 'openRides':
|
||||
return data.openRides.length === 0 ? (
|
||||
@@ -321,29 +367,16 @@ function WidgetBody({ id, data }: { id: string; data: DashboardData }) {
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
case 'loadingSheetsByStatus': {
|
||||
const total = data.plannedSheetsCount + data.confirmedSheetsCount
|
||||
const plannedPct = total > 0 ? (data.plannedSheetsCount / total) * 100 : 0
|
||||
const confirmedPct = total > 0 ? (data.confirmedSheetsCount / total) * 100 : 0
|
||||
case 'loadingSheetsByStatus':
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">Planned</span>
|
||||
<span className="font-semibold text-foreground">{data.plannedSheetsCount}</span>
|
||||
</div>
|
||||
<Bar pct={plannedPct} colorClass="bg-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">Confirmed</span>
|
||||
<span className="font-semibold text-foreground">{data.confirmedSheetsCount}</span>
|
||||
</div>
|
||||
<Bar pct={confirmedPct} colorClass="bg-violet-600" />
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalBarChart
|
||||
data={[
|
||||
{ name: 'Planned', value: data.plannedSheetsCount },
|
||||
{ name: 'Confirmed', value: data.confirmedSheetsCount },
|
||||
]}
|
||||
emptyText="No loading sheets yet."
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'deliveryTasks':
|
||||
return (
|
||||
<div>
|
||||
@@ -387,24 +420,8 @@ function WidgetBody({ id, data }: { id: string; data: DashboardData }) {
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
case 'warehouseBalance': {
|
||||
const max = Math.max(1, ...data.warehouseBalances.map(w => w.quantity))
|
||||
return data.warehouseBalances.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No warehouse stock recorded.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{data.warehouseBalances.map(w => (
|
||||
<div key={w.locationId}>
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">{w.name}</span>
|
||||
<span className="font-semibold text-foreground">{w.quantity}</span>
|
||||
</div>
|
||||
<Bar pct={(w.quantity / max) * 100} colorClass="bg-amber-600" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
case 'warehouseBalance':
|
||||
return <HorizontalBarChart data={data.warehouseBalances.map(w => ({ name: w.name, value: w.quantity }))} emptyText="No warehouse stock recorded." />
|
||||
case 'outstandingPay':
|
||||
return (
|
||||
<div>
|
||||
@@ -412,34 +429,39 @@ function WidgetBody({ id, data }: { id: string; data: DashboardData }) {
|
||||
<div className="mt-1.5 text-xs text-muted-foreground">{data.outstandingInvoiceCount} invoices</div>
|
||||
</div>
|
||||
)
|
||||
case 'invoiceStatus': {
|
||||
const total = data.draftInvoiceCount + data.printedInvoiceCount + data.finalizedInvoiceCount
|
||||
const segs = [
|
||||
{ label: 'Draft', count: data.draftInvoiceCount, color: 'bg-slate-400' },
|
||||
{ label: 'Printed', count: data.printedInvoiceCount, color: 'bg-amber-600' },
|
||||
{ label: 'Finalized', count: data.finalizedInvoiceCount, color: 'bg-emerald-600' },
|
||||
]
|
||||
case 'invoiceStatus':
|
||||
return (
|
||||
<div>
|
||||
<div className="flex h-2.5 overflow-hidden rounded-full bg-muted">
|
||||
{segs.map(s => (
|
||||
<div key={s.label} className={s.color} style={{ width: total > 0 ? `${(s.count / total) * 100}%` : '0%' }} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3.5 flex flex-col gap-1.5">
|
||||
{segs.map(s => (
|
||||
<div key={s.label} className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<span className={`inline-block h-2 w-2 rounded-full ${s.color}`} />
|
||||
{s.label}
|
||||
</span>
|
||||
<span className="font-semibold text-foreground">{s.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DonutChart
|
||||
data={[
|
||||
{ name: 'Draft', value: data.draftInvoiceCount },
|
||||
{ name: 'Printed', value: data.printedInvoiceCount },
|
||||
{ name: 'Finalized', value: data.finalizedInvoiceCount },
|
||||
]}
|
||||
centerLabel="invoices"
|
||||
emptyText="No invoices yet."
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'salesTrendChart':
|
||||
return <SalesTrendChart data={data.monthlySales} />
|
||||
case 'paymentStatusChart':
|
||||
return <DonutChart data={data.paymentStatusChart} centerLabel="invoices" emptyText="No invoices yet." />
|
||||
case 'poStatusChart':
|
||||
return <DonutChart data={data.poStatusChart} centerLabel="orders" emptyText="No shop POs yet." />
|
||||
case 'loadingSheetsChart':
|
||||
return (
|
||||
<DonutChart
|
||||
data={[
|
||||
{ name: 'Planned', value: data.plannedSheetsCount },
|
||||
{ name: 'Confirmed', value: data.confirmedSheetsCount },
|
||||
]}
|
||||
centerLabel="sheets"
|
||||
emptyText="No loading sheets yet."
|
||||
/>
|
||||
)
|
||||
case 'warehouseChart':
|
||||
return <HorizontalBarChart data={data.warehouseBalances.map(w => ({ name: w.name, value: w.quantity }))} emptyText="No warehouse stock recorded." />
|
||||
case 'movementTypeChart':
|
||||
return <ColumnBarChart data={data.movementTypeChart} label="Movements" emptyText="No recent stock movements." />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -653,7 +675,9 @@ export function DashboardClient() {
|
||||
<div className="text-sm font-semibold text-foreground">{w.title}</div>
|
||||
<div className="mt-1"><CategoryPill cat={w.cat} /></div>
|
||||
</div>
|
||||
<WidgetBody id={w.id} data={data} />
|
||||
<div className="min-h-0 flex-1">
|
||||
<WidgetBody id={w.id} data={data} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
|
||||
Reference in New Issue
Block a user