require(); $customer = $request->user()->customerProfiles()->where('businessId', $business->id)->firstOrFail(); $appointments = $customer->appointments()->with(['staff.user', 'services.service'])->orderByDesc('startAt')->get(); return view('customer.appointments', compact('business', 'appointments')); } public function show(Request $request, TenantContext $tenant, Appointment $appointment): View { $business = $tenant->require(); $customer = $request->user()->customerProfiles()->where('businessId', $business->id)->firstOrFail(); abort_unless($appointment->businessId === $business->id && $appointment->customerId === $customer->id, 404); $appointment->load(['staff.user', 'services.service']); return view('customer.appointment', compact('business', 'appointment')); } public function cancel(Request $request, TenantContext $tenant, Appointment $appointment, NotificationService $notifications): RedirectResponse { $business = $tenant->require(); $customer = $request->user()->customerProfiles()->where('businessId', $business->id)->firstOrFail(); abort_unless($appointment->businessId === $business->id && $appointment->customerId === $customer->id, 404); if (! in_array($appointment->status, [AppointmentStatus::Pending, AppointmentStatus::Confirmed], true)) throw new RuntimeException('This appointment cannot be cancelled.'); $window = (int) ($business->bookingRule?->cancellationWindowHours ?? 24); if ($appointment->startAt->isBefore(now()->addHours($window))) throw new RuntimeException('This appointment is inside the cancellation window.'); $appointment->status = AppointmentStatus::Cancelled; $appointment->cancelledAt = now(); $appointment->cancelledByUserId = $request->user()->id; $appointment->cancellationReason = 'Cancelled by customer'; $appointment->save(); $notifications->create($business, NotificationType::BookingCancelled, 'Your appointment has been cancelled.', "appointment-cancelled:{$appointment->id}", null, $customer->id, $appointment->id, '/appointments', $business->name); return redirect()->route('customer.appointments')->with('status', 'Appointment cancelled.'); } }