commission overrides in org Settings page

This commit is contained in:
Satyam-Rastogi
2026-05-13 12:51:36 +05:30
parent 7598f8982a
commit 9bc8a42354
6 changed files with 177 additions and 24 deletions
@@ -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 }) => {
<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 === 'role' && 'Per-role commission overrides'}
{level === 'user' && 'Per-user commission overrides'}
{level === 'job' && 'Override commission for this specific job'}
</p>
@@ -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 = ({
</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 &rarr; User &rarr; Org</p>
<p className="text-[10px] text-zinc-500 mt-0.5">Configure commission hierarchy: Job &rarr; User &rarr; Role &rarr; Org</p>
</div>
</div>
<button onClick={onClose} className="p-2 rounded-lg hover:bg-white/10 text-zinc-500 hover:text-white transition-colors">
@@ -202,8 +207,8 @@ const CommissionSettingsModal = ({
{/* 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) => {
<div className="flex items-center justify-center gap-1 text-[10px] font-bold uppercase tracking-widest flex-wrap">
{['org', 'role', 'user', 'job'].map((level, i) => {
const cfg = LEVEL_CONFIG[level];
const Icon = cfg.icon;
const isActive = activeSource === level;
@@ -282,6 +287,67 @@ const CommissionSettingsModal = ({
</AnimatePresence>
</div>
{/* ── ROLE OVERRIDES ── */}
<div>
<SectionHeader level="role" isOpen={openSections.role} onToggle={() => toggleSection('role')} />
<AnimatePresence>
{openSections.role && (
<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(16,185,129,0.2)' }}>
{roleOverrides.length === 0 ? (
<p className="text-xs text-zinc-600 italic px-2">No role overrides configured.</p>
) : (
roleOverrides.map(ov => {
const role = orgRoles.find(r => r.key === ov.roleKey);
const isApplicable = ov.roleKey === assignedUserRoleKey;
const typeLabel = COMMISSION_TYPES.find(c => c.value === ov.type)?.label || ov.type;
const rateDisplay = (ov.type === 'percent_gross' || ov.type === 'percent_net') ? `${ov.rate}%` : formatCurrency(ov.rate);
return (
<div
key={ov.roleKey}
className={`flex items-center gap-3 px-4 py-3 rounded-xl border transition-all ${
isApplicable
? 'bg-emerald-500/10 border-emerald-500/30'
: 'bg-white/[0.03] border-white/5 opacity-60'
}`}
>
<div className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: role?.color || '#10b981' }} />
<div className="flex-1 min-w-0">
<span className="text-sm font-bold text-white">{role?.name || ov.roleKey}</span>
<span className="text-xs text-zinc-500 ml-2">
{typeLabel} {rateDisplay}
</span>
</div>
<div className="flex items-center gap-2 shrink-0">
{ov.enabled ? (
<span className="text-[10px] font-bold text-emerald-400 uppercase">Active</span>
) : (
<span className="text-[10px] font-bold text-zinc-600 uppercase">Disabled</span>
)}
{isApplicable && (
<span className="text-[10px] font-bold text-emerald-400 bg-emerald-500/20 px-1.5 py-0.5 rounded-full">Applies</span>
)}
</div>
</div>
);
})
)}
<div className="flex items-start gap-2 px-2 pt-1">
<Info size={12} className="text-zinc-600 mt-0.5 shrink-0" />
<p className="text-[10px] text-zinc-600">Role overrides are managed in <span className="font-bold text-zinc-400">Org Settings Commission Rules</span>.</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* ── USER OVERRIDES ── */}
<div>
<SectionHeader level="user" isOpen={openSections.user} onToggle={() => toggleSection('user')} />
@@ -466,8 +532,8 @@ const CommissionSettingsModal = ({
<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 &rarr; User Override &rarr; 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>
<p><span className="font-bold text-zinc-400">Override Priority:</span> Job Override &rarr; User Override &rarr; Role Override &rarr; Org Default</p>
<p>The most specific level always takes precedence. If a job override is set it will be used regardless of user, role, or org settings.</p>
</div>
</div>
</div>
+18 -6
View File
@@ -27,6 +27,7 @@ const COMMISSION_TYPES = {
const COMMISSION_SOURCE_CFG = {
org: { label: 'Org Default', color: 'text-cyan-400', bg: 'bg-cyan-500/10 border-cyan-500/30', dot: 'bg-cyan-400' },
role: { label: 'Role', color: 'text-emerald-400', bg: 'bg-emerald-500/10 border-emerald-500/30', dot: 'bg-emerald-400' },
user: { label: 'User', color: 'text-violet-400', bg: 'bg-violet-500/10 border-violet-500/30', dot: 'bg-violet-400' },
job: { label: 'Job', color: 'text-amber-400', bg: 'bg-amber-500/10 border-amber-500/30', dot: 'bg-amber-400' },
};
@@ -42,7 +43,7 @@ const ROLE_COLORS = {
'Supplier': 'text-yellow-400 bg-yellow-500/10 border-yellow-500/20',
};
function resolveCommissionForMember(member, userCommissionOverrides, orgCommissionDefaults) {
function resolveCommissionForMember(member, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults) {
if (member.jobCommission) {
return { type: member.jobCommission.type, rate: Number(member.jobCommission.rate) || 0, source: 'job' };
}
@@ -50,6 +51,13 @@ function resolveCommissionForMember(member, userCommissionOverrides, orgCommissi
if (userOvr) {
return { type: userOvr.type, rate: Number(userOvr.rate) || 0, source: 'user' };
}
const orgMember = (orgMembers || []).find(m => m.userId === member.userId);
if (orgMember) {
const roleOvr = (roleCommissionOverrides || []).find(o => o.enabled && o.roleKey === orgMember.roleKey);
if (roleOvr) {
return { type: roleOvr.type, rate: Number(roleOvr.rate) || 0, source: 'role' };
}
}
return { type: orgCommissionDefaults.type, rate: Number(orgCommissionDefaults.rate) || 0, source: 'org' };
}
@@ -75,6 +83,8 @@ const TeamManagementPanel = ({
userRole,
orgCommissionDefaults,
userCommissionOverrides,
roleCommissionOverrides = [],
orgMembers = [],
totalReceived = 0,
grossProfit = 0,
onAddMember,
@@ -114,11 +124,11 @@ const TeamManagementPanel = ({
const memberCommissions = useMemo(() => {
return visibleMembers.map(m => {
const resolved = resolveCommissionForMember(m, userCommissionOverrides, orgCommissionDefaults);
const resolved = resolveCommissionForMember(m, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults);
const value = calcCommissionValue(resolved, totalReceived, grossProfit);
return { ...m, resolved, value };
});
}, [visibleMembers, userCommissionOverrides, orgCommissionDefaults, totalReceived, grossProfit]);
}, [visibleMembers, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults, totalReceived, grossProfit]);
const activeMembers = visibleMembers.filter(m => m.status === 'active');
const pendingMembers = visibleMembers.filter(m => m.status === 'pending');
@@ -404,6 +414,8 @@ const TeamManagementPanel = ({
member={editingCommission}
orgDefaults={orgCommissionDefaults}
userOverrides={userCommissionOverrides}
roleOverrides={roleCommissionOverrides}
orgMembers={orgMembers}
onSave={(memberId, jobCommission) => {
onUpdateMember(project.id, memberId, { jobCommission });
setEditingCommission(null);
@@ -638,8 +650,8 @@ const RemoveConfirmModal = ({ member, projectName, onConfirm, onClose }) => (
// ─── Edit Commission Modal ─────────────────────────────────────────────────────
const EditCommissionModal = ({ member, orgDefaults, userOverrides, onSave, onClear, onClose }) => {
const resolved = resolveCommissionForMember(member, userOverrides, orgDefaults);
const EditCommissionModal = ({ member, orgDefaults, userOverrides, roleOverrides, orgMembers, onSave, onClear, onClose }) => {
const resolved = resolveCommissionForMember(member, userOverrides, roleOverrides, orgMembers, orgDefaults);
const hasJobOverride = !!member.jobCommission;
const [type, setType] = useState(hasJobOverride ? member.jobCommission.type : resolved.type);
@@ -717,7 +729,7 @@ const EditCommissionModal = ({ member, orgDefaults, userOverrides, onSave, onCle
onClick={() => onClear(member.id)}
className="w-full py-2 text-xs font-semibold text-zinc-500 hover:text-red-500 transition-colors"
>
Clear job override (revert to {COMMISSION_SOURCE_CFG[resolveCommissionForMember({ ...member, jobCommission: null }, userOverrides, orgDefaults).source]?.label || 'default'})
Clear job override (revert to {COMMISSION_SOURCE_CFG[resolveCommissionForMember({ ...member, jobCommission: null }, userOverrides, roleOverrides, orgMembers, orgDefaults).source]?.label || 'default'})
</button>
)}
</div>