authorizeAbility($request, 'visitors.view'); $organization = $this->organization($request); $visitors = Visitor::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->when($request->q, fn ($q, $search) => $q->where('full_name', 'like', "%{$search}%")) ->orderByDesc('is_frequent') ->orderBy('full_name') ->paginate(25) ->withQueryString(); $statsQuery = Visitor::owned($this->ownerRef($request)) ->where('organization_id', $organization->id); $heroStats = [ 'total' => (clone $statsQuery)->count(), 'frequent' => (clone $statsQuery)->where('is_frequent', true)->count(), 'flagged' => (clone $statsQuery)->whereIn('watchlist_status', [ Visitor::WATCHLIST_REQUIRES_APPROVAL, Visitor::WATCHLIST_BLACKLISTED, ])->count(), ]; return view('frontdesk.visitors.index', compact('visitors', 'organization', 'heroStats')); } public function show(Request $request, Visitor $visitor): View { $this->authorizeAbility($request, 'visitors.view'); $this->authorizeOwner($request, $visitor); $visitor->load(['visits' => fn ($q) => $q->latest()->limit(20), 'visits.host']); $visitIds = $visitor->visits()->pluck('id'); $activity = AuditLog::owned($this->ownerRef($request)) ->where('organization_id', $visitor->organization_id) ->where(function ($q) use ($visitor, $visitIds) { $q->where(function ($inner) use ($visitor) { $inner->where('subject_type', Visitor::class) ->where('subject_id', $visitor->id); }); if ($visitIds->isNotEmpty()) { $q->orWhere(function ($inner) use ($visitIds) { $inner->where('subject_type', Visit::class) ->whereIn('subject_id', $visitIds); }); } }) ->orderByDesc('created_at') ->limit(30) ->get(); return view('frontdesk.visitors.show', [ 'visitor' => $visitor, 'activity' => $activity, 'auditActions' => config('frontdesk.audit_actions'), 'watchlistStatuses' => config('frontdesk.watchlist_statuses'), 'canManage' => app(FrontdeskPermissions::class)->can( $this->member($request), 'visitors.manage' ), ]); } public function checkInForm(Request $request, Visitor $visitor): View { $this->authorizeAbility($request, 'visits.manage'); $this->authorizeOwner($request, $visitor); $organization = $this->organization($request); $hosts = Host::owned($this->ownerRef($request)) ->where('organization_id', $organization->id); $this->scopeToBranch($request, $hosts); $hosts = $hosts->orderBy('name')->get(); $branches = Branch::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->where('is_active', true) ->orderBy('name') ->get(); return view('frontdesk.visitors.check-in', compact('visitor', 'organization', 'hosts', 'branches')); } public function checkIn(Request $request, Visitor $visitor, VisitCheckInService $checkIn): RedirectResponse { $this->authorizeAbility($request, 'visits.manage'); $this->authorizeOwner($request, $visitor); $organization = $this->organization($request); $validated = $request->validate([ 'host_id' => ['nullable', 'integer'], 'branch_id' => ['nullable', 'integer'], 'visitor_type' => ['required', 'string', 'in:'.implode(',', array_keys(config('frontdesk.visitor_types')))], 'purpose' => ['nullable', 'string', 'max:500'], 'policies_accepted' => ['accepted'], ]); $branchScope = app(\App\Services\Frontdesk\OrganizationResolver::class) ->branchScope($this->member($request)); if ($branchScope !== null) { $validated['branch_id'] = $branchScope; } $validated['visitor_id'] = $visitor->id; $visit = $checkIn->checkIn( $this->ownerRef($request), $organization, $validated, $this->ownerRef($request), ); return redirect() ->route('frontdesk.visits.show', $visit) ->with('success', 'Returning visitor checked in.'); } public function update(Request $request, Visitor $visitor): RedirectResponse { $this->authorizeAbility($request, 'visitors.manage'); $this->authorizeOwner($request, $visitor); $validated = $request->validate([ 'full_name' => ['required', 'string', 'max:255'], 'company' => ['nullable', 'string', 'max:255'], 'phone' => ['nullable', 'string', 'max:50'], 'email' => ['nullable', 'email', 'max:255'], 'notes' => ['nullable', 'string', 'max:2000'], 'photo' => ['nullable', 'image', 'max:5120'], 'id_document' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:10240'], ]); if ($request->hasFile('photo')) { if ($visitor->photo_path) { Storage::disk('public')->delete($visitor->photo_path); } $validated['photo_path'] = $request->file('photo')->store('frontdesk/visitors/photos', 'public'); } if ($request->hasFile('id_document')) { if ($visitor->id_document_path) { Storage::disk('public')->delete($visitor->id_document_path); } $validated['id_document_path'] = $request->file('id_document')->store('frontdesk/visitors/documents', 'public'); } unset($validated['photo'], $validated['id_document']); $visitor->update($validated); return back()->with('success', 'Visitor profile updated.'); } public function updateWatchlist(Request $request, Visitor $visitor, WatchlistService $watchlist): RedirectResponse { $this->authorizeAbility($request, 'visitors.manage'); $this->authorizeOwner($request, $visitor); $organization = $this->organization($request); $validated = $request->validate([ 'watchlist_status' => ['required', 'string', 'in:'.implode(',', array_keys(config('frontdesk.watchlist_statuses')))], 'notes' => ['nullable', 'string', 'max:2000'], ]); $visitor->update($validated); $watchlist->syncEntryForVisitor( $visitor, $validated['watchlist_status'], $validated['notes'] ?? null, $this->ownerRef($request), ); AuditLog::record( $this->ownerRef($request), 'watchlist.entry_created', $organization->id, $this->ownerRef($request), Visitor::class, $visitor->id, ['watchlist_status' => $validated['watchlist_status']], ); return back()->with('success', 'Visitor watchlist status updated.'); } }