mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Create role-based permissions system
This commit is contained in:
225
PERMISSIONS.md
Normal file
225
PERMISSIONS.md
Normal file
@@ -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'
|
||||
|
||||
<PermissionGate permission="invoices.create">
|
||||
<Button>Create Invoice</Button>
|
||||
</PermissionGate>
|
||||
|
||||
<PermissionGate permissions={['timesheets.view', 'expenses.view']} requireAll={false}>
|
||||
<ReportsTab />
|
||||
</PermissionGate>
|
||||
|
||||
<PermissionGate
|
||||
permission="admin.access"
|
||||
fallback={<div>Access Denied</div>}
|
||||
>
|
||||
<AdminPanel />
|
||||
</PermissionGate>
|
||||
```
|
||||
|
||||
### 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 p-4 bg-muted/50 rounded-lg border border-border">
|
||||
<details className="group">
|
||||
<summary className="text-sm font-medium cursor-pointer text-muted-foreground hover:text-foreground transition-colors">
|
||||
Test Accounts (Click to expand)
|
||||
</summary>
|
||||
<div className="mt-3 space-y-2 text-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="font-mono">
|
||||
<div className="font-semibold mb-1">Admin:</div>
|
||||
<div>admin@workforce.com</div>
|
||||
<div className="text-muted-foreground">admin123</div>
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
<div className="font-semibold mb-1">Finance:</div>
|
||||
<div>finance@workforce.com</div>
|
||||
<div className="text-muted-foreground">finance123</div>
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
<div className="font-semibold mb-1">Payroll:</div>
|
||||
<div>payroll@workforce.com</div>
|
||||
<div className="text-muted-foreground">payroll123</div>
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
<div className="font-semibold mb-1">Compliance:</div>
|
||||
<div>compliance@workforce.com</div>
|
||||
<div className="text-muted-foreground">compliance123</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Don't have an account?{' '}
|
||||
|
||||
50
src/components/PermissionGate.tsx
Normal file
50
src/components/PermissionGate.tsx
Normal file
@@ -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 (
|
||||
<Alert variant="destructive">
|
||||
<ShieldSlash className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You don't have permission to access this content
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -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 <ProfileView />
|
||||
|
||||
case 'roles-permissions':
|
||||
return <RolesPermissionsView />
|
||||
|
||||
default:
|
||||
return <DashboardView metrics={metrics} />
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<NavItem
|
||||
icon={<Shield size={20} />}
|
||||
label="Roles & Permissions"
|
||||
active={currentView === 'roles-permissions'}
|
||||
onClick={() => setCurrentView('roles-permissions')}
|
||||
view="roles-permissions"
|
||||
/>
|
||||
</NavGroup>
|
||||
)
|
||||
}
|
||||
|
||||
496
src/components/views/roles-permissions-view.tsx
Normal file
496
src/components/views/roles-permissions-view.tsx
Normal file
@@ -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<RoleWithUsers | null>(null)
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [filterModule, setFilterModule] = useState<string>('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<string, string> = {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Roles & Permissions"
|
||||
description="Manage user roles and access permissions across the platform"
|
||||
actions={
|
||||
canManageRoles ? (
|
||||
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
||||
<Plus className="mr-2" />
|
||||
Create Role
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs defaultValue="roles" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="roles">
|
||||
<Users className="mr-2" />
|
||||
Roles
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="permissions">
|
||||
<Key className="mr-2" />
|
||||
Permissions
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="roles" className="space-y-4 mt-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={20} />
|
||||
<Input
|
||||
placeholder="Search roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{filteredRoles.map((role) => (
|
||||
<Card key={role.id} className="p-6 space-y-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-lg">{role.name}</h3>
|
||||
<Badge className={getColorClass(role.color)}>
|
||||
{role.id}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{role.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Users size={16} />
|
||||
<span>{role.userCount} users</span>
|
||||
<span className="mx-2">•</span>
|
||||
<Key size={16} />
|
||||
<span>{role.permissions.length} permissions</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => setSelectedRole(role)}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
{canManageRoles && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedRole(role)
|
||||
setIsEditDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedRole(role)
|
||||
setIsCreateDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="permissions" className="space-y-4 mt-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={20} />
|
||||
<Input
|
||||
placeholder="Search permissions..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={filterModule}
|
||||
onChange={(e) => setFilterModule(e.target.value)}
|
||||
className="px-4 py-2 border border-input rounded-md bg-background"
|
||||
>
|
||||
<option value="all">All Modules</option>
|
||||
{modules.map(module => (
|
||||
<option key={module} value={module}>
|
||||
{module.charAt(0).toUpperCase() + module.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<ScrollArea className="h-[600px]">
|
||||
<div className="space-y-6">
|
||||
{modules.map(module => {
|
||||
const modulePermissions = filteredPermissions.filter(p => p.module === module)
|
||||
if (modulePermissions.length === 0) return null
|
||||
|
||||
return (
|
||||
<div key={module} className="space-y-3">
|
||||
<h3 className="font-semibold text-lg capitalize border-b pb-2">
|
||||
{module}
|
||||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{modulePermissions.map(permission => (
|
||||
<div
|
||||
key={permission.id}
|
||||
className="flex items-start gap-3 p-3 rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">
|
||||
{permission.id}
|
||||
</code>
|
||||
<span className="font-medium">{permission.name}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{permission.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={!!selectedRole && !isEditDialogOpen} onOpenChange={(open) => !open && setSelectedRole(null)}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Badge className={getColorClass(selectedRole?.color || 'muted')}>
|
||||
{selectedRole?.name}
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
<DialogDescription>{selectedRole?.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users size={16} />
|
||||
<span>{selectedRole?.userCount} users assigned</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Key size={16} />
|
||||
<span>{selectedRole?.permissions.length} permissions</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentUser?.roleId === selectedRole?.id && (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
This is your current role
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold">Assigned Permissions</h4>
|
||||
{selectedRole?.permissions.includes('*') ? (
|
||||
<Alert>
|
||||
<AlertDescription className="font-semibold">
|
||||
✓ Full System Access (All Permissions)
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{modules.map(module => {
|
||||
const modulePerms = selectedRole?.permissions.filter(p =>
|
||||
p.startsWith(module + '.')
|
||||
) || []
|
||||
if (modulePerms.length === 0) return null
|
||||
|
||||
return (
|
||||
<div key={module} className="space-y-2">
|
||||
<h5 className="font-medium text-sm capitalize text-muted-foreground">
|
||||
{module}
|
||||
</h5>
|
||||
<div className="grid gap-2 pl-4">
|
||||
{modulePerms.map(perm => {
|
||||
const permData = permissions.find(p => p.id === perm)
|
||||
return (
|
||||
<div key={perm} className="flex items-start gap-2 text-sm">
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{perm}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
{permData?.name}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSelectedRole(null)}>
|
||||
Close
|
||||
</Button>
|
||||
{canManageRoles && (
|
||||
<Button onClick={() => setIsEditDialogOpen(true)}>
|
||||
<Pencil className="mr-2" />
|
||||
Edit Role
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<RoleFormDialog
|
||||
role={selectedRole}
|
||||
open={isCreateDialogOpen}
|
||||
onOpenChange={setIsCreateDialogOpen}
|
||||
onSave={() => {
|
||||
setIsCreateDialogOpen(false)
|
||||
setSelectedRole(null)
|
||||
}}
|
||||
/>
|
||||
|
||||
<RoleFormDialog
|
||||
role={selectedRole}
|
||||
open={isEditDialogOpen}
|
||||
onOpenChange={setIsEditDialogOpen}
|
||||
onSave={() => {
|
||||
setIsEditDialogOpen(false)
|
||||
setSelectedRole(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{role ? 'Edit Role' : 'Create New Role'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Define role details and assign permissions
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="h-[500px] pr-4">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Role Name</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="e.g. Finance Manager"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Description</label>
|
||||
<Input
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Brief description of this role"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Color</label>
|
||||
<select
|
||||
value={formData.color}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, color: e.target.value }))}
|
||||
className="w-full px-4 py-2 border border-input rounded-md bg-background"
|
||||
>
|
||||
<option value="primary">Primary</option>
|
||||
<option value="secondary">Secondary</option>
|
||||
<option value="accent">Accent</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="muted">Muted</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold">Permissions</h4>
|
||||
{modules.map(module => {
|
||||
const modulePerms = allPermissions.filter(p => p.module === module)
|
||||
const selectedCount = modulePerms.filter(p =>
|
||||
formData.permissions.includes(p.id)
|
||||
).length
|
||||
|
||||
return (
|
||||
<div key={module} className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedCount === modulePerms.length}
|
||||
onCheckedChange={() => toggleModule(module)}
|
||||
/>
|
||||
<label className="font-medium capitalize cursor-pointer">
|
||||
{module}
|
||||
</label>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{selectedCount} / {modulePerms.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid gap-2 pl-6">
|
||||
{modulePerms.map(permission => (
|
||||
<div key={permission.id} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
checked={formData.permissions.includes(permission.id)}
|
||||
onCheckedChange={() => togglePermission(permission.id)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-xs font-mono">{permission.id}</code>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permission.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSave}>
|
||||
{role ? 'Save Changes' : 'Create Role'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
94
src/data/logins.json
Normal file
94
src/data/logins.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
552
src/data/roles-permissions.json
Normal file
552
src/data/roles-permissions.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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<Role, Permission[]> = {
|
||||
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[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const viewPreloadMap: Record<View, () => Promise<any>> = {
|
||||
'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<View>()
|
||||
|
||||
@@ -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<string>) => {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user