feat: Complete Z.CRM system with all 6 modules

 Features:
- Complete authentication system with JWT
- Dashboard with all 6 modules visible
- Contact Management module (Salesforce-style)
- CRM & Sales Pipeline module (Pipedrive-style)
- Inventory & Assets module (SAP-style)
- Tasks & Projects module (Jira/Asana-style)
- HR Management module (BambooHR-style)
- Marketing Management module (HubSpot-style)
- Admin Panel with user management and role matrix
- World-class UI/UX with RTL Arabic support
- Cairo font (headings) + Readex Pro font (body)
- Sample data for all modules
- Protected routes and authentication flow
- Backend API with Prisma + PostgreSQL
- Comprehensive documentation

🎨 Design:
- Color-coded modules
- Professional data tables
- Stats cards with metrics
- Progress bars and status badges
- Search and filters
- Responsive layout

📊 Tech Stack:
- Frontend: Next.js 14, TypeScript, Tailwind CSS
- Backend: Node.js, Express, Prisma
- Database: PostgreSQL
- Auth: JWT with bcrypt

🚀 Production-ready frontend with all features accessible
This commit is contained in:
Talal Sharabi
2026-01-06 18:43:43 +04:00
commit 35daa52767
82 changed files with 29445 additions and 0 deletions

View File

