Add employee presence with kiosk sign-in, QR badges, and mobile step-out.
Deploy Ladill Frontdesk / deploy (push) Failing after 26s

Enables staff roster, PIN/code kiosk flow, attendance reports, evacuation staff roll call, webhooks, branch mismatch warnings, and a linked-user My presence portal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-28 21:11:18 +00:00
co-authored by Cursor
parent 905d7268e4
commit 890c2c71e3
40 changed files with 2613 additions and 53 deletions
@@ -4,11 +4,13 @@ namespace App\Http\Controllers\Frontdesk;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
use App\Models\EmployeePresence;
use App\Models\Visit;
use App\Services\Frontdesk\VisitCheckOutService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
class SecurityController extends Controller
{
@@ -27,10 +29,13 @@ class SecurityController extends Controller
$this->scopeToBranch($request, $occupancy);
$occupancy = $occupancy->orderBy('checked_in_at')->get();
$staffOnSite = $this->staffOccupancy($request, $owner, $organization);
$expiredBadges = $occupancy->filter(fn (Visit $v) => $v->isBadgeExpired());
return view('frontdesk.security.index', [
'occupancy' => $occupancy,
'staffOnSite' => $staffOnSite,
'expiredBadges' => $expiredBadges,
'organization' => $organization,
'canCheckout' => app(\App\Services\Frontdesk\FrontdeskPermissions::class)
@@ -51,7 +56,59 @@ class SecurityController extends Controller
$this->scopeToBranch($request, $occupancy);
$occupancy = $occupancy->orderBy('checked_in_at')->get();
return view('frontdesk.security.evacuation', compact('occupancy', 'organization'));
$staffOnSite = $this->staffOccupancy($request, $owner, $organization, withBranch: true);
return view('frontdesk.security.evacuation', compact('occupancy', 'staffOnSite', 'organization'));
}
public function evacuationExport(Request $request): StreamedResponse
{
$this->authorizeAbility($request, 'security.view');
$organization = $this->organization($request);
$owner = $this->ownerRef($request);
$occupancy = Visit::owned($owner)
->where('organization_id', $organization->id)
->currentlyInside()
->with(['visitor', 'host', 'branch']);
$this->scopeToBranch($request, $occupancy);
$occupancy = $occupancy->orderBy('checked_in_at')->get();
$staffOnSite = $this->staffOccupancy($request, $owner, $organization, withBranch: true);
$filename = 'evacuation-'.now()->format('Y-m-d-His').'.csv';
return response()->streamDownload(function () use ($occupancy, $staffOnSite) {
$handle = fopen('php://output', 'w');
fputcsv($handle, ['Type', 'Name', 'Detail', 'Branch', 'Since', 'Phone', 'Status']);
foreach ($occupancy as $visit) {
fputcsv($handle, [
'Visitor',
$visit->visitor->full_name,
ucfirst(str_replace('_', ' ', $visit->visitor_type)).($visit->host ? ' · Host: '.$visit->host->name : ''),
$visit->branch?->name ?? '—',
$visit->checked_in_at?->format('Y-m-d g:i A') ?? '—',
$visit->visitor->phone ?? '—',
'On site',
]);
}
foreach ($staffOnSite as $presence) {
fputcsv($handle, [
'Staff',
$presence->employee->full_name,
$presence->employee->department ?? '—',
$presence->branch?->name ?? $presence->employee->branch?->name ?? '—',
$presence->signed_in_at?->format('Y-m-d g:i A') ?? '—',
$presence->employee->phone ?? '—',
config('frontdesk.employee_presence_statuses.'.$presence->status, $presence->status)
.($presence->destination ? ' · '.$presence->destination : ''),
]);
}
fclose($handle);
}, $filename, ['Content-Type' => 'text/csv']);
}
public function evacuationBadges(Request $request, \App\Services\Frontdesk\BadgeRenderService $badges): View
@@ -118,4 +175,21 @@ class SecurityController extends Controller
'visit' => $visit,
]);
}
protected function staffOccupancy(Request $request, string $owner, $organization, bool $withBranch = false)
{
$query = EmployeePresence::query()
->whereIn('status', [EmployeePresence::STATUS_ON_SITE, EmployeePresence::STATUS_STEPPED_OUT])
->whereHas('employee', function ($q) use ($owner, $organization, $request) {
$q->owned($owner)->where('organization_id', $organization->id)->where('active', true);
$this->scopeToBranch($request, $q, 'branch_id');
})
->with(['employee']);
if ($withBranch) {
$query->with(['branch', 'employee.branch']);
}
return $query->orderBy('employee_id')->get();
}
}