From e85ecd257164aadf359e4bae1d18398fbd51a4c4 Mon Sep 17 00:00:00 2001 From: Satyam-Rastogi Date: Tue, 12 May 2026 20:36:44 +0530 Subject: [PATCH] Org settings module and user roles --- src/App.jsx | 6 + src/components/Layout.jsx | 3 +- src/data/mockStore.jsx | 148 +++++- src/pages/owner/OrgSettings.jsx | 860 ++++++++++++++++++++++++++++++++ 4 files changed, 998 insertions(+), 19 deletions(-) create mode 100644 src/pages/owner/OrgSettings.jsx diff --git a/src/App.jsx b/src/App.jsx index accfdc8..5100797 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -19,6 +19,7 @@ import VendorDirectory from './pages/owner/VendorDirectory'; import DocumentManagement from './pages/owner/DocumentManagement'; import OwnerProjectList from './pages/owner/OwnerProjectList'; import OwnerProjectDetail from './pages/owner/OwnerProjectDetail'; +import OrgSettings from './pages/owner/OrgSettings'; import PlaceholderDashboard from './pages/PlaceholderDashboard'; import ProCanvas from './pages/ProCanvas'; import ProjectList from './pages/contractor/ProjectList'; @@ -213,6 +214,11 @@ function App() { } /> + + + + } /> {/* Protected Admin Routes — Owner has full admin access */} { { to: "/owner/estimates", icon: Calculator, label: "Estimates" }, { to: "/admin/schedule", icon: Calendar, label: "Team Schedule" }, { to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" }, + { to: "/owner/settings", icon: Settings, label: "Org Settings" }, ...commonItems, ]; case 'ADMIN': diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 187eb56..d08a748 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -4971,6 +4971,104 @@ export const MockStoreProvider = ({ children }) => { { userId: 'p2', userName: 'Sarah Sales', enabled: true, type: 'flat', rate: 2000 }, ]); + // ── Org Settings & Access Control ── + const [orgSettings, setOrgSettings] = useState({ + orgName: 'LynkedUp Pro Roofing', + orgEmail: 'info@lynkeduppro.com', + orgPhone: '214-555-1000', + orgAddress: '5800 Legacy Dr, Plano, TX 75024', + timezone: 'America/Chicago', + }); + + const [orgRoles, setOrgRoles] = useState([ + { + id: 'role_owner', + name: 'Org Owner', + key: 'OWNER', + description: 'Full access to all modules, settings, billing, and access control', + isSystem: true, + memberCount: 2, + color: '#fda913', + }, + { + id: 'role_admin', + name: 'Admin / Manager', + key: 'ADMIN', + description: 'Manage jobs, view financials, assign team, verify leads, configure commissions', + isSystem: true, + memberCount: 3, + color: '#3b82f6', + }, + { + id: 'role_sales', + name: 'Sales Rep', + key: 'SALES_REP', + description: 'View/manage assigned jobs, verify canvasser leads, view own commission', + isSystem: true, + memberCount: 2, + color: '#a855f7', + }, + { + id: 'role_canvasser', + name: 'Canvasser', + key: 'CANVASSER', + description: 'Log door knocks, add leads, view leaderboard, view verified leads', + isSystem: true, + memberCount: 5, + color: '#10b981', + }, + { + id: 'role_subcontractor', + name: 'Subcontractor', + key: 'SUBCONTRACTOR', + description: 'Receive tasks, update status, reply to tasks, log expenses', + isSystem: true, + memberCount: 1, + color: '#06b6d4', + }, + ]); + + const [orgPermissions, setOrgPermissions] = useState({ + jobs: { OWNER: ['view','edit','delete'], ADMIN: ['view','edit','delete'], SALES_REP: ['view','edit'], CANVASSER: [], SUBCONTRACTOR: [] }, + financials: { OWNER: ['view','edit'], ADMIN: ['view','edit'], SALES_REP: ['view'], CANVASSER: [], SUBCONTRACTOR: [] }, + commission: { OWNER: ['view','edit'], ADMIN: ['view','edit'], SALES_REP: ['view'], CANVASSER: [], SUBCONTRACTOR: [] }, + team: { OWNER: ['view','invite','remove'], ADMIN: ['view','invite'], SALES_REP: ['view'], CANVASSER: [], SUBCONTRACTOR: [] }, + leads: { OWNER: ['view','log','verify'], ADMIN: ['view','log','verify'], SALES_REP: ['view','verify'], CANVASSER: ['view','log'], SUBCONTRACTOR: [] }, + leaderboard: { OWNER: ['view'], ADMIN: ['view'], SALES_REP: ['view'], CANVASSER: ['view'], SUBCONTRACTOR: [] }, + subcontractor_tasks: { OWNER: ['view','assign'], ADMIN: ['view','assign'], SALES_REP: [], CANVASSER: [], SUBCONTRACTOR: ['view','receive'] }, + }); + + const PERMISSION_MODULES = { + jobs: { label: 'Jobs', actions: ['view','edit','delete'] }, + financials: { label: 'Financials', actions: ['view','edit'] }, + commission: { label: 'Commission Settings', actions: ['view','edit'] }, + team: { label: 'Team Members', actions: ['view','invite','remove'] }, + leads: { label: 'Leads & Door Knocks', actions: ['view','log','verify'] }, + leaderboard: { label: 'Leaderboard', actions: ['view'] }, + subcontractor_tasks: { label: 'Subcontractor Tasks', actions: ['view','assign','receive'] }, + }; + + const [roleCommissionOverrides, setRoleCommissionOverrides] = useState([ + { roleKey: 'SALES_REP', enabled: true, type: 'percent_net', rate: 5 }, + { roleKey: 'CANVASSER', enabled: true, type: 'flat', rate: 300 }, + ]); + + 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' }, + ]); + // Initialize properties once useEffect(() => { const data = generateProperties(); @@ -5048,8 +5146,6 @@ export const MockStoreProvider = ({ children }) => { users, meetings, salesHistory, // Exported for Leaderboard - - // New Data owners, personnel, vendors, @@ -5057,7 +5153,6 @@ export const MockStoreProvider = ({ children }) => { projects, orders, vendorInvoices, - leads, addLead: (lead) => { const newLead = { @@ -5180,24 +5275,41 @@ export const MockStoreProvider = ({ children }) => { 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)}` }] }; + // Org Settings & Access Control + orgSettings, + updateOrgSettings: (data) => setOrgSettings(prev => ({ ...prev, ...data })), + orgRoles, + updateOrgRole: (roleId, data) => setOrgRoles(prev => prev.map(r => r.id === roleId ? { ...r, ...data } : r)), + orgPermissions, + PERMISSION_MODULES, + updateOrgPermissions: (moduleKey, roleKey, actions) => { + setOrgPermissions(prev => ({ + ...prev, + [moduleKey]: { ...prev[moduleKey], [roleKey]: actions }, })); }, - removeJobTeamMember: (projectId, memberId) => { - setProjects(prev => prev.map(p => { - if (p.id !== projectId) return p; - return { ...p, teamMembers: (p.teamMembers || []).filter(m => m.id !== memberId) }; - })); + roleCommissionOverrides, + setRoleCommissionOverride: (override) => { + setRoleCommissionOverrides(prev => { + const idx = prev.findIndex(o => o.roleKey === override.roleKey); + if (idx >= 0) return prev.map((o, i) => i === idx ? { ...o, ...override } : o); + return [...prev, override]; + }); }, - 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) }; - })); + removeRoleCommissionOverride: (roleKey) => { + setRoleCommissionOverrides(prev => prev.filter(o => o.roleKey !== roleKey)); + }, + orgMembers, + updateOrgMember: (memberId, data) => { + 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) }; + setOrgMembers(prev => [...prev, newMember]); + return newMember; + }, + removeOrgMember: (memberId) => { + setOrgMembers(prev => prev.filter(m => m.id !== memberId)); }, }}> {children} diff --git a/src/pages/owner/OrgSettings.jsx b/src/pages/owner/OrgSettings.jsx new file mode 100644 index 0000000..71898b8 --- /dev/null +++ b/src/pages/owner/OrgSettings.jsx @@ -0,0 +1,860 @@ +import React, { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Settings, Building2, Shield, Users, DollarSign, ChevronDown, ChevronRight, + Search, Check, X, Plus, Trash2, Edit3, Save, User, Crown, + ToggleLeft, ToggleRight, Info, ArrowRight, Mail, Phone, MapPin, Clock, + AlertTriangle, UserPlus, UserMinus, Eye, Pencil, Lock +} from 'lucide-react'; +import { useMockStore } from '../../data/mockStore'; +import { useAuth } from '../../context/AuthContext'; +import { toast } from 'sonner'; + +const COMMISSION_TYPES = [ + { value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' }, + { value: 'percent_gross', label: '% of Gross', hint: '% of Total Received' }, + { value: 'percent_net', label: '% of Net', hint: '% of Gross Profit' }, + { value: 'custom', label: 'Custom / Manual', hint: 'Manual entry' }, +]; + +const formatCurrency = (amt) => + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0); + +const formatCommission = (type, rate) => { + if (type === 'percent_gross' || type === 'percent_net') return `${rate}%`; + return formatCurrency(rate); +}; + +const getCommissionLabel = (type) => + COMMISSION_TYPES.find(c => c.value === type)?.label || type; + +// ──────────────────────────────────────────────────── +// Section Wrapper +// ──────────────────────────────────────────────────── +const SettingsSection = ({ id, icon: Icon, title, subtitle, color, isOpen, onToggle, children }) => ( +
+ + + {isOpen && ( + +
+ {children} +
+
+ )} +
+
+); + +// ──────────────────────────────────────────────────── +// 1. Organization Info Section +// ──────────────────────────────────────────────────── +const OrgInfoSection = ({ orgSettings, onSave }) => { + const [editing, setEditing] = useState(false); + const [local, setLocal] = useState({ ...orgSettings }); + + const handleSave = () => { + onSave(local); + setEditing(false); + toast.success('Organization settings saved'); + }; + + const handleCancel = () => { + setLocal({ ...orgSettings }); + setEditing(false); + }; + + const fields = [ + { key: 'orgName', label: 'Organization Name', icon: Building2 }, + { key: 'orgEmail', label: 'Email', icon: Mail }, + { key: 'orgPhone', label: 'Phone', icon: Phone }, + { key: 'orgAddress', label: 'Address', icon: MapPin }, + { key: 'timezone', label: 'Timezone', icon: Clock }, + ]; + + return ( +
+
+

Manage your organization's core details

+ {!editing ? ( + + ) : ( +
+ + +
+ )} +
+
+ {fields.map(({ key, label, icon: FIcon }) => ( +
+ + {editing ? ( + setLocal(prev => ({ ...prev, [key]: e.target.value }))} + 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-blue-500/50 transition-colors" + /> + ) : ( +
+ {orgSettings[key]} +
+ )} +
+ ))} +
+
+ ); +}; + +// ──────────────────────────────────────────────────── +// 2. Role Management Section +// ──────────────────────────────────────────────────── +const RoleManagementSection = ({ roles, members }) => { + const [expandedRole, setExpandedRole] = useState(null); + + return ( +
+

