add delete option to roles

This commit is contained in:
yotakii
2026-03-02 10:44:23 +03:00
parent 5164f04b66
commit 0b886e81f0
6 changed files with 197 additions and 16 deletions

Binary file not shown.

View File

@@ -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();

View File

@@ -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'),

View File

@@ -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();

View File

@@ -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,6 +210,9 @@ 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>
{/* Actions: Edit + Delete */}
<div className="flex items-center gap-1">
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -184,9 +220,22 @@ export default function RolesManagement() {
setShowEditModal(true); setShowEditModal(true);
}} }}
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors" className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="تعديل"
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</button> </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}

View File

@@ -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[] }>