# HR Module — Technical & Code Documentation

## Route Summary

All HR routes are registered inside the `auth:admin` middleware group and the locale prefix (e.g. `/en/admin/...`). Every route also requires the Spatie permission listed. The route name prefix for all HR routes is `admin.dashboard.hr.*`.

### Employee Routes — prefix `hr/employees`, as `dashboard.hr.employees.`

| Method | URI | Controller@method | Permission |
|--------|-----|-------------------|------------|
| GET | `hr/employees` | `EmployeeMasterController@index` | `hr.employees.view` |
| GET | `hr/employees/create` | `EmployeeMasterController@create` | `hr.employees.create` |
| POST | `hr/employees` | `EmployeeMasterController@store` | `hr.employees.create` |
| GET | `hr/employees/{employee}/edit` | `EmployeeMasterController@edit` | `hr.employees.update` |
| PUT | `hr/employees/{employee}` | `EmployeeMasterController@update` | `hr.employees.update` |
| DELETE | `hr/employees/{employee}` | `EmployeeMasterController@destroy` | `hr.employees.delete` |
| POST | `hr/employees/{employee}/contracts` | `EmployeeContractController@store` | `hr.employee_contracts.create` |
| PUT | `hr/employees/{employee}/contracts/{contract}` | `EmployeeContractController@update` | `hr.employee_contracts.update` |
| DELETE | `hr/employees/{employee}/contracts/{contract}` | `EmployeeContractController@destroy` | `hr.employee_contracts.delete` |
| POST | `hr/employees/{employee}/documents` | `EmployeeDocumentController@store` | `hr.employee_documents.create` |
| DELETE | `hr/employees/{employee}/documents/{document}` | `EmployeeDocumentController@destroy` | `hr.employee_documents.delete` |
| PUT | `hr/employees/{employee}/salary-profile` | `EmployeeSalaryProfileController@update` | `hr.employee_salary.update` |
| POST | `hr/employees/{employee}/transfers` | `EmployeeTransferController@store` | `hr.employee_transfers.create` |
| DELETE | `hr/employees/{employee}/transfers/{transfer}` | `EmployeeTransferController@destroy` | `hr.employee_transfers.delete` |
| POST | `hr/employees/{employee}/secondments` | `EmployeeSecondmentController@store` | `hr.employee_secondments.create` |
| DELETE | `hr/employees/{employee}/secondments/{secondment}` | `EmployeeSecondmentController@destroy` | `hr.employee_secondments.delete` |

### Attendance Routes

| Method | URI | Controller@method | Permission |
|--------|-----|-------------------|------------|
| GET | `hr/attendance/records` | `AttendanceController@index` | `hr.attendance_records.view` |
| GET | `hr/attendance/records/create` | `AttendanceController@create` | `hr.attendance_records.create` |
| POST | `hr/attendance/records` | `AttendanceController@store` | `hr.attendance_records.create` |
| GET | `hr/attendance/records/{attendanceRecord}/edit` | `AttendanceController@edit` | `hr.attendance_records.update` |
| PUT | `hr/attendance/records/{attendanceRecord}` | `AttendanceController@update` | `hr.attendance_records.update` |
| DELETE | `hr/attendance/records/{attendanceRecord}` | `AttendanceController@destroy` | `hr.attendance_records.delete` |
| GET | `hr/attendance/imports` | `AttendanceImportController@index` | `hr.attendance_imports.view` |
| GET | `hr/attendance/imports/create` | `AttendanceImportController@create` | `hr.attendance_imports.create` |
| POST | `hr/attendance/imports` | `AttendanceImportController@store` | `hr.attendance_imports.create` |
| GET | `hr/attendance/overtime` | `OvertimeController@index` | `hr.overtime_records.view` |
| GET | `hr/attendance/overtime/create` | `OvertimeController@create` | `hr.overtime_records.create` |
| POST | `hr/attendance/overtime` | `OvertimeController@store` | `hr.overtime_records.create` |
| GET | `hr/attendance/overtime/{overtimeRecord}/edit` | `OvertimeController@edit` | `hr.overtime_records.update` |
| PUT | `hr/attendance/overtime/{overtimeRecord}` | `OvertimeController@update` | `hr.overtime_records.update` |
| DELETE | `hr/attendance/overtime/{overtimeRecord}` | `OvertimeController@destroy` | `hr.overtime_records.delete` |
| GET | `hr/attendance/absences` | `AbsenceController@index` | `hr.absence_records.view` |
| GET | `hr/attendance/absences/create` | `AbsenceController@create` | `hr.absence_records.create` |
| POST | `hr/attendance/absences` | `AbsenceController@store` | `hr.absence_records.create` |
| GET | `hr/attendance/absences/{absenceRecord}/edit` | `AbsenceController@edit` | `hr.absence_records.update` |
| PUT | `hr/attendance/absences/{absenceRecord}` | `AbsenceController@update` | `hr.absence_records.update` |
| DELETE | `hr/attendance/absences/{absenceRecord}` | `AbsenceController@destroy` | `hr.absence_records.delete` |

### Request Routes (all follow the same CRUD pattern)

| Prefix | Route key | Permission prefix |
|--------|-----------|-------------------|
| `hr/requests/leave` | `dashboard.hr.requests.leave.` | `hr.leave_requests` |
| `hr/requests/permission` | `dashboard.hr.requests.permission.` | `hr.permission_requests` |
| `hr/requests/loans` | `dashboard.hr.requests.loans.` | `hr.loan_requests` |
| `hr/requests/bonuses` | `dashboard.hr.requests.bonuses.` | `hr.bonus_requests` |
| `hr/requests/data-changes` | `dashboard.hr.requests.data_changes.` | `hr.data_change_requests` |
| `hr/requests/complaints` | `dashboard.hr.requests.complaints.` | `hr.complaint_requests` |
| `hr/requests/resignations` | `dashboard.hr.requests.resignations.` | `hr.resignation_requests` |

Each group exposes `index`, `create`, `store`, `{model}/edit`, `{model}` (PUT), `{model}` (DELETE) with the corresponding `.view`, `.create`, `.update`, `.delete` permissions.

### Approval Routes — prefix `hr/requests/approvals`

| Method | URI | Controller@method | Permission |
|--------|-----|-------------------|------------|
| GET | `hr/requests/approvals` | `ApprovalController@index` | `hr.request_approvals.view` |
| GET | `hr/requests/approvals/{approval}` | `ApprovalController@show` | `hr.request_approvals.view` |
| PUT | `hr/requests/approvals/{approval}` | `ApprovalController@update` | `hr.request_approvals.action` |

### Payroll Routes — prefix `hr/payroll`

