feat(hr): Complete HR module with Employee Portal, Loans, Leave, Purchase Requests, Contracts

- Database: Add Loan, LoanInstallment, PurchaseRequest, LeaveEntitlement, EmployeeContract models
- Database: Extend Attendance with ZK Tico fields (sourceDeviceId, externalId, rawData)
- Database: Add Employee.attendancePin for device mapping
- Backend: HR admin - Loans, Purchase Requests, Leave entitlements, Employee contracts CRUD
- Backend: Leave reject, bulk attendance sync (ZK Tico ready)
- Backend: Employee Portal API - scoped by employeeId (loans, leaves, purchase-requests, attendance, salaries)
- Frontend: Employee Portal - dashboard, loans, leave, purchase-requests, attendance, salaries
- Frontend: HR Admin - new tabs for Leaves, Loans, Purchase Requests, Contracts (approve/reject)
- Dashboard: Add My Portal link
- No destructive schema changes; additive migrations only

Made-with: Cursor
This commit is contained in:
Talal Sharabi
2026-03-04 19:44:09 +04:00
parent ae890ca1c5
commit 72ed9a2ff5
18 changed files with 2649 additions and 8 deletions

View File

@@ -1,12 +1,24 @@
import { Router } from 'express';
import { body, param } from 'express-validator';
import { hrController } from './hr.controller';
import { portalController } from './portal.controller';
import { authenticate, authorize } from '../../shared/middleware/auth';
import { validate } from '../../shared/middleware/validation';
const router = Router();
router.use(authenticate);
// ========== EMPLOYEE PORTAL (authenticate only, scoped by employeeId) ==========
router.get('/portal/me', portalController.getMe);
router.get('/portal/loans', portalController.getMyLoans);
router.post('/portal/loans', portalController.submitLoanRequest);
router.get('/portal/leave-balance', portalController.getMyLeaveBalance);
router.get('/portal/leaves', portalController.getMyLeaves);
router.post('/portal/leaves', portalController.submitLeaveRequest);
router.get('/portal/purchase-requests', portalController.getMyPurchaseRequests);
router.post('/portal/purchase-requests', portalController.submitPurchaseRequest);
router.get('/portal/attendance', portalController.getMyAttendance);
router.get('/portal/salaries', portalController.getMySalaries);
// ========== EMPLOYEES ==========
router.get('/employees', authorize('hr', 'employees', 'read'), hrController.findAllEmployees);
@@ -19,11 +31,14 @@ router.post('/employees/:id/terminate', authorize('hr', 'employees', 'terminate'
router.post('/attendance', authorize('hr', 'attendance', 'create'), hrController.recordAttendance);
router.get('/attendance/:employeeId', authorize('hr', 'attendance', 'read'), hrController.getAttendance);
router.post('/attendance/sync', authorize('hr', 'attendance', 'create'), hrController.bulkSyncAttendance);
// ========== LEAVES ==========
router.get('/leaves', authorize('hr', 'leaves', 'read'), hrController.findAllLeaves);
router.post('/leaves', authorize('hr', 'leaves', 'create'), hrController.createLeaveRequest);
router.post('/leaves/:id/approve', authorize('hr', 'leaves', 'approve'), hrController.approveLeave);
router.post('/leaves/:id/reject', authorize('hr', 'leaves', 'approve'), hrController.rejectLeave);
// ========== SALARIES ==========
@@ -41,5 +56,35 @@ router.delete('/departments/:id', authorize('hr', 'all', 'delete'), hrController
router.get('/positions', authorize('hr', 'all', 'read'), hrController.findAllPositions);
// ========== LOANS ==========
router.get('/loans', authorize('hr', 'all', 'read'), hrController.findAllLoans);
router.get('/loans/:id', authorize('hr', 'all', 'read'), hrController.findLoanById);
router.post('/loans', authorize('hr', 'all', 'create'), hrController.createLoan);
router.post('/loans/:id/approve', authorize('hr', 'all', 'approve'), hrController.approveLoan);
router.post('/loans/:id/reject', authorize('hr', 'all', 'approve'), hrController.rejectLoan);
router.post('/loans/:id/pay-installment', authorize('hr', 'all', 'update'), hrController.recordLoanInstallmentPayment);
// ========== PURCHASE REQUESTS ==========
router.get('/purchase-requests', authorize('hr', 'all', 'read'), hrController.findAllPurchaseRequests);
router.get('/purchase-requests/:id', authorize('hr', 'all', 'read'), hrController.findPurchaseRequestById);
router.post('/purchase-requests', authorize('hr', 'all', 'create'), hrController.createPurchaseRequest);
router.post('/purchase-requests/:id/approve', authorize('hr', 'all', 'approve'), hrController.approvePurchaseRequest);
router.post('/purchase-requests/:id/reject', authorize('hr', 'all', 'approve'), hrController.rejectPurchaseRequest);
// ========== LEAVE ENTITLEMENTS ==========
router.get('/leave-balance/:employeeId', authorize('hr', 'all', 'read'), hrController.getLeaveBalance);
router.get('/leave-entitlements', authorize('hr', 'all', 'read'), hrController.findAllLeaveEntitlements);
router.post('/leave-entitlements', authorize('hr', 'all', 'create'), hrController.upsertLeaveEntitlement);
// ========== EMPLOYEE CONTRACTS ==========
router.get('/contracts', authorize('hr', 'all', 'read'), hrController.findAllEmployeeContracts);
router.get('/contracts/:id', authorize('hr', 'all', 'read'), hrController.findEmployeeContractById);
router.post('/contracts', authorize('hr', 'all', 'create'), hrController.createEmployeeContract);
router.put('/contracts/:id', authorize('hr', 'all', 'update'), hrController.updateEmployeeContract);
export default router;