updates budget & cost feature of projects: added sub contrcator comm, team cost

This commit is contained in:
Satyam Rastogi
2026-05-20 14:34:38 +05:30
parent 2589975074
commit 11b15a2e30
+279 -104
View File
@@ -1,4 +1,4 @@
import React, { useState, useMemo, useRef } from 'react';
import React, { useState, useMemo, useRef, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import { useMockStore } from '../../data/mockStore';
@@ -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, Info, Settings, Building2, User as UserIcon, Briefcase, Users
Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon, Info, Settings, Building2, User as UserIcon, Briefcase, Users, HardHat, Wrench, UserCheck, Wallet
} from 'lucide-react';
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
@@ -125,6 +125,51 @@ function seedPaymentsMock(projectId) {
const PAYMENTS_PER_PAGE = 5;
// ── Expense type taxonomy ──────────────────────────────────────────────
const EXPENSE_TYPE_CONFIG = {
material: { label: 'Material', icon: Wrench, color: 'text-sky-600 dark:text-[#00f0ff]', bg: 'bg-sky-50 dark:bg-blue-500/10', border: 'border-sky-200 dark:border-blue-500/20' },
labor: { label: 'Labor', icon: HardHat, color: 'text-emerald-600 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-500/10', border: 'border-emerald-200 dark:border-emerald-500/20' },
team: { label: 'Team Commission', icon: UserCheck, color: 'text-purple-600 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20' },
subcontractor: { label: 'Subcontractor', icon: HardHat, color: 'text-amber-600 dark:text-[#fda913]', bg: 'bg-amber-50 dark:bg-[#fda913]/10', border: 'border-amber-200 dark:border-[#fda913]/20' },
other: { label: 'Other', icon: DollarSign, color: 'text-zinc-600 dark:text-zinc-400', bg: 'bg-zinc-100 dark:bg-white/5', border: 'border-zinc-200 dark:border-white/10' },
};
const SUBCONTRACTOR_WORK_TYPES = ['Plumbing', 'Electrical', 'Painting', 'Roofing', 'Civil work', 'HVAC', 'Carpentry', 'Other'];
const inferExpenseType = (category = '') => {
const c = (category || '').toLowerCase();
if (c.includes('material')) return 'material';
if (c.includes('labor')) return 'labor';
if (c.includes('commission') || c.includes('sales rep')) return 'team';
if (SUBCONTRACTOR_WORK_TYPES.some(w => c.includes(w.toLowerCase()))) return 'subcontractor';
return 'other';
};
function seedExpenseRowsMock(project) {
const baseRows = (project?.budgetBreakdown || []).map((b, i) => ({
id: `exp_base_${i}`,
type: inferExpenseType(b.category),
name: b.name || '—',
category: b.category || '',
allocated: Number(b.allocated) || 0,
actual: Number(b.actual) || 0,
paid: Number(b.actual) > 0 && Number(b.actual) >= Number(b.allocated) * 0.9,
}));
const seed = (project?.id || '').charCodeAt((project?.id || 'x').length - 1) % 3;
const teamRows = [
{ id: 'exp_team_1', type: 'team', name: 'Jesus Gonzales', category: 'Sales Rep', allocated: 4200, actual: 4200, paid: true },
{ id: 'exp_team_2', type: 'team', name: 'Frank Agent', category: 'Canvasser', allocated: 1500, actual: 1500, paid: false },
{ id: 'exp_team_3', type: 'team', name: 'Sarah Sales', category: 'Estimator', allocated: 1200, actual: 0, paid: false },
].slice(0, 2 + seed);
const subRows = [
{ id: 'exp_sub_1', type: 'subcontractor', name: 'PlumbPro Services', category: 'Plumbing', allocated: 1800, actual: 1800, paid: true },
{ id: 'exp_sub_2', type: 'subcontractor', name: 'Bright Electric LLC', category: 'Electrical', allocated: 2400, actual: 2150, paid: false },
{ id: 'exp_sub_3', type: 'subcontractor', name: 'Premium Painters', category: 'Painting', allocated: 950, actual: 950, paid: true },
{ id: 'exp_sub_4', type: 'subcontractor', name: 'Solid Civil Works', category: 'Civil work', allocated: 3200, actual: 0, paid: false },
].slice(0, 3 + seed);
return [...baseRows, ...teamRows, ...subRows];
}
const COMMISSION_TYPES = [
{ value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' },
{ value: 'percent_gross', label: '% of Gross', hint: 'Percentage of Total Received' },
@@ -158,6 +203,10 @@ const OwnerProjectDetail = () => {
const [paymentSort, setPaymentSort] = useState({ field: 'date', dir: 'desc' });
const [paymentPage, setPaymentPage] = useState(0);
// ── Expense rows state (unified cost breakdown) ───────────────────────────
const [expenseRows, setExpenseRows] = useState([]);
const [expensesSeeded, setExpensesSeeded] = useState(false);
const [openTooltip, setOpenTooltip] = useState(null);
// ── Commission config state ─────────────────────────────────────────────
@@ -248,6 +297,31 @@ const OwnerProjectDetail = () => {
projects.find(p => p.id === projectId && p.ownerId === user?.id)
, [projects, projectId, user]);
useEffect(() => {
if (project && !expensesSeeded) {
setExpenseRows(seedExpenseRowsMock(project));
setExpensesSeeded(true);
}
}, [project, expensesSeeded]);
const togglePaid = (id) => {
setExpenseRows(prev => prev.map(r => r.id === id ? { ...r, paid: !r.paid } : r));
};
const handleAddExpense = (data, type) => {
const amt = parseFloat(data.amount) || 0;
const newRow = {
id: `exp_${Date.now()}`,
type: type || data.type || 'other',
name: data.name || '',
category: data.category || '',
allocated: amt,
actual: amt,
paid: false,
};
setExpenseRows(prev => [newRow, ...prev]);
};
const teamFinancials = useMemo(() => {
if (!project) return { totalReceived: 0, grossProfit: 0 };
const paid = (project.paymentSchedule || [])
@@ -283,9 +357,16 @@ const OwnerProjectDetail = () => {
const activeCommissionConfig = resolvedCommission;
const expenseTotals = useMemo(() => {
const total = expenseRows.reduce((s, r) => s + (Number(r.actual) || Number(r.allocated) || 0), 0);
const paid = expenseRows.reduce((s, r) => s + (r.paid ? (Number(r.actual) || Number(r.allocated) || 0) : 0), 0);
const outstanding = Math.max(0, total - paid);
return { total, paid, outstanding };
}, [expenseRows]);
const profitMetrics = useMemo(() => {
if (!project) return { totalCosts: 0, commission: 0, commissionFormula: '', grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
const totalCosts = Number(project.actualCost ?? project.spent) || 0;
if (!project) return { totalCosts: 0, totalExpenses: 0, paidExpenses: 0, outstandingExpenses: 0, totalBudget: 0, commission: 0, commissionFormula: '', grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
const totalCosts = expenseTotals.total || Number(project.actualCost ?? project.spent) || 0;
const gross = totalReceived - totalCosts;
const { type, rate } = activeCommissionConfig;
@@ -315,11 +396,25 @@ const OwnerProjectDetail = () => {
}
commission = Number.isFinite(commission) ? commission : 0;
const net = gross - commission;
// Net Profit = Total Payments Received Total Expenses (expense table already includes team commission + subcontractors)
const net = totalReceived - expenseTotals.total;
const grossMargin = totalReceived > 0 ? (gross / totalReceived) * 100 : 0;
const netMargin = totalReceived > 0 ? (net / totalReceived) * 100 : 0;
return { totalCosts, commission, commissionFormula, grossProfit: gross, netProfit: net, grossMargin, netMargin };
}, [project, totalReceived, activeCommissionConfig]);
const totalBudget = Number(project.approvedBudget ?? project.budget) || 0;
return {
totalCosts,
totalExpenses: expenseTotals.total,
paidExpenses: expenseTotals.paid,
outstandingExpenses: expenseTotals.outstanding,
totalBudget,
commission,
commissionFormula,
grossProfit: gross,
netProfit: net,
grossMargin,
netMargin,
};
}, [project, totalReceived, activeCommissionConfig, expenseTotals]);
if (!project) {
return (
@@ -691,29 +786,54 @@ const OwnerProjectDetail = () => {
{/* BUDGET TAB */}
{activeTab === 'budget' && (
<div className="space-y-6 max-w-7xl mx-auto">
{budgetChartData.length > 0 ? (
<>
{/* Budget & Costs Summary Cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
{[
{ label: 'Total Budget', value: profitMetrics.totalBudget, icon: Wallet, color: 'text-sky-600 dark:text-[#00f0ff]', spotlight: 'rgba(0, 240, 255, 0.12)' },
{ label: 'Total Expenses', value: profitMetrics.totalExpenses, icon: Wrench, color: 'text-amber-600 dark:text-[#fda913]', spotlight: 'rgba(253, 169, 19, 0.12)' },
{ label: 'Payments Received', value: totalReceived, icon: Banknote, color: 'text-emerald-600 dark:text-[#39ff14]', spotlight: 'rgba(57, 255, 20, 0.12)' },
{ label: 'Outstanding Expenses', value: profitMetrics.outstandingExpenses, icon: AlertCircle, color: 'text-orange-600 dark:text-[#ff4500]', spotlight: 'rgba(255, 69, 0, 0.12)' },
{
label: 'Net Profit',
value: profitMetrics.netProfit,
icon: TrendingUp,
color: profitMetrics.netProfit >= 0 ? 'text-emerald-600 dark:text-[#39ff14]' : 'text-red-600 dark:text-[#ff003c]',
spotlight: profitMetrics.netProfit >= 0 ? 'rgba(57, 255, 20, 0.12)' : 'rgba(255, 0, 60, 0.12)',
emphasis: true,
},
].map((c, i) => {
const CardIcon = c.icon;
return (
<NeoCard key={i} spotlightColor={c.spotlight} innerClassName="!p-4">
<div className="flex items-center gap-2 mb-2">
<CardIcon size={14} className={c.color} />
<h4 className="text-[10px] font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest truncate">{c.label}</h4>
</div>
<div className={`font-mono font-black tracking-tight ${c.emphasis ? 'text-2xl sm:text-3xl' : 'text-lg sm:text-xl'} ${c.color}`}>
{c.value >= 0 ? '' : '-'}{formatCurrency(Math.abs(c.value))}
</div>
{c.label === 'Net Profit' && (
<p className="mt-1 text-[10px] font-mono text-zinc-500 dark:text-zinc-500 truncate">
Received Expenses
</p>
)}
{c.label === 'Outstanding Expenses' && (
<p className="mt-1 text-[10px] font-mono text-zinc-500 dark:text-zinc-500 truncate">
Paid: {formatCurrency(profitMetrics.paidExpenses)}
</p>
)}
</NeoCard>
);
})}
</div>
{budgetChartData.length > 0 && (
<NeoCard spotlightColor="rgba(0, 240, 255, 0.1)">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-3">
<DollarSign className={NEON_BLUE} size={20} />
<h3 className="text-lg font-bold text-zinc-900 dark:text-white tracking-widest uppercase">Budget Breakdown</h3>
</div>
<button
onClick={() => setCreateModalConfig({
title: 'Add Budget Cost', icon: DollarSign, iconColor: 'text-[#39ff14]', iconBg: 'bg-[#39ff14]/10',
submitLabel: 'Save Cost', submitColor: 'bg-[#39ff14]/20 text-[#39ff14] border-[#39ff14]/30',
fields: [
{ name: 'category', label: 'Category', type: 'text', required: true },
{ name: 'allocated', label: 'Allocated Amount', type: 'number', required: true },
{ name: 'spent', label: 'Actual Spent', type: 'number' }
],
onSubmit: handleCreateSubmit
})}
className="bg-blue-500/10 hover:bg-blue-500/20 text-[#00f0ff] border border-blue-500/30 px-3 py-1.5 rounded-lg text-xs font-bold transition flex items-center gap-2"
>
<Plus size={14} /> Add Cost
</button>
</div>
<p className="text-xs text-zinc-400 mb-6 font-mono">Allocated vs Actual ($k)</p>
<div className="h-64 bg-zinc-50 dark:bg-black/20 rounded-xl p-4 border border-zinc-200 dark:border-white/5">
@@ -728,60 +848,138 @@ const OwnerProjectDetail = () => {
</ResponsiveContainer>
</div>
</NeoCard>
)}
<NeoCard spotlightColor="rgba(253, 169, 19, 0.1)" innerClassName="p-0 lg:p-0">
<div className="p-5 border-b border-zinc-200 dark:border-white/5 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-3">
<DollarSign className={NEON_GOLD} size={20} />
<h3 className="text-lg font-bold text-zinc-900 dark:text-white tracking-widest uppercase">Cost Breakdown / Expenses</h3>
<span className="text-xs font-bold bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/5 px-2 py-0.5 rounded-full text-zinc-500 dark:text-zinc-400">
{expenseRows.length}
</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => setCreateModalConfig({
title: 'Add Team Commission Cost', icon: UserCheck, iconColor: 'text-purple-500 dark:text-purple-400', iconBg: 'bg-purple-500/10',
submitLabel: 'Save Team Cost', submitColor: 'bg-purple-500/20 text-purple-500 dark:text-purple-400 border-purple-500/30',
fields: [
{ name: 'name', label: 'Team Member Name', type: 'text', required: true, placeholder: 'e.g. Jesus Gonzales' },
{ name: 'category', label: 'Role / Category', type: 'text', required: true, placeholder: 'e.g. Sales Rep' },
{ name: 'amount', label: 'Commission Amount ($)', type: 'number', required: true },
],
onSubmit: (d) => handleAddExpense(d, 'team'),
})}
className="bg-purple-500/10 hover:bg-purple-500/20 text-purple-600 dark:text-purple-400 border border-purple-500/30 px-3 py-1.5 rounded-lg text-xs font-bold transition flex items-center gap-2"
>
<UserCheck size={14} /> Add Team Cost
</button>
<button
onClick={() => setCreateModalConfig({
title: 'Add Subcontractor Cost', icon: HardHat, iconColor: 'text-amber-500 dark:text-[#fda913]', iconBg: 'bg-amber-500/10',
submitLabel: 'Save Subcontractor Cost', submitColor: 'bg-amber-500/20 text-amber-500 dark:text-[#fda913] border-amber-500/30',
fields: [
{ name: 'name', label: 'Subcontractor Name', type: 'text', required: true, placeholder: 'e.g. PlumbPro Services' },
{ name: 'category', label: 'Category / Work Type', type: 'select', required: true, options: SUBCONTRACTOR_WORK_TYPES.map(w => ({ label: w, value: w })) },
{ name: 'amount', label: 'Amount ($)', type: 'number', required: true },
],
onSubmit: (d) => handleAddExpense(d, 'subcontractor'),
})}
className="bg-amber-500/10 hover:bg-amber-500/20 text-amber-600 dark:text-[#fda913] border border-amber-500/30 px-3 py-1.5 rounded-lg text-xs font-bold transition flex items-center gap-2"
>
<HardHat size={14} /> Add Subcontractor
</button>
<button
onClick={() => setCreateModalConfig({
title: 'Add Cost', icon: DollarSign, iconColor: 'text-[#39ff14]', iconBg: 'bg-[#39ff14]/10',
submitLabel: 'Save Cost', submitColor: 'bg-[#39ff14]/20 text-[#39ff14] border-[#39ff14]/30',
fields: [
{ name: 'name', label: 'Vendor / Source Name', type: 'text', required: true },
{ name: 'category', label: 'Category', type: 'text', required: true, placeholder: 'e.g. Materials - Shingles' },
{ name: 'amount', label: 'Amount ($)', type: 'number', required: true },
],
onSubmit: (d) => handleAddExpense(d, inferExpenseType(d.category)),
})}
className="bg-blue-500/10 hover:bg-blue-500/20 text-[#00f0ff] border border-blue-500/30 px-3 py-1.5 rounded-lg text-xs font-bold transition flex items-center gap-2"
>
<Plus size={14} /> Add Cost
</button>
</div>
</div>
<div className="overflow-x-auto w-full rounded-2xl">
<table className="w-full text-left border-collapse min-w-[600px]">
<table className="w-full text-left border-collapse min-w-[860px]">
<thead className="bg-zinc-100 dark:bg-[#18181b]/80 border-b border-zinc-200 dark:border-white/10">
<tr>
{['Name', 'Category', 'Allocated', 'Actual', 'Variance'].map(h => (
<th key={h} className={`px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-400 ${h !== 'Name' && h !== 'Category' ? 'text-right' : ''}`}>{h}</th>
))}
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Type</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Name</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Category / Role</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right">Allocated</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right">Actual</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-center">Paid</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
{(project.budgetBreakdown || []).map((b, i) => {
const variance = b.actual - b.allocated;
{expenseRows.map((row) => {
const cfg = EXPENSE_TYPE_CONFIG[row.type] || EXPENSE_TYPE_CONFIG.other;
const TypeIcon = cfg.icon;
return (
<tr key={i} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
<td className="px-5 py-4 text-sm text-zinc-500 dark:text-zinc-400">{b.name || '—'}</td>
<td className="px-5 py-4 font-bold text-sm text-zinc-900 dark:text-white">{b.category}</td>
<td className="px-5 py-4 text-right font-mono text-sm text-zinc-600 dark:text-zinc-300">{formatCurrency(b.allocated)}</td>
<td className="px-5 py-4 text-right font-mono text-sm text-[#00f0ff]">{formatCurrency(b.actual)}</td>
<td className={`px-5 py-4 text-right font-mono text-sm font-bold ${variance > 0 ? 'text-[#ff4500]' : 'text-[#39ff14]'}`}>
{variance > 0 ? '+' : ''}{formatCurrency(variance)}
<tr key={row.id} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
<td className="px-5 py-4">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border ${cfg.color} ${cfg.bg} ${cfg.border}`}>
<TypeIcon size={11} />
{cfg.label}
</span>
</td>
<td className="px-5 py-4 font-bold text-sm text-zinc-900 dark:text-white">{row.name || '—'}</td>
<td className="px-5 py-4 text-sm text-zinc-500 dark:text-zinc-400">{row.category || '—'}</td>
<td className="px-5 py-4 text-right font-mono text-sm text-zinc-600 dark:text-zinc-300">{formatCurrency(row.allocated)}</td>
<td className="px-5 py-4 text-right font-mono text-sm text-[#00f0ff]">{formatCurrency(row.actual)}</td>
<td className="px-5 py-4 text-center">
<label className="inline-flex items-center justify-center cursor-pointer">
<input
type="checkbox"
checked={!!row.paid}
onChange={() => togglePaid(row.id)}
className="sr-only peer"
aria-label={`Mark ${row.name || 'expense'} paid`}
/>
<span className={`w-5 h-5 rounded-md border flex items-center justify-center transition-colors ${row.paid
? 'bg-emerald-500 border-emerald-500 dark:bg-[#39ff14] dark:border-[#39ff14]'
: 'bg-white dark:bg-[#18181b] border-zinc-300 dark:border-white/20 hover:border-emerald-400'}`}>
{row.paid && <CheckCircle size={12} className="text-white dark:text-black" strokeWidth={3} />}
</span>
</label>
</td>
</tr>
);
})}
{expenseRows.length === 0 && (
<tr>
<td colSpan={6} className="px-5 py-10 text-center text-sm text-zinc-500 dark:text-zinc-400">
No expenses recorded yet.
</td>
</tr>
)}
</tbody>
<tfoot className="border-t-2 border-zinc-300 dark:border-white/10">
<tr className="bg-zinc-50 dark:bg-white/[0.03]">
<td className="px-5 py-4" colSpan={2}>
<td className="px-5 py-4" colSpan={3}>
<span className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400">Totals</span>
</td>
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-green-600 dark:text-[#39ff14]">{formatCurrency(totalReceived)}</td>
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-[#00f0ff]">{formatCurrency(profitMetrics.totalCosts)}</td>
{(() => {
const totalVariance = profitMetrics.totalCosts - totalReceived;
return (
<td className={`px-5 py-4 text-right font-mono text-sm font-bold ${totalVariance > 0 ? 'text-[#ff4500]' : 'text-[#39ff14]'}`}>
{totalVariance > 0 ? '+' : ''}{formatCurrency(totalVariance)}
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-zinc-600 dark:text-zinc-300">
{formatCurrency(expenseRows.reduce((s, r) => s + (Number(r.allocated) || 0), 0))}
</td>
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-[#00f0ff]">
{formatCurrency(profitMetrics.totalExpenses)}
</td>
<td className="px-5 py-4 text-center text-[10px] font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">
{expenseRows.filter(r => r.paid).length}/{expenseRows.length}
</td>
);
})()}
</tr>
</tfoot>
</table>
</div>
</NeoCard>
</>
) : (
<NeoCard className="p-12 text-center text-zinc-500">
<DollarSign size={48} className="mx-auto mb-4 text-zinc-600" />
<p className="font-bold text-lg text-zinc-900 dark:text-white">No detailed budget breakdown available.</p>
<p className="text-sm mt-2 text-zinc-400 font-mono">Budget: {formatCurrency(project.approvedBudget || project.budget)} &nbsp;&middot;&nbsp; Spent: {formatCurrency(project.actualCost || project.spent)}</p>
</NeoCard>
)}
{/* Commission Configuration Card */}
<NeoCard spotlightColor="rgba(253, 169, 19, 0.1)" innerClassName="!p-5">
@@ -888,54 +1086,15 @@ const OwnerProjectDetail = () => {
</div>
</NeoCard>
{/* Profit Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<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">
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'gross' ? null : 'gross'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
</button>
{openTooltip === 'gross' && (
<div className="absolute right-0 top-2 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">
<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)} = {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 Breakdown */}
<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-start justify-between mb-4">
<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>
<TrendingUp size={18} className={profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED} />
<h4 className="text-sm font-mono font-bold text-zinc-900 dark:text-white uppercase tracking-widest">Net Profit Breakdown</h4>
</div>
<div className="relative">
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'net' ? null : 'net'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
@@ -944,18 +1103,32 @@ const OwnerProjectDetail = () => {
{openTooltip === 'net' && (
<div className="absolute right-0 top-2 z-30 w-72 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">
<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</p>
<p className="font-mono text-[11px]">Total Payments Received Total 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.netProfit)}</p>
<p>{formatCurrency(totalReceived)} {formatCurrency(profitMetrics.totalExpenses)} = {formatCurrency(profitMetrics.netProfit)}</p>
</div>
<p className="mt-2 text-[10px] text-zinc-500">Total expenses include materials, labor, team commission, and subcontractor costs.</p>
</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 className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
<div className="bg-zinc-50 dark:bg-black/30 rounded-xl px-4 py-3 border border-zinc-100 dark:border-white/5">
<p className="text-[10px] font-mono font-bold text-zinc-500 dark:text-zinc-500 uppercase tracking-widest mb-1">Payments Received</p>
<p className="font-mono text-lg font-black text-emerald-600 dark:text-[#39ff14]">{formatCurrency(totalReceived)}</p>
</div>
<div className="flex items-center gap-2 mt-2">
<div className="bg-zinc-50 dark:bg-black/30 rounded-xl px-4 py-3 border border-zinc-100 dark:border-white/5">
<p className="text-[10px] font-mono font-bold text-zinc-500 dark:text-zinc-500 uppercase tracking-widest mb-1"> Total Expenses</p>
<p className="font-mono text-lg font-black text-amber-600 dark:text-[#fda913]">{formatCurrency(profitMetrics.totalExpenses)}</p>
</div>
<div className={`rounded-xl px-4 py-3 border ${profitMetrics.netProfit >= 0 ? 'bg-emerald-50/60 dark:bg-[#39ff14]/5 border-emerald-200 dark:border-[#39ff14]/20' : 'bg-red-50/60 dark:bg-[#ff003c]/5 border-red-200 dark:border-[#ff003c]/20'}`}>
<p className="text-[10px] font-mono font-bold text-zinc-500 dark:text-zinc-500 uppercase tracking-widest mb-1">= Net Profit</p>
<p className={`font-mono text-lg font-black ${profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
{profitMetrics.netProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.netProfit)}
</p>
</div>
</div>
<div className="flex items-center gap-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'
@@ -963,9 +1136,11 @@ const OwnerProjectDetail = () => {
}`}>
{profitMetrics.netMargin >= 0 ? '+' : ''}{profitMetrics.netMargin.toFixed(1)}% margin
</span>
<span className="text-[10px] font-mono text-zinc-500 dark:text-zinc-500">
Outstanding: {formatCurrency(profitMetrics.outstandingExpenses)}
</span>
</div>
</NeoCard>
</div>
{/* Payments Received Table */}
<NeoCard spotlightColor="rgba(57, 255, 20, 0.1)" innerClassName="p-0 lg:p-0">