laraveleasyattendance maintained by easybdit
Laravel Easy Attendance — Laravel Attendance Management & ZKTeco Biometric Integration
A complete Laravel attendance management package for employee check-in and check-out, biometric attendance, ZKTeco device synchronization, shifts, leave management, holidays, overtime, attendance reports, salary generation, and payroll workflows. Built for modern Laravel applications that need a flexible employee attendance and HR solution.
Built for Laravel 11, Laravel 12, and Laravel 13 with PHP 8.2+, Laravel Easy Attendance provides a flexible attendance system that can work with your existing User model or its built-in Employee model.
It supports both ZKTeco Pull mode and Push/ADMS mode, making it suitable for offices, schools, factories, corporate HR systems, ERP applications, and other employee attendance environments.
Extracted and redesigned from a production HR system's attendance module, and validated end-to-end against real ZKTeco hardware (see Tested against real devices).
Contents
Why Laravel Easy Attendance?
Laravel Easy Attendance is designed as a reusable attendance and HR package for Laravel applications. It covers the complete flow from raw check-in/check-out punches to daily attendance summaries, leave, overtime, special working days, and salary generation.
Key features
- Employee check-in and check-out tracking
- Laravel attendance management with reusable Eloquent models and traits
- Biometric attendance integration with ZKTeco devices
- ZKTeco Pull mode over IP
- ZKTeco Push / ADMS synchronization
- Attendance correction requests and approval workflow
- Shift and employee schedule management
- Leave and holiday management
- Daily attendance summaries: present, late, absent, leave, holiday, and day off
- Overtime detection, approval, and salary calculation
- Special working-day pay
- Salary slip generation from attendance data
- Attendance and salary reports
- Laravel events for attendance, correction, leave, overtime, and device-sync workflows
- Configurable feature flags so you can enable only the modules you need
- Polymorphic attendance subjects for
User,Employee,Staff, or another model
Use cases
Laravel Easy Attendance can be used for:
- Employee attendance systems
- HR and payroll applications
- School and college staff attendance
- Office attendance management
- Factory and industrial workforce tracking
- Corporate ERP systems
- Biometric attendance systems
- ZKTeco attendance integrations
- Multi-purpose Laravel business applications
Requirements
-
PHP 8.2 or higher
-
Laravel 11, 12, or 13
-
MySQL, MariaDB, SQLite, or another supported Laravel database driver
-
HR & Payroll Core: Shifts, Leave, Holidays, Attendance Summaries, Overtime & Salary
Install
composer require easybdit/laraveleasyattendance
php artisan attendance:install
attendance:install publishes config/attendance.php and offers to run migrations. Everything works with zero config after that — the subject model defaults to your app's auth user model.
Quick start
Add the trait to whichever model represents "who checks in" (defaults to your auth user model):
use Easybdit\LaravelEasyAttendance\Traits\HasAttendance;
class User extends Authenticatable
{
use HasAttendance;
}
$user->checkIn();
$user->checkOut();
$user->attendanceOn('2026-09-07'); // ['first_in' => Carbon, 'last_out' => Carbon]
$user->attendances; // MorphMany<Attendance>
Or via the built-in routes (auth-protected, prefixed /attendance):
POST /attendance/check-in
POST /attendance/check-out
GET /attendance/today
Core concept: the subject model
Every other attendance package on Packagist hardcodes employee_id. This one doesn't — attendances.subject_id + subject_type is a polymorphic relation (morphs('subject')), so a punch can belong to User, Employee, Staff, or anything else you point it at:
// .env
ATTENDANCE_SUBJECT_MODEL=App\Models\Employee
Add HasAttendance to that model instead of User. Nothing else in the package needs to change — controllers, events, and device sync all resolve the subject model from config, never a hardcoded class.
How a punch is resolved
attendances is a punch log, not a one-row-per-day table — every check-in and check-out is its own row (type: check_in/check_out, source: manual/device/correction). A device logs every raw punch of the day (in, out, in, out, ...), not a single clean pair, and a manual correction shouldn't just sit beside a bad device punch — it needs to actually win.
Attendance::resolveDayWindow($punches) (used by attendanceOn() and available for your own reports) handles this: within a day's punches, manual entries take priority over device/api entries in each direction — first-in is the earliest manual check-in if one exists, otherwise the earliest check-in of any source; last-out mirrors that for check-out. This is what makes an approved correction actually override a stray device punch instead of just adding a second, conflicting record.
Corrections
$correction = $user->requestAttendanceCorrection([
'date' => '2026-09-07',
'requested_in' => '09:05',
'requested_out' => '18:10',
'reason' => 'forgot to punch',
]);
$correction->approve($reviewerId, 'looks fine'); // or ->reject(...)
Approving doesn't just flip a status flag — it creates real check_in/check_out punches (source: 'correction') from the requested times, so the correction flows through the exact same resolveDayWindow() logic as any other punch instead of living as a separate "note" your reports have to special-case.
Corrections are optional (config('attendance.features.corrections')) and routed at /attendance/corrections; approve/reject sit behind a separate review_middleware you can point at your own admin gate.
Events
| Event | Fired when |
|---|---|
AttendanceRecorded |
Any punch is created — manual, device, or correction-approved |
AttendanceCorrectionRequested |
A subject submits a correction |
AttendanceCorrectionReviewed |
A correction is approved or rejected ($correction->status tells which) |
AttendanceDeviceSyncFailed |
A device fails to sync, escalating (Nth failure, then every Mth after — see config) |
LeaveRequested |
An employee submits a leave request |
LeaveReviewed |
A leave request is approved or rejected ($leave->status tells which) |
OvertimeReviewed |
An overtime record is approved or rejected — only approved reaches a salary slip |
AttendanceMarkedLate |
A day's summary (re)builds as late and wasn't already — a rebuild of an already-late day doesn't re-fire this |
The package has no opinion on notifications — listen for these and send however your app already does.
ZKTeco Biometric Attendance & Device Sync
The one rule that matters for both modes below: nothing shows up in attendances until a sync actually runs. Adding a device just registers it — it does not fetch anything by itself. Pull mode fetches only when you call pull (or the scheduled command runs); push mode only stores data once the physical device actually calls your server. Always: connect/register → sync → then query the data — never the other way round.
0. Turn the feature on (once)
ATTENDANCE_FEATURE_DEVICE_SYNC=true
php artisan migrate
This adds the attendance_devices table and device_id/device_user_id columns on attendances (unique-constrained, so the same physical punch can never be imported twice even if you sync it twice).
Your subject model (User, Employee, ...) also needs a PIN column — this is what matches an incoming punch to a person. Add it yourself, e.g.:
Schema::table('users', function (Blueprint $table) {
$table->string('device_user_id')->nullable()->unique();
});
Then set each person's device PIN ($user->device_user_id = '1001'). A punch whose PIN matches nobody is skipped and reported back, not silently dropped — you'll see it in the pull response's unmatched list, or in the log on a push.
Mode A — Pull (server connects out to the device)
Use this when your server can reach the device's IP directly (same network / VPN).
- Install the ZK client library (only needed for pull):
composer require coding-libs/zkteco-php - Register the device:
or$device = AttendanceDevice::create([ 'name' => 'Main Gate', 'ip' => '192.168.1.50', 'port' => 4370, 'status' => 'active', ]);POST /attendance/deviceswith the same fields. Nothing is fetched yet at this point. - (optional) Test the connection first, before pulling any data — confirms the device is reachable without importing anything:
POST /attendance/devices/{id}/test - Pull — this is the step that actually fetches and stores the data:
or on a schedule so backlogs stay small:POST /attendance/devices/{id}/pull
The response tells you exactly what happened:// routes/console.php Schedule::command('attendance:sync-devices')->everyFiveMinutes();{"success": true, "message": "134 logs fetched · 12 new · 2 PIN(s) not matched...", "imported": 12, "unmatched": [...]}. - Now query the data (see Viewing synced data below) — before this step there is nothing to see for this device.
Mode B — Push / ADMS (the device connects to you)
Use this when your server can't reach the device directly (remote site, no static IP, no VPN) — the device dials home to you instead. No extra composer package needed.
- Register the device with its serial number (found on the device itself / its admin menu):
AttendanceDevice::create(['name' => 'Branch Office', 'serial_number' => 'ABCD1234', 'status' => 'active']); - Point the device at your server: on the device, Menu → Comm → Cloud Server Setting → Server Mode
ADMS, Server Address = your app's domain, Enable = on. - Wait for the device to call in. It hits these fixed paths itself, on its own schedule (typically every 30s–a few minutes) — nothing to trigger from your side:
GET|POST /iclock/cdata (handshake, then the actual punch data) GET /iclock/getrequest (heartbeat / command poll) - Check
is_online/last_seen_aton the device to confirm it has connected — that tells you the connection is live, before you check for data:$device->fresh()->is_online; // true once it's called in within the last 90s $device->fresh()->last_synced_at; // set the first time it actually sends punch data - Now query the data — populated automatically as the device pushes, no action needed on your end once step 2 is configured correctly.
Viewing synced data
Regardless of which mode filled it in, synced punches are ordinary Attendance rows (source: 'device') on the matched subject — query them the same way as manual punches:
$user->attendances()->where('source', 'device')->get(); // raw punch log
$user->attendanceOn('2026-09-07'); // resolved first-in/last-out for a day
$device->attendances()->latest('time')->first(); // most recent punch from a specific device
Both sync modes funnel through one shared AttendanceDeviceSyncService::ingestLogs(), so a punch is handled identically no matter which direction it arrived from — same matching, same dedup, same AttendanceRecorded event.
HR & Payroll Core: Shifts, Leave, Holidays, Attendance Summaries, Overtime & Salary
Everything above works against any subject model with just a punch log. This layer is different — it's built around the package's own Employee model (salary, allowances, a device PIN) because computing "present vs. late vs. absent" and generating a payslip genuinely needs real employee data, not an arbitrary model. Off by default; turn the whole stack on with one var:
ATTENDANCE_FEATURE_HR_CORE=true
php artisan migrate
(Each piece — employees, shifts, holidays, leave, summaries, salary, overtime, special_working_days — is also an individually toggleable ATTENDANCE_FEATURE_* flag, in case you only want some of them.)
Every class below is under Easybdit\LaravelEasyAttendance\:
use Easybdit\LaravelEasyAttendance\Models\{Employee, Shift, EmployeeShift, Holiday, LeaveType, Leave, OvertimeRecord, SpecialWorkingDay};
use Easybdit\LaravelEasyAttendance\Services\{AttendanceSummaryService, SalaryService};
The pieces, in the order you'll normally set them up:
-
Employee — the subject everything else attaches to.
$employee = Employee::create([ 'employee_code' => 'E-100', 'name' => 'Nusrat Jahan', 'device_user_id' => '9001', // matches device sync's pin_column 'basic_salary' => 30000, 'allowances' => ['house_rent' => 5000, 'medical' => 1000], 'status' => 'active', ]);Employeeitself usesHasAttendance, so$employee->checkIn(),->checkOut(), device sync — everything from the sections above — works on it directly. Also manageable over HTTP:GET/POST /attendance/employees,GET/PUT/DELETE /attendance/employees/{id}. -
Shift — working hours + late grace + off days.
$shift = Shift::create(['name' => 'General', 'start_time' => '09:00', 'end_time' => '17:00', 'late_grace_minutes' => 10, 'off_days' => ['Friday']]);Or
GET/POST /attendance/shifts,PUT/DELETE /attendance/shifts/{id}. -
Schedule (
EmployeeShift) — assign a shift to an employee for a date range (open-endedend_date= still current):EmployeeShift::create(['employee_id' => $employee->id, 'shift_id' => $shift->id, 'start_date' => '2026-08-01']);No assignment covering a date?
ShiftResolverfalls back toconfig('attendance.default_shift')— summaries work from day one, before you've set up a single shift. OrGET/POST /attendance/employees/{id}/schedule. -
Holiday — a date nobody's expected to work, with no punch needed to explain the day.
Holiday::create(['name' => 'Independence Day', 'date' => '2026-03-26', 'is_recurring_yearly' => true]); -
Leave — request → approve/reject, same pattern as attendance corrections:
$leave = $employee->requestLeave(['start_date' => '2026-09-03', 'end_date' => '2026-09-03', 'reason' => 'personal']); $leave->approve($reviewerId); // or ->reject(...)An approved leave outranks everything else for that date — even a stray punch. Or
GET/POST /attendance/employees/{id}/leaves,POST /attendance/leaves/{id}/approve|reject. -
Attendance summary — the actual present/late/absent/leave/holiday/day_off computation, one row per employee per day:
php artisan attendance:build-summaries 2026-09-07 # one date, every active employee(new AttendanceSummaryService)->buildOne($employee, '2026-09-07'); (new AttendanceSummaryService)->buildForMonth($employee, 2026, 9);Priority order per day: leave → holiday → day off → absent (no punch) → late/present (from the shift-vs-first-punch comparison). Run
attendance:build-summariesafter every device sync (or schedule it) so summaries stay current. -
Overtime — auto-detected from each day's summary (
ot_minutes: last-out past the shift's end time), but lands as apendingOvertimeRecord— it only reaches a payslip once approved, so a punch-clock quirk can't quietly inflate pay:$ot = OvertimeRecord::where('employee_id', $employee->id)->where('date', '2026-10-08')->first(); $ot->approve($reviewerId, 'confirmed with supervisor'); // or ->reject(...)Rate follows the BD Labour Act convention this package was first built under —
hourly_rate = basic_salary / (salary_divisor × 8), OT paysrate_multiplier ×that, capped atmax_hours_per_day(config('attendance.overtime'), all adjustable). Rebuilding a summary never reopens a record someone already approved/rejected — only an untouched auto/pending row gets updated. -
Special working days — an employee specifically asked to work a day that's normally off (their shift's off day, or a company
Holiday) gets extra pay for it, on top of ordinary salary, instead of that day just quietly counting as a plain "present":SpecialWorkingDay::create(['employee_id' => $employee->id, 'date' => '2026-10-09', 'is_payable' => true]);type(day_off/holiday/other) is auto-detected from the date itself against that employee's own shift — not chosen by hand, so it can't drift out of sync. Payment defaults to a plain day's rate (config('attendance.special_working_days')—daily_rate/fixed_amount/multiplier, per type), or set a custompayment_amounton the record to override it. Only pays if the employee actually punched in that day — marking a date special doesn't create attendance, it just adds pay to attendance that already happened. -
Salary — generated from that month's summaries (rebuilds them first, so a slip always reflects the latest synced attendance) — includes approved overtime and payable special-working-day pay automatically:
php artisan attendance:generate-salary 2026 9 # every active employee php artisan attendance:generate-salary 2026 9 --employee=5 # just oneDefault rule (override by reading
SalaryService— this is a starting point, not a full payroll engine): each absent day docks onebasic_salary / working_days_per_month; every Nth late day docks one more — the common "3 late = 1 absent" office policy (config('attendance.salary.late_deduction_ratio'), default3).net_salary = basic_salary + allowances - deductions + overtime_amount + special_pay_amount, snapshotted onto theSalarySlipso a later raise never reshapes an already-generated one. -
Reports — read-only JSON over the summary/salary tables (presentation is up to your own app/GUI):
GET /attendance/reports/daily?date=2026-09-07
GET /attendance/reports/monthly?year=2026&month=9
GET /attendance/reports/employee/{employee}?from=2026-09-01&to=2026-09-07
GET /attendance/reports/salary?year=2026&month=9
Full worked example
All eight pieces together, one employee, one week — this is a real tinker run, output included, so you can see exactly what each step produces. (Overtime + special working days are a second, separate run below, on their own employee/month, so the numbers stay easy to follow.)
$employee = Employee::create([
'employee_code' => 'E-100', 'name' => 'Nusrat Jahan', 'device_user_id' => '9001',
'basic_salary' => 30000, 'allowances' => ['house_rent' => 5000, 'medical' => 1000],
'status' => 'active',
]);
$shift = Shift::create(['name' => 'General', 'start_time' => '09:00', 'end_time' => '17:00', 'late_grace_minutes' => 10, 'off_days' => ['Friday']]);
EmployeeShift::create(['employee_id' => $employee->id, 'shift_id' => $shift->id, 'start_date' => '2026-08-01']);
Holiday::create(['name' => 'Independence Day', 'date' => '2026-09-05']);
$leave = $employee->requestLeave(['start_date' => '2026-09-03', 'end_date' => '2026-09-03', 'reason' => 'personal']);
$leave->approve();
// A normal week: present, a late day, on-leave, present, a holiday, present — Sunday's a
// working day here (only Friday is off) — then the 7th is skipped entirely (genuinely absent).
$employee->checkIn(['time' => '2026-09-01 09:05:00']); $employee->checkOut(['time' => '2026-09-01 17:10:00']);
$employee->checkIn(['time' => '2026-09-02 09:45:00']); $employee->checkOut(['time' => '2026-09-02 17:00:00']);
$employee->checkIn(['time' => '2026-09-04 08:55:00']); $employee->checkOut(['time' => '2026-09-04 17:05:00']);
$employee->checkIn(['time' => '2026-09-06 09:00:00']); $employee->checkOut(['time' => '2026-09-06 17:00:00']);
foreach (['2026-09-01','2026-09-02','2026-09-03','2026-09-04','2026-09-05','2026-09-06','2026-09-07'] as $d) {
echo $d.': '.(new AttendanceSummaryService)->buildOne($employee, $d)->status.PHP_EOL;
}
2026-09-01: present
2026-09-02: late (checked in 09:45, 45 min past the 09:00 shift start)
2026-09-03: leave (the approved leave — outranks everything)
2026-09-04: present
2026-09-05: holiday (no punch needed — Holiday already explains the day)
2026-09-06: present
2026-09-07: absent (no punch, not a holiday/leave/off-day)
$slip = (new SalaryService)->generate($employee, 2026, 9);
// present=4 absent=21 late=1 leave=1
// basic=30000.00 deduction=21000.00 net=15000.00
// (21 absent days × 30000/30 = 21000 deduction — buildForMonth() filled in every
// unpunched day of September as absent/day_off, not just the 7 days above)
Overtime + special working day, worked example
Same shift setup, a different employee, basic 26000 (per-day rate = 26000/30 ≈ 866.67):
// 3 late days in October — the late-ratio deduction (config default: every 3rd = 1 absent) kicks in.
foreach (['2026-10-05', '2026-10-06', '2026-10-07'] as $d) {
$employee->checkIn(['time' => $d.' 09:45:00']);
$employee->checkOut(['time' => $d.' 17:00:00']);
}
// Worked till 19:30 on the 8th — 2.5h past the 17:00 shift end.
$employee->checkIn(['time' => '2026-10-08 09:00:00']);
$employee->checkOut(['time' => '2026-10-08 19:30:00']);
// Asked to work Friday the 9th (the shift's off_day) — and did.
$employee->checkIn(['time' => '2026-10-09 09:00:00']);
$employee->checkOut(['time' => '2026-10-09 17:00:00']);
$special = SpecialWorkingDay::create(['employee_id' => $employee->id, 'date' => '2026-10-09', 'is_payable' => true]);
// $special->type === 'day_off' — auto-detected
foreach (['2026-10-05','2026-10-06','2026-10-07','2026-10-08','2026-10-09'] as $d) {
(new AttendanceSummaryService)->buildOne($employee, $d);
}
$ot = OvertimeRecord::where('employee_id', $employee->id)->where('date', '2026-10-08')->first();
// status=pending ot_hours=2.00 (capped from 2.5) ot_rate=250.0000 ot_amount=500.00
$ot->approve();
$slip = (new SalaryService)->generate($employee, 2026, 10);
present=5 absent=22 late=3
basic=26000.00 deduction=19933.33 (23 days × 866.67 — 22 absent + 1 from 3 late ÷ 3)
overtime_hours=2.00 overtime_amount=500.00
special_pay_amount=866.67 (one day's rate, for showing up on an off day)
net_salary=7433.34 (26000 − 19933.33 + 500 + 866.67)
Configuration reference
config/attendance.php, after php artisan vendor:publish --tag=attendance-config:
| Key | Default | Purpose |
|---|---|---|
subject_model |
your auth user model | The model attendance belongs to |
routes.enabled |
true |
Turn off the built-in HTTP routes entirely |
routes.prefix |
attendance |
URL prefix for all routes |
routes.middleware |
['web','auth'] |
Applied to every route below the prefix |
routes.review_middleware |
['web','auth'] |
Extra gate on correction/device management routes — point at your own admin can: |
features.corrections |
true |
Correction request/approve/reject |
features.device_sync |
false |
ZKTeco pull + push/ADMS |
features.employees / shifts / holidays / leave / summaries / salary / overtime / special_working_days |
false |
HR core, each individually toggleable — or set ATTENDANCE_FEATURE_HR_CORE=true to flip all eight at once |
device_sync.pin_column |
device_user_id |
Column on the subject model's table holding the device PIN |
device_sync.online_threshold_seconds |
90 |
How recently a push device must have been seen to count "online" |
device_sync.notify_after_failures / notify_every |
2 / 5 |
AttendanceDeviceSyncFailed escalation schedule |
default_shift |
09:00–18:00, 15min grace, Friday off | Fallback used by ShiftResolver when no roster entry covers a date |
salary.working_days_per_month |
30 |
Divides basic_salary into a per-day rate for deductions |
salary.late_deduction_ratio |
3 |
Every Nth late day docks one more day's pay |
salary.deduct_for_absent / deduct_for_late |
true / true |
Turn either deduction rule off |
overtime.salary_divisor / rate_multiplier |
26 / 2 |
OT hourly rate = basic_salary / (divisor × 8), paid at multiplier × that |
overtime.max_hours_per_day |
2 |
Caps auto-detected OT per day, however late the last punch |
overtime.auto_detect |
true |
Auto-create a pending OvertimeRecord whenever a summary has ot_minutes |
special_working_days.day_off_payment_type / holiday_payment_type |
daily_rate |
daily_rate | fixed_amount | multiplier, per special-day type |
special_working_days.*_fixed_amount / *_multiplier |
1000 / 1.0 |
Used when the payment type above is fixed_amount / multiplier |
Routes reference
| Method | URI | Feature |
|---|---|---|
| POST | /attendance/check-in |
core |
| POST | /attendance/check-out |
core |
| GET | /attendance/today |
core |
| GET/POST | /attendance/corrections |
corrections |
| POST | /attendance/corrections/{id}/approve|reject |
corrections (behind review_middleware) |
| GET/POST/PUT/DELETE | /attendance/devices... |
device_sync (behind review_middleware) |
| GET/POST | /iclock/cdata, /iclock/getrequest, /iclock/devicecmd |
device_sync — public, no prefix, fixed paths (device firmware calls these directly) |
| GET | /attendance/reports/daily|monthly|employee/{id}|salary |
summaries (salary route also needs features.salary), behind review_middleware |
| GET/POST/PUT/DELETE | /attendance/employees... |
employees, behind review_middleware |
| GET/POST | /attendance/employees/{id}/schedule |
employees + shifts, behind review_middleware |
| GET/POST/PUT/DELETE | /attendance/shifts... |
shifts, behind review_middleware |
| GET/POST | /attendance/employees/{id}/leaves |
employees + leave, behind review_middleware |
| POST | /attendance/leaves/{id}/approve|reject |
leave, behind review_middleware |
All the employee/shift/leave routes above take an explicit {employee} — they're HR/admin management endpoints, not "my own" self-service, since Employee is a separate concept from whatever attendance.subject_model your Auth::user() actually is (see Core concept: the subject model). Holiday/LeaveType/OvertimeRecord/SpecialWorkingDay still have no bundled routes — plain Eloquent models; build whatever your app/GUI needs directly against them.
Tested against real devices
Not just unit-tested against fixtures — validated end to end against actual production ZKTeco hardware, following the exact register → connect/sync → query sequence documented above:
- Push (ADMS): simulated a real device's ATTLOG push (its genuine serial number) against
/iclock/cdata— handshake accepted, punch ingested, heartbeat updated. - Pull (IP), connection only: registered a live device, tested the connection — succeeded, confirmed zero attendance rows existed for it beforehand (nothing is fetched just by registering/testing).
- Pull (IP), full sync: same device,
pull— 7,136 real attendance logs fetched in ~13 seconds. PINs with no matching subject were correctly reported (not silently dropped) and produced no rows. - Pull (IP), matched subject: assigned a subject a real PIN seen in that log, pulled again — 23 real historical punches (spanning roughly 4 months of real dates) landed on that subject and were immediately queryable via
$user->attendances()and$user->attendanceOn($date).
Testing
composer install
composer test
Runs against sqlite in-memory by default (Orchestra Testbench). To run against another driver instead — e.g. this repo's own dev environment, which has no pdo_sqlite — set real env vars, no file to edit:
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_DATABASE=your_test_db DB_USERNAME=... DB_PASSWORD=... composer test
43 tests / 113 assertions cover: punch resolution priority (manual over device), correction approve/reject creating real punches, every event, device push matching/unmatched-PIN/idempotency, pull-mode failure escalation, the check-in/correction HTTP routes, the HR core — summary status priority (leave > holiday > day off > absent > late/present), late-minute math, recurring-yearly holidays, an approved leave overriding a stray punch, overtime/special-working-day pay (the late-ratio deduction boundary, OT capped-and-approval-gated, special-day type auto-detection, pay withheld unless the employee showed up) — and the employee/shift/schedule/leave management HTTP routes.
Roadmap
- Maintain releases and improve compatibility across supported Laravel versions.
- A pluggable
ShiftResolver/leave/holiday contract for teams who want summaries against their own existing shift/roster system instead of this package'sShift/EmployeeShift.
Frequently Asked Questions
Is this a Laravel attendance package?
Yes. Laravel Easy Attendance provides employee check-in/check-out, attendance logs, daily summaries, corrections, shifts, leave, overtime, salary generation, and attendance reports for Laravel applications.
Does it support ZKTeco biometric devices?
Yes. The package supports both ZKTeco Pull mode and ZKTeco Push / ADMS mode. Pull mode connects from the Laravel server to the device, while Push / ADMS allows the device to send attendance data to the Laravel application.
Can I use my existing User model?
Yes. The attendance system uses a polymorphic subject relation, so you can attach attendance to your existing User, Employee, Staff, or another model.
Does it include HR and payroll features?
The optional HR core includes employees, shifts, schedules, holidays, leave, attendance summaries, overtime, special working-day pay, and salary-slip generation.
Which Laravel versions are supported?
The package is designed for Laravel 11, Laravel 12, and Laravel 13 and requires PHP 8.2 or higher.
Keywords
Laravel attendance, Laravel attendance package, Laravel attendance management, employee attendance, employee attendance system, biometric attendance, ZKTeco attendance, ZKTeco Laravel integration, ZKTeco ADMS, biometric attendance system, attendance management system, HR management, leave management, shift management, overtime management, payroll, salary management, attendance reports, Laravel HR package.
License
MIT.