HRM — Org Structure & Employees
Departments, job titles, and the employee record that everything else hangs off
Every other HR feature — attendance, shifts, leave, payroll, self-service — points back to an Employee record. Set this up first. The recommended order is: Departments → Designations → Employees.
What This Module Does (Plain Language)
This is your staff directory. It tracks everyone who works at the business — not only the people who log into the POS. A delivery driver who never touches a screen, a cleaner, and a cashier are all "employees" here. The module stores their personal details, job title, department, reporting manager, employment dates, uploaded documents (visas, certificates), and — for authorized HR staff only — sensitive data like national ID and bank details (stored encrypted).
Three Kinds of People
A login account (User) and an employment record (Employee) are deliberately
separate things. That separation is what lets POSVelo handle real-world
staffing cleanly.
| Persona | Has a login (User)? | Has an Employee record? | Example |
|---|---|---|---|
| System-only | Yes | No | SUPER_ADMIN / platform operator — never on payroll |
| Worker-only | No | Yes | Cleaner or driver who is paid & scheduled but never logs in |
| Full staff | Yes | Yes | Cashier, manager, HR manager — logs in AND is on payroll |
You can suspend someone from working without deleting their login, or give a worker-only employee a login later. When creating an employee you may optionally provision a login at the same time (see below).
Departments & Designations
| Entity | What it is | Key fields |
|---|---|---|
| Department | An organizational unit. Can nest (a parent department). | name, parentDepartmentId, headEmployeeId |
| Designation | A job title / role. Carries a seniority <code>level</code> used for leave-policy defaults. | name, level |
Both support soft delete (archive) and restore, and both are blocked
from deletion while employees still reference them — you'll get a 409 Conflict
telling you how many employees are attached.
In the app
Departments
Go to HR › Departments (/hr/departments).
Add units like "Sales Floor", "Kitchen", "Back Office".
Designations
Go to HR › Designations (/hr/designations).
Add titles like "Cashier", "Shift Lead", "Store Manager" and set a
seniority level.
The Employee Record
| Field group | Fields | Notes |
|---|---|---|
| Identity | employeeCode (unique per business), firstName, lastName, dateOfBirth, gender | employeeCode disambiguates two people with the same name. |
| Contact | email, phone, emergencyContact | emergencyContact is a small object: name, relationship, phone. |
| Placement | storeId, departmentId, designationId, reportsToId | reportsToId is the person's manager — drives team-scoped approvals. |
| Employment | employmentType, employmentStatus, employmentStartDate | See the enums below. |
| Login link | userId (nullable) | Set when the employee also has a login account. |
| Sensitive (encrypted) | nationalId, passportNumber, taxId, bankDetails | Hidden unless the caller has the sensitive read permission. |
Employment type & status
| Enum | Allowed values |
|---|---|
employmentType | FULL_TIME · PART_TIME · CONTRACT · INTERN |
employmentStatus | ACTIVE · PROBATION · ON_LEAVE · SUSPENDED · INACTIVE · TERMINATED · RESIGNED · RETIRED · DECEASED · CONTRACT_ENDED |
Attendance, shifts, and new leave requests are blocked for employees whose
status is TERMINATED, RESIGNED, or
SUSPENDED. Rosters can only be built for ACTIVE and
PROBATION staff. This is enforced server-side across modules.
Creating an Employee
In the app
Go to HR › Employees › New (/hr/employees/new),
fill in identity, placement, and employment details, then save. The list at
/hr/employees supports search, filters, and pagination; each row
opens a detail page at /hr/employees/[id].
Via the API
Dev Only Requires permission hr.employees.create.
Add a createUser object ({ email, password, role, storeId })
to provision a login user atomically. This needs the users.create
permission, and the role is limited to MANAGER, HR_MANAGER, CASHIER,
or EMPLOYEE. A SUPER_ADMIN can never be an employee.
Sensitive Data & Salary — Handled Specially
Two rules protect personal and pay data:
| Rule | How it works |
|---|---|
| Sensitive fields are filtered, not refused | If you read an employee without the sensitive-read permission, you get a normal 200 response with the sensitive fields simply omitted — you can see the person exists, just not their NID/bank. |
| Salary is edited via a dedicated endpoint | Pay is never edited inline. Updating salary creates a new effective-dated EmployeeSalary row; the old one is closed off (it gets an effectiveTo and is marked superseded). This preserves a full salary history — see Payroll. |
The detail view exposes these as separate cards/endpoints:
| Action | Endpoint | Permission |
|---|---|---|
| Update basic fields | PATCH /api/v2/hr/employees/:id | hr.employees.update |
| Update sensitive PII | PATCH /api/v2/hr/employees/:id/sensitive | hr.employees.update.sensitive |
| Update salary | PATCH /api/v2/hr/employees/:id/salary | hr.employees.update.salary |
Contracts & Documents
| What | Endpoints | Notes |
|---|---|---|
| Employment contracts | GET / POST /api/v2/hr/employees/:id/contracts | A history of employment periods (useful for rehires and fixed-term staff). |
| Documents | GET / POST / DELETE /api/v2/hr/employees/:id/documents[/:docId] | Uploaded files like passports or certificates; a document can carry an expiry date. |
Termination (Separation)
Ending employment is a workflow, not a delete. The record is never destroyed — history is preserved for audit and compliance.
Dev Only POST /api/v2/hr/employees/:id/terminate · permission hr.employees.terminate
When you terminate, the system:
Sets the separation outcome
Status becomes one of RESIGNED, TERMINATED,
RETIRED, DECEASED, or CONTRACT_ENDED,
and records employmentEndDate + a reason.
Optionally deactivates the login
If you set deactivateUser: true, the linked login is
soft-deactivated (the User row is kept for audit).
Cleans up downstream
All pending leave requests are auto-cancelled, future shift schedules are cancelled, and a final settlement can be flagged for the next payroll run.
DELETE /api/v2/hr/employees/:id is a soft delete
(archive) — reversible via PATCH .../:id/restore. Termination is
the proper way to end an employment relationship while keeping the person in
history. Neither removes data.
Who Can Do What
| Capability | ADMIN | HR_MANAGER | MANAGER | ACCOUNTANT | EMPLOYEE |
|---|---|---|---|---|---|
| Read employees (basic) | Yes | Yes | Team only | Limited | Own (via ESS) |
| Read sensitive (NID/bank) | Yes | Yes | No | No | No |
| Read salary | Yes | Yes | No | Yes | Own (via ESS) |
| Create / update / terminate | Yes | Yes | No | No | No |
| Manage departments & designations | Yes | Yes | No | No | No |
"Team" for a MANAGER means employees whose reportsToId points at that
manager's own employee record. The backend always enforces these rules; the UI
just hides controls you can't use.
Common Gotchas
| Situation | What happens |
|---|---|
| Duplicate employeeCode | 409 Conflict — codes are unique per business. |
| Manager loop (A reports to B reports to A) | Rejected with a validation error. |
| employmentEndDate before start date | Validation error. |
| Editing a TERMINATED/RESIGNED employee | Blocked except for separation fields. |
| Deleting a department with staff in it | 409 Conflict with the count of attached employees. |
| Rehiring someone | Create a fresh employment cycle (new contract & start date); old data is retained; leave balances start fresh by default. |
With employees in place, set up Attendance & Shifts so staff can clock in and be rostered.