authorizeAbility($request, 'visits.view'); $organization = $this->organization($request); $visits = Visit::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->with(['visitor', 'host']); $this->scopeToBranch($request, $visits); $visits = $visits ->when($request->status, fn ($q, $status) => $q->where('status', $status)) ->when($request->q, function ($q, $search) { $q->whereHas('visitor', fn ($v) => $v->where('full_name', 'like', "%{$search}%")); }) ->latest() ->paginate(25) ->withQueryString(); return view('frontdesk.visits.index', compact('visits', 'organization')); } public function calendar(Request $request): View { $this->authorizeAbility($request, 'visits.view'); $organization = $this->organization($request); $start = $request->filled('start') ? Carbon::parse($request->string('start')->toString())->startOfWeek() : now()->startOfWeek(); $end = $start->copy()->endOfWeek(); $visits = Visit::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->whereNotNull('scheduled_at') ->whereBetween('scheduled_at', [$start, $end]) ->whereNotIn('status', [Visit::STATUS_CANCELLED]) ->with(['visitor', 'host']); $this->scopeToBranch($request, $visits); $visits = $visits->orderBy('scheduled_at')->get()->groupBy( fn (Visit $visit) => $visit->scheduled_at->toDateString(), ); return view('frontdesk.visits.calendar', compact('visits', 'organization', 'start', 'end')); } public function create(Request $request, VisitorSearchService $search): View { $this->authorizeAbility($request, 'visits.manage'); $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(); $returningVisitors = $request->filled('q') ? $search->search($this->ownerRef($request), $organization->id, $request->string('q')->toString()) : collect(); return view('frontdesk.visits.create', [ 'organization' => $organization, 'hosts' => $hosts, 'branches' => $branches, 'returningVisitors' => $returningVisitors, 'typeConfigs' => app(VisitorTypeService::class)->configsForFrontend(), ]); } public function scheduleForm(Request $request, VisitorSearchService $search): View { $this->authorizeAbility($request, 'visits.manage'); $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(); $returningVisitors = $request->filled('q') ? $search->search($this->ownerRef($request), $organization->id, $request->string('q')->toString()) : collect(); return view('frontdesk.visits.schedule', compact('organization', 'hosts', 'branches', 'returningVisitors')); } public function store(Request $request, VisitCheckInService $checkIn, VisitorTypeService $visitorTypes): RedirectResponse { $this->authorizeAbility($request, 'visits.manage'); $organization = $this->organization($request); $visitorType = $request->input('visitor_type', 'visitor'); $validated = $request->validate(array_merge([ 'visitor_id' => ['nullable', 'integer'], 'full_name' => ['required_without:visitor_id', 'string', 'max:255'], 'company' => ['nullable', 'string', 'max:255'], 'phone' => ['nullable', 'string', 'max:50'], 'email' => ['nullable', 'email', 'max:255'], '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'], 'expected_duration_minutes' => ['nullable', 'integer', 'min:5', 'max:480'], 'policies_accepted' => ['accepted'], 'photo_data' => ['nullable', 'string'], ], $visitorTypes->validationRules($visitorType))); $branchScope = app(\App\Services\Frontdesk\OrganizationResolver::class) ->branchScope($this->member($request)); if ($branchScope !== null) { $validated['branch_id'] = $branchScope; } $payload = $visitorTypes->enrichCheckInData($visitorType, $validated, $organization, $request); $visit = $checkIn->checkIn( $this->ownerRef($request), $organization, $payload, $this->ownerRef($request), ); $message = $visit->awaitingApproval() ? 'Visit submitted for approval.' : 'Visitor checked in successfully.'; return redirect() ->route('frontdesk.visits.show', $visit) ->with('success', $message); } public function scheduleStore(Request $request, VisitScheduleService $scheduler): RedirectResponse { $this->authorizeAbility($request, 'visits.manage'); $organization = $this->organization($request); $validated = $request->validate([ 'visitor_id' => ['nullable', 'integer'], 'full_name' => ['required_without:visitor_id', 'string', 'max:255'], 'company' => ['nullable', 'string', 'max:255'], 'phone' => ['nullable', 'string', 'max:50'], 'email' => ['nullable', 'email', 'max:255'], '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'], 'expected_duration_minutes' => ['nullable', 'integer', 'min:5', 'max:480'], 'scheduled_at' => ['required', 'date'], 'notes' => ['nullable', 'string', 'max:2000'], ]); $branchScope = app(\App\Services\Frontdesk\OrganizationResolver::class) ->branchScope($this->member($request)); if ($branchScope !== null) { $validated['branch_id'] = $branchScope; } $visit = $scheduler->schedule( $this->ownerRef($request), $organization, $validated, $this->ownerRef($request), ); return redirect() ->route('frontdesk.visits.show', $visit) ->with('success', 'Visit scheduled successfully.'); } public function show(Request $request, Visit $visit): View { $this->authorizeAbility($request, 'visits.view'); $this->authorizeOwner($request, $visit); $visit->load(['visitor', 'host', 'organization', 'branch']); $canManage = app(FrontdeskPermissions::class)->can( $this->member($request), 'visits.manage', ); return view('frontdesk.visits.show', compact('visit', 'canManage')); } public function activate(Request $request, Visit $visit, VisitLifecycleService $lifecycle): RedirectResponse { $this->authorizeAbility($request, 'visits.manage'); $this->authorizeOwner($request, $visit); $request->validate(['policies_accepted' => ['accepted']]); $lifecycle->checkInFromSchedule( $visit, $this->ownerRef($request), true, ); return redirect() ->route('frontdesk.visits.show', $visit) ->with('success', 'Visitor checked in successfully.'); } public function markWaiting(Request $request, Visit $visit, VisitLifecycleService $lifecycle): RedirectResponse { $this->authorizeAbility($request, 'visits.manage'); $this->authorizeOwner($request, $visit); $lifecycle->markWaiting($visit, $this->ownerRef($request)); return back()->with('success', 'Visitor marked as waiting.'); } public function cancel(Request $request, Visit $visit, VisitLifecycleService $lifecycle): RedirectResponse { $this->authorizeAbility($request, 'visits.manage'); $this->authorizeOwner($request, $visit); $validated = $request->validate([ 'reason' => ['nullable', 'string', 'max:500'], ]); $lifecycle->cancel($visit, $this->ownerRef($request), $validated['reason'] ?? null); return redirect() ->route('frontdesk.visits.index', ['status' => 'cancelled']) ->with('success', 'Visit cancelled.'); } public function approve(Request $request, Visit $visit, VisitLifecycleService $lifecycle): RedirectResponse { $this->authorizeAbility($request, 'visits.manage'); $this->authorizeOwner($request, $visit); $lifecycle->approve($visit, $this->ownerRef($request)); return redirect() ->route('frontdesk.visits.show', $visit) ->with('success', 'Visit approved and visitor checked in.'); } public function checkOut(Request $request, Visit $visit, VisitCheckOutService $checkOut): RedirectResponse { $this->authorizeAbility($request, 'visits.manage'); $this->authorizeOwner($request, $visit); $checkOut->checkOut($visit, $this->ownerRef($request)); return back()->with('success', 'Visitor checked out.'); } public function badge(Request $request, Visit $visit, PrinterManager $printers) { $this->authorizeAbility($request, 'visits.view'); $this->authorizeOwner($request, $visit); $rendered = $printers->renderBadge($visit, $request->query('driver')); if ($rendered['format'] === 'html') { return response($rendered['content'])->header('Content-Type', 'text/html'); } return response($rendered['content']) ->header('Content-Type', 'text/plain') ->header('Content-Disposition', 'attachment; filename="'.$rendered['filename'].'"'); } }