Initial Ladill Frontdesk release with deploy pipeline.
Deploy Ladill Frontdesk / deploy (push) Failing after 35s
Test / test (push) Failing after 2m45s

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:
isaacclad
2026-06-27 20:37:15 +00:00
co-authored by Cursor
commit 9e2d79936c
284 changed files with 29134 additions and 0 deletions
@@ -0,0 +1,100 @@
<?php
namespace App\Services\Frontdesk;
use App\Models\AuditLog;
use App\Models\Organization;
use App\Models\Visit;
use Illuminate\Support\Facades\DB;
class VisitLifecycleService
{
public function __construct(
protected VisitCheckInService $checkIns,
protected NotificationDispatcher $notifications,
) {}
public function checkInFromSchedule(Visit $visit, ?string $actorRef = null, bool $policiesAccepted = true): Visit
{
abort_unless($visit->canActivateCheckIn(), 422, 'This visit cannot be checked in.');
$visit->load('visitor');
app(WatchlistService::class)->assertCanCheckIn($visit->visitor);
return $this->checkIns->activateCheckIn($visit, $actorRef, $policiesAccepted);
}
public function markWaiting(Visit $visit, ?string $actorRef = null): Visit
{
abort_unless(in_array($visit->status, [
Visit::STATUS_EXPECTED,
Visit::STATUS_SCHEDULED,
Visit::STATUS_OVERDUE,
], true), 422, 'Visit is not awaiting arrival.');
$visit->update(['status' => Visit::STATUS_WAITING]);
AuditLog::record(
$visit->owner_ref,
'visit.waiting',
$visit->organization_id,
$actorRef,
Visit::class,
$visit->id,
['visitor' => $visit->visitor->full_name],
);
$this->notifications->visitorWaiting($visit);
return $visit->fresh(['visitor', 'host']);
}
public function cancel(Visit $visit, ?string $actorRef = null, ?string $reason = null): Visit
{
abort_if($visit->isInside(), 422, 'Checked-in visits must be checked out, not cancelled.');
abort_if($visit->status === Visit::STATUS_CANCELLED, 422, 'Visit is already cancelled.');
$visit->update([
'status' => Visit::STATUS_CANCELLED,
'notes' => trim(($visit->notes ?? '').($reason ? "\nCancelled: {$reason}" : '')),
]);
AuditLog::record(
$visit->owner_ref,
'visit.cancelled',
$visit->organization_id,
$actorRef,
Visit::class,
$visit->id,
['visitor' => $visit->visitor->full_name, 'reason' => $reason],
);
$this->notifications->visitCancelled($visit);
return $visit->fresh(['visitor', 'host']);
}
/** Mark expected/scheduled visits past their scheduled time as overdue. */
public function markOverdueVisits(?Organization $organization = null): int
{
$query = Visit::query()
->whereIn('status', [Visit::STATUS_EXPECTED, Visit::STATUS_SCHEDULED])
->whereNotNull('scheduled_at')
->where('scheduled_at', '<', now());
if ($organization) {
$query->where('organization_id', $organization->id);
}
return $query->update(['status' => Visit::STATUS_OVERDUE]);
}
public function approve(Visit $visit, ?string $actorRef = null): Visit
{
abort_unless($visit->awaitingApproval(), 422, 'This visit is not awaiting approval.');
$visit->load('visitor');
app(WatchlistService::class)->assertCanCheckIn($visit->visitor);
return $this->checkIns->activateCheckIn($visit, $actorRef, (bool) $visit->policies_accepted);
}
}