Add employee presence with kiosk sign-in, QR badges, and mobile step-out.
Deploy Ladill Frontdesk / deploy (push) Failing after 26s
Deploy Ladill Frontdesk / deploy (push) Failing after 26s
Enables staff roster, PIN/code kiosk flow, attendance reports, evacuation staff roll call, webhooks, branch mismatch warnings, and a linked-user My presence portal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToOwner;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Employee extends Model
|
||||
{
|
||||
use BelongsToOwner, SoftDeletes;
|
||||
|
||||
protected $table = 'frontdesk_employees';
|
||||
|
||||
protected $fillable = [
|
||||
'owner_ref', 'organization_id', 'branch_id', 'employee_code', 'qr_token', 'full_name',
|
||||
'department', 'email', 'phone', 'pin_hash', 'host_id', 'user_ref', 'active',
|
||||
];
|
||||
|
||||
protected $hidden = ['pin_hash'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['active' => 'boolean'];
|
||||
}
|
||||
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organization::class, 'organization_id');
|
||||
}
|
||||
|
||||
public function branch(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Branch::class, 'branch_id');
|
||||
}
|
||||
|
||||
public function host(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Host::class, 'host_id');
|
||||
}
|
||||
|
||||
public function presence(): HasOne
|
||||
{
|
||||
return $this->hasOne(EmployeePresence::class, 'employee_id');
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (Employee $employee) {
|
||||
if (! $employee->qr_token) {
|
||||
$employee->qr_token = Str::random(32);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function findByQrLookup(string $lookup, int $organizationId): ?self
|
||||
{
|
||||
$token = self::parseQrLookup($lookup);
|
||||
|
||||
if ($token === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::query()
|
||||
->where('organization_id', $organizationId)
|
||||
->where('qr_token', $token)
|
||||
->where('active', true)
|
||||
->first();
|
||||
}
|
||||
|
||||
public static function parseQrLookup(string $lookup): ?string
|
||||
{
|
||||
$lookup = trim($lookup);
|
||||
|
||||
if ($lookup === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('#/eq/([A-Za-z0-9]+)#', $lookup, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
if (preg_match('/^[A-Za-z0-9]{16,64}$/', $lookup)) {
|
||||
return $lookup;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EmployeePresence extends Model
|
||||
{
|
||||
public const STATUS_OFF_SITE = 'off_site';
|
||||
|
||||
public const STATUS_ON_SITE = 'on_site';
|
||||
|
||||
public const STATUS_STEPPED_OUT = 'stepped_out';
|
||||
|
||||
protected $table = 'frontdesk_employee_presence';
|
||||
|
||||
protected $fillable = [
|
||||
'employee_id', 'status', 'branch_id', 'device_id', 'destination',
|
||||
'step_out_reason', 'notes', 'expected_return_at', 'return_alert_sent_at',
|
||||
'signed_in_at', 'last_event_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'signed_in_at' => 'datetime',
|
||||
'last_event_at' => 'datetime',
|
||||
'expected_return_at' => 'datetime',
|
||||
'return_alert_sent_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function employee(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Employee::class, 'employee_id');
|
||||
}
|
||||
|
||||
public function branch(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Branch::class, 'branch_id');
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class, 'device_id');
|
||||
}
|
||||
|
||||
public function isOnSite(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_ON_SITE;
|
||||
}
|
||||
|
||||
public function isSteppedOut(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_STEPPED_OUT;
|
||||
}
|
||||
|
||||
public function isOffSite(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_OFF_SITE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToOwner;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EmployeePresenceEvent extends Model
|
||||
{
|
||||
use BelongsToOwner;
|
||||
public const EVENT_SIGN_IN = 'sign_in';
|
||||
|
||||
public const EVENT_SIGN_OUT = 'sign_out';
|
||||
|
||||
public const EVENT_STEP_OUT = 'step_out';
|
||||
|
||||
public const EVENT_STEP_IN = 'step_in';
|
||||
|
||||
protected $table = 'frontdesk_employee_presence_events';
|
||||
|
||||
protected $fillable = [
|
||||
'owner_ref', 'organization_id', 'employee_id', 'event', 'status_after',
|
||||
'branch_id', 'device_id', 'destination', 'step_out_reason', 'notes',
|
||||
];
|
||||
|
||||
public function employee(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Employee::class, 'employee_id');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user