| Method | URI | Controller@method | Permission |
|--------|-----|-------------------|------------|
| GET | `hr/payroll` | `PayrollRunController@index` | `hr.payroll_runs.view` |
| GET | `hr/payroll/periods` | `PayrollPeriodController@index` | `hr.payroll_periods.view` |
| GET | `hr/payroll/periods/create` | `PayrollPeriodController@create` | `hr.payroll_periods.create` |
| POST | `hr/payroll/periods` | `PayrollPeriodController@store` | `hr.payroll_periods.create` |
| GET | `hr/payroll/periods/{payrollPeriod}/edit` | `PayrollPeriodController@edit` | `hr.payroll_periods.update` |
| PUT | `hr/payroll/periods/{payrollPeriod}` | `PayrollPeriodController@update` | `hr.payroll_periods.update` |
| DELETE | `hr/payroll/periods/{payrollPeriod}` | `PayrollPeriodController@destroy` | `hr.payroll_periods.delete` |
| GET | `hr/payroll/runs` | `PayrollRunController@index` | `hr.payroll_runs.view` |
| GET | `hr/payroll/runs/create` | `PayrollRunController@create` | `hr.payroll_runs.create` |
| POST | `hr/payroll/runs` | `PayrollRunController@store` | `hr.payroll_runs.create` |
| GET | `hr/payroll/runs/{payrollRun}/edit` | `PayrollRunController@edit` | `hr.payroll_runs.update` |
| PUT | `hr/payroll/runs/{payrollRun}` | `PayrollRunController@update` | `hr.payroll_runs.update` |
| PUT | `hr/payroll/runs/{payrollRun}/process` | `PayrollRunController@process` | `hr.payroll_runs.run` |
| DELETE | `hr/payroll/runs/{payrollRun}` | `PayrollRunController@destroy` | `hr.payroll_runs.delete` |
| GET | `hr/payroll/details/{payrollRun}` | `PayrollDetailController@show` | `hr.payroll_runs.view` |
| GET | `hr/payroll/approvals` | `PayrollApprovalController@index` | `hr.payroll_approvals.view` |
| GET | `hr/payroll/approvals/{payrollRun}` | `PayrollApprovalController@show` | `hr.payroll_approvals.view` |
| PUT | `hr/payroll/approvals/{payrollRun}` | `PayrollApprovalController@update` | `hr.payroll_approvals.action` |
| GET | `hr/payroll/postings` | `PayrollPostingController@index` | `hr.payroll_postings.view` |
| POST | `hr/payroll/postings` | `PayrollPostingController@store` | `hr.payroll_postings.create` |
| GET | `hr/payroll/postings/{payrollPosting}` | `PayrollPostingController@show` | `hr.payroll_postings.view` |
| PUT | `hr/payroll/postings/{payrollPosting}` | `PayrollPostingController@update` | `hr.payroll_postings.post` |
| DELETE | `hr/payroll/postings/{payrollPosting}` | `PayrollPostingController@destroy` | `hr.payroll_postings.delete` |
| GET | `hr/payroll/payments` | `SalaryPaymentController@index` | `hr.salary_payments.view` |
| GET | `hr/payroll/payments/create` | `SalaryPaymentController@create` | `hr.salary_payments.create` |
| POST | `hr/payroll/payments` | `SalaryPaymentController@store` | `hr.salary_payments.create` |
| GET | `hr/payroll/payments/{salaryPayment}/edit` | `SalaryPaymentController@edit` | `hr.salary_payments.update` |
| PUT | `hr/payroll/payments/{salaryPayment}` | `SalaryPaymentController@update` | `hr.salary_payments.update` |
| DELETE | `hr/payroll/payments/{salaryPayment}` | `SalaryPaymentController@destroy` | `hr.salary_payments.delete` |
| GET | `hr/payroll/eos` | `EndOfServiceController@index` | `hr.eos_settlements.view` |
| GET | `hr/payroll/eos/create` | `EndOfServiceController@create` | `hr.eos_settlements.create` |
| POST | `hr/payroll/eos` | `EndOfServiceController@store` | `hr.eos_settlements.create` |
| GET | `hr/payroll/eos/{endOfServiceSettlement}` | `EndOfServiceController@show` | `hr.eos_settlements.view` |
| GET | `hr/payroll/eos/{endOfServiceSettlement}/edit` | `EndOfServiceController@edit` | `hr.eos_settlements.update` |
| PUT | `hr/payroll/eos/{endOfServiceSettlement}` | `EndOfServiceController@update` | `hr.eos_settlements.update` |
| PATCH | `hr/payroll/eos/{endOfServiceSettlement}/process` | `EndOfServiceController@process` | `hr.eos_settlements.action` |
| DELETE | `hr/payroll/eos/{endOfServiceSettlement}` | `EndOfServiceController@destroy` | `hr.eos_settlements.delete` |

### Recruitment Routes

Same CRUD pattern for:
- `hr/recruitment/job-requests` → `JobRequestController`
- `hr/recruitment/candidates` → `CandidateController`
- `hr/recruitment/interviews` → `InterviewController`
- `hr/recruitment/job-offers` → `JobOfferController`

### Setup Routes

All eleven master-data setup entities (`departments`, `designations`, `grades`, `contract-types`, `leave-types`, `permission-types`, `loan-types`, `bonus-types`, `penalty-types`, `earning-types`, `deduction-types`) are served by a single controller (`SetupMasterController`) using route defaults to inject the `entity` slug. Routes follow the standard CRUD pattern at `hr/setup/{entity}`.

Additional dedicated controllers:
- `hr/setup/request-workflows` → `RequestWorkflowController`
- `hr/setup/payroll-posting-profiles` → `PayrollPostingProfileController`

### Reports & Analytics

| Method | URI | Controller@method | Permission |
|--------|-----|-------------------|------------|
| GET | `hr/reports/payroll-summary` | `PayrollSummaryController@index` | `hr.payroll_summary.view` |
| GET | `hr/analytics/dashboard` | `HrAnalyticsDashboardController@index` | `hr.dashboard.view` |

---

## Controllers

### `EmployeeController` — `App\Http\Controllers\Admin\HR\Employee`

Injected services: `EmployeeService`, `EmployeeContractService`, `EmployeeDocumentService`, `EmployeeSalaryService`, `EmployeeMovementService`, `EmployeeStatusService`.

```
index(Request $request): View
```
- Accepts filters: `search`, `branch_id`, `designation_id`, `employment_status`.
- Calls `EmployeeService::getAll(15, $filters)`.
- Returns `dashboard.admin.hr.employees.index` with paginated employees and index options.

```
create(): View
```
- Returns `dashboard.admin.hr.employees.create` with `EmployeeService::getFormOptions()`.

```
store(StoreEmployeeRequest $request): RedirectResponse
```
- Calls `EmployeeService::create($request->validated())`.
- Redirects to the new employee's edit screen.

