fix roles view

This commit is contained in:
yotakii
2026-03-02 13:24:57 +03:00
parent 0b886e81f0
commit e74f872e92
7 changed files with 615 additions and 108 deletions

View File

@@ -134,6 +134,42 @@ class AdminController {
}
}
async createPosition(req: AuthRequest, res: Response, next: NextFunction) {
try {
const userId = req.user!.id;
const position = await adminService.createPosition(
{
title: req.body.title,
titleAr: req.body.titleAr,
departmentId: req.body.departmentId,
level: req.body.level,
code: req.body.code,
},
userId
);
res.status(201).json(ResponseFormatter.success(position));
} catch (error) {
next(error);
}
}
async updatePosition(req: AuthRequest, res: Response, next: NextFunction) {
try {
const userId = req.user!.id;
const position = await adminService.updatePosition(
req.params.id,
{
title: req.body.title,
titleAr: req.body.titleAr,
},
userId
);
res.json(ResponseFormatter.success(position));
} catch (error) {
next(error);
}
}
async updatePositionPermissions(req: AuthRequest, res: Response, next: NextFunction) {
try {
const position = await adminService.updatePositionPermissions(

View File

@@ -89,6 +89,34 @@ router.get(
adminController.getPositions
);
// Create role
router.post(
'/positions',
authorize('admin', 'roles', 'create'),
[
body('title').notEmpty().trim(),
body('titleAr').optional().isString().trim(),
body('departmentId').isUUID(),
body('level').optional().isInt({ min: 1 }),
body('code').optional().isString().trim(),
],
validate,
adminController.createPosition
);
// Update role name (title/titleAr)
router.put(
'/positions/:id',
authorize('admin', 'roles', 'update'),
[
param('id').isUUID(),
body('title').optional().notEmpty().trim(),
body('titleAr').optional().isString().trim(),
],
validate,
adminController.updatePosition
);
// Delete (soft delete) a role/position
router.delete(
'/positions/:id',

View File

@@ -39,6 +39,19 @@ export interface AuditLogFilters {
pageSize?: number;
}
export interface CreatePositionData {
title: string;
titleAr?: string;
departmentId: string;
level?: number;
code?: string;
}
export interface UpdatePositionData {
title?: string;
titleAr?: string;
}
class AdminService {
// ========== USERS ==========
@@ -94,7 +107,7 @@ class AdminService {
]);
const sanitized = users.map((u) => {
const { password: _, ...rest } = u;
const { password: _, ...rest } = u as any;
return rest;
});
@@ -124,7 +137,7 @@ class AdminService {
throw new AppError(404, 'المستخدم غير موجود - User not found');
}
const { password: _, ...rest } = user;
const { password: _, ...rest } = user as any;
return rest;
}
@@ -223,7 +236,7 @@ class AdminService {
...(data.email && { email: data.email }),
...(data.username && { username: data.username }),
...(data.isActive !== undefined && { isActive: data.isActive }),
...(data.employeeId !== undefined && { employeeId: data.employeeId || null }),
...(data.employeeId !== undefined && { employeeId: (data.employeeId as any) || null }),
};
if (data.password && data.password.length >= 8) {
@@ -242,7 +255,7 @@ class AdminService {
},
});
const { password: _, ...sanitized } = user;
const { password: _, ...sanitized } = user as any;
await AuditLogger.log({
entityType: 'USER',
@@ -273,7 +286,7 @@ class AdminService {
},
});
const { password: _, ...sanitized } = updated;
const { password: _, ...sanitized } = updated as any;
await AuditLogger.log({
entityType: 'USER',
@@ -380,7 +393,7 @@ class AdminService {
const positions = await prisma.position.findMany({
where: { isActive: true },
include: {
department: { select: { name: true, nameAr: true } },
department: { select: { id: true, name: true, nameAr: true } },
permissions: true,
_count: {
select: {
@@ -406,6 +419,120 @@ class AdminService {
return withUserCount;
}
private async generateUniqueCode(base: string) {
const cleaned = (base || 'ROLE')
.toUpperCase()
.replace(/[^A-Z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.slice(0, 18) || 'ROLE';
for (let i = 0; i < 25; i++) {
const suffix = Math.floor(1000 + Math.random() * 9000);
const code = `${cleaned}_${suffix}`;
const exists = await prisma.position.findUnique({ where: { code } });
if (!exists) return code;
}
// fallback
return `${cleaned}_${Date.now()}`;
}
async createPosition(data: CreatePositionData, createdById: string) {
const title = (data.title || '').trim();
const titleAr = (data.titleAr || '').trim();
if (!title && !titleAr) {
throw new AppError(400, 'اسم الدور مطلوب - Role name is required');
}
const department = await prisma.department.findUnique({
where: { id: data.departmentId },
});
if (!department || !department.isActive) {
throw new AppError(400, 'القسم غير موجود - Department not found');
}
let code = (data.code || '').trim();
if (code) {
code = code.toUpperCase().replace(/[^A-Z0-9_]+/g, '_');
const exists = await prisma.position.findUnique({ where: { code } });
if (exists) {
throw new AppError(400, 'الكود مستخدم بالفعل - Code already exists');
}
} else {
code = await this.generateUniqueCode(title || titleAr || 'ROLE');
}
const level = Number.isFinite(data.level as any) ? Math.max(1, Number(data.level)) : 1;
const created = await prisma.position.create({
data: {
title: title || titleAr,
titleAr: titleAr || null,
code,
departmentId: data.departmentId,
level,
},
});
await AuditLogger.log({
entityType: 'POSITION',
entityId: created.id,
action: 'CREATE',
userId: createdById,
changes: {
created: {
title: created.title,
titleAr: created.titleAr,
code: created.code,
departmentId: created.departmentId,
level: created.level,
},
},
});
const all = await this.getPositions();
return all.find((p: any) => p.id === created.id) || created;
}
async updatePosition(positionId: string, data: UpdatePositionData, updatedById: string) {
const existing = await prisma.position.findUnique({ where: { id: positionId } });
if (!existing) {
throw new AppError(404, 'الدور غير موجود - Position not found');
}
const nextTitle = data.title !== undefined ? (data.title || '').trim() : existing.title;
const nextTitleAr = data.titleAr !== undefined ? (data.titleAr || '').trim() : (existing.titleAr || '');
const finalTitle = nextTitle || nextTitleAr;
if (!finalTitle) {
throw new AppError(400, 'اسم الدور مطلوب - Role name is required');
}
const updated = await prisma.position.update({
where: { id: positionId },
data: {
title: finalTitle,
titleAr: nextTitleAr ? nextTitleAr : null,
},
});
await AuditLogger.log({
entityType: 'POSITION',
entityId: positionId,
action: 'UPDATE',
userId: updatedById,
changes: {
before: { title: existing.title, titleAr: existing.titleAr },
after: { title: updated.title, titleAr: updated.titleAr },
},
});
const all = await this.getPositions();
return all.find((p: any) => p.id === positionId) || updated;
}
async updatePositionPermissions(positionId: string, permissions: Array<{ module: string; resource: string; actions: string[] }>) {
const position = await prisma.position.findUnique({ where: { id: positionId } });
if (!position) {
@@ -427,7 +554,7 @@ class AdminService {
});
}
return this.getPositions().then((pos) => pos.find((p) => p.id === positionId) || position);
return this.getPositions().then((pos: any) => pos.find((p: any) => p.id === positionId) || position);
}
/**