diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index a02b46d..269061b 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -301,6 +301,105 @@ async def get_admin_earnings( } +# ============== Earnings History ============== + +@router.get("/earnings/history") +async def get_admin_earnings_history( + db: Database, + admin: User = Depends(require_admin), + months: int = 12 +): + """ + Get historical earnings data for charts. + + Calculates MRR for each month based on subscription start dates. + """ + tier_prices = { + SubscriptionTier.SCOUT: 0, + SubscriptionTier.TRADER: 9, + SubscriptionTier.TYCOON: 29, + } + + # Get all subscriptions + result = await db.execute(select(Subscription)) + all_subs = result.scalars().all() + + # Generate monthly data for the last N months + monthly_data = [] + now = datetime.utcnow() + + for i in range(months - 1, -1, -1): + # Calculate the start of each month + month_start = datetime(now.year, now.month, 1) - timedelta(days=i * 30) + month_end = month_start + timedelta(days=30) + month_name = month_start.strftime("%b %Y") + + # Calculate MRR for this month + mrr = 0.0 + tier_counts = {"scout": 0, "trader": 0, "tycoon": 0} + new_subs = 0 + churned = 0 + + for sub in all_subs: + # Was this subscription active during this month? + started_before_month_end = sub.started_at <= month_end + cancelled_after_month_start = (sub.cancelled_at is None or sub.cancelled_at >= month_start) + + if started_before_month_end and cancelled_after_month_start: + price = tier_prices.get(sub.tier, 0) + mrr += price + tier_key = sub.tier.value + if tier_key in tier_counts: + tier_counts[tier_key] += 1 + + # New subscriptions in this month + if month_start <= sub.started_at < month_end and sub.tier != SubscriptionTier.SCOUT: + new_subs += 1 + + # Churned in this month + if sub.cancelled_at and month_start <= sub.cancelled_at < month_end: + churned += 1 + + monthly_data.append({ + "month": month_name, + "mrr": round(mrr, 2), + "arr": round(mrr * 12, 2), + "paying_customers": tier_counts["trader"] + tier_counts["tycoon"], + "scout": tier_counts["scout"], + "trader": tier_counts["trader"], + "tycoon": tier_counts["tycoon"], + "new_subscriptions": new_subs, + "churn": churned, + }) + + # Calculate growth metrics + if len(monthly_data) >= 2: + current_mrr = monthly_data[-1]["mrr"] + prev_mrr = monthly_data[-2]["mrr"] if monthly_data[-2]["mrr"] > 0 else 1 + mrr_growth = ((current_mrr - prev_mrr) / prev_mrr) * 100 + else: + mrr_growth = 0 + + # Calculate average revenue per user (ARPU) + current_paying = monthly_data[-1]["paying_customers"] if monthly_data else 0 + current_mrr = monthly_data[-1]["mrr"] if monthly_data else 0 + arpu = current_mrr / current_paying if current_paying > 0 else 0 + + # Calculate LTV (assuming 12 month average retention) + ltv = arpu * 12 + + return { + "monthly_data": monthly_data, + "metrics": { + "mrr_growth_percent": round(mrr_growth, 1), + "arpu": round(arpu, 2), + "ltv": round(ltv, 2), + "total_customers": sum(m["paying_customers"] for m in monthly_data[-1:]), + }, + "timestamp": datetime.utcnow().isoformat(), + } + + # ============== User Management ============== class UpdateUserRequest(BaseModel): diff --git a/frontend/package.json b/frontend/package.json index 8cea8b0..6d9ef9c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,8 @@ "lucide-react": "^0.303.0", "zustand": "^4.4.7", "clsx": "^2.1.0", - "sharp": "^0.33.5" + "sharp": "^0.33.5", + "recharts": "^2.15.0" }, "devDependencies": { "@types/node": "^20.10.6", diff --git a/frontend/src/components/admin/EarningsTab.tsx b/frontend/src/components/admin/EarningsTab.tsx index c557e80..78297b7 100644 --- a/frontend/src/components/admin/EarningsTab.tsx +++ b/frontend/src/components/admin/EarningsTab.tsx @@ -5,6 +5,7 @@ import { api } from '@/lib/api' import { DollarSign, TrendingUp, + TrendingDown, Users, Crown, Zap, @@ -13,8 +14,30 @@ import { ArrowUpRight, ArrowDownRight, Coins, + Target, + Activity, + BarChart3, + PieChart as PieChartIcon, } from 'lucide-react' import clsx from 'clsx' +import { + AreaChart, + Area, + BarChart, + Bar, + LineChart, + Line, + PieChart, + Pie, + Cell, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Legend, + ComposedChart, +} from 'recharts' interface EarningsData { mrr: number @@ -32,15 +55,84 @@ interface EarningsData { timestamp: string } +interface HistoryData { + monthly_data: Array<{ + month: string + mrr: number + arr: number + paying_customers: number + scout: number + trader: number + tycoon: number + new_subscriptions: number + churn: number + }> + metrics: { + mrr_growth_percent: number + arpu: number + ltv: number + total_customers: number + } + timestamp: string +} + +// Custom colors matching the design system +const COLORS = { + primary: '#ef4444', // red-500 + accent: '#22c55e', // green-500 + amber: '#f59e0b', // amber-500 + blue: '#3b82f6', // blue-500 + purple: '#8b5cf6', // purple-500 + cyan: '#06b6d4', // cyan-500 + rose: '#f43f5e', // rose-500 +} + +const TIER_COLORS = { + scout: '#6b7280', // gray-500 + trader: '#22c55e', // green-500 + tycoon: '#f59e0b', // amber-500 +} + +// Custom tooltip component +const CustomTooltip = ({ active, payload, label }: any) => { + if (!active || !payload || !payload.length) return null + + return ( +
{label}
+ {payload.map((entry: any, index: number) => ( +Revenue Dashboard
+Last updated: {new Date(data.timestamp).toLocaleString()}
@@ -95,144 +203,380 @@ export function EarningsTab() {Tycoon
+$29/month · Premium
+Tycoon
-$29/month
+{data.tier_breakdown.tycoon.count}
+${data.tier_breakdown.tycoon.revenue}/mo
+{data.tier_breakdown.tycoon.count}
-${data.tier_breakdown.tycoon.revenue}/mo
+ + {/* Trader */} +Trader
+$9/month · Standard
+{data.tier_breakdown.trader.count}
+${data.tier_breakdown.trader.revenue}/mo
+Scout
+Free tier
+{data.tier_breakdown.scout.count}
+$0/mo
++ {data.name}: {data.value} +
+Trader
-$9/month
-{data.tier_breakdown.trader.count}
-${data.tier_breakdown.trader.revenue}/mo
++ {data.name}: ${data.value} +
+Scout
-Free
-{data.tier_breakdown.scout.count}
-$0/mo
+{data.new_subscriptions.week}
+{data.new_subscriptions.week}
paid subscriptions
{data.churn.month}
-cancelled
+this month
+${data.yield_revenue_month.toFixed(0)}
+platform cut (30%)
Total Monthly Revenue
-Subscriptions + Yield (30%)
+Total Monthly Revenue
+Subscriptions + Yield Platform Cut
${data.total_revenue_month.toLocaleString()}
-+
${data.total_revenue_month.toLocaleString()}
+${(data.total_revenue_month * 12).toLocaleString()} projected ARR