```
edit(Employee $employee): View
```
- Eager-loads candidate, branch, department, designation, grade, directManager, creator, updater on the employee.
- Calls private `editViewData()` to assemble: form options, contracts, contract options, documents, document types, salary profile, bank accounts, salary options, transfers, secondments, status histories.
- Returns `dashboard.admin.hr.employees.edit`.

```
update(UpdateEmployeeRequest $request, Employee $employee): RedirectResponse
```
- Calls `EmployeeService::update($employee, $request->validated())`.
- Redirects to edit screen preserving the active `tab` query parameter.

```
destroy(Employee $employee): RedirectResponse
```
- Calls `EmployeeService::delete($employee)`.
- Redirects to index.

### `EmployeeContractController`

```
store(Request $request, Employee $employee): RedirectResponse
```
- Calls `EmployeeContractService::create($employee, $request->validated())`.

```
update(Request $request, Employee $employee, EmployeeContract $contract): RedirectResponse
```
- Calls `EmployeeContractService::update($contract, $request->validated())`.

```
destroy(Employee $employee, EmployeeContract $contract): RedirectResponse
```
- Calls `EmployeeContractService::delete($contract)`.

### `EmployeeDocumentController`

```
store(Request $request, Employee $employee): RedirectResponse
```
- Calls `EmployeeDocumentService::create($employee, $request->validated())`.

```
destroy(Employee $employee, EmployeeDocument $document): RedirectResponse
```
- Calls `EmployeeDocumentService::delete($document)`.

### `EmployeeSalaryProfileController`

```
update(Request $request, Employee $employee): RedirectResponse
```
- Calls `EmployeeSalaryService::updateProfile($employee, $request->validated())`.
- This is a single PUT endpoint that handles the entire salary profile, allowances, deductions, and bank accounts in one transaction.

### `EmployeeTransferController`

```
store(Request $request, Employee $employee): RedirectResponse
```
- Calls `EmployeeMovementService::createTransfer($employee, $request->validated())`.
- The service also updates the employee's own branch/department/designation/grade in the same transaction.

### `EmployeeSecondmentController`

```
store(Request $request, Employee $employee): RedirectResponse
```
- Calls `EmployeeMovementService::createSecondment($employee, $request->validated())`.

### `SetupMasterController` — `App\Http\Controllers\Admin\HR\Setup`

Single controller serving 11 entity types via the `entity` route default.

```
index(string $entity): View
resolveResource($entity)  →  array of [title, singular, service, model, route, has_payroll_affects]
resolveService($entity)   →  resolves service class from container
resolveModel($entity, $record)  →  Model::findOrFail($record)
```

All five methods (index, create, store, edit, update, destroy) call `resolveResource()` to get the resource config, then delegate to the entity-specific service. Shared view: `dashboard.admin.hr.setup.masters.*`.

Supported entities and their service/model pairs:

| Entity slug | Service | Model |
|-------------|---------|-------|
| `departments` | `DepartmentService` | `Department` |
| `designations` | `DesignationService` | `Designation` |
| `grades` | `GradeService` | `Grade` |
| `contract-types` | `ContractTypeService` | `ContractType` |
| `leave-types` | `LeaveTypeService` | `LeaveType` |
| `permission-types` | `PermissionTypeService` | `PermissionType` |
| `loan-types` | `LoanTypeService` | `LoanType` |
| `bonus-types` | `BonusTypeService` | `BonusType` |
| `penalty-types` | `PenaltyTypeService` | `PenaltyType` |
| `earning-types` | `EarningTypeService` | `EarningType` |
| `deduction-types` | `DeductionTypeService` | `DeductionType` |

Entities where `has_payroll_affects = true` (leave-types, loan-types, bonus-types, penalty-types, earning-types, deduction-types) show an extra `payroll_affects` toggle in their create/edit views.

### `RequestWorkflowController` — `App\Http\Controllers\Admin\HR\Setup`

```
index(): View          // WorkflowService::getAll() → paginated with steps.role
create(): View         // WorkflowService::getRoles() passed as $roles
store(Request): RedirectResponse   // WorkflowService::create($validated)
edit(RequestWorkflow): View        // loads steps.role, passes $roles
update(Request, RequestWorkflow): RedirectResponse  // WorkflowService::update()
destroy(RequestWorkflow): RedirectResponse          // WorkflowService::delete()
```

### `ApprovalController` — `App\Http\Controllers\Admin\HR\Requests`

```
index(Request $request): View
```
- Filters: `request_type`, `status`, `employee_id`.
- Returns paginated approvals with `steps.role`, `steps.approverEmployee`, `steps.actorAdmin`.

```
show(Approval $approval): View
```
- Loads the approval with all step relations.
- Calls `ApprovalWorkflowService::resolveRequestRecord()` to load the actual request entity (LeaveRequest, LoanRequest, etc.) with its `approval.steps.*` relations.
- Returns `dashboard.admin.hr.requests.approvals.show` with both `$approval` and `$requestRecord`.

```
update(ProcessApprovalRequest $request, Approval $approval): RedirectResponse
```
- Validated inputs: `action` (`approve` or `reject`), `action_notes` (nullable string).
- Calls `ApprovalWorkflowService::processApproval($approval, $action, $notes)`.

### `PayrollRunController`

```
index(Request): View    // filters: search, payroll_period_id, branch_id, run_type, status, payment_status
create(): View
store(StorePayrollRunRequest): RedirectResponse   // creates run, redirects to index
edit(PayrollRun): View
update(UpdatePayrollRunRequest, PayrollRun): RedirectResponse
process(ProcessPayrollRunRequest, PayrollRun): RedirectResponse
    // Calls PayrollRunService::process() → redirects to details show
destroy(PayrollRun): RedirectResponse
```

### `EndOfServiceController`

```
index(Request): View       // filters: search, employee_id, branch_id, status, payment_status
create(): View
store(StoreEndOfServiceSettlementRequest): RedirectResponse
show(EndOfServiceSettlement): View
edit(EndOfServiceSettlement): View
update(UpdateEndOfServiceSettlementRequest, EndOfServiceSettlement): RedirectResponse
process(ProcessEndOfServiceSettlementRequest, EndOfServiceSettlement): RedirectResponse
    // Validated input: action (approve | post | pay), journal_entry_id?, settlement_date?
    // Routes to EndOfServiceService::approve(), markAsPosted(), or markAsPaid()
destroy(EndOfServiceSettlement): RedirectResponse
```

---

## Services

### `EmployeeService` — `App\Services\HR\Employee`

```php
getAll(int $perPage = 15, array $filters = []): LengthAwarePaginator
```
Eager-loads candidate, branch, department, designation, grade, directManager, creator, updater. Filters on `search` (searches `code`, `email`, `phone`, `name->ar`, `name->en`), `branch_id`, `designation_id`, `employment_status`.

```php
getIndexOptions(): array
```
Returns `branches` (active), `designations` (active), `employmentStatuses`.

