306 lines
9.6 KiB
TypeScript
Executable file
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
|