Skip to content

Chapter 25 — Leave & Attendance

Book 6 (HR & Payroll). Scope: the two sidebar groups Leave Management and Time & Attendance under resources/views/admin/includes/sidebar_includes/hr.blade.php. Employee master and payroll runs are Chapter 23; deductions/statutory are Chapter 24; Sacco is Chapter 26. This chapter also traces the seams where leave and attendance feed the payroll run.


1. Purpose

Plain language. This module answers two everyday HR questions: "Who is allowed to be away from work, and did they get permission?" (Leave Management) and "Who actually showed up, and when?" (Time & Attendance). Staff apply for leave, managers approve or reject it, and the people going on leave hand over their duties to a colleague. Separately, the system records clock-in/clock-out "punches" — either from physical fingerprint/face terminals mounted at each branch, or from a mobile app that lets staff clock in remotely — and turns raw punches into a daily verdict per person: Present, Late, Absent, Off Day, or on Leave. That daily verdict is what payroll later reads to decide how much of the month each person actually worked.

Technical. Two route groups under the admin/hr prefix in routes/modules/hr.php:

  • Leave Management (routes/modules/hr.php:85-138) — holidays CRUD, a leave-request lifecycle with a two-tier maker-checker workflow (branch approval → HQ approval), handover assignments, and ad-hoc employee off-days. Driven by LeaveController (app/Http/Controllers/LeaveController.php, 2,579 lines), HolidayController, and EmployeeOffDaysController.
  • Time & Attendance (routes/modules/hr.php:524-550, 731-732+) — remote-login requests, remote-clocking approvals, employee/casual attendance viewers, a real-time board, biometric device management, and a manual-attendance reconciliation sub-module. Driven by HrAttendanceController (app/Http/Controllers/Admin/HrAttendanceController.php, 2,385 lines), HrDeviceController, HrManualAttendanceController, and the batch engine AttendanceService (app/Services/AttendanceService.php, 1,999 lines).

The processing pipeline is: raw punches land in attendances → AttendanceService collapses them per employee/day into processed_attendance rows carrying an attendance_status → PayrollCalculationService reads processed_attendance to prorate pay (§5).


2. Users & roles

Plain language. Access is gated three ways: the tenant super-admin (role 1) sees everything; users with specific named permissions see specific screens; and there is a special "HR access" flag that grants a manager HR visibility even without the granular permissions. Approving leave is deliberately split — a branch-level approver signs off first, then an HQ approver gives final sign-off — so no single person both requests and fully approves.

Technical. Guard patterns seen across the sidebar and controllers:

  • Super admin: $logged_user_info->role_id == 1 bypasses every gate (sidebar hr.blade.php throughout; controllers use || $user->role_id == 1).
  • HR-access shortcut: $logged_user_info->hasHrAccess() / auth()->user()->hasHrAccess() unlocks off-days, casuals attendance, employee attendance, and realtime even without granular permissions (hr.blade.php:628,673,683,693; HrAttendanceController::casuals/realtime; EmployeeOffDaysController::index).
  • Granular permissions via the can($ability, $permissionSlug) helper and isset($my_permissions[...]) sidebar checks. Key slugs:
  • Leave view: hr-and-leave-management-pending-requests___view, -leave-requests___view, -rejected-requests___view, -employees-on-leave___view, -handover-details___view, -holidays___view, -off-days___view.
  • Leave actions (fine-grained abilities on the hr-and-leave-management-pending-requests model): create, branch-approve, hq-approve, reject, manage (LeaveController::store/approveLeave/hqApproveLeave/rejectLeave, lines 212, 254, 300, 384).
  • Attendance: attendance-remote-clocking___view, attendance-employee-records___view, attendance-casual-records___view, attendance-realtime-records___view, attendance-devices___view; remote-clocking approve gated by can('approve','attendance-remote-clocking') (HrAttendanceController:155,200); devices by can('view','attendance-devices') (HrDeviceController:36).

