configurable commission setting
This commit is contained in:
@@ -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 (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onToggle}
|
||||||
|
className="w-full flex items-center justify-between px-5 py-4 rounded-2xl border transition-all duration-200 hover:brightness-110"
|
||||||
|
style={{ backgroundColor: cfg.bg, borderColor: cfg.border }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-xl" style={{ backgroundColor: cfg.bg }}>
|
||||||
|
<Icon size={18} style={{ color: cfg.color }} />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<h3 className="text-sm font-bold text-white tracking-widest uppercase">{cfg.label}</h3>
|
||||||
|
<p className="text-[10px] text-zinc-500 mt-0.5">
|
||||||
|
{level === 'org' && 'Default commission for all jobs'}
|
||||||
|
{level === 'user' && 'Per-user commission overrides'}
|
||||||
|
{level === 'job' && 'Override commission for this specific job'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className="text-zinc-400 transition-transform duration-200"
|
||||||
|
style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommissionForm = ({ type, rate, onTypeChange, onRateChange, accentColor = '#fda913' }) => (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">Type</label>
|
||||||
|
<select
|
||||||
|
value={type}
|
||||||
|
onChange={e => onTypeChange(e.target.value)}
|
||||||
|
className="w-full bg-[#18181b] border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white outline-none transition-all"
|
||||||
|
style={{ '--tw-ring-color': accentColor }}
|
||||||
|
>
|
||||||
|
{COMMISSION_TYPES.map(ct => (
|
||||||
|
<option key={ct.value} value={ct.value}>{ct.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">
|
||||||
|
{type === 'percent_gross' || type === 'percent_net' ? 'Rate (%)' : 'Amount ($)'}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-zinc-500 pointer-events-none">
|
||||||
|
{type === 'percent_gross' || type === 'percent_net' ? '%' : '$'}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
min="0"
|
||||||
|
value={rate}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4" role="dialog" aria-modal="true">
|
||||||
|
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="relative w-full max-w-2xl max-h-[90vh] bg-[#121214] rounded-2xl shadow-[0_0_60px_rgba(0,0,0,0.9)] border border-white/10 overflow-hidden flex flex-col"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-5 border-b border-white/10 flex justify-between items-center bg-white/[0.03] shrink-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-xl bg-amber-500/10">
|
||||||
|
<DollarSign size={20} className="text-[#fda913]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold text-white tracking-widest uppercase">Commission Settings</h2>
|
||||||
|
<p className="text-[10px] text-zinc-500 mt-0.5">Configure commission hierarchy: Job → User → Org</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="p-2 rounded-lg hover:bg-white/10 text-zinc-500 hover:text-white transition-colors">
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Override Priority Visualization */}
|
||||||
|
<div className="px-6 pt-4 pb-2 shrink-0">
|
||||||
|
<div className="flex items-center justify-center gap-2 text-[10px] font-bold uppercase tracking-widest">
|
||||||
|
{['org', 'user', 'job'].map((level, i) => {
|
||||||
|
const cfg = LEVEL_CONFIG[level];
|
||||||
|
const Icon = cfg.icon;
|
||||||
|
const isActive = activeSource === level;
|
||||||
|
return (
|
||||||
|
<React.Fragment key={level}>
|
||||||
|
{i > 0 && <ArrowRight size={12} className="text-zinc-600 mx-1" />}
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full border transition-all ${
|
||||||
|
isActive
|
||||||
|
? 'ring-1 shadow-lg'
|
||||||
|
: 'opacity-40'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: cfg.bg,
|
||||||
|
borderColor: isActive ? cfg.color : cfg.border,
|
||||||
|
...(isActive ? { boxShadow: `0 0 12px ${cfg.color}40`, ringColor: cfg.color } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={12} style={{ color: cfg.color }} />
|
||||||
|
<span style={{ color: cfg.color }}>{cfg.label}</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="ml-1 w-1.5 h-1.5 rounded-full animate-pulse" style={{ backgroundColor: cfg.color }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-[10px] text-zinc-600 mt-2">
|
||||||
|
Currently using: <span className="font-bold" style={{ color: LEVEL_CONFIG[activeSource]?.color || '#fda913' }}>
|
||||||
|
{LEVEL_CONFIG[activeSource]?.label || 'Org Default'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-3 custom-scrollbar">
|
||||||
|
|
||||||
|
{/* ── ORG DEFAULTS ── */}
|
||||||
|
<div>
|
||||||
|
<SectionHeader level="org" isOpen={openSections.org} onToggle={() => toggleSection('org')} />
|
||||||
|
<AnimatePresence>
|
||||||
|
{openSections.org && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="pt-3 pb-1 pl-4 border-l-2 ml-7 space-y-3" style={{ borderColor: 'rgba(0,240,255,0.2)' }}>
|
||||||
|
{isAdmin ? (
|
||||||
|
<CommissionForm
|
||||||
|
type={localOrgDefaults.type}
|
||||||
|
rate={localOrgDefaults.rate}
|
||||||
|
onTypeChange={v => setLocalOrgDefaults(prev => ({ ...prev, type: v, rate: '' }))}
|
||||||
|
onRateChange={v => setLocalOrgDefaults(prev => ({ ...prev, rate: v }))}
|
||||||
|
accentColor="#00f0ff"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 px-4 py-3 bg-white/[0.03] rounded-xl border border-white/5">
|
||||||
|
<Shield size={14} className="text-zinc-500" />
|
||||||
|
<span className="text-xs text-zinc-400">
|
||||||
|
{COMMISSION_TYPES.find(c => c.value === orgDefaults.type)?.label} — {
|
||||||
|
orgDefaults.type === 'percent_gross' || orgDefaults.type === 'percent_net'
|
||||||
|
? `${orgDefaults.rate}%`
|
||||||
|
: formatCurrency(orgDefaults.rate)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-zinc-600 ml-auto">(Admin only)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── USER OVERRIDES ── */}
|
||||||
|
<div>
|
||||||
|
<SectionHeader level="user" isOpen={openSections.user} onToggle={() => toggleSection('user')} />
|
||||||
|
<AnimatePresence>
|
||||||
|
{openSections.user && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="pt-3 pb-1 pl-4 border-l-2 ml-7 space-y-3" style={{ borderColor: 'rgba(168,85,247,0.2)' }}>
|
||||||
|
{!isAdmin ? (
|
||||||
|
<div className="flex items-center gap-2 px-4 py-3 bg-white/[0.03] rounded-xl border border-white/5">
|
||||||
|
<Shield size={14} className="text-zinc-500" />
|
||||||
|
<span className="text-xs text-zinc-400">{localUserOverrides.length} user override(s) configured</span>
|
||||||
|
<span className="text-[10px] text-zinc-600 ml-auto">(Admin only)</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{localUserOverrides.length === 0 && !addingUser && (
|
||||||
|
<p className="text-xs text-zinc-600 italic px-2">No user overrides configured.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{localUserOverrides.map((ov, idx) => (
|
||||||
|
<div key={ov.userId} className="bg-white/[0.03] border border-white/5 rounded-xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User size={14} className="text-purple-400" />
|
||||||
|
<span className="text-sm font-bold text-white">{ov.userName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setLocalUserOverrides(prev =>
|
||||||
|
prev.map((o, i) => i === idx ? { ...o, enabled: !o.enabled } : o)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="p-1 rounded-lg hover:bg-white/10 transition-colors"
|
||||||
|
title={ov.enabled ? 'Disable override' : 'Enable override'}
|
||||||
|
>
|
||||||
|
{ov.enabled
|
||||||
|
? <ToggleRight size={20} className="text-purple-400" />
|
||||||
|
: <ToggleLeft size={20} className="text-zinc-600" />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLocalUserOverrides(prev => prev.filter((_, i) => i !== idx))}
|
||||||
|
className="p-1 rounded-lg hover:bg-red-500/10 text-zinc-600 hover:text-red-400 transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ov.enabled && (
|
||||||
|
<CommissionForm
|
||||||
|
type={ov.type}
|
||||||
|
rate={ov.rate}
|
||||||
|
onTypeChange={v => 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 && (
|
||||||
|
<p className="text-[10px] text-zinc-600 italic">Override disabled — org default applies</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Add New User Override */}
|
||||||
|
{addingUser ? (
|
||||||
|
<div className="bg-purple-500/5 border border-purple-500/20 rounded-xl p-4 space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">Select User</label>
|
||||||
|
<select
|
||||||
|
value={newUserOverride.userId}
|
||||||
|
onChange={e => setNewUserOverride(prev => ({ ...prev, userId: e.target.value }))}
|
||||||
|
className="w-full bg-[#18181b] border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white outline-none"
|
||||||
|
>
|
||||||
|
<option value="">— Select —</option>
|
||||||
|
{availablePersonnel.map(p => (
|
||||||
|
<option key={p.id} value={p.id}>{p.name} ({p.role})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<CommissionForm
|
||||||
|
type={newUserOverride.type}
|
||||||
|
rate={newUserOverride.rate}
|
||||||
|
onTypeChange={v => setNewUserOverride(prev => ({ ...prev, type: v, rate: '' }))}
|
||||||
|
onRateChange={v => setNewUserOverride(prev => ({ ...prev, rate: v }))}
|
||||||
|
accentColor="#a855f7"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2 pt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAddingUser(false)}
|
||||||
|
className="flex-1 px-4 py-2 rounded-xl border border-white/10 text-zinc-400 text-xs font-semibold hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddUserOverride}
|
||||||
|
disabled={!newUserOverride.userId}
|
||||||
|
className="flex-1 px-4 py-2 rounded-xl bg-purple-500/20 text-purple-300 border border-purple-500/30 text-xs font-bold hover:bg-purple-500/30 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
Add Override
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAddingUser(true)}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-dashed border-purple-500/30 text-purple-400 text-xs font-bold hover:bg-purple-500/5 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus size={14} /> Add User Override
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── JOB OVERRIDE ── */}
|
||||||
|
<div>
|
||||||
|
<SectionHeader level="job" isOpen={openSections.job} onToggle={() => toggleSection('job')} />
|
||||||
|
<AnimatePresence>
|
||||||
|
{openSections.job && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="pt-3 pb-1 pl-4 border-l-2 ml-7 space-y-3" style={{ borderColor: 'rgba(253,169,19,0.2)' }}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-zinc-400">Enable job-level override</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLocalJobOverride(prev => ({ ...prev, enabled: !prev.enabled }))}
|
||||||
|
className="p-1 rounded-lg hover:bg-white/10 transition-colors"
|
||||||
|
>
|
||||||
|
{localJobOverride.enabled
|
||||||
|
? <ToggleRight size={24} className="text-[#fda913]" />
|
||||||
|
: <ToggleLeft size={24} className="text-zinc-600" />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{localJobOverride.enabled ? (
|
||||||
|
<CommissionForm
|
||||||
|
type={localJobOverride.type}
|
||||||
|
rate={localJobOverride.rate}
|
||||||
|
onTypeChange={v => setLocalJobOverride(prev => ({ ...prev, type: v, rate: '' }))}
|
||||||
|
onRateChange={v => setLocalJobOverride(prev => ({ ...prev, rate: v }))}
|
||||||
|
accentColor="#fda913"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-[10px] text-zinc-600 italic px-2">
|
||||||
|
No job override — falls back to {activeSource === 'user' ? 'user override' : 'org default'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hierarchy Info */}
|
||||||
|
<div className="mt-2 p-4 bg-white/[0.02] rounded-xl border border-white/5">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Info size={14} className="text-zinc-600 mt-0.5 shrink-0" />
|
||||||
|
<div className="text-[11px] text-zinc-500 space-y-1">
|
||||||
|
<p><span className="font-bold text-zinc-400">Override Priority:</span> Job Override → User Override → Org Default</p>
|
||||||
|
<p>The most specific level always takes precedence. If a job override is set it will be used regardless of user or org settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 flex items-center justify-between bg-white/[0.02] shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-white/10 text-zinc-400 text-sm font-semibold hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-[#fda913]/20 text-[#fda913] border border-[#fda913]/30 text-sm font-bold hover:bg-[#fda913]/30 transition-colors"
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommissionSettingsModal;
|
||||||
+31
-1
@@ -4917,6 +4917,16 @@ export const MockStoreProvider = ({ children }) => {
|
|||||||
const [kanbanColumns, setKanbanColumns] = useState(KANBAN_COLUMNS_INITIAL);
|
const [kanbanColumns, setKanbanColumns] = useState(KANBAN_COLUMNS_INITIAL);
|
||||||
const [kanbanLeads, setKanbanLeads] = useState(KANBAN_LEADS_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
|
// Initialize properties once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const data = generateProperties();
|
const data = generateProperties();
|
||||||
@@ -5104,7 +5114,27 @@ export const MockStoreProvider = ({ children }) => {
|
|||||||
},
|
},
|
||||||
assignAgent,
|
assignAgent,
|
||||||
markLeadContacted,
|
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}
|
{children}
|
||||||
</MockStoreContext.Provider>
|
</MockStoreContext.Provider>
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import {
|
|||||||
ArrowLeft, CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert,
|
ArrowLeft, CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert,
|
||||||
FileText, DollarSign, GitPullRequest, AlertCircle, Activity, Milestone,
|
FileText, DollarSign, GitPullRequest, AlertCircle, Activity, Milestone,
|
||||||
Shield, ChevronRight, Phone, MessageSquare, Mail, AlertOctagon, TrendingUp, Key, Zap, Camera, Plus, Map, User, Crosshair, Image as ImageIcon, MapPin,
|
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';
|
} from 'lucide-react';
|
||||||
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
|
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
|
||||||
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
|
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
|
||||||
import CreateItemModal from '../../components/owner/CreateItemModal';
|
import CreateItemModal from '../../components/owner/CreateItemModal';
|
||||||
|
import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal';
|
||||||
|
|
||||||
// --- NEOMORPHIC STYLING CONSTANTS ---
|
// --- 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";
|
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 OwnerProjectDetail = () => {
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { projects, vendors } = useMockStore();
|
const {
|
||||||
|
projects, vendors, personnel,
|
||||||
|
orgCommissionDefaults, userCommissionOverrides,
|
||||||
|
updateOrgCommissionDefaults, setUserCommissionOverride,
|
||||||
|
removeUserCommissionOverride, updateProject,
|
||||||
|
} = useMockStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeTab, setActiveTab] = useState('overview');
|
const [activeTab, setActiveTab] = useState('overview');
|
||||||
const [selectedCO, setSelectedCO] = useState(null);
|
const [selectedCO, setSelectedCO] = useState(null);
|
||||||
@@ -150,6 +156,7 @@ const OwnerProjectDetail = () => {
|
|||||||
|
|
||||||
// ── Commission config state ─────────────────────────────────────────────
|
// ── Commission config state ─────────────────────────────────────────────
|
||||||
const [commissionConfig, setCommissionConfig] = useState(null);
|
const [commissionConfig, setCommissionConfig] = useState(null);
|
||||||
|
const [commissionSettingsOpen, setCommissionSettingsOpen] = useState(false);
|
||||||
|
|
||||||
// ── Docs state ────────────────────────────────────────────────────────────
|
// ── Docs state ────────────────────────────────────────────────────────────
|
||||||
const [docs, setDocs] = useState(() => seedDocsMock(projectId));
|
const [docs, setDocs] = useState(() => seedDocsMock(projectId));
|
||||||
@@ -235,15 +242,28 @@ const OwnerProjectDetail = () => {
|
|||||||
projects.find(p => p.id === projectId && p.ownerId === user?.id)
|
projects.find(p => p.id === projectId && p.ownerId === user?.id)
|
||||||
, [projects, projectId, user]);
|
, [projects, projectId, user]);
|
||||||
|
|
||||||
const activeCommissionConfig = useMemo(() => {
|
// ── Commission hierarchy resolution: Job → User → Org ──────────────────
|
||||||
|
const jobCommissionOverride = useMemo(() => {
|
||||||
if (commissionConfig) return commissionConfig;
|
if (commissionConfig) return commissionConfig;
|
||||||
if (!project) return { type: 'flat', rate: 0 };
|
if (!project) return null;
|
||||||
return {
|
if (project.commissionOverride) return project.commissionOverride;
|
||||||
type: project.commissionType || 'flat',
|
return null;
|
||||||
rate: Number(project.commissionRate) || 0,
|
|
||||||
};
|
|
||||||
}, [commissionConfig, project]);
|
}, [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(() => {
|
const profitMetrics = useMemo(() => {
|
||||||
if (!project) return { totalCosts: 0, commission: 0, commissionFormula: '', additionalExp: 0, grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
|
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;
|
const totalCosts = Number(project.actualCost ?? project.spent) || 0;
|
||||||
@@ -987,22 +1007,51 @@ const OwnerProjectDetail = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<DollarSign size={18} className={NEON_GOLD} />
|
<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>
|
<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>
|
||||||
<div className="relative">
|
<div className="flex items-center gap-1">
|
||||||
<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">
|
<button
|
||||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
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>
|
</button>
|
||||||
{openTooltip === 'commission' && (
|
<div className="relative">
|
||||||
<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">
|
<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">
|
||||||
<p className="font-bold text-zinc-900 dark:text-white mb-2">Commission Types</p>
|
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||||
<div className="space-y-1.5">
|
</button>
|
||||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">Flat:</span> Fixed dollar amount</p>
|
{openTooltip === 'commission' && (
|
||||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">% of Gross:</span> Total Received x %</p>
|
<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><span className="font-bold text-amber-600 dark:text-amber-400">% of Net:</span> Gross Profit x %</p>
|
<p className="font-bold text-zinc-900 dark:text-white mb-2">Commission Hierarchy</p>
|
||||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">Custom:</span> Manual dollar entry</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>
|
</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>
|
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 block mb-1.5">Type</label>
|
||||||
<select
|
<select
|
||||||
value={activeCommissionConfig.type}
|
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"
|
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 => (
|
{COMMISSION_TYPES.map(ct => (
|
||||||
@@ -1035,7 +1084,7 @@ const OwnerProjectDetail = () => {
|
|||||||
step="any"
|
step="any"
|
||||||
min="0"
|
min="0"
|
||||||
value={activeCommissionConfig.rate}
|
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'}
|
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"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Live Formula Preview */}
|
{/* 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">
|
<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>
|
<span className="text-amber-600 dark:text-amber-500 font-bold">Formula: </span>
|
||||||
{profitMetrics.commissionFormula}
|
{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>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user