# Service Record — Frontend API Reference

## Overview

The Service Record is the official career history of an employee. It tracks every position held, with position title, office/division, nature of appointment, salary grade/step, and effective dates. The record follows the **V2 history/approval pattern** — all changes are submitted as pending requests and only reflected after HR approval.

### Key behaviors

- **Approval workflow**: Writes go to `service_record_histories` with `status=pending`. HR must approve/reject before the main `service_records` table is updated.
- **Auto-close on approval**: When a new entry is approved, the previous active record (`date_to` IS NULL) is automatically closed with `date_to` = new entry's `date_from` - 1 day.
- **Auto-population**: When an employee's position is changed (via `EmployeePositionDetail` update), a pending service record entry is automatically created with pre-filled position title, office/division, and nature of appointment.
- **Salary manual-only**: Salary, salary grade, and salary step are **never** sourced from PDS. They must be entered manually by HR.
- **Manual historical entry**: Legacy data migration is supported via `isManualEntry: true` flag.

---

## Endpoints

All endpoints are prefixed with `/api/v2` and require the standard API authentication (service token / JWT).

---

### Employee-Scoped Endpoints

These endpoints operate on a single employee identified by their `employeeId` (integer, the `employees.id` primary key).

---

#### 1. List Approved Service Records

```
GET /api/v2/employee/{employeeId}/service-records/verified
```

Returns all approved service records for the employee, ordered by `dateFrom` descending.

**Response `200 OK`:**
```json
{
  "data": [
    {
      "id": 1,
      "employee_id": 42,
      "position_title": "Administrative Officer III",
      "office_division": "Administrative Division - HR Department",
      "nature_of_appointment": "Permanent",
      "salary": 45000.00,
      "salary_grade": "SG 16",
      "salary_step": "3",
      "date_from": "2025-06-01",
      "date_to": null,
      "remarks": null,
      "is_manual_entry": false,
      "approved_at": "2025-06-01T10:00:00.000000Z",
      "approved_by": 1,
      "created_at": "2025-06-01T10:00:00.000000Z",
      "updated_at": "2025-06-01T10:00:00.000000Z"
    }
  ]
}
```

**Response `404`:** Returns if `employee_no` does not exist.

```json
{
  "message": "Service records not found"
}
```

---

#### 2. List Pending Records

```
GET /api/v2/employee/{employeeId}/service-records/pending
```

Returns all **pending** history entries (awaiting HR approval) for this employee, ordered by `requestedAt` ascending (oldest first).

**Response `200 OK`:**
```json
{
  "data": [
    {
      "id": 5,
      "service_record_id": null,
      "employee_id": 42,
      "action_type": "create",
      "status": "pending",
      "position_title": "Administrative Assistant II",
      "office_division": "Finance Division",
      "nature_of_appointment": "Contractual",
      "salary": 35000.00,
      "salary_grade": "SG 12",
      "salary_step": "1",
      "date_from": "2026-06-15",
      "date_to": null,
      "remarks": "Manual entry for data migration",
      "is_manual_entry": true,
      "requested_by": 1,
      "requested_at": "2026-06-01T08:30:00.000000Z",
      "reviewed_by": null,
      "reviewed_at": null,
      "created_at": "2026-06-01T08:30:00.000000Z",
      "updated_at": "2026-06-01T08:30:00.000000Z"
    }
  ]
}
```

**Response `404`:** Returns if `employee_no` does not exist.

```json
{
  "message": "Pending service records not found"
}
```

---

#### 3. Create Service Record (Submit for Approval)

```
POST /api/v2/employee/{employeeId}/service-records
```

Submits a new service record entry for approval. The entry is stored as a pending history record — it will only appear in the approved list once HR approves it.

**Request body:** All fields under `serviceRecords[0]` etc.

```json
{
  "serviceRecords": [
    {
      "positionTitle": "Administrative Assistant II",
      "officeDivision": "Finance Division",
      "natureOfAppointment": "Contractual",
      "salary": 35000.00,
      "salaryGrade": "SG 12",
      "salaryStep": "1",
      "dateFrom": "2026-06-15",
      "dateTo": null,
      "remarks": "Manual entry for data migration",
      "isManualEntry": true,
      "requestedBy": 1
    }
  ]
}
```

