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() {
+
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}
+
+
+
+ ))}
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+ {
+ 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 (
+
+ )
+}
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