Files
betterhuman/backend/src/routes/employees.ts
T

252 lines
7.5 KiB
TypeScript

import { Router, Response } from 'express';
import bcrypt from 'bcryptjs';
import { z } from 'zod';
import prisma from '../lib/prisma';
import { requireAuth, AuthRequest } from '../middleware/auth';
import { validate } from '../middleware/validate';
const router = Router();
const createEmployeeSchema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
workEmail: z.string().email(),
departmentId: z.string().optional(),
positionId: z.string().optional(),
locationId: z.string().optional(),
managerId: z.string().optional(),
employmentType: z.enum(['FULL_TIME', 'PART_TIME', 'CONTRACT', 'INTERN']).default('FULL_TIME'),
salary: z.number().min(0).default(0),
phone: z.string().optional(),
gender: z.string().optional(),
startDate: z.string().optional(),
pfApplicable: z.boolean().default(true),
esiApplicable: z.boolean().default(true),
});
// GET /employees
router.get('/', requireAuth, async (req: AuthRequest, res: Response) => {
try {
const { search, departmentId, status, page = '1', limit = '20' } = req.query as any;
const skip = (parseInt(page) - 1) * parseInt(limit);
const where: any = {
companyId: req.user!.companyId,
};
if (search) {
where.OR = [
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ workEmail: { contains: search, mode: 'insensitive' } },
{ employeeCode: { contains: search, mode: 'insensitive' } },
];
}
if (departmentId) where.departmentId = departmentId;
if (status) where.status = status;
const [employees, total] = await Promise.all([
prisma.employee.findMany({
where,
skip,
take: parseInt(limit),
include: {
department: { select: { name: true } },
position: { select: { name: true } },
location: { select: { name: true } },
manager: { select: { firstName: true, lastName: true } },
},
orderBy: { createdAt: 'desc' },
}),
prisma.employee.count({ where }),
]);
return res.json({
data: employees,
total,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(total / parseInt(limit)),
});
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Internal server error' });
}
});
// GET /employees/org-chart
router.get('/org-chart', requireAuth, async (req: AuthRequest, res: Response) => {
try {
const employees = await prisma.employee.findMany({
where: { companyId: req.user!.companyId, status: { not: 'TERMINATED' } },
select: {
id: true,
firstName: true,
lastName: true,
managerId: true,
position: { select: { name: true } },
department: { select: { name: true } },
},
});
const buildTree = (employees: any[], parentId: string | null = null): any[] => {
return employees
.filter(e => e.managerId === parentId)
.map(e => ({
...e,
children: buildTree(employees, e.id),
}));
};
const tree = buildTree(employees);
return res.json(tree);
} catch (err) {
return res.status(500).json({ error: 'Internal server error' });
}
});
// GET /employees/:id
router.get('/:id', requireAuth, async (req: AuthRequest, res: Response) => {
try {
const employee = await prisma.employee.findFirst({
where: { id: req.params.id, companyId: req.user!.companyId },
include: {
department: true,
position: true,
location: true,
manager: { select: { id: true, firstName: true, lastName: true } },
reports: { select: { id: true, firstName: true, lastName: true, position: { select: { name: true } } } },
leaveBalances: { include: { policy: true } },
},
});
if (!employee) {
return res.status(404).json({ error: 'Employee not found' });
}
return res.json(employee);
} catch (err) {
return res.status(500).json({ error: 'Internal server error' });
}
});
// POST /employees
router.post('/', requireAuth, validate(createEmployeeSchema), async (req: AuthRequest, res: Response) => {
try {
const companyId = req.user!.companyId;
const data = req.body;
// Generate employee code
const count = await prisma.employee.count({ where: { companyId } });
const employeeCode = `EMP${String(count + 1).padStart(3, '0')}`;
// Create user account
const tempPassword = Math.random().toString(36).slice(-8);
const passwordHash = await bcrypt.hash(tempPassword, 12);
const user = await prisma.user.create({
data: {
companyId,
email: data.workEmail,
passwordHash,
role: 'EMPLOYEE',
},
});
const employee = await prisma.employee.create({
data: {
userId: user.id,
companyId,
employeeCode,
firstName: data.firstName,
lastName: data.lastName,
workEmail: data.workEmail,
departmentId: data.departmentId || null,
positionId: data.positionId || null,
locationId: data.locationId || null,
managerId: data.managerId || null,
employmentType: data.employmentType,
salary: data.salary,
phone: data.phone || null,
gender: data.gender ? data.gender.toUpperCase() : null,
startDate: data.startDate ? new Date(data.startDate) : new Date(),
pfApplicable: data.pfApplicable,
esiApplicable: data.esiApplicable,
},
include: {
department: true,
position: true,
location: true,
},
});
// Initialize leave balances for current year
const policies = await prisma.leavePolicy.findMany({ where: { companyId, isActive: true } });
const year = new Date().getFullYear();
if (policies.length > 0) {
await prisma.leaveBalance.createMany({
data: policies.map(p => ({
employeeId: employee.id,
policyId: p.id,
year,
allocated: p.accrualDays * 12,
used: 0,
balance: p.accrualDays * 12,
})),
skipDuplicates: true,
});
}
return res.status(201).json({ ...employee, tempPassword });
} catch (err: any) {
if (err.code === 'P2002') {
return res.status(409).json({ error: 'Email already exists' });
}
console.error(err);
return res.status(500).json({ error: 'Internal server error' });
}
});
// PATCH /employees/:id
router.patch('/:id', requireAuth, async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const existing = await prisma.employee.findFirst({
where: { id, companyId: req.user!.companyId },
});
if (!existing) return res.status(404).json({ error: 'Employee not found' });
const employee = await prisma.employee.update({
where: { id },
data: {
...req.body,
updatedAt: new Date(),
},
include: {
department: true,
position: true,
location: true,
},
});
return res.json(employee);
} catch (err) {
return res.status(500).json({ error: 'Internal server error' });
}
});
// DELETE /employees/:id (soft delete)
router.delete('/:id', requireAuth, async (req: AuthRequest, res: Response) => {
try {
await prisma.employee.update({
where: { id: req.params.id },
data: { status: 'TERMINATED' },
});
return res.json({ message: 'Employee terminated' });
} catch (err) {
return res.status(500).json({ error: 'Internal server error' });
}
});
export default router;