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>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user