Add analytics plugin package for tracking and metrics. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
236 lines
7.2 KiB
TypeScript
236 lines
7.2 KiB
TypeScript
import { useState } from 'react'
|
|
|
|
import { useRevenueMetrics, useRevenueTrend, useRevenueBreakdown } from '../hooks/useAdminQuery'
|
|
|
|
interface RevenueMetrics {
|
|
totalRevenue: number;
|
|
monthlyRecurring: number;
|
|
oneTimeRevenue: number;
|
|
cryptoRevenue: number;
|
|
growthRate: number;
|
|
avgRevenuePerUser: number;
|
|
}
|
|
|
|
interface TrendPoint {
|
|
date: string;
|
|
revenue: number;
|
|
recurring: number;
|
|
oneTime: number;
|
|
}
|
|
|
|
interface SourceItem {
|
|
source: string;
|
|
amount: number;
|
|
percentage: number;
|
|
}
|
|
|
|
interface ProviderItem {
|
|
provider: string;
|
|
amount: number;
|
|
percentage: number;
|
|
}
|
|
|
|
interface RevenueBreakdown {
|
|
bySource: SourceItem[];
|
|
byProvider: ProviderItem[];
|
|
}
|
|
|
|
export function RevenuePage() {
|
|
const [dateRange, setDateRange] = useState('30d')
|
|
const [showExportMenu, setShowExportMenu] = useState(false)
|
|
const [compareMode, setCompareMode] = useState(false)
|
|
|
|
const { data: metrics, isLoading: metricsLoading, isError: metricsError } = useRevenueMetrics()
|
|
const { data: trend } = useRevenueTrend()
|
|
const { data: breakdown } = useRevenueBreakdown()
|
|
|
|
if (metricsLoading) {
|
|
return <div data-testid="loading-spinner">Loading revenue data...</div>
|
|
}
|
|
|
|
if (metricsError) {
|
|
return <div data-testid="error-message">Failed to load revenue data</div>
|
|
}
|
|
|
|
const metricsData = metrics as RevenueMetrics
|
|
const trendData = trend as TrendPoint[]
|
|
const breakdownData = breakdown as RevenueBreakdown
|
|
|
|
const formatCurrency = (value: number) => {
|
|
if (value === undefined || value === null) return '$0.00'
|
|
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
}
|
|
|
|
const formatPercent = (value: number) => {
|
|
if (value === undefined || value === null) return '0.0%'
|
|
if (value < 0) return `${value.toFixed(1)}%`
|
|
return `${value.toFixed(1)}%`
|
|
}
|
|
|
|
return (
|
|
<div className="revenue-page">
|
|
<h1 data-testid="page-title">Revenue Analytics</h1>
|
|
|
|
{/* Date Range Filter */}
|
|
<div className="filters" data-testid="date-filter">
|
|
<span>Date Range</span>
|
|
<button
|
|
data-testid="filter-7d"
|
|
className={dateRange === '7d' ? 'active' : ''}
|
|
onClick={() => setDateRange('7d')}
|
|
>
|
|
Last 7 Days
|
|
</button>
|
|
<button
|
|
data-testid="filter-30d"
|
|
className={dateRange === '30d' ? 'active' : ''}
|
|
onClick={() => setDateRange('30d')}
|
|
>
|
|
Last 30 Days
|
|
</button>
|
|
<button
|
|
data-testid="filter-this-month"
|
|
className={dateRange === 'this-month' ? 'active' : ''}
|
|
onClick={() => setDateRange('this-month')}
|
|
>
|
|
This Month
|
|
</button>
|
|
<button
|
|
data-testid="filter-last-month"
|
|
className={dateRange === 'last-month' ? 'active' : ''}
|
|
onClick={() => setDateRange('last-month')}
|
|
>
|
|
Last Month
|
|
</button>
|
|
<button
|
|
data-testid="filter-custom"
|
|
onClick={() => setDateRange('custom')}
|
|
>
|
|
Custom Range
|
|
</button>
|
|
</div>
|
|
|
|
{/* Last Updated */}
|
|
<div className="last-updated">
|
|
<span>Last Updated: Just now</span>
|
|
<button onClick={() => {}}>Refresh</button>
|
|
</div>
|
|
|
|
{/* Compare Period Toggle */}
|
|
<div className="compare-section">
|
|
<button onClick={() => setCompareMode(!compareMode)}>
|
|
Compare Period
|
|
</button>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
<div className="kpi-grid" data-testid="kpi-section">
|
|
<div className="kpi-card" data-testid="total-revenue-card">
|
|
<h3>Total Revenue</h3>
|
|
<p className="value">{formatCurrency(metricsData?.totalRevenue)}</p>
|
|
</div>
|
|
|
|
<div className="kpi-card" data-testid="mrr-card">
|
|
<h3>Monthly Recurring</h3>
|
|
<p className="value">{formatCurrency(metricsData?.monthlyRecurring)}</p>
|
|
</div>
|
|
|
|
<div className="kpi-card" data-testid="one-time-revenue-card">
|
|
<h3>One-Time Revenue</h3>
|
|
<p className="value">{formatCurrency(metricsData?.oneTimeRevenue)}</p>
|
|
</div>
|
|
|
|
<div className="kpi-card" data-testid="crypto-revenue-card">
|
|
<h3>Crypto Revenue</h3>
|
|
<p className="value">{formatCurrency(metricsData?.cryptoRevenue)}</p>
|
|
</div>
|
|
|
|
<div className="kpi-card" data-testid="growth-rate-card">
|
|
<h3>Growth Rate</h3>
|
|
<p className="value">{formatPercent(metricsData?.growthRate)}</p>
|
|
</div>
|
|
|
|
<div className="kpi-card" data-testid="arpu-card">
|
|
<h3>Avg Revenue Per User</h3>
|
|
<p className="value">{formatCurrency(metricsData?.avgRevenuePerUser)}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Revenue Trend Chart */}
|
|
<div className="chart-section" data-testid="revenue-trend-chart">
|
|
<h2>Revenue Trend</h2>
|
|
<div className="chart">
|
|
<div>Recurring</div>
|
|
<div>One-Time</div>
|
|
{trendData?.map((point, idx) => (
|
|
<div key={idx} data-testid={`trend-point-${idx}`}>
|
|
<span>{point.date}</span>
|
|
<span>Recurring: {formatCurrency(point.recurring)}</span>
|
|
<span>One-Time: {formatCurrency(point.oneTime)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Revenue by Source */}
|
|
<div className="breakdown-section" data-testid="revenue-breakdown">
|
|
<h2>Revenue by Source</h2>
|
|
<table data-testid="breakdown-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Source</th>
|
|
<th>Amount</th>
|
|
<th>Percentage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{breakdownData?.bySource?.map((source) => (
|
|
<tr key={source.source} data-testid={`source-${source.source.toLowerCase()}`}>
|
|
<td>{source.source}</td>
|
|
<td>{formatCurrency(source.amount)}</td>
|
|
<td>{formatPercent(source.percentage)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Revenue by Provider */}
|
|
<div className="provider-section" data-testid="provider-breakdown">
|
|
<h2>Revenue by Provider</h2>
|
|
<table data-testid="provider-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Provider</th>
|
|
<th>Amount</th>
|
|
<th>Percentage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{breakdownData?.byProvider?.map((provider) => (
|
|
<tr key={provider.provider} data-testid={`provider-${provider.provider.toLowerCase()}`}>
|
|
<td>{provider.provider}</td>
|
|
<td>{formatCurrency(provider.amount)}</td>
|
|
<td>{formatPercent(provider.percentage)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Export Actions */}
|
|
<div className="actions" data-testid="export-section">
|
|
<button onClick={() => setShowExportMenu(!showExportMenu)}>Export</button>
|
|
{showExportMenu && (
|
|
<div className="export-menu">
|
|
<button data-testid="export-csv">CSV</button>
|
|
<button data-testid="export-excel">Excel</button>
|
|
<button data-testid="export-pdf">PDF</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default RevenuePage
|