```php
getFormOptions(?int $employeeId = null): array
```
Returns candidates, branches, departments, designations, grades, managers (active employees excluding self when editing), and enum lists for status, gender, marital status.

```php
create(array $data): Employee
```
Creates employee, stamps `created_by` with the authenticated admin ID.

```php
update(Employee $employee, array $data): Employee
```
Updates employee, stamps `updated_by`.

```php
delete(Employee $employee): bool
```
Calls `$employee->delete()`.

### `EmployeeContractService`

```php
getForEmployee(Employee $employee): Collection
```
Returns contracts ordered by `start_date` desc, with `contractType`, `creator`, `updater`.

```php
getFormOptions(): array
```
Returns `contractTypes` (active) and `statuses`.

```php
create(Employee $employee, array $data): EmployeeContract
update(EmployeeContract $contract, array $data): EmployeeContract
delete(EmployeeContract $contract): bool
```

### `EmployeeDocumentService`

```php
getForEmployee(Employee $employee): Collection
getDocumentTypes(): array   // ['id','passport','contract','certificate','medical','other']
create(Employee $employee, array $data): EmployeeDocument
update(EmployeeDocument $document, array $data): EmployeeDocument
delete(EmployeeDocument $document): bool
```

### `EmployeeSalaryService`

```php
getCurrentProfile(Employee $employee): ?EmployeeSalaryProfile
```
Returns the profile where `is_current = true`, ordered by `effective_from` desc, with allowances, deductions, and posting profile loaded.

```php
getFormOptions(): array
```
Returns costCenters, payrollPostingProfiles, allowanceTypes, deductionTypes, and enum lists.

```php
updateProfile(Employee $employee, array $data): EmployeeSalaryProfile
```
Runs in a DB transaction:
1. Extracts `allowances`, `deductions`, `bank_accounts` from `$data`.
2. Upserts the current salary profile (creates if none exists, updates if one exists).
3. If `allowances` key was present, calls `syncAllowances()` — deletes all existing allowance records and re-inserts the submitted array.
4. If `deductions` key was present, calls `syncDeductions()` — same pattern.
5. If `bank_accounts` key was present, calls `syncBankAccounts()` — same pattern.

### `EmployeeMovementService`

```php
getTransfers(Employee $employee): Collection
getSecondments(Employee $employee): Collection

createTransfer(Employee $employee, array $data): EmployeeTransfer
```
Runs in a DB transaction: creates the transfer record, then immediately updates the employee's `branch_id`, `department_id`, `designation_id`, `grade_id` with the `to_*` values.

```php
createSecondment(Employee $employee, array $data): EmployeeSecondment
```
Creates secondment; does not alter the employee's primary assignment fields.

### `EmployeeStatusService`

```php
getHistory(Employee $employee): Collection

changeStatus(Employee $employee, array $data): EmployeeStatusHistory
```
Runs in a DB transaction:
1. Creates a `EmployeeStatusHistory` with `previous_status = $employee->employment_status`, `new_status`, `effective_date`, `reason`, `notes`.
2. Updates `$employee->employment_status` and sets `is_active` to `true` only for statuses `active` or `seconded`.

### `AttendanceService`

```php
getAll(int $perPage, array $filters): LengthAwarePaginator
// filters: employee_id, branch_id, status, source_type, date_from, date_to
getFormOptions(): array
// statuses: present, absent, late, early_leave, partial_day
// sourceTypes: manual, excel, biometric

create(array $data): AttendanceRecord
update(AttendanceRecord $record, array $data): AttendanceRecord
delete(AttendanceRecord $record): bool

upsertFromImport(array $data): AttendanceRecord
```
Used by the import service. Calls `AttendanceRecord::updateOrCreate` on `['employee_id', 'attendance_date']`. Calculates `worked_minutes` from `check_in_at` and `check_out_at` if not supplied.

### `AttendanceImportService`

```php
createBatch(array $data): AttendanceImportBatch
// Generates batch code ATT-IMP-{timestamp}-{random4}

processRows(AttendanceImportBatch $batch, array $rows): AttendanceImportBatch
```
Sets batch status to `processing`, iterates rows inside a transaction, calls `AttendanceService::upsertFromImport()` per row. Catches `\Throwable` per row; increments `failed_rows` without aborting the batch. Sets final status to `completed` or `completed_with_errors`.

### `OvertimeService`

```php
getAll(int $perPage, array $filters): LengthAwarePaginator
// filters: employee_id, branch_id, status, date_from, date_to
// statuses: pending, approved, rejected

create(array $data): OvertimeRecord
update(OvertimeRecord $record, array $data): OvertimeRecord
delete(OvertimeRecord $record): bool
```
Calculates `requested_minutes` from `start_at` / `end_at` if not supplied.

### `AbsenceService`

```php
getAll(int $perPage, array $filters): LengthAwarePaginator
// filters: employee_id, branch_id, absence_type, status, date_from, date_to
// absenceTypes: unexcused, excused, sick, leave
// statuses: open, approved, rejected

create(array $data): AbsenceRecord
update(AbsenceRecord $record, array $data): AbsenceRecord
delete(AbsenceRecord $record): bool
```

### `ApprovalWorkflowService` — `App\Services\HR\Requests`

Central service governing all request approvals.

```php
getAll(int $perPage, array $filters): LengthAwarePaginator
// filters: request_type, status, employee_id

getFilterOptions(): array
// employees (active), requestTypes, statuses

createForRequest(string $requestType, int $requestId, Employee $requester): Approval
```
DB transaction:
1. Finds active `RequestWorkflow` for `$requestType` with steps loaded.
2. `updateOrCreate` an `Approval` keyed on `[request_type, request_id]`, resetting status to `pending`, clearing completion fields, stamping `submitted_at`.
3. Deletes existing steps for the approval.
4. Re-creates `ApprovalStep` rows from the workflow steps, setting the first step to `pending` and all others to `waiting`.
5. Resolves `approver_employee_id` for manager-type steps by walking up `directManager` relationships `manager_level` times.
6. Syncs the request record's `status`, `current_approval_step`, `approved_at`, `rejected_at` fields.

```php
refreshForRequest(string $requestType, int $requestId, Employee $requester): Approval
```
Alias for `createForRequest()` — replaces the approval wholesale.

```php
deleteForRequest(string $requestType, int $requestId): void
```
Deletes the `Approval` record (and cascades to steps).

```php
getByRequest(string $requestType, int $requestId): ?Approval
getCurrentStep(Approval $approval): ?ApprovalStep
```
Returns the `pending` step with the lowest `sequence_order`.

