From 365c1ab5d095bee626868131bf164c82be38b692 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Tue, 24 Jun 2025 17:07:46 +0100 Subject: [PATCH 01/20] Bookings Concept --- .../Site/BookingsPagesController.php | 33 ++++++++++++ .../views/site/bookings/calendar.blade.php | 52 +++++++++++++++++++ routes/web-public.php | 7 +++ 3 files changed, 92 insertions(+) create mode 100644 app/Http/Controllers/Site/BookingsPagesController.php create mode 100644 resources/views/site/bookings/calendar.blade.php diff --git a/app/Http/Controllers/Site/BookingsPagesController.php b/app/Http/Controllers/Site/BookingsPagesController.php new file mode 100644 index 0000000000..10b45f7a59 --- /dev/null +++ b/app/Http/Controllers/Site/BookingsPagesController.php @@ -0,0 +1,33 @@ +year, $month ?? now()->month, 1)->startOfMonth(); + + $start = $date->copy()->startOfWeek(); + $end = $date->copy()->endOfMonth()->endOfWeek(); + + $calendar = []; + $current = $start->copy(); + + while ($current <= $end) { + $week = []; + for ($i = 0; $i < 7; $i++) { + $week[] = $current->copy(); + $current->addDay(); + } + $calendar[] = $week; + } + + $prevMonth = $date->copy()->subMonth(); + $nextMonth = $date->copy()->addMonth(); + + return view('site.bookings.calendar', compact('calendar', 'date', 'prevMonth', 'nextMonth')); + } +} diff --git a/resources/views/site/bookings/calendar.blade.php b/resources/views/site/bookings/calendar.blade.php new file mode 100644 index 0000000000..1c575505c3 --- /dev/null +++ b/resources/views/site/bookings/calendar.blade.php @@ -0,0 +1,52 @@ +@extends('layout') + +@section('content') + +
+
+
  Bookings Calendar +
+
+

+ The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. +

+
+

{{ $date->format('F Y') }}

+
+
+

Use the links below to navigate through the months.

