diff --git a/src/components/owner/CommissionSettingsModal.jsx b/src/components/owner/CommissionSettingsModal.jsx
new file mode 100644
index 0000000..8253e84
--- /dev/null
+++ b/src/components/owner/CommissionSettingsModal.jsx
@@ -0,0 +1,499 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { createPortal } from 'react-dom';
+import {
+ X, DollarSign, Building2, User, Briefcase, ChevronDown,
+ Plus, Trash2, ToggleLeft, ToggleRight, Info, Shield, ArrowRight
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+
+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 LEVEL_CONFIG = {
+ org: { label: 'Org Default', icon: Building2, color: '#00f0ff', bg: 'rgba(0,240,255,0.1)', border: 'rgba(0,240,255,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)' },
+};
+
+const formatCurrency = (amt) =>
+ new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0);
+
+const SectionHeader = ({ level, isOpen, onToggle }) => {
+ const cfg = LEVEL_CONFIG[level];
+ const Icon = cfg.icon;
+ return (
+
+ );
+};
+
+const CommissionForm = ({ type, rate, onTypeChange, onRateChange, accentColor = '#fda913' }) => (
+
+
+
+
+
+
+
+
+
+ {type === 'percent_gross' || type === 'percent_net' ? '%' : '$'}
+
+ onRateChange(e.target.value)}
+ placeholder={type === 'percent_gross' || type === 'percent_net' ? 'e.g. 10' : 'e.g. 5000'}
+ className="w-full bg-[#18181b] border border-white/10 rounded-xl pl-8 pr-4 py-2.5 text-sm text-white placeholder-zinc-600 outline-none transition-all"
+ />
+
+
+
+);
+
+const CommissionSettingsModal = ({
+ isOpen,
+ onClose,
+ orgDefaults,
+ userOverrides,
+ jobOverride,
+ activeSource,
+ personnel,
+ onSaveOrgDefaults,
+ onSaveUserOverride,
+ onRemoveUserOverride,
+ onSaveJobOverride,
+ onClearJobOverride,
+ userRole,
+}) => {
+ const [openSections, setOpenSections] = useState({ org: true, 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([]);
+ const [addingUser, setAddingUser] = useState(false);
+ const [newUserOverride, setNewUserOverride] = useState({ userId: '', type: 'flat', rate: 0 });
+
+ useEffect(() => {
+ if (isOpen) {
+ setLocalOrgDefaults({ ...orgDefaults });
+ setLocalUserOverrides(userOverrides.map(o => ({ ...o })));
+ setLocalJobOverride(
+ jobOverride
+ ? { enabled: true, ...jobOverride }
+ : { enabled: false, type: orgDefaults.type, rate: 0 }
+ );
+ setAddingUser(false);
+ setNewUserOverride({ userId: '', type: 'flat', rate: 0 });
+ }
+ }, [isOpen, orgDefaults, userOverrides, jobOverride]);
+
+ useEffect(() => {
+ const handleEsc = (e) => { if (e.key === 'Escape') onClose(); };
+ if (isOpen) window.addEventListener('keydown', handleEsc);
+ return () => window.removeEventListener('keydown', handleEsc);
+ }, [isOpen, onClose]);
+
+ const availablePersonnel = useMemo(() => {
+ const overriddenIds = new Set(localUserOverrides.map(o => o.userId));
+ return (personnel || []).filter(p => !overriddenIds.has(p.id));
+ }, [personnel, localUserOverrides]);
+
+ const isAdmin = userRole === 'OWNER' || userRole === 'ADMIN';
+
+ const toggleSection = (key) => setOpenSections(prev => ({ ...prev, [key]: !prev[key] }));
+
+ const handleSave = () => {
+ if (isAdmin) {
+ onSaveOrgDefaults(localOrgDefaults);
+ localUserOverrides.forEach(o => onSaveUserOverride(o));
+ const removedIds = userOverrides
+ .filter(o => !localUserOverrides.find(lo => lo.userId === o.userId))
+ .map(o => o.userId);
+ removedIds.forEach(id => onRemoveUserOverride(id));
+ }
+
+ if (localJobOverride.enabled) {
+ onSaveJobOverride({ type: localJobOverride.type, rate: Number(localJobOverride.rate) || 0 });
+ } else {
+ onClearJobOverride();
+ }
+ onClose();
+ };
+
+ const handleAddUserOverride = () => {
+ if (!newUserOverride.userId) return;
+ const person = (personnel || []).find(p => p.id === newUserOverride.userId);
+ if (!person) return;
+ setLocalUserOverrides(prev => [
+ ...prev,
+ { userId: person.id, userName: person.name, enabled: true, type: newUserOverride.type, rate: Number(newUserOverride.rate) || 0 }
+ ]);
+ setAddingUser(false);
+ setNewUserOverride({ userId: '', type: 'flat', rate: 0 });
+ };
+
+ if (!isOpen) return null;
+
+ return createPortal(
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+
Commission Settings
+
Configure commission hierarchy: Job → User → Org
+
+
+
+
+
+ {/* Override Priority Visualization */}
+
+
+ {['org', 'user', 'job'].map((level, i) => {
+ const cfg = LEVEL_CONFIG[level];
+ const Icon = cfg.icon;
+ const isActive = activeSource === level;
+ return (
+
+ {i > 0 && }
+
+
+ {cfg.label}
+ {isActive && (
+
+ )}
+
+
+ );
+ })}
+
+
+ Currently using:
+ {LEVEL_CONFIG[activeSource]?.label || 'Org Default'}
+
+
+
+
+ {/* Scrollable Content */}
+
+
+ {/* ── ORG DEFAULTS ── */}
+
+
toggleSection('org')} />
+
+ {openSections.org && (
+
+
+ {isAdmin ? (
+
setLocalOrgDefaults(prev => ({ ...prev, type: v, rate: '' }))}
+ onRateChange={v => setLocalOrgDefaults(prev => ({ ...prev, rate: v }))}
+ accentColor="#00f0ff"
+ />
+ ) : (
+
+
+
+ {COMMISSION_TYPES.find(c => c.value === orgDefaults.type)?.label} — {
+ orgDefaults.type === 'percent_gross' || orgDefaults.type === 'percent_net'
+ ? `${orgDefaults.rate}%`
+ : formatCurrency(orgDefaults.rate)
+ }
+
+ (Admin only)
+
+ )}
+
+
+ )}
+
+
+
+ {/* ── USER OVERRIDES ── */}
+
+
toggleSection('user')} />
+
+ {openSections.user && (
+
+
+ {!isAdmin ? (
+
+
+ {localUserOverrides.length} user override(s) configured
+ (Admin only)
+
+ ) : (
+ <>
+ {localUserOverrides.length === 0 && !addingUser && (
+
No user overrides configured.
+ )}
+
+ {localUserOverrides.map((ov, idx) => (
+
+
+
+
+ {ov.userName}
+
+
+
+
+
+
+ {ov.enabled && (
+
setLocalUserOverrides(prev =>
+ prev.map((o, i) => i === idx ? { ...o, type: v, rate: '' } : o)
+ )}
+ onRateChange={v => setLocalUserOverrides(prev =>
+ prev.map((o, i) => i === idx ? { ...o, rate: v } : o)
+ )}
+ accentColor="#a855f7"
+ />
+ )}
+ {!ov.enabled && (
+ Override disabled — org default applies
+ )}
+
+ ))}
+
+ {/* Add New User Override */}
+ {addingUser ? (
+
+
+
+
+
+
setNewUserOverride(prev => ({ ...prev, type: v, rate: '' }))}
+ onRateChange={v => setNewUserOverride(prev => ({ ...prev, rate: v }))}
+ accentColor="#a855f7"
+ />
+
+
+
+
+
+ ) : (
+
+ )}
+ >
+ )}
+
+
+ )}
+
+
+
+ {/* ── JOB OVERRIDE ── */}
+
+
toggleSection('job')} />
+
+ {openSections.job && (
+
+
+
+ Enable job-level override
+
+
+ {localJobOverride.enabled ? (
+
setLocalJobOverride(prev => ({ ...prev, type: v, rate: '' }))}
+ onRateChange={v => setLocalJobOverride(prev => ({ ...prev, rate: v }))}
+ accentColor="#fda913"
+ />
+ ) : (
+
+ No job override — falls back to {activeSource === 'user' ? 'user override' : 'org default'}
+
+ )}
+
+
+ )}
+
+
+
+ {/* Hierarchy Info */}
+
+
+
+
+
Override Priority: Job Override → User Override → Org Default
+
The most specific level always takes precedence. If a job override is set it will be used regardless of user or org settings.
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
,
+ document.body
+ );
+};
+
+export default CommissionSettingsModal;
diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx
index 7e913b0..a1b0a5d 100644
--- a/src/data/mockStore.jsx
+++ b/src/data/mockStore.jsx
@@ -4917,6 +4917,16 @@ export const MockStoreProvider = ({ children }) => {
const [kanbanColumns, setKanbanColumns] = useState(KANBAN_COLUMNS_INITIAL);
const [kanbanLeads, setKanbanLeads] = useState(KANBAN_LEADS_INITIAL);
+ // Commission Settings — Org defaults + per-user overrides
+ const [orgCommissionDefaults, setOrgCommissionDefaults] = useState({
+ type: 'percent_gross',
+ rate: 10,
+ });
+ const [userCommissionOverrides, setUserCommissionOverrides] = useState([
+ { userId: 'p1', userName: 'Jesus Gonzales', enabled: true, type: 'percent_gross', rate: 12 },
+ { userId: 'p2', userName: 'Sarah Sales', enabled: true, type: 'flat', rate: 2000 },
+ ]);
+
// Initialize properties once
useEffect(() => {
const data = generateProperties();
@@ -5104,7 +5114,27 @@ export const MockStoreProvider = ({ children }) => {
},
assignAgent,
markLeadContacted,
- sendReminder
+ sendReminder,
+
+ // Commission Settings
+ orgCommissionDefaults,
+ userCommissionOverrides,
+ updateOrgCommissionDefaults: (defaults) => {
+ setOrgCommissionDefaults(prev => ({ ...prev, ...defaults }));
+ },
+ setUserCommissionOverride: (override) => {
+ setUserCommissionOverrides(prev => {
+ const idx = prev.findIndex(o => o.userId === override.userId);
+ if (idx >= 0) return prev.map((o, i) => i === idx ? { ...o, ...override } : o);
+ return [...prev, override];
+ });
+ },
+ removeUserCommissionOverride: (userId) => {
+ setUserCommissionOverrides(prev => prev.filter(o => o.userId !== userId));
+ },
+ updateProject: (projectId, data) => {
+ setProjects(prev => prev.map(p => p.id === projectId ? { ...p, ...data } : p));
+ },
}}>
{children}
diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx
index f1af201..8ae540a 100644
--- a/src/pages/owner/OwnerProjectDetail.jsx
+++ b/src/pages/owner/OwnerProjectDetail.jsx
@@ -12,11 +12,12 @@ import {
ArrowLeft, CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert,
FileText, DollarSign, GitPullRequest, AlertCircle, Activity, Milestone,
Shield, ChevronRight, Phone, MessageSquare, Mail, AlertOctagon, TrendingUp, Key, Zap, Camera, Plus, Map, User, Crosshair, Image as ImageIcon, MapPin,
- Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon, Info
+ Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon, Info, Settings, Building2, User as UserIcon, Briefcase
} from 'lucide-react';
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
import CreateItemModal from '../../components/owner/CreateItemModal';
+import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal';
// --- 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";
@@ -134,7 +135,12 @@ const formatCurrency = (amt) =>
const OwnerProjectDetail = () => {
const { projectId } = useParams();
const { user } = useAuth();
- const { projects, vendors } = useMockStore();
+ const {
+ projects, vendors, personnel,
+ orgCommissionDefaults, userCommissionOverrides,
+ updateOrgCommissionDefaults, setUserCommissionOverride,
+ removeUserCommissionOverride, updateProject,
+ } = useMockStore();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('overview');
const [selectedCO, setSelectedCO] = useState(null);
@@ -150,6 +156,7 @@ const OwnerProjectDetail = () => {
// ── Commission config state ─────────────────────────────────────────────
const [commissionConfig, setCommissionConfig] = useState(null);
+ const [commissionSettingsOpen, setCommissionSettingsOpen] = useState(false);
// ── Docs state ────────────────────────────────────────────────────────────
const [docs, setDocs] = useState(() => seedDocsMock(projectId));
@@ -235,15 +242,28 @@ const OwnerProjectDetail = () => {
projects.find(p => p.id === projectId && p.ownerId === user?.id)
, [projects, projectId, user]);
- const activeCommissionConfig = useMemo(() => {
+ // ── Commission hierarchy resolution: Job → User → Org ──────────────────
+ const jobCommissionOverride = useMemo(() => {
if (commissionConfig) return commissionConfig;
- if (!project) return { type: 'flat', rate: 0 };
- return {
- type: project.commissionType || 'flat',
- rate: Number(project.commissionRate) || 0,
- };
+ if (!project) return null;
+ if (project.commissionOverride) return project.commissionOverride;
+ 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 activeCommissionConfig = resolvedCommission;
+
const profitMetrics = useMemo(() => {
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;
@@ -987,22 +1007,51 @@ const OwnerProjectDetail = () => {
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}
+
+ );
+ })()}
-
-
{/* Live Formula Preview */}
-
+
Formula:
{profitMetrics.commissionFormula}
@@ -1808,6 +1857,29 @@ const OwnerProjectDetail = () => {
)}
igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
+
+ {/* Commission Settings Modal */}
+ setCommissionSettingsOpen(false)}
+ orgDefaults={orgCommissionDefaults}
+ userOverrides={userCommissionOverrides}
+ jobOverride={project?.commissionOverride || (commissionConfig ? { type: commissionConfig.type, rate: commissionConfig.rate } : null)}
+ activeSource={resolvedCommission.source}
+ personnel={personnel}
+ onSaveOrgDefaults={updateOrgCommissionDefaults}
+ onSaveUserOverride={setUserCommissionOverride}
+ onRemoveUserOverride={removeUserCommissionOverride}
+ onSaveJobOverride={(override) => {
+ setCommissionConfig(override);
+ updateProject(projectId, { commissionOverride: override });
+ }}
+ onClearJobOverride={() => {
+ setCommissionConfig(null);
+ updateProject(projectId, { commissionOverride: null });
+ }}
+ userRole={user?.role}
+ />
);
};