diff --git a/src/components/owner/OrgTeamManagement.jsx b/src/components/owner/OrgTeamManagement.jsx new file mode 100644 index 0000000..e2a103b --- /dev/null +++ b/src/components/owner/OrgTeamManagement.jsx @@ -0,0 +1,1009 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Search, UserPlus, Filter, MoreVertical, Edit3, Trash2, Mail, Shield, + Eye, DollarSign, X, Check, ChevronDown, Crown, AlertTriangle, + Users, RefreshCw, Ban, UserCheck, Clock, Briefcase, ArrowUpDown, +} from 'lucide-react'; +import { toast } from 'sonner'; + +const ROLE_OPTIONS = [ + { key: 'ADMIN', label: 'Admin / Manager' }, + { key: 'SALES_REP', label: 'Sales Rep' }, + { key: 'CANVASSER', label: 'Canvasser' }, + { key: 'SUBCONTRACTOR', label: 'Subcontractor' }, +]; + +const STATUS_CONFIG = { + active: { label: 'Active', bg: 'bg-emerald-50 dark:bg-emerald-500/10', border: 'border-emerald-200 dark:border-emerald-500/20', text: 'text-emerald-600 dark:text-emerald-400' }, + invited: { label: 'Invited', bg: 'bg-amber-50 dark:bg-amber-500/10', border: 'border-amber-200 dark:border-amber-500/20', text: 'text-amber-600 dark:text-amber-400' }, + suspended: { label: 'Suspended', bg: 'bg-red-50 dark:bg-red-500/10', border: 'border-red-200 dark:border-red-500/20', text: 'text-red-500 dark:text-red-400' }, +}; + +const inputClasses = "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-blue-500/50 transition-colors"; + +// ──────────────────────────────────────────────────── +// Invite Member Modal +// ──────────────────────────────────────────────────── +const InviteMemberModal = ({ isOpen, onClose, onInvite, existingMembers, users }) => { + const [inviteMethod, setInviteMethod] = useState('email'); + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + const [roleKey, setRoleKey] = useState(''); + const [userSearch, setUserSearch] = useState(''); + const [selectedUser, setSelectedUser] = useState(null); + + const existingIds = new Set(existingMembers.map(m => m.userId)); + const existingEmails = new Set(existingMembers.map(m => m.email.toLowerCase())); + + const searchResults = useMemo(() => { + if (!userSearch.trim() || inviteMethod !== 'username') return []; + const term = userSearch.toLowerCase(); + return (users || []) + .filter(u => + (u.type === 'employee' || u.type === 'owner' || u.type === 'contractor' || u.type === 'subcontractor') && + !existingIds.has(u.id) && + (u.name?.toLowerCase().includes(term) || u.email?.toLowerCase().includes(term)) + ) + .slice(0, 5); + }, [userSearch, users, existingIds, inviteMethod]); + + const handleSubmit = () => { + if (!roleKey) { + toast.error('Please select a role'); + return; + } + + if (inviteMethod === 'email') { + if (!email.trim() || !email.includes('@')) { + toast.error('Please enter a valid email address'); + return; + } + if (existingEmails.has(email.toLowerCase())) { + toast.error('This email is already a member of the organization'); + return; + } + onInvite({ + userId: `usr_${Date.now()}`, + name: name.trim() || email.split('@')[0], + email: email.trim().toLowerCase(), + roleKey, + status: 'invited', + }); + } else { + if (!selectedUser) { + toast.error('Please select a user'); + return; + } + onInvite({ + userId: selectedUser.id, + name: selectedUser.name, + email: selectedUser.email, + roleKey, + status: 'invited', + }); + } + + setEmail(''); + setName(''); + setRoleKey(''); + setUserSearch(''); + setSelectedUser(null); + onClose(); + toast.success('Invitation sent successfully'); + }; + + if (!isOpen) return null; + + return ( + + + e.stopPropagation()} + > + {/* Header */} +
+
+
+ +
+
+

Invite Team Member

+

Add a new member to your organization