```php
processApproval(Approval $approval, string $action, ?string $notes = null): Approval
```
DB transaction:
- Gets `currentStep` (the `pending` step with lowest sequence).
- On `reject`: marks step `rejected`, marks remaining `waiting` steps `skipped`, sets approval `status = rejected`, `completed_at = now()`. Updates request model: `status = rejected`, `rejected_at = now()`.
- On `approve`: marks step `approved`. Looks for next `waiting` step ordered by sequence. If found, advances it to `pending`, updates `current_sequence_order`. If not found (last step), sets approval `status = approved`, `completed_at = now()`. Updates request model: `status = approved`, `approved_at = now()`.

```php
resolveRequestRecord(string $requestType, int $requestId): ?Model
```
Returns the full request model with employee, branch, attachments, and approval.steps loaded.

Private helpers:
- `resolveManager(Employee, ?int $level)` — walks the `directManager` chain `$level` times.
- `updateRequestState(string, int, array)` — issues a direct `whereKey` update on the request model table.
- `resolveRequestModelClass(string)` — maps request type string to Eloquent class:
  - `leave` → `LeaveRequest`
  - `permission` → `PermissionRequest`
  - `loan` → `LoanRequest`
  - `bonus` → `BonusRequest`
  - `data_change` → `DataChangeRequest`
  - `complaint` → `ComplaintRequest`
  - `resignation` → `ResignationRequest`

### Request-specific Services

The seven request services (`LeaveRequestService`, `PermissionRequestService`, `LoanRequestService`, `BonusRequestService`, `DataCorrectionService`, `ComplaintService`, `ResignationService`) share a common pattern. Each injects `ApprovalWorkflowService`.

**`create(array $data)`** (all types):
1. Extracts attachments from `$data`.
2. Finds employee by `employee_id`.
3. Calls `preparePayload()` to fill in the auto-generated `request_code`, the resolved `branch_id`, and type-specific computed fields.
4. Creates the request model.
5. Calls `syncAttachments()` to delete existing and re-insert submitted file records.
6. Calls `ApprovalWorkflowService::createForRequest()`.
7. Returns fully loaded model.

**`update(Model, array $data)`** (all types):
1. Extracts attachments (null = do not touch, `[]` = clear).
2. Resolves employee.
3. Updates the model.
4. If attachments not null, syncs them.
5. If request is still `pending`, calls `ApprovalWorkflowService::refreshForRequest()`.

**`delete(Model)`** (all types):
1. Calls `ApprovalWorkflowService::deleteForRequest()`.
2. Deletes the request model.

**Type-specific `preparePayload()` logic:**

- `LeaveRequestService` — auto-generates code `LV-{timestamp}-{random4}`, resolves `branch_id`, calculates `requested_days` from start/end dates (0.5 for half-day).
- `PermissionRequestService` — code prefix `PM-`, calculates `requested_minutes` from `start_time` / `end_time`.
- `LoanRequestService` — code prefix `LN-`, calculates `installment_amount = amount / installment_count`, sets initial `payroll_status = pending`.
- `BonusRequestService` — code prefix `BN-`, sets initial `payroll_status = pending`.
- `DataCorrectionService` — code prefix `DC-`.
- `ComplaintService` — code prefix `CP-`, resolves `responded_at` when `response_notes` is set, resolves `closed_at` when status becomes `closed`.
- `ResignationService` — code prefix `RS-`, sets initial `settlement_status = pending`.

### `WorkflowService` — `App\Services\HR\Setup`

```php
getAll(int $perPage = 15): LengthAwarePaginator
getActive(): Collection   // is_active = true, ordered by request_type
getRoles(): Collection    // all Roles ordered by name

create(array $data): RequestWorkflow
update(RequestWorkflow $workflow, array $data): RequestWorkflow
delete(RequestWorkflow $workflow): bool
```

**`create` / `update`** run in a DB transaction and call private `syncSteps()`:
- Deletes all existing steps for the workflow.
- Re-inserts each step from the `steps` array with `sequence_order`, `approval_type`, `role_id`, and `manager_level`.

### `PayrollPeriodService`

```php
getAll(int $perPage, array $filters): LengthAwarePaginator
// filters: search (code), status, period_year, period_month
// statuses: open, processing, closed

create(array $data): PayrollPeriod
update(PayrollPeriod, array $data): PayrollPeriod
delete(PayrollPeriod): bool
```
Auto-generates code as `PP-YYYYMM`. Defaults status to `open`.

### `PayrollRunService`

```php
getAll(int $perPage, array $filters): LengthAwarePaginator
// filters: search, payroll_period_id, branch_id, run_type, status, payment_status

create(array $data): PayrollRun
// Auto-generates code RUN-{timestamp}-{random4}. Defaults run_type=monthly, status=draft.

update(PayrollRun, array $data): PayrollRun

process(PayrollRun): PayrollRun
```
DB transaction:
1. Calls `PayrollCalculationService::buildRunPayloads()` to compute employee lines.
2. Calls `PayrollLineService::replaceForRun()` to delete existing lines and insert new ones.
3. Sets `status = processed`, `processed_at = now()`.

```php
processFreeSalary(PayrollRun, array $lines, FreeSalaryService): PayrollRun
```
DB transaction: delegates to `FreeSalaryService::syncLines()`.

```php
handleApproval(PayrollRun, string $action, ?string $notes): PayrollRun
```
On `approve`: sets `status = approved`, stamps `approved_by`, `approved_at`.
On any other action: reverts to `status = draft`, clears approval fields.

```php
refreshPaymentStatus(PayrollRun): PayrollRun
```
Computes aggregate payment status from `salaryPayments` collection:
- All `paid` → `paid`; run status → `paid`.
- Any `paid` or `processing` → `partially_paid`.
- Otherwise → `unpaid`.
If run was `paid` but now status recalculates lower, reverts run to `posted`.

```php
delete(PayrollRun): bool
```

### `PayrollCalculationService`

```php
buildRunPayloads(PayrollRun $payrollRun): Collection
```
Queries active employees (optionally filtered by `branch_id`), having at least one salary profile effective within the period. Maps each through `calculateLine()`. Filters out nulls (employees with no matching profile).

```php
calculateLine(PayrollRun, Employee, ?PayrollPeriod): ?array
```
Returns null if no salary profile is found. Otherwise builds:
1. Basic salary item (`item_type = basic_salary`, `item_category = earning`).
2. Allowance items via `buildAllowanceItems()`.
3. Bonus items via `buildBonusItems()`.
4. Deduction items via `buildDeductionItems()`.
5. Loan items via `buildLoanItems()`.
6. Leave deduction items via `buildLeaveDeductionItems()`.

Returns array: `employee_id`, `salary_profile_id`, `cost_center_id`, `basic_salary`, `total_earnings` (sum of non-basic earnings), `total_deductions`, `net_salary` (basic + earnings - deductions), `status = draft`, `is_free_salary`, `items`.

**Allowances** (`buildAllowanceItems`): filters profile allowances by effective date range vs period. Calculates amount as `fixed = value` or `percentage = basic * value / 100`.

