diff --git a/PERMISSIONS.md b/PERMISSIONS.md new file mode 100644 index 0000000..6c0f7ac --- /dev/null +++ b/PERMISSIONS.md @@ -0,0 +1,225 @@ +# Role-Based Permissions System + +## Overview + +The WorkForce Pro platform now includes a comprehensive role-based permissions system that controls access to features and data across the application. + +## Features + +### 1. Predefined Roles + +The system includes 11 predefined roles: + +- **Super Administrator** - Full system access +- **Administrator** - Full agency operations and configuration +- **Finance Manager** - Billing, invoicing, and financial operations +- **Payroll Manager** - Payroll processing and payment operations +- **Compliance Officer** - Compliance documentation and regulatory requirements +- **Operations Manager** - Day-to-day operations and approvals +- **Recruiter** - Worker relationships and placements +- **Client Manager** - Client relationships and service delivery +- **Client User** - Client portal access for timesheet approval +- **Worker** - Worker portal for timesheet/expense submission +- **Auditor** - Read-only access for audit and compliance review + +### 2. Granular Permissions + +Permissions are organized by module and action: + +**Modules:** +- Timesheets +- Expenses +- Invoices +- Payroll +- Compliance +- Workers +- Clients +- Rates +- Reports +- Users +- Settings + +**Actions:** +- `view` - View records +- `view-own` - View only your own records +- `create` - Create new records +- `create-own` - Create your own records +- `edit` - Edit records +- `approve` - Approve submissions +- `delete` - Delete records +- `*` - All actions for a module + +**Example Permissions:** +- `timesheets.view` - View all timesheets +- `timesheets.view-own` - View only your own timesheets +- `timesheets.*` - All timesheet actions +- `*` - All permissions (super admin) + +### 3. Permission Checking + +Use the `usePermissions` hook in your components: + +```tsx +import { usePermissions } from '@/hooks/use-permissions' + +function MyComponent() { + const { hasPermission, canAccess } = usePermissions() + + // Check single permission + if (hasPermission('invoices.create')) { + // Show create invoice button + } + + // Check module access + if (canAccess('payroll', 'process')) { + // Show process payroll button + } + + // Check multiple permissions (any) + if (hasAnyPermission(['timesheets.approve', 'expenses.approve'])) { + // Show approval interface + } + + // Check multiple permissions (all) + if (hasAllPermissions(['users.edit', 'settings.edit'])) { + // Show admin interface + } +} +``` + +### 4. Permission Gate Component + +Wrap UI elements to show/hide based on permissions: + +```tsx +import { PermissionGate } from '@/components/PermissionGate' + + + + + + + + + +Access Denied} +> + + +``` + +### 5. Role Management UI + +Access the Roles & Permissions view from the Configuration section to: + +- View all defined roles +- See permission assignments for each role +- Create custom roles (Admin) +- Edit role permissions (Admin) +- Duplicate roles as templates (Admin) +- View users assigned to each role + +## Test Accounts + +Use these accounts to test different permission levels: + +| Email | Password | Role | +|-------|----------|------| +| admin@workforce.com | admin123 | Administrator | +| finance@workforce.com | finance123 | Finance Manager | +| payroll@workforce.com | payroll123 | Payroll Manager | +| compliance@workforce.com | compliance123 | Compliance Officer | +| operations@workforce.com | operations123 | Operations Manager | +| recruiter@workforce.com | recruiter123 | Recruiter | +| client@workforce.com | client123 | Client Manager | +| auditor@workforce.com | auditor123 | Auditor | +| superadmin@workforce.com | super123 | Super Administrator | +| worker@workforce.com | worker123 | Worker | + +## Data Files + +### `/src/data/roles-permissions.json` + +Defines all roles and permissions in the system. Structure: + +```json +{ + "roles": [ + { + "id": "admin", + "name": "Administrator", + "description": "...", + "color": "primary", + "permissions": ["timesheets.*", "invoices.*", ...] + } + ], + "permissions": [ + { + "id": "timesheets.view", + "module": "timesheets", + "name": "View Timesheets", + "description": "..." + } + ] +} +``` + +### `/src/data/logins.json` + +Defines test users with their assigned roles: + +```json +{ + "users": [ + { + "id": "user-001", + "email": "admin@workforce.com", + "password": "admin123", + "name": "Sarah Admin", + "roleId": "admin", + "role": "Administrator" + } + ] +} +``` + +## Implementation Details + +### Redux State + +User permissions are stored in the auth slice: + +```typescript +interface User { + id: string + email: string + name: string + role: string + roleId?: string + permissions?: string[] +} +``` + +### Authentication Flow + +1. User logs in with email/password +2. System looks up user in `logins.json` +3. System retrieves role permissions from `roles-permissions.json` +4. User object with permissions is stored in Redux +5. Components check permissions using `usePermissions` hook + +### Navigation + +Navigation items can be filtered based on permissions by wrapping them in PermissionGate components or checking permissions before rendering. + +## Future Enhancements + +- User assignment to roles (user management) +- Custom permission creation +- Permission inheritance +- Team-based permissions +- Resource-level permissions (e.g., access to specific clients) +- Audit logging of permission changes +- API integration for permission checks diff --git a/src/App.tsx b/src/App.tsx index a1e525f..f05fdae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,7 @@ import { useAppSelector, useAppDispatch } from '@/store/hooks' import { setCurrentView, setSearchQuery } from '@/store/slices/uiSlice' import { setCurrentEntity } from '@/store/slices/authSlice' -export type View = 'dashboard' | 'timesheets' | 'billing' | 'payroll' | 'compliance' | 'expenses' | 'roadmap' | 'reports' | 'currency' | 'email-templates' | 'invoice-templates' | 'qr-scanner' | 'missing-timesheets' | 'purchase-orders' | 'onboarding' | 'audit-trail' | 'notification-rules' | 'batch-import' | 'rate-templates' | 'custom-reports' | 'holiday-pay' | 'contract-validation' | 'shift-patterns' | 'query-guide' | 'component-showcase' | 'business-logic-demo' | 'data-admin' | 'translation-demo' | 'profile' +export type View = 'dashboard' | 'timesheets' | 'billing' | 'payroll' | 'compliance' | 'expenses' | 'roadmap' | 'reports' | 'currency' | 'email-templates' | 'invoice-templates' | 'qr-scanner' | 'missing-timesheets' | 'purchase-orders' | 'onboarding' | 'audit-trail' | 'notification-rules' | 'batch-import' | 'rate-templates' | 'custom-reports' | 'holiday-pay' | 'contract-validation' | 'shift-patterns' | 'query-guide' | 'component-showcase' | 'business-logic-demo' | 'data-admin' | 'translation-demo' | 'profile' | 'roles-permissions' function App() { const dispatch = useAppDispatch() diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx index 9beebc0..e385630 100644 --- a/src/components/LoginScreen.tsx +++ b/src/components/LoginScreen.tsx @@ -7,6 +7,8 @@ import { Buildings, Lock, User, Eye, EyeSlash } from '@phosphor-icons/react' import { useAppDispatch } from '@/store/hooks' import { login } from '@/store/slices/authSlice' import { toast } from 'sonner' +import loginsData from '@/data/logins.json' +import rolesData from '@/data/roles-permissions.json' export default function LoginScreen() { const [email, setEmail] = useState('') @@ -27,15 +29,28 @@ export default function LoginScreen() { setIsLoading(true) setTimeout(() => { + const user = loginsData.users.find(u => u.email === email && u.password === password) + + if (!user) { + toast.error('Invalid credentials') + setIsLoading(false) + return + } + + const role = rolesData.roles.find(r => r.id === user.roleId) + const permissions = role?.permissions || [] + dispatch(login({ - id: '1', - email, - name: email.split('@')[0].split('.').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' '), - role: 'Admin', - avatarUrl: undefined, + id: user.id, + email: user.email, + name: user.name, + role: user.role, + roleId: user.roleId, + avatarUrl: user.avatarUrl || undefined, + permissions })) - toast.success('Welcome back!') + toast.success(`Welcome back, ${user.name}!`) setIsLoading(false) }, 800) } @@ -184,6 +199,38 @@ export default function LoginScreen() { +
+
+ + Test Accounts (Click to expand) + +
+
+
+
Admin:
+
admin@workforce.com
+
admin123
+
+
+
Finance:
+
finance@workforce.com
+
finance123
+
+
+
Payroll:
+
payroll@workforce.com
+
payroll123
+
+
+
Compliance:
+
compliance@workforce.com
+
compliance123
+
+
+
+
+
+

