From bebcbd0d39f9e6ac27175a261bf2387f4fe06fbe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Jul 2025 05:26:44 +0000 Subject: [PATCH 1/3] Initial plan From 7818c8ad38e6c4e47a1f11d63438e1836de0be34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Jul 2025 05:38:50 +0000 Subject: [PATCH 2/3] Merge conflict resolution: employee.routes.ts with Phase 2 Smart Attendance and enhanced security Co-authored-by: W3JDev <174652026+W3JDev@users.noreply.github.com> --- apps/backend/src/routes/employee.routes.ts | 480 ++++++++++++++++++--- 1 file changed, 432 insertions(+), 48 deletions(-) diff --git a/apps/backend/src/routes/employee.routes.ts b/apps/backend/src/routes/employee.routes.ts index 483e8df..e4f6e08 100644 --- a/apps/backend/src/routes/employee.routes.ts +++ b/apps/backend/src/routes/employee.routes.ts @@ -12,8 +12,43 @@ const router = Router(); router.use(authenticateToken); router.use(tenantIsolation); +// Schema validation types for Phase 2 Smart Attendance System +interface CreateEmployeeRequest { + organizationId: string; + employeeId: string; + firstName: string; + lastName: string; + email?: string; + phone?: string; + dateOfBirth?: string; + position?: string; + departmentId?: string; + employmentType?: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT' | 'INTERN' | 'TEMPORARY'; + hireDate?: string; + salary?: number; + hourlyRate?: number; + currency?: string; + pin?: string; +} + +interface UpdateEmployeeRequest { + firstName?: string; + lastName?: string; + email?: string; + phone?: string; + dateOfBirth?: string; + position?: string; + departmentId?: string; + employmentType?: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT' | 'INTERN' | 'TEMPORARY'; + terminationDate?: string; + salary?: number; + hourlyRate?: number; + pin?: string; + isActive?: boolean; +} + // @route GET /api/v1/employees -// @desc Get all employees in the organization +// @desc Get all employees in the organization (Phase 2 Smart Attendance System) // @access Private (Manager+) router.get('/', requireManager, [ query('page') @@ -36,7 +71,11 @@ router.get('/', requireManager, [ query('isActive') .optional() .isBoolean() - .withMessage('isActive must be a boolean') + .withMessage('isActive must be a boolean'), + query('employmentType') + .optional() + .isIn(['FULL_TIME', 'PART_TIME', 'CONTRACT', 'INTERN', 'TEMPORARY']) + .withMessage('Invalid employment type') ], auditRead('Employee'), async (req: Request, res: Response): Promise => { try { // Check validation errors @@ -61,6 +100,7 @@ router.get('/', requireManager, [ const skip = (page - 1) * limit; const search = req.query.search as string; const departmentId = req.query.departmentId as string; + const employmentType = req.query.employmentType as string; const isActive = req.query.isActive === 'true' ? true : req.query.isActive === 'false' ? false : undefined; // Build where clause with tenant isolation @@ -81,11 +121,15 @@ router.get('/', requireManager, [ where.departmentId = departmentId; } + if (employmentType) { + where.employmentType = employmentType; + } + if (isActive !== undefined) { where.isActive = isActive; } - // Get employees with pagination + // Get employees with pagination and Phase 2 enhanced data const [employees, totalCount] = await Promise.all([ db.employee.findMany({ where, @@ -102,6 +146,8 @@ router.get('/', requireManager, [ select: { id: true, email: true, + role: true, + isActive: true, lastLoginAt: true } }, @@ -135,7 +181,8 @@ router.get('/', requireManager, [ hasNextPage: page < totalPages, hasPreviousPage: page > 1 } - } + }, + count: employees.length }); } catch (error: any) { logger.error('Failed to get employees:', error); @@ -147,7 +194,7 @@ router.get('/', requireManager, [ }); // @route POST /api/v1/employees -// @desc Create new employee +// @desc Create new employee (Phase 2 Smart Attendance System) // @access Private (HR Manager+) router.post('/', requireHRManager, [ body('firstName') @@ -196,7 +243,19 @@ router.post('/', requireHRManager, [ body('hireDate') .optional() .isISO8601() - .withMessage('Hire date must be a valid date') + .withMessage('Hire date must be a valid date'), + body('dateOfBirth') + .optional() + .isISO8601() + .withMessage('Date of birth must be a valid date'), + body('currency') + .optional() + .isLength({ min: 3, max: 3 }) + .withMessage('Currency must be a 3-letter code'), + body('pin') + .optional() + .isLength({ min: 4, max: 8 }) + .withMessage('PIN must be between 4 and 8 characters') ], auditCreate('Employee'), async (req: Request, res: Response): Promise => { try { // Check validation errors @@ -222,12 +281,15 @@ router.post('/', requireHRManager, [ employeeId, email, phone, + dateOfBirth, position, departmentId, employmentType, + hireDate, salary, hourlyRate, - hireDate + currency, + pin } = req.body; // Check if employee ID already exists in this organization @@ -281,34 +343,51 @@ router.post('/', requireHRManager, [ } } - // Create employee + // Create employee with Phase 2 enhanced data + const employeeData = { + organizationId: req.organization.id, + employeeId, + firstName, + lastName, + email, + phone, + dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : null, + position, + departmentId, + employmentType: employmentType || 'FULL_TIME', + hireDate: hireDate ? new Date(hireDate) : new Date(), + salary: salary ? parseFloat(salary) : null, + hourlyRate: hourlyRate ? parseFloat(hourlyRate) : null, + currency: currency || 'USD', + pin, + isActive: true + }; + const employee = await db.employee.create({ - data: { - organizationId: req.organization.id, - firstName, - lastName, - employeeId, - email, - phone, - position, - departmentId, - employmentType: employmentType || 'FULL_TIME', - salary: salary ? parseFloat(salary) : null, - hourlyRate: hourlyRate ? parseFloat(hourlyRate) : null, - hireDate: hireDate ? new Date(hireDate) : new Date(), - isActive: true - }, + data: employeeData, include: { department: { select: { id: true, name: true } + }, + organization: { + select: { + id: true, + businessName: true, + orgCode: true + } } } }); - logger.info(`Employee created: ${employeeId} by ${req.user?.email} in organization ${req.organization.orgCode}`); + logger.info(`Employee created: ${employeeId} by ${req.user?.email} in organization ${req.organization.orgCode}`, { + organizationId: req.organization.id, + employeeId, + firstName, + lastName + }); return res.status(201).json({ success: true, @@ -325,7 +404,7 @@ router.post('/', requireHRManager, [ }); // @route GET /api/v1/employees/:id -// @desc Get employee details +// @desc Get employee details (Phase 2 Smart Attendance System) // @access Private (Manager+) router.get('/:id', requireManager, [ param('id') @@ -362,15 +441,42 @@ router.get('/:id', requireManager, [ department: { select: { id: true, - name: true + name: true, + description: true } }, user: { select: { id: true, email: true, - lastLoginAt: true, - role: true + role: true, + isActive: true, + lastLoginAt: true + } + }, + attendanceRecords: { + orderBy: { date: 'desc' }, + take: 10, + select: { + id: true, + date: true, + checkInTime: true, + checkOutTime: true, + hoursWorked: true, + status: true, + isLate: true, + isEarlyLeave: true, + overtimeHours: true + } + }, + leaveRequests: { + where: { status: 'PENDING' }, + select: { + id: true, + leaveType: true, + startDate: true, + endDate: true, + status: true } }, _count: { @@ -405,7 +511,7 @@ router.get('/:id', requireManager, [ }); // @route PUT /api/v1/employees/:id -// @desc Update employee +// @desc Update employee (Phase 2 Smart Attendance System) // @access Private (HR Manager+) router.put('/:id', requireHRManager, [ param('id') @@ -456,7 +562,19 @@ router.put('/:id', requireHRManager, [ body('isActive') .optional() .isBoolean() - .withMessage('isActive must be a boolean') + .withMessage('isActive must be a boolean'), + body('dateOfBirth') + .optional() + .isISO8601() + .withMessage('Date of birth must be a valid date'), + body('terminationDate') + .optional() + .isISO8601() + .withMessage('Termination date must be a valid date'), + body('pin') + .optional() + .isLength({ min: 4, max: 8 }) + .withMessage('PIN must be between 4 and 8 characters') ], auditUpdate('Employee'), async (req: Request, res: Response): Promise => { try { // Check validation errors @@ -529,15 +647,22 @@ router.put('/:id', requireHRManager, [ } } + // Process Phase 2 enhanced update data + const processedUpdateData: any = { ...updateData }; + // Convert numeric strings to numbers - if (updateData.salary) updateData.salary = parseFloat(updateData.salary); - if (updateData.hourlyRate) updateData.hourlyRate = parseFloat(updateData.hourlyRate); + if (updateData.salary) processedUpdateData.salary = parseFloat(updateData.salary); + if (updateData.hourlyRate) processedUpdateData.hourlyRate = parseFloat(updateData.hourlyRate); + + // Convert date strings to Date objects + if (updateData.dateOfBirth) processedUpdateData.dateOfBirth = new Date(updateData.dateOfBirth); + if (updateData.terminationDate) processedUpdateData.terminationDate = new Date(updateData.terminationDate); // Update employee const updatedEmployee = await db.employee.update({ where: { id }, data: { - ...updateData, + ...processedUpdateData, updatedAt: new Date() }, include: { @@ -546,11 +671,23 @@ router.put('/:id', requireHRManager, [ id: true, name: true } + }, + user: { + select: { + id: true, + email: true, + role: true, + isActive: true + } } } }); - logger.info(`Employee updated: ${existingEmployee.employeeId} by ${req.user?.email} in organization ${req.organization.orgCode}`); + logger.info(`Employee updated: ${existingEmployee.employeeId} by ${req.user?.email} in organization ${req.organization.orgCode}`, { + organizationId: req.organization.id, + employeeId: existingEmployee.employeeId, + changes: Object.keys(updateData) + }); return res.status(200).json({ success: true, @@ -567,13 +704,17 @@ router.put('/:id', requireHRManager, [ }); // @route DELETE /api/v1/employees/:id -// @desc Delete employee (soft delete by setting isActive to false) +// @desc Delete employee (soft delete by setting isActive to false) - Phase 2 Smart Attendance System // @access Private (HR Manager+) router.delete('/:id', requireHRManager, [ param('id') .isString() .isLength({ min: 1 }) - .withMessage('Valid employee ID is required') + .withMessage('Valid employee ID is required'), + query('permanent') + .optional() + .isBoolean() + .withMessage('Permanent flag must be a boolean') ], auditDelete('Employee'), async (req: Request, res: Response): Promise => { try { // Check validation errors @@ -594,6 +735,7 @@ router.delete('/:id', requireHRManager, [ } const { id } = req.params; + const { permanent } = req.query; // Check if employee exists in this organization const employee = await db.employee.findFirst({ @@ -610,28 +752,270 @@ router.delete('/:id', requireHRManager, [ }); } - // Soft delete by setting isActive to false - const updatedEmployee = await db.employee.update({ - where: { id }, - data: { - isActive: false, - terminationDate: new Date(), - updatedAt: new Date() + if (permanent === 'true') { + // Permanent deletion (only if no attendance records) - Phase 2 feature + const attendanceCount = await db.attendanceRecord.count({ + where: { employeeId: id } + }); + + if (attendanceCount > 0) { + return res.status(400).json({ + success: false, + error: 'Cannot permanently delete employee with attendance records. Use soft delete instead.' + }); } + + await db.employee.delete({ where: { id } }); + + logger.info(`Employee permanently deleted: ${employee.employeeId} by ${req.user?.email} in organization ${req.organization.orgCode}`, { + organizationId: req.organization.id, + employeeId: employee.employeeId + }); + + return res.status(200).json({ + success: true, + message: 'Employee permanently deleted' + }); + } else { + // Soft delete by setting isActive to false + const updatedEmployee = await db.employee.update({ + where: { id }, + data: { + isActive: false, + terminationDate: new Date(), + updatedAt: new Date() + } + }); + + logger.info(`Employee deactivated: ${employee.employeeId} by ${req.user?.email} in organization ${req.organization.orgCode}`, { + organizationId: req.organization.id, + employeeId: employee.employeeId + }); + + return res.status(200).json({ + success: true, + message: 'Employee deactivated successfully', + data: { employee: updatedEmployee } + }); + } + } catch (error: any) { + logger.error('Failed to delete employee:', error); + return res.status(500).json({ + success: false, + error: 'Failed to delete employee' }); + } +}); + +// @route POST /api/v1/employees/bulk-import +// @desc Bulk import employees (Phase 2 Smart Attendance System) +// @access Private (HR Manager+) +router.post('/bulk-import', requireHRManager, [ + body('employees') + .isArray({ min: 1 }) + .withMessage('Employees array is required and must not be empty'), + body('employees.*.employeeId') + .trim() + .isLength({ min: 1, max: 20 }) + .withMessage('Each employee must have a valid employee ID'), + body('employees.*.firstName') + .trim() + .isLength({ min: 1, max: 50 }) + .withMessage('Each employee must have a valid first name'), + body('employees.*.lastName') + .trim() + .isLength({ min: 1, max: 50 }) + .withMessage('Each employee must have a valid last name') +], auditCreate('Employee'), async (req: Request, res: Response): Promise => { + try { + // Check validation errors + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + error: 'Validation failed', + details: errors.array() + }); + } - logger.info(`Employee deactivated: ${employee.employeeId} by ${req.user?.email} in organization ${req.organization.orgCode}`); + if (!req.organization) { + return res.status(400).json({ + success: false, + error: 'Organization context not found' + }); + } + + const { employees } = req.body; + + const results = { + successful: [], + failed: [] + }; + + for (const employeeData of employees) { + try { + // Check if employee ID already exists + const existing = await db.employee.findFirst({ + where: { + organizationId: req.organization.id, + employeeId: employeeData.employeeId + } + }); + + if (existing) { + results.failed.push({ + employeeId: employeeData.employeeId, + error: 'Employee ID already exists' + }); + continue; + } + + // Validate department if provided + if (employeeData.departmentId) { + const department = await db.department.findFirst({ + where: { + id: employeeData.departmentId, + organizationId: req.organization.id + } + }); + + if (!department) { + results.failed.push({ + employeeId: employeeData.employeeId, + error: 'Department not found' + }); + continue; + } + } + + const employee = await db.employee.create({ + data: { + organizationId: req.organization.id, + employeeId: employeeData.employeeId, + firstName: employeeData.firstName, + lastName: employeeData.lastName, + email: employeeData.email, + phone: employeeData.phone, + dateOfBirth: employeeData.dateOfBirth ? new Date(employeeData.dateOfBirth) : null, + position: employeeData.position, + departmentId: employeeData.departmentId, + employmentType: employeeData.employmentType || 'FULL_TIME', + hireDate: employeeData.hireDate ? new Date(employeeData.hireDate) : new Date(), + salary: employeeData.salary ? parseFloat(employeeData.salary) : null, + hourlyRate: employeeData.hourlyRate ? parseFloat(employeeData.hourlyRate) : null, + currency: employeeData.currency || 'USD', + pin: employeeData.pin, + isActive: true + } + }); + + results.successful.push(employee); + } catch (error) { + results.failed.push({ + employeeId: employeeData.employeeId, + error: 'Failed to create employee' + }); + } + } + + logger.info(`Bulk import completed by ${req.user?.email} in organization ${req.organization.orgCode}`, { + organizationId: req.organization.id, + successful: results.successful.length, + failed: results.failed.length + }); return res.status(200).json({ success: true, - message: 'Employee deactivated successfully', - data: { employee: updatedEmployee } + data: results, + message: `Import completed: ${results.successful.length} successful, ${results.failed.length} failed` }); } catch (error: any) { - logger.error('Failed to delete employee:', error); + logger.error('Error in bulk import:', error); return res.status(500).json({ success: false, - error: 'Failed to delete employee' + error: 'Failed to process bulk import' + }); + } +}); + +// @route GET /api/v1/employees/bulk-export +// @desc Export employees data (Phase 2 Smart Attendance System) +// @access Private (Manager+) +router.get('/bulk-export', requireManager, [ + query('format') + .optional() + .isIn(['json', 'csv']) + .withMessage('Format must be either json or csv') +], auditRead('Employee'), async (req: Request, res: Response): Promise => { + try { + // Check validation errors + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + error: 'Validation failed', + details: errors.array() + }); + } + + if (!req.organization) { + return res.status(400).json({ + success: false, + error: 'Organization context not found' + }); + } + + const { format = 'json' } = req.query; + + const employees = await db.employee.findMany({ + where: { organizationId: req.organization.id }, + include: { + department: { + select: { + name: true + } + } + }, + orderBy: { + employeeId: 'asc' + } + }); + + if (format === 'csv') { + // Simple CSV format for Phase 2 + const csvHeader = 'Employee ID,First Name,Last Name,Email,Phone,Position,Department,Employment Type,Hire Date,Salary,Hourly Rate,Is Active\n'; + const csvData = employees.map(emp => + `${emp.employeeId},"${emp.firstName}","${emp.lastName}","${emp.email || ''}","${emp.phone || ''}","${emp.position || ''}","${emp.department?.name || ''}",${emp.employmentType},"${emp.hireDate.toISOString().split('T')[0]}","${emp.salary || ''}","${emp.hourlyRate || '"}",${emp.isActive}` + ).join('\n'); + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename="employees-${req.organization.orgCode}-${new Date().toISOString().split('T')[0]}.csv"`); + + logger.info(`Employee data exported as CSV by ${req.user?.email} in organization ${req.organization.orgCode}`, { + organizationId: req.organization.id, + count: employees.length + }); + + return res.send(csvHeader + csvData); + } else { + logger.info(`Employee data exported as JSON by ${req.user?.email} in organization ${req.organization.orgCode}`, { + organizationId: req.organization.id, + count: employees.length + }); + + return res.status(200).json({ + success: true, + data: employees, + count: employees.length, + exportedAt: new Date().toISOString(), + organizationCode: req.organization.orgCode + }); + } + } catch (error: any) { + logger.error('Error exporting employees:', error); + return res.status(500).json({ + success: false, + error: 'Failed to export employees' }); } }); From f1b201866435e1544ac91c0c83b4d8e8a94a7553 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Jul 2025 05:42:02 +0000 Subject: [PATCH 3/3] Fix TypeScript compilation errors in merged employee.routes.ts Co-authored-by: W3JDev <174652026+W3JDev@users.noreply.github.com> --- apps/backend/src/routes/employee.routes.ts | 44 +++------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/apps/backend/src/routes/employee.routes.ts b/apps/backend/src/routes/employee.routes.ts index e4f6e08..e353b2e 100644 --- a/apps/backend/src/routes/employee.routes.ts +++ b/apps/backend/src/routes/employee.routes.ts @@ -12,41 +12,6 @@ const router = Router(); router.use(authenticateToken); router.use(tenantIsolation); -// Schema validation types for Phase 2 Smart Attendance System -interface CreateEmployeeRequest { - organizationId: string; - employeeId: string; - firstName: string; - lastName: string; - email?: string; - phone?: string; - dateOfBirth?: string; - position?: string; - departmentId?: string; - employmentType?: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT' | 'INTERN' | 'TEMPORARY'; - hireDate?: string; - salary?: number; - hourlyRate?: number; - currency?: string; - pin?: string; -} - -interface UpdateEmployeeRequest { - firstName?: string; - lastName?: string; - email?: string; - phone?: string; - dateOfBirth?: string; - position?: string; - departmentId?: string; - employmentType?: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT' | 'INTERN' | 'TEMPORARY'; - terminationDate?: string; - salary?: number; - hourlyRate?: number; - pin?: string; - isActive?: boolean; -} - // @route GET /api/v1/employees // @desc Get all employees in the organization (Phase 2 Smart Attendance System) // @access Private (Manager+) @@ -847,7 +812,10 @@ router.post('/bulk-import', requireHRManager, [ const { employees } = req.body; - const results = { + const results: { + successful: any[]; + failed: Array<{ employeeId: string; error: string }>; + } = { successful: [], failed: [] }; @@ -984,8 +952,8 @@ router.get('/bulk-export', requireManager, [ if (format === 'csv') { // Simple CSV format for Phase 2 const csvHeader = 'Employee ID,First Name,Last Name,Email,Phone,Position,Department,Employment Type,Hire Date,Salary,Hourly Rate,Is Active\n'; - const csvData = employees.map(emp => - `${emp.employeeId},"${emp.firstName}","${emp.lastName}","${emp.email || ''}","${emp.phone || ''}","${emp.position || ''}","${emp.department?.name || ''}",${emp.employmentType},"${emp.hireDate.toISOString().split('T')[0]}","${emp.salary || ''}","${emp.hourlyRate || '"}",${emp.isActive}` + const csvData = employees.map((emp: any) => + `${emp.employeeId},"${emp.firstName}","${emp.lastName}","${emp.email || ''}","${emp.phone || ''}","${emp.position || ''}","${emp.department?.name || ''}",${emp.employmentType},"${emp.hireDate.toISOString().split('T')[0]}","${emp.salary || ''}","${emp.hourlyRate || ''}",${emp.isActive}` ).join('\n'); res.setHeader('Content-Type', 'text/csv');