diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index a1b0a5d..cf3fa90 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -2014,7 +2014,6 @@ const MOCK_PROJECTS = [ commissionType: 'percent_gross', commissionRate: 10, commission: 4500, - additionalExpenses: 1200, startDate: '2026-01-20', endDate: '2026-03-05', completionPercentage: 55, @@ -2091,7 +2090,6 @@ const MOCK_PROJECTS = [ commissionType: 'flat', commissionRate: 850, commission: 850, - additionalExpenses: 350, startDate: '2025-11-15', endDate: '2025-12-10', completionPercentage: 100, @@ -2141,7 +2139,6 @@ const MOCK_PROJECTS = [ commissionType: 'percent_net', commissionRate: 15, commission: 3200, - additionalExpenses: 900, startDate: '2026-01-06', endDate: '2026-02-28', completionPercentage: 42, @@ -2217,7 +2214,6 @@ const MOCK_PROJECTS = [ commissionType: 'custom', commissionRate: 5500, commission: 5500, - additionalExpenses: 3800, startDate: '2025-12-01', endDate: '2026-02-28', completionPercentage: 78, @@ -2304,7 +2300,6 @@ const MOCK_PROJECTS = [ commissionType: 'percent_gross', commissionRate: 10, commission: 2800, - additionalExpenses: 600, startDate: '2026-01-06', endDate: '2026-02-20', completionPercentage: 38, @@ -2378,7 +2373,6 @@ const MOCK_PROJECTS = [ commissionType: 'flat', commissionRate: 1850, commission: 1850, - additionalExpenses: 450, startDate: '2026-02-03', endDate: '2026-03-15', completionPercentage: 22, @@ -2431,7 +2425,6 @@ const MOCK_PROJECTS = [ commissionType: 'percent_gross', commissionRate: 10, commission: 12500, - additionalExpenses: 2800, startDate: '2026-01-27', endDate: '2026-04-15', completionPercentage: 32, @@ -2526,7 +2519,6 @@ const MOCK_PROJECTS = [ commissionType: 'percent_net', commissionRate: 12, commission: 7500, - additionalExpenses: 4200, startDate: '2025-11-18', endDate: '2026-02-28', completionPercentage: 88, @@ -2577,7 +2569,6 @@ const MOCK_PROJECTS = [ commissionType: 'percent_gross', commissionRate: 10, commission: 4200, - additionalExpenses: 1100, startDate: '2026-01-13', endDate: '2026-03-10', completionPercentage: 50, @@ -2626,7 +2617,6 @@ const MOCK_PROJECTS = [ commissionType: 'custom', commissionRate: 3500, commission: 3500, - additionalExpenses: 800, startDate: '2025-09-15', endDate: '2025-10-28', completionPercentage: 100, @@ -2675,7 +2665,6 @@ const MOCK_PROJECTS = [ commissionType: 'flat', commissionRate: 9500, commission: 9500, - additionalExpenses: 5200, startDate: '2025-12-08', endDate: '2026-03-20', completionPercentage: 62, @@ -2732,7 +2721,6 @@ const MOCK_PROJECTS = [ commissionType: 'percent_gross', commissionRate: 10, commission: 5800, - additionalExpenses: 1500, startDate: '2026-01-13', endDate: '2026-04-01', completionPercentage: 28, diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx index 8ae540a..23a8842 100644 --- a/src/pages/owner/OwnerProjectDetail.jsx +++ b/src/pages/owner/OwnerProjectDetail.jsx @@ -68,7 +68,6 @@ const tabs = [ { id: 'rfis', label: 'RFIs', icon: FileText }, { id: 'milestones', label: 'Milestones', icon: Milestone }, { id: 'invoices', label: 'Invoices', icon: DollarSign }, - { id: 'payments', label: 'Payments', icon: Banknote }, { id: 'risks', label: 'Risk & Issues', icon: AlertCircle }, { id: 'activity', label: 'Activity', icon: Clock }, { id: 'docs', label: 'Docs', icon: FolderOpen }, @@ -265,9 +264,8 @@ const OwnerProjectDetail = () => { const activeCommissionConfig = resolvedCommission; const profitMetrics = useMemo(() => { - if (!project) return { totalCosts: 0, commission: 0, commissionFormula: '', additionalExp: 0, grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 }; + if (!project) return { totalCosts: 0, commission: 0, commissionFormula: '', grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 }; const totalCosts = Number(project.actualCost ?? project.spent) || 0; - const additionalExp = Number(project.additionalExpenses) || 0; const gross = totalReceived - totalCosts; const { type, rate } = activeCommissionConfig; @@ -297,10 +295,10 @@ const OwnerProjectDetail = () => { } commission = Number.isFinite(commission) ? commission : 0; - const net = gross - commission - additionalExp; + const net = gross - commission; const grossMargin = totalReceived > 0 ? (gross / totalReceived) * 100 : 0; const netMargin = totalReceived > 0 ? (net / totalReceived) * 100 : 0; - return { totalCosts, commission, commissionFormula, additionalExp, grossProfit: gross, netProfit: net, grossMargin, netMargin }; + return { totalCosts, commission, commissionFormula, grossProfit: gross, netProfit: net, grossMargin, netMargin }; }, [project, totalReceived, activeCommissionConfig]); if (!project) { @@ -736,6 +734,23 @@ const OwnerProjectDetail = () => { ); })} + + + + Totals + + {formatCurrency(totalReceived)} + {formatCurrency(profitMetrics.totalCosts)} + {(() => { + const totalVariance = profitMetrics.totalCosts - totalReceived; + return ( + 0 ? 'text-[#ff4500]' : 'text-[#39ff14]'}`}> + {totalVariance > 0 ? '+' : ''}{formatCurrency(totalVariance)} + + ); + })()} + + @@ -747,6 +762,329 @@ const OwnerProjectDetail = () => {

Budget: {formatCurrency(project.approvedBudget || project.budget)}  ·  Spent: {formatCurrency(project.actualCost || project.spent)}

)} + + {/* Commission Configuration Card */} + +
openTooltip && setOpenTooltip(null)}> +
+ +

Commission

+ {/* Source Badge */} + {(() => { + const src = resolvedCommission.source; + const badgeCfg = { + org: { icon: Building2, label: 'Org Default', color: 'text-sky-500 dark:text-[#00f0ff]', bg: 'bg-sky-50 dark:bg-[#00f0ff]/10', border: 'border-sky-200 dark:border-[#00f0ff]/20' }, + user: { icon: UserIcon, label: 'User Override', color: 'text-purple-500 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20' }, + job: { icon: Briefcase, label: 'Job Override', color: 'text-amber-600 dark:text-[#fda913]', bg: 'bg-amber-50 dark:bg-[#fda913]/10', border: 'border-amber-200 dark:border-[#fda913]/20' }, + }; + const b = badgeCfg[src]; + const BadgeIcon = b.icon; + return ( + + + {b.label} + + ); + })()} +
+
+ +
+ + {openTooltip === 'commission' && ( +
+

Commission Hierarchy

+
+

Job Override: Highest priority — set per job

+

User Override: Per-user commission settings

+

Org Default: Organization-wide fallback

+
+
+

Priority: Job → User → Org

+
+
+ )} +
+
+
+ +
+
+ + +
+
+ +
+ + {activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? '%' : '$'} + + setCommissionConfig(prev => ({ ...(prev || { type: activeCommissionConfig.type, rate: activeCommissionConfig.rate }), 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" + /> +
+
+
+ +
+ {formatCurrency(profitMetrics.commission)} +
+
+
+ +
+

+ Formula: + {profitMetrics.commissionFormula} +

+
+
+ + {/* Profit Summary Cards */} +
+ = 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"} + innerClassName="!p-5" + > +
+
+ = 0 ? NEON_GREEN : NEON_RED} /> +

Gross Profit

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

Gross Profit Formula

+

Total Received − Total Actual Costs

+
+

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

+
+
+ )} +
+
+
= 0 ? NEON_GREEN : NEON_RED}`}> + {profitMetrics.grossProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.grossProfit)} +
+
+ = 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 + +
+
+ + = 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"} + innerClassName="!p-5" + > +
+
+ = 0 ? NEON_GREEN : NEON_RED} /> +

Net Profit

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

Net Profit Formula

+

Gross Profit − Commission

+
+

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

+
+
+ )} +
+
+
= 0 ? NEON_GREEN : NEON_RED}`}> + {profitMetrics.netProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.netProfit)} +
+
+ = 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 + +
+
+
+ + {/* Payments Received Table */} + +
+
+ +

Payments Received

+ + {payments.length} + +
+ +
+ + {payments.length > 0 ? ( + <> +
+ + + + + + + + + + + + {paginatedPayments.map((pay) => ( + + + + + + + + ))} + +
From / Method togglePaymentSort('amount')}> + Amount {paymentSort.field === 'amount' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''} + togglePaymentSort('date')}> + Date {paymentSort.field === 'date' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''} + Check No. / RefMemo
+
{pay.from}
+ + {pay.method} + +
+ {formatCurrency(pay.amount)} + {pay.date}{pay.refNumber || '—'}{pay.memo || '—'}
+
+ +
+ {paginatedPayments.map((pay) => ( +
+
+
+

{pay.from}

+ + {pay.method} + +
+ {formatCurrency(pay.amount)} +
+
+
+ Date +

{pay.date}

+
+
+ Ref # +

{pay.refNumber || '—'}

+
+
+ {pay.memo && ( +

{pay.memo}

+ )} +
+ ))} +
+ + {totalPaymentPages > 1 && ( +
+

+ {paymentPage * PAYMENTS_PER_PAGE + 1}–{Math.min((paymentPage + 1) * PAYMENTS_PER_PAGE, sortedPayments.length)} of {sortedPayments.length} +

+
+ + +
+
+ )} + + ) : ( +
+ +

No payments recorded yet.

+

Record your first payment to start tracking.

+
+ )} +
)} @@ -982,367 +1320,6 @@ const OwnerProjectDetail = () => { )} - {/* PAYMENTS TAB */} - {activeTab === 'payments' && ( -
openTooltip && setOpenTooltip(null)}> - {/* Financial Summary Row */} -
- -
{formatCurrency(totalReceived)}
-
Total Received
-
- -
{formatCurrency(profitMetrics.totalCosts)}
-
Total Costs
-
- -
{formatCurrency(profitMetrics.additionalExp)}
-
Addl. Expenses
-
-
- - {/* Commission Configuration Card */} - -
-
- -

Commission

- {/* Source Badge */} - {(() => { - const src = resolvedCommission.source; - const badgeCfg = { - org: { icon: Building2, label: 'Org Default', color: 'text-sky-500 dark:text-[#00f0ff]', bg: 'bg-sky-50 dark:bg-[#00f0ff]/10', border: 'border-sky-200 dark:border-[#00f0ff]/20' }, - user: { icon: UserIcon, label: 'User Override', color: 'text-purple-500 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20' }, - job: { icon: Briefcase, label: 'Job Override', color: 'text-amber-600 dark:text-[#fda913]', bg: 'bg-amber-50 dark:bg-[#fda913]/10', border: 'border-amber-200 dark:border-[#fda913]/20' }, - }; - const b = badgeCfg[src]; - const BadgeIcon = b.icon; - return ( - - - {b.label} - - ); - })()} -
-
- -
- - {openTooltip === 'commission' && ( -
-

Commission Hierarchy

-
-

Job Override: Highest priority — set per job

-

User Override: Per-user commission settings

-

Org Default: Organization-wide fallback

-
-
-

Priority: Job → User → Org

-
-
- )} -
-
-
- -
- {/* Type Selector */} -
- - -
- - {/* Rate / Value Input */} -
- -
- - {activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? '%' : '$'} - - setCommissionConfig(prev => ({ ...(prev || { type: activeCommissionConfig.type, rate: activeCommissionConfig.rate }), 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 */} - = 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"} - innerClassName="!p-5" - > -
-
- = 0 ? NEON_GREEN : NEON_RED} /> -

Gross Profit

-
-
- - {openTooltip === 'gross' && ( -
-

Gross Profit Formula

-

Total Received − Total Actual Costs

-
-

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

-
-
- )} -
-
-
= 0 ? NEON_GREEN : NEON_RED}`}> - {profitMetrics.grossProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.grossProfit)} -
-
- = 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 - -
-
- - {/* Net Profit */} - = 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"} - innerClassName="!p-5" - > -
-
- = 0 ? NEON_GREEN : NEON_RED} /> -

Net Profit

-
-
- - {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}`}> - {profitMetrics.netProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.netProfit)} -
-
- = 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 - -
-
-
- - {/* Payments Table */} - -
-
- -

Payments Received

- - {payments.length} - -
- -
- - {payments.length > 0 ? ( - <> - {/* Desktop Table */} -
- - - - - - - - - - - - {paginatedPayments.map((pay) => ( - - - - - - - - ))} - -
From / Method togglePaymentSort('amount')} - > - Amount {paymentSort.field === 'amount' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''} - togglePaymentSort('date')} - > - Date {paymentSort.field === 'date' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''} - Check No. / RefMemo
-
{pay.from}
- - {pay.method} - -
- {formatCurrency(pay.amount)} - {pay.date}{pay.refNumber || '—'}{pay.memo || '—'}
-
- - {/* Mobile Card View */} -
- {paginatedPayments.map((pay) => ( -
-
-
-

{pay.from}

- - {pay.method} - -
- {formatCurrency(pay.amount)} -
-
-
- Date -

{pay.date}

-
-
- Ref # -

{pay.refNumber || '—'}

-
-
- {pay.memo && ( -

{pay.memo}

- )} -
- ))} -
- - {/* Pagination */} - {totalPaymentPages > 1 && ( -
-

- {paymentPage * PAYMENTS_PER_PAGE + 1}–{Math.min((paymentPage + 1) * PAYMENTS_PER_PAGE, sortedPayments.length)} of {sortedPayments.length} -

-
- - -
-
- )} - - ) : ( -
- -

No payments recorded yet.

-

Record your first payment to start tracking.

-
- )} -
-
- )} - {/* RISK & ISSUES TAB */} {activeTab === 'risks' && (