Maker-checker separation. LeaveController::approveLeave requires branch-approve and only moves pending → branch_approved; hqApproveLeave requires hq-approve and moves pending/branch_approved → hq_approved (LeaveController:212-297). The show() method further restricts HQ approval to users where $user->is_hq_user || role_id == 1 (LeaveController:346-350). Branch scoping: non-HQ users are constrained to their own branch — whereHas('employee', fn($q) => $q->where('branch_id', $user->restaurant_id)) recurs across pending/leave/rejected/on-leave/off-day listings (e.g. LeaveController:56,729,992,1426; EmployeeOffDaysController:34).

Approvers-not-curators note. Leave approval, remote-clocking approval, remote-login grants, and manual-attendance approvals are all pure boolean sign-offs — an approver flips a status and optionally leaves a comment. They do not verify the underlying facts (was the person really sick? was the remote punch really at the workplace?). The remote-clocking flow is the sharpest example: an approver's click creates real attendances rows out of self-reported punches (§3), which then flow untouched into payroll proration. There is no geofence or evidence gate in code (§7).


3. Processes

3.1 Leave request lifecycle

Plain language. An HR user (or the employee's manager) fills in a leave request: employee, leave type, dates, reason, optional salary advance, and optional handover to a colleague. The system checks the dates don't overlap an existing request and that the person has enough leave balance. The request starts pending. A branch approver signs off (branch_approved), then an HQ approver gives final approval (hq_approved). When the start date arrives the person is marked on_leave; when they come back they are marked completed. Anyone in the chain can reject with a reason, and rejected requests can be resubmitted.

stateDiagram-v2
    [*] --> pending: store() — balance & overlap checks
    pending --> branch_approved: approveLeave() (branch-approve)
    pending --> hq_approved: hqApproveLeave() (hq-approve)
    branch_approved --> hq_approved: hqApproveLeave() (hq-approve)
    pending --> rejected: rejectLeave() (reject + reason)
    branch_approved --> rejected: rejectLeave()
    hq_approved --> on_leave: markOnLeave() / auto if start_date is today
    on_leave --> completed: markCompleted() / markReturn()
    on_leave --> recalled: (recalled flag)
    rejected --> pending: resubmitLeave()
    hq_approved --> [*]
    completed --> [*]

Technical. Status enum is fixed at the DB level: pending, branch_approved, hq_approved, rejected, on_leave, recalled, completed (create_leaves_table.php:44-52, default pending).

  • store() (LeaveController:384): permission create; loads LeaveType + Employee; rejects overlapping periods against statuses ['pending','branch_approved','hq_approved','on_leave'] (:394-425); enforces balance for capped leave types — Annual Leave uses accrual (LeaveTransactions::calculateAccruedAnnualLeave), all others use flat max_days_per_year minus used minus pending (:428-480); validates handovers (can't hand to self, recipient can't be on leave in the same window, :483+).
  • approveLeave() (:212): pending → branch_approved, stamps branch_approved_by, branch_approval_reason. Only pending may be branch-approved.
  • hqApproveLeave() (:254): pending/branch_approved → hq_approved, stamps hq_approved_by; if start_date is today, immediately flips to on_leave (:283-285).
  • rejectLeave() (:300): only pending/branch_approved; requires rejection_reason.
  • Transitions: markOnLeave, markCompleted, markReturn, cancelLeave, resubmitLeave, plus archiveLeave/deleteLeave (hr.php:110-117).
  • Balance ledger: LeaveTransactions is a running-balance ledger (recordLeaveDebit/recordLeaveCredit/recordCarryover, each writing balance_before/balance_after), and getEmployeeLeaveBalance = credits+allocation+carryover − debits ± adjustments (LeaveTransactions.php:48-121).

Handover. LeaveHandoverDetails (app/Models/LeaveHandoverDetails.php) carries handover_to_employee_id, handover_notes, handover_attachments (JSON array), and its own status lifecycle assigned → acknowledged / rejected → completed via acknowledge()/reject() (:47-66). Attachments are downloadable through downloadHandoverAttachment (hr.php:104). This is a mini maker-checker of its own: the receiving employee acknowledges or rejects the handover.

3.2 Clocking → processed attendance → payroll

Plain language. Punches arrive two ways: from biometric terminals (fingerprint/face/palm) pushed via the ZKTeco cloud API, or from the mobile app as remote punches. Depending on a tenant setting, remote punches either record immediately or wait for an approver. A batch engine then walks each employee day-by-day, decides the day's status against their shift (and honours leave/off-day/holiday overrides), and writes one processed_attendance row per shift per day. Payroll later counts how many working days scored an "attended" status.

flowchart TD
    A[Biometric terminal<br/>fingerprint/face/palm] -->|ZKTeco cloud API push| B[(attendances<br/>emp_code, punch_date, punch_time)]
    C[Mobile app remote punch] --> D{REQUIRE_REMOTE_<br/>CLOCKING_APPROVAL?}
    D -- no --> B
    D -- yes --> E[(remote_clocking_requests<br/>bundled punches, pending)]
    E -->|approveRemoteClockingRequest| B
    B --> F[AttendanceService::processAttendance]
    G[Leave on_leave] -.override.-> F
    H[EmployeeOffDay] -.override.-> F
    F --> I[(processed_attendance<br/>attendance_status per shift/day)]
    I --> J[PayrollCalculationService::calculateAttendance]
    J --> K[attendance_ratio prorates gross pay]

Technical — remote clocking (HrAttendanceController::remoteClocking, :240): JWT-authenticated (JWTAuth::toUser). If setting('REQUIRE_REMOTE_CLOCKING_APPROVAL', false) is falsy, a row is inserted straight into attendances (:260-272). If truthy, punches are bundled into a per-employee-per-day remote_clocking_requests row with status pending (:275-315). Approval (approveRemoteClockingRequest, :155) runs in a DB transaction, creates real Attendance rows from the stored punches JSON, stamps attendance_ids, then dispatches ProcessAttendanceJob for that day so the new punches reach processed_attendance (:168-198). Rejection just flips status (:200).

Technical — batch processing (AttendanceService::processAttendance, :33): pre-loads shifts, off-days (loadEmployeeOffDays) and active leaves (loadEmployeeLeaves), then per employee per date: 1. Leave wins over everything — if ($this->isOnLeave(...)) { buildOverrideRecord('Leave'); continue; } — punches for that day are ignored (:169-172). 2. Recorded off-day wins next — buildOverrideRecord('Off Day') (:174-178). 3. Otherwise resolve shift(s) for the day (static mode = assigned shift; dynamic mode = auto-assign by clock-in time) and emit a status: Present/Late/Absent/Off Day (processStaticMode:240, processDynamicMode:324). No shift assigned → Absent (:283); has shift but not scheduled today → Off Day (:264); cross-midnight shifts are completed by a separate 06:30 run (completePendingShifts:106).

Results are upserted into processed_attendance with manual-edit protection: rows carrying a previous_status are treated as manually edited and skipped unless overwriteManualEdits is set (ProcessedAttendance::scopeManuallyEdited; AttendanceService::upsertProcessedAttendance).

3.3 Remote login (system access) requests

Distinct from clocking: remote_login_requests records a request to let a user log into the ERP from outside (systemRemoteAccessRequests/...Store, HrAttendanceController:100-235). It stores user_id + requested_by and is created by an HR/admin on behalf of a user (RemoteLoginRequest::hasRequestToday enforces one-per-day). No approve/reject verbs — it is a grant record, not a maker-checker.


4. Tables touched & key data

Plain language. Leave data lives in a small cluster of tables (leaves and its ledger/handover children). Attendance splits into a raw table (attendances) and a computed table (processed_attendance). Biometric hardware lives in its own tiny hr_devices table — which is not the GPS tracker table nor the repair-device table (see disambiguation below).

Technical — Leave cluster - leaves (create_leaves_table.php): employee_id, leave_type_id, start_date, end_date, total_days (int), reason, status enum (§3.1), approval columns (branch_approved_by, hq_approved_by, hq_approval_required default true), rejection/approval reason columns, actual_return_date, actual_start_date, and an embedded salary-advance block (has_advance, salary_advance_id, advance_amount, advance_status enum pending|approved, finance_approver_id). Quirk: user FKs point at the legacy App\Model\User namespace while employee/leave-type point at App\Models\... (Leave.php:60-83) — the codebase straddles two model namespaces (app/Model and app/Models). - leave_types (create_leave_types_table.php, 2025-08-12): name, max_days_per_year, min_days_notice, carries_forward, max_carry_forward_days, active. Note two leave-type migrations exist — an old create_leave_type_table.php (2023, singular) and the current create_leave_types_table.php (2025); the live model App\Models\LeaveType uses leave_types. add_annual_leave_type.php seeds the accrual-driven "ANNUAL LEAVE" type special-cased in store(). - leave_transactions: running-balance ledger with transaction_type (debit|credit|allocation|carryover|adjustment), days, balance_before, balance_after. - leave_handover_details: handover_to_employee_id, handover_notes, handover_attachments (JSON), handover_status, assigned_at/acknowledged_at/completed_at, handover_employee_comments. - employee_off_days (create_employee_off_days_table.php, 2026-07-22): employee_id, off_date (cast date), reason, created_by. Ad-hoc, no approval workflow — an HR user records the off-day directly. - holidays + holiday_branch pivot (create_holidays_table.php, create_holiday_branch_table.php, both 2026-06-10): type, is_recurring, applies_to_all_branches; many-to-many to restaurants via holiday_branch. An older wa_holidays table also exists (2023) — legacy.

Technical — Attendance cluster - attendances (create_attendances_table.php, 2025-02-11, later modified/indexed): emp_code, punch_date, punch_time, punch_state_display, is_processed, processed_at. emp_code joins to Employee.id_no and Casual.id_no (Attendance.php:22-29) — the same raw table serves both staff and casuals. - processed_attendance (create_processed_attendance_table.php, 2025-03; heavily evolved): emp_code, date, employee_name, branch_id, shift_id, shift_sequence, shift_name, shift_start, shift_end, attendance_status (the load-bearing string: Present/Late/Absent/Off Day/Leave), early_punch, late_punch, requires_in_day_checkin, checkin_periods (JSON), pending_completion, previous_status/previous_shift_id (manual-edit tracking), reason, type. Unique key evolved from (emp_code,date) to include shift_sequence then shift_id (drop_processed_attendance_unique_constraint_add_shift_sequence.php, add_shift_id_to_processed_attendance_unique_key.php) to support multiple shifts per day. - remote_clocking_requests (2026-08-17): user_id, emp_code, punch_date, punches (JSON), status, reviewed_by, reviewed_at, review_remarks, attendance_ids (JSON of created rows). - remote_login_requests (2025-08-11): user_id, requested_by. - Manual attendance: ManualAttendanceRecord + ManualAttendanceReconciliation back the HrManualAttendanceController upload/reconcile/approve flow (hr.php:524-536).

Technical — HR device table disambiguation (required)

The hr.devices.index nav points to hr_devices via App\Models\HrDevice (create_hr_devices_table.php, 2025-07-14). Columns: branch_id, device_sn, device_name; belongsTo(Restaurant::class,'branch_id') (HrDevice.php). These are ZKTeco biometric clocking terminals — HrDeviceController is constructed with ZKTecoApiService and the index merges the local hr_devices name-map against live devices pulled from the ZKTeco cloud API, surfacing fp_count/face_count/palm_count (fingerprint/face/palm enrollment counts) (HrDeviceController:44-89).

This is a different table and model from: - Book 5 GPS trackers — App\Models\TrackingDevice / tracking_devices (fleet/telematics). - Repair devices — App\Models\Device / devices (the repair-shop device, with DeviceLog, DeviceRepair, DeviceType, DeviceSimCard).

So: three unrelated "device" concepts coexist. The HR nav here is strictly hr_devices (biometric terminals), keyed by device_sn.


5. Interactions with other modules

Plain language. This is the important seam: attendance and leave do change pay. Each month, payroll counts how many working days an employee scored an "attended" status in processed_attendance, divides by the number of working days in the period, and multiplies gross pay by that fraction. Because leave and off-days write override rows into processed_attendance, whether a leave type counts as "paid" comes down to one tenant setting listing which statuses count as attended. A salary advance can also be requested inside a leave application and pushed to Finance.

Technical — attendance → payroll proration (the core seam). PayrollCalculationService::calculateAttendance (app/Services/PayrollCalculationService.php:201-237):

$countAsAttended = PayrollSetting::where('name','allowed_attendance_statuses')->first()?->json_value ?? [];
$attendanceRecords = ProcessedAttendance::where('emp_code',$employee->id_no)
    ->whereBetween('date', [$payrollMonth->start_date, $payrollMonth->end_date])->get();
// group by day, keep only working weekdays, count a day if ANY record that day has an allowed status
$totalDays = payrollWorkingDaysInPeriod(...);
'attendance_ratio' => $totalDays > 0 ? round($attendedDays/$totalDays, 4) : 0.0

That attendance_ratio then prorates every gross bucket — NSSF, SHIF, housing-levy, PAYE, cash, and display gross are each multiplied by the ratio before statutory and net-pay math (PayrollCalculationService:77-135). The month detail is stamped with attendance_days, total_days, attendance_ratio. Consequences:

  • Absence reduces pay. Days marked Absent (or any status not in allowed_attendance_statuses) drop the ratio, cutting gross and therefore net pay. There is no separate "absence deduction" line — proration is the mechanism.
  • Unpaid vs paid leave is a config decision, not a code flag. Leave writes an override row with attendance_status = 'Leave' (§3.2). Whether that counts as a worked day depends entirely on whether 'Leave' appears in the allowed_attendance_statuses payroll setting. LeaveType has no is_paid/unpaid column (create_leave_types_table.php), and PayrollCalculationService does not read leaves at all — it only reads processed_attendance. So "unpaid leave" is achieved by omitting Leave from the allowed statuses (or the day being Absent), not by a per-leave-type paid/unpaid switch. Similarly Off Day is paid or not purely by its presence in that list.
  • Casuals. The same attendances table serves casuals (Attendance::casual() join on id_no). Casual attendance is surfaced via HrAttendanceController::casuals/RtCasualAttendance and CasualLimit. Casual pay-days linkage is via the shared attendance/processed-attendance data, but the casual pay computation itself lives outside this chapter's controllers (Ch23 payroll); confirmed linkage is the shared attendances/processed_attendance tables keyed by emp_code↔id_no (§7 flags the exact casual-pay formula as not traced here).

Technical — other seams. - Leave → Salary Advance / Finance: the leaves table embeds salary_advance_id, advance_status, finance_approver_id; Leave::salaryAdvance() belongs to SalaryAdvance. LeaveController::pendingAdvances/approvedAdvances (hr.php:118-119) route advance approvals to Finance — an advance requested with a leave becomes a payroll deduction downstream (Ch24). - Leave/attendance → Shifts: AttendanceService depends heavily on shift definitions (ShiftAssignmentValidator, static/dynamic modes, requires_in_day_checkin). Shifts are the schedule against which Present/Absent/Late is judged. - Attendance → ZKTeco cloud + Redis: biometric punches originate outside the DB via ZKTecoApiService (dual endpoints, Redis-cached tokens; §6). Reports: HrAttendanceReportController daily/weekly/monthly (hr.php). - Realtime board / Management dashboard: HrAttendanceController::realtimeViewData() is shared by the standalone realtime page and the HR management dashboard (:549), so attendance feeds a live ops view too.


6. Alternatives & variants

Plain language. The system flexes along several axes: clocking can be hardware or app-based; app clocking can require approval or not; shifts can be fixed or auto-detected; leave types can be capped, accrual-based, or unlimited; holidays can be global or branch-specific.

Technical — variants and tenant flags

  • Clocking source: biometric terminal (ZKTeco push → attendances) vs mobile remote clocking (remoteClocking → attendances or remote_clocking_requests) vs manual bulk upload (HrManualAttendanceController upload/reconcile/approve, hr.php:524-536).
  • REQUIRE_REMOTE_CLOCKING_APPROVAL (setting()): off = remote punches record directly; on = they bundle into a pending remote_clocking_requests row needing approval (HrAttendanceController:260,275).
  • Shift mode — static vs dynamic: AttendanceService::isDynamicModeEnabled() toggles between assigned-shift (static) and auto-assign-by-clock-in (dynamic) processing (:63-71,240,324). Dynamic mode specifically handles continuous night-shift workers (evening punches = clock-in, morning = clock-out).
  • Leave-type variants: capped (max_days_per_year + used/pending check), accrual ("ANNUAL LEAVE" special case via LeaveTransactions::calculateAccruedAnnualLeave), unlimited (is_unlimited short-circuits the balance check, LeaveController:428), and carry-forward (carries_forward + max_carry_forward_days, realised as carryover ledger entries).
  • HQ approval optionality: leaves.hq_approval_required defaults true but is a per-row boolean, and hqApproveLeave accepts pending directly — so a tenant/flow can collapse to single-tier approval (HQ approves a still-pending request) or run the full two-tier chain.
  • Holiday scope: applies_to_all_branches (global) vs holiday_branch pivot (branch-specific); is_recurring for annual holidays.
  • ZKTeco dual endpoints: ZKTECO_API_BASE_URL (default 41.90.112.98:82) and ZKTECO_API_BASE_URL_2 (default 41.90.112.99:82), auto-http-for-IP / https-for-domain, Redis-cached tokens (6h) — responses from both endpoints are merged (ZKTecoApiService:25-108,356). Suggests a two-server / redundant biometric backend.
  • Allowed attendance statuses: PayrollSetting 'allowed_attendance_statuses' (JSON) is the tenant lever defining which of Present/Late/Off Day/Leave/... count as a paid attended day (§5).

7. Open questions

Code-proven (facts established from source): - Absence reduces pay via attendance_ratio proration of all gross buckets — no separate absence-deduction line (PayrollCalculationService:77-135,201-237). - Paid vs unpaid leave is not a LeaveType column; it is determined solely by whether 'Leave' is in the allowed_attendance_statuses payroll setting (create_leave_types_table.php has no is_paid; PayrollCalculationService:203). - Leave and off-days override attendance for the day, ignoring punches (AttendanceService:169-178). - Remote-clocking approval fabricates real attendances rows from self-reported punches (HrAttendanceController:168-190). - HR devices = ZKTeco biometric terminals in hr_devices, distinct from tracking_devices and devices (HrDeviceController, HrDevice.php). - Two-tier leave maker-checker with branch→HQ statuses (LeaveController:212-297).

Inference / not traced here (needs confirmation): - Casual pay formula. The shared attendances/processed_attendance linkage is proven, but the exact casual pay-per-day computation (rate × attended-days) lives in Ch23 payroll code not read here — the precise formula is inferred, not confirmed. - No geofencing found. remoteClocking records only time_stamp — no latitude/longitude/geofence validation appears in the read paths. Geofencing seems absent, but I did not exhaustively search the mobile/API layer; treat "no geofence" as strongly-indicated inference. - Holiday → payroll effect. Holidays are stored and branch-scoped, but I did not find Holiday being read inside PayrollCalculationService or calculateAttendance (which uses payrollWorkingWeekdays()/payrollWorkingDaysInPeriod() helpers). Whether holidays reduce the working-day denominator lives inside those global helpers, which were not opened — flagged for follow-up. - Who submits remote punches. remoteClocking is JWT-authed (mobile app) but its route wasn't confirmed in routes/modules/hr.php (likely api.php); the exact client wasn't traced. - recalled lifecycle. The recalled status/flag and LeaveRecalls/LeaveReversal models (app/Model/) exist but their trigger paths weren't fully traced. - Namespace split risk. leaves FKs mix App\Model\User and App\Models\*; whether both resolve to the same users table at runtime is assumed, not verified.


8. Source references

Nav - resources/views/admin/includes/sidebar_includes/hr.blade.php:560-707 — Leave Management + Time & Attendance nav, permission slugs, hasHrAccess() gates.

Routes - routes/modules/hr.php:85-138 — leave-management (holidays CRUD, request lifecycle, handover, off-days). - routes/modules/hr.php:524-550 — manual attendance, casuals/employees viewers, remote-access, remote-clocking approve/reject, realtime, processAttendance. - routes/modules/hr.php:731-732+ — hr.devices.index/details/sync/refresh/test-connection/config/areas.

Controllers - app/Http/Controllers/LeaveController.php — store:384, approveLeave:212, hqApproveLeave:254, rejectLeave:300, show:342, handoverDetails:1158, employeesOnLeave:1407, balance calculateEmployeeLeaveBalance:671. - app/Http/Controllers/Admin/HrAttendanceController.php — systemRemoteAccessRequests:100, remoteClockingRequests:124, approveRemoteClockingRequest:155, rejectRemoteClockingRequest:200, remoteClocking:240, casuals:508, realtime:529, realtimeViewData:549, processAttendance:49. - app/Http/Controllers/Admin/HrDeviceController.php:15-120 — ZKTeco device index/details. - app/Http/Controllers/Admin/EmployeeOffDaysController.php:18-90 — off-days list/store, branch scoping. - app/Http/Controllers/HolidayController.php — holidays CRUD. - app/Http/Controllers/Admin/HrManualAttendanceController.php — manual upload/reconcile/approve.

Services / Jobs - app/Services/AttendanceService.php:33-232 — processing pipeline; leave/off-day overrides :169-178; static/dynamic modes :240,324; completePendingShifts:106. - app/Services/PayrollCalculationService.php:77-135 (proration), :201-237 (calculateAttendance) — the payroll seam. - app/Services/ZKTecoApiService.php:17-108,356,427-497 — dual-endpoint biometric cloud API. - app/Jobs/ProcessAttendanceJob.php — dispatched after remote-clocking approval.

Models - app/Models/Leave.php (namespace-split FKs :60-83), LeaveType.php, LeaveTransactions.php:48-121, LeaveHandoverDetails.php:47-66, EmployeeOffDay.php, Holiday.php, Attendance.php:22-29 (dual emp/casual join), ProcessedAttendance.php, RemoteClockingRequest.php, RemoteLoginRequest.php, HrDevice.php.

Migrations - create_leaves_table.php (status enum :44-52, advance block), create_leave_types_table.php, create_leave_transactions_table.php, create_leave_handover_details_table.php, create_employee_off_days_table.php, create_holidays_table.php + create_holiday_branch_table.php, create_attendances_table.php (+ modify/index), create_processed_attendance_table.php (+ unique-key evolution add_shift_id_to_processed_attendance_unique_key.php), create_hr_devices_table.php, create_remote_clocking_requests_table.php, create_remote_login_requests_table.php, add_annual_leave_type.php.