Skip to main content

HR API

Employees, org structure, attendance, and leave. Conventions (pagination, filter params, error envelope, permission model): conventions.md. Full field-level contracts: Swagger (README.md). For a guided full-stack trace of the employees feature, see ../learn/employees-walkthrough.md.

Employees — /api/employees

EmployeeController.java

MethodPathPermissionPurpose
GET/api/employees/meemployee.profile.viewOwn profile (self-service)
PATCH/api/employees/meemployee.profile.viewUpdate own contact details
GET/api/employeeshr.employees.viewPaged/filtered list (grid backend)
GET/api/employees/{id}hr.employees.viewOne employee
POST/api/employeeshr.employees.createCreate
PUT/api/employees/{id}hr.employees.editFull update
PATCH/api/employees/{id}/statushr.employees.deleteChange employment status
PATCH/api/employees/{id}/archivehr.employees.deleteSoft-delete
PATCH/api/employees/{id}/restorehr.employees.deleteUn-archive

Two module-wide behaviors:

  • Soft delete — "delete" is archival: archive flips the is_deleted column, every list/spec excludes archived rows by default, and ?archived=true returns only the archive. Nothing is physically removed; restore brings a row back.
  • Unique government IDs — SSS, PhilHealth, TIN, and Pag-IBIG numbers are unique across employees. Create/update pre-checks them and returns 409 (e.g. "SSS number is already assigned to another employee") instead of letting the DB constraint surface as a 500.

Deep-dive: POST /api/employees

Request (EmployeeCreateRequest — all listed fields required unless noted):

{
"lastName": "Reyes",
"firstName": "Ana",
"middleName": "L",
"nationality": "Filipino",
"birthday": "1994-03-12",
"dateHired": "2026-08-01",
"address": "123 Rizal St, Quezon City",
"phoneNumber": "0917-555-1234",
"sssNumber": "34-1234567-8",
"philhealthNumber": "12-345678901-2",
"tinNumber": "123-456-789-000",
"pagibigNumber": "1234-5678-9012",
"status": "PROBATIONARY",
"positionId": 5,
"basicSalary": 35000,
"riceSubsidy": 1500,
"phoneAllowance": 1000,
"clothingAllowance": 800,
"grossSemiMonthlyRate": 17500,
"hourlyRate": 208.33
}

Validation highlights: status must be PROBATIONARY | REGULAR | INACTIVE; all money fields must be ≥ 0; optional separationDate + separationReason (T|TR|R|D). middleName and nationality are optional. Pay is hours-basedhourlyRate is what the payroll engine actually multiplies; the salary fields feed contributions and reporting.

Responses: 200 with the created EmployeeDto (same fields plus generated employeeNumber and a nested position summary); 400 on validation failure; 409 on a duplicate government ID.

Deep-dive: GET /api/employees (paged + filtered)

The reference implementation of the grid filtering convention: Pageable params plus per-column filters — lastNameSearch/lastNameSearchType, firstNameSearch/...Type, status/statusFilterType, departmentId, positionId, employeeIdMin/Max/FilterType, positionSearch, departmentSearch, salaryMin/Max/salaryFilterType, hireDateFrom/To/FilterType, archived, and fields (column projection).

GET /api/employees?page=0&size=25&sort=lastName,asc&statusFilterType=equals&status=REGULAR&salaryMin=30000

Response: the standard PageResponseDto envelope of EmployeeDtos — or, when fields=lastName,firstName,basicSalary is present, of flat maps holding only those columns (the enterprise grid derives fields from its visible columns).

Departments — /api/departments and Positions — /api/positions

DepartmentController.java, PositionController.java — identical permission pattern:

MethodPathPermissionPurpose
GET/api/departments · /api/positionshr.employees.viewList all
GET.../searchhr.employees.viewPaged/filtered grid list
GET.../{id}hr.employees.viewOne record
POST...hr.employees.manageCreate
PUT.../{id}hr.employees.manageUpdate
DELETE.../{id}hr.employees.manageDelete
PATCH.../{id}/archive · .../{id}/restorehr.employees.manageSoft-delete / restore

Departments form a hierarchy — each department may have a parentDepartment — and positions belong to a department, which is how an employee (→ position → department) rolls up for org reporting.

Timesheets — /api/timesheets

TimesheetController.java