+ + ← Previous +  |  + + Next → +
+
+ + + + @foreach(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as $day) + + + @endforeach + + + + @foreach($calendar as $week) + + @foreach($week as $day) + + @endforeach + + @endforeach + +
{{ $day }}
+ {{ $day->day }} +
+ +
+
+
+
+ +@stop \ No newline at end of file diff --git a/routes/web-public.php b/routes/web-public.php index a19b0f4292..05050b7c64 100644 --- a/routes/web-public.php +++ b/routes/web-public.php @@ -58,4 +58,11 @@ ], function () { Route::get('/branding')->uses('MarketingPagesController@viewBranding')->name('branding'); }); + + Route::group([ + 'as' => 'bookings.', + 'prefix' => 'bookings', + ], function () { + Route::get('/calendar/{year?}/{month?}')->uses('BookingsPagesController@index')->name('calendar'); + }); }); From cf7c5327278a86e87c487b4e3054efd8e939b310 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Tue, 24 Jun 2025 22:41:39 +0100 Subject: [PATCH 02/20] Further work --- .../Site/BookingsPagesController.php | 42 +++++- app/Repositories/Cts/BookingRepository.php | 12 ++ .../views/site/bookings/calendar.blade.php | 52 ------- resources/views/site/bookings/index.blade.php | 137 ++++++++++++++++++ routes/web-public.php | 3 +- 5 files changed, 191 insertions(+), 55 deletions(-) delete mode 100644 resources/views/site/bookings/calendar.blade.php create mode 100644 resources/views/site/bookings/index.blade.php diff --git a/app/Http/Controllers/Site/BookingsPagesController.php b/app/Http/Controllers/Site/BookingsPagesController.php index 10b45f7a59..51cba5d1a7 100644 --- a/app/Http/Controllers/Site/BookingsPagesController.php +++ b/app/Http/Controllers/Site/BookingsPagesController.php @@ -3,9 +3,37 @@ namespace App\Http\Controllers\Site; use Carbon\Carbon; +use App\Repositories\Cts\BookingRepository; +use App\Models\Cts\Booking; class BookingsPagesController extends \App\Http\Controllers\BaseController { + protected BookingRepository $bookingRepo; + + public function __construct(BookingRepository $bookingRepo) + { + $this->bookingRepo = $bookingRepo; + } + + public function show($id) + { + $booking = Booking::findOrFail($id); + + return response()->json([ + 'id' => $booking->id, + 'position' => $booking->position, + 'controller_name' => $booking->member_id, + 'start_time' => $booking->from, + 'end_time' => $booking->to, + ]); + } + /** + * Display the bookings calendar for a given month and year. + * + * @param int|null $year + * @param int|null $month + * @return \Illuminate\View\View + */ public function index($year = null, $month = null) { $date = Carbon::createFromDate($year ?? now()->year, $month ?? now()->month, 1)->startOfMonth(); @@ -16,10 +44,20 @@ public function index($year = null, $month = null) $calendar = []; $current = $start->copy(); + // Fetch Bookings for the entire Calendar Range + $bookings = $this->bookingRepo->getBookingsBetween($start, $end); + while ($current <= $end) { $week = []; for ($i = 0; $i < 7; $i++) { - $week[] = $current->copy(); + $dayBookings = $bookings->filter(function ($booking) use ($current) { + return $booking->date->isSameDay($current); + }); + + $week[] = [ + 'date' =>$current->copy(), + 'bookings' => $dayBookings, + ]; $current->addDay(); } $calendar[] = $week; @@ -28,6 +66,6 @@ public function index($year = null, $month = null) $prevMonth = $date->copy()->subMonth(); $nextMonth = $date->copy()->addMonth(); - return view('site.bookings.calendar', compact('calendar', 'date', 'prevMonth', 'nextMonth')); + return view('site.bookings.index', compact('calendar', 'date', 'prevMonth', 'nextMonth')); } } diff --git a/app/Repositories/Cts/BookingRepository.php b/app/Repositories/Cts/BookingRepository.php index d56400a386..25085b6370 100644 --- a/app/Repositories/Cts/BookingRepository.php +++ b/app/Repositories/Cts/BookingRepository.php @@ -4,10 +4,22 @@ use App\Models\Cts\Booking; use Carbon\Carbon; +use Carbon\CarbonPeriod; use Illuminate\Support\Collection; class BookingRepository { + public function getBookingsBetween($startDate, $endDate) + { + $bookings = Booking::whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()]) + ->get() + ->each(function ($booking) { + $booking->date = Carbon::parse($booking->date); + }); + + return $this->formatBookings($bookings); + } + public function getBookings(Carbon $date) { $bookings = Booking::where('date', '=', $date->toDateString()) diff --git a/resources/views/site/bookings/calendar.blade.php b/resources/views/site/bookings/calendar.blade.php deleted file mode 100644 index 1c575505c3..0000000000 --- a/resources/views/site/bookings/calendar.blade.php +++ /dev/null @@ -1,52 +0,0 @@ -@extends('layout') - -@section('content') - -
-
-
  Bookings Calendar -
-
-

- The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. -

-
-

{{ $date->format('F Y') }}

-
-
-

Use the links below to navigate through the months.

- - ← Previous -  |  - - Next → -
-
- - - - @foreach(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as $day) - - - @endforeach - - - - @foreach($calendar as $week) - - @foreach($week as $day) - - @endforeach - - @endforeach - -
{{ $day }}
- {{ $day->day }} -
- -
-
-
-
- -@stop \ No newline at end of file diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php new file mode 100644 index 0000000000..d6995a890b --- /dev/null +++ b/resources/views/site/bookings/index.blade.php @@ -0,0 +1,137 @@ +@extends('layout') + +@section('content') + + + + +
+
+
  Bookings Calendar +
+
+

+ The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. +

+
+
+
+ +
+
+
{{ $date->format('F Y') }}
+
+
+

Use the links below to navigate through the months.

+ + ← Previous +  |  + + Next → +
+
+ + + + @foreach(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as $day) + + + @endforeach + + + + @foreach($calendar as $week) + + @foreach($week as $day) + + @endforeach + + @endforeach + +
{{ $day }}
+ {{ $day['date']->day }} + @foreach($day['bookings'] as $booking) +
+ 📌 {{ strtoupper($booking->position) ?? 'Booking' }} +
+ @endforeach +
+
+
+
+
+ + + + +@stop \ No newline at end of file diff --git a/routes/web-public.php b/routes/web-public.php index 05050b7c64..6373ed044e 100644 --- a/routes/web-public.php +++ b/routes/web-public.php @@ -63,6 +63,7 @@ 'as' => 'bookings.', 'prefix' => 'bookings', ], function () { - Route::get('/calendar/{year?}/{month?}')->uses('BookingsPagesController@index')->name('calendar'); + Route::get('calendar/{year?}/{month?}')->uses('BookingsPagesController@index')->name('index'); + Route::get('/{id}')->uses('BookingsPagesController@show')->name('bookings.show'); }); }); From de6d1abd96fa2ce41d77f701673c49eb03b7a108 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Tue, 24 Jun 2025 22:58:20 +0100 Subject: [PATCH 03/20] Update BookingsPagesController.php --- .../Site/BookingsPagesController.php | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/Site/BookingsPagesController.php b/app/Http/Controllers/Site/BookingsPagesController.php index 51cba5d1a7..84f6538094 100644 --- a/app/Http/Controllers/Site/BookingsPagesController.php +++ b/app/Http/Controllers/Site/BookingsPagesController.php @@ -2,9 +2,9 @@ namespace App\Http\Controllers\Site; -use Carbon\Carbon; -use App\Repositories\Cts\BookingRepository; use App\Models\Cts\Booking; +use App\Repositories\Cts\BookingRepository; +use Carbon\Carbon; class BookingsPagesController extends \App\Http\Controllers\BaseController { @@ -17,21 +17,22 @@ public function __construct(BookingRepository $bookingRepo) public function show($id) { - $booking = Booking::findOrFail($id); + $booking = Booking::findOrFail($id); - return response()->json([ - 'id' => $booking->id, - 'position' => $booking->position, - 'controller_name' => $booking->member_id, - 'start_time' => $booking->from, - 'end_time' => $booking->to, - ]); + return response()->json([ + 'id' => $booking->id, + 'position' => $booking->position, + 'controller_name' => $booking->member_id, + 'start_time' => $booking->from, + 'end_time' => $booking->to, + ]); } + /** * Display the bookings calendar for a given month and year. * - * @param int|null $year - * @param int|null $month + * @param int|null $year + * @param int|null $month * @return \Illuminate\View\View */ public function index($year = null, $month = null) @@ -55,7 +56,7 @@ public function index($year = null, $month = null) }); $week[] = [ - 'date' =>$current->copy(), + 'date' => $current->copy(), 'bookings' => $dayBookings, ]; $current->addDay(); From 164d39526a4dd750c3362bc06f27bc5f652bfe74 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Tue, 24 Jun 2025 23:01:17 +0100 Subject: [PATCH 04/20] Update BookingRepository.php --- app/Repositories/Cts/BookingRepository.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/Repositories/Cts/BookingRepository.php b/app/Repositories/Cts/BookingRepository.php index 25085b6370..da7f6d9377 100644 --- a/app/Repositories/Cts/BookingRepository.php +++ b/app/Repositories/Cts/BookingRepository.php @@ -4,7 +4,6 @@ use App\Models\Cts\Booking; use Carbon\Carbon; -use Carbon\CarbonPeriod; use Illuminate\Support\Collection; class BookingRepository @@ -12,11 +11,11 @@ class BookingRepository public function getBookingsBetween($startDate, $endDate) { $bookings = Booking::whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()]) - ->get() - ->each(function ($booking) { - $booking->date = Carbon::parse($booking->date); - }); - + ->get() + ->each(function ($booking) { + $booking->date = Carbon::parse($booking->date); + }); + return $this->formatBookings($bookings); } From 44bd67f7ba106a9d601157fd31eb12f24d78bf49 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Tue, 24 Jun 2025 23:32:35 +0100 Subject: [PATCH 05/20] member_id to name --- .../Controllers/Site/BookingsPagesController.php | 6 +++--- app/Repositories/Cts/BookingRepository.php | 13 ++++++------- resources/views/site/bookings/index.blade.php | 12 ++++++++---- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/Site/BookingsPagesController.php b/app/Http/Controllers/Site/BookingsPagesController.php index 84f6538094..d92198db8e 100644 --- a/app/Http/Controllers/Site/BookingsPagesController.php +++ b/app/Http/Controllers/Site/BookingsPagesController.php @@ -22,9 +22,9 @@ public function show($id) return response()->json([ 'id' => $booking->id, 'position' => $booking->position, - 'controller_name' => $booking->member_id, - 'start_time' => $booking->from, - 'end_time' => $booking->to, + 'controller_name' => $booking->member->name ?? 'Unknown', + 'from' => $booking->from, + 'to' => $booking->to, ]); } diff --git a/app/Repositories/Cts/BookingRepository.php b/app/Repositories/Cts/BookingRepository.php index da7f6d9377..0fc57c50e5 100644 --- a/app/Repositories/Cts/BookingRepository.php +++ b/app/Repositories/Cts/BookingRepository.php @@ -8,16 +8,15 @@ class BookingRepository { + public function getBookingsBetween($startDate, $endDate) - { - $bookings = Booking::whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()]) - ->get() - ->each(function ($booking) { +{ + return Booking::with('member') + ->whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()]) + ->get()->each(function ($booking) { $booking->date = Carbon::parse($booking->date); }); - - return $this->formatBookings($bookings); - } +} public function getBookings(Carbon $date) { diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index d6995a890b..fe6a07800c 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -70,7 +70,7 @@ @foreach($day['bookings'] as $booking)
📌 {{ strtoupper($booking->position) ?? 'Booking' }} @@ -128,10 +128,14 @@ function closeBookingModal() { document.getElementById('booking-modal').classList.add('hidden'); document.getElementById('booking-modal').classList.remove('flex'); } - + function formatTime(timeStr) { - const time = new Date('1970-01-01T' + timeStr + 'Z'); - return time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + // Assumes timeStr is in "HH:mm" or "HH:mm:ss" and UTC + const [hour, minute] = timeStr.split(':'); + // Create a UTC date + const date = new Date(Date.UTC(1970, 0, 1, hour, minute)); + // Format as HH:mmZ + return date.toISOString().substr(11, 5) + 'Z'; } @stop \ No newline at end of file From d54913619bec5f2927f3b92fc7652571f1d44d2a Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Tue, 24 Jun 2025 23:37:31 +0100 Subject: [PATCH 06/20] Update BookingRepository.php --- app/Repositories/Cts/BookingRepository.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/Repositories/Cts/BookingRepository.php b/app/Repositories/Cts/BookingRepository.php index 0fc57c50e5..edf5e6f9a7 100644 --- a/app/Repositories/Cts/BookingRepository.php +++ b/app/Repositories/Cts/BookingRepository.php @@ -8,15 +8,14 @@ class BookingRepository { - public function getBookingsBetween($startDate, $endDate) -{ - return Booking::with('member') - ->whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()]) - ->get()->each(function ($booking) { + { + return Booking::with('member') + ->whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()]) + ->get()->each(function ($booking) { $booking->date = Carbon::parse($booking->date); }); -} + } public function getBookings(Carbon $date) { From aba2b103b27efbf6e2fa4620ddcb0e20dabb0ac4 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Sun, 10 Aug 2025 20:17:39 +0100 Subject: [PATCH 07/20] Further Progress Currently stuck on getting Mentor Name and Accepted date --- resources/views/site/bookings/index.blade.php | 244 ++++++++++-------- .../site/bookings/partials/tooltip.blade.php | 37 +++ 2 files changed, 169 insertions(+), 112 deletions(-) create mode 100644 resources/views/site/bookings/partials/tooltip.blade.php diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index fe6a07800c..dd4f820153 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -2,140 +2,160 @@ @section('content') +@php + use Carbon\Carbon; + + if (is_string($date)) { + $date = Carbon::parse($date); + } + + $typeRowStyles = [ + 'mentoring' => 'bg-blue-100 border-blue-400', + 'EX' => 'color:red; bg-red-100 border-red-400 font-bold', + 'solo' => 'bg-yellow-100 border-yellow-400', + 'normal' => 'bg-gray-100 border-gray-300', + ]; +@endphp -
-
-
  Bookings Calendar -
-
-

- The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. -

-
-
+
+
+
+   Bookings Calendar +
+
+

+ The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. +

+
+
-
-
-
{{ $date->format('F Y') }}
-
-
-

Use the links below to navigate through the months.

- - ← Previous -  |  - - Next → -
-
- - - - @foreach(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as $day) - - - @endforeach - - - - @foreach($calendar as $week) - - @foreach($week as $day) - + @endforeach + +
{{ $day }}
- {{ $day['date']->day }} - @foreach($day['bookings'] as $booking) -
- 📌 {{ strtoupper($booking->position) ?? 'Booking' }} -
- @endforeach +
+
+
{{ $date->format('F Y') }}
+
+
+

Use the links below to navigate through the months.

+ ← Previous +  |  + Next → +
+
+ + + + @foreach(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as $day) + + @endforeach + + + + @foreach($calendar as $week) + + @foreach($week as $day) + @php + $dayDate = is_string($day['date']) ? Carbon::parse($day['date']) : $day['date']; + @endphp + - @endforeach - @endforeach - -
{{ $day }}
+ {{ $dayDate->day }} + @if(!$dayDate->isBefore(Carbon::today())) + @foreach($day['bookings'] as $booking) + @php + $type = $booking->type ?? 'normal'; + $rowClass = $typeRowStyles[$type] ?? 'bg-white border-gray-200'; + $bookingTypeClass = ''; + if ($booking->isMentoring()) { + $bookingTypeClass = 'booking-mentoring'; + } elseif ($booking->isEvent()) { + $bookingTypeClass = 'booking-event'; + } elseif ($booking->isExam()) { + $bookingTypeClass = 'booking-exam'; + } + $fromTime = Carbon::parse($booking->from)->format('H:i'); + $toTime = Carbon::parse($booking->to)->format('H:i'); + $tooltipHtml = !$booking->isEvent() ? view('site.bookings.partials.tooltip', compact('booking', 'dayDate', 'fromTime', 'toTime'))->render() : null; + @endphp +
+ 📌 {{ strtoupper($booking->position) ?? 'Booking' }}
+ 🕒 {{ $fromTime }} - {{ $toTime }} + @if(!$booking->isEvent()) +
+ {!! $tooltipHtml !!} +
+ @endif +
+ @endforeach + @endif
-
-
+
- - - - -@stop \ No newline at end of file +@stop diff --git a/resources/views/site/bookings/partials/tooltip.blade.php b/resources/views/site/bookings/partials/tooltip.blade.php new file mode 100644 index 0000000000..e05d2d05f9 --- /dev/null +++ b/resources/views/site/bookings/partials/tooltip.blade.php @@ -0,0 +1,37 @@ +@php + if ($booking->type === 'EX') { + $displayType = 'Confirmed Practical Exam'; + $displayName = 'HIDDEN'; + $bookedLabel = 'Requested on'; + $showMentor = true; + } elseif ($booking->type === 'ME') { + $displayType = 'Confirmed Mentoring Session'; + $displayName = 'HIDDEN'; + $bookedLabel = 'Requested on'; + $showMentor = true; + } else { + $displayType = 'Position Booking'; + $displayName = $booking->member->name ?? 'Unknown'; + $bookedLabel = 'Booked on'; + $showMentor = false; + } +@endphp + +
+

Booking Information

+

Booking Type: {{ $displayType }}

+

Position: {{ $booking->position }}

+

Date: {{ $dayDate->format('D jS M Y') }}

+

Book Time: {{ $fromTime }} - {{ $toTime }}

+
+

Booked By: {{ $displayName }}

+

{{ $bookedLabel }}: {{ $booking->requested_at ? Carbon::parse($booking->requested_at)->format('d/m/Y H:i:s') : 'N/A' }}

+ @if($showMentor) +
+

Mentor: {{ $booking->mentor->name ?? 'Unknown' }}

+

Accepted on: {{ $booking->accepted_at ? Carbon\Carbon::parse($booking->accepted_at)->format('d/m/Y H:i:s') : 'N/A' }}

+ @endif + @if(!empty($booking->notes)) + Notes: {{ $booking->notes }} + @endif +
\ No newline at end of file From 5de4cc0e3920a096a81ead9ffb638195e1da780a Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Mon, 18 Aug 2025 15:06:21 +0100 Subject: [PATCH 08/20] Further improvements --- app/Repositories/Cts/BookingRepository.php | 32 ++++++-- resources/views/site/bookings/index.blade.php | 25 +++++++ .../site/bookings/partials/tooltip.blade.php | 74 ++++++++++++++----- 3 files changed, 106 insertions(+), 25 deletions(-) diff --git a/app/Repositories/Cts/BookingRepository.php b/app/Repositories/Cts/BookingRepository.php index edf5e6f9a7..f64558dbf9 100644 --- a/app/Repositories/Cts/BookingRepository.php +++ b/app/Repositories/Cts/BookingRepository.php @@ -10,7 +10,7 @@ class BookingRepository { public function getBookingsBetween($startDate, $endDate) { - return Booking::with('member') + return Booking::with(['member', 'session.mentor']) ->whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()]) ->get()->each(function ($booking) { $booking->date = Carbon::parse($booking->date); @@ -20,7 +20,7 @@ public function getBookingsBetween($startDate, $endDate) public function getBookings(Carbon $date) { $bookings = Booking::where('date', '=', $date->toDateString()) - ->with('member') + ->with(['member', 'session.mentor']) ->orderBy('from') ->get(); @@ -30,7 +30,7 @@ public function getBookings(Carbon $date) public function getTodaysBookings() { $bookings = Booking::where('date', '=', Carbon::now()->toDateString()) - ->with('member') + ->with(['member', 'session.mentor']) ->orderBy('from') ->get(); @@ -41,7 +41,7 @@ public function getTodaysLiveAtcBookings() { $bookings = Booking::where('date', '=', Carbon::now()->toDateString()) ->networkAtc() - ->with('member') + ->with(['member', 'session.mentor']) ->orderBy('from') ->get(); @@ -53,7 +53,7 @@ public function getTodaysLiveAtcBookingsWithoutEvents() $bookings = Booking::where('date', '=', Carbon::now()->toDateString()) ->notEvent() ->networkAtc() - ->with('member') + ->with(['member', 'session.mentor']) ->orderBy('from') ->get(); @@ -69,6 +69,28 @@ private function formatBookings(Collection $bookings) $booking->member = $this->formatMember($booking); $booking->unsetRelation('member'); + if ($booking->type === 'ME' && $booking->session) { + $mentorName = 'Unknown'; + + // Safely get mentor name and ID + if ($booking->session->mentor) { + $mentorName = $booking->session->mentor->name.' ('.$booking->session->mentor->cid.')'; + } + + $booking->session_details = [ + 'id' => $booking->session->id, + 'position' => $booking->session->position, + 'student_id' => $booking->session->student_id, + 'mentor_id' => $booking->session->mentor_id, + 'mentor' => $mentorName, + 'date' => $booking->session->date_1, + 'from' => $booking->session->from_1, + 'to' => $booking->session->to_1, + 'request_time' => $booking->session->request_time, + 'taken_time' => $booking->session->taken_time, + ]; + } + return $booking; }); diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index dd4f820153..02cff34cc7 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -158,4 +158,29 @@
+
+
+
+   Bookings Calendar +
+
+

+ The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. +

+
+
+
+ +
+
+
+   Bookings Calendar +
+
+

+ The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. +

+
+
+
@stop diff --git a/resources/views/site/bookings/partials/tooltip.blade.php b/resources/views/site/bookings/partials/tooltip.blade.php index e05d2d05f9..8efe67f989 100644 --- a/resources/views/site/bookings/partials/tooltip.blade.php +++ b/resources/views/site/bookings/partials/tooltip.blade.php @@ -1,20 +1,48 @@ @php - if ($booking->type === 'EX') { - $displayType = 'Confirmed Practical Exam'; - $displayName = 'HIDDEN'; - $bookedLabel = 'Requested on'; - $showMentor = true; - } elseif ($booking->type === 'ME') { - $displayType = 'Confirmed Mentoring Session'; - $displayName = 'HIDDEN'; - $bookedLabel = 'Requested on'; - $showMentor = true; - } else { - $displayType = 'Position Booking'; - $displayName = $booking->member->name ?? 'Unknown'; - $bookedLabel = 'Booked on'; - $showMentor = false; +use Carbon\Carbon; + +if (!function_exists('formatDate')) { + function formatDate($datetime) { + return $datetime ? Carbon::parse($datetime)->format('d/m/Y H:i:s') : 'N/A'; + } +} + +// Default values +$displayType = 'Position Booking'; +$displayName = $booking->member ? $booking->member->name : 'Unknown'; +$bookedLabel = 'Booked on'; +$sessionRoles = []; // Will hold dynamic roles (mentor, examiner, etc.) +$requestTime = 'N/A'; +$takenTime = 'N/A'; + +// Handle session types dynamically +if ($booking->session) { + switch ($booking->type) { + case 'EX': + $displayType = 'Confirmed Practical Exam'; + $displayName = 'HIDDEN'; + $bookedLabel = 'Requested on'; + //if ($booking->session->examiner) { + // $sessionRoles['Examiner'] = $booking->session->examiner->name . ' (' . $booking->session->examiner->cid . ')'; + //} + break; + + case 'ME': + $displayType = 'Confirmed Mentoring Session'; + $displayName = 'HIDDEN'; + $bookedLabel = 'Requested on'; + if ($booking->session->mentor) { + $sessionRoles['Mentor'] = $booking->session->mentor->name . ' (' . $booking->session->mentor->cid . ')'; + } + break; } + + // Common times for all session types + $requestTime = formatDate($booking->session->request_time); + $takenTime = formatDate($booking->session->taken_time); +} + +$timeBooked = formatDate($booking->time_booked); @endphp
@@ -25,13 +53,19 @@

Book Time: {{ $fromTime }} - {{ $toTime }}


Booked By: {{ $displayName }}

-

{{ $bookedLabel }}: {{ $booking->requested_at ? Carbon::parse($booking->requested_at)->format('d/m/Y H:i:s') : 'N/A' }}

- @if($showMentor) + + @if(!empty($sessionRoles)) +

Requested on: {{ $requestTime }}


-

Mentor: {{ $booking->mentor->name ?? 'Unknown' }}

-

Accepted on: {{ $booking->accepted_at ? Carbon\Carbon::parse($booking->accepted_at)->format('d/m/Y H:i:s') : 'N/A' }}

+ @foreach($sessionRoles as $role => $name) +

{{ $role }}: {{ $name }}

+

Accepted on: {{ $takenTime }}

+ @endforeach + @else +

{{ $bookedLabel }}: {{ $timeBooked }}

@endif + @if(!empty($booking->notes)) Notes: {{ $booking->notes }} @endif -
\ No newline at end of file +
From 34e103e177ce95effc7118ed7e4ef893c41209d9 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Mon, 18 Aug 2025 15:25:08 +0100 Subject: [PATCH 09/20] Forgot to commit Booking --- app/Models/Cts/Booking.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/Models/Cts/Booking.php b/app/Models/Cts/Booking.php index 39178f22b6..ce7a9c47ef 100644 --- a/app/Models/Cts/Booking.php +++ b/app/Models/Cts/Booking.php @@ -64,4 +64,14 @@ public function isMentoring() { return $this->type == 'ME'; } + + public function session() + { + return $this->belongsTo(\App\Models\Cts\Session::class, 'type_id', 'id'); + } + + public function exams() + { + return $this->belongsTo(\App\Models\Cts\ExamBooking::class, 'type_id', 'id'); + } } From b284488cd2de9f7628f05cfc6e7d99959d4136b5 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Mon, 18 Aug 2025 16:10:02 +0100 Subject: [PATCH 10/20] Bookings now display Bookings now Display For Events, Exams and Mentoring --- app/Models/Cts/Booking.php | 2 +- app/Models/Cts/Exams.php | 24 +++++++ app/Models/Cts/Session.php | 2 +- app/Repositories/Cts/BookingRepository.php | 22 ++++++ .../site/bookings/partials/tooltip.blade.php | 68 +++++++++++-------- 5 files changed, 86 insertions(+), 32 deletions(-) create mode 100644 app/Models/Cts/Exams.php diff --git a/app/Models/Cts/Booking.php b/app/Models/Cts/Booking.php index ce7a9c47ef..11ce0e65bb 100644 --- a/app/Models/Cts/Booking.php +++ b/app/Models/Cts/Booking.php @@ -72,6 +72,6 @@ public function session() public function exams() { - return $this->belongsTo(\App\Models\Cts\ExamBooking::class, 'type_id', 'id'); + return $this->belongsTo(\App\Models\Cts\Exams::class, 'type_id', 'id'); } } diff --git a/app/Models/Cts/Exams.php b/app/Models/Cts/Exams.php new file mode 100644 index 0000000000..d24f102d74 --- /dev/null +++ b/app/Models/Cts/Exams.php @@ -0,0 +1,24 @@ +belongsTo(Member::class, 'exmr_id', 'id'); + } +} \ No newline at end of file diff --git a/app/Models/Cts/Session.php b/app/Models/Cts/Session.php index 6e0eed6467..b5ce8dae68 100644 --- a/app/Models/Cts/Session.php +++ b/app/Models/Cts/Session.php @@ -18,5 +18,5 @@ class Session extends Model public function mentor() { return $this->belongsTo(Member::class, 'mentor_id', 'id'); - } + } } diff --git a/app/Repositories/Cts/BookingRepository.php b/app/Repositories/Cts/BookingRepository.php index f64558dbf9..c2ecb927a3 100644 --- a/app/Repositories/Cts/BookingRepository.php +++ b/app/Repositories/Cts/BookingRepository.php @@ -91,6 +91,28 @@ private function formatBookings(Collection $bookings) ]; } + if ($booking->type === 'EX' && $booking->exams) { + $examinerName = 'Unknown'; + + // Safely get mentor name and ID + if ($booking->exams->mentor) { + $examinerName = $booking->exams->examiner->name.' ('.$booking->exams->examiner->cid.')'; + } + + $booking->exams_details = [ + 'id' => $booking->exams->id, + 'position' => $booking->exams->position, + 'student_id' => $booking->exams->student_id, + 'exmr_id' => $booking->exams->exmr_id, + 'examiner' => $examinerName, + 'date' => $booking->exams->date_1, + 'from' => $booking->exams->from_1, + 'to' => $booking->exams->to_1, + 'time_book' => $booking->exams->time_book, + 'taken_time' => $booking->exams->time_taken, + ]; + } + return $booking; }); diff --git a/resources/views/site/bookings/partials/tooltip.blade.php b/resources/views/site/bookings/partials/tooltip.blade.php index 8efe67f989..997e8a6386 100644 --- a/resources/views/site/bookings/partials/tooltip.blade.php +++ b/resources/views/site/bookings/partials/tooltip.blade.php @@ -11,35 +11,41 @@ function formatDate($datetime) { $displayType = 'Position Booking'; $displayName = $booking->member ? $booking->member->name : 'Unknown'; $bookedLabel = 'Booked on'; -$sessionRoles = []; // Will hold dynamic roles (mentor, examiner, etc.) +$showMentor = false; +$showExaminer = false; +$mentor = 'N/A'; +$examiner = 'N/A'; $requestTime = 'N/A'; $takenTime = 'N/A'; -// Handle session types dynamically -if ($booking->session) { - switch ($booking->type) { - case 'EX': - $displayType = 'Confirmed Practical Exam'; - $displayName = 'HIDDEN'; - $bookedLabel = 'Requested on'; - //if ($booking->session->examiner) { - // $sessionRoles['Examiner'] = $booking->session->examiner->name . ' (' . $booking->session->examiner->cid . ')'; - //} - break; +// EX (Practical Exam) +if ($booking->type === 'EX') { + $displayType = 'Confirmed Practical Exam'; + $displayName = 'HIDDEN'; + $bookedLabel = 'Requested on'; + $showExaminer = true; - case 'ME': - $displayType = 'Confirmed Mentoring Session'; - $displayName = 'HIDDEN'; - $bookedLabel = 'Requested on'; - if ($booking->session->mentor) { - $sessionRoles['Mentor'] = $booking->session->mentor->name . ' (' . $booking->session->mentor->cid . ')'; - } - break; + if ($booking->exams && $booking->exams->examiner) { + $examiner = $booking->exams->examiner->name . ' (' . $booking->exams->examiner->cid . ')'; } - // Common times for all session types - $requestTime = formatDate($booking->session->request_time); - $takenTime = formatDate($booking->session->taken_time); + $requestTime = $booking->exams ? formatDate($booking->exams->time_book) : 'N/A'; + $takenTime = $booking->exams ? formatDate($booking->exams->time_taken) : 'N/A'; +} + +// ME (Mentoring Session) +if ($booking->type === 'ME') { + $displayType = 'Confirmed Mentoring Session'; + $displayName = 'HIDDEN'; + $bookedLabel = 'Requested on'; + $showMentor = true; + + if ($booking->session && $booking->session->mentor) { + $mentor = $booking->session->mentor->name . ' (' . $booking->session->mentor->cid . ')'; + } + + $requestTime = $booking->session ? formatDate($booking->session->request_time) : 'N/A'; + $takenTime = $booking->session ? formatDate($booking->session->taken_time) : 'N/A'; } $timeBooked = formatDate($booking->time_booked); @@ -53,14 +59,16 @@ function formatDate($datetime) {

Book Time: {{ $fromTime }} - {{ $toTime }}


Booked By: {{ $displayName }}

- - @if(!empty($sessionRoles)) + @if($showExaminer) +

Requested on: {{ $requestTime }}

+
+

Examiner: {{ $examiner }}

+

Accepted on: {{ $takenTime }}

+ @elseif($showMentor)

Requested on: {{ $requestTime }}


- @foreach($sessionRoles as $role => $name) -

{{ $role }}: {{ $name }}

-

Accepted on: {{ $takenTime }}

- @endforeach +

Mentor: {{ $mentor }}

+

Accepted on: {{ $takenTime }}

@else

{{ $bookedLabel }}: {{ $timeBooked }}

@endif @@ -68,4 +76,4 @@ function formatDate($datetime) { @if(!empty($booking->notes)) Notes: {{ $booking->notes }} @endif - + \ No newline at end of file From 621bab546cb32b9a6064d2a617dc02b64b627079 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Mon, 18 Aug 2025 16:12:12 +0100 Subject: [PATCH 11/20] Linting --- app/Models/Cts/Booking.php | 2 +- app/Models/Cts/Exams.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Models/Cts/Booking.php b/app/Models/Cts/Booking.php index 11ce0e65bb..656b9353af 100644 --- a/app/Models/Cts/Booking.php +++ b/app/Models/Cts/Booking.php @@ -73,5 +73,5 @@ public function session() public function exams() { return $this->belongsTo(\App\Models\Cts\Exams::class, 'type_id', 'id'); - } + } } diff --git a/app/Models/Cts/Exams.php b/app/Models/Cts/Exams.php index d24f102d74..a4cb57d68c 100644 --- a/app/Models/Cts/Exams.php +++ b/app/Models/Cts/Exams.php @@ -20,5 +20,5 @@ class Exams extends Model public function examiner() { return $this->belongsTo(Member::class, 'exmr_id', 'id'); - } -} \ No newline at end of file + } +} From faa0a74cad8701767975643600b14f7a31ec8706 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Mon, 18 Aug 2025 16:15:41 +0100 Subject: [PATCH 12/20] Linting again --- app/Models/Cts/Session.php | 2 +- resources/views/site/bookings/index.blade.php | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/Models/Cts/Session.php b/app/Models/Cts/Session.php index b5ce8dae68..6e0eed6467 100644 --- a/app/Models/Cts/Session.php +++ b/app/Models/Cts/Session.php @@ -18,5 +18,5 @@ class Session extends Model public function mentor() { return $this->belongsTo(Member::class, 'mentor_id', 'id'); - } + } } diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index 02cff34cc7..08c5e8bba1 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -158,15 +158,26 @@ -
+
-   Bookings Calendar +   Legend
-

