Org settings module and user roles

This commit is contained in:
Satyam-Rastogi
2026-05-12 20:36:44 +05:30
parent 17c3dc7307
commit e85ecd2571
4 changed files with 998 additions and 19 deletions
+6
View File
@@ -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() {
<EstimateBuilder />
</ProtectedRoute>
} />
<Route path="/owner/settings" element={
<ProtectedRoute allowedRoles={['OWNER']}>
<OrgSettings />
</ProtectedRoute>
} />
{/* Protected Admin Routes — Owner has full admin access */}
<Route
+2 -1
View File
@@ -5,7 +5,7 @@ import { useTheme } from '../context/ThemeContext';
import {
LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare,
ChevronLeft, ChevronRight, Sun, Moon, Trophy, Users, Briefcase,
FileText, Menu, X, Calculator, PlusCircle, ClipboardList, Zap, LayoutGrid
FileText, Menu, X, Calculator, PlusCircle, ClipboardList, Zap, LayoutGrid, Settings
} from 'lucide-react';
import PageTransition from './PageTransition';
@@ -149,6 +149,7 @@ const Layout = () => {
{ 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':
+130 -18
View File
@@ -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}
+860
View File
@@ -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 }) => (
<div className="rounded-2xl border border-zinc-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] overflow-hidden shadow-sm dark:shadow-none">
<button
type="button"
onClick={onToggle}
className="w-full flex items-center justify-between px-6 py-5 hover:bg-zinc-50 dark:hover:bg-white/[0.02] transition-colors"
>
<div className="flex items-center gap-4">
<div className="p-2.5 rounded-xl" style={{ backgroundColor: `${color}15` }}>
<Icon size={20} style={{ color }} />
</div>
<div className="text-left">
<h2 className="text-base font-bold text-zinc-900 dark:text-white tracking-wide">{title}</h2>
<p className="text-xs text-zinc-500 mt-0.5">{subtitle}</p>
</div>
</div>
<ChevronDown
size={18}
className="text-zinc-400 dark:text-zinc-500 transition-transform duration-200"
style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25 }}
className="overflow-hidden"
>
<div className="px-6 pb-6 pt-2 border-t border-zinc-100 dark:border-white/5">
{children}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
// ────────────────────────────────────────────────────
// 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 (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-xs text-zinc-500">Manage your organization's core details</p>
{!editing ? (
<button onClick={() => setEditing(true)} className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-xs font-semibold text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-white/10 transition-colors">
<Edit3 size={12} /> Edit
</button>
) : (
<div className="flex gap-2">
<button onClick={handleCancel} className="px-3 py-1.5 rounded-lg border border-zinc-200 dark:border-white/10 text-xs font-semibold text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors">Cancel</button>
<button onClick={handleSave} className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-blue-50 dark:bg-blue-500/20 border border-blue-200 dark:border-blue-500/30 text-xs font-bold text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-500/30 transition-colors">
<Save size={12} /> Save
</button>
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{fields.map(({ key, label, icon: FIcon }) => (
<div key={key} className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 flex items-center gap-1.5">
<FIcon size={11} className="text-zinc-400 dark:text-zinc-600" /> {label}
</label>
{editing ? (
<input
value={local[key]}
onChange={e => 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"
/>
) : (
<div className="px-4 py-2.5 bg-zinc-50 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/5 rounded-xl text-sm text-zinc-700 dark:text-zinc-300">
{orgSettings[key]}
</div>
)}
</div>
))}
</div>
</div>
);
};
// ────────────────────────────────────────────────────
// 2. Role Management Section
// ────────────────────────────────────────────────────
const RoleManagementSection = ({ roles, members }) => {
const [expandedRole, setExpandedRole] = useState(null);
return (
<div className="space-y-3">
<p className="text-xs text-zinc-500 mb-4">System roles define base-level access for your organization members</p>
{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 (
<div key={role.id} className="rounded-xl border border-zinc-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] overflow-hidden">
<button
type="button"
onClick={() => setExpandedRole(isExpanded ? null : role.id)}
className="w-full flex items-center justify-between px-5 py-4 hover:bg-zinc-50 dark:hover:bg-white/[0.02] transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: role.color }} />
<div className="text-left">
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-zinc-900 dark:text-white">{role.name}</span>
{role.key === 'OWNER' && <Crown size={12} className="text-amber-400" />}
<span className="text-[10px] px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-zinc-500 font-mono">{role.key}</span>
</div>
<p className="text-[11px] text-zinc-500 mt-0.5">{role.description}</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-zinc-500 font-mono">{roleMemberCount} member{roleMemberCount !== 1 ? 's' : ''}</span>
<ChevronRight
size={14}
className="text-zinc-400 dark:text-zinc-600 transition-transform duration-200"
style={{ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)' }}
/>
</div>
</button>
<AnimatePresence>
{isExpanded && (
<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="px-5 pb-4 pt-1 border-t border-zinc-100 dark:border-white/5">
{roleMembers.length === 0 ? (
<p className="text-xs text-zinc-400 dark:text-zinc-600 italic py-2">No members assigned to this role</p>
) : (
<div className="space-y-2 mt-2">
{roleMembers.map(member => (
<div key={member.id} className="flex items-center justify-between px-3 py-2 rounded-lg bg-zinc-50 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/5">
<div className="flex items-center gap-3">
<div className="w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-bold" style={{ backgroundColor: `${role.color}20`, color: role.color }}>
{member.name.charAt(0)}
</div>
<div>
<span className="text-xs font-semibold text-zinc-900 dark:text-white">{member.name}</span>
<span className="text-[10px] text-zinc-500 ml-2">{member.email}</span>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`text-[10px] px-2 py-0.5 rounded-full font-semibold ${member.status === 'active' ? 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/20' : 'bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20'}`}>
{member.status}
</span>
</div>
</div>
))}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
);
};
// ────────────────────────────────────────────────────
// 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 (
<div className="space-y-4">
<div className="flex items-center gap-2 text-xs text-zinc-500">
<Info size={12} /> Click cells to toggle permissions per role
</div>
{/* Desktop Matrix */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="text-left text-[10px] font-bold uppercase tracking-wider text-zinc-500 px-4 py-3 border-b border-zinc-200 dark:border-white/5 w-48">Module</th>
<th className="text-left text-[10px] font-bold uppercase tracking-wider text-zinc-500 px-4 py-3 border-b border-zinc-200 dark:border-white/5 w-28">Action</th>
{roles.map(role => (
<th key={role.key} className="text-center text-[10px] font-bold uppercase tracking-wider px-3 py-3 border-b border-zinc-200 dark:border-white/5 min-w-[90px]" style={{ color: role.color }}>
{role.name.split(' ')[0]}
</th>
))}
</tr>
</thead>
<tbody>
{moduleEntries.map(([moduleKey, module]) =>
module.actions.map((action, actionIdx) => (
<tr key={`${moduleKey}-${action}`} className="hover:bg-zinc-50 dark:hover:bg-white/[0.02] transition-colors">
{actionIdx === 0 && (
<td
className="text-xs font-semibold text-zinc-900 dark:text-white px-4 py-2.5 border-b border-zinc-100 dark:border-white/5 align-top"
rowSpan={module.actions.length}
>
<div className="flex items-center gap-2">
<Shield size={12} className="text-zinc-400 dark:text-zinc-600" />
{module.label}
</div>
</td>
)}
<td className="text-[11px] text-zinc-500 dark:text-zinc-400 px-4 py-2.5 border-b border-zinc-100 dark:border-white/5 capitalize font-mono">{action}</td>
{roles.map(role => {
const hasPermission = (permissions[moduleKey]?.[role.key] || []).includes(action);
const isOwner = role.key === 'OWNER';
return (
<td key={role.key} className="text-center px-3 py-2.5 border-b border-zinc-100 dark:border-white/5">
{isOwner ? (
<div className="flex justify-center">
<div className="w-6 h-6 rounded-md bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 flex items-center justify-center" title="Owner always has full access">
<Lock size={10} className="text-amber-500 dark:text-amber-400" />
</div>
</div>
) : (
<button
type="button"
onClick={() => togglePermission(moduleKey, role.key, action)}
className={`mx-auto w-6 h-6 rounded-md border flex items-center justify-center transition-all duration-150 ${
hasPermission
? 'border-emerald-300 dark:border-emerald-500/30 bg-emerald-50 dark:bg-emerald-500/15 hover:bg-emerald-100 dark:hover:bg-emerald-500/25'
: 'border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/[0.03] hover:bg-zinc-100 dark:hover:bg-white/[0.06]'
}`}
>
{hasPermission && <Check size={12} className="text-emerald-500 dark:text-emerald-400" />}
</button>
)}
</td>
);
})}
</tr>
))
)}
</tbody>
</table>
</div>
{/* Mobile Cards */}
<div className="lg:hidden space-y-3">
{roles.filter(r => r.key !== 'OWNER').map(role => (
<div key={role.key} className="rounded-xl border border-zinc-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] p-4 space-y-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: role.color }} />
<span className="text-sm font-bold text-zinc-900 dark:text-white">{role.name}</span>
</div>
{moduleEntries.map(([moduleKey, module]) => (
<div key={moduleKey} className="space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-zinc-500">{module.label}</span>
<div className="flex flex-wrap gap-1.5">
{module.actions.map(action => {
const hasPermission = (permissions[moduleKey]?.[role.key] || []).includes(action);
return (
<button
key={action}
type="button"
onClick={() => togglePermission(moduleKey, role.key, action)}
className={`px-2.5 py-1 rounded-md text-[10px] font-semibold capitalize border transition-colors ${
hasPermission
? 'bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
: 'bg-zinc-50 dark:bg-white/[0.03] border-zinc-200 dark:border-white/10 text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400'
}`}
>
{action}
</button>
);
})}
</div>
</div>
))}
</div>
))}
</div>
</div>
);
};
// ────────────────────────────────────────────────────
// 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 (
<div className="space-y-6">
{/* Hierarchy Visualization */}
<div className="flex items-center justify-center gap-2 text-[10px] font-bold uppercase tracking-widest py-3 flex-wrap">
{[
{ 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) => (
<React.Fragment key={level.label}>
{i > 0 && <ArrowRight size={10} className="text-zinc-300 dark:text-zinc-700 mx-0.5" />}
<div className="flex items-center gap-1 px-2.5 py-1 rounded-full border" style={{ backgroundColor: `${level.color}10`, borderColor: `${level.color}25`, color: level.color }}>
<level.icon size={10} />
<span>{level.label}</span>
</div>
</React.Fragment>
))}
</div>
<p className="text-center text-[10px] text-zinc-400 dark:text-zinc-600">Highest priority on the right. Job override supersedes all.</p>
{/* Org Default */}
<div className="rounded-xl border border-cyan-200 dark:border-cyan-500/10 bg-cyan-50/50 dark:bg-cyan-500/[0.03] p-5 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Building2 size={16} className="text-cyan-500 dark:text-cyan-400" />
<span className="text-sm font-bold text-zinc-900 dark:text-white">Org Default Commission</span>
</div>
{!editingOrg ? (
<button onClick={() => setEditingOrg(true)} className="text-[10px] font-semibold text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300 transition-colors">Edit</button>
) : (
<div className="flex gap-2">
<button onClick={() => { setLocalOrgDefaults({ ...orgDefaults }); setEditingOrg(false); }} className="text-[10px] font-semibold text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300">Cancel</button>
<button onClick={handleSaveOrg} className="text-[10px] font-bold text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300">Save</button>
</div>
)}
</div>
{editingOrg ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">Type</label>
<select value={localOrgDefaults.type} onChange={e => setLocalOrgDefaults(prev => ({ ...prev, type: e.target.value }))} className={inputClasses}>
{COMMISSION_TYPES.map(ct => <option key={ct.value} value={ct.value}>{ct.label}</option>)}
</select>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">Rate</label>
<input type="number" value={localOrgDefaults.rate} onChange={e => setLocalOrgDefaults(prev => ({ ...prev, rate: e.target.value }))} className={inputClasses} />
</div>
</div>
) : (
<div className="flex items-center gap-3 px-4 py-3 bg-white/60 dark:bg-white/[0.03] rounded-xl border border-zinc-200 dark:border-white/5">
<DollarSign size={14} className="text-cyan-500 dark:text-cyan-400" />
<span className="text-sm text-zinc-700 dark:text-zinc-300 font-semibold">{getCommissionLabel(orgDefaults.type)}</span>
<span className="text-sm font-mono text-cyan-600 dark:text-cyan-400">{formatCommission(orgDefaults.type, orgDefaults.rate)}</span>
</div>
)}
</div>
{/* Role Overrides */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield size={16} className="text-emerald-500 dark:text-emerald-400" />
<span className="text-sm font-bold text-zinc-900 dark:text-white">Role Overrides</span>
</div>
</div>
{roleOverrides.length === 0 && !addingRole && (
<p className="text-xs text-zinc-400 dark:text-zinc-600 italic px-2">No role overrides — all roles use org default</p>
)}
{roleOverrides.map(ov => {
const role = roles.find(r => r.key === ov.roleKey);
return (
<div key={ov.roleKey} className="flex items-center justify-between px-4 py-3 rounded-xl bg-zinc-50 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/5">
<div className="flex items-center gap-3 flex-wrap">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: role?.color || '#10b981' }} />
<span className="text-sm font-semibold text-zinc-900 dark:text-white">{role?.name || ov.roleKey}</span>
<ArrowRight size={12} className="text-zinc-400 dark:text-zinc-600" />
<span className="text-xs font-mono text-emerald-600 dark:text-emerald-400">{getCommissionLabel(ov.type)} — {formatCommission(ov.type, ov.rate)}</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
onSaveRoleOverride({ ...ov, enabled: !ov.enabled });
}}
className="p-1 hover:bg-zinc-100 dark:hover:bg-white/10 rounded-lg transition-colors"
>
{ov.enabled ? <ToggleRight size={18} className="text-emerald-500 dark:text-emerald-400" /> : <ToggleLeft size={18} className="text-zinc-400 dark:text-zinc-600" />}
</button>
<button
type="button"
onClick={() => onRemoveRoleOverride(ov.roleKey)}
className="p-1 hover:bg-red-50 dark:hover:bg-red-500/10 text-zinc-400 dark:text-zinc-600 hover:text-red-500 dark:hover:text-red-400 rounded-lg transition-colors"
>
<Trash2 size={14} />
</button>
</div>
</div>
);
})}
{addingRole ? (
<div className="rounded-xl border border-emerald-200 dark:border-emerald-500/20 bg-emerald-50/50 dark:bg-emerald-500/[0.03] p-4 space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">Role</label>
<select value={newRoleOverride.roleKey} onChange={e => setNewRoleOverride(prev => ({ ...prev, roleKey: e.target.value }))} className={inputClasses}>
<option value="">— Select Role —</option>
{availableRolesForOverride.map(r => <option key={r.key} value={r.key}>{r.name}</option>)}
</select>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">Type</label>
<select value={newRoleOverride.type} onChange={e => setNewRoleOverride(prev => ({ ...prev, type: e.target.value }))} className={inputClasses}>
{COMMISSION_TYPES.map(ct => <option key={ct.value} value={ct.value}>{ct.label}</option>)}
</select>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">Rate</label>
<input type="number" value={newRoleOverride.rate} onChange={e => setNewRoleOverride(prev => ({ ...prev, rate: e.target.value }))} className={inputClasses} />
</div>
</div>
<div className="flex gap-2 pt-1">
<button onClick={() => setAddingRole(false)} className="flex-1 px-4 py-2 rounded-xl border border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 text-xs font-semibold hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors">Cancel</button>
<button onClick={handleAddRoleOverride} disabled={!newRoleOverride.roleKey} className="flex-1 px-4 py-2 rounded-xl bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-500/30 text-xs font-bold hover:bg-emerald-200 dark:hover:bg-emerald-500/30 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">Add Override</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setAddingRole(true)}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-dashed border-emerald-300 dark:border-emerald-500/30 text-emerald-600 dark:text-emerald-400 text-xs font-bold hover:bg-emerald-50 dark:hover:bg-emerald-500/5 transition-colors"
>
<Plus size={14} /> Add Role Override
</button>
)}
</div>
{/* Info */}
<div className="p-4 bg-zinc-50 dark:bg-white/[0.02] rounded-xl border border-zinc-200 dark:border-white/5">
<div className="flex items-start gap-2">
<Info size={14} className="text-zinc-400 dark: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-700 dark:text-zinc-400">Commission Hierarchy:</span> Job Override &rarr; Individual Override &rarr; Role Override &rarr; Org Default</p>
<p>The most specific level takes precedence. Job-level overrides (set on each project) always win.</p>
</div>
</div>
</div>
</div>
);
};
// ────────────────────────────────────────────────────
// 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 (
<div className="space-y-4">
{/* Search */}
<div className="relative">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400 dark:text-zinc-500" />
<input
type="text"
placeholder="Search members by name, email, or role..."
value={searchTerm}
onChange={e => 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"
/>
</div>
{/* Member Cards */}
<div className="space-y-2 max-h-[500px] overflow-y-auto custom-scrollbar pr-1">
{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 (
<div key={member.id} className="rounded-xl border border-zinc-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] overflow-hidden">
<div className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold shrink-0" style={{ backgroundColor: `${role?.color || '#3b82f6'}20`, color: role?.color || '#3b82f6' }}>
{member.name.charAt(0)}
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-zinc-900 dark:text-white truncate">{member.name}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded-full border border-zinc-200 dark:border-white/10 text-zinc-500 font-mono shrink-0">{role?.name || member.roleKey}</span>
</div>
<span className="text-[10px] text-zinc-500">{member.email}</span>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
{/* Commission Source Badge */}
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-full border text-[10px] font-bold ${sc.bg} ${sc.border} ${sc.text}`}>
<span>{sc.label}</span>
<span className="font-mono">{formatCommission(effective.type, effective.rate)}</span>
</div>
{member.roleKey !== 'OWNER' && (
<button
type="button"
onClick={() => isEditing ? setEditingMember(null) : startEdit(member)}
className="p-1.5 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 text-zinc-400 dark:text-zinc-500 hover:text-zinc-700 dark:hover:text-white transition-colors"
>
{isEditing ? <X size={14} /> : <Pencil size={12} />}
</button>
)}
</div>
</div>
{/* Edit Form */}
<AnimatePresence>
{isEditing && (
<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="px-4 pb-4 pt-2 border-t border-zinc-100 dark:border-white/5 space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">Commission Type</label>
<select value={editForm.type} onChange={e => setEditForm(prev => ({ ...prev, type: e.target.value }))} className={inputClasses}>
{COMMISSION_TYPES.map(ct => <option key={ct.value} value={ct.value}>{ct.label}</option>)}
</select>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 block mb-1.5">
{editForm.type.includes('percent') ? 'Rate (%)' : 'Amount ($)'}
</label>
<input type="number" value={editForm.rate} onChange={e => setEditForm(prev => ({ ...prev, rate: e.target.value }))} className={inputClasses} />
</div>
</div>
<div className="flex gap-2">
{hasIndividualOverride && (
<button onClick={() => handleRemoveIndividual(member)} className="px-3 py-2 rounded-xl border border-red-200 dark:border-red-500/20 text-red-600 dark:text-red-400 text-xs font-semibold hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors">
Remove Override
</button>
)}
<div className="flex-1" />
<button onClick={() => setEditingMember(null)} className="px-4 py-2 rounded-xl border border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 text-xs font-semibold hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors">Cancel</button>
<button onClick={() => handleSaveIndividual(member)} className="px-4 py-2 rounded-xl bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-500/30 text-xs font-bold hover:bg-purple-200 dark:hover:bg-purple-500/30 transition-colors">Save Override</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
{filteredMembers.length === 0 && (
<div className="text-center py-8 text-zinc-400 dark:text-zinc-600 text-sm">
No members found matching "{searchTerm}"
</div>
)}
</div>
</div>
);
};
// ────────────────────────────────────────────────────
// 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: (
<OrgInfoSection
orgSettings={store.orgSettings}
onSave={store.updateOrgSettings}
/>
),
},
{
id: 'roles',
icon: Crown,
title: 'Role Management',
subtitle: `${store.orgRoles.length} system roles configured`,
color: '#fda913',
content: (
<RoleManagementSection
roles={store.orgRoles}
members={store.orgMembers}
/>
),
},
{
id: 'access',
icon: Shield,
title: 'Access Control Matrix',
subtitle: 'Configure module-level permissions per role',
color: '#10b981',
content: (
<AccessControlMatrix
permissions={store.orgPermissions}
permissionModules={store.PERMISSION_MODULES}
roles={store.orgRoles}
onUpdate={store.updateOrgPermissions}
/>
),
},
{
id: 'commission',
icon: DollarSign,
title: 'Commission Rules',
subtitle: 'Org defaults and role-level commission overrides',
color: '#06b6d4',
content: (
<CommissionRulesSection
orgDefaults={store.orgCommissionDefaults}
onSaveOrgDefaults={store.updateOrgCommissionDefaults}
roleOverrides={store.roleCommissionOverrides}
roles={store.orgRoles}
onSaveRoleOverride={store.setRoleCommissionOverride}
onRemoveRoleOverride={store.removeRoleCommissionOverride}
/>
),
},
{
id: 'overrides',
icon: Users,
title: 'User Overrides',
subtitle: `${store.orgMembers.length} members — manage individual commission overrides`,
color: '#a855f7',
content: (
<UserOverridesSection
members={store.orgMembers}
roles={store.orgRoles}
orgDefaults={store.orgCommissionDefaults}
roleOverrides={store.roleCommissionOverrides}
userCommissionOverrides={store.userCommissionOverrides}
onSaveOverride={store.setUserCommissionOverride}
onRemoveOverride={store.removeUserCommissionOverride}
/>
),
},
];
return (
<div className="min-h-screen bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white pb-12 transition-colors duration-300">
{/* Header */}
<div className="px-6 md:px-10 pt-8 pb-6">
<div className="flex items-center gap-4 mb-1">
<div className="p-3 rounded-2xl bg-gradient-to-br from-blue-100 to-purple-100 dark:from-blue-500/20 dark:to-purple-500/20 border border-zinc-200 dark:border-white/10">
<Settings size={24} className="text-blue-500 dark:text-blue-400" />
</div>
<div>
<h1 className="text-2xl md:text-3xl font-bold text-zinc-900 dark:text-white tracking-tight">
Org Settings
</h1>
<p className="text-sm text-zinc-500 mt-0.5">
Manage roles, permissions, and commission rules for your organization
</p>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-6">
{[
{ 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 => (
<div key={stat.label} className="px-4 py-3 rounded-xl bg-white dark:bg-white/[0.03] border border-zinc-200 dark:border-white/5 shadow-sm dark:shadow-none">
<div className="text-2xl font-bold font-mono" style={{ color: stat.color }}>{stat.value}</div>
<div className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 mt-0.5">{stat.label}</div>
</div>
))}
</div>
</div>
{/* Sections */}
<div className="px-6 md:px-10 space-y-4">
{sections.map(section => (
<SettingsSection
key={section.id}
id={section.id}
icon={section.icon}
title={section.title}
subtitle={section.subtitle}
color={section.color}
isOpen={openSections[section.id]}
onToggle={() => toggleSection(section.id)}
>
{section.content}
</SettingsSection>
))}
</div>
</div>
);
};
export default OrgSettings;