diff --git a/src/components/owner/CommissionSettingsModal.jsx b/src/components/owner/CommissionSettingsModal.jsx index 8253e84..8239c7c 100644 --- a/src/components/owner/CommissionSettingsModal.jsx +++ b/src/components/owner/CommissionSettingsModal.jsx @@ -15,6 +15,7 @@ const COMMISSION_TYPES = [ const LEVEL_CONFIG = { org: { label: 'Org Default', icon: Building2, color: '#00f0ff', bg: 'rgba(0,240,255,0.1)', border: 'rgba(0,240,255,0.2)' }, + role: { label: 'Role Override', icon: Shield, color: '#10b981', bg: 'rgba(16,185,129,0.1)', border: 'rgba(16,185,129,0.2)' }, user: { label: 'User Override', icon: User, color: '#a855f7', bg: 'rgba(168,85,247,0.1)', border: 'rgba(168,85,247,0.2)' }, job: { label: 'Job Override', icon: Briefcase, color: '#fda913', bg: 'rgba(253,169,19,0.1)', border: 'rgba(253,169,19,0.2)' }, }; @@ -40,6 +41,7 @@ const SectionHeader = ({ level, isOpen, onToggle }) => {

{cfg.label}

{level === 'org' && 'Default commission for all jobs'} + {level === 'role' && 'Per-role commission overrides'} {level === 'user' && 'Per-user commission overrides'} {level === 'job' && 'Override commission for this specific job'}

@@ -96,6 +98,9 @@ const CommissionSettingsModal = ({ onClose, orgDefaults, userOverrides, + roleOverrides = [], + orgRoles = [], + assignedUserRoleKey, jobOverride, activeSource, personnel, @@ -106,7 +111,7 @@ const CommissionSettingsModal = ({ onClearJobOverride, userRole, }) => { - const [openSections, setOpenSections] = useState({ org: true, user: false, job: true }); + const [openSections, setOpenSections] = useState({ org: true, role: false, user: false, job: true }); const [localOrgDefaults, setLocalOrgDefaults] = useState({ type: 'percent_gross', rate: 10 }); const [localJobOverride, setLocalJobOverride] = useState({ enabled: false, type: 'flat', rate: 0 }); const [localUserOverrides, setLocalUserOverrides] = useState([]); @@ -192,7 +197,7 @@ const CommissionSettingsModal = ({

Commission Settings

-

Configure commission hierarchy: Job → User → Org

+

Configure commission hierarchy: Job → User → Role → Org

)} diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index d08a748..74a6727 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -5274,6 +5274,24 @@ export const MockStoreProvider = ({ children }) => { updateProject: (projectId, data) => { setProjects(prev => prev.map(p => p.id === projectId ? { ...p, ...data } : p)); }, + addJobTeamMember: (projectId, member) => { + setProjects(prev => prev.map(p => p.id === projectId + ? { ...p, teamMembers: [...(p.teamMembers || []), { ...member, id: `jtm_${Date.now()}` }] } + : p + )); + }, + removeJobTeamMember: (projectId, memberId) => { + setProjects(prev => prev.map(p => p.id === projectId + ? { ...p, teamMembers: (p.teamMembers || []).filter(m => m.id !== memberId) } + : p + )); + }, + updateJobTeamMember: (projectId, memberId, data) => { + setProjects(prev => prev.map(p => p.id === projectId + ? { ...p, teamMembers: (p.teamMembers || []).map(m => m.id === memberId ? { ...m, ...data } : m) } + : p + )); + }, // Org Settings & Access Control orgSettings, diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx index 62e6e67..f89007c 100644 --- a/src/pages/owner/OwnerProjectDetail.jsx +++ b/src/pages/owner/OwnerProjectDetail.jsx @@ -19,6 +19,7 @@ import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal'; import CreateItemModal from '../../components/owner/CreateItemModal'; import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal'; import { usePermissions } from '../../hooks/usePermissions'; +import { resolveCommission, findAssignedUserRoleKey } from '../../utils/commissionResolver'; // --- NEOMORPHIC STYLING CONSTANTS --- const NEO_PANEL_CLASS = "dark:bg-zinc-900/60 dark:backdrop-blur-3xl border border-zinc-200 dark:border-white/5 dark:shadow-[8px_8px_16px_rgba(0,0,0,0.6),-8px_-8px_16px_rgba(255,255,255,0.02)] rounded-3xl relative overflow-hidden transition-all duration-300"; @@ -138,6 +139,7 @@ const OwnerProjectDetail = () => { const { projects, vendors, personnel, orgCommissionDefaults, userCommissionOverrides, + roleCommissionOverrides, orgMembers, orgRoles, updateOrgCommissionDefaults, setUserCommissionOverride, removeUserCommissionOverride, updateProject, } = useMockStore(); @@ -243,7 +245,7 @@ const OwnerProjectDetail = () => { projects.find(p => p.id === projectId && p.ownerId === user?.id) , [projects, projectId, user]); - // ── Commission hierarchy resolution: Job → User → Org ────────────────── + // ── Commission hierarchy: Job → Individual → Role → Org ────────────── const jobCommissionOverride = useMemo(() => { if (commissionConfig) return commissionConfig; if (!project) return null; @@ -251,17 +253,20 @@ const OwnerProjectDetail = () => { return null; }, [commissionConfig, project]); - const resolvedCommission = useMemo(() => { - // Priority: Job Override → User Override → Org Default - if (jobCommissionOverride) { - return { source: 'job', type: jobCommissionOverride.type, rate: Number(jobCommissionOverride.rate) || 0 }; - } - const userOverride = userCommissionOverrides.find(o => o.enabled && o.userId === (project?.salesRepId || project?.contractorId)); - if (userOverride) { - return { source: 'user', type: userOverride.type, rate: Number(userOverride.rate) || 0 }; - } - return { source: 'org', type: orgCommissionDefaults.type, rate: Number(orgCommissionDefaults.rate) || 0 }; - }, [jobCommissionOverride, userCommissionOverrides, orgCommissionDefaults, project]); + const assignedUserId = project?.salesRepId || project?.contractorId; + const assignedUserRoleKey = useMemo( + () => findAssignedUserRoleKey(assignedUserId, orgMembers), + [assignedUserId, orgMembers] + ); + + const resolvedCommission = useMemo(() => resolveCommission({ + jobOverride: jobCommissionOverride, + userId: assignedUserId, + userCommissionOverrides, + orgMembers, + roleCommissionOverrides, + orgCommissionDefaults, + }), [jobCommissionOverride, assignedUserId, userCommissionOverrides, orgMembers, roleCommissionOverrides, orgCommissionDefaults]); const activeCommissionConfig = resolvedCommission; @@ -776,6 +781,7 @@ const OwnerProjectDetail = () => { 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' }, + role: { icon: Shield, label: 'Role Override', color: 'text-emerald-500 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-500/10', border: 'border-emerald-200 dark:border-emerald-500/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' }, }; @@ -1845,6 +1851,9 @@ const OwnerProjectDetail = () => { onClose={() => setCommissionSettingsOpen(false)} orgDefaults={orgCommissionDefaults} userOverrides={userCommissionOverrides} + roleOverrides={roleCommissionOverrides} + orgRoles={orgRoles} + assignedUserRoleKey={assignedUserRoleKey} jobOverride={project?.commissionOverride || (commissionConfig ? { type: commissionConfig.type, rate: commissionConfig.rate } : null)} activeSource={resolvedCommission.source} personnel={personnel} diff --git a/src/pages/owner/OwnerSnapshot.jsx b/src/pages/owner/OwnerSnapshot.jsx index a8c4e3c..cf04d49 100644 --- a/src/pages/owner/OwnerSnapshot.jsx +++ b/src/pages/owner/OwnerSnapshot.jsx @@ -21,6 +21,7 @@ const OwnerSnapshot = () => { const { owners, projects, users, personnel, orgCommissionDefaults, userCommissionOverrides, + roleCommissionOverrides, orgMembers, addJobTeamMember, removeJobTeamMember, updateJobTeamMember, } = useMockStore(); @@ -372,6 +373,8 @@ const OwnerSnapshot = () => { userRole={user?.role} orgCommissionDefaults={orgCommissionDefaults} userCommissionOverrides={userCommissionOverrides} + roleCommissionOverrides={roleCommissionOverrides} + orgMembers={orgMembers} totalReceived={teamFinancials.totalReceived} grossProfit={teamFinancials.grossProfit} onAddMember={addJobTeamMember} diff --git a/src/utils/commissionResolver.js b/src/utils/commissionResolver.js new file mode 100644 index 0000000..a359460 --- /dev/null +++ b/src/utils/commissionResolver.js @@ -0,0 +1,45 @@ +/** + * Centralized commission resolution — 4-level hierarchy: + * Job Override → Individual Override → Role Override → Org Default + */ +export function resolveCommission({ + jobOverride, + userId, + userCommissionOverrides = [], + orgMembers = [], + roleCommissionOverrides = [], + orgCommissionDefaults = {}, +}) { + // Level 1: Job Override (highest priority) + if (jobOverride) { + return { source: 'job', type: jobOverride.type, rate: Number(jobOverride.rate) || 0 }; + } + + // Level 2: Individual User Override + if (userId) { + const userOverride = userCommissionOverrides.find(o => o.enabled && o.userId === userId); + if (userOverride) { + return { source: 'user', type: userOverride.type, rate: Number(userOverride.rate) || 0 }; + } + } + + // Level 3: Role Override + if (userId) { + const member = orgMembers.find(m => m.userId === userId); + if (member) { + const roleOverride = roleCommissionOverrides.find(o => o.enabled && o.roleKey === member.roleKey); + if (roleOverride) { + return { source: 'role', type: roleOverride.type, rate: Number(roleOverride.rate) || 0, roleKey: member.roleKey }; + } + } + } + + // Level 4: Org Default (lowest priority) + return { source: 'org', type: orgCommissionDefaults.type, rate: Number(orgCommissionDefaults.rate) || 0 }; +} + +export function findAssignedUserRoleKey(userId, orgMembers = []) { + if (!userId) return null; + const member = orgMembers.find(m => m.userId === userId); + return member?.roleKey || null; +}