**Field details:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `positionTitle` | string | **yes** | Position title (max 255) |
| `officeDivision` | string | **yes** | Office or division assignment (max 255) |
| `natureOfAppointment` | string | no | e.g. "Permanent", "Contractual", "Temporary" |
| `salary` | decimal | no | Monthly salary amount |
| `salaryGrade` | string | no | e.g. "SG 16" |
| `salaryStep` | string | no | e.g. "3" |
| `dateFrom` | date | **yes** | Effective start date (format: YYYY-MM-DD) |
| `dateTo` | date | no | End date (nullable — null = current/active) |
| `remarks` | string | no | Notes about this entry (max 1000) |
| `isManualEntry` | boolean | no | Set to `true` for data migration entries |
| `requestedBy` | integer | **yes** | Employee ID of the requester |

**Response `202 Accepted`:**
```json
{
  "message": "Service record submitted for approval"
}
```

**Response `422` (validation error):**
```json
{
  "message": "The positionTitle field is required.",
  "errors": {
    "serviceRecords.0.positionTitle": ["The positionTitle field is required."]
  }
}
```

**Response `500` (server error):**
```json
{
  "message": "Failed to submit service record",
  "error": "requested_by is required"
}
```

---

#### 4. Update Service Record (Submit Change for Approval)

```
PUT /api/v2/employee/{employeeId}/service-records
```

Submits changes to an existing approved service record. The update request goes into a pending history entry (just like create). Requires the `id` of the approved record to identify which record to update.

**Request body:**
```json
{
  "serviceRecords": [
    {
      "id": 1,
      "positionTitle": "Administrative Officer III",
      "officeDivision": "Administrative Division - HR Department",
      "natureOfAppointment": "Permanent",
      "salary": 50000.00,
      "salaryGrade": "SG 18",
      "salaryStep": "2",
      "dateFrom": "2025-06-01",
      "dateTo": null,
      "isManualEntry": false,
      "requestedBy": 1
    }
  ]
}
```

**Guard:** If there is already a pending update request for the same record, this endpoint returns an error:

```json
{
  "message": "Failed to submit service record update",
  "error": "There is already a pending request for this service record."
}
```

**Response `202 Accepted`:**
```json
{
  "message": "Service record update submitted for approval"
}
```

---

#### 5. Delete Service Record (Submit Deletion for Approval)

```
DELETE /api/v2/employee/{employeeId}/service-records/{record_id}
```

Submits a deletion request for an approved service record. The deletion goes into a pending history entry (soft-delete only takes effect after HR approval).

**Guard:** Same pending-check as update — if a pending request already exists for this record, an error is returned.

**Response `202 Accepted`:**
```json
{
  "message": "Service record deletion submitted for approval"
}
```

**Response `404`:** If the record does not exist.

---

#### 6. Full History (per Employee)

```
GET /api/v2/employee/{employeeId}/service-records/history
```

Returns **all** history entries (pending, approved, rejected) for this employee, ordered by `requestedAt` descending. Useful for auditing and tracking the lifecycle of each change.

**Response `200 OK`:**
```json
{
  "data": [
    {
      "id": 5,
      "service_record_id": 1,
      "employee_id": 42,
      "action_type": "update",
      "status": "approved",
      "position_title": "Administrative Officer III",
      "office_division": "Administrative Division - HR Department",
      "nature_of_appointment": "Permanent",
      "salary": 50000.00,
      "salary_grade": "SG 18",
      "salary_step": "2",
      "date_from": "2025-06-01",
      "date_to": null,
      "remarks": null,
      "is_manual_entry": false,
      "requested_by": 1,
      "requested_at": "2025-06-01T10:00:00.000000Z",
      "reviewed_by": 1,
      "reviewed_at": "2025-06-01T10:30:00.000000Z",
      "created_at": "2025-06-01T10:00:00.000000Z",
      "updated_at": "2025-06-01T10:30:00.000000Z"
    }
  ]
}
```

---

### Admin/Reviewer Endpoints

These endpoints are scoped under `employee/all/` and are intended for HR administrators and reviewers.

---

#### 7. List All Histories (Admin View)

```
GET /api/v2/employee/all/service-records
```

Returns all history entries across all employees, joined with employee number for identification. Ordered by `requestedAt` descending.

**Query Parameters:**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `employee_no` | string | no | Filter by employee number |
| `status` | string | no | Filter by status: `pending`, `approved`, `rejected` |
| `action_type` | string | no | Filter by action: `create`, `update`, `delete` |

