277 lines
8.6 KiB
PHP
277 lines
8.6 KiB
PHP
<?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}%.");
|
||
}
|
||
}
|