+
+
+ +
+ + {/* Body */} +
+ {/* Invite Method Tabs */} +
+ {[ + { key: 'email', label: 'Email Invite', icon: Mail }, + { key: 'username', label: 'Search Users', icon: Search }, + ].map(tab => ( + + ))} +
+ + {inviteMethod === 'email' ? ( +
+
+ + setEmail(e.target.value)} + placeholder="colleague@company.com" + className={inputClasses} + /> +
+
+ + setName(e.target.value)} + placeholder="John Doe (optional)" + className={inputClasses} + /> +
+
+ ) : ( +
+
+ +
+ + { setUserSearch(e.target.value); setSelectedUser(null); }} + placeholder="Search by name or email..." + className={`${inputClasses} pl-9`} + /> +
+
+ {searchResults.length > 0 && !selectedUser && ( +
+ {searchResults.map(u => ( + + ))} +
+ )} + {selectedUser && ( +
+
+ {selectedUser.name?.charAt(0)} +
+
+
{selectedUser.name}
+
{selectedUser.email}
+
+ +
+ )} + {userSearch.trim() && searchResults.length === 0 && !selectedUser && ( +

No matching users found

+ )} +
+ )} + + {/* Role Selection */} +
+ + +
+
+ + {/* Footer */} +
+ + +
+
+
+
+ ); +}; + +// ──────────────────────────────────────────────────── +// Confirmation Dialog +// ──────────────────────────────────────────────────── +const ConfirmDialog = ({ isOpen, onClose, onConfirm, title, message, confirmLabel, confirmColor }) => { + if (!isOpen) return null; + const colorMap = { + red: 'bg-red-500 hover:bg-red-600', + blue: 'bg-blue-500 hover:bg-blue-600', + emerald: 'bg-emerald-500 hover:bg-emerald-600', + amber: 'bg-amber-500 hover:bg-amber-600', + }; + return ( + + + e.stopPropagation()} + > +
+
+ +
+

{title}

+
+

{message}

+
+ + +
+
+
+
+ ); +}; + +// ──────────────────────────────────────────────────── +// View Permissions Modal +// ──────────────────────────────────────────────────── +const ViewPermissionsModal = ({ isOpen, onClose, member, permissions, permissionModules, roles }) => { + if (!isOpen || !member) return null; + const role = roles.find(r => r.key === member.roleKey); + const isOwner = member.roleKey === 'OWNER'; + + return ( + + + e.stopPropagation()} + > +
+
+
+ {member.name.charAt(0)} +
+
+

{member.name}

+

{role?.name || member.roleKey} — Permissions

