Files
beenvoice-web/src/app/dashboard/_components/invoice-status-chart.tsx
T
soconnorandCursor c53f2e6c4d Unify the dashboard experience and retire the multi-theme engine so onboarding and day-to-day invoicing feel consistent and easier to maintain.
Shared layout, tabs, and sidebar timer; user onboarding and registration polish; settings danger zone and data export; chart and tRPC perf fixes; migrations for onboarding and dropped appearance columns.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 03:08:22 -04:00

139 lines
3.9 KiB
TypeScript

"use client";
import { Cell, Pie, PieChart, Tooltip } from "recharts";
import { ResponsiveChart } from "~/components/charts/responsive-chart";
import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
export interface StatusChartDatum {
status: string;
name: string;
count: number;
value: number;
}
interface InvoiceStatusChartProps {
data: StatusChartDatum[];
}
const STATUS_COLORS = {
draft: "hsl(0, 0%, 60%)",
sent: "hsl(217, 91%, 60%)",
pending: "hsl(217, 91%, 60%)",
paid: "hsl(142, 71%, 45%)",
overdue: "hsl(var(--destructive))",
} as const;
const formatChartCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
function StatusTooltip({
active,
payload,
}: {
active?: boolean;
payload?: Array<{
payload: { name: string; count: number; value: number };
}>;
}) {
if (active && payload?.length) {
const data = payload[0]!.payload;
return (
<div className="bg-card border-border rounded-lg border p-3 shadow-lg">
<p className="font-medium">{data.name}</p>
<p className="font-mono text-sm tabular-nums">
{data.count} invoice{data.count !== 1 ? "s" : ""}
</p>
<p className="font-mono text-sm tabular-nums">
{formatChartCurrency(data.value)}
</p>
</div>
);
}
return null;
}
export function InvoiceStatusChart({ data }: InvoiceStatusChartProps) {
const { prefersReducedMotion, animationSpeedMultiplier } =
useAnimationPreferences();
const pieAnimationDuration = Math.round(
600 / (animationSpeedMultiplier || 1),
);
if (data.length === 0) {
return (
<div className="flex h-64 items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground text-sm">
No invoice data available
</p>
<p className="text-muted-foreground text-xs">
Status breakdown will appear here once you create invoices
</p>
</div>
</div>
);
}
return (
<div className="space-y-4">
<ResponsiveChart height={192} className="h-48">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={40}
outerRadius={80}
stroke="none"
dataKey="count"
isAnimationActive={!prefersReducedMotion}
animationDuration={pieAnimationDuration}
animationEasing="ease-out"
>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={
STATUS_COLORS[entry.status as keyof typeof STATUS_COLORS]
}
/>
))}
</Pie>
<Tooltip content={<StatusTooltip />} />
</PieChart>
</ResponsiveChart>
<div className="space-y-2">
{data.map((item) => (
<div key={item.status} className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div
className="h-3 w-3 rounded-full"
style={{
backgroundColor:
STATUS_COLORS[item.status as keyof typeof STATUS_COLORS],
}}
/>
<span className="text-sm font-medium">{item.name}</span>
</div>
<div className="text-right">
<p className="font-mono text-sm font-medium tabular-nums">
{item.count}
</p>
<p className="text-muted-foreground font-mono text-xs tabular-nums">
{formatChartCurrency(item.value)}
</p>
</div>
</div>
))}
</div>
</div>
);
}