**Bonuses** (`buildBonusItems`): queries `BonusRequest` where `employee_id` matches, `status = approved`, `bonus_date` within period, `bonusType.payroll_affects = true` (or no type), `payroll_status` is null or `pending`.

**Deductions** (`buildDeductionItems`): same pattern as allowances, negative amounts.

**Loans** (`buildLoanItems`): queries `LoanRequest` where `status = approved`, `deduction_start_date <= period end_date`, `loanType.payroll_affects = true` (or no type), `payroll_status` is null or `pending`. Amount = `installment_amount`.

**Leave deductions** (`buildLeaveDeductionItems`): queries `LeaveRequest` where `status = approved`, dates overlap period, `leaveType.payroll_affects = true`. Daily rate = `basic / days_in_period`. Leave days within period are clamped to the period boundary. Half-day single-day leave = 0.5 days.

`makeItem()` returns null if `amount <= 0`, effectively excluding zero-value items from the run.

### `PayrollPostingService`

```php
getAll(int $perPage, array $filters): LengthAwarePaginator
getFormOptions(): array   // runs, postingProfiles, statuses

createForRun(PayrollRun $payrollRun, array $data = []): PayrollPosting
```
DB transaction. Calculates `gross_amount = sum(basic_salary + total_earnings)` across all lines. Upserts the posting record (keyed on `payroll_run_id`). Generates code `PST-{timestamp}-{random4}`.

```php
markAsPosted(PayrollPosting, ?int $journalEntryId): PayrollPosting
```
DB transaction with `lockForUpdate`. Creates or posts a journal entry via `PayrollFinanceMappingService`. Sets posting `status = posted`, `posted_by`, `posted_at`. Updates parent run to `status = posted`.

```php
delete(PayrollPosting): bool
```

### `PayrollFinanceMappingService`

Bridges payroll results to the general ledger.

```php
createAndPostPayrollRunEntry(PayrollRun, PayrollPostingProfile, ?string $description): JournalEntry
```
Iterates run lines. For each line item:
- `basic_salary` → debit `basic_salary_account_id`
- `allowance` → debit `allowance_account_id`
- `loan` → credit `loan_receivable_account_id`
- Any other earning → debit `earning_account_id`
- Any deduction → credit `deduction_account_id`
- Net salary per line → credit `payroll_payable_account_id`

Lines with the same `account_id + cost_center_id` are collapsed (summed). The resulting journal entry is posted via `JournalPostingService`, subject to open-period check.

```php
createAndPostEndOfServiceAccrualEntry(EndOfServiceSettlement, PayrollPostingProfile, ?string): JournalEntry
```
- Debit `eos_account_id` for gross amount.
- Credit `deduction_account_id` for deduction amount (if > 0).
- Credit `payroll_payable_account_id` for net amount.

```php
createAndPostEndOfServicePaymentEntry(EndOfServiceSettlement, PayrollPostingProfile, ?string $settlementDate, ?string): JournalEntry
```
- Debit `payroll_payable_account_id` for net amount.
- Credit `bank_cash_account_id` for net amount.

```php
postExistingJournalEntry(JournalEntry): JournalEntry
```
If already posted, returns as-is. Otherwise calls `JournalPostingService::post()`.

### `SalaryPaymentService`

```php
create(array $data): SalaryPayment
```
DB transaction: creates the payment record, then calls `PayrollRunService::refreshPaymentStatus()` on the parent run.

```php
update(SalaryPayment, array $data): SalaryPayment
```
Same pattern.

```php
delete(SalaryPayment): bool
```
DB transaction: deletes, then refreshes run payment status.

Auto-generates code `PAY-{timestamp}-{random4}`. Resolves `employee_id` and `branch_id` from the linked payroll line if not supplied.

### `EndOfServiceService`

```php
create(array $data): EndOfServiceSettlement
```
DB transaction. Resolves resignation request and employee from IDs. Calculates `service_years = diffInDays(hire_date, last_working_date) / 365`. Computes `net_amount = gross_amount - deduction_amount`. Sets resignation request `settlement_status = processing`.

```php
update(EndOfServiceSettlement, array $data): EndOfServiceSettlement
approve(EndOfServiceSettlement): EndOfServiceSettlement   // status → approved
markAsPosted(EndOfServiceSettlement, ?int $journalEntryId): EndOfServiceSettlement
// Calls PayrollFinanceMappingService::createAndPostEndOfServiceAccrualEntry
// Sets status → posted

markAsPaid(EndOfServiceSettlement, ?string $settlementDate): EndOfServiceSettlement
```
DB transaction. Requires `status = posted`. Creates and posts EOS payment journal entry. Sets `payment_status = paid`, records `paid_at`. Sets resignation request `settlement_status = completed`.

```php
delete(EndOfServiceSettlement): bool
```

### `JobRequestService` / `CandidateService` / `InterviewService` / `JobOfferService`

All four follow the thin service pattern: `getAll()` returns paginated results with related models, `getFormOptions()` returns dropdown data, `create()` / `update()` / `delete()` perform the CRUD with creator/updater stamping.

- **`CandidateService`** additionally calls private `syncDocuments()` on create and update (delete-then-reinsert pattern, keyed on `document_type + file_path`).
- **`InterviewService`** calls `syncEvaluations()` in the same fashion.

---

## Models

### `Employee` — table `hr_employees`

```php
use HasTranslations, Notifiable;
implements Authenticatable   // employee portal guard
```

**Fillable**: `code`, `candidate_id`, `branch_id`, `department_id`, `designation_id`, `grade_id`, `direct_manager_id`, `name`, `email`, `password`, `phone`, `hire_date`, `date_of_birth`, `gender`, `marital_status`, `nationality`, `national_id_number`, `passport_number`, `address`, `emergency_contact_name`, `emergency_contact_phone`, `employment_status`, `is_active`, `created_by`, `updated_by`.

**Translatable**: `['name']` — stored as JSON `{"en":"...","ar":"..."}`. Searched via `name->ar` and `name->en` JSON column paths.

**Casts**: `hire_date` → date, `date_of_birth` → date, `is_active` → boolean, `password` → hashed.

**Hidden**: `password`, `remember_token`.

**Relationships**:

| Method | Type | Target |
|--------|------|--------|
| `candidate()` | BelongsTo | `Candidate` (candidate_id) |
| `branch()` | BelongsTo | `Branch` |
| `department()` | BelongsTo | `Department` |
| `designation()` | BelongsTo | `Designation` |
| `grade()` | BelongsTo | `Grade` |
| `directManager()` | BelongsTo | `Employee` (direct_manager_id) — self-referential |
| `subordinates()` | HasMany | `Employee` (direct_manager_id) — self-referential |
| `contracts()` | HasMany | `EmployeeContract` |
| `documents()` | HasMany | `EmployeeDocument` |
| `salaryProfiles()` | HasMany | `EmployeeSalaryProfile` |
| `bankAccounts()` | HasMany | `EmployeeBankAccount` |
| `transfers()` | HasMany | `EmployeeTransfer` |
| `secondments()` | HasMany | `EmployeeSecondment` |
| `statusHistories()` | HasMany | `EmployeeStatusHistory` |
| `creator()` | BelongsTo | `Admin` (created_by) |
| `updater()` | BelongsTo | `Admin` (updated_by) |

