From 69d81c78d0c9dcb7035bd435511dba0f1afae72e Mon Sep 17 00:00:00 2001 From: Satyam-Rastogi Date: Thu, 14 May 2026 13:24:16 +0530 Subject: [PATCH] org level commission distribution for each member --- .../owner/CommissionDistributionPanel.jsx | 180 ++++++++++++++++++ src/pages/owner/OwnerSnapshot.jsx | 20 ++ 2 files changed, 200 insertions(+) create mode 100644 src/components/owner/CommissionDistributionPanel.jsx diff --git a/src/components/owner/CommissionDistributionPanel.jsx b/src/components/owner/CommissionDistributionPanel.jsx new file mode 100644 index 0000000..7a6441e --- /dev/null +++ b/src/components/owner/CommissionDistributionPanel.jsx @@ -0,0 +1,180 @@ +import React, { useMemo } from 'react'; +import { DollarSign, Users } from 'lucide-react'; + +const COMMISSION_TYPES = { + percent_gross: '% of Gross', + percent_net: '% of Net', + flat: 'Flat Amount', + custom: 'Manual', +}; + +const ROLE_COLORS = { + 'Project Manager': 'text-cyan-500 dark:text-cyan-400 bg-cyan-500/10 border-cyan-500/20', + 'Sales Rep': 'text-green-500 dark:text-green-400 bg-green-500/10 border-green-500/20', + 'Lead Installer': 'text-orange-500 dark:text-orange-400 bg-orange-500/10 border-orange-500/20', + 'Estimator': 'text-blue-500 dark:text-blue-400 bg-blue-500/10 border-blue-500/20', + 'Inspector': 'text-purple-500 dark:text-purple-400 bg-purple-500/10 border-purple-500/20', + 'Canvasser': 'text-teal-500 dark:text-teal-400 bg-teal-500/10 border-teal-500/20', + 'Subcontractor': 'text-rose-500 dark:text-rose-400 bg-rose-500/10 border-rose-500/20', + 'Supplier': 'text-yellow-500 dark:text-yellow-400 bg-yellow-500/10 border-yellow-500/20', +}; + +function resolveCommission(member, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults) { + if (member.jobCommission) { + return { type: member.jobCommission.type, rate: Number(member.jobCommission.rate) || 0 }; + } + const userOvr = (userCommissionOverrides || []).find(o => o.enabled && o.userId === member.userId); + if (userOvr) { + return { type: userOvr.type, rate: Number(userOvr.rate) || 0 }; + } + const orgMember = (orgMembers || []).find(m => m.userId === member.userId); + if (orgMember) { + const roleOvr = (roleCommissionOverrides || []).find(o => o.enabled && o.roleKey === orgMember.roleKey); + if (roleOvr) { + return { type: roleOvr.type, rate: Number(roleOvr.rate) || 0 }; + } + } + return { type: orgCommissionDefaults?.type, rate: Number(orgCommissionDefaults?.rate) || 0 }; +} + +function calcValue(comm, totalReceived, grossProfit) { + const r = Number(comm.rate) || 0; + switch (comm.type) { + case 'percent_gross': return totalReceived * (r / 100); + case 'percent_net': return grossProfit * (r / 100); + case 'flat': return r; + case 'custom': return r; + default: return 0; + } +} + +function getProjectFinancials(project) { + const paid = (project.paymentSchedule || []) + .filter(ps => ps.status === 'paid') + .reduce((s, ps) => s + (ps.amount || 0), 0); + const totalReceived = paid || (project.budget || 0); + const totalCosts = Number(project.actualCost ?? project.spent) || 0; + return { totalReceived, grossProfit: totalReceived - totalCosts }; +} + +const fmt = (amt) => + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0); + +const CommissionDistributionPanel = ({ + ownerProjects, + orgCommissionDefaults, + userCommissionOverrides, + roleCommissionOverrides, + orgMembers, +}) => { + const rows = useMemo(() => { + const entries = []; + for (const project of ownerProjects) { + const members = (project.teamMembers || []).filter(m => m.status === 'active'); + if (members.length === 0) continue; + const fin = getProjectFinancials(project); + for (const m of members) { + const resolved = resolveCommission(m, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults); + const amount = calcValue(resolved, fin.totalReceived, fin.grossProfit); + entries.push({ + key: `${project.id}_${m.id}`, + name: m.name, + role: m.jobRole, + amount, + projectId: project.id, + projectLabel: project.projectType, + commissionType: COMMISSION_TYPES[resolved.type] || resolved.type || '—', + }); + } + } + return entries; + }, [ownerProjects, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults]); + + const total = useMemo(() => rows.reduce((s, r) => s + r.amount, 0), [rows]); + + return ( +
+ {/* Header */} +
+ +

+ Commission Distribution +

+ + {rows.length} entr{rows.length === 1 ? 'y' : 'ies'} across {ownerProjects.length} project{ownerProjects.length !== 1 ? 's' : ''} + +
+ + {rows.length === 0 ? ( +
+ +

No commission recipients across your projects.

+

Add team members with commission rules to see the distribution.

+
+ ) : ( +
+ + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + ))} + + + + + + + +
NameRoleAmountProject IDCommission Type
+
+
+ {row.name?.[0] || '?'} +
+ {row.name} +
+
+ + {row.role} + + + + {fmt(row.amount)} + + + + {row.projectId} + + + + {row.commissionType} + +
+ Total Commission Amount + + + {fmt(total)} + + +
+
+ )} +
+ ); +}; + +export default CommissionDistributionPanel; diff --git a/src/pages/owner/OwnerSnapshot.jsx b/src/pages/owner/OwnerSnapshot.jsx index cf04d49..c9b1ac5 100644 --- a/src/pages/owner/OwnerSnapshot.jsx +++ b/src/pages/owner/OwnerSnapshot.jsx @@ -7,6 +7,7 @@ import UrgentItemsPanel from '../../components/owner/UrgentItemsPanel'; import FinancialDetailsModal from '../../components/owner/FinancialDetailsModal'; import ActionCenterModal from '../../components/owner/ActionCenterModal'; import TeamManagementPanel from '../../components/owner/TeamManagementPanel'; +import CommissionDistributionPanel from '../../components/owner/CommissionDistributionPanel'; import { SpotlightCard } from '../../components/SpotlightCard'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend @@ -384,6 +385,25 @@ const OwnerSnapshot = () => { )} + + {/* ── Owner's Box — Org-Wide Commission Distribution ── */} + {ownerProjects.length > 0 && ( +
+

+ + Organization Commission Overview +

+ + + +
+ )} {/* Interactive Modals */}