247 lines
8.1 KiB
PHP
247 lines
8.1 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use Illuminate\Http\Request;
|
||
use App\Models\Slot;
|
||
use Carbon\Carbon;
|
||
|
||
class BookingController extends Controller
|
||
{
|
||
public function create(Request $request)
|
||
{
|
||
// Validate query params: /admin/bookings/create?date=YYYY-MM-DD&slot_id=123
|
||
$data = $request->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.");
|
||
}
|
||
|
||
}
|