configurable commission setting
This commit is contained in:
@@ -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 = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign size={18} className={NEON_GOLD} />
|
||||
<h4 className="text-sm font-mono font-bold text-zinc-900 dark:text-white uppercase tracking-widest">Commission</h4>
|
||||
{/* 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 (
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest border ${b.color} ${b.bg} ${b.border}`}>
|
||||
<BadgeIcon size={10} />
|
||||
{b.label}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'commission' ? null : 'commission'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); setCommissionSettingsOpen(true); }}
|
||||
className="p-1.5 rounded-lg hover:bg-amber-500/10 transition-colors group"
|
||||
title="Commission Settings"
|
||||
>
|
||||
<Settings size={15} className="text-zinc-400 dark:text-zinc-500 group-hover:text-amber-500 dark:group-hover:text-[#fda913] transition-colors" />
|
||||
</button>
|
||||
{openTooltip === 'commission' && (
|
||||
<div className="absolute right-0 top-2 z-30 w-72 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-2">Commission Types</p>
|
||||
<div className="space-y-1.5">
|
||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">Flat:</span> Fixed dollar amount</p>
|
||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">% of Gross:</span> Total Received x %</p>
|
||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">% of Net:</span> Gross Profit x %</p>
|
||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">Custom:</span> Manual dollar entry</p>
|
||||
<div className="relative">
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'commission' ? null : 'commission'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||
</button>
|
||||
{openTooltip === 'commission' && (
|
||||
<div className="absolute right-0 top-2 z-30 w-72 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-2">Commission Hierarchy</p>
|
||||
<div className="space-y-1.5">
|
||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">Job Override:</span> Highest priority — set per job</p>
|
||||
<p><span className="font-bold text-purple-600 dark:text-purple-400">User Override:</span> Per-user commission settings</p>
|
||||
<p><span className="font-bold text-sky-600 dark:text-[#00f0ff]">Org Default:</span> Organization-wide fallback</p>
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5">
|
||||
<p className="text-[10px] text-zinc-500">Priority: Job → User → Org</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1012,7 +1061,7 @@ const OwnerProjectDetail = () => {
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 block mb-1.5">Type</label>
|
||||
<select
|
||||
value={activeCommissionConfig.type}
|
||||
onChange={e => setCommissionConfig(prev => ({ ...(prev || activeCommissionConfig), type: e.target.value, rate: '' }))}
|
||||
onChange={e => setCommissionConfig(prev => ({ ...(prev || { type: activeCommissionConfig.type, rate: activeCommissionConfig.rate }), type: e.target.value, rate: '' }))}
|
||||
className="w-full bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-xl px-4 py-2.5 text-sm text-zinc-900 dark:text-white outline-none focus:border-amber-500 dark:focus:border-amber-500 focus:ring-1 focus:ring-amber-500/30 transition-all"
|
||||
>
|
||||
{COMMISSION_TYPES.map(ct => (
|
||||
@@ -1035,7 +1084,7 @@ const OwnerProjectDetail = () => {
|
||||
step="any"
|
||||
min="0"
|
||||
value={activeCommissionConfig.rate}
|
||||
onChange={e => setCommissionConfig(prev => ({ ...(prev || activeCommissionConfig), type: activeCommissionConfig.type, rate: e.target.value }))}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
@@ -1052,7 +1101,7 @@ const OwnerProjectDetail = () => {
|
||||
</div>
|
||||
|
||||
{/* Live Formula Preview */}
|
||||
<div className="mt-3 px-3 py-2 bg-zinc-50 dark:bg-black/30 rounded-lg border border-zinc-100 dark:border-white/5">
|
||||
<div className="mt-3 px-3 py-2 bg-zinc-50 dark:bg-black/30 rounded-lg border border-zinc-100 dark:border-white/5 flex items-center justify-between">
|
||||
<p className="text-[11px] font-mono text-zinc-500 dark:text-zinc-400">
|
||||
<span className="text-amber-600 dark:text-amber-500 font-bold">Formula: </span>
|
||||
{profitMetrics.commissionFormula}
|
||||
@@ -1808,6 +1857,29 @@ const OwnerProjectDetail = () => {
|
||||
)}
|
||||
|
||||
<span className="attribution-ghost">igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
|
||||
|
||||
{/* Commission Settings Modal */}
|
||||
<CommissionSettingsModal
|
||||
isOpen={commissionSettingsOpen}
|
||||
onClose={() => 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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user