f15b035b88
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>
82 lines
3.5 KiB
TypeScript
82 lines
3.5 KiB
TypeScript
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>
|
|
)
|
|
}
|