configurable commission setting

This commit is contained in:
Satyam-Rastogi
2026-05-12 15:54:01 +05:30
parent d32b3ced21
commit c82cadd547
3 changed files with 626 additions and 25 deletions
+96 -24
View File
@@ -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 &rarr; User &rarr; 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>
);
};