first commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"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 }
|
||||
Reference in New Issue
Block a user