Deploy Ladill Frontdesk / deploy (push) Successful in 46s
Let guests check out by badge code or QR scan before leaving, with device and staff kiosk API routes. Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.1 KiB
PHP
111 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Frontdesk;
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\Organization;
|
|
use App\Models\Visit;
|
|
|
|
class VisitCheckOutService
|
|
{
|
|
public function __construct(
|
|
protected NotificationDispatcher $notifications,
|
|
protected \App\Services\Integrations\WebhookDispatcher $webhooks,
|
|
) {}
|
|
|
|
public function checkOut(Visit $visit, ?string $actorRef = null): Visit
|
|
{
|
|
abort_unless($visit->isInside(), 422, 'Visitor is not currently checked in.');
|
|
|
|
$visit->update([
|
|
'status' => Visit::STATUS_CHECKED_OUT,
|
|
'checked_out_at' => now(),
|
|
'checked_out_by' => $actorRef,
|
|
]);
|
|
|
|
AuditLog::record(
|
|
$visit->owner_ref,
|
|
'visit.checked_out',
|
|
$visit->organization_id,
|
|
$actorRef,
|
|
Visit::class,
|
|
$visit->id,
|
|
['visitor' => $visit->visitor->full_name],
|
|
);
|
|
|
|
$this->notifications->visitorCheckedOut($visit);
|
|
$this->webhooks->dispatch('visit.checked_out', $visit->fresh(['visitor', 'host']));
|
|
|
|
return $visit->fresh(['visitor', 'host']);
|
|
}
|
|
|
|
public function checkOutByQrToken(string $ownerRef, string $qrToken, ?string $actorRef = null): Visit
|
|
{
|
|
$visit = Visit::owned($ownerRef)
|
|
->where('qr_token', $qrToken)
|
|
->currentlyInside()
|
|
->firstOrFail();
|
|
|
|
return $this->checkOut($visit, $actorRef);
|
|
}
|
|
|
|
public function checkOutFromKiosk(
|
|
string $ownerRef,
|
|
Organization $organization,
|
|
?string $badgeCode = null,
|
|
?string $qrInput = null,
|
|
?int $branchId = null,
|
|
): Visit {
|
|
$visit = $this->resolveCheckedInVisitForKiosk($ownerRef, $organization, $badgeCode, $qrInput, $branchId);
|
|
|
|
return $this->checkOut($visit);
|
|
}
|
|
|
|
protected function resolveCheckedInVisitForKiosk(
|
|
string $ownerRef,
|
|
Organization $organization,
|
|
?string $badgeCode,
|
|
?string $qrInput,
|
|
?int $branchId,
|
|
): Visit {
|
|
$query = Visit::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->currentlyInside()
|
|
->with('visitor');
|
|
|
|
if ($branchId !== null) {
|
|
$query->where(function ($q) use ($branchId) {
|
|
$q->whereNull('branch_id')->orWhere('branch_id', $branchId);
|
|
});
|
|
}
|
|
|
|
$badgeCode = strtoupper(trim((string) $badgeCode));
|
|
if ($badgeCode !== '') {
|
|
return (clone $query)->where('badge_code', $badgeCode)->firstOrFail();
|
|
}
|
|
|
|
$qrToken = $this->parseVisitQrToken((string) $qrInput);
|
|
abort_unless($qrToken !== null, 422, 'Invalid badge scan.');
|
|
|
|
return (clone $query)->where('qr_token', $qrToken)->firstOrFail();
|
|
}
|
|
|
|
protected function parseVisitQrToken(string $input): ?string
|
|
{
|
|
$trimmed = trim($input);
|
|
if ($trimmed === '') {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('#/q/([A-Za-z0-9]+)#', $trimmed, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
|
|
if (preg_match('/^[A-Za-z0-9]{16,64}$/', $trimmed)) {
|
|
return $trimmed;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|