From d3792d19eb73ce0c70e7e6e813ac1513d07bf398 Mon Sep 17 00:00:00 2001 From: Forge Date: Wed, 25 Mar 2026 08:41:24 +0800 Subject: [PATCH] feat: Tickets + Reports pages with real data and charts --- app/(app)/reports/page.tsx | 271 ++++++++++++++++++++----------------- 1 file changed, 148 insertions(+), 123 deletions(-) diff --git a/app/(app)/reports/page.tsx b/app/(app)/reports/page.tsx index abb8f18..f416ecd 100644 --- a/app/(app)/reports/page.tsx +++ b/app/(app)/reports/page.tsx @@ -1,168 +1,193 @@ 'use client'; +import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; +import { api } from '@/lib/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; -import { api } from '@/lib/api'; -import { - BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, - PieChart, Pie, Cell, Legend, -} from 'recharts'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts'; -const PIE_COLORS = ['#0891B2', '#059669', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']; +const peso = (v: number) => '₱' + (v ?? 0).toLocaleString('en-PH', { minimumFractionDigits: 2 }); export default function ReportsPage() { - const { data: collection, isLoading: loadingCollection } = useQuery({ - queryKey: ['reports-collection'], - queryFn: async () => (await api.get('/api/v1/reports/collection')).data, + const [from, setFrom] = useState(() => { + const d = new Date(); + d.setDate(1); + return d.toISOString().split('T')[0]; + }); + const [to, setTo] = useState(() => new Date().toISOString().split('T')[0]); + + const { data: collection, isLoading: collLoading } = useQuery({ + queryKey: ['reports', 'collection', from, to], + queryFn: async () => { + const res = await api.get(`/api/v1/reports/collection?from=${from}&to=${to}`); + return res.data as Array<{ collector: string; totalAmount: number; paymentCount: number }>; + }, }); - const { data: aging, isLoading: loadingAging } = useQuery({ - queryKey: ['reports-aging'], - queryFn: async () => (await api.get('/api/v1/reports/aging')).data, + const { data: aging, isLoading: agingLoading } = useQuery({ + queryKey: ['reports', 'aging'], + queryFn: async () => { + const res = await api.get('/api/v1/reports/aging'); + return res.data as Array<{ bucket: string; invoiceCount: number; totalAmount: number }>; + }, }); - const { data: subscribers, isLoading: loadingSubs } = useQuery({ - queryKey: ['reports-subscribers'], - queryFn: async () => (await api.get('/api/v1/reports/subscribers')).data, + const { data: subscribers, isLoading: subLoading } = useQuery({ + queryKey: ['reports', 'subscribers'], + queryFn: async () => { + const res = await api.get('/api/v1/reports/subscribers'); + return res.data as Array<{ plan: string; count: number; revenue: number }>; + }, }); - const { data: revenue, isLoading: loadingRevenue } = useQuery({ - queryKey: ['reports-revenue'], - queryFn: async () => (await api.get('/api/v1/reports/revenue')).data, + const { data: revenue, isLoading: revLoading } = useQuery({ + queryKey: ['reports', 'revenue'], + queryFn: async () => { + const res = await api.get('/api/v1/reports/revenue'); + return res.data as Array<{ month: string; total: number }>; + }, }); - const { data: plans, isLoading: loadingPlans } = useQuery({ - queryKey: ['reports-plans'], - queryFn: async () => (await api.get('/api/v1/reports/plans')).data, - }); - - const collectionList = Array.isArray(collection) ? collection : []; - const agingList = Array.isArray(aging) ? aging : []; - const revList = Array.isArray(revenue) ? revenue : []; - const planList = Array.isArray(plans) ? plans : []; - - // Subscribers summary - const subSummary = subscribers ? [ - { name: 'Active', value: (subscribers as any).active ?? 0 }, - { name: 'Pending', value: (subscribers as any).pending ?? 0 }, - { name: 'Suspended', value: (subscribers as any).suspended ?? 0 }, - { name: 'Disconnected', value: (subscribers as any).disconnected ?? 0 }, - ].filter(s => s.value > 0) : []; - return (
-

Reports & Analytics

-

Business performance overview

+

Reports

+

Financial and operational analytics

-
+ {/* Collection Report */} + + + Collection Report +
+ setFrom(e.target.value)} + className="border rounded-md px-2 py-1 text-sm text-slate-700" /> + to + setTo(e.target.value)} + className="border rounded-md px-2 py-1 text-sm text-slate-700" /> +
+
+ + {collLoading ? ( + + ) : !collection?.length ? ( +

No collections in this period

+ ) : ( + + + + + + + + + + {collection?.map((row, i) => ( + + + + + + ))} + + + + + + +
CollectorPaymentsTotal
{row.collector}{row.paymentCount}{peso(row.totalAmount)}
Total + {collection?.reduce((s, r) => s + r.paymentCount, 0)} + + {peso(collection?.reduce((s, r) => s + r.totalAmount, 0) ?? 0)} +
+ )} +
+
+
{/* Revenue Trend */} - - Monthly Revenue (₱) + + + Revenue Trend (12 months) + - {loadingRevenue ? : ( + {revLoading ? : ( - - - - `₱${Number(v).toLocaleString()}`} /> - [`₱${Number(v).toLocaleString()}`, 'Revenue']} /> - + + + `₱${(v/1000).toFixed(0)}k`} /> + peso(v)} /> + )} - {/* Subscriber Status */} - - Subscriber Status - - {loadingSubs ? : subSummary.length === 0 ? ( -

No data

- ) : ( - - - `${name}: ${value}`}> - {subSummary.map((_, i) => )} - - - - - )} -
-
- {/* Aging */} - - Invoice Aging (₱) + + + Accounts Receivable Aging + - {loadingAging ? : agingList.length === 0 ? ( -

No outstanding invoices

- ) : ( - - - - - - [`₱${Number(v).toLocaleString()}`, 'Amount']} /> - - - - )} -
-
- - {/* Collection by Collector */} - - Collection by Collector - - {loadingCollection ? : collectionList.length === 0 ? ( -

No collection data

- ) : ( + {agingLoading ? : (
- {collectionList.map((c: any, i: number) => ( -
-
-

{c.collector}

-

{c.paymentCount} payments

+ {aging?.map((bucket) => ( +
+ {bucket.bucket}d +
+
b.totalAmount) ?? [1])) || 1)) * 100)}%`, + backgroundColor: bucket.bucket === '90+' ? '#DC2626' : bucket.bucket === '61-90' ? '#D97706' : '#0891B2', + }} + />
-

₱{Number(c.totalAmount).toLocaleString()}

+ {peso(bucket.totalAmount)} + {bucket.invoiceCount} inv
))} + {!aging?.some(b => b.totalAmount > 0) && ( +

No overdue invoices 🎉

+ )}
)} - - {/* Plans Distribution */} - - Subscribers by Plan - - {loadingPlans ? : planList.length === 0 ? ( -

No data

- ) : ( -
- {planList.map((p: any, i: number) => ( -
-

{p.planName ?? p.name ?? 'Plan ' + (i+1)}

-
-
-
x.count || 1))) * 100)}%` }} /> -
-

{p.count}

-
-
- ))} -
- )} - - -
+ + {/* Subscribers by Plan */} + + + Subscribers by Plan + + + {subLoading ? : ( + + + + + + + + + + {(subscribers as any[])?.map((row: any, i: number) => ( + + + + + + ))} + +
PlanSubscribersMonthly Revenue
{row.plan ?? row.name ?? '—'}{row.count ?? row.subscribers ?? 0} + {peso(row.revenue ?? row.monthlyRevenue ?? 0)} +
+ )} +
+
); }