Initial commit

This commit is contained in:
Richard Jolley committed 2026-01-14 00:54:08 +00:00
commit c909d539bc
7178 files changed
+756624

No files matched your search

@@ -0,0 +1,246 @@
<?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.");
}
}