System roles define base-level access for your organization members

+ {roles.map(role => { + const roleMemberCount = members.filter(m => m.roleKey === role.key).length; + const roleMembers = members.filter(m => m.roleKey === role.key); + const isExpanded = expandedRole === role.id; + + return ( +
+ + + {isExpanded && ( + +
+ {roleMembers.length === 0 ? ( +

No members assigned to this role

+ ) : ( +
+ {roleMembers.map(member => ( +
+
+
+ {member.name.charAt(0)} +
+
+ {member.name} + {member.email} +
+
+
+ + {member.status} + +
+
+ ))} +
+ )} +
+
+ )} +
+
+ ); + })} +
+ ); +}; + +// ──────────────────────────────────────────────────── +// 3. Access Control Matrix +// ──────────────────────────────────────────────────── +const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }) => { + const togglePermission = (moduleKey, roleKey, action) => { + const current = permissions[moduleKey]?.[roleKey] || []; + const updated = current.includes(action) + ? current.filter(a => a !== action) + : [...current, action]; + onUpdate(moduleKey, roleKey, updated); + }; + + const moduleEntries = Object.entries(permissionModules); + + return ( +
+
+ Click cells to toggle permissions per role +
+ + {/* Desktop Matrix */} +
+ + + + + + {roles.map(role => ( + + ))} + + + + {moduleEntries.map(([moduleKey, module]) => + module.actions.map((action, actionIdx) => ( + + {actionIdx === 0 && ( + + )} + + {roles.map(role => { + const hasPermission = (permissions[moduleKey]?.[role.key] || []).includes(action); + const isOwner = role.key === 'OWNER'; + return ( + + ); + })} + + )) + )} + +
ModuleAction + {role.name.split(' ')[0]} +
+
+ + {module.label} +
+
{action} + {isOwner ? ( +
+
+ +
+
+ ) : ( + + )} +
+
+ + {/* Mobile Cards */} +
+ {roles.filter(r => r.key !== 'OWNER').map(role => ( +
+
+
+ {role.name} +
+ {moduleEntries.map(([moduleKey, module]) => ( +
+ {module.label} +
+ {module.actions.map(action => { + const hasPermission = (permissions[moduleKey]?.[role.key] || []).includes(action); + return ( + + ); + })} +
+
+ ))} +
+ ))} +
+
+ ); +}; + +// ──────────────────────────────────────────────────── +// 4. Commission Rules Section +// ──────────────────────────────────────────────────── +const CommissionRulesSection = ({ + orgDefaults, onSaveOrgDefaults, + roleOverrides, roles, onSaveRoleOverride, onRemoveRoleOverride, +}) => { + const [localOrgDefaults, setLocalOrgDefaults] = useState({ ...orgDefaults }); + const [editingOrg, setEditingOrg] = useState(false); + const [addingRole, setAddingRole] = useState(false); + const [newRoleOverride, setNewRoleOverride] = useState({ roleKey: '', type: 'flat', rate: 0 }); + + const availableRolesForOverride = useMemo(() => { + const overriddenKeys = new Set(roleOverrides.map(o => o.roleKey)); + return roles.filter(r => r.key !== 'OWNER' && !overriddenKeys.has(r.key)); + }, [roles, roleOverrides]); + + const handleSaveOrg = () => { + onSaveOrgDefaults({ type: localOrgDefaults.type, rate: Number(localOrgDefaults.rate) || 0 }); + setEditingOrg(false); + toast.success('Org commission defaults updated'); + }; + + const handleAddRoleOverride = () => { + if (!newRoleOverride.roleKey) return; + onSaveRoleOverride({ ...newRoleOverride, enabled: true, rate: Number(newRoleOverride.rate) || 0 }); + setAddingRole(false); + setNewRoleOverride({ roleKey: '', type: 'flat', rate: 0 }); + toast.success('Role commission override added'); + }; + + 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-400 dark:focus:border-blue-500/50 transition-colors"; + + return ( +
+ {/* Hierarchy Visualization */} +
+ {[ + { label: 'Org Default', color: '#00f0ff', icon: Building2 }, + { label: 'Role Override', color: '#10b981', icon: Shield }, + { label: 'Individual', color: '#a855f7', icon: User }, + { label: 'Job Override', color: '#fda913', icon: DollarSign }, + ].map((level, i) => ( + + {i > 0 && } +
+ + {level.label} +
+
+ ))} +
+

