Files
isaaccladandCursor 9e2d79936c
Deploy Ladill Frontdesk / deploy (push) Failing after 35s
Test / test (push) Failing after 2m45s
Initial Ladill Frontdesk release with deploy pipeline.
Visitor management app with SSO, kiosk, badges, reports, and Gitea CI deploy to frontdesk.ladill.com.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 20:37:15 +00:00

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);
}
}