- The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. -

+
+ + Mentoring + + + Event + + + Exam + + + Normal Booking + +
From 9471adf828aa78dbf65ff98187db89e728bd342b Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Mon, 18 Aug 2025 17:21:59 +0100 Subject: [PATCH 13/20] Need to fix the Tests Due to changes I need to fix the Tests --- resources/assets/sass/app.scss | 190 +++++++++++++---- resources/views/site/bookings/index.blade.php | 200 ++++++++++-------- .../site/bookings/partials/tooltip.blade.php | 1 - 3 files changed, 266 insertions(+), 125 deletions(-) diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 05ce5714ca..3b3634bd22 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -10,7 +10,8 @@ $screen-sm: 802px; @import "_padding-utils"; @import "_top-notification"; -body, html { +body, +html { margin: 0 auto; font-size: 14px; font-family: Calibri, Tahoma, Geneva, sans-serif; @@ -33,15 +34,16 @@ body, html { height: 70%; float: left; margin: 10px 15px 0 5.0%; + img { height: 100%; } } .dev_environment_notification { - height: 20px; - background-color: red; - text-align: center; + height: 20px; + background-color: red; + text-align: center; } .sys_notification { @@ -50,13 +52,13 @@ body, html { position: relative; .text { - margin: 0; - position: absolute; - top: 50%; - -ms-transform: translateY(-50%); - transform: translateY(-50%); - text-align: center; - width: 100%; + margin: 0; + position: absolute; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); + text-align: center; + width: 100%; } } @@ -68,6 +70,7 @@ body, html { transform-style: preserve-3d; transform: perspective(auto) translateZ(-30vw) scale(1.4); perspective: 1000; + img { margin-top: 90px; height: 260px; @@ -77,8 +80,9 @@ body, html { .fuzzy_date { color: #337ab7; text-decoration: none; - &:hover{ - cursor:pointer; + + &:hover { + cursor: pointer; } } @@ -86,11 +90,11 @@ body, html { color: white !important; } -.vertical-center{ +.vertical-center { vertical-align: middle !important; } -.table.table-borders >tbody > tr > td { +.table.table-borders>tbody>tr>td { border: 1px solid grey; } @@ -109,7 +113,8 @@ body, html { width: 90%; } -.breadcrumb_content_left, .breadcrumb_content_right { +.breadcrumb_content_left, +.breadcrumb_content_right { margin: 0 auto; padding-top: 8px; height: 100%; @@ -205,7 +210,9 @@ body, html { border: 0; border-top-left-radius: 0; border-top-right-radius: 0; - > .panel-heading, > .panel-heading-link > .panel-heading { + + >.panel-heading, + >.panel-heading-link>.panel-heading { background-color: #17375e; border-color: #17375E; color: white; @@ -226,36 +233,42 @@ body, html { padding: 5px; } - > .panel-title a { + >.panel-title a { color: white; } } + .panel-body .row-text-contain { word-break: break-word; } - .panel-footer.panel-footer-primary { - color: white; - background-color: $brand-primary; - a { - color: white; - } - a:hover { - color: $gray-lighter; - } + + .panel-footer.panel-footer-primary { + color: white; + background-color: $brand-primary; + + a { + color: white; } + + a:hover { + color: $gray-lighter; + } + } } .panel-uk-danger { border: 0; border-top-left-radius: 0; border-top-right-radius: 0; - > .panel-heading { + + >.panel-heading { background-color: #c30c1d; border-color: #840712; color: white; border-top-left-radius: 0; border-top-right-radius: 0; } + .panel-body .row-text-contain { word-break: break-word; } @@ -265,20 +278,24 @@ body, html { border: 0; border-top-left-radius: 0; border-top-right-radius: 0; - > .panel-heading { + + >.panel-heading { background-color: #009700; border-color: #006900; color: white; border-top-left-radius: 0; border-top-right-radius: 0; } + .panel-body .row-text-contain { word-break: break-word; } } -.panel-group#unreadNotifications, .panel-group#readNotifications { +.panel-group#unreadNotifications, +.panel-group#readNotifications { padding: 10px; + .panel { margin-bottom: 15px; } @@ -309,18 +326,117 @@ body, html { .profile-picture { object-fit: cover; border-radius: 50px; - height:50px; + height: 50px; width: 50px; } .equal { - display: flex; - display: -webkit-flex; - flex-wrap: wrap; + display: flex; + display: -webkit-flex; + flex-wrap: wrap; } .flex-centered { - display: flex; - justify-content: center; - align-items: center; + display: flex; + justify-content: center; + align-items: center; +} + +.calendar { + width: 100%; + max-width: 850px; + margin: 0 auto; + border-collapse: collapse; + table-layout: fixed; +} + +.calendar th, +.calendar td { + width: 14.28%; + height: 100px; + text-align: center; + font-weight: bold; + vertical-align: top; + padding: 5px; + border: 1px solid #ccc; + box-sizing: border-box; +} + +.bg-gray-100 { + background-color: #f0f0f0; +} + +.booking-entry { + cursor: pointer; + text-decoration: none; + font-size: 15px; + margin-top: 5px; +} + +.booking-entry:hover { + text-decoration: underline; +} + +.today-cell { + background-color: #FFFFCC !important; + border: 2px solid #FFCC00 !important; +} + +.booking { + color: #336633; + border-color: #336633; + padding: 4px; + border: 2px solid transparent; + border-radius: 4px; +} + +.booking-mentoring { + color: #7429C7; + border-color: #7429C7; +} + +.booking-event { + color: #FF0000; + border-color: #FF0000; +} + +.booking-exam { + color: #993300; + border-color: #993300; +} + +.tooltip-container { + position: relative; + display: inline-block; +} + +.tooltip-content { + display: none; + position: absolute; + z-index: 10; + left: 100%; + top: 0; + min-width: 250px; + background: #fff; + color: #222; + border: 1px solid #888; + border-radius: 6px; + padding: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + white-space: normal; + font-size: 13px; +} + +.tooltip-container:hover .tooltip-content, +.tooltip-container:focus-within .tooltip-content { + display: block; } + +.no-x-overflow { + overflow-x: visible !important; +} + +.legend-table td { + vertical-align: middle !important; + padding: 6px 10px !important; +} \ No newline at end of file diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index 08c5e8bba1..2c20ab3cef 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -17,66 +17,6 @@ ]; @endphp - -
@@ -132,12 +72,29 @@ $bookingTypeClass = 'booking-event'; } elseif ($booking->isExam()) { $bookingTypeClass = 'booking-exam'; + } else { + $bookingTypeClass = 'booking'; } $fromTime = Carbon::parse($booking->from)->format('H:i'); $toTime = Carbon::parse($booking->to)->format('H:i'); $tooltipHtml = !$booking->isEvent() ? view('site.bookings.partials.tooltip', compact('booking', 'dayDate', 'fromTime', 'toTime'))->render() : null; + // Decide a normalized kind for filtering + $kind = 'normal'; + if ($booking->isEvent()) { + $kind = 'event'; + } elseif ($booking->isExam()) { + $kind = 'exam'; + } elseif ($booking->isMentoring()) { + $kind = 'mentoring'; + } elseif (strtolower($booking->type ?? '') === 'seminar') { + $kind = 'seminar'; + } @endphp -
+
user_id === Auth::id()) + border-4 + @endif" + tabindex="0" data-kind="{{ $kind }}"> 📌 {{ strtoupper($booking->position) ?? 'Booking' }}
🕒 {{ $fromTime }} - {{ $toTime }} @if(!$booking->isEvent()) @@ -158,40 +115,109 @@
+
-   Legend +   Calendar Legend / Filter
-
- - Mentoring - - - Event - - - Exam - - - Normal Booking - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Booking + +
Mentoring + +
Seminar + +
Exam + +
Event + +
+ +{{--
+ +    + +
+
-
-
-
-
-
-
-   Bookings Calendar -
-
-

- The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. -

+
+ + +
--}}
+ + @stop diff --git a/resources/views/site/bookings/partials/tooltip.blade.php b/resources/views/site/bookings/partials/tooltip.blade.php index 997e8a6386..f3690c0755 100644 --- a/resources/views/site/bookings/partials/tooltip.blade.php +++ b/resources/views/site/bookings/partials/tooltip.blade.php @@ -63,7 +63,6 @@ function formatDate($datetime) {

Requested on: {{ $requestTime }}


Examiner: {{ $examiner }}

-

Accepted on: {{ $takenTime }}

@elseif($showMentor)

Requested on: {{ $requestTime }}


From 4cb355ac019ecedfef1acd2db71acc36575768b4 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Mon, 1 Sep 2025 08:05:11 +0900 Subject: [PATCH 14/20] Changes Included local time worked on display old bookings need to correct the logic --- .nvmrc | 1 + app/Models/Cts/Booking.php | 5 + resources/assets/sass/app.scss | 5 + resources/views/site/bookings/index.blade.php | 320 ++++++++++++++---- 4 files changed, 257 insertions(+), 74 deletions(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..cabf43b5dd --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 \ No newline at end of file diff --git a/app/Models/Cts/Booking.php b/app/Models/Cts/Booking.php index 656b9353af..dadbf8f5ca 100644 --- a/app/Models/Cts/Booking.php +++ b/app/Models/Cts/Booking.php @@ -65,6 +65,11 @@ public function isMentoring() return $this->type == 'ME'; } + public function isSeminar() + { + return $this->type == 'GS'; + } + public function session() { return $this->belongsTo(\App\Models\Cts\Session::class, 'type_id', 'id'); diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 3b3634bd22..0d4d7f52cc 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -405,6 +405,11 @@ html { border-color: #993300; } +.booking-seminar { + color: #FFA500; + border-color: #FFA500; +} + .tooltip-container { position: relative; display: inline-block; diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index 2c20ab3cef..5f36531ad0 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -17,15 +17,45 @@ ]; @endphp -
+
  Bookings Calendar
+

The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below.

+ + +
+
+
+
+ +
+
+
+   Information +
+
+
+

+ All times on Bookings are in UTC (Zulu). +

+ +

+ Your local time is: + (Local) +
+ The current Zulu time is + (UTC/Zulu) +
+ +

+

+
@@ -56,7 +86,7 @@ @php $dayDate = is_string($day['date']) ? Carbon::parse($day['date']) : $day['date']; @endphp - format('Y-m-d') }}" class="align-top border p-2 {{ $dayDate->month !== $date->month ? 'bg-gray-100' : '' }} {{ $dayDate->isToday() ? 'today-cell' : '' }}"> {{ $dayDate->day }} @@ -72,6 +102,8 @@ $bookingTypeClass = 'booking-event'; } elseif ($booking->isExam()) { $bookingTypeClass = 'booking-exam'; + } elseif ($booking->isSeminar()) { + $bookingTypeClass = 'booking-seminar'; } else { $bookingTypeClass = 'booking'; } @@ -86,7 +118,7 @@ $kind = 'exam'; } elseif ($booking->isMentoring()) { $kind = 'mentoring'; - } elseif (strtolower($booking->type ?? '') === 'seminar') { + } elseif ($booking->isSeminar()) { $kind = 'seminar'; } @endphp @@ -96,7 +128,11 @@ @endif" tabindex="0" data-kind="{{ $kind }}"> 📌 {{ strtoupper($booking->position) ?? 'Booking' }}
- 🕒 {{ $fromTime }} - {{ $toTime }} + 🕒 + {{ $fromTime }}z - {{ $toTime }}z + @if(!$booking->isEvent())
{!! $tooltipHtml !!} @@ -122,77 +158,79 @@   Calendar Legend / Filter
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Booking - -
Mentoring - -
Seminar - -
Exam - -
Event - -
- -{{--
- -    - -
- -
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Booking + +
Mentoring + +
Seminar + +
Exam + +
Event + +
+
-
- - -
--}} + +
+
+ + +
+
+
@@ -218,6 +256,140 @@ function applyTypeFilters() { // Run once on load applyTypeFilters(); }); +document.addEventListener("DOMContentLoaded", function () { + function updateTimes() { + const now = new Date(); + + // Local time + zone + const local = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // UTC time + const utc = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: 'UTC' }); + + // Offset + const offsetMinutes = -now.getTimezoneOffset(); + const absMinutes = Math.abs(offsetMinutes); + const hours = Math.floor(absMinutes / 60); + const minutes = absMinutes % 60; + + let relation = ""; + if (offsetMinutes === 0) { + relation = "the same as Zulu."; + } else if (offsetMinutes > 0) { + relation = `${hours} hour${hours !== 1 ? "s" : ""}${minutes ? ` ${minutes} min` : ""} ahead of Zulu.`; + } else { + relation = `${hours} hour${hours !== 1 ? "s" : ""}${minutes ? ` ${minutes} min` : ""} behind Zulu.`; + } + + // Conversion hint + let conversionHint = ""; + if (offsetMinutes === 0) { + conversionHint = "No conversion is needed."; + } else if (offsetMinutes > 0) { + conversionHint = `To convert any time on Bookings to your local time, simply add ${hours} hour${hours !== 1 ? "s" : ""}${minutes ? ` ${minutes} min` : ""}.`; + } else { + conversionHint = `To convert any time on Bookings to your local time, simply subtract ${hours} hour${hours !== 1 ? "s" : ""}${minutes ? ` ${minutes} min` : ""}.`; + } + + document.getElementById("tz-message").innerHTML = + `Your local time (${tz}) is ${relation}
${conversionHint}`; + } + + updateTimes(); + setInterval(updateTimes, 1000); +}); +document.addEventListener("DOMContentLoaded", function () { + function updateTimes() { + const now = new Date(); + + // Local time + const local = now.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + // UTC time + const utc = now.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZone: 'UTC' + }); + + document.getElementById("local-time").textContent = local; + document.getElementById("utc-time").textContent = utc; + } + + updateTimes(); + setInterval(updateTimes, 1000); +}); +document.addEventListener("DOMContentLoaded", function () { + function filterBookings() { + const nowUtc = new Date(); + const nowDate = nowUtc.toISOString().split("T")[0]; + const nowMinutes = nowUtc.getUTCHours() * 60 + nowUtc.getUTCMinutes(); + const showPast = document.getElementById("filter-old").checked; + + document.querySelectorAll(".booking-entry").forEach(entry => { + const parentCell = entry.closest("td"); + if (!parentCell) return; + + const bookingDate = parentCell.getAttribute("data-date"); + if (!bookingDate) return; + + const timeMatch = entry.innerText.match(/(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})/); + if (timeMatch) { + const [, , , toH, toM] = timeMatch.map(Number); + const bookingEnd = toH * 60 + toM; + + // Hide if booking is in the past, unless checkbox is ticked + if (!showPast && (bookingDate < nowDate || (bookingDate === nowDate && bookingEnd < nowMinutes))) { + entry.style.display = "none"; + } else { + entry.style.display = ""; + } + } + }); + } + + // Initial run + auto refresh + filterBookings(); + setInterval(filterBookings, 60000); + + // Checkbox toggle + document.getElementById("filter-old").addEventListener("change", filterBookings); +}); +document.addEventListener("DOMContentLoaded", function () { + const toggleLocalTime = document.getElementById("toggle-localtime"); + + function updateBookingTimes() { + const useLocal = toggleLocalTime.checked; + + document.querySelectorAll(".booking-time").forEach(span => { + const startUtc = new Date(span.dataset.start); + const endUtc = new Date(span.dataset.end); + + if (useLocal) { + let startLocal = startUtc.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + let endLocal = endUtc.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + let tzName = Intl.DateTimeFormat().resolvedOptions().timeZone; + + span.textContent = `${startLocal} - ${endLocal} (${tzName})`; + } else { + let startZulu = startUtc.toISOString().substr(11,5); + let endZulu = endUtc.toISOString().substr(11,5); + span.textContent = `${startZulu} - ${endZulu}z`; + } + }); + } + + // Run once at page load so it's consistent with checkbox state + updateBookingTimes(); + + toggleLocalTime.addEventListener("change", updateBookingTimes); +}); @stop From 4e5797b1d054dad8aedfa9af3c523d15c96f9915 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Mon, 1 Sep 2025 08:10:07 +0900 Subject: [PATCH 15/20] Got rid of the Emoji --- resources/views/site/bookings/index.blade.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index 5f36531ad0..e208a35c3d 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -90,6 +90,7 @@ {{ $dayDate->month !== $date->month ? 'bg-gray-100' : '' }} {{ $dayDate->isToday() ? 'today-cell' : '' }}"> {{ $dayDate->day }} +
@if(!$dayDate->isBefore(Carbon::today())) @foreach($day['bookings'] as $booking) @php @@ -127,11 +128,11 @@ border-4 @endif" tabindex="0" data-kind="{{ $kind }}"> - 📌 {{ strtoupper($booking->position) ?? 'Booking' }}
- 🕒 position) ?? 'Booking' }}
+ - {{ $fromTime }}z - {{ $toTime }}z + {{ $fromTime }} z - {{ $toTime }}z @if(!$booking->isEvent())
From f6ba9bdfef7c5e3ae19fdc1c3124641b5856bba6 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Sun, 12 Oct 2025 05:59:20 +0100 Subject: [PATCH 16/20] Finally working Fixed Old Bookings not Displaying --- resources/assets/sass/app.scss | 9 + resources/views/site/bookings/index.blade.php | 367 ++++++++---------- .../site/bookings/partials/tooltip.blade.php | 13 + 3 files changed, 174 insertions(+), 215 deletions(-) diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 0d4d7f52cc..5246985165 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -415,6 +415,15 @@ html { display: inline-block; } +.booking-entry.is-past { + display: none !important; +} + +.show-old .booking-entry.is-past { + display: block !important; +} + + .tooltip-content { display: none; position: absolute; diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index e208a35c3d..e6ce97d4f9 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -11,10 +11,13 @@ $typeRowStyles = [ 'mentoring' => 'bg-blue-100 border-blue-400', - 'EX' => 'color:red; bg-red-100 border-red-400 font-bold', - 'solo' => 'bg-yellow-100 border-yellow-400', - 'normal' => 'bg-gray-100 border-gray-300', + 'EX' => 'color:red; bg-red-100 border-red-400 font-bold', + 'solo' => 'bg-yellow-100 border-yellow-400', + 'normal' => 'bg-gray-100 border-gray-300', ]; + + // Current time in Zulu (UTC) – used server-side to mark past bookings + $nowUtc = Carbon::now('UTC'); @endphp
@@ -24,37 +27,31 @@
-

- The bookings calendar shows the availability of our controllers for bookings. You can navigate through the months using the links below. -

- - +

+ The bookings calendar shows the availability of our controllers for bookings. + You can navigate through the months using the links below. +

-
+
  Information
-

- All times on Bookings are in UTC (Zulu). -

- -

- Your local time is: - (Local) -
- The current Zulu time is - (UTC/Zulu) -
- -

-

+

All times on Bookings are in UTC (Zulu).

+

+ Your local time is: + (Local) +
+ The current Zulu time is + (UTC/Zulu) +

+

@@ -70,6 +67,7 @@  |  Next →
+
@@ -86,62 +84,60 @@ @php $dayDate = is_string($day['date']) ? Carbon::parse($day['date']) : $day['date']; @endphp - @endforeach @@ -153,14 +149,14 @@ -
+
  Calendar Legend / Filter
- +
+ {{ $dayDate->day }}
- @if(!$dayDate->isBefore(Carbon::today())) - @foreach($day['bookings'] as $booking) - @php - $type = $booking->type ?? 'normal'; - $rowClass = $typeRowStyles[$type] ?? 'bg-white border-gray-200'; - $bookingTypeClass = ''; - if ($booking->isMentoring()) { - $bookingTypeClass = 'booking-mentoring'; - } elseif ($booking->isEvent()) { - $bookingTypeClass = 'booking-event'; - } elseif ($booking->isExam()) { - $bookingTypeClass = 'booking-exam'; - } elseif ($booking->isSeminar()) { - $bookingTypeClass = 'booking-seminar'; - } else { - $bookingTypeClass = 'booking'; - } - $fromTime = Carbon::parse($booking->from)->format('H:i'); - $toTime = Carbon::parse($booking->to)->format('H:i'); - $tooltipHtml = !$booking->isEvent() ? view('site.bookings.partials.tooltip', compact('booking', 'dayDate', 'fromTime', 'toTime'))->render() : null; - // Decide a normalized kind for filtering - $kind = 'normal'; - if ($booking->isEvent()) { - $kind = 'event'; - } elseif ($booking->isExam()) { - $kind = 'exam'; - } elseif ($booking->isMentoring()) { - $kind = 'mentoring'; - } elseif ($booking->isSeminar()) { - $kind = 'seminar'; - } - @endphp -
user_id === Auth::id()) - border-4 - @endif" - tabindex="0" data-kind="{{ $kind }}"> - {{ strtoupper($booking->position) ?? 'Booking' }}
- - {{ $fromTime }} z - {{ $toTime }}z - - @if(!$booking->isEvent()) -
- {!! $tooltipHtml !!} -
- @endif -
- @endforeach - @endif + + @foreach($day['bookings'] as $booking) + @php + $type = $booking->type ?? 'normal'; + $rowClass = $typeRowStyles[$type] ?? 'bg-white border-gray-200'; + + $bookingTypeClass = ''; + if ($booking->isMentoring()) $bookingTypeClass = 'booking-mentoring'; + elseif ($booking->isEvent()) $bookingTypeClass = 'booking-event'; + elseif ($booking->isExam()) $bookingTypeClass = 'booking-exam'; + elseif ($booking->isSeminar()) $bookingTypeClass = 'booking-seminar'; + else $bookingTypeClass = 'booking'; + + $fromTime = Carbon::parse($booking->from)->format('H:i'); + $toTime = Carbon::parse($booking->to)->format('H:i'); + $tooltipHtml = !$booking->isEvent() + ? view('site.bookings.partials.tooltip', compact('booking', 'dayDate', 'fromTime', 'toTime'))->render() + : null; + + // Normalized kind for legend filters + $kind = 'normal'; + if ($booking->isEvent()) $kind = 'event'; + elseif ($booking->isExam()) $kind = 'exam'; + elseif ($booking->isMentoring()) $kind = 'mentoring'; + elseif ($booking->isSeminar()) $kind = 'seminar'; + + // Server-side "is past?" (UTC) + $endUtc = Carbon::parse($booking->to)->setTimezone('UTC'); + $isPast = $dayDate->lt($nowUtc->copy()->startOfDay()) + || ($dayDate->isSameDay($nowUtc) && $endUtc->lte($nowUtc)); + @endphp + +
+ {{ strtoupper($booking->position) ?? 'Booking' }}
+ + + {{ $fromTime }}z - {{ $toTime }}z + + + @if(!$booking->isEvent()) +
{!! $tooltipHtml !!}
+ @endif +
+ @endforeach
@@ -168,57 +164,42 @@
Booking - +
Mentoring - +
Seminar - +
Exam - +
Event - +
- +
+ +
+
+
+   Booking Information +
+
+

+ Hover over a booking to view more information about the session. + Displayed information may vary depending on booking type. +

+
+
+
+ diff --git a/resources/views/site/bookings/partials/tooltip.blade.php b/resources/views/site/bookings/partials/tooltip.blade.php index f3690c0755..e5a163166e 100644 --- a/resources/views/site/bookings/partials/tooltip.blade.php +++ b/resources/views/site/bookings/partials/tooltip.blade.php @@ -48,6 +48,19 @@ function formatDate($datetime) { $takenTime = $booking->session ? formatDate($booking->session->taken_time) : 'N/A'; } +if ($booking->type === 'GS') { + $displayType = 'Seminar Booking'; + $bookedLabel = 'Booked on'; + $showMentor = false; + + if ($booking->session && $booking->session->mentor) { + $mentor = $booking->session->mentor->name . ' (' . $booking->session->mentor->cid . ')'; + } + + $requestTime = $booking->session ? formatDate($booking->session->request_time) : 'N/A'; + $takenTime = $booking->session ? formatDate($booking->session->taken_time) : 'N/A'; +} + $timeBooked = formatDate($booking->time_booked); @endphp From 02d8bef70c871fa6f2bc83b244a1955855ae1d29 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Sun, 12 Oct 2025 06:28:10 +0100 Subject: [PATCH 17/20] Tidying up the blade file --- app/Libraries/Bookings.php | 26 +++++ resources/assets/js/bookings.js | 100 ++++++++++++++++ resources/views/site/bookings/index.blade.php | 108 +----------------- vite.config.js | 7 +- 4 files changed, 136 insertions(+), 105 deletions(-) create mode 100644 app/Libraries/Bookings.php create mode 100644 resources/assets/js/bookings.js diff --git a/app/Libraries/Bookings.php b/app/Libraries/Bookings.php new file mode 100644 index 0000000000..66da15591a --- /dev/null +++ b/app/Libraries/Bookings.php @@ -0,0 +1,26 @@ +setTimezone('UTC'); + + if ($dayDate->lt($nowUtc->copy()->startOfDay())) { + return true; + } + + if ($dayDate->isSameDay($nowUtc)) { + return $endUtc->lte($nowUtc); + } + + return false; + } +} \ No newline at end of file diff --git a/resources/assets/js/bookings.js b/resources/assets/js/bookings.js new file mode 100644 index 0000000000..0317a25380 --- /dev/null +++ b/resources/assets/js/bookings.js @@ -0,0 +1,100 @@ +document.addEventListener('DOMContentLoaded', function () { + // ---- Display old bookings: toggle a CSS class on ---- + const oldCb = document.getElementById('filter-old'); + if (oldCb) { + function syncOldToggle() { + document.body.classList.toggle('show-old', !!oldCb.checked); + } + syncOldToggle(); + oldCb.addEventListener('change', syncOldToggle); + } + + // ---- Type filters ---- + const typeCheckboxes = Array.from(document.querySelectorAll('input[type="checkbox"][data-filter]')); + function applyTypeFilters() { + const allowed = new Set(typeCheckboxes.filter(cb => cb.checked).map(cb => cb.getAttribute('data-filter'))); + document.querySelectorAll('.booking-entry').forEach(el => { + const kind = el.getAttribute('data-kind') || 'normal'; + el.style.display = allowed.has(kind) ? '' : 'none'; + }); + } + typeCheckboxes.forEach(cb => cb.addEventListener('change', applyTypeFilters)); + applyTypeFilters(); + + // ---- Live clocks + TZ message ---- + function updateTimes() { + const now = new Date(); + const local = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const utc = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: 'UTC' }); + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + + const offsetMinutes = -now.getTimezoneOffset(); // + ahead, - behind + const absMinutes = Math.abs(offsetMinutes); + const hours = Math.floor(absMinutes / 60); + const minutes = absMinutes % 60; + + let relation, hint; + if (offsetMinutes === 0) { + relation = "the same as Zulu."; + hint = "No conversion is needed."; + } else if (offsetMinutes > 0) { + relation = `${hours}h${minutes ? ` ${minutes}m` : ""} ahead of Zulu.`; + hint = `To convert any time on Bookings to your local time, add ${hours}h${minutes ? ` ${minutes}m` : ""}.`; + } else { + relation = `${hours}h${minutes ? ` ${minutes}m` : ""} behind Zulu.`; + hint = `To convert any time on Bookings to your local time, subtract ${hours}h${minutes ? ` ${minutes}m` : ""}.`; + } + + document.querySelectorAll('#local-time').forEach(el => el.textContent = local); + document.querySelectorAll('#utc-time').forEach(el => el.textContent = utc); + document.querySelectorAll('#tz-message').forEach(el => el.innerHTML = + `Your local time (${tz}) is ${relation}
${hint}`); + } + updateTimes(); + setInterval(updateTimes, 1000); + + // ---- Promote bookings to .is-past as time passes ---- + function rollPastOverTime() { + const now = new Date(); + document.querySelectorAll('.booking-entry:not(.is-past)').forEach(el => { + const span = el.querySelector('.booking-time'); + if (!span) return; + const endISO = span.dataset.end; + if (!endISO) return; + const end = new Date(endISO); + if (!isNaN(end) && end <= now) el.classList.add('is-past'); + }); + } + rollPastOverTime(); + setInterval(rollPastOverTime, 60000); + + // ---- Local time toggle ---- + const toggleLocalTime = document.getElementById('toggle-localtime'); + if (toggleLocalTime) { + function updateBookingTimes() { + const useLocal = toggleLocalTime.checked; + document.querySelectorAll('.booking-time').forEach(span => { + const startISO = span.dataset.start; + const endISO = span.dataset.end; + if (!startISO || !endISO) return; + + const startUtc = new Date(startISO); + const endUtc = new Date(endISO); + if (isNaN(startUtc) || isNaN(endUtc)) return; + + if (useLocal) { + const startLocal = startUtc.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const endLocal = endUtc.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const tzName = Intl.DateTimeFormat().resolvedOptions().timeZone; + span.textContent = `${startLocal} - ${endLocal} (${tzName})`; + } else { + const startZulu = startUtc.toISOString().substr(11, 5); + const endZulu = endUtc.toISOString().substr(11, 5); + span.textContent = `${startZulu}z - ${endZulu}z`; + } + }); + } + updateBookingTimes(); + toggleLocalTime.addEventListener('change', updateBookingTimes); + } +}); diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index e6ce97d4f9..ccd3751ec4 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -4,6 +4,7 @@ @php use Carbon\Carbon; + use App\Libraries\Bookings; // ← add this import if (is_string($date)) { $date = Carbon::parse($date); @@ -116,10 +117,8 @@ class="align-top border p-2 elseif ($booking->isMentoring()) $kind = 'mentoring'; elseif ($booking->isSeminar()) $kind = 'seminar'; - // Server-side "is past?" (UTC) - $endUtc = Carbon::parse($booking->to)->setTimezone('UTC'); - $isPast = $dayDate->lt($nowUtc->copy()->startOfDay()) - || ($dayDate->isSameDay($nowUtc) && $endUtc->lte($nowUtc)); + // Server-side "is past?" (UTC) via pure function + $isPast = Bookings::isPastUtc($dayDate->copy(), $booking->to, $nowUtc); @endphp
cb.checked).map(cb => cb.getAttribute('data-filter'))); - document.querySelectorAll('.booking-entry').forEach(el => { - const kind = el.getAttribute('data-kind') || 'normal'; - el.style.display = allowed.has(kind) ? '' : 'none'; - }); - } - typeCheckboxes.forEach(cb => cb.addEventListener('change', applyTypeFilters)); - applyTypeFilters(); - - // ---- Live clocks + TZ message ---- - function updateTimes() { - const now = new Date(); - const local = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - const utc = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: 'UTC' }); - const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - - const offsetMinutes = -now.getTimezoneOffset(); // + ahead, - behind - const absMinutes = Math.abs(offsetMinutes); - const hours = Math.floor(absMinutes / 60); - const minutes = absMinutes % 60; - - let relation, hint; - if (offsetMinutes === 0) { - relation = "the same as Zulu."; - hint = "No conversion is needed."; - } else if (offsetMinutes > 0) { - relation = `${hours}h${minutes ? ` ${minutes}m` : ""} ahead of Zulu.`; - hint = `To convert any time on Bookings to your local time, add ${hours}h${minutes ? ` ${minutes}m` : ""}.`; - } else { - relation = `${hours}h${minutes ? ` ${minutes}m` : ""} behind Zulu.`; - hint = `To convert any time on Bookings to your local time, subtract ${hours}h${minutes ? ` ${minutes}m` : ""}.`; - } - - document.querySelectorAll('#local-time').forEach(el => el.textContent = local); - document.querySelectorAll('#utc-time').forEach(el => el.textContent = utc); - document.querySelectorAll('#tz-message').forEach(el => el.innerHTML = - `Your local time (${tz}) is ${relation}
${hint}`); - } - updateTimes(); - setInterval(updateTimes, 1000); - - // ---- Promote bookings to .is-past as time passes (every minute) ---- - function rollPastOverTime() { - const now = new Date(); - document.querySelectorAll('.booking-entry:not(.is-past)').forEach(el => { - const span = el.querySelector('.booking-time'); - if (!span) return; - const endISO = span.dataset.end; - if (!endISO) return; - const end = new Date(endISO); - if (!isNaN(end) && end <= now) el.classList.add('is-past'); - }); - } - rollPastOverTime(); - setInterval(rollPastOverTime, 60000); - - // ---- Local time toggle (uses span data-start/data-end) ---- - const toggleLocalTime = document.getElementById('toggle-localtime'); - function updateBookingTimes() { - const useLocal = toggleLocalTime.checked; - document.querySelectorAll('.booking-time').forEach(span => { - const startISO = span.dataset.start; - const endISO = span.dataset.end; - if (!startISO || !endISO) return; - - const startUtc = new Date(startISO); - const endUtc = new Date(endISO); - if (isNaN(startUtc) || isNaN(endUtc)) return; - - if (useLocal) { - const startLocal = startUtc.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - const endLocal = endUtc .toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - const tzName = Intl.DateTimeFormat().resolvedOptions().timeZone; - span.textContent = `${startLocal} - ${endLocal} (${tzName})`; - } else { - const startZulu = startUtc.toISOString().substr(11, 5); - const endZulu = endUtc .toISOString().substr(11, 5); - span.textContent = `${startZulu}z - ${endZulu}z`; - } - }); - } - updateBookingTimes(); - toggleLocalTime.addEventListener('change', updateBookingTimes); -}); - +@section('scripts') + @vite(['resources/assets/js/bookings.js']) +@endsection @stop diff --git a/vite.config.js b/vite.config.js index 2c3c9cd3cc..6f631262d5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,5 @@ -import {defineConfig} from 'vite'; -import laravel, {refreshPaths} from 'laravel-vite-plugin' +import { defineConfig } from 'vite'; +import laravel, { refreshPaths } from 'laravel-vite-plugin' export default defineConfig({ plugins: [ @@ -12,6 +12,7 @@ export default defineConfig({ 'resources/assets/sass/home.scss', 'resources/assets/js/home.js', 'resources/assets/js/snow.js', + 'resources/assets/js/bookings.js', 'resources/assets/js/top-notification.js' ], refresh: [ @@ -20,7 +21,7 @@ export default defineConfig({ }), { name: 'blade', - handleHotUpdate({file, server}) { + handleHotUpdate({ file, server }) { if (file.endsWith('.blade.php')) { server.ws.send({ type: 'full-reload', From 6ea42b471ac03c15dc6ef79df0e1adbcd5388a88 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Sun, 12 Oct 2025 06:31:05 +0100 Subject: [PATCH 18/20] EOF issue --- app/Libraries/Bookings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Libraries/Bookings.php b/app/Libraries/Bookings.php index 66da15591a..45e1c9cd58 100644 --- a/app/Libraries/Bookings.php +++ b/app/Libraries/Bookings.php @@ -23,4 +23,4 @@ public static function isPastUtc(Carbon $dayDate, $bookingEnd, Carbon $nowUtc): return false; } -} \ No newline at end of file +} From 083d9930c0ad357a1d20504ef1475f3ad6e5c3a3 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Sun, 12 Oct 2025 07:31:46 +0100 Subject: [PATCH 19/20] Tests and Navigation Tests and Navigation --- resources/views/components/nav.blade.php | 1 + resources/views/site/bookings/index.blade.php | 2 +- resources/views/site/home.blade.php | 2 +- .../Bookings/CalendarViewPastClassTest.php | 157 ++++++++++++++++++ tests/Unit/Bookings/BookingsTest.php | 102 ++++++++++++ tests/Unit/CTS/BookingsRepositoryTest.php | 123 +++++++++----- 6 files changed, 348 insertions(+), 39 deletions(-) create mode 100644 tests/Feature/Bookings/CalendarViewPastClassTest.php create mode 100644 tests/Unit/Bookings/BookingsTest.php diff --git a/resources/views/components/nav.blade.php b/resources/views/components/nav.blade.php index fe41f89a1d..cc2b830c1b 100644 --- a/resources/views/components/nav.blade.php +++ b/resources/views/components/nav.blade.php @@ -61,6 +61,7 @@
  • Endorsements
  • Become a Mentor
  • Bookings
  • +
  • Bookings Calendar
  • @if(currentUserHasAuth())
  • UK Controller Plugin
  • @endif diff --git a/resources/views/site/bookings/index.blade.php b/resources/views/site/bookings/index.blade.php index ccd3751ec4..1c94b1ccbd 100644 --- a/resources/views/site/bookings/index.blade.php +++ b/resources/views/site/bookings/index.blade.php @@ -4,7 +4,7 @@ @php use Carbon\Carbon; - use App\Libraries\Bookings; // ← add this import + use App\Libraries\Bookings; if (is_string($date)) { $date = Carbon::parse($date); diff --git a/resources/views/site/home.blade.php b/resources/views/site/home.blade.php index c663bd18f3..60e9ed8cb0 100644 --- a/resources/views/site/home.blade.php +++ b/resources/views/site/home.blade.php @@ -126,7 +126,7 @@ Events diff --git a/tests/Feature/Bookings/CalendarViewPastClassTest.php b/tests/Feature/Bookings/CalendarViewPastClassTest.php new file mode 100644 index 0000000000..71ec36bdca --- /dev/null +++ b/tests/Feature/Bookings/CalendarViewPastClassTest.php @@ -0,0 +1,157 @@ +type = $type; + $this->from = $from; + $this->to = $to; + $this->position = $position; + + // Provide defaults the tooltip can read + $this->member = $member ?: (object) [ + 'cid' => '123456', + 'name' => 'Test User', + ]; + + // time the booking was made (string expected by view) + $this->time_booked = $time_booked ?: '2025-10-10 09:00:00'; + } + + public function isEvent(): bool + { + return $this->type === 'EV'; + } + + public function isExam(): bool + { + return $this->type === 'EX'; + } + + public function isMentoring(): bool + { + return $this->type === 'ME'; + } + + public function isSeminar(): bool + { + return $this->type === 'SE'; + } + + /** Return null for any field the view asks for that we didn't stub explicitly */ + public function __get($name) + { + return null; + } +} + +class CalendarViewPastClassTest extends TestCase +{ + protected function tearDown(): void + { + Carbon::setTestNow(); // clear frozen time + parent::tearDown(); + } + + #[Test] + public function past_bookings_have_is_past_class_and_future_dont() + { + // Freeze "now" in UTC so server-side isPast logic is deterministic + Carbon::setTestNow(Carbon::parse('2025-10-12 12:00:00', 'UTC')); + + $date = Carbon::parse('2025-10-12'); // the month being rendered + $prevMonth = $date->copy()->subMonth(); + $nextMonth = $date->copy()->addMonth(); + + $today = Carbon::parse('2025-10-12'); + $yesterday = $today->copy()->subDay(); + $tomorrow = $today->copy()->addDay(); + + // Build the minimal calendar structure the Blade expects + $calendar = [[ + [ + 'date' => $yesterday->toDateString(), + 'bookings' => [ + new BookingStub('normal', '2025-10-11 10:00:00', '2025-10-11 11:00:00', 'EGLL_TWR'), + ], + ], + [ + 'date' => $today->toDateString(), + 'bookings' => [ + // Past today + new BookingStub('normal', '2025-10-12 10:00:00', '2025-10-12 11:00:00', 'EGKK_GND'), + // Future today + new BookingStub('normal', '2025-10-12 12:01:00', '2025-10-12 13:00:00', 'EGKK_TWR'), + ], + ], + [ + 'date' => $tomorrow->toDateString(), + 'bookings' => [ + new BookingStub('normal', '2025-10-13 09:00:00', '2025-10-13 10:00:00', 'EGLL_APP'), + ], + ], + ]]; + + // Render the Blade with all required variables + $html = view('site.bookings.index', [ + 'calendar' => $calendar, + 'date' => $date, + 'prevMonth' => $prevMonth, + 'nextMonth' => $nextMonth, + ])->render(); + + // Smoke checks + $this->assertStringContainsString('booking-entry', $html); + + // A booking from yesterday should be marked as past + $this->assertStringContainsString('is-past', $html, 'Expected at least one .is-past element for past bookings.'); + + // Future-today (EGKK_TWR) should be present but not marked past + $this->assertStringContainsString('EGKK_TWR', $html); + $this->assertStringNotContainsString('EGKK_TWR" class="is-past', $html); + + // Tomorrow (EGLL_APP) should not be marked past + $this->assertStringContainsString('EGLL_APP', $html); + $this->assertStringNotContainsString('EGLL_APP" class="is-past', $html); + } +} diff --git a/tests/Unit/Bookings/BookingsTest.php b/tests/Unit/Bookings/BookingsTest.php new file mode 100644 index 0000000000..dcd7729e46 --- /dev/null +++ b/tests/Unit/Bookings/BookingsTest.php @@ -0,0 +1,102 @@ +assertTrue(Bookings::isPastUtc($day, $end, $nowUtc)); + } + + #[Test] + public function today_before_now_is_past() + { + $nowUtc = Carbon::parse('2025-10-12 12:00:00', 'UTC'); + $day = Carbon::parse('2025-10-12', 'UTC'); + $end = '2025-10-12 11:59:00'; + + $this->assertTrue(Bookings::isPastUtc($day, $end, $nowUtc)); + } + + #[Test] + public function today_equal_now_is_past() + { + $nowUtc = Carbon::parse('2025-10-12 12:00:00', 'UTC'); + $day = Carbon::parse('2025-10-12', 'UTC'); + $end = '2025-10-12 12:00:00'; + + $this->assertTrue(Bookings::isPastUtc($day, $end, $nowUtc)); + } + + #[Test] + public function today_after_now_is_not_past() + { + $nowUtc = Carbon::parse('2025-10-12 12:00:00', 'UTC'); + $day = Carbon::parse('2025-10-12', 'UTC'); + $end = '2025-10-12 12:01:00'; + + $this->assertFalse(Bookings::isPastUtc($day, $end, $nowUtc)); + } + + #[Test] + public function future_day_is_not_past() + { + $nowUtc = Carbon::parse('2025-10-12 12:00:00', 'UTC'); + $day = Carbon::parse('2025-10-13', 'UTC'); + $end = '2025-10-13 08:00:00'; + + $this->assertFalse(Bookings::isPastUtc($day, $end, $nowUtc)); + } + + #[Test] + public function timezone_on_booking_end_is_respected() + { + // Now is 12:00 UTC + $nowUtc = Carbon::parse('2025-10-12 12:00:00', 'UTC'); + $day = Carbon::parse('2025-10-12', 'UTC'); + + // End at 13:00 local (+02:00) => 11:00 UTC -> should be past + $endPastTz = '2025-10-12T13:00:00+02:00'; + $this->assertTrue(Bookings::isPastUtc($day, $endPastTz, $nowUtc)); + + // End at 13:30 local (+02:00) => 11:30 UTC -> still past + $endStillPastTz = '2025-10-12T13:30:00+02:00'; + $this->assertTrue(Bookings::isPastUtc($day, $endStillPastTz, $nowUtc)); + + // End at 16:00 local (+02:00) => 14:00 UTC -> not past + $endFutureTz = '2025-10-12T16:00:00+02:00'; + $this->assertFalse(Bookings::isPastUtc($day, $endFutureTz, $nowUtc)); + } + + #[Test] + public function midnight_boundary_cases() + { + // At midnight UTC at the start of 2025-10-12 + $nowUtc = Carbon::parse('2025-10-12 00:00:00', 'UTC'); + + // Previous day: always past + $yesterday = Carbon::parse('2025-10-11', 'UTC'); + $endYesterday = '2025-10-11 23:59:59'; + $this->assertTrue(Bookings::isPastUtc($yesterday, $endYesterday, $nowUtc)); + + // Today, end exactly at midnight -> should be past (<= now) + $today = Carbon::parse('2025-10-12', 'UTC'); + $endAtMidnight = '2025-10-12 00:00:00'; + $this->assertTrue(Bookings::isPastUtc($today, $endAtMidnight, $nowUtc)); + + // Today, 00:00:01 -> not past + $endJustAfter = '2025-10-12 00:00:01'; + $this->assertFalse(Bookings::isPastUtc($today, $endJustAfter, $nowUtc)); + } +} diff --git a/tests/Unit/CTS/BookingsRepositoryTest.php b/tests/Unit/CTS/BookingsRepositoryTest.php index ab5013c966..dfaeff0094 100644 --- a/tests/Unit/CTS/BookingsRepositoryTest.php +++ b/tests/Unit/CTS/BookingsRepositoryTest.php @@ -15,13 +15,13 @@ class BookingsRepositoryTest extends TestCase { use DatabaseTransactions; - /* @var BookingRepository */ + /** @var BookingRepository */ protected $subjectUnderTest; - /* @var Carbon */ + /** @var string */ protected $today; - /* @var Carbon */ + /** @var string */ protected $tomorrow; protected function setUp(): void @@ -36,72 +36,113 @@ protected function setUp(): void #[Test] public function it_can_return_a_list_of_bookings_for_today() { - Booking::factory()->count(10)->create(['date' => Carbon::now()]); + // Ensure created items are on the same date the repo will query + Booking::factory()->count(10)->create([ + 'date' => $this->today, + 'from' => '10:00', + 'to' => '11:00', + 'type' => 'BK', + ]); $bookings = $this->subjectUnderTest->getBookings(Carbon::parse($this->today)); - $this->assertInstanceOf(Collection::class, $bookings); + // Repo returns a Collection... + $this->assertInstanceOf(\Illuminate\Support\Collection::class, $bookings); $this->assertCount(10, $bookings); + + // ...of Eloquent Booking models (mutated/formatted by formatBookings) $this->assertInstanceOf(Booking::class, $bookings->first()); } #[Test] public function it_can_return_a_list_of_todays_bookings_with_owner_and_type() { - Booking::factory()->count(2)->create(['date' => $this->knownDate->copy()->addDays(5)->toDateString()]); + // Noise on a different date + \App\Models\Cts\Booking::factory()->count(2)->create([ + 'date' => $this->knownDate->copy()->addDays(5)->toDateString(), + 'from' => '10:00', + 'to' => '11:00', + 'type' => 'BK', + ]); - $bookingTodayOne = Booking::Factory()->create([ + $bookingTodayOne = \App\Models\Cts\Booking::factory()->create([ 'id' => '96155', 'date' => $this->today, 'from' => '17:00', - 'member_id' => Member::Factory()->create()->id, + 'to' => '18:00', + 'member_id' => \App\Models\Cts\Member::factory()->create()->id, 'type' => 'BK', ]); - $bookingTodayTwo = Booking::Factory()->create([ + $bookingTodayTwo = \App\Models\Cts\Booking::factory()->create([ 'id' => '96156', 'date' => $this->today, 'from' => '18:00', - 'member_id' => Member::Factory()->create()->id, + 'to' => '19:00', + 'member_id' => \App\Models\Cts\Member::factory()->create()->id, 'type' => 'ME', ]); $bookings = $this->subjectUnderTest->getTodaysBookings(); - $this->assertInstanceOf(Collection::class, $bookings); + $this->assertInstanceOf(\Illuminate\Support\Collection::class, $bookings); $this->assertCount(2, $bookings); - $this->assertEquals([ + // Build expected minimal payloads + $expected0 = [ 'id' => $bookingTodayOne->id, - 'date' => $this->today, - 'from' => Carbon::parse($bookingTodayOne->from)->format('H:i'), - 'to' => Carbon::parse($bookingTodayOne->to)->format('H:i'), + 'date' => $this->today, // repo formats to Y-m-d + 'from' => \Carbon\Carbon::parse($bookingTodayOne->from)->format('H:i'), + 'to' => \Carbon\Carbon::parse($bookingTodayOne->to)->format('H:i'), 'position' => $bookingTodayOne->position, 'type' => $bookingTodayOne->type, 'member' => [ - 'id' => $bookingTodayOne['member']['cid'], - 'name' => $bookingTodayOne['member']['name'], + 'id' => $bookingTodayOne->member->cid, + 'name' => $bookingTodayOne->member->name, ], - ], $bookings->get(0)->toArray()); - $this->assertEquals([ + ]; + + $expected1 = [ 'id' => $bookingTodayTwo->id, 'date' => $this->today, - 'from' => Carbon::parse($bookingTodayTwo->from)->format('H:i'), - 'to' => Carbon::parse($bookingTodayTwo->to)->format('H:i'), + 'from' => \Carbon\Carbon::parse($bookingTodayTwo->from)->format('H:i'), + 'to' => \Carbon\Carbon::parse($bookingTodayTwo->to)->format('H:i'), 'position' => $bookingTodayTwo->position, 'type' => $bookingTodayTwo->type, 'member' => [ - 'id' => $bookingTodayTwo['member']['cid'], - 'name' => $bookingTodayTwo['member']['name'], + 'id' => $bookingTodayTwo->member->cid, + 'name' => $bookingTodayTwo->member->name, ], - ], $bookings->get(1)->toArray()); + ]; + + // Only compare the keys we care about (Eloquent adds many others) + $actual0 = collect($bookings->get(0)->toArray())->only(array_keys($expected0))->toArray(); + $actual1 = collect($bookings->get(1)->toArray())->only(array_keys($expected1))->toArray(); + + // For nested 'member', also narrow to the expected keys + $actual0['member'] = collect($actual0['member'])->only(['id', 'name'])->toArray(); + $actual1['member'] = collect($actual1['member'])->only(['id', 'name'])->toArray(); + + $this->assertEquals($expected0, $actual0); + $this->assertEquals($expected1, $actual1); } #[Test] public function it_hides_member_details_on_exam_booking() { - $normalBooking = Booking::Factory()->create(['date' => $this->today, 'from' => '17:00', 'type' => 'BK']); - Booking::Factory()->create(['date' => $this->today, 'from' => '18:00', 'type' => 'EX']); + $normalBooking = Booking::factory()->create([ + 'date' => $this->today, + 'from' => '17:00', + 'to' => '18:00', + 'type' => 'BK', + ]); + + Booking::factory()->create([ + 'date' => $this->today, + 'from' => '18:00', + 'to' => '19:00', + 'type' => 'EX', + ]); $bookings = $this->subjectUnderTest->getTodaysBookings(); @@ -119,11 +160,11 @@ public function it_hides_member_details_on_exam_booking() #[Test] public function it_can_return_a_list_of_todays_live_atc_bookings() { - Booking::Factory()->create(['date' => $this->today, 'position' => 'EGKK_APP']); // Live ATC booking today - Booking::Factory()->create(['date' => $this->today, 'position' => 'EGKK_SBAT']); // Sweatbox ATC booking today - Booking::Factory()->create(['date' => $this->today, 'position' => 'P1_VATSIM']); // Pilot booking today - Booking::Factory()->create(['date' => $this->tomorrow, 'position' => 'EGKK_APP']); // ATC booking tomorrow - Booking::Factory()->create(['date' => $this->tomorrow, 'position' => 'P1_VATSIM']); // Pilot booking tomorrw + Booking::factory()->create(['date' => $this->today, 'position' => 'EGKK_APP', 'from' => '10:00', 'to' => '11:00']); // live ATC + Booking::factory()->create(['date' => $this->today, 'position' => 'EGKK_SBAT', 'from' => '12:00', 'to' => '13:00']); // sweatbox + Booking::factory()->create(['date' => $this->today, 'position' => 'P1_VATSIM', 'from' => '14:00', 'to' => '15:00']); // pilot + Booking::factory()->create(['date' => $this->tomorrow, 'position' => 'EGKK_APP', 'from' => '10:00', 'to' => '11:00']); + Booking::factory()->create(['date' => $this->tomorrow, 'position' => 'P1_VATSIM', 'from' => '12:00', 'to' => '13:00']); $atcBookings = $this->subjectUnderTest->getTodaysLiveAtcBookings(); @@ -134,19 +175,27 @@ public function it_can_return_a_list_of_todays_live_atc_bookings() #[Test] public function it_can_return_a_booking_without_a_known_member() { - Booking::Factory()->create(['date' => $this->today, 'member_id' => 0, 'type' => 'BK']); + Booking::factory()->create([ + 'date' => $this->today, + 'from' => '10:00', + 'to' => '11:00', + 'member_id' => 0, + 'type' => 'BK', + ]); - $this->subjectUnderTest->getTodaysLiveAtcBookings(); + $bookings = $this->subjectUnderTest->getTodaysBookings(); - $this->expectNotToPerformAssertions(); + $this->assertEquals('', $bookings->first()['member']['id']); + $this->assertEquals('Unknown', $bookings->first()['member']['name']); } #[Test] public function it_returns_bookings_in_start_time_order() { - $afternoon = Booking::Factory()->create(['date' => $this->today, 'from' => '16:00', 'to' => '17:00', 'type' => 'BK']); - $morning = Booking::Factory()->create(['date' => $this->today, 'from' => '09:00', 'to' => '11:00', 'type' => 'BK']); - $night = Booking::Factory()->create(['date' => $this->today, 'from' => '22:00', 'to' => '23:00', 'type' => 'BK']); + // Ensure these are "live ATC" positions so both methods return same set + $afternoon = Booking::factory()->create(['date' => $this->today, 'from' => '16:00', 'to' => '17:00', 'type' => 'BK', 'position' => 'EGLL_TWR']); + $morning = Booking::factory()->create(['date' => $this->today, 'from' => '09:00', 'to' => '11:00', 'type' => 'BK', 'position' => 'EGLL_TWR']); + $night = Booking::factory()->create(['date' => $this->today, 'from' => '22:00', 'to' => '23:00', 'type' => 'BK', 'position' => 'EGLL_TWR']); $todaysBookings = $this->subjectUnderTest->getTodaysBookings(); $todaysAtcBookings = $this->subjectUnderTest->getTodaysLiveAtcBookings(); From 726d0f9a9cd081e6f31659076004c84c18be4d21 Mon Sep 17 00:00:00 2001 From: Daniel 'MrAdder' Green Date: Wed, 12 Nov 2025 13:08:39 +0000 Subject: [PATCH 20/20] Most of Changes Rejected --- app/Libraries/Bookings.php | 11 +++- resources/assets/js/bookings.js | 113 ++++++++++++++++++++++++++++++++ resources/assets/sass/app.scss | 53 ++++++++++++++- 3 files changed, 173 insertions(+), 4 deletions(-) diff --git a/app/Libraries/Bookings.php b/app/Libraries/Bookings.php index 45e1c9cd58..1088db41e7 100644 --- a/app/Libraries/Bookings.php +++ b/app/Libraries/Bookings.php @@ -9,18 +9,23 @@ class Bookings /** * Determine if a booking should be considered past (ended) in UTC. */ - public static function isPastUtc(Carbon $dayDate, $bookingEnd, Carbon $nowUtc): bool + public static function isPastUtc(Carbon $dayDate, string $bookingEnd, Carbon $nowUtc): bool { - $endUtc = Carbon::parse($bookingEnd)->setTimezone('UTC'); + // Ensure both dates are compared in UTC + $dayDate = $dayDate->copy()->setTimezone('UTC'); + $endUtc = Carbon::parse($bookingEnd, 'UTC')->setTimezone('UTC'); + // 1️⃣ If the booking date is before today (UTC), it’s past if ($dayDate->lt($nowUtc->copy()->startOfDay())) { return true; } + // 2️⃣ If the booking is today and has already ended, it’s past if ($dayDate->isSameDay($nowUtc)) { - return $endUtc->lte($nowUtc); + return $endUtc->lessThanOrEqualTo($nowUtc); } + // 3️⃣ Otherwise it’s upcoming return false; } } diff --git a/resources/assets/js/bookings.js b/resources/assets/js/bookings.js index 0317a25380..f6e043460e 100644 --- a/resources/assets/js/bookings.js +++ b/resources/assets/js/bookings.js @@ -97,4 +97,117 @@ document.addEventListener('DOMContentLoaded', function () { updateBookingTimes(); toggleLocalTime.addEventListener('change', updateBookingTimes); } + + // ---- Tooltip containers ---- + const items = Array.from(document.querySelectorAll('.tooltip-container')); + + // Hide all except optional "keep" element + function hideAll(keep = null) { + items.forEach(el => { + if (el !== keep) { + el.classList.remove('is-open'); + if (el === document.activeElement) el.blur(); + } + }); + } + + items.forEach(el => { + // Open on hover, but ensure only one is active + el.addEventListener('mouseenter', () => { + hideAll(el); + // purely hover should still show via your CSS .tooltip-container:hover .tooltip-content + // we don't set is-open here to keep hover behavior unchanged + }); + + // Keyboard focus: keep only one open + el.addEventListener('focus', () => hideAll(el)); + + // Click to "pin" (toggle). Only one pinned at a time. + el.addEventListener('click', (e) => { + const willOpen = !el.classList.contains('is-open'); + hideAll(willOpen ? el : null); + el.classList.toggle('is-open', willOpen); + // Prevent the click from bubbling to the document "outside click" handler + e.stopPropagation(); + }); + + // If you leave with the mouse and it's not pinned (no .is-open), let it close + el.addEventListener('mouseleave', () => { + if (!el.classList.contains('is-open') && !el.matches(':focus')) { + // hover-out: your CSS already hides, nothing needed + } + }); + + // When focus is lost, unpin + el.addEventListener('blur', () => el.classList.remove('is-open')); + }); + + // Click outside → close any pinned tooltip + document.addEventListener('click', () => hideAll()); + + // ESC to close + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') hideAll(); + }); + + const containers = Array.from(document.querySelectorAll('.tooltip-container')); + + function positionTip(container) { + const tip = container.querySelector('.tooltip-content'); + if (!tip) return; + + // Reset any previous inline transforms/positions + tip.style.transform = ''; + tip.classList.remove('pos-above', 'pos-below', 'pos-left', 'pos-right'); + + // Start with a sensible default (to the right, below) + tip.classList.add('pos-right', 'pos-below'); + + // Wait a frame so the browser can measure with classes applied + requestAnimationFrame(() => { + const vw = window.innerWidth; + const vh = window.innerHeight; + const tipRect = tip.getBoundingClientRect(); + const contRect = container.getBoundingClientRect(); + + // Decide vertical placement + let vertical = 'below'; + if (tipRect.bottom > vh && contRect.top > tipRect.height) vertical = 'above'; + tip.classList.toggle('pos-below', vertical === 'below'); + tip.classList.toggle('pos-above', vertical === 'above'); + + // Re-measure if we changed vertical + let r = tip.getBoundingClientRect(); + + // Decide horizontal placement + let horizontal = 'right'; + if (r.right > vw && contRect.left > r.width) horizontal = 'left'; + tip.classList.toggle('pos-right', horizontal === 'right'); + tip.classList.toggle('pos-left', horizontal === 'left'); + + // Final nudge to keep fully on-screen + r = tip.getBoundingClientRect(); + let dx = 0, dy = 0; + if (r.left < 0) dx += -r.left + 8; + if (r.right > vw) dx += vw - r.right - 8; + if (r.top < 0) dy += -r.top + 8; + if (r.bottom > vh) dy += vh - r.bottom - 8; + if (dx || dy) tip.style.transform = `translate(${dx}px, ${dy}px)`; + }); + } + + // Position when tip becomes visible (hover/focus/pin) + containers.forEach(c => { + c.addEventListener('mouseenter', () => positionTip(c)); + c.addEventListener('focus', () => positionTip(c), true); + c.addEventListener('click', () => positionTip(c)); // if you support click-to-pin + }); + + // Reposition any open ones on resize/scroll + const repack = () => { + document.querySelectorAll('.tooltip-container.is-open, .tooltip-container:hover, .tooltip-container:focus-within') + .forEach(c => positionTip(c)); + }; + window.addEventListener('resize', repack, { passive: true }); + window.addEventListener('scroll', repack, { passive: true }); }); diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 4a5bd3e297..830d97f359 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -172,6 +172,7 @@ html { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; + line-clamp: 3; -webkit-box-orient: vertical; } @@ -428,7 +429,7 @@ html { .tooltip-content { display: none; position: absolute; - z-index: 10; + z-index: 1000; left: 100%; top: 0; min-width: 250px; @@ -447,6 +448,56 @@ html { display: block; } +.tooltip-container.is-open .tooltip-content { + display: block; +} + +/* keep your existing tooltip styles; add these utility positions */ +.tooltip-container { + position: relative; +} + +.tooltip-content { + will-change: transform; +} + +.tooltip-content.pos-right { + left: 100%; + top: 0; + right: auto; + bottom: auto; + margin-left: 8px; +} + +.tooltip-content.pos-left { + right: 100%; + left: auto; + top: 0; + bottom: auto; + margin-right: 8px; +} + +.tooltip-content.pos-below { + top: 100%; + bottom: auto; + left: 0; + right: auto; + margin-top: 8px; +} + +.tooltip-content.pos-above { + bottom: 100%; + top: auto; + left: 0; + right: auto; + margin-bottom: 8px; +} + +/* ensure pinned tips (from your click-to-pin code) also show */ +.tooltip-container.is-open .tooltip-content { + display: block; +} + .no-x-overflow { overflow-x: visible !important; }