Highest priority on the right. Job override supersedes all.

+ + {/* Org Default */} +
+
+
+ + Org Default Commission +
+ {!editingOrg ? ( + + ) : ( +
+ + +
+ )} +
+ {editingOrg ? ( +
+
+ + +
+
+ + setLocalOrgDefaults(prev => ({ ...prev, rate: e.target.value }))} className={inputClasses} /> +
+
+ ) : ( +
+ + {getCommissionLabel(orgDefaults.type)} + {formatCommission(orgDefaults.type, orgDefaults.rate)} +
+ )} +
+ + {/* Role Overrides */} +
+
+
+ + Role Overrides +
+
+ + {roleOverrides.length === 0 && !addingRole && ( +

No role overrides — all roles use org default

+ )} + + {roleOverrides.map(ov => { + const role = roles.find(r => r.key === ov.roleKey); + return ( +
+
+
+ {role?.name || ov.roleKey} + + {getCommissionLabel(ov.type)} — {formatCommission(ov.type, ov.rate)} +
+
+ + +
+
+ ); + })} + + {addingRole ? ( +
+
+
+ + +
+
+ + +
+
+ + setNewRoleOverride(prev => ({ ...prev, rate: e.target.value }))} className={inputClasses} /> +
+
+
+ + +
+
+ ) : ( + + )} +
+ + {/* Info */} +
+
+ +
+