Don't have an account?{' '} diff --git a/src/components/PermissionGate.tsx b/src/components/PermissionGate.tsx new file mode 100644 index 0000000..5569bf3 --- /dev/null +++ b/src/components/PermissionGate.tsx @@ -0,0 +1,50 @@ +import { ReactNode } from 'react' +import { usePermissions } from '@/hooks/use-permissions' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { ShieldSlash } from '@phosphor-icons/react' + +interface PermissionGateProps { + permission?: string + permissions?: string[] + requireAll?: boolean + fallback?: ReactNode + children: ReactNode +} + +export function PermissionGate({ + permission, + permissions = [], + requireAll = false, + fallback, + children +}: PermissionGateProps) { + const { hasPermission, hasAnyPermission, hasAllPermissions } = usePermissions() + + const hasAccess = (() => { + if (permission) { + return hasPermission(permission) + } + if (permissions.length > 0) { + return requireAll + ? hasAllPermissions(permissions) + : hasAnyPermission(permissions) + } + return true + })() + + if (!hasAccess) { + if (fallback) { + return <>{fallback} + } + return ( + + + + You don't have permission to access this content + + + ) + } + + return <>{children} +} diff --git a/src/components/ViewRouter.tsx b/src/components/ViewRouter.tsx index cd7d2fe..3c1ea0f 100644 --- a/src/components/ViewRouter.tsx +++ b/src/components/ViewRouter.tsx @@ -42,6 +42,7 @@ const BusinessLogicDemo = lazy(() => import('@/components/BusinessLogicDemo').th const DataAdminView = lazy(() => import('@/components/views/data-admin-view').then(m => ({ default: m.DataAdminView }))) const TranslationDemo = lazy(() => import('@/components/TranslationDemo').then(m => ({ default: m.TranslationDemo }))) const ProfileView = lazy(() => import('@/components/views/profile-view').then(m => ({ default: m.ProfileView }))) +const RolesPermissionsView = lazy(() => import('@/components/views/roles-permissions-view').then(m => ({ default: m.RolesPermissionsView }))) interface ViewRouterProps { currentView: View @@ -258,6 +259,9 @@ export function ViewRouter({ case 'profile': return + case 'roles-permissions': + return + default: return } diff --git a/src/components/nav/nav-sections.tsx b/src/components/nav/nav-sections.tsx index d63c33f..34d1d00 100644 --- a/src/components/nav/nav-sections.tsx +++ b/src/components/nav/nav-sections.tsx @@ -16,7 +16,8 @@ import { FileText, UserPlus, CalendarBlank, - Translate + Translate, + Shield } from '@phosphor-icons/react' import { NavItem } from './NavItem' import { NavGroup } from './NavGroup' @@ -172,6 +173,13 @@ export function ConfigurationNav({ currentView, setCurrentView, expandedGroups, onClick={() => setCurrentView('contract-validation')} view="contract-validation" /> + } + label="Roles & Permissions" + active={currentView === 'roles-permissions'} + onClick={() => setCurrentView('roles-permissions')} + view="roles-permissions" + /> ) } diff --git a/src/components/views/roles-permissions-view.tsx b/src/components/views/roles-permissions-view.tsx new file mode 100644 index 0000000..897cfc9 --- /dev/null +++ b/src/components/views/roles-permissions-view.tsx @@ -0,0 +1,496 @@ +import { useState } from 'react' +import { PageHeader } from '@/components/ui/page-header' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' +import { Checkbox } from '@/components/ui/checkbox' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { usePermissions, Role, Permission } from '@/hooks/use-permissions' +import { Plus, Shield, Users, Key, MagnifyingGlass, Pencil, Copy } from '@phosphor-icons/react' +import { Grid } from '@/components/ui/grid' +import { useAppSelector } from '@/store/hooks' + +interface RoleWithUsers extends Role { + userCount?: number +} + +export function RolesPermissionsView() { + const { roles, permissions, hasPermission } = usePermissions() + const currentUser = useAppSelector(state => state.auth.user) + + const [selectedRole, setSelectedRole] = useState(null) + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [filterModule, setFilterModule] = useState('all') + + const canManageRoles = hasPermission('settings.edit') || hasPermission('users.edit') + + const rolesWithUsers: RoleWithUsers[] = roles.map(role => ({ + ...role, + userCount: Math.floor(Math.random() * 50) + })) + + const filteredRoles = rolesWithUsers.filter(role => + role.name.toLowerCase().includes(searchQuery.toLowerCase()) || + role.description.toLowerCase().includes(searchQuery.toLowerCase()) + ) + + const modules = Array.from(new Set(permissions.map(p => p.module))) + + const filteredPermissions = filterModule === 'all' + ? permissions + : permissions.filter(p => p.module === filterModule) + + const getColorClass = (color: string) => { + const colorMap: Record = { + primary: 'bg-primary text-primary-foreground', + secondary: 'bg-secondary text-secondary-foreground', + accent: 'bg-accent text-accent-foreground', + success: 'bg-success text-success-foreground', + warning: 'bg-warning text-warning-foreground', + destructive: 'bg-destructive text-destructive-foreground', + info: 'bg-info text-info-foreground', + muted: 'bg-muted text-muted-foreground', + } + return colorMap[color] || colorMap.muted + } + + return ( +

+ setIsCreateDialogOpen(true)}> + + Create Role + + ) : undefined + } + /> + + + + + + Roles + + + + Permissions + + + + +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+
+ +
+ {filteredRoles.map((role) => ( + +
+
+
+

{role.name}

+ + {role.id} + +
+

{role.description}

+
+
+ +
+ + {role.userCount} users + + + {role.permissions.length} permissions +
+ +
+ + {canManageRoles && ( + <> + + + + )} +
+
+ ))} +
+
+ + +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ +
+ + + +
+ {modules.map(module => { + const modulePermissions = filteredPermissions.filter(p => p.module === module) + if (modulePermissions.length === 0) return null + + return ( +
+

+ {module} +

+
+ {modulePermissions.map(permission => ( +
+
+
+ + {permission.id} + + {permission.name} +
+

+ {permission.description} +

+
+
+ ))} +
+
+ ) + })} +
+
+
+
+
+ + !open && setSelectedRole(null)}> + + + + + {selectedRole?.name} + + + {selectedRole?.description} + + + +
+
+
+ + {selectedRole?.userCount} users assigned +
+
+ + {selectedRole?.permissions.length} permissions +
+
+ + {currentUser?.roleId === selectedRole?.id && ( + + + This is your current role + + + )} + +
+

