add delete option to roles
This commit is contained in:
BIN
backend.zip
BIN
backend.zip
Binary file not shown.
@@ -145,6 +145,16 @@ class AdminController {
|
|||||||
next(error);
|
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();
|
||||||
@@ -89,6 +89,15 @@ router.get(
|
|||||||
adminController.getPositions
|
adminController.getPositions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Delete (soft delete) a role/position
|
||||||
|
router.delete(
|
||||||
|
'/positions/:id',
|
||||||
|
authorize('admin', 'roles', 'delete'),
|
||||||
|
param('id').isUUID(),
|
||||||
|
validate,
|
||||||
|
adminController.deletePosition
|
||||||
|
);
|
||||||
|
|
||||||
router.put(
|
router.put(
|
||||||
'/positions/:id/permissions',
|
'/positions/:id/permissions',
|
||||||
authorize('admin', 'roles', 'update'),
|
authorize('admin', 'roles', 'update'),
|
||||||
|
|||||||
@@ -429,6 +429,57 @@ class AdminService {
|
|||||||
|
|
||||||
return this.getPositions().then((pos) => pos.find((p) => p.id === positionId) || position);
|
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();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
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 { positionsAPI } from '@/lib/api/admin';
|
||||||
import type { PositionRole, PositionPermission } from '@/lib/api/admin';
|
import type { PositionRole, PositionPermission } from '@/lib/api/admin';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
@@ -67,6 +67,9 @@ export default function RolesManagement() {
|
|||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [permissionMatrix, setPermissionMatrix] = useState<Record<string, Record<string, boolean>>>({});
|
const [permissionMatrix, setPermissionMatrix] = useState<Record<string, Record<string, boolean>>>({});
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
const [roleToDelete, setRoleToDelete] = useState<PositionRole | null>(null);
|
||||||
|
|
||||||
const fetchRoles = useCallback(async () => {
|
const fetchRoles = useCallback(async () => {
|
||||||
setLoading(true);
|
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) => {
|
const handleSelectRole = (id: string) => {
|
||||||
setSelectedRoleId(id);
|
setSelectedRoleId(id);
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
@@ -177,16 +210,32 @@ export default function RolesManagement() {
|
|||||||
<Users className="h-4 w-4" />
|
<Users className="h-4 w-4" />
|
||||||
<span>{role.usersCount ?? role._count?.employees ?? 0} مستخدم</span>
|
<span>{role.usersCount ?? role._count?.employees ?? 0} مستخدم</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
{/* Actions: Edit + Delete */}
|
||||||
e.stopPropagation();
|
<div className="flex items-center gap-1">
|
||||||
setSelectedRoleId(role.id);
|
<button
|
||||||
setShowEditModal(true);
|
onClick={(e) => {
|
||||||
}}
|
e.stopPropagation();
|
||||||
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
setSelectedRoleId(role.id);
|
||||||
>
|
setShowEditModal(true);
|
||||||
<Edit className="h-4 w-4" />
|
}}
|
||||||
</button>
|
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||||
|
title="تعديل"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openDeleteDialog(role);
|
||||||
|
}}
|
||||||
|
className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||||
|
title="حذف"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -269,6 +318,64 @@ export default function RolesManagement() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
{showDeleteDialog && roleToDelete && (
|
||||||
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50"
|
||||||
|
onClick={() => {
|
||||||
|
if (!deleting) {
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
setRoleToDelete(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex min-h-screen items-center justify-center p-4">
|
||||||
|
<div className="relative bg-white rounded-xl shadow-2xl p-6 max-w-md w-full">
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="bg-red-100 p-3 rounded-full">
|
||||||
|
<Trash2 className="h-6 w-6 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">حذف الدور</h3>
|
||||||
|
<p className="text-sm text-gray-600">هذا الإجراء لا يمكن التراجع عنه</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
هل أنت متأكد أنك تريد حذف دور{' '}
|
||||||
|
<span className="font-semibold">{roleToDelete.titleAr || roleToDelete.title}</span>؟
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
setRoleToDelete(null);
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteRole}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{deleting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
جاري الحذف...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'حذف'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Edit Permissions Modal */}
|
{/* Edit Permissions Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showEditModal}
|
isOpen={showEditModal}
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ export const positionsAPI = {
|
|||||||
return response.data.data || [];
|
return response.data.data || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
delete: async (positionId: string): Promise<void> => {
|
||||||
|
await api.delete(`/admin/positions/${positionId}`);
|
||||||
|
},
|
||||||
|
|
||||||
updatePermissions: async (
|
updatePermissions: async (
|
||||||
positionId: string,
|
positionId: string,
|
||||||
permissions: Array<{ module: string; resource: string; actions: string[] }>
|
permissions: Array<{ module: string; resource: string; actions: string[] }>
|
||||||
|
|||||||
Reference in New Issue
Block a user