diff --git a/src/app/admin/accounts/reports/client.tsx b/src/app/admin/accounts/reports/client.tsx
index c36c5f0..a6b4fdc 100644
--- a/src/app/admin/accounts/reports/client.tsx
+++ b/src/app/admin/accounts/reports/client.tsx
@@ -3,6 +3,7 @@
import { useState, useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import Price from '@/components/Price';
+import { generateLedgerCSV, downloadCSV, penceToPounds } from '@/lib/csvExport';
interface ReportData {
orders: Array<{ id: string; total: number; createdAt: Date }>;
@@ -14,6 +15,8 @@ interface ReportData {
export default function ReportsPageClient({ data }: { data: ReportData }) {
const searchParams = useSearchParams();
const [tab, setTab] = useState(searchParams.get('tab') ?? 'monthly-profit');
+ const [fromDate, setFromDate] = useState('');
+ const [toDate, setToDate] = useState('');
// Ensure dates are parsed as Date objects (they might be strings from server)
const normalizedData = {
@@ -127,10 +130,103 @@ export default function ReportsPageClient({ data }: { data: ReportData }) {
};
}, [normalizedData]);
+ const handleExportLedger = () => {
+ const from = fromDate ? new Date(fromDate) : null;
+ const to = toDate ? new Date(`${toDate}T23:59:59`) : null;
+
+ const ledgerRows = [
+ ...normalizedData.orders
+ .filter((o) => (!from || o.createdAt >= from) && (!to || o.createdAt <= to))
+ .map((o) => ({
+ date: o.createdAt,
+ type: 'INCOME' as const,
+ description: 'Website order',
+ amount: o.total,
+ category: 'Sales',
+ })),
+ ...normalizedData.manualSales
+ .filter((s) => (!from || s.soldAt >= from) && (!to || s.soldAt <= to))
+ .map((s) => ({
+ date: s.soldAt,
+ type: 'INCOME' as const,
+ description: 'Manual sale',
+ amount: s.total,
+ category: 'Sales',
+ })),
+ ...normalizedData.purchases
+ .filter((p) => (!from || p.purchasedAt >= from) && (!to || p.purchasedAt <= to))
+ .map((p) => ({
+ date: p.purchasedAt,
+ type: 'EXPENSE' as const,
+ description: 'Stock purchase',
+ amount: p.total,
+ category: 'Cost of goods',
+ })),
+ ...normalizedData.expenses
+ .filter((e) => (!from || e.date >= from) && (!to || e.date <= to))
+ .map((e) => ({
+ date: e.date,
+ type: 'EXPENSE' as const,
+ description: e.category,
+ amount: e.amount,
+ category: e.category,
+ })),
+ ];
+
+ const dateRange = fromDate || toDate
+ ? ` (${fromDate || 'start'} to ${toDate || 'today'})`
+ : '';
+ const csv = generateLedgerCSV(ledgerRows, `Ledger Export${dateRange}`);
+ downloadCSV(csv, `ledger-${new Date().toISOString().split('T')[0]}.csv`);
+ };
+
return (
<>
+ {/* Date Filtering */}
+
+
{[
+ { id: 'trends', label: 'Trends' },
{ id: 'monthly-profit', label: 'Monthly Profit' },
{ id: 'expense-breakdown', label: 'Expenses' },
{ id: 'cash-flow', label: 'Cash Flow' },
@@ -147,6 +243,88 @@ export default function ReportsPageClient({ data }: { data: ReportData }) {
+ {tab === 'trends' && (
+
+
+
Monthly Profit Trend
+
+
+
+
+
+
+
+
Avg Monthly Profit
+
+ sum + m.profit, 0) / Math.max(monthlyProfit.length, 1))} />
+
+
+
+
Best Month
+
+ m.profit), 0)} />
+
+
+
+
Worst Month
+
+ m.profit), 0)} />
+
+
+
+
+ )}
+
{tab === 'monthly-profit' && (
diff --git a/src/lib/csvExport.ts b/src/lib/csvExport.ts
new file mode 100644
index 0000000..b0c15e8
--- /dev/null
+++ b/src/lib/csvExport.ts
@@ -0,0 +1,122 @@
+/**
+ * Export financial data as CSV for accountants
+ */
+
+interface LedgerRow {
+ date: Date;
+ type: 'INCOME' | 'EXPENSE';
+ description: string;
+ amount: number; // pence
+ category?: string;
+}
+
+/**
+ * Convert pence to pounds string (e.g., 5000 -> "50.00")
+ */
+export function penceToPounds(pence: number): string {
+ return (pence / 100).toFixed(2);
+}
+
+/**
+ * Generate CSV from ledger data
+ */
+export function generateLedgerCSV(rows: LedgerRow[], title: string): string {
+ const lines: string[] = [];
+
+ // Header
+ lines.push(`${title}`);
+ lines.push(`Generated: ${new Date().toISOString().split('T')[0]}`);
+ lines.push('');
+
+ // Column headers
+ lines.push('Date,Type,Description,Category,Amount (£)');
+
+ // Data rows
+ const sortedRows = [...rows].sort((a, b) => b.date.getTime() - a.date.getTime());
+ for (const row of sortedRows) {
+ const dateStr = row.date.toLocaleDateString('en-GB', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ });
+ const amountStr = penceToPounds(row.amount);
+ const category = row.category || '';
+
+ // Escape quotes in description
+ const description = `"${row.description.replace(/"/g, '""')}"`;
+
+ lines.push(`${dateStr},${row.type},${description},${category},${amountStr}`);
+ }
+
+ // Summary
+ lines.push('');
+ const totalIncome = rows
+ .filter((r) => r.type === 'INCOME')
+ .reduce((sum, r) => sum + r.amount, 0);
+ const totalExpense = rows
+ .filter((r) => r.type === 'EXPENSE')
+ .reduce((sum, r) => sum + r.amount, 0);
+ const profit = totalIncome - totalExpense;
+
+ lines.push(`TOTAL INCOME,,,${penceToPounds(totalIncome)}`);
+ lines.push(`TOTAL EXPENSES,,,${penceToPounds(totalExpense)}`);
+ lines.push(`NET PROFIT,,,${penceToPounds(profit)}`);
+
+ return lines.join('\n');
+}
+
+/**
+ * Generate CSV of audit changes for compliance
+ */
+export interface AuditRow {
+ date: Date;
+ action: string;
+ entity: string;
+ id: string;
+ amount?: number;
+ changedBy: string;
+ reason?: string;
+}
+
+export function generateAuditCSV(rows: AuditRow[]): string {
+ const lines: string[] = [];
+
+ lines.push('Financial Audit Trail');
+ lines.push(`Generated: ${new Date().toISOString().split('T')[0]}`);
+ lines.push('');
+
+ lines.push('Date,Time,Action,Entity Type,Entity ID,Amount (£),Changed By,Reason');
+
+ const sortedRows = [...rows].sort((a, b) => b.date.getTime() - a.date.getTime());
+ for (const row of sortedRows) {
+ const dateStr = row.date.toLocaleDateString('en-GB', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ });
+ const timeStr = row.date.toLocaleTimeString('en-GB', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ });
+ const amountStr = row.amount ? penceToPounds(row.amount) : '';
+ const reason = `"${(row.reason || '').replace(/"/g, '""')}"`;
+
+ lines.push(
+ `${dateStr},${timeStr},${row.action},${row.entity},${row.id},${amountStr},${row.changedBy},${reason}`
+ );
+ }
+
+ return lines.join('\n');
+}
+
+/**
+ * Trigger browser download of CSV file
+ */
+export function downloadCSV(csv: string, filename: string) {
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = filename;
+ link.click();
+}