mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 17:48:55 -04:00
Theme overhaul - missing files
This commit is contained in:
@@ -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} />;
|
||||
}
|
||||
Reference in New Issue
Block a user