Theme overhaul - missing files

This commit is contained in:
2025-07-31 18:37:45 -04:00
parent 8a2565adad
commit 2a4f78a762
17 changed files with 2953 additions and 0 deletions
@@ -0,0 +1,152 @@
"use client";
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
import type { StoredInvoiceStatus } from "~/types/invoice";
interface Invoice {
id: string;
totalAmount: number;
status: string;
dueDate: Date | string;
}
interface InvoiceStatusChartProps {
invoices: Invoice[];
}
export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
// Process invoice data to create status breakdown
const statusData = invoices.reduce(
(acc, invoice) => {
const effectiveStatus = getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
acc[effectiveStatus] ??= {
status: effectiveStatus,
count: 0,
value: 0,
};
acc[effectiveStatus].count += 1;
acc[effectiveStatus].value += invoice.totalAmount;
return acc;
},
{} as Record<string, { status: string; count: number; value: number }>,
);
const chartData = Object.values(statusData).map((item) => ({
...item,
name: item.status.charAt(0).toUpperCase() + item.status.slice(1),
}));
// Light pastel colors for different statuses
const COLORS = {
draft: "hsl(220 9% 46%)", // muted gray
sent: "hsl(210 40% 70%)", // light blue
paid: "hsl(142 76% 85%)", // light green
overdue: "hsl(0 84% 85%)", // light red
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
const CustomTooltip = ({
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="text-sm">
{data.count} invoice{data.count !== 1 ? "s" : ""}
</p>
<p className="text-sm">{formatCurrency(data.value)}</p>
</div>
);
}
return null;
};
if (chartData.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">
<div className="h-48 w-full">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={40}
outerRadius={80}
paddingAngle={2}
dataKey="count"
>
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[entry.status as keyof typeof COLORS]}
/>
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="space-y-2">
{chartData.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: COLORS[item.status as keyof typeof COLORS],
}}
/>
<span className="text-sm font-medium">{item.name}</span>
</div>
<div className="text-right">
<p className="text-sm font-medium">{item.count}</p>
<p className="text-muted-foreground text-xs">
{formatCurrency(item.value)}
</p>
</div>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,206 @@
"use client";
import {
Bar,
BarChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
import type { StoredInvoiceStatus } from "~/types/invoice";
interface Invoice {
id: string;
totalAmount: number;
issueDate: Date | string;
status: string;
dueDate: Date | string;
}
interface MonthlyMetricsChartProps {
invoices: Invoice[];
}
export function MonthlyMetricsChart({ invoices }: MonthlyMetricsChartProps) {
// Process invoice data to create monthly metrics
const monthlyData = invoices.reduce(
(acc, invoice) => {
const date = new Date(invoice.issueDate);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
const effectiveStatus = getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
acc[monthKey] ??= {
month: monthKey,
totalInvoices: 0,
paidInvoices: 0,
pendingInvoices: 0,
overdueInvoices: 0,
};
acc[monthKey].totalInvoices += 1;
switch (effectiveStatus) {
case "paid":
acc[monthKey].paidInvoices += 1;
break;
case "sent":
acc[monthKey].pendingInvoices += 1;
break;
case "overdue":
acc[monthKey].overdueInvoices += 1;
break;
}
return acc;
},
{} as Record<
string,
{
month: string;
totalInvoices: number;
paidInvoices: number;
pendingInvoices: number;
overdueInvoices: number;
}
>,
);
// Convert to array and sort by month
const chartData = Object.values(monthlyData)
.sort((a, b) => a.month.localeCompare(b.month))
.slice(-6) // Show last 6 months
.map((item) => ({
...item,
monthLabel: new Date(item.month + "-01").toLocaleDateString("en-US", {
month: "short",
year: "2-digit",
}),
}));
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{
payload: {
paidInvoices: number;
pendingInvoices: number;
overdueInvoices: number;
totalInvoices: number;
};
}>;
label?: string;
}) => {
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">{label}</p>
<div className="space-y-1 text-sm">
<p style={{ color: "hsl(142 45% 45%)" }}>
Paid: {data.paidInvoices}
</p>
<p style={{ color: "hsl(210 40% 50%)" }}>
Pending: {data.pendingInvoices}
</p>
<p style={{ color: "hsl(0 65% 55%)" }}>
Overdue: {data.overdueInvoices}
</p>
<p className="text-muted-foreground border-t pt-1">
Total: {data.totalInvoices}
</p>
</div>
</div>
);
}
return null;
};
if (chartData.length === 0) {
return (
<div className="flex h-64 items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground text-sm">
No metrics data available
</p>
<p className="text-muted-foreground text-xs">
Monthly metrics will appear here once you create invoices
</p>
</div>
</div>
);
}
return (
<div className="space-y-4">
<div className="h-48 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData}>
<XAxis
dataKey="monthLabel"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey="paidInvoices"
stackId="a"
fill="hsl(142 76% 85%)"
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="pendingInvoices"
stackId="a"
fill="hsl(210 40% 85%)"
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="overdueInvoices"
stackId="a"
fill="hsl(0 84% 85%)"
radius={[2, 2, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="flex justify-center space-x-4">
<div className="flex items-center space-x-2">
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: "hsl(142 76% 85%)" }}
/>
<span className="text-xs">Paid</span>
</div>
<div className="flex items-center space-x-2">
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: "hsl(210 40% 85%)" }}
/>
<span className="text-xs">Pending</span>
</div>
<div className="flex items-center space-x-2">
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: "hsl(0 84% 85%)" }}
/>
<span className="text-xs">Overdue</span>
</div>
</div>
</div>
);
}
@@ -0,0 +1,159 @@
"use client";
import {
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
import type { StoredInvoiceStatus } from "~/types/invoice";
interface Invoice {
id: string;
totalAmount: number;
issueDate: Date | string;
status: string;
dueDate: Date | string;
}
interface RevenueChartProps {
invoices: Invoice[];
}
export function RevenueChart({ invoices }: RevenueChartProps) {
// Process invoice data to create monthly revenue data
const monthlyData = invoices
.filter(
(invoice) =>
getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
) === "paid",
)
.reduce(
(acc, invoice) => {
const date = new Date(invoice.issueDate);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
acc[monthKey] ??= {
month: monthKey,
revenue: 0,
count: 0,
};
acc[monthKey].revenue += invoice.totalAmount;
acc[monthKey].count += 1;
return acc;
},
{} as Record<string, { month: string; revenue: number; count: number }>,
);
// Convert to array and sort by month
const chartData = Object.values(monthlyData)
.sort((a, b) => a.month.localeCompare(b.month))
.slice(-6) // Show last 6 months
.map((item) => ({
...item,
monthLabel: new Date(item.month + "-01").toLocaleDateString("en-US", {
month: "short",
year: "2-digit",
}),
}));
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{ payload: { revenue: number; count: number } }>;
label?: string;
}) => {
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">{label}</p>
<p style={{ color: "hsl(210 40% 50%)" }}>
Revenue: {formatCurrency(data.revenue)}
</p>
<p className="text-muted-foreground text-sm">
{data.count} invoice{data.count !== 1 ? "s" : ""}
</p>
</div>
);
}
return null;
};
if (chartData.length === 0) {
return (
<div className="flex h-64 items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground text-sm">
No revenue data available
</p>
<p className="text-muted-foreground text-xs">
Revenue will appear here once you have paid invoices
</p>
</div>
</div>
);
}
return (
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<defs>
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(210 40% 70%)"
stopOpacity={0.4}
/>
<stop
offset="95%"
stopColor="hsl(210 40% 70%)"
stopOpacity={0.05}
/>
</linearGradient>
</defs>
<XAxis
dataKey="monthLabel"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
tickFormatter={formatCurrency}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="revenue"
stroke="hsl(210 40% 60%)"
strokeWidth={2}
fill="url(#revenueGradient)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}
@@ -0,0 +1,12 @@
"use client";
import { useParams } from "next/navigation";
import InvoiceForm from "~/components/forms/invoice-form";
export default function InvoiceFormPage() {
const params = useParams();
const id = params.id as string;
// Pass the actual id, let the form component handle the logic
return <InvoiceForm invoiceId={id} />;
}