### `EmployeeContract` — table `hr_employee_contracts`

**Fillable**: `employee_id`, `contract_type_id`, `contract_number`, `start_date`, `end_date`, `probation_end_date`, `signed_at`, `status`, `is_current`, `notes`, `created_by`, `updated_by`.

**Casts**: `start_date`, `end_date`, `probation_end_date`, `signed_at` → date; `is_current` → boolean.

**Relationships**: `employee()` BelongsTo `Employee`; `contractType()` BelongsTo `ContractType`.

### `EmployeeSalaryProfile` — table `hr_employee_salary_profiles`

**Fillable**: `employee_id`, `cost_center_id`, `payroll_posting_profile_id`, `basic_salary`, `payment_method`, `payroll_cycle`, `effective_from`, `effective_to`, `is_current`, `notes`, `created_by`, `updated_by`.

**Casts**: `basic_salary` → decimal:2; `effective_from`, `effective_to` → date; `is_current` → boolean.

**Relationships**: `employee()`, `costCenter()`, `payrollPostingProfile()`, `allowances()` HasMany `EmployeeAllowance`, `deductions()` HasMany `EmployeeDeduction`.

### `EmployeeAllowance` / `EmployeeDeduction` (not shown in detail)

Both belong to a salary profile via `salary_profile_id`. Both have `name` (translatable JSON), `calculation_type` (`fixed` or `percentage`), `value`, `effective_from`, `effective_to`.

### `PayrollRun` — table `hr_payroll_runs`

**Fillable**: `code`, `payroll_period_id`, `branch_id`, `payroll_posting_profile_id`, `run_type`, `status`, `payment_status`, `approved_by`, `processed_at`, `approved_at`, `posted_at`, `paid_at`, `notes`, `created_by`, `updated_by`.

**Casts**: `processed_at`, `approved_at`, `posted_at`, `paid_at` → datetime.

**Relationships**: `payrollPeriod()` BelongsTo `PayrollPeriod`; `branch()` BelongsTo `Branch`; `payrollPostingProfile()` BelongsTo `PayrollPostingProfile`; `approvedBy()` BelongsTo `Admin`; `lines()` HasMany `PayrollLine`; `salaryPayments()` HasMany `SalaryPayment`; `postings()` HasMany `PayrollPosting`.

### `PayrollPeriod` — table `hr_payroll_periods`

**Fillable**: `code`, `period_year`, `period_month`, `start_date`, `end_date`, `payment_date`, `status`, `notes`, `created_by`, `updated_by`.

**Casts**: `period_year`, `period_month` → integer; `start_date`, `end_date`, `payment_date` → date.

**Relationships**: `runs()` HasMany `PayrollRun`.

### `PayrollLine` — table `hr_payroll_lines`

**Fillable**: `payroll_run_id`, `employee_id`, `salary_profile_id`, `cost_center_id`, `basic_salary`, `total_earnings`, `total_deductions`, `net_salary`, `status`, `is_free_salary`, `notes`, `created_by`, `updated_by`.

**Casts**: all money fields → decimal:2; `is_free_salary` → boolean.

**Relationships**: `payrollRun()`, `employee()`, `salaryProfile()`, `costCenter()`, `lineItems()` HasMany `PayrollLineItem`, `salaryPayments()` HasMany `SalaryPayment`.

### `PayrollLineItem` — table `hr_payroll_line_items`

Stores individual earnings/deduction components. Key columns: `payroll_line_id`, `item_type` (basic_salary, allowance, bonus, deduction, loan, leave_deduction), `item_category` (earning, deduction), `reference_id`, `name` (JSON), `quantity`, `rate`, `amount`, `affects_net_salary`, `calculation_type`.

### `EndOfServiceSettlement` — table `hr_end_of_service_settlements`

**Fillable**: `settlement_code`, `employee_id`, `branch_id`, `resignation_request_id`, `payroll_posting_profile_id`, `journal_entry_id`, `last_working_date`, `settlement_date`, `settlement_reason`, `service_years`, `gross_amount`, `deduction_amount`, `net_amount`, `status`, `payment_status`, `approved_by`, `approved_at`, `posted_at`, `paid_at`, `notes`, `created_by`, `updated_by`.

**Casts**: dates → date; amounts → decimal:2; timestamps → datetime.

**Relationships**: `employee()`, `branch()`, `resignationRequest()` BelongsTo `ResignationRequest`, `payrollPostingProfile()`, `journalEntry()` BelongsTo `JournalEntry`, `approvedBy()` BelongsTo `Admin`.

### `RequestWorkflow` — table `hr_request_workflows`

**Fillable**: `request_type`, `is_active`, `created_by`, `updated_by`.

**Casts**: `is_active` → boolean.

**Relationships**: `steps()` HasMany `RequestWorkflowStep` ordered by `sequence_order`.

### `RequestWorkflowStep` — table `hr_request_workflow_steps`

**Fillable**: `request_workflow_id`, `sequence_order`, `approval_type`, `role_id`, `manager_level`.

**Casts**: `sequence_order`, `manager_level` → integer.

**Relationships**: `workflow()` BelongsTo `RequestWorkflow`; `role()` BelongsTo `Roles`.

### `Approval` — table `hr_approvals`

**Fillable**: `request_type`, `request_id`, `request_workflow_id`, `requester_employee_id`, `status`, `current_sequence_order`, `submitted_at`, `completed_at`, `created_by`, `updated_by`.

**Casts**: `current_sequence_order` → integer; `submitted_at`, `completed_at` → datetime.

**Relationships**: `workflow()` BelongsTo `RequestWorkflow`; `requesterEmployee()` BelongsTo `Employee`; `steps()` HasMany `ApprovalStep` ordered by `sequence_order`.

Note: `Approval` uses a polymorphic-like approach without Eloquent morphs. The pair `(request_type, request_id)` identifies the source record. The service resolves the correct Eloquent class via a `match` expression.

### `ApprovalStep` — table `hr_approval_steps`

**Fillable**: `approval_id`, `workflow_step_id`, `sequence_order`, `approval_type`, `approver_role_id`, `approver_employee_id`, `acted_by_admin_id`, `status`, `action_notes`, `acted_at`.

**Casts**: `sequence_order` → integer; `acted_at` → datetime.

**Relationships**: `approval()`, `workflowStep()` BelongsTo `RequestWorkflowStep`, `role()` BelongsTo `Roles`, `approverEmployee()` BelongsTo `Employee`, `actorAdmin()` BelongsTo `Admin`.

Step status values: `pending`, `waiting`, `approved`, `rejected`, `skipped`.

### `LeaveRequest` — table `hr_leave_requests`

**Fillable**: `request_code`, `employee_id`, `branch_id`, `leave_type_id`, `start_date`, `end_date`, `requested_days`, `is_half_day`, `half_day_session`, `reason`, `status`, `current_approval_step`, `approved_at`, `rejected_at`, `created_by`, `updated_by`.

**Casts**: dates → date; `requested_days` → decimal:2; `is_half_day` → boolean; `approved_at`, `rejected_at` → datetime.

**Relationships**: `employee()`, `branch()`, `leaveType()` BelongsTo `LeaveType`, `attachments()` HasMany `RequestAttachment` filtered by `where('request_type', 'leave')`, `approval()` HasOne `Approval` filtered by `where('request_type', 'leave')`.

All other request models (`LoanRequest`, `BonusRequest`, `PermissionRequest`, `DataChangeRequest`, `ComplaintRequest`, `ResignationRequest`) follow the same pattern: polymorphic-like attachment and approval relations scoped by `request_type`.

### `JobRequest` — table `hr_job_requests`

**Fillable**: `code`, `branch_id`, `department_id`, `designation_id`, `requested_count`, `status`, `notes`.

**Relationships**: `branch()`, `department()`, `designation()`, `candidates()` HasMany `Candidate`.

### `Candidate` — table `hr_candidates`

```php
use HasTranslations;
```

**Translatable**: `['name']`.

**Fillable**: `code`, `job_request_id`, `branch_id`, `designation_id`, `name`, `email`, `phone`, `status`, `source`, `notes`.

**Relationships**: `jobRequest()`, `branch()`, `designation()`, `documents()` HasMany `CandidateDocument`, `interviews()` HasMany `Interview`, `jobOffers()` HasMany `JobOffer`.

### Setup Models

All setup models (`Department`, `Designation`, `Grade`, `ContractType`, `LeaveType`, `PermissionType`, `LoanType`, `BonusType`, `PenaltyType`, `EarningType`, `DeductionType`) share the same structure:

- **Translatable fields**: `['name']`
- **Fillable**: `code`, `name`, `is_active`, `created_by`, `updated_by`
- **payroll_affects flag** (boolean, present on `LeaveType`, `LoanType`, `BonusType`, `PenaltyType`, `EarningType`, `DeductionType`): controls whether items of this type are pulled into payroll calculations.
- **Table names**: `hr_departments`, `hr_designations`, `hr_grades`, `hr_contract_types`, `hr_leave_types`, `hr_permission_types`, `hr_loan_types`, `hr_bonus_types`, `hr_penalty_types`, `hr_earning_types`, `hr_deduction_types`.

---

## Spatie Translatable — Fields Using `HasTranslations`

The following models use `spatie/laravel-translatable` (`HasTranslations` trait) with their `$translatable` declarations:

| Model | Table | Translatable field(s) |
|-------|-------|-----------------------|
| `Employee` | `hr_employees` | `name` |
| `Candidate` | `hr_candidates` | `name` |
| `Department` | `hr_departments` | `name` |
| `Designation` | `hr_designations` | `name` |
| `Grade` | `hr_grades` | `name` |
| `ContractType` | `hr_contract_types` | `name` |
| `LeaveType` | `hr_leave_types` | `name` |
| `LoanType` | `hr_loan_types` | `name` |
| `BonusType` | `hr_bonus_types` | `name` |
| `PenaltyType` | `hr_penalty_types` | `name` |
| `EarningType` | `hr_earning_types` | `name` |
| `DeductionType` | `hr_deduction_types` | `name` |
| `PermissionType` | `hr_permission_types` | `name` |
| `EmployeeAllowance` | `hr_employee_allowances` | `name` |
| `EmployeeDeduction` | `hr_employee_deductions` | `name` |

The `name` column is stored as a JSON object (`{"en":"English value","ar":"Arabic value"}`). Payroll calculation reads translations via `getTranslations('name')` to produce the bilingual `name` array on each `PayrollLineItem`.

Searching translatable names in Eloquent queries uses the JSON column path syntax:
```php
->orWhere('name->ar', 'like', "%{$search}%")
->orWhere('name->en', 'like', "%{$search}%")
```

---

## Payroll Calculation — Step-by-Step Reference

```
PayrollRunController::process()
  └── PayrollRunService::process()
        ├── PayrollCalculationService::buildRunPayloads($payrollRun)
        │     └── for each eligible Employee:
        │           PayrollCalculationService::calculateLine($payrollRun, $employee, $period)
        │             ├── resolveSalaryProfile() → EmployeeSalaryProfile (most recent effective)
        │             ├── buildAllowanceItems()  → fixed/percentage per allowance line
        │             ├── buildBonusItems()       → BonusRequest query (approved, payroll_affects, within period)
        │             ├── buildDeductionItems()   → fixed/percentage per deduction line
        │             ├── buildLoanItems()        → LoanRequest query (approved, payroll_affects, deduction_start <= end_date)
        │             ├── buildLeaveDeductionItems() → LeaveRequest query (approved, payroll_affects, overlapping period)
        │             └── returns: { employee_id, basic_salary, total_earnings, total_deductions, net_salary, items[] }
        └── PayrollLineService::replaceForRun($payrollRun, $linePayloads)
              └── deletes existing lines, bulk-inserts new PayrollLine + PayrollLineItem rows
```

**Net salary formula**: `net_salary = basic_salary + total_earnings - total_deductions`

**Daily rate for leave deductions**: `basic_salary / (days_in_period)` where `days_in_period = end_date.diffInDays(start_date) + 1`, minimum 1.

**Salary profile resolution**: ordered by `is_current DESC`, `effective_from DESC`. Only profiles whose `effective_from <= period.end_date` AND (`effective_to IS NULL` OR `effective_to >= period.start_date`) qualify.

---

## Payroll Finance Mapping — Account Key Reference

The `PayrollPostingProfile` model holds FK columns for each GL account. The `PayrollFinanceMappingService` maps these keys:

| Mapping key | Profile column | Entry direction |
|-------------|---------------|----------------|
| `basic_salary` | `basic_salary_account_id` | Debit |
| `allowances` | `allowance_account_id` | Debit |
| `earnings` | `earning_account_id` | Debit |
| `deductions` | `deduction_account_id` | Credit |
| `loans` | `loan_receivable_account_id` | Credit |
| `payroll_payable` | `payroll_payable_account_id` | Credit (payroll run) / Debit (EOS payment) |
| `bank_cash` | `bank_cash_account_id` | Credit (EOS payment) |
| `eos` | `eos_account_id` | Debit (EOS accrual) |

Lines with the same `account_id + cost_center_id` pair are collapsed into a single journal entry line before posting.