MethodPathPermissionPurpose
GET/api/timesheets/meemployee.timesheet.viewOwn entries
POST/api/timesheets/clock-inemployee.timesheet.viewClock in (today)
POST/api/timesheets/clock-outemployee.timesheet.viewClock out (today)
POST/api/timesheets/{id}/submitemployee.timesheet.viewSubmit own entry for approval
POST/api/timesheetshr.timesheets.manageCreate an entry for an employee
GET/api/timesheetshr.timesheets.viewPaged/filtered list
GET/api/timesheets/{id}hr.timesheets.viewOne entry
PUT/api/timesheets/{id}hr.timesheets.manageEdit an entry
PATCH/api/timesheets/{id}/statushr.timesheets.manageApprove / disapprove
DELETE/api/timesheets/{id}hr.timesheets.manageDelete

Status workflow: Not Submitted → (employee submits) → Submitted → (approver decides) → Approved | Disapproved. The state machine is enforced in the entity itself and violations return 422 (InvalidTimesheetStateException): clocking out twice, submitting an entry that isn't Not Submitted, future work dates, timeOut before timeIn. Submitting someone else's entry is 403 (UnauthorizedTimesheetActionException: "You can only submit your own timesheet entries").

Deep-dive: PATCH /api/timesheets/{id}/status (approval)

{ "status": "Approved" }

status accepts the display names "Approved" or "Disapproved" (any other value, including "Submitted", is rejected — 422 "Status must be 'Approved' or 'Disapproved'"). Only a timesheet currently in Submitted may be decided (otherwise 422 "Only 'Submitted' timesheets can be approved or disapproved"). The caller's own employee record is stamped as the approver on the entry. Response: 200 with the updated TimesheetDto.

Leave — /api/leave-requests, /api/leave-balances, /api/leave-types

LeaveRequestController.java, LeaveBalanceController.java, LeaveTypeController.java

MethodPathPermissionPurpose
GET/api/leave-requests/meemployee.leave.history.viewOwn request history
POST/api/leave-requestsemployee.leave.requestFile a request (leaveTypeId, startDate, endDate, optional description)
DELETE/api/leave-requests/{id}employee.leave.requestCancel own pending request
GET/api/leave-requestshr.leave.requests.viewPaged/filtered list
GET/api/leave-requests/{id}hr.leave.requests.viewOne request
POST/api/leave-requests/{id}/approvehr.leave.requests.approveApprove
POST/api/leave-requests/{id}/rejecthr.leave.requests.rejectReject
GET/api/leave-balances/meemployee.leave.balance.viewOwn balances per leave type
GET/api/leave-typesemployee.leave.request or hr.leave.requests.viewLeave-type lookup

Deep-dive: POST /api/leave-requests/{id}/approve

No request body. Only a Pending request can be decided (otherwise 400 "Only pending leave requests can be approved"). Approval computes the chargeable days of the range, checks the employee's balance for that leave type, and decrements the balance atomically with the approval — if the balance is short, the whole call fails with 400 "Employee has insufficient leave balance for <type>" and nothing changes. On success the request becomes Approved, is marked paid, and records the caller as approver with the decision timestamp; response is the updated LeaveRequestDto. /reject mirrors this without touching the balance.

Overtime — /api/overtime-requests

OvertimeRequestController.java

MethodPathPermissionPurpose
GET/api/overtime-requests/meemployee.overtime.requestOwn requests
POST/api/overtime-requestsemployee.overtime.requestFile a request
PUT/api/overtime-requests/{id}employee.overtime.requestEdit own pending request
DELETE/api/overtime-requests/{id}employee.overtime.requestCancel own request
GET/api/overtime-requestshr.overtime.requests.view or payroll.overtime.approvePaged/filtered list
GET/api/overtime-requests/{id}hr.overtime.requests.view or payroll.overtime.approveOne request
PATCH/api/overtime-requests/{id}/decisionhr.overtime.requests.approve or payroll.overtime.approveApprove / reject

Approved overtime is what the payroll engine pays out at the overtime multiplier (payroll.md).

Work suspensions — /api/work-suspensions

WorkSuspensionController.java — company-declared no-work days (typhoons etc.) that the payroll engine treats like calendar events alongside holidays.

MethodPathPermissionPurpose
GET/api/work-suspensionspayroll.holidays.manage or payroll.manageList
POST/api/work-suspensionspayroll.holidays.manageDeclare a suspension date
DELETE/api/work-suspensions/{id}payroll.holidays.manageRemove