validate([ 'date' => ['required','date_format:Y-m-d'], 'slot_id' => ['required','exists:slots,id'], ]); $slot = Slot::findOrFail($data['slot_id']); // Build nice labels (Europe/London) 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('bookings.create', [ 'slot' => $slot, 'date' => $data['date'], 'prettyDate' => $prettyDate, 'prettyTime' => $prettyTime, 'weekdayName' => $weekdayName, ]); } public function store(\Illuminate\Http\Request $request) { // Needed at top of file (ensure these use's exist): // 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'], 'student_name' => ['required','string','max:255'], 'booked_from_page' => ['nullable','string','max:255'], 'message' => ['nullable','string'], ]); // Enforce > 2 hours' notice (exactly 120 mins = NOT allowed) $slot = \App\Models\Slot::findOrFail($data['slot_id']); $sessionStart = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$data['date']} {$slot->time}", 'Europe/London'); $now = \Carbon\Carbon::now('Europe/London'); $minutesUntil = $now->diffInMinutes($sessionStart, false); if ($minutesUntil <= 120) { throw \Illuminate\Validation\ValidationException::withMessages([ 'date' => 'Bookings require more than 2 hours’ notice.', ]); } // Persist booking; default status for admin-created bookings = 'booked' $data['status'] = 'booked'; try { \App\Models\Booking::create($data); } catch (\Illuminate\Database\QueryException $e) { // Handle unique (slot_id, date) violation gracefully if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) { throw \Illuminate\Validation\ValidationException::withMessages([ 'slot_id' => 'That slot has just been taken. Please choose another.', ]); } throw $e; } return redirect() ->route('admin.calendar') ->with('success', 'Booking created successfully.'); } public function createBlock(\Illuminate\Http\Request $request) { // Needed at top of file: // use App\Models\Slot; // use Carbon\Carbon; $data = $request->validate([ 'date' => ['required','date_format:Y-m-d'], 'slot_id' => ['required','exists:slots,id'], ]); $slot = \App\Models\Slot::findOrFail($data['slot_id']); // Prettify for the view (no Carbon in Blade) $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'); return view('bookings.block', [ 'slot' => $slot, 'date' => $data['date'], 'prettyDate' => $prettyDate, 'prettyTime' => $prettyTime, 'weekdayName' => $weekdayName, ]); } public function storeBlock(\Illuminate\Http\Request $request) { // Validation: block a specific slot on a specific date $data = $request->validate([ 'slot_id' => ['required','exists:slots,id'], 'date' => ['required','date_format:Y-m-d'], 'message' => ['nullable','string'], // optional reason/notes for the block ]); // Create a "blocked" booking for that slot/date try { \App\Models\Booking::create([ 'slot_id' => (int)$data['slot_id'], 'date' => $data['date'], 'student_name' => null, // blocks don't need a student 'booked_from_page' => 'admin:block', // provenance 'message' => $data['message'] ?? null, 'status' => 'blocked', ]); } catch (\Illuminate\Database\QueryException $e) { // Unique (slot_id, date) hit — something already exists for that slot/date if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) { return back() ->withErrors(['slot_id' => 'That slot/date already has a booking or block.']) ->withInput(); } throw $e; } return redirect() ->route('admin.calendar') ->with('success', 'Slot blocked successfully.'); } public function createSeries(\Illuminate\Http\Request $request) { // Needed at top of file (ensure these exist): // use App\Models\Slot; // use Carbon\Carbon; // Accept prefill from calendar click: ?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']); // Precompute nice 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; // sensible default return view('bookings.series', [ 'slot' => $slot, 'date' => $data['date'], 'prettyDate' => $prettyDate, 'prettyTime' => $prettyTime, 'weekdayName' => $weekdayName, 'weeks' => $weeks, ]); } public function storeSeries(\Illuminate\Http\Request $request) { // Needed at top of file: // use App\Models\Slot; // use App\Models\Booking; // use Carbon\Carbon; // use Illuminate\Database\QueryException; $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'); $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'); // Skip past or ≤ 2 hours from now $minutesUntil = $now->diffInMinutes($sessionStart, false); if ($minutesUntil <= 120) { $skipped++; continue; } try { \App\Models\Booking::create([ 'slot_id' => $slot->id, 'date' => $date, 'student_name' => null, // series = no student name 'booked_from_page' => 'admin:block-series', // provenance 'message' => null, 'status' => 'booked', // shows as “Booked” on the calendar ]); $created++; } catch (\Illuminate\Database\QueryException $e) { // Skip duplicates (unique (slot_id, date) might already exist as booked/closed) if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) { $skipped++; continue; } throw $e; } } // Redirect back to the week containing the start date (nice UX) $displayStart = $now->copy()->startOfWeek(\Carbon\Carbon::MONDAY); $fridayCutoff = $displayStart->copy()->addDays(4)->setTime(15, 0); if ($now->greaterThanOrEqualTo($fridayCutoff)) { $displayStart->addWeek(); } $weekOffset = $displayStart->diffInWeeks($startDate->copy()->startOfWeek(\Carbon\Carbon::MONDAY), false); return redirect() ->route('admin.calendar', ['week' => $weekOffset ?: null]) ->with('success', "Series created: {$created} added, {$skipped} skipped."); } }