tutoring/app/Http/Controllers/Student/BookingController.php

277 lines
8.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 doesnt 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 cant 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 cant 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}%.");
}
}