144 lines
5.3 KiB
TypeScript
144 lines
5.3 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { BanknoteIcon, TrendingUpIcon, UsersIcon } from "lucide-react"
|
|
|
|
import { getAttendanceCounts } from "@/lib/api/attendance"
|
|
import { getMemberInvoices } from "@/lib/api/invoices"
|
|
import { getMonthlyRevenue, getStatSummary } from "@/lib/api/stats"
|
|
import { formatCurrency } from "@/lib/format"
|
|
import {
|
|
DEMO_ATTENDANCE_COUNTS,
|
|
DEMO_REVENUE,
|
|
DEMO_STAT_SUMMARY,
|
|
DEMO_SUBSCRIPTIONS,
|
|
} from "@/lib/demo/mock-data"
|
|
import { useFetch } from "@/lib/use-fetch"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import { AttendanceChartCard } from "@/components/hexa/attendance-chart-card"
|
|
import { PageHeader } from "@/components/hexa/page-header"
|
|
import { RevenueChartCard } from "@/components/hexa/revenue-chart-card"
|
|
import { StatCard } from "@/components/hexa/stat-card"
|
|
import { ProfitLossView } from "@/components/hexa/views/profit-loss-view"
|
|
|
|
interface ReportsViewProps {
|
|
/** Sandbox demo mode: local in-memory data, no API calls. */
|
|
demo?: boolean
|
|
}
|
|
|
|
function ReportsView({ demo = false }: ReportsViewProps) {
|
|
const summary = useFetch(demo ? async () => DEMO_STAT_SUMMARY : getStatSummary)
|
|
const revenue = useFetch(demo ? async () => DEMO_REVENUE : getMonthlyRevenue)
|
|
const counts = useFetch(demo ? async () => DEMO_ATTENDANCE_COUNTS : () => getAttendanceCounts())
|
|
const invoices = useFetch(demo ? async () => [] : getMemberInvoices)
|
|
|
|
const paidTotal = demo
|
|
? DEMO_SUBSCRIPTIONS.filter((s) => s.paymentStatus === "Paid").reduce((sum, s) => sum + s.paidAmount, 0)
|
|
: (invoices.data ?? [])
|
|
.filter((i) => i.status.toLowerCase() === "paid")
|
|
.reduce((sum, i) => sum + i.amount, 0)
|
|
const pendingTotal = demo
|
|
? DEMO_SUBSCRIPTIONS.filter((s) => s.paymentStatus !== "Paid").reduce((sum, s) => sum + s.balance, 0)
|
|
: (invoices.data ?? [])
|
|
.filter((i) => i.status.toLowerCase() === "pending")
|
|
.reduce((sum, i) => sum + i.amount, 0)
|
|
|
|
const growthHint = (pct: number) => `${pct >= 0 ? "+" : ""}${pct.toFixed(1)}% vs last month`
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<PageHeader title="Reports" description="Revenue, attendance, and member trends" />
|
|
|
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
<StatCard
|
|
icon={BanknoteIcon}
|
|
tone="green"
|
|
label="Total Revenue"
|
|
value={formatCurrency(summary.data?.totalRevenue ?? 0)}
|
|
hint="All time"
|
|
/>
|
|
<StatCard
|
|
icon={TrendingUpIcon}
|
|
tone="blue"
|
|
label="This Month"
|
|
value={formatCurrency(summary.data?.thisMonthRevenue ?? 0)}
|
|
hint={growthHint(summary.data?.revenueGrowthPercentage ?? 0)}
|
|
/>
|
|
<StatCard
|
|
icon={UsersIcon}
|
|
label="Total Members"
|
|
value={summary.data?.totalCustomerCount ?? 0}
|
|
hint={growthHint(summary.data?.customerGrowthPercentage ?? 0)}
|
|
/>
|
|
<StatCard
|
|
icon={BanknoteIcon}
|
|
tone="amber"
|
|
label="Outstanding"
|
|
value={formatCurrency(pendingTotal)}
|
|
hint={`${formatCurrency(paidTotal)} collected`}
|
|
/>
|
|
</div>
|
|
|
|
<Tabs defaultValue="revenue">
|
|
<TabsList className="w-full sm:w-auto">
|
|
<TabsTrigger value="revenue" className="flex-1 sm:flex-none">Revenue</TabsTrigger>
|
|
<TabsTrigger value="attendance" className="flex-1 sm:flex-none">Attendance</TabsTrigger>
|
|
<TabsTrigger value="members" className="flex-1 sm:flex-none">Members</TabsTrigger>
|
|
<TabsTrigger value="profit-loss" className="flex-1 sm:flex-none">Profit & Loss</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="revenue" className="mt-3">
|
|
<RevenueChartCard data={revenue.data} loading={demo ? false : revenue.loading} title="Monthly revenue (12 months)" />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="attendance" className="mt-3">
|
|
<AttendanceChartCard
|
|
data={counts.data}
|
|
loading={demo ? false : counts.loading}
|
|
title="Most active members (this month)"
|
|
limit={10}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="members" className="mt-3">
|
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
<StatCard
|
|
icon={UsersIcon}
|
|
label="Total"
|
|
value={summary.data?.totalCustomerCount ?? 0}
|
|
hint="All members"
|
|
/>
|
|
<StatCard
|
|
icon={UsersIcon}
|
|
tone="blue"
|
|
label="This Month"
|
|
value={summary.data?.thisMonthCustomerCount ?? 0}
|
|
hint="New joiners"
|
|
/>
|
|
<StatCard
|
|
icon={UsersIcon}
|
|
tone="amber"
|
|
label="Last Month"
|
|
value={summary.data?.lastMonthCustomerCount ?? 0}
|
|
hint="Previous period"
|
|
/>
|
|
<StatCard
|
|
icon={TrendingUpIcon}
|
|
tone="green"
|
|
label="Growth"
|
|
value={`${(summary.data?.customerGrowthPercentage ?? 0).toFixed(1)}%`}
|
|
hint="Month over month"
|
|
/>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="profit-loss" className="mt-3">
|
|
<ProfitLossView demo={demo} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { ReportsView }
|