import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { UserPlus, UserMinus, Shield, ChevronDown, Search, Mail, X as XIcon, Users, Briefcase, DollarSign, MoreVertical, Check, Clock, Crown, ChevronUp, ToggleLeft, ToggleRight } from 'lucide-react'; const JOB_ROLES = [ 'Project Manager', 'Sales Rep', 'Lead Installer', 'Estimator', 'Inspector', 'Canvasser', 'Subcontractor', 'Supplier', ]; const COMMISSION_TYPES = { percent_gross: '% of Gross', percent_net: '% of Net', flat: 'Flat Amount', custom: 'Custom / Manual', }; 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' }, }; const ROLE_COLORS = { 'Project Manager': 'text-cyan-400 bg-cyan-500/10 border-cyan-500/20', 'Sales Rep': 'text-green-400 bg-green-500/10 border-green-500/20', 'Lead Installer': 'text-orange-400 bg-orange-500/10 border-orange-500/20', 'Estimator': 'text-blue-400 bg-blue-500/10 border-blue-500/20', 'Inspector': 'text-purple-400 bg-purple-500/10 border-purple-500/20', 'Canvasser': 'text-teal-400 bg-teal-500/10 border-teal-500/20', 'Subcontractor': 'text-rose-400 bg-rose-500/10 border-rose-500/20', 'Supplier': 'text-yellow-400 bg-yellow-500/10 border-yellow-500/20', }; function resolveCommissionForMember(member, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults) { if (member.jobCommission) { return { type: member.jobCommission.type, rate: Number(member.jobCommission.rate) || 0, source: 'job' }; } const userOvr = (userCommissionOverrides || []).find(o => o.enabled && o.userId === member.userId); 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' }; } function calcCommissionValue(comm, totalReceived, grossProfit) { const r = Number(comm.rate) || 0; switch (comm.type) { case 'percent_gross': return totalReceived * (r / 100); case 'percent_net': return grossProfit * (r / 100); case 'flat': return r; case 'custom': return r; default: return 0; } } const formatCurrency = (amt) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0); const TeamManagementPanel = ({ project, teamMembers = [], allUsers = [], personnel = [], userRole, orgCommissionDefaults, userCommissionOverrides, roleCommissionOverrides = [], orgMembers = [], totalReceived = 0, grossProfit = 0, onAddMember, onRemoveMember, onUpdateMember, }) => { const [inviteOpen, setInviteOpen] = useState(false); const [removeConfirm, setRemoveConfirm] = useState(null); const [actionMenu, setActionMenu] = useState(null); const [editingRole, setEditingRole] = useState(null); const [commissionOpen, setCommissionOpen] = useState(false); const [editingCommission, setEditingCommission] = useState(null); const menuRef = useRef(null); // ── Commission toggle default ───────────────────────────────────────── // Reads commissionEnabled directly from the member data in mockStore. // Falls back to true if a jobCommission override is explicitly set. const getMemberCommissionDefault = useCallback((member) => { if (typeof member.commissionEnabled === 'boolean') return member.commissionEnabled; return !!member.jobCommission; // fallback for legacy data without the field }, []); // Per-member commission toggle: { [memberId]: boolean } const [commissionToggle, setCommissionToggle] = useState(() => { const init = {}; teamMembers.forEach(m => { init[m.id] = getMemberCommissionDefault(m); }); return init; }); // Keep new members initialized when teamMembers changes useEffect(() => { setCommissionToggle(prev => { const next = { ...prev }; let changed = false; teamMembers.forEach(m => { if (!(m.id in next)) { next[m.id] = getMemberCommissionDefault(m); changed = true; } }); return changed ? next : prev; }); }, [teamMembers, getMemberCommissionDefault]); // Toggle and persist back to store so the change survives re-renders const toggleMemberCommission = useCallback((memberId) => { setCommissionToggle(prev => { const next = { ...prev, [memberId]: !prev[memberId] }; onUpdateMember(project.id, memberId, { commissionEnabled: next[memberId] }); return next; }); }, [project.id, onUpdateMember]); const canManageTeam = ['OWNER', 'ADMIN'].includes(userRole); const canManageCommissions = ['OWNER', 'ADMIN'].includes(userRole); const canViewCommissions = ['OWNER', 'ADMIN', 'CONTRACTOR'].includes(userRole); const isSubcontractor = userRole === 'SUBCONTRACTOR'; useEffect(() => { if (!actionMenu) return; const handleClick = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setActionMenu(null); }; document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, [actionMenu]); const visibleMembers = useMemo(() => { if (isSubcontractor) { const currentUserId = allUsers?.find(u => u.role === 'SUBCONTRACTOR')?.id; return teamMembers.filter(m => m.userId === currentUserId); } return teamMembers; }, [teamMembers, isSubcontractor, allUsers]); const memberCommissions = useMemo(() => { return visibleMembers.map(m => { const resolved = resolveCommissionForMember(m, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults); const value = calcCommissionValue(resolved, totalReceived, grossProfit); return { ...m, resolved, value }; }); }, [visibleMembers, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults, totalReceived, grossProfit]); const activeMembers = visibleMembers.filter(m => m.status === 'active'); const pendingMembers = visibleMembers.filter(m => m.status === 'pending'); return ( <>
{/* Header */}

Team Management

{activeMembers.length} active{pendingMembers.length > 0 && ` · ${pendingMembers.length} pending`}
{canManageTeam && ( setInviteOpen(true)} className="flex items-center gap-2 px-4 py-2 bg-violet-500/10 hover:bg-violet-500/20 text-violet-600 dark:text-violet-400 border border-violet-500/30 rounded-xl text-xs font-bold transition-colors" > Invite Member )}
{/* Member Grid */} {visibleMembers.length === 0 ? (

No team members assigned to this job yet.

{canManageTeam && ( )}
) : (
{visibleMembers.map((member) => (
{/* Avatar */}
{member.name?.[0] || '?'}
{member.name} {member.status === 'pending' && ( Pending )}

{member.email}

{/* Job Role Badge */}
{editingRole === member.id ? ( ) : ( )} org: {member.orgRole?.replace(/_/g, ' ')}
{/* Actions Menu */} {canManageTeam && (
{actionMenu === member.id && ( {/* Change Job Role */} {/* Commission Toggle */} {canManageCommissions && (
Commission
)} {/* Edit Commission — visible only when toggle is ON */} {canManageCommissions && commissionToggle[member.id] && ( )} {/* Remove from Job */}
)}
)}
))}
)} {/* Commission Distribution */} {canViewCommissions && visibleMembers.length > 0 && (
{commissionOpen && (
{memberCommissions.filter(m => m.status === 'active').map((mc) => { const srcCfg = COMMISSION_SOURCE_CFG[mc.resolved.source] || COMMISSION_SOURCE_CFG.org; return ( ); })}
Member Job Role Type Rate Value Source
{mc.name?.[0]}
{mc.name}
{mc.jobRole} {COMMISSION_TYPES[mc.resolved.type] || mc.resolved.type} {mc.resolved.type === 'flat' || mc.resolved.type === 'custom' ? formatCurrency(mc.resolved.rate) : `${mc.resolved.rate}%`} {formatCurrency(mc.value)} {srcCfg.label}
Total Distribution {formatCurrency(memberCommissions.filter(m => m.status === 'active').reduce((s, mc) => s + mc.value, 0))}
)}
)}
{/* ── Invite Modal ── */} {inviteOpen && createPortal( m.userId)} onInvite={(member) => { onAddMember(project.id, member); setInviteOpen(false); }} onClose={() => setInviteOpen(false)} />, document.body )} {/* ── Remove Confirmation ── */} {removeConfirm && createPortal( { onRemoveMember(project.id, removeConfirm.id); setRemoveConfirm(null); }} onClose={() => setRemoveConfirm(null)} />, document.body )} {/* ── Edit Commission Modal ── */} {editingCommission && createPortal( { onUpdateMember(project.id, memberId, { jobCommission }); setEditingCommission(null); }} onClear={(memberId) => { onUpdateMember(project.id, memberId, { jobCommission: null }); setEditingCommission(null); }} onClose={() => setEditingCommission(null)} />, document.body )} ); }; // ─── Invite Modal ────────────────────────────────────────────────────────────── const InviteModal = ({ allUsers, personnel, existingIds, onInvite, onClose }) => { const [tab, setTab] = useState('search'); const [query, setQuery] = useState(''); const [selectedRole, setSelectedRole] = useState('Sales Rep'); const [emailForm, setEmailForm] = useState({ name: '', email: '' }); const searchable = useMemo(() => { const fromUsers = allUsers .filter(u => !['customer'].includes(u.type)) .map(u => ({ userId: u.id, name: u.name, email: u.email, orgRole: u.role || u.type?.toUpperCase() || 'EMPLOYEE' })); const fromPersonnel = personnel .map(p => ({ userId: p.id, name: p.name, email: p.email, orgRole: p.role })); const merged = new Map(); [...fromUsers, ...fromPersonnel].forEach(s => merged.set(s.userId, s)); return Array.from(merged.values()).filter(s => !existingIds.includes(s.userId)); }, [allUsers, personnel, existingIds]); const filtered = useMemo(() => { if (!query.trim()) return searchable; const q = query.toLowerCase(); return searchable.filter(s => s.name.toLowerCase().includes(q) || s.email.toLowerCase().includes(q)); }, [searchable, query]); const handleSearchInvite = (person) => { onInvite({ userId: person.userId, name: person.name, email: person.email, orgRole: person.orgRole, jobRole: selectedRole, status: 'active', jobCommission: null, }); }; const handleEmailInvite = () => { if (!emailForm.name.trim() || !emailForm.email.trim()) return; onInvite({ userId: `inv_${Date.now()}`, name: emailForm.name.trim(), email: emailForm.email.trim(), orgRole: 'INVITED', jobRole: selectedRole, status: 'pending', jobCommission: null, }); }; return (
e.stopPropagation()} > {/* Header */}

