adds access modifiers for each role

This commit is contained in:
Satyam-Rastogi
2026-05-12 21:08:19 +05:30
parent e85ecd2571
commit 7598f8982a
7 changed files with 517 additions and 119 deletions
+17 -1
View File
@@ -1,11 +1,13 @@
import React, { useState, useMemo } from 'react';
import { useMockStore } from '../data/mockStore';
import { Trophy, TrendingUp, DollarSign, Target, Calendar, Medal, ArrowUpRight, Crown } from 'lucide-react';
import { Trophy, TrendingUp, DollarSign, Target, Calendar, Medal, ArrowUpRight, Crown, ShieldOff } from 'lucide-react';
import { SpotlightCard } from '../components/SpotlightCard';
import AnimatedNumber from '../components/AnimatedNumber';
import { usePermissions } from '../hooks/usePermissions';
const LeaderboardPage = () => {
const { users, salesHistory } = useMockStore();
const { can } = usePermissions();
const [timeframe, setTimeframe] = useState('month'); // 'week', 'month', 'custom'
const [metric, setMetric] = useState('revenue'); // 'revenue', 'volume', 'winRate'
@@ -108,6 +110,20 @@ const LeaderboardPage = () => {
);
};
if (!can('leaderboard', 'view')) {
return (
<div className="min-h-full flex items-center justify-center p-8">
<div className="text-center space-y-4 max-w-md">
<div className="mx-auto w-16 h-16 rounded-2xl bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
<ShieldOff size={28} className="text-zinc-400" />
</div>
<h2 className="text-xl font-bold text-zinc-900 dark:text-white">Access Restricted</h2>
<p className="text-sm text-zinc-500">You don't have permission to view the Leaderboard. Contact your org admin for access.</p>
</div>
</div>
);
}
return (
<div className="min-h-full p-8 max-w-7xl mx-auto space-y-8 pb-20">
{/* Header */}
+20 -10
View File
@@ -3,9 +3,11 @@ import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { useTheme } from '../context/ThemeContext';
import { useMockStore } from '../data/mockStore';
import { usePermissions } from '../hooks/usePermissions';
import PermissionGate from '../components/PermissionGate';
import {
Plus, Search, MapPin, Phone, Zap, FileText,
Clock, User, ChevronRight, Filter,
Clock, User, ChevronRight, Filter, Lock,
} from 'lucide-react';
// ---------------------------------------------------------------------------
@@ -183,6 +185,7 @@ function EmptyState({ isDark, onNewLead }) {
export default function LeadsListPage() {
const { theme } = useTheme();
const { leads } = useMockStore();
const { can } = usePermissions();
const navigate = useNavigate();
const isDark = theme === 'dark';
@@ -214,15 +217,22 @@ export default function LeadsListPage() {
{leads.length} total lead{leads.length !== 1 ? 's' : ''}
</p>
</div>
<button
type="button"
onClick={() => navigate('/emp/fa/leads/new')}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl font-bold text-sm uppercase tracking-widest text-white transition-all active:scale-[0.97] hover:opacity-90 shrink-0"
style={{ backgroundColor: '#3B82F6' }}
>
<Plus size={14} />
New Lead
</button>
{can('leads', 'log') ? (
<button
type="button"
onClick={() => navigate('/emp/fa/leads/new')}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl font-bold text-sm uppercase tracking-widest text-white transition-all active:scale-[0.97] hover:opacity-90 shrink-0"
style={{ backgroundColor: '#3B82F6' }}
>
<Plus size={14} />
New Lead
</button>
) : (
<div className="flex items-center gap-2 px-4 py-2.5 rounded-xl font-bold text-sm uppercase tracking-widest text-zinc-400 bg-zinc-200 dark:bg-zinc-800 cursor-not-allowed shrink-0" title="You don't have permission to create leads">
<Lock size={14} />
New Lead
</div>
)}
</div>
{/* --- Search bar --- */}
+329 -57
View File
@@ -1,14 +1,16 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useCallback, useRef } 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
AlertTriangle, UserPlus, UserMinus, Eye, Pencil, Lock,
RotateCcw, Filter, CheckSquare, Square, BarChart3, History
} from 'lucide-react';
import { useMockStore } from '../../data/mockStore';
import { useAuth } from '../../context/AuthContext';
import { toast } from 'sonner';
import { computeRolePermissionStats } from '../../utils/permissions';
const COMMISSION_TYPES = [
{ value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' },
@@ -225,35 +227,213 @@ const RoleManagementSection = ({ roles, members }) => {
};
// ────────────────────────────────────────────────────
// 3. Access Control Matrix
// 3. Access Control Matrix (Full)
// ────────────────────────────────────────────────────
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 [filterModule, setFilterModule] = useState('all');
const [filterRole, setFilterRole] = useState('all');
const [changeLog, setChangeLog] = useState([]);
const [showLog, setShowLog] = useState(false);
const snapshotRef = useRef(JSON.stringify(permissions));
const moduleEntries = Object.entries(permissionModules);
const moduleEntries = useMemo(() => {
const entries = Object.entries(permissionModules);
if (filterModule === 'all') return entries;
return entries.filter(([key]) => key === filterModule);
}, [permissionModules, filterModule]);
const visibleRoles = useMemo(() => {
if (filterRole === 'all') return roles;
return roles.filter(r => r.key === filterRole);
}, [roles, filterRole]);
const isDirty = JSON.stringify(permissions) !== snapshotRef.current;
const togglePermission = useCallback((moduleKey, roleKey, action) => {
const current = permissions[moduleKey]?.[roleKey] || [];
const has = current.includes(action);
const updated = has ? current.filter(a => a !== action) : [...current, action];
onUpdate(moduleKey, roleKey, updated);
const modLabel = permissionModules[moduleKey]?.label || moduleKey;
const roleName = roles.find(r => r.key === roleKey)?.name || roleKey;
setChangeLog(prev => [
{ id: Date.now(), module: modLabel, role: roleName, action, granted: !has, time: new Date().toLocaleTimeString() },
...prev.slice(0, 49),
]);
}, [permissions, onUpdate, permissionModules, roles]);
const toggleAllForRole = useCallback((roleKey, grant) => {
for (const [moduleKey, mod] of Object.entries(permissionModules)) {
onUpdate(moduleKey, roleKey, grant ? [...mod.actions] : []);
}
const roleName = roles.find(r => r.key === roleKey)?.name || roleKey;
toast.success(grant ? `All permissions granted for ${roleName}` : `All permissions revoked for ${roleName}`);
}, [permissionModules, onUpdate, roles]);
const toggleAllForModule = useCallback((moduleKey, grant) => {
const mod = permissionModules[moduleKey];
if (!mod) return;
for (const role of roles) {
if (role.key === 'OWNER') continue;
onUpdate(moduleKey, role.key, grant ? [...mod.actions] : []);
}
toast.success(grant ? `All roles granted access to ${mod.label}` : `All roles revoked from ${mod.label}`);
}, [permissionModules, roles, onUpdate]);
const handleReset = useCallback(() => {
const snap = JSON.parse(snapshotRef.current);
for (const [moduleKey, roleMap] of Object.entries(snap)) {
for (const [roleKey, actions] of Object.entries(roleMap)) {
onUpdate(moduleKey, roleKey, actions);
}
}
setChangeLog([]);
toast.success('Permissions reset to last saved state');
}, [onUpdate]);
const handleSave = useCallback(() => {
snapshotRef.current = JSON.stringify(permissions);
setChangeLog([]);
toast.success('Access control permissions saved');
}, [permissions]);
const roleStats = useMemo(() => {
const map = {};
for (const role of roles) {
if (role.key === 'OWNER') {
const total = Object.values(permissionModules).reduce((s, m) => s + m.actions.length, 0);
map[role.key] = { granted: total, total };
} else {
map[role.key] = computeRolePermissionStats(role.key, permissions, permissionModules);
}
}
return map;
}, [roles, permissions, 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 className="space-y-5">
{/* ── Toolbar ── */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div className="flex items-center gap-2 flex-wrap">
{/* Module filter */}
<div className="flex items-center gap-1.5">
<Filter size={12} className="text-zinc-400" />
<select
value={filterModule}
onChange={e => setFilterModule(e.target.value)}
className="bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-lg px-3 py-1.5 text-xs text-zinc-700 dark:text-zinc-300 outline-none"
>
<option value="all">All Modules</option>
{Object.entries(permissionModules).map(([key, mod]) => (
<option key={key} value={key}>{mod.label}</option>
))}
</select>
</div>
{/* Role filter */}
<select
value={filterRole}
onChange={e => setFilterRole(e.target.value)}
className="bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-lg px-3 py-1.5 text-xs text-zinc-700 dark:text-zinc-300 outline-none"
>
<option value="all">All Roles</option>
{roles.map(r => (
<option key={r.key} value={r.key}>{r.name}</option>
))}
</select>
{/* Change log toggle */}
<button
type="button"
onClick={() => setShowLog(p => !p)}
className={`flex items-center gap-1 px-2.5 py-1.5 rounded-lg border text-[10px] font-semibold transition-colors ${
showLog
? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/20 text-blue-600 dark:text-blue-400'
: 'bg-zinc-50 dark:bg-white/[0.03] border-zinc-200 dark:border-white/10 text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
}`}
>
<History size={10} />
Log{changeLog.length > 0 && ` (${changeLog.length})`}
</button>
</div>
<div className="flex items-center gap-2">
{isDirty && (
<button
type="button"
onClick={handleReset}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg border border-zinc-200 dark:border-white/10 text-xs font-semibold text-zinc-500 hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors"
>
<RotateCcw size={11} /> Reset
</button>
)}
<button
type="button"
onClick={handleSave}
disabled={!isDirty}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-emerald-100 dark:bg-emerald-500/20 border border-emerald-200 dark:border-emerald-500/30 text-xs font-bold text-emerald-700 dark:text-emerald-300 hover:bg-emerald-200 dark:hover:bg-emerald-500/30 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<Save size={11} /> Save Changes
</button>
</div>
</div>
{/* Desktop Matrix */}
<div className="hidden lg:block overflow-x-auto">
{/* ── Role Summary Stats ── */}
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
{roles.map(role => {
const stats = roleStats[role.key];
const pct = stats.total > 0 ? Math.round((stats.granted / stats.total) * 100) : 0;
return (
<div key={role.key} className="px-3 py-2.5 rounded-xl bg-zinc-50 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/5">
<div className="flex items-center gap-1.5 mb-1.5">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: role.color }} />
<span className="text-[10px] font-bold uppercase tracking-wider text-zinc-500">{role.name.split(' ')[0]}</span>
</div>
<div className="flex items-end justify-between">
<span className="text-lg font-bold font-mono" style={{ color: role.color }}>{stats.granted}</span>
<span className="text-[10px] text-zinc-400">/ {stats.total}</span>
</div>
<div className="mt-1.5 h-1 rounded-full bg-zinc-200 dark:bg-white/10 overflow-hidden">
<div
className="h-full rounded-full transition-all duration-300"
style={{ width: `${pct}%`, backgroundColor: role.color }}
/>
</div>
</div>
);
})}
</div>
{/* ── Desktop Matrix ── */}
<div className="hidden lg:block overflow-x-auto rounded-xl border border-zinc-200 dark:border-white/[0.06]">
<table className="w-full border-collapse">
<thead>
<tr>
<tr className="bg-zinc-50/50 dark:bg-white/[0.02]">
<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]}
{visibleRoles.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-[100px]">
<div className="flex flex-col items-center gap-1">
<span style={{ color: role.color }}>{role.name.split(' ')[0]}</span>
{role.key !== 'OWNER' && (
<div className="flex gap-0.5">
<button
type="button"
onClick={() => toggleAllForRole(role.key, true)}
title={`Grant all to ${role.name}`}
className="p-0.5 rounded hover:bg-emerald-50 dark:hover:bg-emerald-500/10 text-zinc-400 hover:text-emerald-500 transition-colors"
>
<CheckSquare size={10} />
</button>
<button
type="button"
onClick={() => toggleAllForRole(role.key, false)}
title={`Revoke all from ${role.name}`}
className="p-0.5 rounded hover:bg-red-50 dark:hover:bg-red-500/10 text-zinc-400 hover:text-red-500 transition-colors"
>
<Square size={10} />
</button>
</div>
)}
</div>
</th>
))}
</tr>
@@ -267,14 +447,34 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
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 className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Shield size={12} className="text-zinc-400 dark:text-zinc-600" />
{module.label}
</div>
<div className="flex gap-0.5">
<button
type="button"
onClick={() => toggleAllForModule(moduleKey, true)}
title={`Grant all roles for ${module.label}`}
className="p-0.5 rounded hover:bg-emerald-50 dark:hover:bg-emerald-500/10 text-zinc-300 dark:text-zinc-700 hover:text-emerald-500 transition-colors"
>
<CheckSquare size={10} />
</button>
<button
type="button"
onClick={() => toggleAllForModule(moduleKey, false)}
title={`Revoke all roles for ${module.label}`}
className="p-0.5 rounded hover:bg-red-50 dark:hover:bg-red-500/10 text-zinc-300 dark:text-zinc-700 hover:text-red-500 transition-colors"
>
<Square size={10} />
</button>
</div>
</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 => {
{visibleRoles.map(role => {
const hasPermission = (permissions[moduleKey]?.[role.key] || []).includes(action);
const isOwner = role.key === 'OWNER';
return (
@@ -289,13 +489,14 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
<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 ${
className={`mx-auto w-7 h-7 rounded-lg 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-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 shadow-sm shadow-emerald-500/10'
: '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]'
}`}
title={`${hasPermission ? 'Revoke' : 'Grant'} ${action} on ${module.label} for ${role.name}`}
>
{hasPermission && <Check size={12} className="text-emerald-500 dark:text-emerald-400" />}
{hasPermission && <Check size={13} className="text-emerald-500 dark:text-emerald-400" strokeWidth={2.5} />}
</button>
)}
</td>
@@ -308,40 +509,111 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
</table>
</div>
{/* Mobile Cards */}
{/* ── 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>
);
})}
{visibleRoles.filter(r => r.key !== 'OWNER').map(role => {
const stats = roleStats[role.key];
return (
<div key={role.key} 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 bg-zinc-50/50 dark:bg-white/[0.02] border-b border-zinc-100 dark:border-white/5">
<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>
<span className="text-[10px] font-mono text-zinc-400">{stats.granted}/{stats.total}</span>
</div>
<div className="flex gap-1">
<button type="button" onClick={() => toggleAllForRole(role.key, true)} className="px-2 py-1 rounded-md text-[9px] font-bold bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/20">All</button>
<button type="button" onClick={() => toggleAllForRole(role.key, false)} className="px-2 py-1 rounded-md text-[9px] font-bold bg-zinc-100 dark:bg-white/5 text-zinc-500 border border-zinc-200 dark:border-white/10">None</button>
</div>
</div>
))}
<div className="p-4 space-y-3">
{moduleEntries.map(([moduleKey, module]) => (
<div key={moduleKey} className="space-y-1.5">
<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'
}`}
>
{hasPermission && <Check size={8} className="inline mr-0.5 -mt-0.5" />}
{action}
</button>
);
})}
</div>
</div>
))}
</div>
</div>
);
})}
</div>
{/* ── Change Log ── */}
<AnimatePresence>
{showLog && (
<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="rounded-xl border border-blue-200 dark:border-blue-500/10 bg-blue-50/50 dark:bg-blue-500/[0.03] p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<History size={14} className="text-blue-500 dark:text-blue-400" />
<span className="text-xs font-bold text-zinc-700 dark:text-white">Change Log</span>
<span className="text-[10px] text-zinc-500">(unsaved session)</span>
</div>
{changeLog.length > 0 && (
<button type="button" onClick={() => setChangeLog([])} className="text-[10px] text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300">Clear</button>
)}
</div>
{changeLog.length === 0 ? (
<p className="text-xs text-zinc-400 dark:text-zinc-600 italic">No changes yet</p>
) : (
<div className="space-y-1 max-h-40 overflow-y-auto custom-scrollbar">
{changeLog.map(entry => (
<div key={entry.id} className="flex items-center gap-2 text-[11px] px-2 py-1 rounded-md bg-white/60 dark:bg-white/[0.03]">
<span className="text-zinc-400 font-mono text-[9px] w-16 shrink-0">{entry.time}</span>
<span className={`font-bold ${entry.granted ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500 dark:text-red-400'}`}>
{entry.granted ? '+' : '-'}
</span>
<span className="text-zinc-700 dark:text-zinc-300">
<span className="font-semibold">{entry.role}</span>
<span className="text-zinc-400 mx-1">/</span>
<span>{entry.module}</span>
<span className="text-zinc-400 mx-1">/</span>
<span className="font-mono capitalize">{entry.action}</span>
</span>
</div>
))}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* ── Info Footer ── */}
<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">Org Owner</span> always has full access to all modules and cannot be restricted.</p>
<p>Permission changes apply immediately to all users with the affected role. Use the <span className="font-mono">Save</span> button to persist changes or <span className="font-mono">Reset</span> to revert.</p>
</div>
))}
</div>
</div>
</div>
);
+12 -8
View File
@@ -18,6 +18,7 @@ import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
import CreateItemModal from '../../components/owner/CreateItemModal';
import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal';
import { usePermissions } from '../../hooks/usePermissions';
// --- NEOMORPHIC STYLING CONSTANTS ---
const NEO_PANEL_CLASS = "dark:bg-zinc-900/60 dark:backdrop-blur-3xl border border-zinc-200 dark:border-white/5 dark:shadow-[8px_8px_16px_rgba(0,0,0,0.6),-8px_-8px_16px_rgba(255,255,255,0.02)] rounded-3xl relative overflow-hidden transition-all duration-300";
@@ -141,6 +142,7 @@ const OwnerProjectDetail = () => {
removeUserCommissionOverride, updateProject,
} = useMockStore();
const navigate = useNavigate();
const { can } = usePermissions();
const [activeTab, setActiveTab] = useState('overview');
const [selectedCO, setSelectedCO] = useState(null);
const [selectedInvoice, setSelectedInvoice] = useState(null);
@@ -788,14 +790,16 @@ const OwnerProjectDetail = () => {
})()}
</div>
<div className="flex items-center gap-1">
<button
type="button"
onClick={(e) => { e.stopPropagation(); setCommissionSettingsOpen(true); }}
className="p-1.5 rounded-lg hover:bg-amber-500/10 transition-colors group"
title="Commission Settings"
>
<Settings size={15} className="text-zinc-400 dark:text-zinc-500 group-hover:text-amber-500 dark:group-hover:text-[#fda913] transition-colors" />
</button>
{can('commission', 'edit') && (
<button
type="button"
onClick={(e) => { e.stopPropagation(); setCommissionSettingsOpen(true); }}
className="p-1.5 rounded-lg hover:bg-amber-500/10 transition-colors group"
title="Commission Settings"
>
<Settings size={15} className="text-zinc-400 dark:text-zinc-500 group-hover:text-amber-500 dark:group-hover:text-[#fda913] transition-colors" />
</button>
)}
<div className="relative">
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'commission' ? null : 'commission'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />