syofyanzuhad/filament-zkteco-adms
Filament plugin for ZKTeco ADMS biometric attendance devices. Receive pushed attendance and user data from fingerprint and facial recognition terminals, sync logs, and manage devices directly from your Filament admin panel.
Install the package and run migrations:
composer require syofyanzuhad/filament-zkteco-adms
php artisan vendor:publish --tag="filament-zkteco-adms-config"
php artisan vendor:publish --tag="filament-zkteco-adms-migrations"
php artisan migrate
Register the plugin in your panel provider:
public function panel(Panel $panel): Panel
{
return $panel
->plugin(ZktecoAdmsPlugin::make());
}
Point the device at your app. On the ZKTeco terminal go to Comm → ADMS / Cloud Server and set the server address to your host (your-app.test, path typically /iclock, port 80/443). The device immediately performs a handshake against the package's /iclock/* routes.
Look here first: the Devices and Attendance Filament resources. The Devices resource is your health dashboard — a device that has completed its handshake shows up there with a serial number and last-activity timestamp. If nothing appears after ~60 seconds, the problem is network/middleware, not the package (see Gotchas).
First useful case: let one device sync, confirm punches land as attendance records, then attach them to your own domain — e.g. a relation from Attendance to your Employee model keyed on the device user/employee ID. That's the first 30 minutes of real value; everything else (leave calculations, payroll export) builds on it.
Exclude /iclock/* from CSRF and auth. Devices POST form-encoded data with no session and no token. The package ships routes, but the middleware stack is yours:
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['iclock/*']);
})
Also ensure the panel's auth middleware doesn't apply to those routes, and that any 2FA / IP allowlist middleware is bypassed for device traffic.
React to punches via events rather than polling the table. Subscribe to the package's attendance/device events and queue listeners so the device's HTTP response is never blocked by your business logic:
Event::listen(AttendanceRecorded::class, fn ($event) => ProcessPunch::dispatch($event->attendance));
Keep listeners ShouldQueue — devices have short timeouts and will retry, and a slow listener looks exactly like an outage to the terminal.
Simulate device traffic in tests. The ADMS protocol is plain HTTP, so you can drive the full path in a feature test without hardware:
$this->post('/iclock/cdata?SN=ABC123456', ['SN' => 'ABC123456'])
->assertOk();
Use the recorded payload format (operator=...&SN=...&options=all) to build fixtures for handshake, heartbeat, and attendance-push requests. This is the single highest-leverage thing to do early — it makes upgrades like v1.2.9 verifiable.
Treat attendance ingestion as idempotent at your layer. Devices resend on flaky networks. Upsert your derived records on (device_sn, employee_id, punched_at) rather than blindly inserting, so retries don't double-count hours.
Multi-device / multi-site. Devices are keyed by serial number, which is the natural tenant key. If devices serve different branches, resolve the branch from the serial number inside your listener/observer rather than adding schema to the package tables.
Command dispatch. The ADMS protocol is bidirectional — the device polls for pending commands. Use this for time sync, reboot, and user push instead of walking to the terminal. Note that command execution is eventually-consistent: queue it, then verify on the next poll window rather than expecting a synchronous result.
Reporting. For anything beyond the panel's default listing (monthly hours, lateness, missing punches), build a query layer over the attendance table and export to CSV/Excel. Don't try to bend the resource's table filters into payroll logic.
php artisan config:cache (and config:clear when debugging). Stale cached config is the most common reason a "fixed" setting appears not to work.curl -X POST https://your-app/iclock/cdata?SN=TEST return 200? (2) check the web server access log for /iclock hits at all; (3) check WAF/CDN — Cloudflare bot rules and ModSecurity rule sets routinely block these unattended POSTs; (4) verify the device's server address has no trailing path or scheme mistake. Absence of log entries means the device never reached you.config('app.timezone')), document it, and never mix.composer.json but don't hesitate to bump.How can I help you explore Laravel packages today?