Org settings module and user roles
This commit is contained in:
@@ -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 → Individual Override → Role Override → 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;
|
||||
Reference in New Issue
Block a user