feat(L1): modular widget dashboard — 15 configurable widgets
Replaces monolithic AdminDashboard/ClientDashboard with a per-user configurable widget grid. 15 widget types: ProductionStats, QueueStatus, RecentRenders, CostOverview, WorkerStatus, KPISummary, OrderThroughput, Revenue, ItemStatus, ProcessingTimes, RenderTimeByOutputType, OutputTypeUsage, TopProducts, OrdersByUser, RenderBackendStats. DashboardTimeframeContext provides shared timeframe state. Dashboard config persisted in DB via GET/PUT /api/dashboard/config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
ResponsiveContainer, BarChart, Bar, Cell,
|
||||
XAxis, YAxis, CartesianGrid, Tooltip,
|
||||
} from 'recharts'
|
||||
import { getDashboardKPIs } from '../../../api/analytics'
|
||||
import { useDashboardTimeframe } from '../DashboardTimeframeContext'
|
||||
|
||||
const TOOLTIP_STYLE = {
|
||||
backgroundColor: 'var(--color-bg-surface)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '8px',
|
||||
color: 'var(--color-text)',
|
||||
}
|
||||
|
||||
const CATEGORY_COLORS = ['#00893d', '#6366f1', '#f59e0b', '#3b82f6', '#8b5cf6', '#14b8a6', '#f43f5e', '#06b6d4']
|
||||
|
||||
export default function RevenueChartWidget() {
|
||||
const { dateFrom, dateTo } = useDashboardTimeframe()
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['analytics-kpis', dateFrom, dateTo],
|
||||
queryFn: () => getDashboardKPIs(dateFrom, dateTo),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="space-y-3"><div className="h-40 animate-pulse rounded-lg bg-surface-muted" /><div className="h-40 animate-pulse rounded-lg bg-surface-muted" /></div>
|
||||
if (error) return <p className="text-xs text-red-500">Failed to load revenue</p>
|
||||
if (!data) return null
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-content-secondary mb-2">Revenue per Month (€)</p>
|
||||
{data.revenue.length === 0 ? (
|
||||
<p className="text-xs text-content-muted text-center py-4">No data yet</p>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<BarChart data={data.revenue} margin={{ top: 4, right: 8, left: -16, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
|
||||
<XAxis dataKey="month" tick={{ fill: 'var(--color-text-muted)', fontSize: 10 }} />
|
||||
<YAxis tick={{ fill: 'var(--color-text-muted)', fontSize: 10 }} />
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
formatter={(v: number | undefined) =>
|
||||
v != null ? [`€ ${v.toFixed(2)}`, 'Revenue'] : ['—', 'Revenue']
|
||||
}
|
||||
/>
|
||||
<Bar dataKey="revenue" fill="#00893d" radius={[3, 3, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium text-content-secondary mb-2">Revenue by Category (€)</p>
|
||||
{data.category_revenue.length === 0 ? (
|
||||
<p className="text-xs text-content-muted text-center py-4">No data yet</p>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<BarChart data={data.category_revenue} margin={{ top: 4, right: 8, left: -16, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
|
||||
<XAxis dataKey="category" tick={{ fill: 'var(--color-text-muted)', fontSize: 10 }} />
|
||||
<YAxis tick={{ fill: 'var(--color-text-muted)', fontSize: 10 }} />
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
formatter={(v: number | undefined) =>
|
||||
v != null ? [`€ ${v.toFixed(2)}`, 'Revenue'] : ['—', 'Revenue']
|
||||
}
|
||||
/>
|
||||
<Bar dataKey="revenue" radius={[3, 3, 0, 0]}>
|
||||
{data.category_revenue.map((_, i) => (
|
||||
<Cell key={i} fill={CATEGORY_COLORS[i % CATEGORY_COLORS.length]} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user