From 3b1a77f5002639598929420e778f9252b22e0bb2 Mon Sep 17 00:00:00 2001 From: Satyam Rastogi Date: Tue, 12 May 2026 19:29:52 +0530 Subject: [PATCH] team management feature in Owners Box --- src/components/owner/TeamManagementPanel.jsx | 730 +++++++++++++++++++ src/data/mockStore.jsx | 78 +- src/pages/owner/OwnerSnapshot.jsx | 68 +- 3 files changed, 873 insertions(+), 3 deletions(-) create mode 100644 src/components/owner/TeamManagementPanel.jsx diff --git a/src/components/owner/TeamManagementPanel.jsx b/src/components/owner/TeamManagementPanel.jsx new file mode 100644 index 0000000..0a87ae9 --- /dev/null +++ b/src/components/owner/TeamManagementPanel.jsx @@ -0,0 +1,730 @@ +import React, { useState, useMemo, useEffect, useRef } 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 +} 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' }, + 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, 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' }; + } + 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, + 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); + + 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, orgCommissionDefaults); + const value = calcCommissionValue(resolved, totalReceived, grossProfit); + return { ...m, resolved, value }; + }); + }, [visibleMembers, userCommissionOverrides, 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 && ( + + + {canManageCommissions && ( + + )} + + + )} + +
+ )} +
+
+ ))} +
+
+ )} + + {/* 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 ( + + + + + + + + + ); + })} + + + + + + + +
MemberJob RoleTypeRateValueSource
+
+
+ {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, onSave, onClear, onClose }) => { + const resolved = resolveCommissionForMember(member, userOverrides, 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 }; diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 15105b5..187eb56 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -2066,6 +2066,13 @@ const MOCK_PROJECTS = [ { name: 'Scope of Work - Roof Replacement', url: '#', type: 'contract' }, { name: 'Permit #26-0401', url: '#', type: 'permit' }, { name: 'Material Order PO-2604', url: '#', type: 'invoice' } + ], + teamMembers: [ + { id: 'jtm_001_1', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', orgRole: 'W2_EMPLOYEE', jobRole: 'Sales Rep', status: 'active', jobCommission: { type: 'percent_gross', rate: 12 } }, + { id: 'jtm_001_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null }, + { id: 'jtm_001_3', userId: 'sub_001', name: 'Carlos Subcontractor', email: 'carlos@electricpro.com', orgRole: 'SUBCONTRACTOR', jobRole: 'Subcontractor', status: 'active', jobCommission: null }, + { id: 'jtm_001_4', userId: 'e1', name: 'Frank Agent', email: 'agent1@plano.com', orgRole: 'FIELD_AGENT', jobRole: 'Canvasser', status: 'active', jobCommission: null }, + { id: 'jtm_001_5', userId: 'p2', name: 'Sarah Sales', email: 'sarah@lynkeduppro.com', orgRole: '1099_SALES_REP', jobRole: 'Estimator', status: 'pending', jobCommission: null } ] }, @@ -2115,6 +2122,10 @@ const MOCK_PROJECTS = [ { name: 'Completion Certificate', url: '#', type: 'certificate' }, { name: 'Final Invoice Package', url: '#', type: 'invoice' }, { name: 'Warranty Registration', url: '#', type: 'warranty' } + ], + teamMembers: [ + { id: 'jtm_002_1', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', orgRole: 'W2_EMPLOYEE', jobRole: 'Sales Rep', status: 'active', jobCommission: null }, + { id: 'jtm_002_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null } ] }, @@ -2190,6 +2201,11 @@ const MOCK_PROJECTS = [ { name: 'HVAC Design Plans', url: '#', type: 'plans' }, { name: 'Mechanical Permit #26-0187', url: '#', type: 'permit' }, { name: 'Equipment Spec Sheets', url: '#', type: 'specification' } + ], + teamMembers: [ + { id: 'jtm_003_1', userId: 'p2', name: 'Sarah Sales', email: 'sarah@lynkeduppro.com', orgRole: '1099_SALES_REP', jobRole: 'Sales Rep', status: 'active', jobCommission: { type: 'flat', rate: 2000 } }, + { id: 'jtm_003_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Project Manager', status: 'active', jobCommission: null }, + { id: 'jtm_003_3', userId: 'sub_001', name: 'Carlos Subcontractor', email: 'carlos@electricpro.com', orgRole: 'SUBCONTRACTOR', jobRole: 'Subcontractor', status: 'active', jobCommission: null } ] }, @@ -2276,6 +2292,11 @@ const MOCK_PROJECTS = [ { name: 'Change Order #1 - Scope Expansion', url: '#', type: 'change_order' }, { name: 'Change Order #2 - Material Upgrade', url: '#', type: 'change_order' }, { name: 'Asbestos Abatement Report', url: '#', type: 'report' } + ], + teamMembers: [ + { id: 'jtm_004_1', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', orgRole: 'W2_EMPLOYEE', jobRole: 'Project Manager', status: 'active', jobCommission: { type: 'percent_net', rate: 8 } }, + { id: 'jtm_004_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null }, + { id: 'jtm_004_3', userId: 'e2', name: 'Fiona Field', email: 'agent2@plano.com', orgRole: 'FIELD_AGENT', jobRole: 'Inspector', status: 'active', jobCommission: null } ] }, @@ -2349,6 +2370,10 @@ const MOCK_PROJECTS = [ { name: 'Dispute Notice - Material Defect', url: '#', type: 'dispute' }, { name: 'Manufacturer Warranty Claim', url: '#', type: 'warranty' }, { name: 'Site Photos - Defective Panels', url: '#', type: 'photos' } + ], + teamMembers: [ + { id: 'jtm_005_1', userId: 'p2', name: 'Sarah Sales', email: 'sarah@lynkeduppro.com', orgRole: '1099_SALES_REP', jobRole: 'Sales Rep', status: 'active', jobCommission: null }, + { id: 'jtm_005_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null } ] }, @@ -2396,6 +2421,10 @@ const MOCK_PROJECTS = [ { name: 'Electrical Upgrade Proposal', url: '#', type: 'contract' }, { name: 'Electrical Permit #26-0223', url: '#', type: 'permit' }, { name: 'Load Calculation Report', url: '#', type: 'report' } + ], + teamMembers: [ + { id: 'jtm_006_1', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', orgRole: 'W2_EMPLOYEE', jobRole: 'Sales Rep', status: 'active', jobCommission: null }, + { id: 'jtm_006_2', userId: 'sub_001', name: 'Carlos Subcontractor', email: 'carlos@electricpro.com', orgRole: 'SUBCONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null } ] }, @@ -2495,6 +2524,12 @@ const MOCK_PROJECTS = [ { name: 'TPO Manufacturer Spec', url: '#', type: 'specification' }, { name: 'Roof Survey Report', url: '#', type: 'report' }, { name: 'Safety Plan - Fall Protection', url: '#', type: 'safety' } + ], + teamMembers: [ + { id: 'jtm_007_1', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', orgRole: 'W2_EMPLOYEE', jobRole: 'Project Manager', status: 'active', jobCommission: { type: 'percent_gross', rate: 5 } }, + { id: 'jtm_007_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null }, + { id: 'jtm_007_3', userId: 'sub_001', name: 'Carlos Subcontractor', email: 'carlos@electricpro.com', orgRole: 'SUBCONTRACTOR', jobRole: 'Subcontractor', status: 'active', jobCommission: null }, + { id: 'jtm_007_4', userId: 'e4', name: 'Felicity Fast', email: 'agent4@plano.com', orgRole: 'FIELD_AGENT', jobRole: 'Canvasser', status: 'active', jobCommission: null } ] }, @@ -2550,6 +2585,10 @@ const MOCK_PROJECTS = [ { name: 'Change Order #1 - Additional Piers', url: '#', type: 'change_order' }, { name: 'Change Order #2 - Plumbing Reroute', url: '#', type: 'change_order' }, { name: 'Soil Analysis Report', url: '#', type: 'report' } + ], + teamMembers: [ + { id: 'jtm_008_1', userId: 'p2', name: 'Sarah Sales', email: 'sarah@lynkeduppro.com', orgRole: '1099_SALES_REP', jobRole: 'Sales Rep', status: 'active', jobCommission: null }, + { id: 'jtm_008_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null } ] }, @@ -2598,6 +2637,10 @@ const MOCK_PROJECTS = [ { name: 'Window Replacement Agreement', url: '#', type: 'contract' }, { name: 'Custom Window Order Confirmation', url: '#', type: 'purchase_order' }, { name: 'Energy Rating Certificates', url: '#', type: 'certificate' } + ], + teamMembers: [ + { id: 'jtm_009_1', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', orgRole: 'W2_EMPLOYEE', jobRole: 'Sales Rep', status: 'active', jobCommission: null }, + { id: 'jtm_009_2', userId: 'e2', name: 'Fiona Field', email: 'agent2@plano.com', orgRole: 'FIELD_AGENT', jobRole: 'Inspector', status: 'active', jobCommission: null } ] }, @@ -2646,6 +2689,11 @@ const MOCK_PROJECTS = [ { name: 'Completion Certificate', url: '#', type: 'certificate' }, { name: 'ADA Compliance Verification', url: '#', type: 'report' }, { name: 'Final Payment Release', url: '#', type: 'invoice' } + ], + teamMembers: [ + { id: 'jtm_010_1', userId: 'p2', name: 'Sarah Sales', email: 'sarah@lynkeduppro.com', orgRole: '1099_SALES_REP', jobRole: 'Estimator', status: 'active', jobCommission: { type: 'flat', rate: 1500 } }, + { id: 'jtm_010_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null }, + { id: 'jtm_010_3', userId: 'e5', name: 'Felix Fixer', email: 'agent5@plano.com', orgRole: 'FIELD_AGENT', jobRole: 'Canvasser', status: 'pending', jobCommission: null } ] }, @@ -2702,6 +2750,10 @@ const MOCK_PROJECTS = [ { name: 'Dispute Filing - Scope Disagreement', url: '#', type: 'dispute' }, { name: 'RFI #1 - Original vs Current Scope', url: '#', type: 'rfi' }, { name: 'RFI #2 - Waterproofing Method', url: '#', type: 'rfi' } + ], + teamMembers: [ + { id: 'jtm_011_1', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', orgRole: 'W2_EMPLOYEE', jobRole: 'Project Manager', status: 'active', jobCommission: null }, + { id: 'jtm_011_2', userId: 'con_001', name: 'Mike Contractor', email: 'mike@texasbuilders.com', orgRole: 'CONTRACTOR', jobRole: 'Lead Installer', status: 'active', jobCommission: null } ] }, @@ -2752,6 +2804,10 @@ const MOCK_PROJECTS = [ { name: 'Plumbing Permit #26-0156', url: '#', type: 'permit' }, { name: 'Scope Hold Notice - Permit Review', url: '#', type: 'notice' }, { name: 'Material Delivery Receipt', url: '#', type: 'receipt' } + ], + teamMembers: [ + { id: 'jtm_012_1', userId: 'p2', name: 'Sarah Sales', email: 'sarah@lynkeduppro.com', orgRole: '1099_SALES_REP', jobRole: 'Sales Rep', status: 'active', jobCommission: null }, + { id: 'jtm_012_2', userId: 'e3', name: 'Fred Flyer', email: 'agent3@plano.com', orgRole: 'FIELD_AGENT', jobRole: 'Canvasser', status: 'active', jobCommission: null } ] } ]; @@ -4993,7 +5049,7 @@ export const MockStoreProvider = ({ children }) => { meetings, salesHistory, // Exported for Leaderboard - // New Owners Box Data + // New Data owners, personnel, vendors, @@ -5123,6 +5179,26 @@ export const MockStoreProvider = ({ children }) => { updateProject: (projectId, data) => { setProjects(prev => prev.map(p => p.id === projectId ? { ...p, ...data } : p)); }, + + // Team Management (job-scoped) + addJobTeamMember: (projectId, member) => { + setProjects(prev => prev.map(p => { + if (p.id !== projectId) return p; + return { ...p, teamMembers: [...(p.teamMembers || []), { ...member, id: `jtm_${Date.now()}_${Math.random().toString(36).slice(2, 6)}` }] }; + })); + }, + removeJobTeamMember: (projectId, memberId) => { + setProjects(prev => prev.map(p => { + if (p.id !== projectId) return p; + return { ...p, teamMembers: (p.teamMembers || []).filter(m => m.id !== memberId) }; + })); + }, + updateJobTeamMember: (projectId, memberId, updates) => { + setProjects(prev => prev.map(p => { + if (p.id !== projectId) return p; + return { ...p, teamMembers: (p.teamMembers || []).map(m => m.id === memberId ? { ...m, ...updates } : m) }; + })); + }, }}> {children} diff --git a/src/pages/owner/OwnerSnapshot.jsx b/src/pages/owner/OwnerSnapshot.jsx index 2135d0d..a8c4e3c 100644 --- a/src/pages/owner/OwnerSnapshot.jsx +++ b/src/pages/owner/OwnerSnapshot.jsx @@ -6,18 +6,23 @@ import FinancialKPICards from '../../components/owner/FinancialKPICards'; import UrgentItemsPanel from '../../components/owner/UrgentItemsPanel'; import FinancialDetailsModal from '../../components/owner/FinancialDetailsModal'; import ActionCenterModal from '../../components/owner/ActionCenterModal'; +import TeamManagementPanel from '../../components/owner/TeamManagementPanel'; import { SpotlightCard } from '../../components/SpotlightCard'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'; import { - Briefcase, TrendingUp, AlertTriangle, Shield, ChevronRight, ArrowUpRight + Briefcase, TrendingUp, AlertTriangle, Shield, ChevronRight, ArrowUpRight, Users, ChevronDown } from 'lucide-react'; const OwnerSnapshot = () => { const { user } = useAuth(); const navigate = useNavigate(); - const { owners, projects } = useMockStore(); + const { + owners, projects, users, personnel, + orgCommissionDefaults, userCommissionOverrides, + addJobTeamMember, removeJobTeamMember, updateJobTeamMember, + } = useMockStore(); // Find the current owner profile const owner = useMemo(() => @@ -33,6 +38,23 @@ const OwnerSnapshot = () => { const [financialModal, setFinancialModal] = useState({ isOpen: false, type: 'revenue' }); const [actionModal, setActionModal] = useState({ isOpen: false, filter: 'all' }); + // ── Team Management (Owner's Box) ────────────────────────────────────── + const [selectedProjectId, setSelectedProjectId] = useState(null); + const selectedProject = useMemo(() => { + const id = selectedProjectId || ownerProjects.find(p => p.status === 'active')?.id; + return ownerProjects.find(p => p.id === id) || ownerProjects[0] || null; + }, [selectedProjectId, ownerProjects]); + + const teamFinancials = useMemo(() => { + if (!selectedProject) return { totalReceived: 0, grossProfit: 0 }; + const paid = (selectedProject.paymentSchedule || []) + .filter(ps => ps.status === 'paid') + .reduce((s, ps) => s + (ps.amount || 0), 0); + const totalReceived = paid || (selectedProject.budget || 0); + const totalCosts = Number(selectedProject.actualCost ?? selectedProject.spent) || 0; + return { totalReceived, grossProfit: totalReceived - totalCosts }; + }, [selectedProject]); + const handleFinancialClick = (type) => { setFinancialModal({ isOpen: true, type }); }; @@ -317,6 +339,48 @@ const OwnerSnapshot = () => { + + {/* ── Owner's Box — Team Management ── */} + {selectedProject && ( +
+
+

+ + Team Management +

+
+ + +
+
+ + + +
+ )} {/* Interactive Modals */}