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,276 @@
<?php
namespace App\Http\Controllers\Student;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Slot;
use Carbon\Carbon;
use App\Services\AvailabilityService;
class BookingController extends Controller
{
public function create(Request $request)
{
// Expect: /student/bookings/create?date=YYYY-MM-DD&slot_id=NN
$data = $request->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}%.");
}
}