diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index f10e536..7e913b0 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -2011,6 +2011,8 @@ const MOCK_PROJECTS = [ spent: 22500, variancePercent: -50.0, margin: 0.40, + commissionType: 'percent_gross', + commissionRate: 10, commission: 4500, additionalExpenses: 1200, startDate: '2026-01-20', @@ -2086,6 +2088,8 @@ const MOCK_PROJECTS = [ spent: 8500, variancePercent: 0, margin: 0.35, + commissionType: 'flat', + commissionRate: 850, commission: 850, additionalExpenses: 350, startDate: '2025-11-15', @@ -2134,6 +2138,8 @@ const MOCK_PROJECTS = [ spent: 18400, variancePercent: -42.5, margin: 0.30, + commissionType: 'percent_net', + commissionRate: 15, commission: 3200, additionalExpenses: 900, startDate: '2026-01-06', @@ -2208,6 +2214,8 @@ const MOCK_PROJECTS = [ spent: 62300, variancePercent: 13.3, margin: -0.13, + commissionType: 'custom', + commissionRate: 5500, commission: 5500, additionalExpenses: 3800, startDate: '2025-12-01', @@ -2293,6 +2301,8 @@ const MOCK_PROJECTS = [ spent: 14200, variancePercent: -49.3, margin: 0.25, + commissionType: 'percent_gross', + commissionRate: 10, commission: 2800, additionalExpenses: 600, startDate: '2026-01-06', @@ -2365,6 +2375,8 @@ const MOCK_PROJECTS = [ spent: 5600, variancePercent: -69.7, margin: 0.38, + commissionType: 'flat', + commissionRate: 1850, commission: 1850, additionalExpenses: 450, startDate: '2026-02-03', @@ -2416,6 +2428,8 @@ const MOCK_PROJECTS = [ spent: 48000, variancePercent: -61.6, margin: 0.35, + commissionType: 'percent_gross', + commissionRate: 10, commission: 12500, additionalExpenses: 2800, startDate: '2026-01-27', @@ -2509,6 +2523,8 @@ const MOCK_PROJECTS = [ spent: 91500, variancePercent: 22.0, margin: -0.22, + commissionType: 'percent_net', + commissionRate: 12, commission: 7500, additionalExpenses: 4200, startDate: '2025-11-18', @@ -2558,6 +2574,8 @@ const MOCK_PROJECTS = [ budget: 42000, spent: 25800, margin: 0.28, + commissionType: 'percent_gross', + commissionRate: 10, commission: 4200, additionalExpenses: 1100, startDate: '2026-01-13', @@ -2605,6 +2623,8 @@ const MOCK_PROJECTS = [ budget: 35000, spent: 35000, margin: 0.30, + commissionType: 'custom', + commissionRate: 3500, commission: 3500, additionalExpenses: 800, startDate: '2025-09-15', @@ -2652,6 +2672,8 @@ const MOCK_PROJECTS = [ budget: 95000, spent: 68400, margin: 0.20, + commissionType: 'flat', + commissionRate: 9500, commission: 9500, additionalExpenses: 5200, startDate: '2025-12-08', @@ -2707,6 +2729,8 @@ const MOCK_PROJECTS = [ budget: 58000, spent: 22600, margin: 0.32, + commissionType: 'percent_gross', + commissionRate: 10, commission: 5800, additionalExpenses: 1500, startDate: '2026-01-13', diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx index 2f52522..f1af201 100644 --- a/src/pages/owner/OwnerProjectDetail.jsx +++ b/src/pages/owner/OwnerProjectDetail.jsx @@ -121,6 +121,16 @@ function seedPaymentsMock(projectId) { const PAYMENTS_PER_PAGE = 5; +const COMMISSION_TYPES = [ + { value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' }, + { value: 'percent_gross', label: '% of Gross', hint: 'Percentage of Total Received' }, + { value: 'percent_net', label: '% of Net', hint: 'Percentage of Gross Profit' }, + { value: 'custom', label: 'Custom / Manual', hint: 'Manually entered value' }, +]; + +const formatCurrency = (amt) => + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0); + const OwnerProjectDetail = () => { const { projectId } = useParams(); const { user } = useAuth(); @@ -136,6 +146,11 @@ const OwnerProjectDetail = () => { const [paymentSort, setPaymentSort] = useState({ field: 'date', dir: 'desc' }); const [paymentPage, setPaymentPage] = useState(0); + const [openTooltip, setOpenTooltip] = useState(null); + + // ── Commission config state ───────────────────────────────────────────── + const [commissionConfig, setCommissionConfig] = useState(null); + // ── Docs state ──────────────────────────────────────────────────────────── const [docs, setDocs] = useState(() => seedDocsMock(projectId)); const [viewDoc, setViewDoc] = useState(null); // doc object being viewed @@ -220,17 +235,53 @@ const OwnerProjectDetail = () => { projects.find(p => p.id === projectId && p.ownerId === user?.id) , [projects, projectId, user]); + const activeCommissionConfig = useMemo(() => { + if (commissionConfig) return commissionConfig; + if (!project) return { type: 'flat', rate: 0 }; + return { + type: project.commissionType || 'flat', + rate: Number(project.commissionRate) || 0, + }; + }, [commissionConfig, project]); + const profitMetrics = useMemo(() => { - if (!project) return { totalCosts: 0, commission: 0, additionalExp: 0, grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 }; + if (!project) return { totalCosts: 0, commission: 0, commissionFormula: '', 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 { type, rate } = activeCommissionConfig; + const safeRate = Number(rate) || 0; + let commission = 0; + let commissionFormula = ''; + switch (type) { + case 'flat': + commission = safeRate; + commissionFormula = `Flat Amount: ${formatCurrency(safeRate)}`; + break; + case 'percent_gross': + commission = totalReceived * (safeRate / 100); + commissionFormula = `${safeRate}% of Total Received (${formatCurrency(totalReceived)})`; + break; + case 'percent_net': + commission = gross * (safeRate / 100); + commissionFormula = `${safeRate}% of Gross Profit (${formatCurrency(gross)})`; + break; + case 'custom': + commission = safeRate; + commissionFormula = `Manual Entry: ${formatCurrency(safeRate)}`; + break; + default: + commission = safeRate; + commissionFormula = `${formatCurrency(safeRate)}`; + } + commission = Number.isFinite(commission) ? commission : 0; + 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]); + return { totalCosts, commission, commissionFormula, additionalExp, grossProfit: gross, netProfit: net, grossMargin, netMargin }; + }, [project, totalReceived, activeCommissionConfig]); if (!project) { return ( @@ -252,9 +303,6 @@ const OwnerProjectDetail = () => { const contractor = vendors.find(v => v.id === project.contractorId) || null; - const formatCurrency = (amt) => - new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amt); - const getHealthColor = (s) => s >= 70 ? 'text-emerald-500' : s >= 40 ? 'text-amber-500' : 'text-red-500'; const getMilestoneColor = (status) => { @@ -916,9 +964,9 @@ const OwnerProjectDetail = () => { {/* PAYMENTS TAB */} {activeTab === 'payments' && ( -
Commission Types
+Flat: Fixed dollar amount
+% of Gross: Total Received x %
+% of Net: Gross Profit x %
+Custom: Manual dollar entry
++ Formula: + {profitMetrics.commissionFormula} +
+Gross Profit Formula
-Total Received − Total Actual Costs
-{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)}
-= {formatCurrency(profitMetrics.grossProfit)}
+Gross Profit Formula
+Total Received − Total Actual Costs
+{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)} = {formatCurrency(profitMetrics.grossProfit)}
+Net Profit Formula
-Gross Profit − Commission − Addl. Expenses
-{formatCurrency(profitMetrics.grossProfit)} − {formatCurrency(profitMetrics.commission)} − {formatCurrency(profitMetrics.additionalExp)}
-= {formatCurrency(profitMetrics.netProfit)}
+Net Profit Formula
+Gross Profit − Commission − Addl. Expenses
+{formatCurrency(profitMetrics.grossProfit)} − {formatCurrency(profitMetrics.commission)} − {formatCurrency(profitMetrics.additionalExp)} = {formatCurrency(profitMetrics.netProfit)}
+