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;