A complete guide — from an employee requesting a day off, to a manager approving it, to the balance being updated automatically.
Imagine you work at a company and you want to take a week off. Here is what happens, from start to finish:
The leave journey: employee submits → system validates → manager decides → balance updates automatically.
Not all leave is the same. HRSanad supports many different leave types. Each one has its own rules — how many days you get, whether you need to submit a document, whether you get paid during it, and whether the company allows you to take more than your balance.
| Setting | What it means | Example |
|---|---|---|
unit | Is the leave counted in days or hours? | Annual = days, Short break = hours |
is_paid | Does the employee get full salary during this leave? | Annual Leave = paid; Unpaid Leave = not paid |
requires_approval | Does someone need to approve it, or is it automatic? | Annual = must be approved; Emergency = sometimes auto-approved |
requires_document | Must the employee upload a supporting document? | Sick leave = medical certificate required |
min_notice_days | How many days in advance must the request be submitted? | Annual Leave = 7 days notice |
max_consecutive_days | Maximum days allowed in a single stretch without special approval | 30 days; beyond that needs HR Director approval |
accrual_method | How the balance builds up over time | monthly = 2.5 days credited each month; annual = all 30 days credited on Jan 1 |
carry_forward_max | How many unused days the employee can keep into next year | 15 days max carry forward |
negative_balance | Can the employee take leave even if their balance is zero? | true = allowed (balance goes negative); false = hard block |
gender_restricted | Applies to a specific gender only | Maternity leave = female only; Paternity leave = male only |
Your leave balance is not just one number. It is calculated from five parts that add up together:
For leave types with accrual_method = "monthly", HRSanad credits a portion of your annual entitlement on the last day of every month. A cron job runs automatically at 20:05 UTC each day and credits the right amount to every active employee.
Monthly credit = 30 ÷ 12 = 2.5 days per month
This means by mid-year (after 6 months) the employee has accrued 15 days — even if they haven't taken any leave yet.
Running balance over the year. The red May column shows where 7.5 days were taken, resetting the available balance back to 2.5 days after that month's accrual.
Employees who have worked longer may get more leave. HRSanad supports entitlement bands by years of service:
| Years of Service | Days Entitled / Year | Monthly Credit |
|---|---|---|
| 0 – 2 years | 21 days | 1.75 days |
| 2 – 5 years | 25 days | 2.08 days |
| 5+ years | 30 days | 2.5 days |
Employees apply through the ESS Portal (Employee Self-Service). This is a separate, secure web portal where each employee can only see their own data.
The employee visits the ESS portal and logs in using their work email address or their mobile number (the system detects which format was entered). After successful login they see their personal dashboard.
On the Leave section, the employee sees their current balance for each leave type — how many days they have available right now. This prevents them submitting a request they cannot take.
API: GET /v1/ess/leave-balance
Before saving the request, the system automatically verifies:
negative_balance = false and balance is insufficient → blocked with an error)If any check fails, the employee sees a clear error and the request is not saved.
If all checks pass, the request is saved with status submitted. The balance is not deducted yet — it is only deducted when the manager approves it. This is important: a submitted request is a pending request, not a confirmed leave.
API: POST /v1/ess/leave-requests
From the ESS portal, the employee can see all their requests and their current statuses. They can also cancel a request (while it's still submitted or even after it's approved, before the leave starts).
API: GET /v1/ess/leave-requests
Once a request is submitted, the approval workflow begins. The manager or HR team is responsible for reviewing and making a decision.
Full access — can approve, reject, cancel, or adjust any employee's leave. Can also override business rules.
Can approve or reject requests for employees who report to them directly.
Can approve or reject leave requests. Similar to HR Admin but without payroll access.
Full system access including all leave operations. Used for setup and emergency corrections.
Every leave request moves through a series of statuses. The diagram below shows every possible path:
Every possible path a leave request can take. Solid lines = normal flow. Dashed lines = exception paths.
| From → To | Who can do it | What happens automatically |
|---|---|---|
| draft / new → submitted | Employee (via ESS) | System validates dates, overlap, and balance. Request saved. Balance NOT deducted yet. |
| submitted → approved | HR Admin, HR Officer, Line Manager, Sysadmin | Days deducted from balance immediately. taken += days_requested. approved_by and approved_at recorded. |
| submitted → rejected | HR Admin, HR Officer, Line Manager, Sysadmin | Rejection reason stored. No balance change (balance was never deducted on submit). Employee notified. |
| submitted → cancelled | Employee, HR Admin | Request cancelled before approval. No balance change (was never deducted). |
| approved → cancelled | Employee, HR Admin | Days restored to balance. taken -= days_requested. Useful if plans change before the leave starts. |
| approved → resumed | HR Admin (on behalf of employee) | Employee returned early. Unused days calculated and credited back. taken -= days_resumed. actual_return_date recorded. |
Life happens. Sometimes an employee is approved for 10 days of leave but comes back after 6. HRSanad handles this cleanly with the Resume action.
days_actually_taken = calendar_days(start_date → actual_return_date) - 1
days_resumed = max(0, days_requested - days_actually_taken)
new_taken_balance = old_taken - days_resumed
The system enforces these rules automatically — HR does not need to check them manually.
You cannot have two leave requests that cover the same dates (unless the earlier one is already rejected or cancelled). The system checks:
existing leave with status NOT IN
('rejected', 'cancelled')
AND dates overlap with new request
→ BLOCKED (error returned)
If a leave type has negative_balance = false, the request is blocked when balance would go below zero. If negative_balance = true, the employee can take more than they have (the balance goes negative — an advance on future accrual).
Leave days are counted as calendar days (including weekends). If you apply for Monday–Friday (5 days), the system counts 5 days, not 5 working days. Holiday exclusion is handled at the entitlement setup level.
days = floor((end_date - start_date)
/ 86400000) + 1
If a leave type requires advance notice (e.g., annual leave requires 7 days notice), the system rejects requests submitted too close to the start date. Sick leave and emergency leave typically have zero notice requirement.
allow_negative_balance = false for the leave type, insufficient balance is a hard error. The request is rejected immediately and not saved. The employee must wait for more days to accrue.
One approval triggers two automatic updates: attendance is marked and balance is reduced.
When a leave request is approved, the TNA (Time & Attendance) module marks those dates as on_leave — not as absent. This matters because:
When payroll is calculated for the month:
on_leave markersis_paid = true): the employee gets full salary for those daysis_paid = false): those days are treated like absent days — salary is pro-ratedHR managers have a separate admin console that shows all leave requests across the company. They can filter, search, approve, and reject from one screen.
| Column | Meaning |
|---|---|
| Employee | Name and employee code |
| Leave Type | Annual, Sick, Emergency, etc. |
| From / To | Requested dates |
| Days | Number of calendar days requested |
| Reason | Employee's note |
| Status | Colour-coded badge — yellow = submitted, green = approved, red = rejected, grey = cancelled, blue = resumed |
| Actions | Approve / Reject buttons (only visible for "submitted" requests) |
HR Admin can create, edit, and deactivate leave types from the Leave Types page. Key actions:
At the end of each calendar year, unused leave days need to be resolved. HRSanad supports two mechanisms:
Unused days are moved to the next year's opening balance — up to the carry_forward_max limit set on the leave type.
Example: Employee has 8 days unused. Leave type allows carry forward of max 5 days → only 5 days move to next year. The remaining 3 days are forfeited.
Some companies allow employees to be paid cash for unused leave days instead of carrying them forward (or at final settlement).
When days are encashed, the balance records encashed += days, reducing the closing balance. The encashed amount is typically calculated as: basic_salary / 30 × days_encashed
carry_forward_expiry_months is set to 3, any carried-forward days that are not taken within 3 months of the new year are automatically forfeited. This encourages employees to use their leave rather than hoard it.
Scenario: Sara is an active employee. She joined 3 years ago. It's August 2026. She wants 5 days annual leave from 15–19 September.
Sara logs into ESS. Her annual leave balance shows: 12.5 days available (8 months × 2.5 days accrued, minus 7.5 days she already took in March).
She selects: Leave Type = Annual, From = 15 Sep, To = 19 Sep, Reason = "Family holiday". She clicks Submit.
The system checks: No overlap with existing leaves ✓ | Balance 12.5 ≥ 5 ✓ | 28 days advance notice given ✓
Request saved: status = submitted. Sara's available balance still shows 12.5 (not yet deducted).
Sara's line manager logs into the admin console. He sees Sara's request in yellow. He checks the team calendar — no other team member is off those dates. He clicks Approve.
The system deducts 5 days: taken += 5. Sara's balance drops from 12.5 to 7.5 days. Status changes to approved. Sara gets a notification.
On 15–19 September, the TNA system marks Sara as on_leave (not absent) for all 5 days. She does not need to clock in.
When payroll is calculated, the engine sees Sara had 5 days on_leave (paid). Annual leave is is_paid = true, so those 5 days count as full working days — no salary deduction. Sara receives her full September salary.
At end of September, the cron job credits another 2.5 days. Sara's balance is now 10.0 days again.
| Action | Method | Endpoint |
|---|---|---|
| Check leave balance | GET | /v1/ess/leave-balance |
| List active leave types | GET | /v1/ess/leave-types |
| View my requests | GET | /v1/ess/leave-requests |
| Submit a new request | POST | /v1/ess/leave-requests |
| Cancel my request | POST | /v1/ess/leave-requests/:id/cancel |
| Action | Method | Endpoint |
|---|---|---|
| List all requests (with filters) | GET | /v1/leave/requests |
| Get a single request | GET | /v1/leave/requests/:id |
| Approve or Reject request | POST | /v1/leave/requests/:id/action |
| Cancel any request | POST | /v1/leave/requests/:id/cancel |
| Resume (early return) | POST | /v1/leave/requests/:id/resume |
| List leave types | GET | /v1/leave/types |
| Create leave type | POST | /v1/leave/types |
| Update leave type | PATCH | /v1/leave/types/:id |
| List entitlements | GET | /v1/leave/entitlements |
| Create entitlement | POST | /v1/leave/entitlements |
| View balances | GET | /v1/leave/balances |
| Manual balance adjustment | POST | /v1/leave/balances/adjust |
| Trigger monthly accrual | POST | /v1/leave/accrue |
hr_admin, hr_officer, sysadmin. Line managers additionally have access to action (approve/reject) requests for their direct reports.
| Status | Meaning | Balance Impact | Who Can Act Next |
|---|---|---|---|
| submitted | Waiting for a decision. Leave not yet confirmed. | None — balance unchanged | HR/Manager: Approve or Reject Employee: Cancel |
| approved | Confirmed. Employee is authorised to take this leave. | Deducted: taken += days | HR/Employee: Cancel HR: Resume (if returns early) |
| rejected | Denied. Employee cannot take this leave. | None — balance unchanged | Terminal state. Employee must submit a new request. |
| cancelled | Withdrawn — either by employee or HR. | If was approved: Restored taken −= days If was submitted: no change |
Terminal state. |
| resumed | Employee came back before the end date. | Partially restored: taken −= unused_days | Terminal state. |