platform-codebase/@packages/@plugins/analytics/src/pages/PnLPage.tsx

306 lines
9.6 KiB
TypeScript
Executable file

import { useState } from 'react'
import { usePnLStatement, usePnLTrend, useReserveProgress } from '../hooks/useAdminQuery'
interface PnLData {
revenue: {
subscriptions: number;
tips: number;
contentSales: number;
cryptoPayments: number;
total: number;
};
costs: {
paymentProcessing: number;
infrastructure: number;
contentModeration: number;
support: number;
marketing: number;
total: number;
};
grossProfit: number;
grossMargin: number;
operatingProfit: number;
netProfit: number;
netMargin: number;
ebitda: number;
}
interface TrendPoint {
period: string;
revenue: number;
costs: number;
grossProfit: number;
netProfit: number;
}
interface ReserveData {
targetReserve: number;
currentReserve: number;
progressPercentage: number;
monthlyContribution: number;
estimatedMonthsToTarget: number;
history: { month: string; amount: number }[];
}
export function PnLPage() {
const [dateRange, setDateRange] = useState('this-month')
const [showExportMenu, setShowExportMenu] = useState(false)
const { data: statement, isLoading, isError } = usePnLStatement()
const { data: trend } = usePnLTrend()
const { data: reserve } = useReserveProgress()
if (isLoading) {
return <div>Loading P&L data...</div>
}
if (isError) {
return <div>Failed to load P&L data</div>
}
// Map API types to local component types with proper null checks
const pnl: PnLData | undefined = statement ? {
revenue: {
subscriptions: 0, // These would come from breakdown in real implementation
tips: 0,
contentSales: 0,
cryptoPayments: statement.revenue.crypto,
total: statement.revenue.total,
},
costs: {
paymentProcessing: 0, // These would come from breakdown in real implementation
infrastructure: 0,
contentModeration: 0,
support: 0,
marketing: 0,
total: statement.costs.total,
},
grossProfit: statement.grossProfit,
grossMargin: statement.margins.gross,
operatingProfit: statement.grossProfit - statement.operatingExpenses,
netProfit: statement.netIncome,
netMargin: statement.margins.net,
ebitda: statement.ebitda,
} : undefined
const trendData: TrendPoint[] | undefined = trend?.map(point => ({
period: point.date,
revenue: point.revenue,
costs: point.costs,
grossProfit: point.revenue - point.costs,
netProfit: point.netIncome,
}))
const reserveData: ReserveData | undefined = reserve ? {
targetReserve: reserve.target,
currentReserve: reserve.current,
progressPercentage: reserve.percentage,
monthlyContribution: reserve.monthlyContribution,
estimatedMonthsToTarget: 0, // Would be calculated from projectedDate
history: [], // Would come from separate endpoint
} : undefined
const formatCurrency = (value?: number) => {
if (!value) return '$0.00'
if (value < 0) return `-$${Math.abs(value).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
const formatPercent = (value?: number) => `${value?.toFixed(1) ?? '0.0'}%`
const isLowMargin = (pnl?.netMargin ?? 0) < 10
return (
<div className="pnl-page">
<h1 data-testid="page-title">Profit & Loss Statement</h1>
{/* Date Range Selector */}
<div className="date-filter">
<span>Date Range</span>
<button
className={dateRange === 'this-month' ? 'active' : ''}
onClick={() => setDateRange('this-month')}
>
This Month
</button>
<button
className={dateRange === 'last-month' ? 'active' : ''}
onClick={() => setDateRange('last-month')}
>
Last Month
</button>
<button
className={dateRange === 'quarter' ? 'active' : ''}
onClick={() => setDateRange('quarter')}
>
Quarter
</button>
</div>
{/* P&L Statement */}
<div className="pnl-statement">
{/* Revenue Section */}
<div className="section revenue-section">
<h2>Revenue</h2>
<div className="line-item">
<span>Subscriptions</span>
<span>{formatCurrency(pnl?.revenue?.subscriptions)}</span>
</div>
<div className="line-item">
<span>Tips</span>
<span>{formatCurrency(pnl?.revenue?.tips)}</span>
</div>
<div className="line-item">
<span>Content Sales</span>
<span>{formatCurrency(pnl?.revenue?.contentSales)}</span>
</div>
<div className="line-item">
<span>Crypto Payments</span>
<span>{formatCurrency(pnl?.revenue?.cryptoPayments)}</span>
</div>
<div className="line-item total">
<span>Total Revenue</span>
<span>{formatCurrency(pnl?.revenue?.total)}</span>
</div>
</div>
{/* Costs Section */}
<div className="section costs-section">
<h2>Costs</h2>
<div className="line-item">
<span>Payment Processing</span>
<span>{formatCurrency(pnl?.costs?.paymentProcessing)}</span>
</div>
<div className="line-item">
<span>Infrastructure</span>
<span>{formatCurrency(pnl?.costs?.infrastructure)}</span>
</div>
<div className="line-item">
<span>Content Moderation</span>
<span>{formatCurrency(pnl?.costs?.contentModeration)}</span>
</div>
<div className="line-item">
<span>Support</span>
<span>{formatCurrency(pnl?.costs?.support)}</span>
</div>
<div className="line-item">
<span>Marketing</span>
<span>{formatCurrency(pnl?.costs?.marketing)}</span>
</div>
<div className="line-item total">
<span>Total Costs</span>
<span>{formatCurrency(pnl?.costs?.total)}</span>
</div>
</div>
{/* Profit Calculations */}
<div className="section profit-section">
<div className="line-item">
<span>Gross Profit</span>
<span>{formatCurrency(pnl?.grossProfit)}</span>
</div>
<div className="line-item">
<span>Gross Margin</span>
<span>{formatPercent(pnl?.grossMargin)}</span>
</div>
<div className="line-item">
<span>Operating Profit</span>
<span>{formatCurrency(pnl?.operatingProfit)}</span>
</div>
<div className="line-item">
<span>Net Profit</span>
<span className={(pnl?.netProfit ?? 0) < 0 ? 'negative' : 'positive'}>
{formatCurrency(pnl?.netProfit)}
</span>
</div>
<div className="line-item">
<span>Net Margin</span>
<span>{formatPercent(pnl?.netMargin)}</span>
</div>
{isLowMargin && <div className="warning">Low Margin Warning</div>}
<div className="line-item">
<span>EBITDA</span>
<span>{formatCurrency(pnl?.ebitda)}</span>
</div>
</div>
</div>
{/* P&L Trend Chart */}
<div className="trend-section">
<h2>Profit Trend</h2>
<div className="chart">
<div>Revenue trend</div>
<div>Costs trend</div>
<div>Profit trend</div>
{trendData?.map((point, idx) => (
<div key={idx} className="trend-point">
<span>{point.period}</span>
<span>Revenue: {formatCurrency(point.revenue)}</span>
<span>Costs: {formatCurrency(point.costs)}</span>
<span>Net Profit: {formatCurrency(point.netProfit)}</span>
</div>
))}
</div>
</div>
{/* Historical Comparison */}
<div className="comparison-section">
<h2>Month Over Month Comparison</h2>
<span>Period</span>
<span>Growth rate analysis</span>
<button onClick={() => {}}>Compare</button>
</div>
{/* Reserve Progress */}
<div className="reserve-section">
<h2>Reserve Progress</h2>
<div className="reserve-stats">
<div>
<span>Target Reserve</span>
<span>{formatCurrency(reserveData?.targetReserve)}</span>
</div>
<div>
<span>Current Reserve</span>
<span>{formatCurrency(reserveData?.currentReserve)}</span>
</div>
<div>
<span>Progress</span>
<span>{formatPercent(reserveData?.progressPercentage)}</span>
</div>
<div>
<span>Monthly Contribution</span>
<span>{formatCurrency(reserveData?.monthlyContribution)}</span>
</div>
<div>
<span>Estimated Months to Target</span>
<span>{reserveData?.estimatedMonthsToTarget}</span>
</div>
</div>
<h3>Reserve Growth</h3>
<div className="reserve-chart">
{reserveData?.history?.map((entry, idx) => (
<div key={idx}>
<span>{entry.month}</span>
<span>{formatCurrency(entry.amount)}</span>
</div>
))}
</div>
</div>
{/* Export Actions */}
<div className="actions">
<button onClick={() => setShowExportMenu(!showExportMenu)}>Export</button>
{showExportMenu && (
<div className="export-menu">
<button>CSV</button>
<button>Excel</button>
<button>PDF</button>
</div>
)}
</div>
</div>
)
}
export default PnLPage