diff --git a/backend.zip b/backend.zip deleted file mode 100644 index 53d6c33..0000000 Binary files a/backend.zip and /dev/null differ diff --git a/backend/src/modules/admin/admin.controller.ts b/backend/src/modules/admin/admin.controller.ts index 441e44e..4bfaf4d 100644 --- a/backend/src/modules/admin/admin.controller.ts +++ b/backend/src/modules/admin/admin.controller.ts @@ -145,6 +145,16 @@ class AdminController { next(error); } } + + async deletePosition(req: AuthRequest, res: Response, next: NextFunction) { + try { + const userId = req.user!.id; + await adminService.deletePosition(req.params.id, userId); + res.json(ResponseFormatter.success(null, 'Role deleted successfully')); + } catch (error) { + next(error); + } + } } -export const adminController = new AdminController(); +export const adminController = new AdminController(); \ No newline at end of file diff --git a/backend/src/modules/admin/admin.routes.ts b/backend/src/modules/admin/admin.routes.ts index 3ebf286..8ba4f95 100644 --- a/backend/src/modules/admin/admin.routes.ts +++ b/backend/src/modules/admin/admin.routes.ts @@ -89,6 +89,15 @@ router.get( adminController.getPositions ); +// Delete (soft delete) a role/position +router.delete( + '/positions/:id', + authorize('admin', 'roles', 'delete'), + param('id').isUUID(), + validate, + adminController.deletePosition +); + router.put( '/positions/:id/permissions', authorize('admin', 'roles', 'update'), @@ -100,4 +109,4 @@ router.put( adminController.updatePositionPermissions ); -export default router; +export default router; \ No newline at end of file diff --git a/backend/src/modules/admin/admin.service.ts b/backend/src/modules/admin/admin.service.ts index d73e12b..c7d0eac 100644 --- a/backend/src/modules/admin/admin.service.ts +++ b/backend/src/modules/admin/admin.service.ts @@ -429,6 +429,57 @@ class AdminService { return this.getPositions().then((pos) => pos.find((p) => p.id === positionId) || position); } + + /** + * Soft delete a role (Position). + * - Prevent deletion if the position is assigned to any employees. + * - Clean up position permissions. + */ + async deletePosition(positionId: string, deletedById: string) { + const position = await prisma.position.findUnique({ + where: { id: positionId }, + include: { + _count: { select: { employees: true } }, + }, + }); + + if (!position) { + throw new AppError(404, 'الدور غير موجود - Position not found'); + } + + if (position._count.employees > 0) { + throw new AppError( + 400, + 'لا يمكن حذف هذا الدور لأنه مرتبط بموظفين. قم بتغيير دور الموظفين أولاً - Cannot delete: position is assigned to employees' + ); + } + + // Soft delete the position + await prisma.position.update({ + where: { id: positionId }, + data: { isActive: false }, + }); + + // Clean up permissions linked to this position + await prisma.positionPermission.deleteMany({ + where: { positionId }, + }); + + await AuditLogger.log({ + entityType: 'POSITION', + entityId: positionId, + action: 'DELETE', + userId: deletedById, + changes: { + softDeleted: true, + title: position.title, + titleAr: position.titleAr, + code: position.code, + }, + }); + + return { success: true }; + } } -export const adminService = new AdminService(); +export const adminService = new AdminService(); \ No newline at end of file diff --git a/frontend/src/app/admin/roles/page.tsx b/frontend/src/app/admin/roles/page.tsx index 252c981..79ed14d 100644 --- a/frontend/src/app/admin/roles/page.tsx +++ b/frontend/src/app/admin/roles/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { Shield, Edit, Users, Check, X } from 'lucide-react'; +import { Shield, Edit, Trash2, Users, Check, X, Loader2 } from 'lucide-react'; import { positionsAPI } from '@/lib/api/admin'; import type { PositionRole, PositionPermission } from '@/lib/api/admin'; import Modal from '@/components/Modal'; @@ -67,6 +67,9 @@ export default function RolesManagement() { const [showEditModal, setShowEditModal] = useState(false); const [permissionMatrix, setPermissionMatrix] = useState>>({}); const [saving, setSaving] = useState(false); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [deleting, setDeleting] = useState(false); + const [roleToDelete, setRoleToDelete] = useState(null); const fetchRoles = useCallback(async () => { setLoading(true); @@ -121,6 +124,36 @@ export default function RolesManagement() { } }; + const openDeleteDialog = (role: PositionRole) => { + setRoleToDelete(role); + setShowDeleteDialog(true); + }; + + const handleDeleteRole = async () => { + if (!roleToDelete) return; + setDeleting(true); + try { + await positionsAPI.delete(roleToDelete.id); + + if (selectedRoleId === roleToDelete.id) { + setSelectedRoleId(null); + } + + setShowDeleteDialog(false); + setRoleToDelete(null); + await fetchRoles(); + } catch (err: unknown) { + const msg = + (err as any)?.response?.data?.message || + (err as any)?.response?.data?.error || + (err as any)?.message || + 'فشل حذف الدور'; + alert(msg); + } finally { + setDeleting(false); + } + }; + const handleSelectRole = (id: string) => { setSelectedRoleId(id); setShowEditModal(false); @@ -177,16 +210,32 @@ export default function RolesManagement() { {role.usersCount ?? role._count?.employees ?? 0} مستخدم - + + {/* Actions: Edit + Delete */} +
+ + + +
))} @@ -269,6 +318,64 @@ export default function RolesManagement() { )} + {/* Delete Confirmation Dialog */} + {showDeleteDialog && roleToDelete && ( +
+
{ + if (!deleting) { + setShowDeleteDialog(false); + setRoleToDelete(null); + } + }} + /> +
+
+
+
+ +
+
+

حذف الدور

+

هذا الإجراء لا يمكن التراجع عنه

+
+
+

+ هل أنت متأكد أنك تريد حذف دور{' '} + {roleToDelete.titleAr || roleToDelete.title}؟ +

+
+ + +
+
+
+
+ )} + {/* Edit Permissions Modal */}
); -} +} \ No newline at end of file diff --git a/frontend/src/lib/api/admin.ts b/frontend/src/lib/api/admin.ts index 1be6a58..6cd7bf1 100644 --- a/frontend/src/lib/api/admin.ts +++ b/frontend/src/lib/api/admin.ts @@ -138,6 +138,10 @@ export const positionsAPI = { return response.data.data || []; }, + delete: async (positionId: string): Promise => { + await api.delete(`/admin/positions/${positionId}`); + }, + updatePermissions: async ( positionId: string, permissions: Array<{ module: string; resource: string; actions: string[] }> @@ -263,4 +267,4 @@ export const healthAPI = { const response = await api.get('/admin/health').catch(() => ({ data: { data: {} } })); return response.data?.data || response.data || {}; }, -}; +}; \ No newline at end of file