Invite to Job

Add team members to this project

{/* Tabs */}
{[ { key: 'search', label: 'Search Users', icon: Search }, { key: 'email', label: 'Invite by Email', icon: Mail }, ].map(t => ( ))}
{/* Role Selector */}
{/* Search Tab */} {tab === 'search' && (
setQuery(e.target.value)} placeholder="Search by name or email..." autoFocus className="w-full pl-9 pr-3 py-2.5 text-sm rounded-xl bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white placeholder-zinc-400 outline-none focus:border-violet-500 transition-colors" />
{filtered.length === 0 ? (

No matching users found

) : filtered.map(person => ( ))}
)} {/* Email Tab */} {tab === 'email' && (
setEmailForm(prev => ({ ...prev, name: e.target.value }))} placeholder="John Smith" className="w-full px-3 py-2.5 text-sm rounded-xl bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white placeholder-zinc-400 outline-none focus:border-violet-500 transition-colors" />
setEmailForm(prev => ({ ...prev, email: e.target.value }))} placeholder="john@example.com" type="email" className="w-full px-3 py-2.5 text-sm rounded-xl bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white placeholder-zinc-400 outline-none focus:border-violet-500 transition-colors" />

Invitation will be sent with a pending status until accepted.

)}
); }; // ─── Remove Confirmation Modal ───────────────────────────────────────────────── const RemoveConfirmModal = ({ member, projectName, onConfirm, onClose }) => (
e.stopPropagation()} >

Remove from Job?

{member.name} will be removed from this {projectName} job.

This will not affect their organization role or other job assignments.

); // ─── Edit Commission Modal ───────────────────────────────────────────────────── 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); const [rate, setRate] = useState(hasJobOverride ? member.jobCommission.rate : resolved.rate); return (
e.stopPropagation()} >

Job Commission Override

Override commission for {member.name} on this job only.

{/* Current Resolution */}

Currently Resolved From

{(() => { const cfg = COMMISSION_SOURCE_CFG[resolved.source] || COMMISSION_SOURCE_CFG.org; return ( <> {cfg.label} — {COMMISSION_TYPES[resolved.type]} @ {resolved.type === 'flat' || resolved.type === 'custom' ? formatCurrency(resolved.rate) : `${resolved.rate}%`} ); })()}
setRate(e.target.value)} min="0" step={type === 'flat' || type === 'custom' ? '100' : '0.5'} className="w-full px-3 py-2.5 text-sm font-mono rounded-xl bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white outline-none focus:border-amber-500 transition-colors" />
{hasJobOverride && ( )}
); }; export default TeamManagementPanel; export { JOB_ROLES };