Gross, Net Profit calculate
This commit is contained in:
@@ -2011,6 +2011,8 @@ const MOCK_PROJECTS = [
|
||||
spent: 22500,
|
||||
variancePercent: -50.0,
|
||||
margin: 0.40,
|
||||
commission: 4500,
|
||||
additionalExpenses: 1200,
|
||||
startDate: '2026-01-20',
|
||||
endDate: '2026-03-05',
|
||||
completionPercentage: 55,
|
||||
@@ -2084,6 +2086,8 @@ const MOCK_PROJECTS = [
|
||||
spent: 8500,
|
||||
variancePercent: 0,
|
||||
margin: 0.35,
|
||||
commission: 850,
|
||||
additionalExpenses: 350,
|
||||
startDate: '2025-11-15',
|
||||
endDate: '2025-12-10',
|
||||
completionPercentage: 100,
|
||||
@@ -2130,6 +2134,8 @@ const MOCK_PROJECTS = [
|
||||
spent: 18400,
|
||||
variancePercent: -42.5,
|
||||
margin: 0.30,
|
||||
commission: 3200,
|
||||
additionalExpenses: 900,
|
||||
startDate: '2026-01-06',
|
||||
endDate: '2026-02-28',
|
||||
completionPercentage: 42,
|
||||
@@ -2202,6 +2208,8 @@ const MOCK_PROJECTS = [
|
||||
spent: 62300,
|
||||
variancePercent: 13.3,
|
||||
margin: -0.13,
|
||||
commission: 5500,
|
||||
additionalExpenses: 3800,
|
||||
startDate: '2025-12-01',
|
||||
endDate: '2026-02-28',
|
||||
completionPercentage: 78,
|
||||
@@ -2285,6 +2293,8 @@ const MOCK_PROJECTS = [
|
||||
spent: 14200,
|
||||
variancePercent: -49.3,
|
||||
margin: 0.25,
|
||||
commission: 2800,
|
||||
additionalExpenses: 600,
|
||||
startDate: '2026-01-06',
|
||||
endDate: '2026-02-20',
|
||||
completionPercentage: 38,
|
||||
@@ -2355,6 +2365,8 @@ const MOCK_PROJECTS = [
|
||||
spent: 5600,
|
||||
variancePercent: -69.7,
|
||||
margin: 0.38,
|
||||
commission: 1850,
|
||||
additionalExpenses: 450,
|
||||
startDate: '2026-02-03',
|
||||
endDate: '2026-03-15',
|
||||
completionPercentage: 22,
|
||||
@@ -2404,6 +2416,8 @@ const MOCK_PROJECTS = [
|
||||
spent: 48000,
|
||||
variancePercent: -61.6,
|
||||
margin: 0.35,
|
||||
commission: 12500,
|
||||
additionalExpenses: 2800,
|
||||
startDate: '2026-01-27',
|
||||
endDate: '2026-04-15',
|
||||
completionPercentage: 32,
|
||||
@@ -2495,6 +2509,8 @@ const MOCK_PROJECTS = [
|
||||
spent: 91500,
|
||||
variancePercent: 22.0,
|
||||
margin: -0.22,
|
||||
commission: 7500,
|
||||
additionalExpenses: 4200,
|
||||
startDate: '2025-11-18',
|
||||
endDate: '2026-02-28',
|
||||
completionPercentage: 88,
|
||||
@@ -2542,6 +2558,8 @@ const MOCK_PROJECTS = [
|
||||
budget: 42000,
|
||||
spent: 25800,
|
||||
margin: 0.28,
|
||||
commission: 4200,
|
||||
additionalExpenses: 1100,
|
||||
startDate: '2026-01-13',
|
||||
endDate: '2026-03-10',
|
||||
completionPercentage: 50,
|
||||
@@ -2587,6 +2605,8 @@ const MOCK_PROJECTS = [
|
||||
budget: 35000,
|
||||
spent: 35000,
|
||||
margin: 0.30,
|
||||
commission: 3500,
|
||||
additionalExpenses: 800,
|
||||
startDate: '2025-09-15',
|
||||
endDate: '2025-10-28',
|
||||
completionPercentage: 100,
|
||||
@@ -2632,6 +2652,8 @@ const MOCK_PROJECTS = [
|
||||
budget: 95000,
|
||||
spent: 68400,
|
||||
margin: 0.20,
|
||||
commission: 9500,
|
||||
additionalExpenses: 5200,
|
||||
startDate: '2025-12-08',
|
||||
endDate: '2026-03-20',
|
||||
completionPercentage: 62,
|
||||
@@ -2685,6 +2707,8 @@ const MOCK_PROJECTS = [
|
||||
budget: 58000,
|
||||
spent: 22600,
|
||||
margin: 0.32,
|
||||
commission: 5800,
|
||||
additionalExpenses: 1500,
|
||||
startDate: '2026-01-13',
|
||||
endDate: '2026-04-01',
|
||||
completionPercentage: 28,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ArrowLeft, CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert,
|
||||
FileText, DollarSign, GitPullRequest, AlertCircle, Activity, Milestone,
|
||||
Shield, ChevronRight, Phone, MessageSquare, Mail, AlertOctagon, TrendingUp, Key, Zap, Camera, Plus, Map, User, Crosshair, Image as ImageIcon, MapPin,
|
||||
Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon
|
||||
Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon, Info
|
||||
} from 'lucide-react';
|
||||
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
|
||||
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
|
||||
@@ -220,6 +220,18 @@ const OwnerProjectDetail = () => {
|
||||
projects.find(p => p.id === projectId && p.ownerId === user?.id)
|
||||
, [projects, projectId, user]);
|
||||
|
||||
const profitMetrics = useMemo(() => {
|
||||
if (!project) return { totalCosts: 0, commission: 0, additionalExp: 0, grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
|
||||
const totalCosts = Number(project.actualCost ?? project.spent) || 0;
|
||||
const commission = Number(project.commission) || 0;
|
||||
const additionalExp = Number(project.additionalExpenses) || 0;
|
||||
const gross = totalReceived - totalCosts;
|
||||
const net = gross - commission - additionalExp;
|
||||
const grossMargin = totalReceived > 0 ? (gross / totalReceived) * 100 : 0;
|
||||
const netMargin = totalReceived > 0 ? (net / totalReceived) * 100 : 0;
|
||||
return { totalCosts, commission, additionalExp, grossProfit: gross, netProfit: net, grossMargin, netMargin };
|
||||
}, [project, totalReceived]);
|
||||
|
||||
if (!project) {
|
||||
return (
|
||||
<div className="min-h-full bg-zinc-50 dark:bg-[#09090b] flex items-center justify-center text-zinc-500">
|
||||
@@ -905,22 +917,99 @@ const OwnerProjectDetail = () => {
|
||||
{/* PAYMENTS TAB */}
|
||||
{activeTab === 'payments' && (
|
||||
<div className="space-y-6 max-w-7xl mx-auto">
|
||||
{/* Summary Row */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{/* Financial Summary Row */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<SpotlightCard className="p-4">
|
||||
<div className={`text-xl font-extrabold ${NEON_GREEN}`}>{formatCurrency(totalReceived)}</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Total Received</div>
|
||||
</SpotlightCard>
|
||||
<SpotlightCard className="p-4">
|
||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{payments.length}</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Payments</div>
|
||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(profitMetrics.totalCosts)}</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Total Costs</div>
|
||||
</SpotlightCard>
|
||||
<SpotlightCard className="p-4">
|
||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white font-mono">
|
||||
{payments.length > 0 ? [...payments].sort((a, b) => b.date.localeCompare(a.date))[0].date : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Latest Payment</div>
|
||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(profitMetrics.commission)}</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Commission</div>
|
||||
</SpotlightCard>
|
||||
<SpotlightCard className="p-4">
|
||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(profitMetrics.additionalExp)}</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Addl. Expenses</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
{/* Profit Summary Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Gross Profit */}
|
||||
<NeoCard
|
||||
spotlightColor={profitMetrics.grossProfit >= 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"}
|
||||
innerClassName="!p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp size={18} className={profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED} />
|
||||
<h4 className="text-xs font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">Gross Profit</h4>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500 cursor-help" />
|
||||
<div className="absolute right-0 top-6 z-30 w-64 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all pointer-events-none">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-1">Gross Profit Formula</p>
|
||||
<p className="font-mono text-[11px]">Total Received − Total Actual Costs</p>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5 space-y-1">
|
||||
<p>{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)}</p>
|
||||
<p className="font-bold">= {formatCurrency(profitMetrics.grossProfit)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
||||
{profitMetrics.grossProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.grossProfit)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${
|
||||
profitMetrics.grossMargin >= 0
|
||||
? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-[#39ff14]/10 border border-green-200 dark:border-[#39ff14]/20'
|
||||
: 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border border-red-200 dark:border-[#ff003c]/20'
|
||||
}`}>
|
||||
{profitMetrics.grossMargin >= 0 ? '+' : ''}{profitMetrics.grossMargin.toFixed(1)}% margin
|
||||
</span>
|
||||
</div>
|
||||
</NeoCard>
|
||||
|
||||
{/* Net Profit */}
|
||||
<NeoCard
|
||||
spotlightColor={profitMetrics.netProfit >= 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"}
|
||||
innerClassName="!p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign size={18} className={profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED} />
|
||||
<h4 className="text-xs font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">Net Profit</h4>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500 cursor-help" />
|
||||
<div className="absolute right-0 top-6 z-50 w-64 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all pointer-events-none">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-1">Net Profit Formula</p>
|
||||
<p className="font-mono text-[11px]">Gross Profit − Commission − Addl. Expenses</p>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5 space-y-1">
|
||||
<p>{formatCurrency(profitMetrics.grossProfit)} − {formatCurrency(profitMetrics.commission)} − {formatCurrency(profitMetrics.additionalExp)}</p>
|
||||
<p className="font-bold">= {formatCurrency(profitMetrics.netProfit)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
||||
{profitMetrics.netProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.netProfit)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${
|
||||
profitMetrics.netMargin >= 0
|
||||
? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-[#39ff14]/10 border border-green-200 dark:border-[#39ff14]/20'
|
||||
: 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border border-red-200 dark:border-[#ff003c]/20'
|
||||
}`}>
|
||||
{profitMetrics.netMargin >= 0 ? '+' : ''}{profitMetrics.netMargin.toFixed(1)}% margin
|
||||
</span>
|
||||
</div>
|
||||
</NeoCard>
|
||||
</div>
|
||||
|
||||
{/* Payments Table */}
|
||||
|
||||
Reference in New Issue
Block a user