**Response `200 OK`:**
```json
{
  "data": [
    {
      "id": 5,
      "service_record_id": null,
      "employee_id": 42,
      "action_type": "create",
      "status": "pending",
      "position_title": "Administrative Assistant II",
      "office_division": "Finance Division",
      "nature_of_appointment": "Contractual",
      "salary": 35000.00,
      "salary_grade": "SG 12",
      "salary_step": "1",
      "date_from": "2026-06-15",
      "date_to": null,
      "remarks": null,
      "is_manual_entry": true,
      "requested_by": 1,
      "requested_at": "2026-06-01T08:30:00.000000Z",
      "reviewed_by": null,
      "reviewed_at": null,
      "created_at": "2026-06-01T08:30:00.000000Z",
      "updated_at": "2026-06-01T08:30:00.000000Z",
      "employee_no": "2024-001"
    }
  ]
}
```

---

#### 8. Approve a Pending History

```
PATCH /api/v2/employee/all/service-records/{history_id}/approve
```

Approves a pending service record history entry. This is the action that:
- **Create**: Creates a new approved record in `service_records`, auto-closes the previous active record, and links the history back
- **Update**: Updates the existing approved record
- **Delete**: Soft-deletes the approved record

**Request body:**
```json
{
  "approvedBy": 1
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `approvedBy` | integer | **yes** | Employee ID of the approving HR officer |

**Response `200 OK`:**
```json
{
  "message": "Service record request approved"
}
```

**Response `400`** (not in pending status):
```json
{
  "message": "Only pending records can be approved.",
  "error": "..."
}
```

---

#### 9. Reject a Pending History

```
PATCH /api/v2/employee/all/service-records/{history_id}/reject
```

Rejects a pending service record history entry. No changes are made to the `service_records` table — the history entry is simply marked as rejected.

**Request body:**
```json
{
  "rejectedBy": 1
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `rejectedBy` | integer | **yes** | Employee ID of the rejecting HR officer |

**Response `200 OK`:**
```json
{
  "message": "Service record request rejected"
}
```

---

#### 10. History by Record

```
GET /api/v2/employee/all/service-records/{record_id}/history
```

Returns all history entries (pending, approved, rejected) for a specific approved record, ordered by `requestedAt` descending. Useful for seeing the full audit trail of changes to one record.

**Response `200 OK`:**
```json
{
  "data": [
    {
      "id": 5,
      "service_record_id": 1,
      "employee_id": 42,
      "action_type": "update",
      "status": "approved",
      "position_title": "Administrative Officer III",
      "office_division": "Administrative Division - HR Department",
      "nature_of_appointment": "Permanent",
      "salary": 50000.00,
      "salary_grade": "SG 18",
      "salary_step": "2",
      "date_from": "2025-06-01",
      "date_to": null,
      "requested_by": 1,
      "requested_at": "2026-06-01T10:00:00.000000Z",
      "reviewed_by": 1,
      "reviewed_at": "2026-06-01T10:30:00.000000Z"
    }
  ]
}
```

---

#### 11. Diff (Compare Approved Versions)

```
GET /api/v2/employee/all/service-records/{record_id}/diff
```

Compares the two most recent approved snapshots for a specific record, showing what changed between the previous version and the current version.

**Response `200 OK`** (with changes):

```json
{
  "current": {
    "id": 1,
    "position_title": "Administrative Officer III",
    "office_division": "Administrative Division - HR Department",
    "salary": 50000.00,
    "salary_grade": "SG 18",
    "salary_step": "2",
    ...
  },
  "previous": {
    "id": 4,
    "position_title": "Administrative Officer III",
    "office_division": "Administrative Division - HR Department",
    "salary": 45000.00,
    "salary_grade": "SG 16",
    "salary_step": "3",
    ...
  },
  "diff": {
    "salary": {
      "from": 45000.00,
      "to": 50000.00
    },
    "salary_grade": {
      "from": "SG 16",
      "to": "SG 18"
    },
    "salary_step": {
      "from": "3",
      "to": "2"
    }
  }
}
```

**Response `200 OK`** (only one version exists — no comparison possible):

```json
{
  "message": "No previous approved version available for comparison."
}
```

**Fields compared:** `position_title`, `office_division`, `nature_of_appointment`, `salary`, `salary_grade`, `salary_step`, `date_from`, `date_to`

---

## Data Model

### Approved Records (`service_records`)

These are the final approved entries shown on the employee's official service record.

| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Primary key |
| `employee_id` | int | FK to employees table |
| `position_title` | string | Position title |
| `office_division` | string | Office or division assignment |
| `nature_of_appointment` | string? | e.g. "Permanent", "Contractual", "Temporary" |
| `salary` | decimal? | Monthly salary |
| `salary_grade` | string? | e.g. "SG 16" |
| `salary_step` | string? | e.g. "3" |
| `date_from` | date | Effective start date |
| `date_to` | date? | End date (null = current/active) |
| `remarks` | text? | Notes |
| `is_manual_entry` | boolean | True if entered manually (data migration) |
| `approved_at` | datetime | When this entry was approved |
| `approved_by` | int? | Who approved it |
| `deleted_at` | datetime? | Soft delete timestamp |

### History Records (`service_record_histories`)

These track every pending/approved/rejected change. Mirror the approved record fields plus:

| Field | Type | Description |
|-------|------|-------------|
| `service_record_id` | int? | FK to the approved record (null for new creates) |
| `action_type` | enum | `create`, `update`, or `delete` |
| `status` | enum | `pending`, `approved`, or `rejected` |
| `requested_by` | int | Who submitted the request |
| `requested_at` | datetime | When it was submitted |
| `reviewed_by` | int? | Who approved/rejected it |
| `reviewed_at` | datetime? | When it was reviewed |

---

## Auto-Population Behavior

When an employee's position changes (via `EmployeePositionDetail` update), the system **automatically creates a pending service record history entry** with pre-filled values:

| Field | Source |
|-------|--------|
| `position_title` | `JobTitle.position_title` or `JobTitle.name` |
| `office_division` | `Division.name + " - " + Department.name` |
| `nature_of_appointment` | `AppointmentNature.name` |
| `date_from` | `effective_date` (fallback: `date_hired`) |
| `salary`, `salary_grade`, `salary_step` | **Left null** — must be filled manually by HR |

The auto-populated entry is **pending** — it requires HR approval before appearing on the service record.

---

## Notification Types

When a service record history is approved or rejected, a notification is sent to the employee. These notification types are available:

| Type | Triggered When |
|------|---------------|
| `service_record_approved` | Service record entry approved |
| `service_record_rejected` | Service record entry rejected |

See the [Employment Action Notification](employment-action-notification.md) doc for the full notification endpoint reference.

---

## Frontend Integration Checklist

- [ ] **HR Employee Profile**: Display approved service records by calling `GET .../service-records/verified` on employee load
- [ ] **Create Entry Form**: POST to `.../service-records` with `serviceRecords[]` array. Include `isManualEntry: true` for historical data migration entries
- [ ] **Edit Entry**: PUT to `.../service-records` with the record `id` included
- [ ] **Delete Entry**: DELETE `.../service-records/{record_id}` — shows a confirmation dialog
- [ ] **Pending Queue**: Show `GET .../service-records/pending` entries with status badges (pending/approved/rejected)
- [ ] **HR Approval Dashboard**: Use `GET .../employee/all/service-records?status=pending` to list all pending requests across employees
- [ ] **Approve/Reject**: PATCH `.../service-records/{history_id}/approve` or `.../reject` from the review panel
- [ ] **Audit Trail**: Show `GET .../service-records/history` in an expandable "Change History" section
- [ ] **Diff View**: Show `GET .../service-records/{record_id}/diff` when viewing a record's edit history — highlight changed fields
- [ ] **Salary Fields Required**: Salary, salary grade, and salary step must be filled manually — never auto-populated
- [ ] **No Overlapping Dates**: When creating a new record, the previous active record is auto-closed on approval. The frontend doesn't need to handle this explicitly

---

## Error Handling

All endpoints return consistent JSON error responses:

```json
{
  "message": "Human-readable error message",
  "error": "Detailed error (may include stack trace in debug mode)"
}
```

| HTTP Status | Meaning |
|-------------|---------|
| `200` | Success (GET, PATCH approve/reject) |
| `202` | Accepted (POST, PUT, DELETE — submitted for approval) |
| `400` | Business logic error (e.g., duplicate pending request) |
| `404` | Resource not found (employee, record, or history) |
| `422` | Validation error (missing/invalid fields) |
| `500` | Server error |
