Build out Profit Analysis Trends tab (was a placeholder)
Replace 'coming soon' with a real monthly trend view: revenue vs profit bar chart (px-scaled) plus a per-month breakdown table (orders, revenue, printing, expenses, profit, margin) from calculateMonthlyTrends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c5a6cba041
commit
b4d4f34864
1 changed files with 75 additions and 6 deletions
|
|
@ -40,6 +40,11 @@ const ProfitAnalysis = () => {
|
||||||
return ProfitAnalysisService.calculateProfitMetrics(filteredOrders, products || [], filteredExpenses);
|
return ProfitAnalysisService.calculateProfitMetrics(filteredOrders, products || [], filteredExpenses);
|
||||||
}, [filteredOrders, products, filteredExpenses]);
|
}, [filteredOrders, products, filteredExpenses]);
|
||||||
|
|
||||||
|
// Monthly revenue / cost / profit trend
|
||||||
|
const monthlyTrends = useMemo(() => {
|
||||||
|
return ProfitAnalysisService.calculateMonthlyTrends(filteredOrders, products || [], filteredExpenses);
|
||||||
|
}, [filteredOrders, products, filteredExpenses]);
|
||||||
|
|
||||||
// Profit & loss statement + indicative GST summary for the selected period
|
// Profit & loss statement + indicative GST summary for the selected period
|
||||||
const plData = useMemo(() => {
|
const plData = useMemo(() => {
|
||||||
const revenue = filteredOrders.reduce((s, o) => s + orderNetRevenue(o), 0);
|
const revenue = filteredOrders.reduce((s, o) => s + orderNetRevenue(o), 0);
|
||||||
|
|
@ -456,13 +461,77 @@ const ProfitAnalysis = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Placeholder for trends view */}
|
{/* Monthly trends */}
|
||||||
{selectedView === 'trends' && (
|
{selectedView === 'trends' && (
|
||||||
|
monthlyTrends.length === 0 ? (
|
||||||
<div className="bg-white p-8 rounded-lg shadow text-center">
|
<div className="bg-white p-8 rounded-lg shadow text-center">
|
||||||
<BarChart3 className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
<BarChart3 className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Profit Trends</h3>
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Profit Trends</h3>
|
||||||
<p className="text-gray-600">Interactive charts coming soon...</p>
|
<p className="text-gray-600">No data for the selected period.</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Revenue & profit bars */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Revenue & Profit by Month</h3>
|
||||||
|
<span className="flex items-center gap-1 text-xs text-gray-500"><span className="w-3 h-3 rounded-sm bg-blue-500 inline-block" /> Revenue</span>
|
||||||
|
<span className="flex items-center gap-1 text-xs text-gray-500"><span className="w-3 h-3 rounded-sm bg-green-500 inline-block" /> Profit</span>
|
||||||
|
</div>
|
||||||
|
{(() => {
|
||||||
|
const maxVal = Math.max(...monthlyTrends.map(m => Math.max(m.revenue, m.profit)), 0);
|
||||||
|
const maxBarPx = 180;
|
||||||
|
return (
|
||||||
|
<div className="h-60 flex items-end justify-between gap-2 pb-6 overflow-x-auto">
|
||||||
|
{monthlyTrends.map((m, i) => {
|
||||||
|
const revPx = maxVal > 0 ? Math.max((m.revenue / maxVal) * maxBarPx, m.revenue > 0 ? 4 : 0) : 0;
|
||||||
|
const profitPx = maxVal > 0 ? Math.max((Math.max(m.profit, 0) / maxVal) * maxBarPx, 0) : 0;
|
||||||
|
return (
|
||||||
|
<div key={i} className="flex-1 min-w-[40px] flex flex-col items-center justify-end h-full group">
|
||||||
|
<div className="flex items-end gap-0.5 w-full justify-center" style={{ height: `${maxBarPx}px` }}>
|
||||||
|
<div className="w-1/2 bg-blue-500 rounded-t-sm" style={{ height: `${revPx}px` }}
|
||||||
|
title={`Revenue ${formatCurrency(m.revenue)}`} />
|
||||||
|
<div className="w-1/2 bg-green-500 rounded-t-sm" style={{ height: `${profitPx}px` }}
|
||||||
|
title={`Profit ${formatCurrency(m.profit)}`} />
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-gray-500 mt-2 whitespace-nowrap">{m.month.split(' ')[0]}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Monthly breakdown table */}
|
||||||
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
{['Month', 'Orders', 'Revenue', 'Printing', 'Expenses', 'Profit', 'Margin'].map(h => (
|
||||||
|
<th key={h} className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{h}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
|
{monthlyTrends.map((m, i) => (
|
||||||
|
<tr key={i} className="hover:bg-gray-50">
|
||||||
|
<td className="px-4 py-2 text-gray-900">{m.month}</td>
|
||||||
|
<td className="px-4 py-2 text-gray-700">{m.orderCount}</td>
|
||||||
|
<td className="px-4 py-2 text-gray-900">{formatCurrency(m.revenue)}</td>
|
||||||
|
<td className="px-4 py-2 text-gray-500">{formatCurrency(m.costs)}</td>
|
||||||
|
<td className="px-4 py-2 text-gray-500">{formatCurrency(m.expenses)}</td>
|
||||||
|
<td className={`px-4 py-2 font-medium ${m.profit >= 0 ? 'text-green-600' : 'text-red-600'}`}>{formatCurrency(m.profit)}</td>
|
||||||
|
<td className="px-4 py-2 text-gray-700">{m.margin.toFixed(1)}%</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Order Analysis */}
|
{/* Order Analysis */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue