# Work From Home (WFH) Request Feature

## Context

Field/office staff currently must physically punch in inside a configured geofence
(`LocationFencing`, checked in `AttendanceController::validateLocationFence()`), or hold the
blanket `report_management.attendance_location.override` permission to bypass it entirely. There
is no per-day, approval-gated way for a manager to let a specific employee work from home on a
specific date range. The ask is to add a **Work From Home request** workflow — modeled directly on
the existing Leave request/approval flow (`LeaveController`, `LeaveApplication`, `LeaveApproval`)
— so that:

1. A user requests WFH for a date range, naming an approver (mirrors `leave.application.create`).
2. The approver accepts or rejects it (mirrors `leave.application.approve` / `.reject`).
3. Once approved, attendance for those days is auto-marked **Present (Work From Home)** instead of
   requiring a normal office punch, and the geofencing check is bypassed for that user on those
   specific dates only (not a blanket override permission) when they do check in from home.

There is no existing "Exception" request feature in this codebase (confirmed via full-repo
search) — the user's phrasing "exception from geolocation fencing" refers to the WFH day being an
*exception to* the fencing rule, not a second, separate approval workflow. This plan builds one
new workflow only.

## Approach

Mirror the Leave feature's structure and conventions as closely as possible so the codebase stays
consistent (`app/Http/Controllers/LeaveController.php`, `app/Models/LeaveApplication.php` /
`LeaveApproval.php`, `database/migrations/..._leave_applications_table.php` /
`..._leave_approvals_table.php`, `routes/app.php:500-517`).

### 1. Migrations

- `create_wfh_requests_table`: `user_id` (FK users), `applied_to` (FK users, the approver),
  `start_date`, `end_date`, `total_days` (int), `reason` (text, nullable), `status` (enum
  `pending|approved|rejected`, default `pending`), `attachment` (string, nullable), timestamps.
  Directly mirrors `leave_applications` minus `leave_type_id` (no leave type concept for WFH).
- `create_wfh_approvals_table`: `wfh_request_id` (FK, cascade), `approver_id` (FK users),
  `status` (enum `approved|rejected`), `approver_remarks` (text, nullable), `actioned_at`
  (timestamp, nullable), timestamps. Mirrors `leave_approvals`.
- `add_wfh_columns_to_attendances_table`: add `is_wfh` (boolean, default false) and
  `wfh_request_id` (FK → `wfh_requests`, nullable, `nullOnDelete`) to `attendances`. This is the
  attendance-side marker read by `getAttendanceStatusAttribute()` / `FormatsEntityData` and by the
  check-in fence bypass.

### 2. Models

- `app/Models/WfhRequest.php` — fillable per migration; relations `user()`, `appliedTo()`
  (`belongsTo(User::class, 'applied_to')`), `approval()` (`hasOne(WfhApproval::class)`). Mirrors
  `LeaveApplication`.
- `app/Models/WfhApproval.php` — relations `wfhRequest()`, `approver()`. Mirrors `LeaveApproval`.
- `app/Models/Attendance.php` — add `is_wfh`, `wfh_request_id` to `$fillable`, `is_wfh` to
  `$casts` (boolean), add `wfhRequest(): BelongsTo` relation. Update
  `getAttendanceStatusAttribute()` to add a branch: WFH takes priority over the plain "Present"
  label but after holiday/weekoff/absent, e.g. insert `if ($this->is_wfh) return 'Present (Work From Home)';`
  before the existing `if ($this->is_present) return 'Present';` line.

### 3. Attendance status derivation (`app/Traits/FormatsEntityData.php`)

`getAttendanceStatus()` currently returns short machine-readable strings (`present`, `leave`,
`absent`, …). Add a `wfh` branch: after the `leave_type_id` check and before the plain `is_present`
check, `if ($attendance->is_wfh) return 'wfh';`. Downstream consumers (mobile/web) get a new
distinct status value rather than overloading `present`. Also confirm `formatAttendanceData()`
exposes `is_wfh` in its returned array (add the key) so the UI can render the "Present (WFH)"
badge without re-deriving it from `status === 'wfh'` alone.

### 4. Permissions (`app/Support/PermissionCatalog.php`)

