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 byLeaveController(app/Http/Controllers/LeaveController.php, 2,579 lines),HolidayController, andEmployeeOffDaysController. - 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 byHrAttendanceController(app/Http/Controllers/Admin/HrAttendanceController.php, 2,385 lines),HrDeviceController,HrManualAttendanceController, and the batch engineAttendanceService(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 == 1bypasses every gate (sidebarhr.blade.phpthroughout; 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 andisset($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-requestsmodel):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 bycan('approve','attendance-remote-clocking')(HrAttendanceController:155,200); devices bycan('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
attendancesrows 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): permissioncreate; loadsLeaveType+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 flatmax_days_per_yearminus 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, stampsbranch_approved_by,branch_approval_reason. Onlypendingmay be branch-approved.hqApproveLeave()(:254):pending/branch_approved → hq_approved, stampshq_approved_by; ifstart_dateis today, immediately flips toon_leave(:283-285).rejectLeave()(:300): onlypending/branch_approved; requiresrejection_reason.- Transitions:
markOnLeave,markCompleted,markReturn,cancelLeave,resubmitLeave, plusarchiveLeave/deleteLeave(hr.php:110-117). - Balance ledger:
LeaveTransactionsis a running-balance ledger (recordLeaveDebit/recordLeaveCredit/recordCarryover, each writingbalance_before/balance_after), andgetEmployeeLeaveBalance= 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 inallowed_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 theallowed_attendance_statusespayroll setting.LeaveTypehas nois_paid/unpaidcolumn (create_leave_types_table.php), andPayrollCalculationServicedoes not readleavesat all — it only readsprocessed_attendance. So "unpaid leave" is achieved by omittingLeavefrom the allowed statuses (or the day beingAbsent), not by a per-leave-type paid/unpaid switch. SimilarlyOff Dayis paid or not purely by its presence in that list. - Casuals. The same
attendancestable serves casuals (Attendance::casual()join onid_no). Casual attendance is surfaced viaHrAttendanceController::casuals/RtCasualAttendanceandCasualLimit. 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 sharedattendances/processed_attendancetables keyed byemp_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→attendancesorremote_clocking_requests) vs manual bulk upload (HrManualAttendanceControllerupload/reconcile/approve,hr.php:524-536). REQUIRE_REMOTE_CLOCKING_APPROVAL(setting()): off = remote punches record directly; on = they bundle into a pendingremote_clocking_requestsrow 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 viaLeaveTransactions::calculateAccruedAnnualLeave), unlimited (is_unlimitedshort-circuits the balance check,LeaveController:428), and carry-forward (carries_forward+max_carry_forward_days, realised ascarryoverledger entries). - HQ approval optionality:
leaves.hq_approval_requireddefaultstruebut is a per-row boolean, andhqApproveLeaveacceptspendingdirectly — so a tenant/flow can collapse to single-tier approval (HQ approves a still-pendingrequest) or run the full two-tier chain. - Holiday scope:
applies_to_all_branches(global) vsholiday_branchpivot (branch-specific);is_recurringfor annual holidays. - ZKTeco dual endpoints:
ZKTECO_API_BASE_URL(default41.90.112.98:82) andZKTECO_API_BASE_URL_2(default41.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 ofPresent/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.