# Leave Management API Documentation

> **Base URL:** `{{base_url}}/api/v1/employee`
> **Authentication:** All endpoints require `Authorization: Bearer {token}` (Laravel Sanctum)
> **Content-Type:** `application/json`

---

## Table of Contents

1. [Get Leave Types](#1-get-leave-types)
2. [Submit Leave Request (Full Day)](#2-submit-leave-request-full-day)
3. [Submit Leave Request (Half Day)](#3-submit-leave-request-half-day)
4. [Get Leave Allocation & History](#4-get-leave-allocation--history)
5. [Check Leave Eligibility](#5-check-leave-eligibility)
6. [Admin — Approve Leave](#6-admin--approve-leave)
7. [Admin — Reject Leave](#7-admin--reject-leave)
8. [Admin — Update Leave Status](#8-admin--update-leave-status)
9. [Admin — Delete Leave Request](#9-admin--delete-leave-request)
10. [Data Models Reference](#10-data-models-reference)
11. [Status & Error Codes](#11-status--error-codes)

---

## Route Summary

### Employee Mobile API (`/api/v1/employee/` — requires Sanctum token)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/leave/types` | Get active leave types with balance |
| `POST` | `/leave/request` | Submit a new leave request |
| `GET` | `/leave-allocation` | Get allocation, balance & history |
| `POST` | `/check-leave-eligibility` | Check dates eligibility by risk level |

### Admin Web Panel (`/leave/` — web session or token)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/leave/{id}/approve` | Approve a pending leave request |
| `POST` | `/leave/{id}/reject` | Reject a pending leave request |
| `POST` | `/leave/{id}/update-status` | Flexible status update (any → any) |
| `DELETE` | `/leave/{id}` | Delete pending/rejected request |

---

## 1. Get Leave Types

Returns all active leave types with the authenticated employee's current year balance and `.NET` payroll work type IDs.

```
GET /api/v1/employee/leave/types
```

### Headers

| Key | Value |
|-----|-------|
| `Authorization` | `Bearer {token}` |
| `Accept` | `application/json` |

### No request body or query parameters required.

### Response `200 OK`

```json
{
  "success": true,
  "message": "Leave types retrieved successfully",
  "data": {
    "leave_types": [
      {
        "id": 1,
        "name": "Annual Leave",
        "description": "Yearly entitlement for personal use",
        "max_days_per_year": 14,
        "requires_approval": true,
        "is_paid": true,
        "dotnet_work_type_id": 8,
        "balance": {
          "total_days": 14,
          "used_days": 3,
          "remaining_days": 11
        }
      },
      {
        "id": 2,
        "name": "Medical Leave",
        "description": "Sick or medical leave",
        "max_days_per_year": 14,
        "requires_approval": true,
        "is_paid": true,
        "dotnet_work_type_id": 9,
        "balance": {
          "total_days": 14,
          "used_days": 0,
          "remaining_days": 14
        }
      },
      {
        "id": 3,
        "name": "Unpaid Leave",
        "description": "Leave without pay",
        "max_days_per_year": 0,
        "requires_approval": true,
        "is_paid": false,
        "dotnet_work_type_id": 6,
        "balance": {
          "total_days": 0,
          "used_days": 0,
          "remaining_days": 0
        }
      }
    ],
    "dotnet_work_types": [
      { "id": 1,  "name": "General Working" },
      { "id": 2,  "name": "Off Day" },
      { "id": 6,  "name": "Unpaid Leave" },
      { "id": 7,  "name": "Absent" },
      { "id": 8,  "name": "Annual Leave" },
      { "id": 9,  "name": "Medical Leave" },
      { "id": 10, "name": "Maternity Leave" },
      { "id": 11, "name": "Paternity Leave" },
      { "id": 12, "name": "Hospitalization Leave" },
      { "id": 15, "name": "Replacement Leave" },
      { "id": 16, "name": "Compensate Leave" },
      { "id": 17, "name": "Marriage Leave" }
    ],
    "year": "2026",
    "min_leave_date": "2026-10-13",
    "advance_days": 6
  }
}
```

> **`min_leave_date`** = today + 6 days. Enforce this as the earliest selectable date in your date picker.
> **`dotnet_work_type_id`** may be `null` if no matching payroll type is found.
> If the `.NET` API is unreachable, a static fallback map of 17 work types is used automatically.

---

## 2. Submit Leave Request (Full Day)

```
POST /api/v1/employee/leave/request
```

### Headers

| Key | Value |
|-----|-------|
| `Authorization` | `Bearer {token}` |
| `Content-Type` | `application/json` |
| `Accept` | `application/json` |

### Request Body

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `leave_type_id` | integer | ✅ | Must exist in `leave_types` table |
| `from_date` | `YYYY-MM-DD` | ✅ | Must be ≥ today + 6 days |
| `to_date` | `YYYY-MM-DD` | ✅ | Must be ≥ `from_date` |
| `is_half_day` | boolean | ❌ | Default: `false` |
| `half_day_type` | string | Required if `is_half_day=true` | `first_half` \| `second_half` |
| `reason` | string | ✅ | Max 500 characters |
| `remarks` | string | ❌ | Optional additional notes |

### Example Request

```json
{
  "leave_type_id": 1,
  "from_date": "2026-10-20",
  "to_date": "2026-10-22",
  "is_half_day": false,
  "reason": "Family holiday trip",
  "remarks": "Pre-planned family event"
}
```

### Response `200 OK` — Pending Approval

```json
{
  "success": true,
  "message": "Leave request submitted successfully. Pending approval.",
  "data": {
    "leave_request_id": 42,
    "leave_type": "Annual Leave",
    "from_date": "2026-10-20",
    "to_date": "2026-10-22",
    "days": 3,
    "is_half_day": false,
    "half_day_type": null,
    "half_day_display": "Full Day",
    "status": "pending",
    "requires_approval": true
  }
}
```

> **Note:** All leave requests are always created with `status: "pending"`, regardless of the `requires_approval` setting on the leave type. An admin must explicitly approve or reject every request.

### Response `422` — Validation Failed

```json
{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "from_date": [
      "Leave must be applied at least 5 days in advance. Today, tomorrow, and the next 4 days are not eligible. The earliest date you can apply for is 14 Oct 2026."
    ],
    "reason": ["The reason field is required."]
  }
}
```

### Response `400` — Pending Request Already Exists

```json
{
  "success": false,
  "message": "You have a pending leave request (Annual Leave, 20 Oct 2026 – 22 Oct 2026). Please wait for it to be approved or rejected before applying for new leave.",
  "pending_request": {
    "id": 41,
    "leave_type": "Annual Leave",
    "from_date": "20 Oct 2026",
    "to_date": "22 Oct 2026",
    "days": 3,
    "applied_on": "07 Oct 2026"
  }
}
```

### Response `400` — Overlapping Dates

```json
{
  "success": false,
  "message": "You already have a leave request for this period"
}
```

### Response `400` — Insufficient Balance

```json
{
  "success": false,
  "message": "Insufficient leave balance. Available: 2 days"
}
```

### Response `400` — Inactive Leave Type

```json
{
  "success": false,
  "message": "This leave type is not active"
}
```

### Business Rules (enforced in this order)

1. `from_date` must be **at least 6 days ahead** from today.
2. Leave type must be active (`is_active = true`).
3. Employee must have **no existing pending request** — one at a time policy.
4. No **overlapping** dates with any existing `pending` or `approved` leave.
5. If `max_days_per_year > 0`, checks `remaining_days >= requested_days`.
6. If `requires_approval = false`, leave is **auto-approved** and balance is deducted immediately.

### Day Count Formula

| Scenario | Count |
|----------|-------|
| Full day (single or multi-day) | `diffInDays(from_date, to_date) + 1` |
| Half day | `0.5` |

---

## 3. Submit Leave Request (Half Day)

Same endpoint as above — `POST /api/v1/employee/leave/request` — with `is_half_day: true`.

> For half-day: `from_date` and `to_date` should be the **same date**.

### Example Request

```json
{
  "leave_type_id": 2,
  "from_date": "2026-10-20",
  "to_date": "2026-10-20",
  "is_half_day": true,
  "half_day_type": "first_half",
  "reason": "Medical appointment in the morning"
}
```

### Response `200 OK`

```json
{
  "success": true,
  "message": "Leave request submitted successfully. Pending approval.",
  "data": {
    "leave_request_id": 44,
    "leave_type": "Medical Leave",
    "from_date": "2026-10-20",
    "to_date": "2026-10-20",
    "days": 0.5,
    "is_half_day": true,
    "half_day_type": "first_half",
    "half_day_display": "First Half (Morning)",
    "status": "pending",
    "requires_approval": true
  }
}
```

### `half_day_type` Values

| Value | Display |
|-------|---------|
| `first_half` | First Half (Morning) |
| `second_half` | Second Half (Afternoon) |

---

## 4. Get Leave Allocation & History

Returns the employee's risk-based leave allocation, current balance per type, applicable restrictions, a summary total, and their last 10 leave requests.

```
GET /api/v1/employee/leave-allocation
```

### Headers

| Key | Value |
|-----|-------|
| `Authorization` | `Bearer {token}` |
| `Accept` | `application/json` |

### Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `year` | integer | ❌ | Calendar year. Defaults to current year. |

### Example

```
GET /api/v1/employee/leave-allocation?year=2026
```

### Response `200 OK`

```json
{
  "success": true,
  "message": "Leave allocation retrieved successfully",
  "data": {
    "employee": {
      "id": 5,
      "name": "Ahmad Razif",
      "designations": ["Security Guard"],
      "risk_level": "high"
    },
    "leave_balances": {
      "annual_leave": {
        "allocated": 24,
        "range": { "min": 18, "max": 24 },
        "used": 3,
        "remaining": 21
      },
      "sick_leave": {
        "allocated": 12,
        "range": { "min": 10, "max": 12 },
        "used": 0,
        "remaining": 12
      },
      "casual_leave": {
        "allocated": 8,
        "range": { "min": 6, "max": 8 },
        "used": 0,
        "remaining": 8
      }
    },
    "restrictions": {
      "critical_duty_windows": true,
      "advance_approval_required": true,
      "replacement_mandatory": true,
      "blackout_periods": true
    },
    "summary": {
      "total_allocated": 44,
      "total_used": 3,
      "total_remaining": 41
    },
    "history": [
      {
        "id": 42,
        "leave_type": "Annual Leave",
        "from_date": "2026-09-10",
        "to_date": "2026-09-12",
        "days": 3,
        "is_half_day": false,
        "half_day_type": null,
        "half_day_display": "Full Day",
        "reason": "Personal",
        "status": "approved",
        "approved_by": "Admin Manager",
        "approved_at": "2026-09-08 10:00:00",
        "created_at": "2026-09-07 09:00:00"
      }
    ],
    "year": 2026
  }
}
```

### Risk Level Allocation Matrix

| Risk Level | Designations | Annual Leave | Sick Leave | Casual Leave |
|------------|-------------|-------------|------------|--------------|
| **High** | Security Guard, Patrol Staff, Control Room Operator, Night Shift Operator, Security Supervisor, Patrol Supervisor | 18–24 days | 10–12 days | 6–8 days |
| **Medium** | Supervisor, Team Lead, Field Officer, Operations Manager, Shift Supervisor | 15–18 days | 8–12 days | 6 days |
| **Low** | All others | 12–15 days | 8–10 days | 6 days |

### Restrictions by Risk Level

| Restriction | High | Medium | Low |
|-------------|------|--------|-----|
| `critical_duty_windows` | ✅ | ❌ | ❌ |
| `advance_approval_required` | ✅ | ✅ | ❌ |
| `replacement_mandatory` | ✅ | ❌ | ❌ |
| `blackout_periods` | ✅ | ❌ | ❌ |

---

## 5. Check Leave Eligibility

Checks whether an employee is eligible to take leave for a specific date range, based on their designation risk level and applicable restrictions.

```
POST /api/v1/employee/check-leave-eligibility
```

> ⚠️ This checks **only risk-based restrictions**. It does **not** verify leave balance or existing requests.
> Use this to show eligibility warnings in your UI **before** the employee submits.

### Headers

| Key | Value |
|-----|-------|
| `Authorization` | `Bearer {token}` |
| `Content-Type` | `application/json` |
| `Accept` | `application/json` |

### Request Body

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `from_date` | `YYYY-MM-DD` | ✅ | Must be ≥ today |
| `to_date` | `YYYY-MM-DD` | ✅ | Must be ≥ `from_date` |

### Example Request

```json
{
  "from_date": "2026-10-17",
  "to_date": "2026-10-18"
}
```

### Response `200 OK` — Not Eligible (High Risk, Weekend Dates)

```json
{
  "success": true,
  "message": "Leave eligibility checked successfully",
  "data": {
    "employee": {
      "id": 5,
      "name": "Ahmad Razif",
      "risk_level": "high"
    },
    "period": {
      "from_date": "2026-10-17",
      "to_date": "2026-10-18",
      "days": 2
    },
    "eligibility": {
      "allowed": false,
      "restrictions": [
        "Leave cannot be taken during critical duty windows"
      ],
      "requirements": [
        "Replacement must be assigned before leave approval"
      ]
    }
  }
}
```

### Response `200 OK` — Eligible with Requirements (Medium Risk)

```json
{
  "success": true,
  "message": "Leave eligibility checked successfully",
  "data": {
    "employee": {
      "id": 8,
      "name": "Siti Aminah",
      "risk_level": "medium"
    },
    "period": {
      "from_date": "2026-10-20",
      "to_date": "2026-10-21",
      "days": 2
    },
    "eligibility": {
      "allowed": true,
      "restrictions": [],
      "requirements": [
        "Advance approval required (minimum 3 days notice)"
      ]
    }
  }
}
```

### Response `200 OK` — Fully Eligible (Low Risk)

```json
{
  "success": true,
  "message": "Leave eligibility checked successfully",
  "data": {
    "employee": {
      "id": 12,
      "name": "Lim Wei Jian",
      "risk_level": "low"
    },
    "period": {
      "from_date": "2026-10-20",
      "to_date": "2026-10-21",
      "days": 2
    },
    "eligibility": {
      "allowed": true,
      "restrictions": [],
      "requirements": []
    }
  }
}
```

---

## 6. Admin — Approve Leave

> **Base:** `{{base_url}}/leave` (admin web panel)

```
POST /leave/{id}/approve
```

### Request Body

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `approval_notes` | string | ❌ | Max 500 characters |

### Example Request

```json
{
  "approval_notes": "Approved. Ensure handover is completed before leave."
}
```

### Response `200 OK`

```json
{
  "success": true,
  "message": "Leave request approved successfully",
  "data": {
    "status": "approved",
    "approved_at": "2026-10-07T10:30:00.000000Z",
    "approved_by": "Admin Manager"
  }
}
```

### Response `400` — Already Processed

```json
{
  "success": false,
  "message": "Leave request has already been approved"
}
```

> **Side effect:** Increments `LeaveBalance.used_days` by `days_count`. Creates a balance record if one doesn't exist yet.

---

## 7. Admin — Reject Leave

```
POST /leave/{id}/reject
```

### Request Body

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `rejection_reason` | string | ✅ | Max 500 characters |

### Example Request

```json
{
  "rejection_reason": "Insufficient coverage during this period. Please reschedule."
}
```

### Response `200 OK`

```json
{
  "success": true,
  "message": "Leave request rejected successfully",
  "data": {
    "status": "rejected",
    "rejected_at": "2026-10-07T11:00:00.000000Z",
    "rejected_by": "Admin Manager"
  }
}
```

> No balance deduction on rejection. The `rejection_reason` is stored in `approval_notes`.

---

## 8. Admin — Update Leave Status

Flexible status update — moves a leave request to any status from any current state. Used by the admin "Update Status" modal.

```
POST /leave/{id}/update-status
```

### Request Body

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `status` | string | ✅ | `pending` \| `approved` \| `rejected` |
| `comment` | string | ❌ | Max 500 characters |

### Example Request

```json
{
  "status": "approved",
  "comment": "Approved after replacement confirmed."
}
```

### Response `200 OK`

```json
{
  "success": true,
  "message": "Leave status updated to approved successfully",
  "data": {
    "status": "approved",
    "updated_at": "2026-10-07T11:30:00.000000Z",
    "updated_by": "Admin Manager"
  }
}
```

### Response `200 OK` — No-op (already on requested status)

```json
{
  "success": true,
  "message": "Leave request is already approved",
  "data": { "status": "approved" }
}
```

### Response `422` — Unknown Status

```json
{
  "success": false,
  "message": "Status 'cancelled' is not configured. Please contact the administrator."
}
```

### Balance Adjustment Logic

| Transition | Balance Effect |
|-----------|----------------|
| Any → `approved` | Deducts `days_count` from `used_days` |
| `approved` → `pending` or `rejected` | Reverses deduction (restores `used_days`) |
| Same status → same status | No-op, returns success |

---

## 9. Admin — Delete Leave Request

```
DELETE /leave/{id}
```

> ⚠️ Only `pending` or `rejected` requests can be deleted. **Approved requests cannot be deleted.**

No request body required.

### Response `200 OK`

```json
{
  "success": true,
  "message": "Leave request deleted successfully"
}
```

### Response `400` — Cannot Delete Approved

```json
{
  "success": false,
  "message": "Cannot delete approved leave requests"
}
```

---

## 10. Data Models Reference

### LeaveRequest

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Primary key |
| `user_id` | integer | FK → `users` |
| `leave_type_id` | integer | FK → `leave_types` |
| `from_date` | datetime | Leave start date |
| `to_date` | datetime | Leave end date |
| `is_half_day` | boolean | Whether this is a half-day leave |
| `half_day_type` | string | `first_half` or `second_half` |
| `reason` | string | Employee's stated reason |
| `remarks` | string | Optional additional remarks |
| `status_id` | integer | FK → `statuses` (type = `leave`) |
| `approved_by` | integer | FK → `users` (approver) |
| `approved_at` | datetime | Approval/rejection timestamp |
| `approval_notes` | string | Approver notes or rejection reason |
| `days_count` *(computed)* | float | `0.5` for half day, else `diffInDays + 1` |
| `half_day_display` *(computed)* | string | `"First Half (Morning)"`, `"Second Half (Afternoon)"`, or `"Full Day"` |
| `leave_period` *(computed)* | string | Human-readable date range string |

### LeaveType

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Primary key |
| `name` | string | Display name e.g. `"Annual Leave"` |
| `description` | string | Optional description |
| `max_days_per_year` | integer | `0` = unlimited |
| `requires_approval` | boolean | If `false`, auto-approves on submission |
| `is_paid` | boolean | Whether leave is paid |
| `is_active` | boolean | Inactive types hidden from API |

### LeaveBalance

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Primary key |
| `user_id` | integer | FK → `users` |
| `leave_type_id` | integer | FK → `leave_types` |
| `total_days` | integer | Allocated days for the year |
| `used_days` | integer | Days consumed |
| `year` | integer | Calendar year |
| `remaining_days` *(computed)* | integer | `total_days - used_days` |

---

## 11. Status & Error Codes

### Leave Statuses (`type = 'leave'` in statuses table)

| Status | Description |
|--------|-------------|
| `pending` | Default on submission when `requires_approval = true` |
| `approved` | Approved by admin, or auto on submission when `requires_approval = false` |
| `rejected` | Rejected by admin |

### HTTP Status Codes

| Code | Meaning |
|------|---------|
| `200` | Success |
| `400` | Business rule violation |
| `401` | Unauthenticated — missing or invalid token |
| `422` | Validation failed — see `errors` object for field-level details |
| `500` | Server / database error |

### Common Error Response Shape

```json
{
  "success": false,
  "message": "Human-readable error description",
  "errors": {
    "field_name": ["Validation message"]
  }
}
```

> `errors` key is only present on `422` responses.

---

## .NET Payroll Integration

The `GET /leave/types` endpoint fetches a live work-type list from the `.NET` payroll API:

```
GET {DOTNET_API_URL}/api/Payroll/GetAttendanceWorkTypeList
```

Configure in `.env`:

```env
DOTNET_API_URL=http://your-dotnet-api.com
```

### Static Fallback IDs (used when .NET API is unreachable)

| Work Type | .NET ID |
|-----------|---------|
| General Working | 1 |
| Off Day | 2 |
| Off Day Working | 3 |
| Holiday | 4 |
| Holiday Working | 5 |
| Unpaid Leave | 6 |
| Absent | 7 |
| Annual Leave | 8 |
| Medical Leave | 9 |
| Maternity Leave | 10 |
| Paternity Leave | 11 |
| Hospitalization Leave | 12 |
| Paid By SOCSO | 13 |
| Non Schedule Day | 14 |
| Replacement Leave | 15 |
| Compensate Leave | 16 |
| Marriage Leave | 17 |

---

*Generated: October 2026 | FWG Malaysia*