Add a new module, following the `leave_management` pattern exactly:

```php
'wfh_management' => [
    'wfh_request' => $crud, // create, read, update, delete, override
],
```

Controller checks follow the same `$user->can('wfh_management.wfh_request.create')` /
`.override` pairing used throughout `LeaveController`. No new fence-related permission is needed —
the bypass is per-approved-request, not a standing grant (see §6).

### 5. `WfhController` (new, `app/Http/Controllers/WfhController.php`)

Mirror `LeaveController`'s structure and inject `AttendanceBackfillService` the same way
(`__construct(private readonly AttendanceBackfillService $backfillService)`). Endpoints:

- `create_wfh_request` — validates `applied_to` (exists:users), `start_date`, `end_date`
  (`after_or_equal:start_date`), `reason` (required), `attachment` (nullable file). Computes
  `total_days` using the same `UserWorkingDay` logic copied from
  `LeaveController::create_leave_application` (lines 536-563). No entitlement-balance check (WFH
  isn't rationed like leave). Creates `WfhRequest` with `status` defaulting to `pending`. Notifies
  `applied_to` + all `super_admin` users via `notifyUser` (same recipient-merge pattern, lines
  622-648).
- `update_wfh_request` — only while `status === 'pending'`, mirrors
  `update_leave_application`.
- `approve_wfh_request` — permission `wfh_management.wfh_request.update`/`.override`; restrict to
  the named `applied_to` approver unless `super_admin`/`.override` (same `when()` pattern as
  `approve_leave_application` lines 796-816); guard on `status === 'pending'` and no existing
  `WfhApproval`. In `DB::transaction()`: create `WfhApproval` (`status: approved`), update
  `WfhRequest.status = 'approved'`, then call a new
  `AttendanceBackfillService::applyApprovedWfh($user, $start, $end, $wfhRequestId)` (see §7).
  Send push via a new `notifyWfhAction($applicant, $wfhRequest, 'approved')`.
- `reject_wfh_request` — same guard pattern as `reject_leave_application`; creates rejected
  `WfhApproval`, updates request `status = 'rejected'`, notifies applicant. No attendance
  side-effects.
- `fetch_pending_approvals` / `fetch_approved_requests` / `fetch_rejected_requests` (own requests)
  and `fetch_wfh_requests` (subordinate requests visible to an approver, excludes own) — copy the
  four equivalent Leave fetch endpoints verbatim, swapping the model/relations.

Response formatting: a private `wfh_request_data_format()` mirroring
`leave_application_data_format()` (drop the `leaveType` key, keep `user`/`appliedTo`/`approval`).

### 6. Push notifications (`app/Traits/SendsPushNotifications.php`)

Add `notifyWfhAction(User $user, $wfhRequest, string $action): bool`, copied from
`notifyLeaveAction()` (lines 124-142) with `leave_` → `wfh_` type prefixes and WFH-specific
copy ("Your work from home request from {start} to {end} has been approved/rejected").

### 7. Attendance backfill / auto-present (`app/Services/AttendanceBackfillService.php`)

- New method `applyApprovedWfh(User $user, string $startDate, string $endDate, int $wfhRequestId): void`
  — iterates the range like `applyApprovedLeave()`, calling a new private `applyWfhForDate()`.
- `applyWfhForDate()` mirrors `applyLeaveForDate()`: skip if the date is a holiday or week-off
  (same priority: holiday/weekoff always win). Otherwise
  `Attendance::updateOrCreate(['user_id', 'attendance_date'], ['shift_id' => ..., 'is_present' => true, 'is_absent' => false, 'is_holiday' => false, 'is_weekoff' => false, 'is_halfday' => false, 'leave_type_id' => null, 'is_wfh' => true, 'wfh_request_id' => $wfhRequestId, 'remarks' => 'Work From Home'])`.
  Unlike leave (which sets `is_present = false`), WFH sets `is_present = true` immediately on
  approval — the day counts as present without requiring a punch, per the requirement ("update the
  attendance to show its present but work from home"). Actual check-in from home (if the user
  chooses to punch in) is layered on top the same way `checkIn()` already layers onto approved-leave
  rows.
- `determineStatus()` (used by the nightly/on-demand backfill for days that don't yet have an
  attendance row) needs a new branch: query approved `WfhRequest` covering the date the same way
  the existing leave-application query does (lines 179-195), placed **before** the leave check
  (WFH and leave are mutually exclusive by construction — a user wouldn't hold both for the same
  day — so ordering only matters for defensive correctness) and **after** holiday/weekoff. Return
  `is_present => true, is_wfh => true, wfh_request_id => ..., remarks => 'Work From Home'`.

### 8. Geofencing bypass (`app/Http/Controllers/AttendanceController.php`)

In `checkIn()` and `checkOut()`, before calling `validateLocationFence()`, look up today's
attendance row (both methods already do this inside the transaction, but the fence check happens
earlier/outside it — reorder minimally): fetch
`Attendance::where('user_id', $user->id)->where('attendance_date', $today)->value('is_wfh')`
(cheap scalar lookup) and skip the fence check when true:

```php
$isApprovedWfhToday = Attendance::where('user_id', $user->id)
    ->where('attendance_date', $today)
    ->value('is_wfh');

if (! $isApprovedWfhToday && ($locationError = $this->validateLocationFence($user, (float) $lat, (float) $lng))) {
    return $this->error($locationError, 422);
}
```

This bypasses the fence **only** for a user who has an approved WFH request covering today — not a
standing permission grant, matching the requirement that this be per-request, not blanket like
`report_management.attendance_location.override`. `outside_punchin`/`outside_punchout` should NOT
be set to `1` for WFH punches (that flag means "outside fence but authorized via general
override"); leave those `0` and rely on `is_wfh` for the UI to explain the location result.

Update `FormatsEntityData::resolveFenceLocationName()` (or `formatAttendanceData()` which calls
it) to short-circuit to `'Work From Home'` when `$attendance->is_wfh` is true, instead of running
haversine distance checks against configured fences (which would otherwise mislabel a home
location as "Unauthorized place").

### 9. Routes (`routes/app.php`)

Add a new `Route::prefix('wfh')->group(...)` block right after the existing `leave` group
(after line 517), registering `WfhController` methods 1:1 with the `leave/*` route shape:

```
/wfh/request/create
/wfh/request/update
/wfh/request/approve
/wfh/request/reject
/wfh/request/pending/fetch
/wfh/request/approved/fetch
/wfh/request/rejected/fetch
/wfh/request/requests/fetch
```

Add `use App\Http\Controllers\WfhController;` near the existing `LeaveController` import.

### 10. Documentation updates (required same-turn per CLAUDE.md)

- `docs/business-rules.md` — new "### Work From Home Management" section mirroring the existing
  "### Leave Management" block, plus a note under "Attendance Rules" that `is_wfh` days are
  auto-marked present on approval and bypass geofencing only for that user/date.
- `docs/database.md` — add `wfh_requests`, `wfh_approvals` rows to the HR & Attendance table, and
  update the `attendances` row to mention `is_wfh`, `wfh_request_id`.
- `docs/api.md` — document the eight new `/wfh/*` routes (request/response shape mirrors the
  existing `/leave/application/*` entries).
- `docs/architecture.md` — note the new `WfhController` + `AttendanceBackfillService::applyApprovedWfh()`.

## Verification

- `php artisan migrate` locally to confirm the three new migrations run cleanly against existing
  data (no column collisions — confirmed `attendances` has no `is_wfh`/`wfh_request_id` today).
- Manual API walkthrough with a test user/approver pair via REST client (or `php artisan tinker`):
  create a WFH request → approve it → confirm `attendances` row for those dates shows
  `is_present=1, is_wfh=1` without any check-in call → confirm `/attendance/today/own/fetch`
  reports status `wfh`/"Present (Work From Home)".
- Call `/attendance/check_in` with lat/lng far outside any configured `LocationFencing` on an
  approved WFH day and confirm it succeeds (bypass working) — then confirm the same call fails for
  a day *without* an approved WFH request (regression check that the existing fence logic for
  everyone else is untouched).
- Run existing test suite, in particular `tests/Feature/` attendance/leave tests if present, to
  confirm no regression in `checkIn`/`checkOut`/leave approval flows.
