feat: add administration page and account role management

- Implemented `AdministrationContent` component for managing account roles.
- Created `AdministrationPage` to serve as the main entry point for administration tasks.
- Added PDF preview functionality with `PdfPreviewFrame` component for invoice generation.
- Introduced `InputColor` component for advanced color selection with various formats.
- Established color conversion utilities in `color-converter.ts` for handling color formats.
- Defined appearance-related schemas and types in `appearance.ts` for consistent theme management.
This commit is contained in:
2026-04-30 10:50:50 -04:00
parent ddc2b42672
commit 0e46fdafb2
87 changed files with 4566 additions and 2425 deletions
@@ -16,6 +16,47 @@ interface InvoiceStatusChartProps {
invoices: Invoice[];
}
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="text-sm">
{data.count} invoice{data.count !== 1 ? "s" : ""}
</p>
<p className="text-sm">{formatChartCurrency(data.value)}</p>
</div>
);
}
return null;
}
export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
// Process invoice data to create status breakdown
const statusData = invoices.reduce(
@@ -44,14 +85,6 @@ export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
name: item.status.charAt(0).toUpperCase() + item.status.slice(1),
}));
// Use theme-aware colors
const COLORS = {
draft: "hsl(0, 0%, 60%)", // neutral grey - matches monthly metrics chart
sent: "hsl(217, 91%, 60%)", // vibrant blue
pending: "hsl(217, 91%, 60%)", // blue
paid: "hsl(142, 71%, 45%)", // vibrant green
overdue: "hsl(var(--destructive))", // red
};
// Animation / motion preferences
const { prefersReducedMotion, animationSpeedMultiplier } =
useAnimationPreferences();
@@ -59,39 +92,6 @@ export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
600 / (animationSpeedMultiplier || 1),
);
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">
@@ -127,11 +127,13 @@ export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[entry.status as keyof typeof COLORS]}
fill={
STATUS_COLORS[entry.status as keyof typeof STATUS_COLORS]
}
/>
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
<Tooltip content={<StatusTooltip />} />
</PieChart>
</ResponsiveContainer>
</div>
@@ -144,7 +146,8 @@ export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
<div
className="h-3 w-3 rounded-full"
style={{
backgroundColor: COLORS[item.status as keyof typeof COLORS],
backgroundColor:
STATUS_COLORS[item.status as keyof typeof STATUS_COLORS],
}}
/>
<span className="text-sm font-medium">{item.name}</span>
@@ -152,7 +155,7 @@ export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
<div className="text-right">
<p className="text-sm font-medium">{item.count}</p>
<p className="text-muted-foreground text-xs">
{formatCurrency(item.value)}
{formatChartCurrency(item.value)}
</p>
</div>
</div>
@@ -24,6 +24,43 @@ interface MonthlyMetricsChartProps {
invoices: Invoice[];
}
function MonthlyMetricsTooltip({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{
payload: {
paidInvoices: number;
pendingInvoices: number;
overdueInvoices: number;
draftInvoices: 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 className="text-primary font-medium">Paid: {data.paidInvoices}</p>
<p className="text-primary/80">Pending: {data.pendingInvoices}</p>
<p className="text-destructive">Overdue: {data.overdueInvoices}</p>
<p className="text-muted-foreground">Draft: {data.draftInvoices}</p>
<p className="text-foreground border-t pt-1 font-medium">
Total: {data.totalInvoices}
</p>
</div>
</div>
);
}
return null;
}
export function MonthlyMetricsChart({ invoices }: MonthlyMetricsChartProps) {
// Process invoice data to create monthly metrics
const monthlyData = invoices.reduce(
@@ -95,49 +132,6 @@ export function MonthlyMetricsChart({ invoices }: MonthlyMetricsChartProps) {
500 / (animationSpeedMultiplier || 1),
);
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{
payload: {
paidInvoices: number;
pendingInvoices: number;
overdueInvoices: number;
draftInvoices: 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 className="text-primary font-medium">Paid: {data.paidInvoices}</p>
<p className="text-primary/80">
Pending: {data.pendingInvoices}
</p>
<p className="text-destructive">
Overdue: {data.overdueInvoices}
</p>
<p className="text-muted-foreground">
Draft: {data.draftInvoices}
</p>
<p className="text-foreground font-medium 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">
@@ -169,7 +163,7 @@ export function MonthlyMetricsChart({ invoices }: MonthlyMetricsChartProps) {
tickLine={false}
tick={{ fontSize: 12, fill: "var(--muted-foreground)" }}
/>
<Tooltip content={<CustomTooltip />} />
<Tooltip content={<MonthlyMetricsTooltip />} />
<Bar
dataKey="draftInvoices"
stackId="a"
@@ -235,9 +229,7 @@ export function MonthlyMetricsChart({ invoices }: MonthlyMetricsChartProps) {
<span className="text-xs">Pending</span>
</div>
<div className="flex items-center space-x-2">
<div
className="h-3 w-3 rounded-full bg-destructive"
/>
<div className="bg-destructive h-3 w-3 rounded-full" />
<span className="text-xs">Overdue</span>
</div>
</div>
@@ -10,8 +10,6 @@ import {
} from "recharts";
import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
interface RevenueChartProps {
data: {
month: string;
@@ -91,7 +89,11 @@ export function RevenueChart({ data }: RevenueChartProps) {
<AreaChart data={chartData}>
<defs>
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="hsl(217, 91%, 60%)" stopOpacity={0.4} />
<stop
offset="5%"
stopColor="hsl(217, 91%, 60%)"
stopOpacity={0.4}
/>
<stop
offset="95%"
stopColor="hsl(217, 91%, 60%)"
@@ -229,7 +229,7 @@ export function StatusManager({
{/* Overdue Warning */}
{isOverdue && (
<div className="bg-destructive/10 text-destructive flex items-center gap-2 p-3">
<div className="bg-destructive/10 text-destructive flex items-center gap-2 p-3">
<AlertCircle className="h-4 w-4" />
<span className="text-sm font-medium">
{daysPastDue} day{daysPastDue !== 1 ? "s" : ""} overdue
@@ -325,7 +325,7 @@ export function StatusManager({
{/* No Email Warning */}
{!clientEmail && effectiveStatus !== "paid" && (
<div className="bg-muted text-muted-foreground p-3">
<div className="bg-muted text-muted-foreground p-3">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
<span className="text-sm font-medium">