Visitor management app with SSO, kiosk, badges, reports, and Gitea CI deploy to frontdesk.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
5.4 KiB
PHP
180 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Models\Concerns\BelongsToOwner;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Str;
|
|
|
|
class Visit extends Model
|
|
{
|
|
use BelongsToOwner, SoftDeletes;
|
|
|
|
protected $table = 'frontdesk_visits';
|
|
|
|
public const STATUS_SCHEDULED = 'scheduled';
|
|
|
|
public const STATUS_EXPECTED = 'expected';
|
|
|
|
public const STATUS_WAITING = 'waiting';
|
|
|
|
public const STATUS_CHECKED_IN = 'checked_in';
|
|
|
|
public const STATUS_CHECKED_OUT = 'checked_out';
|
|
|
|
public const STATUS_CANCELLED = 'cancelled';
|
|
|
|
public const STATUS_OVERDUE = 'overdue';
|
|
|
|
public const ACTIVE_STATUSES = [
|
|
self::STATUS_EXPECTED,
|
|
self::STATUS_WAITING,
|
|
self::STATUS_CHECKED_IN,
|
|
self::STATUS_OVERDUE,
|
|
];
|
|
|
|
protected $fillable = [
|
|
'public_id', 'external_ref', 'source', 'owner_ref', 'organization_id', 'branch_id', 'reception_desk_id',
|
|
'visitor_id', 'host_id', 'visitor_type', 'status', 'purpose',
|
|
'expected_duration_minutes', 'scheduled_at', 'checked_in_at', 'checked_out_at',
|
|
'badge_expires_at', 'badge_code', 'qr_token', 'photo_path', 'signature_path',
|
|
'policies_accepted', 'vehicle_info', 'contractor_details', 'delivery_details',
|
|
'allowed_areas', 'notes', 'integration_metadata', 'checked_in_by', 'checked_out_by',
|
|
];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'scheduled_at' => 'datetime',
|
|
'checked_in_at' => 'datetime',
|
|
'checked_out_at' => 'datetime',
|
|
'badge_expires_at' => 'datetime',
|
|
'policies_accepted' => 'boolean',
|
|
'vehicle_info' => 'array',
|
|
'contractor_details' => 'array',
|
|
'delivery_details' => 'array',
|
|
'allowed_areas' => 'array',
|
|
'integration_metadata' => 'array',
|
|
];
|
|
}
|
|
|
|
protected static function booted(): void
|
|
{
|
|
static::creating(function (Visit $visit) {
|
|
if (! $visit->public_id) {
|
|
$visit->public_id = (string) Str::uuid();
|
|
}
|
|
if (! $visit->qr_token) {
|
|
$visit->qr_token = Str::random(32);
|
|
}
|
|
if (! $visit->badge_code) {
|
|
$visit->badge_code = strtoupper(Str::random(8));
|
|
}
|
|
});
|
|
}
|
|
|
|
public function organization(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Organization::class, 'organization_id');
|
|
}
|
|
|
|
public function branch(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Branch::class, 'branch_id');
|
|
}
|
|
|
|
public function receptionDesk(): BelongsTo
|
|
{
|
|
return $this->belongsTo(ReceptionDesk::class, 'reception_desk_id');
|
|
}
|
|
|
|
public function visitor(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Visitor::class, 'visitor_id');
|
|
}
|
|
|
|
public function host(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Host::class, 'host_id');
|
|
}
|
|
|
|
public function isInside(): bool
|
|
{
|
|
return $this->status === self::STATUS_CHECKED_IN;
|
|
}
|
|
|
|
public function isPending(): bool
|
|
{
|
|
return in_array($this->status, [
|
|
self::STATUS_EXPECTED,
|
|
self::STATUS_SCHEDULED,
|
|
self::STATUS_WAITING,
|
|
self::STATUS_OVERDUE,
|
|
], true);
|
|
}
|
|
|
|
public function awaitingApproval(): bool
|
|
{
|
|
if ($this->status !== self::STATUS_WAITING || $this->checked_in_at !== null) {
|
|
return false;
|
|
}
|
|
|
|
return (bool) (
|
|
data_get($this->contractor_details, '_awaiting_approval')
|
|
|| data_get($this->delivery_details, '_awaiting_approval')
|
|
);
|
|
}
|
|
|
|
/** @return array<string, string|null> */
|
|
public function typeDetailEntries(): array
|
|
{
|
|
$entries = [];
|
|
$labels = collect(config('frontdesk.visitor_type_config', []))
|
|
->flatMap(fn (array $config) => collect($config['fields'] ?? [])
|
|
->mapWithKeys(fn (array $field) => [$field['name'] => $field['label']]));
|
|
|
|
foreach (array_filter([$this->contractor_details, $this->delivery_details]) as $details) {
|
|
foreach ($details as $key => $value) {
|
|
if (str_starts_with((string) $key, '_') || $value === null || $value === '') {
|
|
continue;
|
|
}
|
|
|
|
$label = $labels->get($key, str_replace('_', ' ', (string) $key));
|
|
|
|
if (str_contains((string) $key, 'photo') || str_contains((string) $key, 'signature')) {
|
|
$entries[$label] = 'On file';
|
|
} elseif ($key === 'received_at') {
|
|
$entries[$label] = \Illuminate\Support\Carbon::parse($value)->format('M j, Y g:i A');
|
|
} else {
|
|
$entries[$label] = is_bool($value) ? ($value ? 'Yes' : 'No') : (string) $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
public function canActivateCheckIn(): bool
|
|
{
|
|
return $this->isPending();
|
|
}
|
|
|
|
public function isBadgeExpired(): bool
|
|
{
|
|
return $this->badge_expires_at && $this->badge_expires_at->isPast();
|
|
}
|
|
|
|
public function scopeToday($query)
|
|
{
|
|
return $query->whereDate('checked_in_at', today())
|
|
->orWhereDate('scheduled_at', today());
|
|
}
|
|
|
|
public function scopeCurrentlyInside($query)
|
|
{
|
|
return $query->where('status', self::STATUS_CHECKED_IN);
|
|
}
|
|
}
|