Commission Hierarchy: Job Override → Individual Override → Role Override → Org Default

+

The most specific level takes precedence. Job-level overrides (set on each project) always win.

+
+
+
+
+ ); +}; + +// ──────────────────────────────────────────────────── +// 5. User Overrides Section +// ──────────────────────────────────────────────────── +const UserOverridesSection = ({ members, roles, orgDefaults, roleOverrides, userCommissionOverrides, onSaveOverride, onRemoveOverride }) => { + const [searchTerm, setSearchTerm] = useState(''); + const [editingMember, setEditingMember] = useState(null); + const [editForm, setEditForm] = useState({ type: 'flat', rate: 0 }); + + const filteredMembers = useMemo(() => { + const term = searchTerm.toLowerCase(); + return members.filter(m => + m.name.toLowerCase().includes(term) || + m.email.toLowerCase().includes(term) || + m.roleKey.toLowerCase().includes(term) + ); + }, [members, searchTerm]); + + const getEffectiveCommission = (member) => { + const userOv = userCommissionOverrides.find(o => o.userId === member.userId && o.enabled); + if (userOv) return { source: 'individual', type: userOv.type, rate: userOv.rate }; + + const indOv = member.individualCommission; + if (indOv?.enabled) return { source: 'individual', type: indOv.type, rate: indOv.rate }; + + const roleOv = roleOverrides.find(o => o.roleKey === member.roleKey && o.enabled); + if (roleOv) return { source: 'role', type: roleOv.type, rate: roleOv.rate }; + + return { source: 'org', type: orgDefaults.type, rate: orgDefaults.rate }; + }; + + const sourceColors = { + individual: { bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20', text: 'text-purple-600 dark:text-purple-400', label: 'Individual' }, + role: { 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', label: 'Role' }, + org: { bg: 'bg-cyan-50 dark:bg-cyan-500/10', border: 'border-cyan-200 dark:border-cyan-500/20', text: 'text-cyan-600 dark:text-cyan-400', label: 'Org Default' }, + }; + + const startEdit = (member) => { + const effective = getEffectiveCommission(member); + setEditingMember(member.id); + setEditForm({ type: effective.type, rate: effective.rate }); + }; + + const handleSaveIndividual = (member) => { + onSaveOverride({ + userId: member.userId, + userName: member.name, + enabled: true, + type: editForm.type, + rate: Number(editForm.rate) || 0, + }); + setEditingMember(null); + toast.success(`Individual commission set for ${member.name}`); + }; + + const handleRemoveIndividual = (member) => { + onRemoveOverride(member.userId); + setEditingMember(null); + toast.success(`Individual override removed for ${member.name}`); + }; + + 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-purple-400 dark:focus:border-purple-500/50 transition-colors"; + + return ( +
+ {/* 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.5 text-sm text-zinc-900 dark:text-white placeholder-zinc-400 dark:placeholder-zinc-600 outline-none focus:border-purple-400 dark:focus:border-purple-500/50 transition-colors" + /> +
+ + {/* Member Cards */} +
+ {filteredMembers.map(member => { + const effective = getEffectiveCommission(member); + const sc = sourceColors[effective.source]; + const role = roles.find(r => r.key === member.roleKey); + const hasIndividualOverride = userCommissionOverrides.some(o => o.userId === member.userId && o.enabled) || member.individualCommission?.enabled; + const isEditing = editingMember === member.id; + + return ( +
+
+
+
+ {member.name.charAt(0)} +
+
+
+ {member.name} + {role?.name || member.roleKey} +
+ {member.email} +
+
+
+ {/* Commission Source Badge */} +
+ {sc.label} + {formatCommission(effective.type, effective.rate)} +
+ {member.roleKey !== 'OWNER' && ( + + )} +
+
+ + {/* Edit Form */} + + {isEditing && ( + +
+
+
+ + +
+
+ + setEditForm(prev => ({ ...prev, rate: e.target.value }))} className={inputClasses} /> +
+
+
+ {hasIndividualOverride && ( + + )} +
+ + +
+
+ + )} + +
+ ); + })} + + {filteredMembers.length === 0 && ( +
+ No members found matching "{searchTerm}" +
+ )} +
+
+ ); +}; + +// ──────────────────────────────────────────────────── +// Main Page +// ──────────────────────────────────────────────────── +const OrgSettings = () => { + const { user } = useAuth(); + const store = useMockStore(); + + const [openSections, setOpenSections] = useState({ + org: true, + roles: false, + access: false, + commission: false, + overrides: false, + }); + + const toggleSection = (key) => setOpenSections(prev => ({ ...prev, [key]: !prev[key] })); + + const sections = [ + { + id: 'org', + icon: Building2, + title: 'Organization Settings', + subtitle: 'Company name, contact info, and general preferences', + color: '#3b82f6', + content: ( + + ), + }, + { + id: 'roles', + icon: Crown, + title: 'Role Management', + subtitle: `${store.orgRoles.length} system roles configured`, + color: '#fda913', + content: ( + + ), + }, + { + id: 'access', + icon: Shield, + title: 'Access Control Matrix', + subtitle: 'Configure module-level permissions per role', + color: '#10b981', + content: ( + + ), + }, + { + id: 'commission', + icon: DollarSign, + title: 'Commission Rules', + subtitle: 'Org defaults and role-level commission overrides', + color: '#06b6d4', + content: ( + + ), + }, + { + id: 'overrides', + icon: Users, + title: 'User Overrides', + subtitle: `${store.orgMembers.length} members — manage individual commission overrides`, + color: '#a855f7', + content: ( + + ), + }, + ]; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

+ Org Settings +

+

+ Manage roles, permissions, and commission rules for your organization +

+
+
+ + {/* 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: 'Commission Overrides', value: store.roleCommissionOverrides.length + store.userCommissionOverrides.length, color: '#a855f7' }, + ].map(stat => ( +
+
{stat.value}
+
{stat.label}
+
+ ))} +
+
+ + {/* Sections */} +
+ {sections.map(section => ( + toggleSection(section.id)} + > + {section.content} + + ))} +
+
+ ); +}; + +export default OrgSettings;