+
+
+ +
+
+ {isOwner && ( +
+ Org Owner has full access to all modules +
+ )} + {Object.entries(permissionModules).map(([moduleKey, mod]) => { + const memberPerms = isOwner ? mod.actions : (permissions[moduleKey]?.[member.roleKey] || []); + return ( +
+
+ + {mod.label} +
+
+ {mod.actions.map(action => { + const has = memberPerms.includes(action); + return ( + + {has && } + {action} + + ); + })} +
+
+ ); + })} +
+
+ +
+
+
+
+ ); +}; + +// ──────────────────────────────────────────────────── +// Member Action Menu +// ──────────────────────────────────────────────────── +const MemberActionMenu = ({ member, onAction, isOpen, onToggle, currentUserRoleKey }) => { + const isOwnerMember = member.roleKey === 'OWNER'; + const canManage = currentUserRoleKey === 'OWNER' || (currentUserRoleKey === 'ADMIN' && !isOwnerMember && member.roleKey !== 'ADMIN'); + const canManageAdmin = currentUserRoleKey === 'OWNER'; + + const isManageable = isOwnerMember + ? false + : member.roleKey === 'ADMIN' + ? canManageAdmin + : canManage; + + const actions = []; + + if (isManageable) { + actions.push({ key: 'editRole', label: 'Edit Role', icon: Edit3 }); + } + + if (member.status === 'invited' && isManageable) { + actions.push({ key: 'resendInvite', label: 'Resend Invite', icon: Mail }); + } + + if (member.status === 'active' && isManageable) { + actions.push({ key: 'suspend', label: 'Disable Account', icon: Ban }); + } + + if (member.status === 'suspended' && isManageable) { + actions.push({ key: 'reactivate', label: 'Reactivate', icon: UserCheck }); + } + + actions.push({ key: 'viewPermissions', label: 'View Permissions', icon: Eye }); + actions.push({ key: 'viewCommission', label: 'Commission Settings', icon: DollarSign }); + + if (isManageable) { + actions.push({ key: 'remove', label: 'Remove Member', icon: Trash2, danger: true }); + } + + return ( +
+ + + {isOpen && ( + + {actions.map(action => ( + + ))} + + )} + +
+ ); +}; + +// ──────────────────────────────────────────────────── +// Edit Role Inline +// ──────────────────────────────────────────────────── +const EditRolePanel = ({ member, roles, onSave, onCancel }) => { + const [newRole, setNewRole] = useState(member.roleKey); + const currentRole = roles.find(r => r.key === member.roleKey); + const targetRole = roles.find(r => r.key === newRole); + + const isPromotion = () => { + const order = ['SUBCONTRACTOR', 'CANVASSER', 'SALES_REP', 'ADMIN']; + return order.indexOf(newRole) > order.indexOf(member.roleKey); + }; + + return ( + +
+
+ Current: + + {currentRole?.name} + + + New: + {newRole !== member.roleKey && ( + + {targetRole?.name} + + )} +
+ + {newRole !== member.roleKey && ( +
+ {isPromotion() ? '↑ Promotion' : '↓ Demotion'}: {currentRole?.name} → {targetRole?.name} +
+ )} +
+ + +
+
+
+ ); +}; + +// ──────────────────────────────────────────────────── +// Main Component +// ──────────────────────────────────────────────────── +const OrgTeamManagement = ({ + members, + roles, + users, + permissions, + permissionModules, + currentUserRoleKey, + onAddMember, + onUpdateMember, + onRemoveMember, + onSuspendMember, + onReactivateMember, + onResendInvite, +}) => { + const [searchTerm, setSearchTerm] = useState(''); + const [filterRole, setFilterRole] = useState('all'); + const [filterStatus, setFilterStatus] = useState('all'); + const [sortField, setSortField] = useState('name'); + const [sortDir, setSortDir] = useState('asc'); + const [showInviteModal, setShowInviteModal] = useState(false); + const [openMenuId, setOpenMenuId] = useState(null); + const [editingRoleId, setEditingRoleId] = useState(null); + const [confirmDialog, setConfirmDialog] = useState(null); + const [viewPermsMember, setViewPermsMember] = useState(null); + + const canInvite = currentUserRoleKey === 'OWNER' || currentUserRoleKey === 'ADMIN'; + + const filteredMembers = useMemo(() => { + let result = [...members]; + + if (searchTerm.trim()) { + const term = searchTerm.toLowerCase(); + result = result.filter(m => + m.name.toLowerCase().includes(term) || + m.email.toLowerCase().includes(term) || + m.roleKey.toLowerCase().includes(term) + ); + } + + if (filterRole !== 'all') { + result = result.filter(m => m.roleKey === filterRole); + } + + if (filterStatus !== 'all') { + result = result.filter(m => m.status === filterStatus); + } + + result.sort((a, b) => { + let valA, valB; + switch (sortField) { + case 'name': valA = a.name.toLowerCase(); valB = b.name.toLowerCase(); break; + case 'role': valA = a.roleKey; valB = b.roleKey; break; + case 'status': valA = a.status; valB = b.status; break; + case 'joinedAt': valA = a.joinedAt; valB = b.joinedAt; break; + case 'lastActivity': valA = a.lastActivity || ''; valB = b.lastActivity || ''; break; + case 'jobs': valA = a.assignedJobsCount || 0; valB = b.assignedJobsCount || 0; break; + default: valA = a.name; valB = b.name; + } + if (valA < valB) return sortDir === 'asc' ? -1 : 1; + if (valA > valB) return sortDir === 'asc' ? 1 : -1; + return 0; + }); + + return result; + }, [members, searchTerm, filterRole, filterStatus, sortField, sortDir]); + + const toggleSort = useCallback((field) => { + if (sortField === field) { + setSortDir(prev => prev === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortDir('asc'); + } + }, [sortField]); + + const handleAction = useCallback((actionKey, member) => { + switch (actionKey) { + case 'editRole': + setEditingRoleId(member.id); + break; + case 'resendInvite': + onResendInvite(member.id); + break; + case 'suspend': + setConfirmDialog({ + title: 'Disable Account', + message: `Are you sure you want to suspend ${member.name}? They will lose access to the organization until reactivated.`, + confirmLabel: 'Suspend', + confirmColor: 'red', + onConfirm: () => { + onSuspendMember(member.id); + toast.success(`${member.name} has been suspended`); + }, + }); + break; + case 'reactivate': + onReactivateMember(member.id); + toast.success(`${member.name} has been reactivated`); + break; + case 'viewPermissions': + setViewPermsMember(member); + break; + case 'viewCommission': + toast.info(`Commission settings for ${member.name} — check the User Overrides section`); + break; + case 'remove': + setConfirmDialog({ + title: 'Remove Member', + message: `Are you sure you want to remove ${member.name} from the organization? This action cannot be undone.`, + confirmLabel: 'Remove', + confirmColor: 'red', + onConfirm: () => { + onRemoveMember(member.id); + toast.success(`${member.name} has been removed from the organization`); + }, + }); + break; + default: + break; + } + }, [onResendInvite, onSuspendMember, onReactivateMember, onRemoveMember]); + + const handleRoleSave = useCallback((memberId, newRoleKey) => { + const member = members.find(m => m.id === memberId); + const newRole = roles.find(r => r.key === newRoleKey); + onUpdateMember(memberId, { roleKey: newRoleKey }); + setEditingRoleId(null); + toast.success(`${member?.name}'s role updated to ${newRole?.name}`); + }, [members, roles, onUpdateMember]); + + const roleCounts = useMemo(() => { + const counts = {}; + roles.forEach(r => { counts[r.key] = members.filter(m => m.roleKey === r.key).length; }); + return counts; + }, [members, roles]); + + const statusCounts = useMemo(() => ({ + active: members.filter(m => m.status === 'active').length, + invited: members.filter(m => m.status === 'invited').length, + suspended: members.filter(m => m.status === 'suspended').length, + }), [members]); + + const SortHeader = ({ field, children, className = '' }) => ( + + ); + + return ( +
+ {/* Quick Stats */} +
+
+
{members.length}
+
Total Members
+
+
+
{statusCounts.active}
+
Active
+
+
+
{statusCounts.invited}
+
Invited
+
+
+
{statusCounts.suspended}
+
Suspended
+
+
+ + {/* Toolbar */} +
+
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="w-full bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-xl pl-9 pr-4 py-2 text-sm text-zinc-900 dark:text-white placeholder-zinc-400 dark:placeholder-zinc-600 outline-none focus:border-blue-500/50 transition-colors" + /> +
+ {/* Role Filter */} +
+ + +
+ {/* Status Filter */} + +
+ {canInvite && ( + + )} +
+ + {/* Desktop Table */} +
+ + + + + + + + + + + + + + {filteredMembers.map(member => { + const role = roles.find(r => r.key === member.roleKey); + const sc = STATUS_CONFIG[member.status] || STATUS_CONFIG.active; + const isEditing = editingRoleId === member.id; + + return ( + + + {/* Member */} + + {/* Role */} + + {/* Status */} + + {/* Joined */} + + {/* Last Active */} + + {/* Jobs */} + + {/* Actions */} + + + {/* Edit Role Inline */} + + {isEditing && ( + + + + )} + + + ); + })} + +
+ Member + + Org Role + + Status + + Joined + + Last Active + + Jobs + + Actions +
+
+
+ {member.name.charAt(0)} +
+
+
+ {member.name} + {member.roleKey === 'OWNER' && } +
+ {member.email} +
+
+
+ + {role?.name || member.roleKey} + + + + {sc.label} + + +
+ + {member.joinedAt} +
+
+ + {member.lastActivity || '—'} + + +
+ + {member.assignedJobsCount || 0} +
+
+ setOpenMenuId(openMenuId === member.id ? null : member.id)} + /> +
+ setEditingRoleId(null)} + /> +
+ {filteredMembers.length === 0 && ( +
+ +

No members found

+

Try adjusting your search or filters

+
+ )} +
+ + {/* Mobile Cards */} +
+ {filteredMembers.map(member => { + const role = roles.find(r => r.key === member.roleKey); + const sc = STATUS_CONFIG[member.status] || STATUS_CONFIG.active; + const isEditing = editingRoleId === member.id; + + return ( +
+
+
+
+ {member.name.charAt(0)} +
+
+
+ {member.name} + {member.roleKey === 'OWNER' && } +
+ {member.email} +
+
+ setOpenMenuId(openMenuId === member.id ? null : member.id)} + /> +
+
+ + {role?.name} + + + {sc.label} + + + {member.assignedJobsCount || 0} jobs + + + {member.joinedAt} + +
+ + {isEditing && ( + setEditingRoleId(null)} + /> + )} + +
+ ); + })} + {filteredMembers.length === 0 && ( +
+ +

No members found

+

Try adjusting your search or filters

+
+ )} +
+ + {/* Result Count */} +
+ Showing {filteredMembers.length} of {members.length} members + {(filterRole !== 'all' || filterStatus !== 'all' || searchTerm.trim()) && ( + + )} +
+ + {/* Modals */} + setShowInviteModal(false)} + onInvite={onAddMember} + existingMembers={members} + users={users} + /> + + setConfirmDialog(null)} + onConfirm={confirmDialog?.onConfirm || (() => {})} + title={confirmDialog?.title || ''} + message={confirmDialog?.message || ''} + confirmLabel={confirmDialog?.confirmLabel || 'Confirm'} + confirmColor={confirmDialog?.confirmColor || 'red'} + /> + + setViewPermsMember(null)} + member={viewPermsMember} + permissions={permissions} + permissionModules={permissionModules} + roles={roles} + /> +
+ ); +}; + +export default OrgTeamManagement; diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 74a6727..c3b5c9b 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -5054,19 +5054,19 @@ export const MockStoreProvider = ({ children }) => { ]); const [orgMembers, setOrgMembers] = useState([ - { id: 'om_001', userId: 'owner_001', name: 'Justin Johnson', email: 'justin@lynkeduppro.com', roleKey: 'OWNER', status: 'active', joinedAt: '2024-01-15' }, - { id: 'om_002', userId: 'owner_002', name: 'Maria Garcia', email: 'maria@lynkeduppro.com', roleKey: 'OWNER', status: 'active', joinedAt: '2024-01-15' }, - { id: 'om_003', userId: 'ADM01', name: 'Admin One', email: 'admin1@lynkeduppro.com', roleKey: 'ADMIN', status: 'active', joinedAt: '2024-02-10' }, - { id: 'om_004', userId: 'ADM02', name: 'Admin Two', email: 'admin2@lynkeduppro.com', roleKey: 'ADMIN', status: 'active', joinedAt: '2024-03-01' }, - { id: 'om_005', userId: 'ADM03', name: 'Admin Three', email: 'admin3@lynkeduppro.com', roleKey: 'ADMIN', status: 'active', joinedAt: '2024-04-20' }, - { id: 'om_006', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', roleKey: 'SALES_REP', status: 'active', joinedAt: '2024-03-15', individualCommission: { enabled: true, type: 'percent_gross', rate: 12 } }, - { id: 'om_007', userId: 'p2', name: 'Sarah Sales', email: 'sarah@lynkeduppro.com', roleKey: 'SALES_REP', status: 'active', joinedAt: '2024-04-01', individualCommission: { enabled: true, type: 'flat', rate: 2000 } }, - { id: 'om_008', userId: 'FA001', name: 'Frank Agent', email: 'agent1@plano.com', roleKey: 'CANVASSER', status: 'active', joinedAt: '2024-05-10' }, - { id: 'om_009', userId: 'FA002', name: 'Fiona Field', email: 'agent2@plano.com', roleKey: 'CANVASSER', status: 'active', joinedAt: '2024-05-15' }, - { id: 'om_010', userId: 'FA003', name: 'Fred Flyer', email: 'agent3@plano.com', roleKey: 'CANVASSER', status: 'active', joinedAt: '2024-06-01' }, - { id: 'om_011', userId: 'FA004', name: 'Felicity Fast', email: 'agent4@plano.com', roleKey: 'CANVASSER', status: 'active', joinedAt: '2024-06-10' }, - { id: 'om_012', userId: 'FA005', name: 'Felix Fixer', email: 'agent5@plano.com', roleKey: 'CANVASSER', status: 'active', joinedAt: '2024-07-01' }, - { id: 'om_013', userId: 'sub_001', name: 'Carlos Subcontractor', email: 'carlos@electricpro.com', roleKey: 'SUBCONTRACTOR', status: 'active', joinedAt: '2024-08-01' }, + { id: 'om_001', userId: 'owner_001', name: 'Justin Johnson', email: 'justin@lynkeduppro.com', roleKey: 'OWNER', status: 'active', joinedAt: '2024-01-15', lastActivity: '2025-05-12', assignedJobsCount: 14 }, + { id: 'om_002', userId: 'owner_002', name: 'Maria Garcia', email: 'maria@lynkeduppro.com', roleKey: 'OWNER', status: 'active', joinedAt: '2024-01-15', lastActivity: '2025-05-10', assignedJobsCount: 8 }, + { id: 'om_003', userId: 'ADM01', name: 'Admin One', email: 'admin1@lynkeduppro.com', roleKey: 'ADMIN', status: 'active', joinedAt: '2024-02-10', lastActivity: '2025-05-11', assignedJobsCount: 6 }, + { id: 'om_004', userId: 'ADM02', name: 'Admin Two', email: 'admin2@lynkeduppro.com', roleKey: 'ADMIN', status: 'active', joinedAt: '2024-03-01', lastActivity: '2025-05-09', assignedJobsCount: 4 }, + { id: 'om_005', userId: 'ADM03', name: 'Admin Three', email: 'admin3@lynkeduppro.com', roleKey: 'ADMIN', status: 'active', joinedAt: '2024-04-20', lastActivity: '2025-05-08', assignedJobsCount: 3 }, + { id: 'om_006', userId: 'p1', name: 'Jesus Gonzales', email: 'jesus@lynkeduppro.com', roleKey: 'SALES_REP', status: 'active', joinedAt: '2024-03-15', lastActivity: '2025-05-12', assignedJobsCount: 9, individualCommission: { enabled: true, type: 'percent_gross', rate: 12 } }, + { id: 'om_007', userId: 'p2', name: 'Sarah Sales', email: 'sarah@lynkeduppro.com', roleKey: 'SALES_REP', status: 'active', joinedAt: '2024-04-01', lastActivity: '2025-05-11', assignedJobsCount: 7, individualCommission: { enabled: true, type: 'flat', rate: 2000 } }, + { id: 'om_008', userId: 'FA001', name: 'Frank Agent', email: 'agent1@plano.com', roleKey: 'CANVASSER', status: 'active', joinedAt: '2024-05-10', lastActivity: '2025-05-12', assignedJobsCount: 2 }, + { id: 'om_009', userId: 'FA002', name: 'Fiona Field', email: 'agent2@plano.com', roleKey: 'CANVASSER', status: 'active', joinedAt: '2024-05-15', lastActivity: '2025-05-07', assignedJobsCount: 3 }, + { id: 'om_010', userId: 'FA003', name: 'Fred Flyer', email: 'agent3@plano.com', roleKey: 'CANVASSER', status: 'invited', joinedAt: '2024-06-01', lastActivity: null, assignedJobsCount: 0 }, + { id: 'om_011', userId: 'FA004', name: 'Felicity Fast', email: 'agent4@plano.com', roleKey: 'CANVASSER', status: 'active', joinedAt: '2024-06-10', lastActivity: '2025-05-06', assignedJobsCount: 1 }, + { id: 'om_012', userId: 'FA005', name: 'Felix Fixer', email: 'agent5@plano.com', roleKey: 'CANVASSER', status: 'suspended', joinedAt: '2024-07-01', lastActivity: '2025-03-20', assignedJobsCount: 0 }, + { id: 'om_013', userId: 'sub_001', name: 'Carlos Subcontractor', email: 'carlos@electricpro.com', roleKey: 'SUBCONTRACTOR', status: 'active', joinedAt: '2024-08-01', lastActivity: '2025-05-10', assignedJobsCount: 5 }, ]); // Initialize properties once @@ -5322,13 +5322,29 @@ export const MockStoreProvider = ({ children }) => { setOrgMembers(prev => prev.map(m => m.id === memberId ? { ...m, ...data } : m)); }, addOrgMember: (member) => { - const newMember = { ...member, id: `om_${Date.now()}`, joinedAt: new Date().toISOString().slice(0, 10) }; + const newMember = { + ...member, + id: `om_${Date.now()}`, + joinedAt: new Date().toISOString().slice(0, 10), + lastActivity: null, + assignedJobsCount: 0, + status: member.status || 'invited', + }; setOrgMembers(prev => [...prev, newMember]); return newMember; }, removeOrgMember: (memberId) => { setOrgMembers(prev => prev.filter(m => m.id !== memberId)); }, + suspendOrgMember: (memberId) => { + setOrgMembers(prev => prev.map(m => m.id === memberId ? { ...m, status: 'suspended' } : m)); + }, + reactivateOrgMember: (memberId) => { + setOrgMembers(prev => prev.map(m => m.id === memberId ? { ...m, status: 'active' } : m)); + }, + resendOrgInvite: (memberId) => { + toast.success('Invite resent successfully'); + }, }}> {children} diff --git a/src/pages/owner/OrgSettings.jsx b/src/pages/owner/OrgSettings.jsx index 708644a..ae7afd0 100644 --- a/src/pages/owner/OrgSettings.jsx +++ b/src/pages/owner/OrgSettings.jsx @@ -5,12 +5,14 @@ import { Search, Check, X, Plus, Trash2, Edit3, Save, User, Crown, ToggleLeft, ToggleRight, Info, ArrowRight, Mail, Phone, MapPin, Clock, AlertTriangle, UserPlus, UserMinus, Eye, Pencil, Lock, - RotateCcw, Filter, CheckSquare, Square, BarChart3, History + RotateCcw, Filter, CheckSquare, Square, BarChart3, History, UsersRound } from 'lucide-react'; import { useMockStore } from '../../data/mockStore'; import { useAuth } from '../../context/AuthContext'; +import { usePermissions } from '../../hooks/usePermissions'; import { toast } from 'sonner'; import { computeRolePermissionStats } from '../../utils/permissions'; +import OrgTeamManagement from '../../components/owner/OrgTeamManagement'; const COMMISSION_TYPES = [ { value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' }, @@ -984,9 +986,11 @@ const UserOverridesSection = ({ members, roles, orgDefaults, roleOverrides, user const OrgSettings = () => { const { user } = useAuth(); const store = useMockStore(); + const { orgRoleKey } = usePermissions(); const [openSections, setOpenSections] = useState({ org: true, + team: false, roles: false, access: false, commission: false, @@ -1009,6 +1013,29 @@ const OrgSettings = () => { /> ), }, + { + id: 'team', + icon: UsersRound, + title: 'Organization Team Management', + subtitle: `${store.orgMembers.length} members — invite, manage roles, and administer your team`, + color: '#8b5cf6', + content: ( + + ), + }, { id: 'roles', icon: Crown, @@ -1093,11 +1120,12 @@ const OrgSettings = () => { {/* Quick Stats */} -
+
{[ { label: 'Roles', value: store.orgRoles.length, color: '#fda913' }, { label: 'Members', value: store.orgMembers.length, color: '#3b82f6' }, - { label: 'Modules', value: Object.keys(store.PERMISSION_MODULES).length, color: '#10b981' }, + { label: 'Active', value: store.orgMembers.filter(m => m.status === 'active').length, color: '#10b981' }, + { label: 'Modules', value: Object.keys(store.PERMISSION_MODULES).length, color: '#8b5cf6' }, { label: 'Commission Overrides', value: store.roleCommissionOverrides.length + store.userCommissionOverrides.length, color: '#a855f7' }, ].map(stat => (