RBAC: Phase 1-3, Total Salary fix, employee creation fix, permission groups, backup script
Made-with: Cursor
This commit is contained in:
@@ -5,14 +5,52 @@ import { AuditLogger } from '../../shared/utils/auditLogger';
|
||||
class HRService {
|
||||
// ========== EMPLOYEES ==========
|
||||
|
||||
private normalizeEmployeeData(data: any): Record<string, any> {
|
||||
const toStr = (v: any) => (v != null && String(v).trim()) ? String(v).trim() : undefined;
|
||||
const toDate = (v: any) => {
|
||||
if (!v || !String(v).trim()) return undefined;
|
||||
const d = new Date(v);
|
||||
return isNaN(d.getTime()) ? undefined : d;
|
||||
};
|
||||
const toNum = (v: any) => (v != null && v !== '') ? Number(v) : undefined;
|
||||
|
||||
const raw: Record<string, any> = {
|
||||
firstName: toStr(data.firstName),
|
||||
lastName: toStr(data.lastName),
|
||||
firstNameAr: toStr(data.firstNameAr),
|
||||
lastNameAr: toStr(data.lastNameAr),
|
||||
email: toStr(data.email),
|
||||
phone: toStr(data.phone),
|
||||
mobile: toStr(data.mobile),
|
||||
dateOfBirth: toDate(data.dateOfBirth),
|
||||
gender: toStr(data.gender),
|
||||
nationality: toStr(data.nationality),
|
||||
nationalId: toStr(data.nationalId),
|
||||
employmentType: toStr(data.employmentType),
|
||||
contractType: toStr(data.contractType),
|
||||
hireDate: toDate(data.hireDate),
|
||||
departmentId: toStr(data.departmentId),
|
||||
positionId: toStr(data.positionId),
|
||||
reportingToId: toStr(data.reportingToId) || undefined,
|
||||
basicSalary: toNum(data.baseSalary ?? data.basicSalary) ?? 0,
|
||||
};
|
||||
return Object.fromEntries(Object.entries(raw).filter(([, v]) => v !== undefined));
|
||||
}
|
||||
|
||||
async createEmployee(data: any, userId: string) {
|
||||
const uniqueEmployeeId = await this.generateEmployeeId();
|
||||
const payload = this.normalizeEmployeeData(data);
|
||||
|
||||
if (!payload.firstName || !payload.lastName || !payload.email || !payload.mobile ||
|
||||
!payload.hireDate || !payload.departmentId || !payload.positionId) {
|
||||
throw new AppError(400, 'بيانات غير مكتملة - Missing required fields: firstName, lastName, email, mobile, hireDate, departmentId, positionId');
|
||||
}
|
||||
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
uniqueEmployeeId,
|
||||
...data,
|
||||
},
|
||||
...payload,
|
||||
} as any,
|
||||
include: {
|
||||
department: true,
|
||||
position: true,
|
||||
@@ -132,9 +170,10 @@ class HRService {
|
||||
throw new AppError(404, 'الموظف غير موجود - Employee not found');
|
||||
}
|
||||
|
||||
const payload = this.normalizeEmployeeData(data);
|
||||
const employee = await prisma.employee.update({
|
||||
where: { id },
|
||||
data,
|
||||
data: payload,
|
||||
include: {
|
||||
department: true,
|
||||
position: true,
|
||||
@@ -383,11 +422,112 @@ class HRService {
|
||||
async findAllDepartments() {
|
||||
const departments = await prisma.department.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
parent: { select: { id: true, name: true, nameAr: true } },
|
||||
_count: { select: { children: true, employees: true } }
|
||||
},
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
return departments;
|
||||
}
|
||||
|
||||
async getDepartmentsHierarchy() {
|
||||
const departments = await prisma.department.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
parent: { select: { id: true, name: true, nameAr: true } },
|
||||
employees: {
|
||||
where: { status: 'ACTIVE' },
|
||||
select: { id: true, firstName: true, lastName: true, firstNameAr: true, lastNameAr: true, position: { select: { title: true, titleAr: true } } }
|
||||
},
|
||||
positions: { select: { id: true, title: true, titleAr: true } },
|
||||
_count: { select: { children: true, employees: true } }
|
||||
},
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
const buildTree = (parentId: string | null): any[] =>
|
||||
departments
|
||||
.filter((d) => d.parentId === parentId)
|
||||
.map((d) => ({
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
nameAr: d.nameAr,
|
||||
code: d.code,
|
||||
parentId: d.parentId,
|
||||
description: d.description,
|
||||
employees: d.employees,
|
||||
positions: d.positions,
|
||||
_count: d._count,
|
||||
children: buildTree(d.id)
|
||||
}));
|
||||
return buildTree(null);
|
||||
}
|
||||
|
||||
async createDepartment(data: { name: string; nameAr?: string; code: string; parentId?: string; description?: string }, userId: string) {
|
||||
const existing = await prisma.department.findUnique({ where: { code: data.code } });
|
||||
if (existing) {
|
||||
throw new AppError(400, 'كود القسم مستخدم مسبقاً - Department code already exists');
|
||||
}
|
||||
if (data.parentId) {
|
||||
const parent = await prisma.department.findUnique({ where: { id: data.parentId } });
|
||||
if (!parent) {
|
||||
throw new AppError(400, 'القسم الأب غير موجود - Parent department not found');
|
||||
}
|
||||
}
|
||||
const department = await prisma.department.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
nameAr: data.nameAr,
|
||||
code: data.code,
|
||||
parentId: data.parentId || null,
|
||||
description: data.description
|
||||
}
|
||||
});
|
||||
await AuditLogger.log({ entityType: 'DEPARTMENT', entityId: department.id, action: 'CREATE', userId, changes: { created: department } });
|
||||
return department;
|
||||
}
|
||||
|
||||
async updateDepartment(id: string, data: { name?: string; nameAr?: string; code?: string; parentId?: string; description?: string; isActive?: boolean }, userId: string) {
|
||||
const existing = await prisma.department.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
throw new AppError(404, 'القسم غير موجود - Department not found');
|
||||
}
|
||||
if (data.code && data.code !== existing.code) {
|
||||
const duplicate = await prisma.department.findUnique({ where: { code: data.code } });
|
||||
if (duplicate) {
|
||||
throw new AppError(400, 'كود القسم مستخدم مسبقاً - Department code already exists');
|
||||
}
|
||||
}
|
||||
if (data.parentId === id) {
|
||||
throw new AppError(400, 'لا يمكن تعيين القسم كأب لنفسه - Department cannot be its own parent');
|
||||
}
|
||||
const department = await prisma.department.update({
|
||||
where: { id },
|
||||
data: { name: data.name, nameAr: data.nameAr, code: data.code, parentId: data.parentId ?? undefined, description: data.description, isActive: data.isActive }
|
||||
});
|
||||
await AuditLogger.log({ entityType: 'DEPARTMENT', entityId: id, action: 'UPDATE', userId, changes: { before: existing, after: department } });
|
||||
return department;
|
||||
}
|
||||
|
||||
async deleteDepartment(id: string, userId: string) {
|
||||
const dept = await prisma.department.findUnique({
|
||||
where: { id },
|
||||
include: { _count: { select: { children: true, employees: true } } }
|
||||
});
|
||||
if (!dept) {
|
||||
throw new AppError(404, 'القسم غير موجود - Department not found');
|
||||
}
|
||||
if (dept._count.children > 0) {
|
||||
throw new AppError(400, 'لا يمكن حذف قسم يحتوي على أقسام فرعية - Cannot delete department with sub-departments');
|
||||
}
|
||||
if (dept._count.employees > 0) {
|
||||
throw new AppError(400, 'لا يمكن حذف قسم فيه موظفون - Cannot delete department with employees');
|
||||
}
|
||||
await prisma.department.delete({ where: { id } });
|
||||
await AuditLogger.log({ entityType: 'DEPARTMENT', entityId: id, action: 'DELETE', userId });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ========== POSITIONS ==========
|
||||
|
||||
async findAllPositions() {
|
||||
|
||||
Reference in New Issue
Block a user