Admin panel: real data - backend API, users/audit/roles/stats, frontend wired

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Talal Sharabi
2026-02-19 15:40:34 +04:00
parent 680ba3871e
commit 842678674b
11 changed files with 2159 additions and 809 deletions

View File

@@ -1,219 +1,233 @@
'use client'
'use client';
import { useState } from 'react'
import {
Shield,
Plus,
Edit,
Trash2,
Users,
Check,
X
} from 'lucide-react'
import { useState, useEffect, useCallback } from 'react';
import { Shield, Edit, Users, Check, X } from 'lucide-react';
import { positionsAPI } from '@/lib/api/admin';
import type { PositionRole, PositionPermission } from '@/lib/api/admin';
import Modal from '@/components/Modal';
import LoadingSpinner from '@/components/LoadingSpinner';
const MODULES = [
{ id: 'contacts', name: 'إدارة جهات الاتصال', nameEn: 'Contact Management' },
{ id: 'crm', name: 'إدارة علاقات العملاء', nameEn: 'CRM' },
{ id: 'inventory', name: 'المخزون والأصول', nameEn: 'Inventory & Assets' },
{ id: 'projects', name: 'المهام والمشاريع', nameEn: 'Tasks & Projects' },
{ id: 'hr', name: 'الموارد البشرية', nameEn: 'HR Management' },
{ id: 'marketing', name: 'التسويق', nameEn: 'Marketing' },
{ id: 'admin', name: 'لوحة الإدارة', nameEn: 'Admin' },
];
const ACTIONS = [
{ id: 'read', name: 'عرض' },
{ id: 'create', name: 'إنشاء' },
{ id: 'update', name: 'تعديل' },
{ id: 'delete', name: 'حذف' },
{ id: 'export', name: 'تصدير' },
{ id: 'approve', name: 'اعتماد' },
{ id: 'merge', name: 'دمج' },
];
function hasAction(permission: PositionPermission | undefined, action: string): boolean {
if (!permission?.actions) return false;
const actions = Array.isArray(permission.actions) ? permission.actions : [];
return actions.includes('*') || actions.includes('all') || actions.includes(action);
}
function buildPermissionsFromMatrix(matrix: Record<string, Record<string, boolean>>) {
return MODULES.filter((m) => Object.values(matrix[m.id] || {}).some(Boolean)).map((m) => {
const actions = ACTIONS.filter((a) => matrix[m.id]?.[a.id]).map((a) => a.id);
return {
module: m.id,
resource: '*',
actions: actions.length === ACTIONS.length ? ['*'] : actions,
};
});
}
function buildMatrixFromPermissions(permissions: PositionPermission[]): Record<string, Record<string, boolean>> {
const matrix: Record<string, Record<string, boolean>> = {};
for (const m of MODULES) {
matrix[m.id] = {};
const perm = permissions.find((p) => p.module === m.id && (p.resource === '*' || p.resource === m.id));
const hasAll = perm && (Array.isArray(perm.actions)
? (perm.actions as string[]).includes('*') || (perm.actions as string[]).includes('all')
: false);
for (const a of ACTIONS) {
matrix[m.id][a.id] = hasAll || hasAction(perm, a.id);
}
}
return matrix;
}
export default function RolesManagement() {
const [selectedRole, setSelectedRole] = useState<string | null>(null)
const [showAddModal, setShowAddModal] = useState(false)
const [roles, setRoles] = useState<PositionRole[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
const [showEditModal, setShowEditModal] = useState(false);
const [permissionMatrix, setPermissionMatrix] = useState<Record<string, Record<string, boolean>>>({});
const [saving, setSaving] = useState(false);
// Modules and their permissions
const modules = [
{
id: 'contacts',
name: 'إدارة جهات الاتصال',
nameEn: 'Contact Management'
},
{
id: 'crm',
name: 'إدارة علاقات العملاء',
nameEn: 'CRM'
},
{
id: 'inventory',
name: 'المخزون والأصول',
nameEn: 'Inventory & Assets'
},
{
id: 'projects',
name: 'المهام والمشاريع',
nameEn: 'Tasks & Projects'
},
{
id: 'hr',
name: 'الموارد البشرية',
nameEn: 'HR Management'
},
{
id: 'marketing',
name: 'التسويق',
nameEn: 'Marketing'
const fetchRoles = useCallback(async () => {
setLoading(true);
setError(null);
try {
const pos = await positionsAPI.getAll();
setRoles(pos);
if (selectedRoleId && !pos.find((p) => p.id === selectedRoleId)) {
setSelectedRoleId(null);
}
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'فشل تحميل الأدوار');
} finally {
setLoading(false);
}
]
}, [selectedRoleId]);
const permissions = [
{ id: 'canView', name: 'عرض', icon: '👁️' },
{ id: 'canCreate', name: 'إنشاء', icon: '' },
{ id: 'canEdit', name: 'تعديل', icon: '✏️' },
{ id: 'canDelete', name: 'حذف', icon: '🗑️' },
{ id: 'canExport', name: 'تصدير', icon: '📤' },
{ id: 'canApprove', name: 'اعتماد', icon: '✅' }
]
useEffect(() => {
fetchRoles();
}, [fetchRoles]);
// Mock roles data
const roles = [
{
id: '1',
name: 'المدير العام',
nameEn: 'General Manager',
description: 'صلاحيات كاملة على النظام',
usersCount: 2,
permissions: {
contacts: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
crm: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
inventory: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
projects: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
hr: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
marketing: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true }
}
},
{
id: '2',
name: 'مدير المبيعات',
nameEn: 'Sales Manager',
description: 'إدارة المبيعات والعملاء مع صلاحيات الاعتماد',
usersCount: 5,
permissions: {
contacts: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: true, canApprove: false },
crm: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: true, canApprove: true },
inventory: { canView: true, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
projects: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: false, canApprove: false },
hr: { canView: false, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
marketing: { canView: true, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false }
}
},
{
id: '3',
name: 'مندوب مبيعات',
nameEn: 'Sales Representative',
description: 'إدخال وتعديل بيانات المبيعات الأساسية',
usersCount: 12,
permissions: {
contacts: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: false, canApprove: false },
crm: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: false, canApprove: false },
inventory: { canView: true, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
projects: { canView: true, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
hr: { canView: false, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
marketing: { canView: false, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false }
}
const currentRole = roles.find((r) => r.id === selectedRoleId);
useEffect(() => {
if (currentRole) {
setPermissionMatrix(buildMatrixFromPermissions(currentRole.permissions || []));
}
]
}, [currentRole?.id, currentRole?.permissions]);
const currentRole = roles.find(r => r.id === selectedRole)
const handleTogglePermission = (moduleId: string, actionId: string) => {
setPermissionMatrix((prev) => ({
...prev,
[moduleId]: {
...(prev[moduleId] || {}),
[actionId]: !prev[moduleId]?.[actionId],
},
}));
};
const handleSavePermissions = async () => {
if (!selectedRoleId) return;
setSaving(true);
try {
const permissions = buildPermissionsFromMatrix(permissionMatrix);
await positionsAPI.updatePermissions(selectedRoleId, permissions);
setShowEditModal(false);
fetchRoles();
} catch (err: unknown) {
alert(err instanceof Error ? err.message : 'فشل حفظ الصلاحيات');
} finally {
setSaving(false);
}
};
const handleSelectRole = (id: string) => {
setSelectedRoleId(id);
setShowEditModal(false);
};
return (
<div>
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">الأدوار والصلاحيات</h1>
<p className="text-gray-600">إدارة أدوار المستخدمين ومصفوفة الصلاحيات</p>
</div>
<button
onClick={() => setShowAddModal(true)}
className="flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-all shadow-md hover:shadow-lg"
>
<Plus className="h-5 w-5" />
<span className="font-semibold">إضافة دور جديد</span>
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Roles List */}
<div className="lg:col-span-1 space-y-4">
<h2 className="text-xl font-bold text-gray-900 mb-4">الأدوار ({roles.length})</h2>
{roles.map((role) => (
<div
key={role.id}
onClick={() => setSelectedRole(role.id)}
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
selectedRole === role.id
? 'border-purple-600 bg-purple-50 shadow-md'
: 'border-gray-200 bg-white hover:border-purple-300 hover:shadow-sm'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${selectedRole === role.id ? 'bg-purple-600' : 'bg-purple-100'}`}>
<Shield className={`h-5 w-5 ${selectedRole === role.id ? 'text-white' : 'text-purple-600'}`} />
</div>
<div>
<h3 className="font-bold text-gray-900">{role.name}</h3>
<p className="text-xs text-gray-600">{role.nameEn}</p>
{loading ? (
<div className="flex justify-center p-12">
<LoadingSpinner />
</div>
) : error ? (
<div className="text-center text-red-600 p-12">{error}</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Roles List */}
<div className="lg:col-span-1 space-y-4">
<h2 className="text-xl font-bold text-gray-900 mb-4">الأدوار ({roles.length})</h2>
{roles.map((role) => (
<div
key={role.id}
onClick={() => handleSelectRole(role.id)}
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
selectedRoleId === role.id
? 'border-purple-600 bg-purple-50 shadow-md'
: 'border-gray-200 bg-white hover:border-purple-300 hover:shadow-sm'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-3">
<div
className={`p-2 rounded-lg ${selectedRoleId === role.id ? 'bg-purple-600' : 'bg-purple-100'}`}
>
<Shield
className={`h-5 w-5 ${selectedRoleId === role.id ? 'text-white' : 'text-purple-600'}`}
/>
</div>
<div>
<h3 className="font-bold text-gray-900">{role.titleAr || role.title}</h3>
<p className="text-xs text-gray-600">{role.title}</p>
</div>
</div>
</div>
</div>
<p className="text-sm text-gray-600 mb-3">{role.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Users className="h-4 w-4" />
<span>{role.usersCount} مستخدم</span>
</div>
<div className="flex gap-1">
<button className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Users className="h-4 w-4" />
<span>{role.usersCount ?? role._count?.employees ?? 0} مستخدم</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setSelectedRoleId(role.id);
setShowEditModal(true);
}}
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
>
<Edit className="h-4 w-4" />
</button>
<button className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors">
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
))}
</div>
{/* Permission Matrix */}
<div className="lg:col-span-2">
{currentRole ? (
<div className="bg-white rounded-xl shadow-lg border border-gray-200">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">{currentRole.name}</h2>
<p className="text-gray-600">{currentRole.description}</p>
{/* Permission Matrix */}
<div className="lg:col-span-2">
{currentRole ? (
<div className="bg-white rounded-xl shadow-lg border border-gray-200">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">{currentRole.titleAr || currentRole.title}</h2>
<p className="text-gray-600">{currentRole.title}</p>
</div>
<button
onClick={() => setShowEditModal(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
>
تعديل الصلاحيات
</button>
</div>
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium">
حفظ التغييرات
</button>
</div>
</div>
<div className="p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">مصفوفة الصلاحيات</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b-2 border-gray-200">
<th className="px-4 py-3 text-right text-sm font-bold text-gray-900 min-w-[200px]">
الوحدة
</th>
{permissions.map((perm) => (
<th key={perm.id} className="px-4 py-3 text-center text-sm font-bold text-gray-900">
<div className="flex flex-col items-center gap-1">
<span className="text-xl">{perm.icon}</span>
<span>{perm.name}</span>
</div>
<div className="p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">مصفوفة الصلاحيات</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b-2 border-gray-200">
<th className="px-4 py-3 text-right text-sm font-bold text-gray-900 min-w-[200px]">
الوحدة
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{modules.map((module) => {
const modulePerms = currentRole.permissions[module.id as keyof typeof currentRole.permissions]
return (
{ACTIONS.map((perm) => (
<th key={perm.id} className="px-4 py-3 text-center text-sm font-bold text-gray-900">
{perm.name}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{MODULES.map((module) => (
<tr key={module.id} className="hover:bg-gray-50 transition-colors">
<td className="px-4 py-4">
<div>
@@ -221,72 +235,106 @@ export default function RolesManagement() {
<p className="text-xs text-gray-600">{module.nameEn}</p>
</div>
</td>
{permissions.map((perm) => {
const hasPermission = modulePerms?.[perm.id as keyof typeof modulePerms]
{ACTIONS.map((action) => {
const hasPermission = permissionMatrix[module.id]?.[action.id];
return (
<td key={perm.id} className="px-4 py-4 text-center">
<label className="inline-flex items-center justify-center cursor-pointer">
<input
type="checkbox"
checked={!!hasPermission}
className="sr-only peer"
/>
<div className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all ${
<td key={action.id} className="px-4 py-4 text-center">
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all mx-auto ${
hasPermission
? 'bg-green-500 shadow-md'
: 'bg-gray-200 hover:bg-gray-300'
}`}>
{hasPermission ? (
<Check className="h-6 w-6 text-white" />
) : (
<X className="h-6 w-6 text-gray-500" />
)}
</div>
</label>
? 'bg-green-500 text-white shadow-md'
: 'bg-gray-200 text-gray-500'
}`}
>
{hasPermission ? <Check className="h-6 w-6" /> : <X className="h-6 w-6" />}
</div>
</td>
)
);
})}
</tr>
)
})}
</tbody>
</table>
</div>
{/* Legend */}
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-bold text-blue-900 mb-2">💡 معلومات:</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li> انقر على المربعات لتفعيل أو إلغاء الصلاحيات</li>
<li> الصلاحيات تطبق فوراً على جميع مستخدمي هذا الدور</li>
<li> يجب أن يكون لديك صلاحية "عرض" على الأقل للوصول إلى الوحدة</li>
</ul>
</div>
{/* Quick Actions */}
<div className="mt-6 flex gap-3">
<button className="flex-1 px-4 py-3 border-2 border-green-600 text-green-600 rounded-lg hover:bg-green-50 transition-colors font-semibold">
منح جميع الصلاحيات
</button>
<button className="flex-1 px-4 py-3 border-2 border-red-600 text-red-600 rounded-lg hover:bg-red-50 transition-colors font-semibold">
إلغاء جميع الصلاحيات
</button>
<button className="flex-1 px-4 py-3 border-2 border-blue-600 text-blue-600 rounded-lg hover:bg-blue-50 transition-colors font-semibold">
👁 صلاحيات العرض فقط
</button>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
) : (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-12 text-center">
<Shield className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-2">اختر دوراً لعرض الصلاحيات</h3>
<p className="text-gray-600">اختر دور من القائمة لعرض وتعديل صلاحياته</p>
</div>
)}
) : (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-12 text-center">
<Shield className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-2">اختر دوراً لعرض الصلاحيات</h3>
<p className="text-gray-600">اختر دور من القائمة لعرض وتعديل صلاحياته</p>
</div>
)}
</div>
</div>
</div>
</div>
)
}
)}
{/* Edit Permissions Modal */}
<Modal
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
title={`تعديل صلاحيات: ${currentRole?.titleAr || currentRole?.title || ''}`}
size="2xl"
>
{currentRole && (
<div>
<div className="overflow-x-auto mb-6">
<table className="w-full">
<thead>
<tr className="border-b-2 border-gray-200">
<th className="px-4 py-3 text-right text-sm font-bold text-gray-900">الوحدة</th>
{ACTIONS.map((perm) => (
<th key={perm.id} className="px-4 py-3 text-center text-sm font-bold text-gray-900">
{perm.name}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{MODULES.map((module) => (
<tr key={module.id}>
<td className="px-4 py-4">
<p className="font-semibold text-gray-900">{module.name}</p>
</td>
{ACTIONS.map((action) => {
const hasPermission = permissionMatrix[module.id]?.[action.id];
return (
<td key={action.id} className="px-4 py-4 text-center">
<button
type="button"
onClick={() => handleTogglePermission(module.id, action.id)}
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all mx-auto ${
hasPermission ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-500'
}`}
>
{hasPermission ? <Check className="h-6 w-6" /> : <X className="h-6 w-6" />}
</button>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={() => setShowEditModal(false)}
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium"
>
إلغاء
</button>
<button
onClick={handleSavePermissions}
disabled={saving}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-semibold disabled:opacity-50"
>
{saving ? 'جاري الحفظ...' : 'حفظ التغييرات'}
</button>
</div>
</div>
)}
</Modal>
</div>
);
}