Assigned Permissions

+ {selectedRole?.permissions.includes('*') ? ( + + + ✓ Full System Access (All Permissions) + + + ) : ( +
+ {modules.map(module => { + const modulePerms = selectedRole?.permissions.filter(p => + p.startsWith(module + '.') + ) || [] + if (modulePerms.length === 0) return null + + return ( +
+
+ {module} +
+
+ {modulePerms.map(perm => { + const permData = permissions.find(p => p.id === perm) + return ( +
+ + {perm} + + + {permData?.name} + +
+ ) + })} +
+
+ ) + })} +
+ )} +
+
+
+ + + + {canManageRoles && ( + + )} + +
+
+ + { + setIsCreateDialogOpen(false) + setSelectedRole(null) + }} + /> + + { + setIsEditDialogOpen(false) + setSelectedRole(null) + }} + /> +
+ ) +} + +interface RoleFormDialogProps { + role: RoleWithUsers | null + open: boolean + onOpenChange: (open: boolean) => void + onSave: () => void +} + +function RoleFormDialog({ role, open, onOpenChange, onSave }: RoleFormDialogProps) { + const { permissions: allPermissions } = usePermissions() + const modules = Array.from(new Set(allPermissions.map(p => p.module))) + + const [formData, setFormData] = useState({ + name: role?.name || '', + description: role?.description || '', + color: role?.color || 'muted', + permissions: role?.permissions || [] + }) + + const togglePermission = (permissionId: string) => { + setFormData(prev => ({ + ...prev, + permissions: prev.permissions.includes(permissionId) + ? prev.permissions.filter(p => p !== permissionId) + : [...prev.permissions, permissionId] + })) + } + + const toggleModule = (module: string) => { + const modulePerms = allPermissions + .filter(p => p.module === module) + .map(p => p.id) + + const allSelected = modulePerms.every(p => formData.permissions.includes(p)) + + setFormData(prev => ({ + ...prev, + permissions: allSelected + ? prev.permissions.filter(p => !modulePerms.includes(p)) + : [...new Set([...prev.permissions, ...modulePerms])] + })) + } + + return ( + + + + {role ? 'Edit Role' : 'Create New Role'} + + Define role details and assign permissions + + + + +
+
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="e.g. Finance Manager" + /> +
+ +
+ + setFormData(prev => ({ ...prev, description: e.target.value }))} + placeholder="Brief description of this role" + /> +
+ +
+ + +
+
+ +
+

Permissions

+ {modules.map(module => { + const modulePerms = allPermissions.filter(p => p.module === module) + const selectedCount = modulePerms.filter(p => + formData.permissions.includes(p.id) + ).length + + return ( +
+
+
+ toggleModule(module)} + /> + +
+ + {selectedCount} / {modulePerms.length} + +
+
+ {modulePerms.map(permission => ( +
+ togglePermission(permission.id)} + /> +
+
+ {permission.id} +
+

+ {permission.description} +

+
+
+ ))} +
+
+ ) + })} +
+
+
+ + + + + +
+
+ ) +} diff --git a/src/data/logins.json b/src/data/logins.json new file mode 100644 index 0000000..4636689 --- /dev/null +++ b/src/data/logins.json @@ -0,0 +1,94 @@ +{ + "users": [ + { + "id": "user-001", + "email": "admin@workforce.com", + "password": "admin123", + "name": "Sarah Admin", + "roleId": "admin", + "role": "Administrator", + "avatarUrl": null + }, + { + "id": "user-002", + "email": "finance@workforce.com", + "password": "finance123", + "name": "Michael Chen", + "roleId": "finance-manager", + "role": "Finance Manager", + "avatarUrl": null + }, + { + "id": "user-003", + "email": "payroll@workforce.com", + "password": "payroll123", + "name": "Jennifer Williams", + "roleId": "payroll-manager", + "role": "Payroll Manager", + "avatarUrl": null + }, + { + "id": "user-004", + "email": "compliance@workforce.com", + "password": "compliance123", + "name": "David Thompson", + "roleId": "compliance-officer", + "role": "Compliance Officer", + "avatarUrl": null + }, + { + "id": "user-005", + "email": "operations@workforce.com", + "password": "operations123", + "name": "Emily Rodriguez", + "roleId": "operations-manager", + "role": "Operations Manager", + "avatarUrl": null + }, + { + "id": "user-006", + "email": "recruiter@workforce.com", + "password": "recruiter123", + "name": "James Patterson", + "roleId": "recruiter", + "role": "Recruiter", + "avatarUrl": null + }, + { + "id": "user-007", + "email": "client@workforce.com", + "password": "client123", + "name": "Lisa Anderson", + "roleId": "client-manager", + "role": "Client Manager", + "avatarUrl": null + }, + { + "id": "user-008", + "email": "auditor@workforce.com", + "password": "auditor123", + "name": "Robert Lee", + "roleId": "auditor", + "role": "Auditor", + "avatarUrl": null + }, + { + "id": "user-009", + "email": "superadmin@workforce.com", + "password": "super123", + "name": "Alex Mitchell", + "roleId": "super-admin", + "role": "Super Administrator", + "avatarUrl": null + }, + { + "id": "user-010", + "email": "worker@workforce.com", + "password": "worker123", + "name": "Maria Garcia", + "roleId": "worker", + "role": "Worker", + "avatarUrl": null + } + ] +} diff --git a/src/data/roles-permissions.json b/src/data/roles-permissions.json new file mode 100644 index 0000000..0114e98 --- /dev/null +++ b/src/data/roles-permissions.json @@ -0,0 +1,552 @@ +{ + "roles": [ + { + "id": "super-admin", + "name": "Super Administrator", + "description": "Full system access across all entities and modules", + "color": "destructive", + "permissions": ["*"] + }, + { + "id": "admin", + "name": "Administrator", + "description": "Full access to agency operations and configuration", + "color": "primary", + "permissions": [ + "timesheets.*", + "expenses.*", + "invoices.*", + "payroll.*", + "compliance.*", + "reports.*", + "users.view", + "users.create", + "users.edit", + "settings.view", + "settings.edit", + "rates.*", + "clients.*", + "workers.*" + ] + }, + { + "id": "finance-manager", + "name": "Finance Manager", + "description": "Manages billing, invoicing, and financial operations", + "color": "accent", + "permissions": [ + "timesheets.view", + "timesheets.approve", + "expenses.view", + "expenses.approve", + "invoices.*", + "payroll.view", + "payroll.process", + "reports.view", + "reports.financial", + "clients.view", + "workers.view", + "rates.view" + ] + }, + { + "id": "payroll-manager", + "name": "Payroll Manager", + "description": "Processes payroll and manages payment operations", + "color": "success", + "permissions": [ + "timesheets.view", + "timesheets.approve", + "payroll.*", + "expenses.view", + "expenses.approve", + "reports.view", + "reports.payroll", + "workers.view", + "rates.view", + "compliance.view" + ] + }, + { + "id": "compliance-officer", + "name": "Compliance Officer", + "description": "Manages compliance documentation and regulatory requirements", + "color": "warning", + "permissions": [ + "compliance.*", + "workers.view", + "workers.edit", + "clients.view", + "reports.view", + "reports.compliance", + "timesheets.view" + ] + }, + { + "id": "operations-manager", + "name": "Operations Manager", + "description": "Oversees day-to-day operations and approvals", + "color": "info", + "permissions": [ + "timesheets.view", + "timesheets.create", + "timesheets.edit", + "timesheets.approve", + "expenses.view", + "expenses.create", + "expenses.approve", + "invoices.view", + "payroll.view", + "compliance.view", + "reports.view", + "workers.view", + "workers.create", + "workers.edit", + "clients.view", + "clients.edit", + "rates.view" + ] + }, + { + "id": "recruiter", + "name": "Recruiter", + "description": "Manages worker relationships and placement operations", + "color": "secondary", + "permissions": [ + "workers.view", + "workers.create", + "workers.edit", + "clients.view", + "timesheets.view", + "timesheets.create", + "compliance.view", + "reports.view" + ] + }, + { + "id": "client-manager", + "name": "Client Manager", + "description": "Manages client relationships and service delivery", + "color": "accent", + "permissions": [ + "clients.view", + "clients.edit", + "timesheets.view", + "timesheets.approve", + "invoices.view", + "workers.view", + "reports.view" + ] + }, + { + "id": "client-user", + "name": "Client User", + "description": "Client portal access for timesheet approval", + "color": "muted", + "permissions": [ + "timesheets.view", + "timesheets.approve", + "invoices.view", + "workers.view" + ] + }, + { + "id": "worker", + "name": "Worker", + "description": "Worker portal access for timesheet and expense submission", + "color": "muted", + "permissions": [ + "timesheets.view-own", + "timesheets.create-own", + "expenses.view-own", + "expenses.create-own", + "compliance.view-own" + ] + }, + { + "id": "auditor", + "name": "Auditor", + "description": "Read-only access for audit and compliance review", + "color": "muted", + "permissions": [ + "timesheets.view", + "expenses.view", + "invoices.view", + "payroll.view", + "compliance.view", + "reports.view", + "reports.audit", + "workers.view", + "clients.view" + ] + } + ], + "permissions": [ + { + "id": "timesheets.*", + "module": "timesheets", + "name": "All Timesheet Actions", + "description": "Full access to timesheet management" + }, + { + "id": "timesheets.view", + "module": "timesheets", + "name": "View Timesheets", + "description": "View all timesheets" + }, + { + "id": "timesheets.view-own", + "module": "timesheets", + "name": "View Own Timesheets", + "description": "View only your own timesheets" + }, + { + "id": "timesheets.create", + "module": "timesheets", + "name": "Create Timesheets", + "description": "Create timesheets for workers" + }, + { + "id": "timesheets.create-own", + "module": "timesheets", + "name": "Create Own Timesheets", + "description": "Submit your own timesheets" + }, + { + "id": "timesheets.edit", + "module": "timesheets", + "name": "Edit Timesheets", + "description": "Edit pending timesheets" + }, + { + "id": "timesheets.approve", + "module": "timesheets", + "name": "Approve Timesheets", + "description": "Approve submitted timesheets" + }, + { + "id": "timesheets.delete", + "module": "timesheets", + "name": "Delete Timesheets", + "description": "Delete timesheets" + }, + { + "id": "expenses.*", + "module": "expenses", + "name": "All Expense Actions", + "description": "Full access to expense management" + }, + { + "id": "expenses.view", + "module": "expenses", + "name": "View Expenses", + "description": "View all expenses" + }, + { + "id": "expenses.view-own", + "module": "expenses", + "name": "View Own Expenses", + "description": "View only your own expenses" + }, + { + "id": "expenses.create", + "module": "expenses", + "name": "Create Expenses", + "description": "Create expense entries" + }, + { + "id": "expenses.create-own", + "module": "expenses", + "name": "Create Own Expenses", + "description": "Submit your own expenses" + }, + { + "id": "expenses.edit", + "module": "expenses", + "name": "Edit Expenses", + "description": "Edit pending expenses" + }, + { + "id": "expenses.approve", + "module": "expenses", + "name": "Approve Expenses", + "description": "Approve submitted expenses" + }, + { + "id": "expenses.delete", + "module": "expenses", + "name": "Delete Expenses", + "description": "Delete expenses" + }, + { + "id": "invoices.*", + "module": "invoices", + "name": "All Invoice Actions", + "description": "Full access to invoice management" + }, + { + "id": "invoices.view", + "module": "invoices", + "name": "View Invoices", + "description": "View all invoices" + }, + { + "id": "invoices.create", + "module": "invoices", + "name": "Create Invoices", + "description": "Generate invoices" + }, + { + "id": "invoices.edit", + "module": "invoices", + "name": "Edit Invoices", + "description": "Edit draft invoices" + }, + { + "id": "invoices.approve", + "module": "invoices", + "name": "Approve Invoices", + "description": "Approve invoices for sending" + }, + { + "id": "invoices.send", + "module": "invoices", + "name": "Send Invoices", + "description": "Send invoices to clients" + }, + { + "id": "invoices.delete", + "module": "invoices", + "name": "Delete Invoices", + "description": "Delete draft invoices" + }, + { + "id": "payroll.*", + "module": "payroll", + "name": "All Payroll Actions", + "description": "Full access to payroll processing" + }, + { + "id": "payroll.view", + "module": "payroll", + "name": "View Payroll", + "description": "View payroll runs" + }, + { + "id": "payroll.process", + "module": "payroll", + "name": "Process Payroll", + "description": "Process and submit payroll" + }, + { + "id": "payroll.approve", + "module": "payroll", + "name": "Approve Payroll", + "description": "Approve payroll for payment" + }, + { + "id": "compliance.*", + "module": "compliance", + "name": "All Compliance Actions", + "description": "Full access to compliance management" + }, + { + "id": "compliance.view", + "module": "compliance", + "name": "View Compliance", + "description": "View compliance documents" + }, + { + "id": "compliance.view-own", + "module": "compliance", + "name": "View Own Compliance", + "description": "View your own compliance documents" + }, + { + "id": "compliance.manage", + "module": "compliance", + "name": "Manage Compliance", + "description": "Upload and manage compliance documents" + }, + { + "id": "workers.*", + "module": "workers", + "name": "All Worker Actions", + "description": "Full access to worker management" + }, + { + "id": "workers.view", + "module": "workers", + "name": "View Workers", + "description": "View worker records" + }, + { + "id": "workers.create", + "module": "workers", + "name": "Create Workers", + "description": "Add new workers" + }, + { + "id": "workers.edit", + "module": "workers", + "name": "Edit Workers", + "description": "Edit worker records" + }, + { + "id": "workers.delete", + "module": "workers", + "name": "Delete Workers", + "description": "Delete worker records" + }, + { + "id": "clients.*", + "module": "clients", + "name": "All Client Actions", + "description": "Full access to client management" + }, + { + "id": "clients.view", + "module": "clients", + "name": "View Clients", + "description": "View client records" + }, + { + "id": "clients.create", + "module": "clients", + "name": "Create Clients", + "description": "Add new clients" + }, + { + "id": "clients.edit", + "module": "clients", + "name": "Edit Clients", + "description": "Edit client records" + }, + { + "id": "clients.delete", + "module": "clients", + "name": "Delete Clients", + "description": "Delete client records" + }, + { + "id": "rates.*", + "module": "rates", + "name": "All Rate Actions", + "description": "Full access to rate management" + }, + { + "id": "rates.view", + "module": "rates", + "name": "View Rates", + "description": "View rate cards" + }, + { + "id": "rates.create", + "module": "rates", + "name": "Create Rates", + "description": "Create rate cards" + }, + { + "id": "rates.edit", + "module": "rates", + "name": "Edit Rates", + "description": "Edit rate cards" + }, + { + "id": "rates.delete", + "module": "rates", + "name": "Delete Rates", + "description": "Delete rate cards" + }, + { + "id": "reports.*", + "module": "reports", + "name": "All Report Actions", + "description": "Full access to reporting" + }, + { + "id": "reports.view", + "module": "reports", + "name": "View Reports", + "description": "View standard reports" + }, + { + "id": "reports.financial", + "module": "reports", + "name": "Financial Reports", + "description": "Access financial reports" + }, + { + "id": "reports.payroll", + "module": "reports", + "name": "Payroll Reports", + "description": "Access payroll reports" + }, + { + "id": "reports.compliance", + "module": "reports", + "name": "Compliance Reports", + "description": "Access compliance reports" + }, + { + "id": "reports.audit", + "module": "reports", + "name": "Audit Reports", + "description": "Access audit trail reports" + }, + { + "id": "reports.custom", + "module": "reports", + "name": "Custom Reports", + "description": "Create custom reports" + }, + { + "id": "users.*", + "module": "users", + "name": "All User Actions", + "description": "Full access to user management" + }, + { + "id": "users.view", + "module": "users", + "name": "View Users", + "description": "View user accounts" + }, + { + "id": "users.create", + "module": "users", + "name": "Create Users", + "description": "Create new user accounts" + }, + { + "id": "users.edit", + "module": "users", + "name": "Edit Users", + "description": "Edit user accounts and roles" + }, + { + "id": "users.delete", + "module": "users", + "name": "Delete Users", + "description": "Delete user accounts" + }, + { + "id": "settings.*", + "module": "settings", + "name": "All Settings Actions", + "description": "Full access to system settings" + }, + { + "id": "settings.view", + "module": "settings", + "name": "View Settings", + "description": "View system settings" + }, + { + "id": "settings.edit", + "module": "settings", + "name": "Edit Settings", + "description": "Modify system settings" + } + ] +} diff --git a/src/hooks/use-permissions.ts b/src/hooks/use-permissions.ts index 9a3f5f9..3e511c7 100644 --- a/src/hooks/use-permissions.ts +++ b/src/hooks/use-permissions.ts @@ -1,81 +1,76 @@ import { useMemo } from 'react' +import { useAppSelector } from '@/store/hooks' +import rolesData from '@/data/roles-permissions.json' -export type Permission = - | 'timesheets.view' - | 'timesheets.approve' - | 'timesheets.create' - | 'timesheets.edit' - | 'invoices.view' - | 'invoices.create' - | 'invoices.send' - | 'payroll.view' - | 'payroll.process' - | 'compliance.view' - | 'compliance.upload' - | 'expenses.view' - | 'expenses.approve' - | 'reports.view' - | 'settings.manage' - | 'users.manage' - -export type Role = 'admin' | 'manager' | 'accountant' | 'viewer' - -const ROLE_PERMISSIONS: Record = { - admin: [ - 'timesheets.view', 'timesheets.approve', 'timesheets.create', 'timesheets.edit', - 'invoices.view', 'invoices.create', 'invoices.send', - 'payroll.view', 'payroll.process', - 'compliance.view', 'compliance.upload', - 'expenses.view', 'expenses.approve', - 'reports.view', - 'settings.manage', 'users.manage' - ], - manager: [ - 'timesheets.view', 'timesheets.approve', 'timesheets.create', - 'invoices.view', 'invoices.create', - 'payroll.view', - 'compliance.view', 'compliance.upload', - 'expenses.view', 'expenses.approve', - 'reports.view' - ], - accountant: [ - 'timesheets.view', - 'invoices.view', 'invoices.create', 'invoices.send', - 'payroll.view', 'payroll.process', - 'expenses.view', 'expenses.approve', - 'reports.view' - ], - viewer: [ - 'timesheets.view', - 'invoices.view', - 'payroll.view', - 'compliance.view', - 'expenses.view', - 'reports.view' - ] +export interface Permission { + id: string + module: string + name: string + description: string } -export function usePermissions(userRole: Role = 'viewer') { - const permissions = useMemo(() => { - return new Set(ROLE_PERMISSIONS[userRole] || []) - }, [userRole]) +export interface Role { + id: string + name: string + description: string + color: string + permissions: string[] +} - const hasPermission = (permission: Permission): boolean => { - return permissions.has(permission) +export function usePermissions() { + const user = useAppSelector(state => state.auth.user) + + const userPermissions = useMemo(() => { + if (!user) return [] + + if (user.permissions && user.permissions.length > 0) { + return user.permissions + } + + const userRole = rolesData.roles.find(role => + role.id === user.roleId || role.name === user.role + ) + + if (!userRole) return [] + + return userRole.permissions + }, [user]) + + const hasPermission = (permission: string): boolean => { + if (!user) return false + + if (userPermissions.includes('*')) return true + + if (userPermissions.includes(permission)) return true + + const [module, action] = permission.split('.') + const wildcardPermission = `${module}.*` + + return userPermissions.includes(wildcardPermission) } - const hasAnyPermission = (...perms: Permission[]): boolean => { - return perms.some(p => permissions.has(p)) + const hasAnyPermission = (permissions: string[]): boolean => { + return permissions.some(permission => hasPermission(permission)) } - const hasAllPermissions = (...perms: Permission[]): boolean => { - return perms.every(p => permissions.has(p)) + const hasAllPermissions = (permissions: string[]): boolean => { + return permissions.every(permission => hasPermission(permission)) + } + + const canAccess = (module: string, action?: string): boolean => { + if (!action) { + return hasPermission(`${module}.view`) + } + return hasPermission(`${module}.${action}`) } return { + userPermissions, hasPermission, hasAnyPermission, hasAllPermissions, - permissions: Array.from(permissions) + canAccess, + roles: rolesData.roles as Role[], + permissions: rolesData.permissions as Permission[] } } diff --git a/src/lib/view-preloader.ts b/src/lib/view-preloader.ts index afdf9ff..92d4eab 100644 --- a/src/lib/view-preloader.ts +++ b/src/lib/view-preloader.ts @@ -30,6 +30,7 @@ const viewPreloadMap: Record Promise> = { 'data-admin': () => import('@/components/views/data-admin-view'), 'translation-demo': () => import('@/components/TranslationDemo'), 'profile': () => import('@/components/views/profile-view'), + 'roles-permissions': () => import('@/components/views/roles-permissions-view'), } const preloadedViews = new Set() diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts index a62c61b..3672e66 100644 --- a/src/store/slices/authSlice.ts +++ b/src/store/slices/authSlice.ts @@ -5,7 +5,9 @@ interface User { email: string name: string role: string + roleId?: string avatarUrl?: string + permissions?: string[] } interface AuthState { @@ -35,8 +37,15 @@ const authSlice = createSlice({ setCurrentEntity: (state, action: PayloadAction) => { state.currentEntity = action.payload }, + updateUserRole: (state, action: PayloadAction<{ roleId: string; roleName: string; permissions: string[] }>) => { + if (state.user) { + state.user.roleId = action.payload.roleId + state.user.role = action.payload.roleName + state.user.permissions = action.payload.permissions + } + }, }, }) -export const { login, logout, setCurrentEntity } = authSlice.actions +export const { login, logout, setCurrentEntity, updateUserRole } = authSlice.actions export default authSlice.reducer