@@ -0,0 +1,383 @@
import prisma from '../../config/database';
import { AppError } from '../../shared/middleware/errorHandler';
import { AuditLogger } from '../../shared/utils/auditLogger';
class HRService {
// ========== EMPLOYEES ==========
async createEmployee(data: any, userId: string) {
const uniqueEmployeeId = await this.generateEmployeeId();
const employee = await prisma.employee.create({
data: {
uniqueEmployeeId,
...data,
},
include: {
department: true,
position: true,
},
});
await AuditLogger.log({
entityType: 'EMPLOYEE',
entityId: employee.id,
action: 'CREATE',
userId,
});
return employee;
}
async findAllEmployees(filters: any, page: number, pageSize: number) {
const skip = (page - 1) * pageSize;
const where: any = {};
if (filters.search) {
where.OR = [
{ firstName: { contains: filters.search, mode: 'insensitive' } },
{ lastName: { contains: filters.search, mode: 'insensitive' } },
{ email: { contains: filters.search, mode: 'insensitive' } },
{ uniqueEmployeeId: { contains: filters.search } },
];
}
if (filters.departmentId) {
where.departmentId = filters.departmentId;
}
if (filters.status) {
where.status = filters.status;
}
const total = await prisma.employee.count({ where });
const employees = await prisma.employee.findMany({
where,
skip,
take: pageSize,
include: {
department: true,
position: true,
reportingTo: {
select: {
id: true,
firstName: true,
lastName: true,
position: true,
},
},
},
orderBy: {
hireDate: 'desc',
},
});
return { employees, total, page, pageSize };
}
async findEmployeeById(id: string) {
const employee = await prisma.employee.findUnique({
where: { id },
include: {
department: true,
position: {
include: {
permissions: true,
},
},
reportingTo: true,
directReports: true,
user: {
select: {
id: true,
email: true,
username: true,
isActive: true,
},
},
attendances: {
take: 30,
orderBy: {
date: 'desc',
},
},
leaves: {
take: 10,
orderBy: {
createdAt: 'desc',
},
},
salaries: {
take: 12,
orderBy: {
year: 'desc',
month: 'desc',
},
},
},
});
if (!employee) {
throw new AppError(404, 'الموظف غير موجود - Employee not found');
}
return employee;
}
async updateEmployee(id: string, data: any, userId: string) {
const existing = await prisma.employee.findUnique({ where: { id } });
if (!existing) {
throw new AppError(404, 'الموظف غير موجود - Employee not found');
}
const employee = await prisma.employee.update({
where: { id },
data,
include: {
department: true,
position: true,
},
});
await AuditLogger.log({
entityType: 'EMPLOYEE',
entityId: employee.id,
action: 'UPDATE',
userId,
changes: {
before: existing,
after: employee,
},
});
return employee;
}
async terminateEmployee(id: string, terminationDate: Date, reason: string, userId: string) {
const employee = await prisma.employee.update({
where: { id },
data: {
status: 'TERMINATED',
terminationDate,
terminationReason: reason,
},
});
// Disable user account
if (employee.id) {
await prisma.user.updateMany({
where: { employeeId: employee.id },
data: { isActive: false },
});
}
await AuditLogger.log({
entityType: 'EMPLOYEE',
entityId: employee.id,
action: 'TERMINATE',
userId,
reason,
});
return employee;
}
// ========== ATTENDANCE ==========
async recordAttendance(data: any, userId: string) {
const attendance = await prisma.attendance.create({
data,
});
return attendance;
}
async getAttendance(employeeId: string, month: number, year: number) {
return prisma.attendance.findMany({
where: {
employeeId,
date: {
gte: new Date(year, month - 1, 1),
lte: new Date(year, month, 0),
},
},
orderBy: {
date: 'asc',
},
});
}
// ========== LEAVES ==========
async createLeaveRequest(data: any, userId: string) {
const leave = await prisma.leave.create({
data: {
...data,
days: this.calculateLeaveDays(data.startDate, data.endDate),
},
include: {
employee: true,
},
});
await AuditLogger.log({
entityType: 'LEAVE',
entityId: leave.id,
action: 'CREATE',
userId,
});
return leave;
}
async approveLeave(id: string, approvedBy: string, userId: string) {
const leave = await prisma.leave.update({
where: { id },
data: {
status: 'APPROVED',
approvedBy,
approvedAt: new Date(),
},
include: {
employee: true,
},
});
await AuditLogger.log({
entityType: 'LEAVE',
entityId: leave.id,
action: 'APPROVE',
userId,
});
return leave;
}
// ========== SALARIES ==========
async processSalary(employeeId: string, month: number, year: number, userId: string) {
const employee = await prisma.employee.findUnique({
where: { id: employeeId },
include: {
allowances: {
where: {
OR: [
{ isRecurring: true },
{
startDate: {
lte: new Date(year, month, 0),
},
OR: [
{ endDate: null },
{
endDate: {
gte: new Date(year, month - 1, 1),
},
},
],
},
],
},
},
commissions: {
where: {
month,
year,
status: 'APPROVED',
},
},
},
});
if (!employee) {
throw new AppError(404, 'الموظف غير موجود - Employee not found');
}
const basicSalary = employee.basicSalary;
const allowances = employee.allowances.reduce((sum, a) => sum + Number(a.amount), 0);
const commissions = employee.commissions.reduce((sum, c) => sum + Number(c.amount), 0);
// Calculate overtime from attendance
const attendance = await prisma.attendance.findMany({
where: {
employeeId,
date: {
gte: new Date(year, month - 1, 1),
lte: new Date(year, month, 0),
},
},
});
const overtimeHours = attendance.reduce((sum, a) => sum + Number(a.overtimeHours || 0), 0);
const overtimePay = overtimeHours * 50; // SAR 50 per hour
const deductions = 0; // Calculate based on business rules
const netSalary = Number(basicSalary) + allowances + commissions + overtimePay - deductions;
const salary = await prisma.salary.create({
data: {
employeeId,
month,
year,
basicSalary,
allowances,
deductions,
commissions,
overtimePay,
netSalary,
},
});
await AuditLogger.log({
entityType: 'SALARY',
entityId: salary.id,
action: 'PROCESS',
userId,
});
return salary;
}
// ========== HELPERS ==========
private async generateEmployeeId(): Promise<string> {
const year = new Date().getFullYear();
const prefix = `EMP-${year}-`;
const lastEmployee = await prisma.employee.findFirst({
where: {
uniqueEmployeeId: {
startsWith: prefix,
},
},
orderBy: {
createdAt: 'desc',
},
select: {
uniqueEmployeeId: true,
},
});
let nextNumber = 1;
if (lastEmployee) {
const lastNumber = parseInt(lastEmployee.uniqueEmployeeId.split('-')[2]);
nextNumber = lastNumber + 1;
}
return `${prefix}${nextNumber.toString().padStart(4, '0')}`;
}
private calculateLeaveDays(startDate: Date, endDate: Date): number {
const start = new Date(startDate);
const end = new Date(endDate);
const diffTime = Math.abs(end.getTime() - start.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays + 1;
}
}
export const hrService = new HRService();