validate([ 'date' => ['required','date_format:Y-m-d'], 'slot_id' => ['required','exists:slots,id'], ]); $slot = Slot::findOrFail($data['slot_id']); // Prettify labels so Blade doesn’t need Carbon $when = Carbon::createFromFormat('Y-m-d H:i:s', "{$data['date']} {$slot->time}", 'Europe/London'); $prettyDate = $when->format('D d M Y'); $prettyTime = $when->format('H:i'); $weekdayName = $when->format('l'); return view('student.bookings.create', [ 'slot' => $slot, 'date' => $data['date'], 'prettyDate' => $prettyDate, 'prettyTime' => $prettyTime, 'weekdayName' => $weekdayName, ]); } public function createSeries(\Illuminate\Http\Request $request) { // Ensure these use's exist at the top of the file: // use App\Models\Slot; // use Carbon\Carbon; // Expect: /student/bookings/series/create?date=YYYY-MM-DD&slot_id=NN&weeks=6 $data = $request->validate([ 'date' => ['required','date_format:Y-m-d'], 'slot_id' => ['required','exists:slots,id'], 'weeks' => ['nullable','integer','min:1','max:52'], ]); $slot = \App\Models\Slot::findOrFail($data['slot_id']); // Prettify labels (keep Blade Carbon-free) $when = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$data['date']} {$slot->time}", 'Europe/London'); $prettyDate = $when->format('D d M Y'); $prettyTime = $when->format('H:i'); $weekdayName = $when->format('l'); $weeks = $data['weeks'] ?? 6; return view('student.bookings.series', [ 'slot' => $slot, 'date' => $data['date'], 'prettyDate' => $prettyDate, 'prettyTime' => $prettyTime, 'weekdayName' => $weekdayName, 'weeks' => $weeks, ]); } public function store(\Illuminate\Http\Request $request) { // Ensure these use's exist at the top of the file: // use App\Models\Slot; // use App\Models\Booking; // use Carbon\Carbon; // use Illuminate\Validation\ValidationException; // use Illuminate\Database\QueryException; $data = $request->validate([ 'slot_id' => ['required','exists:slots,id'], 'date' => ['required','date_format:Y-m-d'], 'message' => ['nullable','string'], ]); $slot = \App\Models\Slot::findOrFail($data['slot_id']); // Enforce > 2 hours’ notice (exactly 120 mins = NOT allowed) $now = \Carbon\Carbon::now('Europe/London'); $sessionStart = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$data['date']} {$slot->time}", 'Europe/London'); $minutesUntil = $now->diffInMinutes($sessionStart, false); if ($minutesUntil <= 120) { throw \Illuminate\Validation\ValidationException::withMessages([ 'date' => 'Bookings require more than 2 hours’ notice.', ]); } // Pull student name from the logged-in user $user = $request->user(); $studentName = $user?->name ?? 'Student'; try { \App\Models\Booking::create([ 'slot_id' => $slot->id, 'date' => $data['date'], 'student_name' => $studentName, 'booked_from_page' => 'student:booking', 'message' => $data['message'] ?? null, 'status' => 'booked', ]); } catch (\Illuminate\Database\QueryException $e) { // Unique (slot_id, date) hit — slot already taken if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) { throw \Illuminate\Validation\ValidationException::withMessages([ 'slot_id' => 'Sorry, that slot was just taken. Please choose another.', ]); } throw $e; } return redirect() ->route('student.calendar') ->with('success', 'Your booking is confirmed.'); } public function storeSeries(\Illuminate\Http\Request $request) { // Validates: slot, start date, and weeks count $data = $request->validate([ 'slot_id' => ['required','exists:slots,id'], 'date' => ['required','date_format:Y-m-d'], // start date 'weeks' => ['required','integer','min:1','max:52'], ]); $slot = \App\Models\Slot::findOrFail($data['slot_id']); $now = \Carbon\Carbon::now('Europe/London'); $startDate = \Carbon\Carbon::createFromFormat('Y-m-d', $data['date'], 'Europe/London'); $user = $request->user(); $student = $user?->name ?? 'Student'; $created = 0; $skipped = 0; for ($i = 0; $i < (int)$data['weeks']; $i++) { $date = $startDate->copy()->addWeeks($i)->toDateString(); $sessionStart = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$date} {$slot->time}", 'Europe/London'); // Enforce > 2 hours’ notice (exactly 120 mins = NOT allowed) $minutesUntil = $now->diffInMinutes($sessionStart, false); if ($minutesUntil <= 120) { $skipped++; continue; } try { \App\Models\Booking::create([ 'slot_id' => $slot->id, 'date' => $date, 'student_name' => $student, 'booked_from_page' => 'student:block-series', 'message' => null, 'status' => 'booked', ]); $created++; } catch (\Illuminate\Database\QueryException $e) { // Skip duplicates (unique (slot_id, date)) if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) { $skipped++; continue; } throw $e; } } return redirect() ->route('student.calendar') ->with('success', "Series created: {$created} added, {$skipped} skipped."); } public function cancelConfirm(\App\Models\Booking $booking) { // Ensure these use's exist at the top: // use App\Models\Booking; // use Carbon\Carbon; $user = auth()->user(); $studentName = $user?->name ?? ''; // Authorize: must be this student's booking and still "booked" if ($booking->student_name !== $studentName || $booking->status !== 'booked') { abort(403); } // Need the slot's time to compute the session start $booking->load('slot'); $now = \Carbon\Carbon::now('Europe/London'); $sessionStart = \Carbon\Carbon::createFromFormat( 'Y-m-d H:i:s', $booking->date.' '.$booking->slot->time, 'Europe/London' ); $minutesUntil = $now->diffInMinutes($sessionStart, false); // If already started or in the past, disallow cancellation if ($minutesUntil <= 0) { return redirect() ->route('student.bookings.index') ->withErrors(['booking' => 'This session has started or is in the past and can’t be cancelled.']); } // Refund policy band (for display only; payments added later) // ≥ 24h => 100%; < 24h => 50% $refundPercent = ($minutesUntil >= 24 * 60) ? 100 : 50; // Prettified labels so Blade stays Carbon-free $prettyDate = $sessionStart->format('D d M Y'); $prettyTime = $sessionStart->format('H:i'); $weekdayName = $sessionStart->format('l'); return view('student.bookings.cancel', [ 'booking' => $booking, 'prettyDate' => $prettyDate, 'prettyTime' => $prettyTime, 'weekdayName' => $weekdayName, 'refundPercent' => $refundPercent, 'minutesUntil' => $minutesUntil, ]); } public function cancelStore(\App\Models\Booking $booking) { // Ensure these use's exist at the top: // use App\Models\Booking; // use Carbon\Carbon; $user = auth()->user(); $studentName = $user?->name ?? ''; // Authorize: must be this student's booking and still "booked" if ($booking->student_name !== $studentName || $booking->status !== 'booked') { abort(403); } // Need slot time to compute the session start $booking->load('slot'); $now = \Carbon\Carbon::now('Europe/London'); $sessionStart = \Carbon\Carbon::createFromFormat( 'Y-m-d H:i:s', $booking->date.' '.$booking->slot->time, 'Europe/London' ); $minutesUntil = $now->diffInMinutes($sessionStart, false); // If already started or in the past, disallow cancellation if ($minutesUntil <= 0) { return redirect() ->route('student.bookings.index') ->withErrors(['booking' => 'This session has started or is in the past and can’t be cancelled.']); } // Refund band (for info only; payments to be integrated later) $refundPercent = ($minutesUntil >= 24 * 60) ? 100 : 50; // For now, delete the booking to free the slot. // (Later we can switch to a "cancelled" status or soft deletes if you want a history.) $booking->delete(); return redirect() ->route('student.bookings.index') ->with('success', "Booking cancelled. Refund: {$refundPercent}%."); } }