From d32b3ced213f995873eb579cf1c9c1d566f8166d Mon Sep 17 00:00:00 2001 From: Satyam-Rastogi Date: Mon, 11 May 2026 19:53:32 +0530 Subject: [PATCH] add commission section --- src/data/mockStore.jsx | 24 ++++ src/pages/owner/OwnerProjectDetail.jsx | 191 +++++++++++++++++++++---- 2 files changed, 184 insertions(+), 31 deletions(-) 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' && ( -
+
openTooltip && setOpenTooltip(null)}> {/* Financial Summary Row */} -
+
{formatCurrency(totalReceived)}
Total Received
@@ -927,16 +975,91 @@ const OwnerProjectDetail = () => {
{formatCurrency(profitMetrics.totalCosts)}
Total Costs
- -
{formatCurrency(profitMetrics.commission)}
-
Commission
-
{formatCurrency(profitMetrics.additionalExp)}
Addl. Expenses
+ {/* Commission Configuration Card */} + +
+
+ +

Commission

+
+
+ + {openTooltip === 'commission' && ( +
+

Commission Types

+
+

Flat: Fixed dollar amount

+

% of Gross: Total Received x %

+

% of Net: Gross Profit x %

+

Custom: Manual dollar entry

+
+
+ )} +
+
+ +
+ {/* Type Selector */} +
+ + +
+ + {/* Rate / Value Input */} +
+ +
+ + {activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? '%' : '$'} + + setCommissionConfig(prev => ({ ...(prev || activeCommissionConfig), type: activeCommissionConfig.type, rate: e.target.value }))} + placeholder={activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? 'e.g. 10' : 'e.g. 5000'} + className="w-full bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-xl pl-8 pr-4 py-2.5 text-sm text-zinc-900 dark:text-white placeholder-zinc-400 dark:placeholder-zinc-600 outline-none focus:border-amber-500 dark:focus:border-amber-500 focus:ring-1 focus:ring-amber-500/30 transition-all" + /> +
+
+ + {/* Calculated Result */} +
+ +
+ {formatCurrency(profitMetrics.commission)} +
+
+
+ + {/* Live Formula Preview */} +
+

+ Formula: + {profitMetrics.commissionFormula} +

+
+
+ {/* Profit Summary Cards */}
{/* Gross Profit */} @@ -949,16 +1072,19 @@ const OwnerProjectDetail = () => { = 0 ? NEON_GREEN : NEON_RED} />

Gross Profit

-
- -
-

Gross Profit Formula

-

Total Received − Total Actual Costs

-
-

{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)}

-

= {formatCurrency(profitMetrics.grossProfit)}

+
+ + {openTooltip === 'gross' && ( +
+

Gross Profit Formula

+

Total Received − Total Actual Costs

+
+

{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)} = {formatCurrency(profitMetrics.grossProfit)}

+
-
+ )}
= 0 ? NEON_GREEN : NEON_RED}`}> @@ -985,16 +1111,19 @@ const OwnerProjectDetail = () => { = 0 ? NEON_GREEN : NEON_RED} />

Net Profit

-
- -
-

Net Profit Formula

-

Gross Profit − Commission − Addl. Expenses

-
-

{formatCurrency(profitMetrics.grossProfit)} − {formatCurrency(profitMetrics.commission)} − {formatCurrency(profitMetrics.additionalExp)}

-

= {formatCurrency(profitMetrics.netProfit)}

+
+ + {openTooltip === 'net' && ( +
+

Net Profit Formula

+

Gross Profit − Commission − Addl. Expenses

+
+

{formatCurrency(profitMetrics.grossProfit)} − {formatCurrency(profitMetrics.commission)} − {formatCurrency(profitMetrics.additionalExp)} = {formatCurrency(profitMetrics.netProfit)}

+
-
+ )}
= 0 ? NEON_GREEN : NEON_RED}`}>