Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
365c1ab
Bookings Concept
MrAdder Jun 24, 2025
cf7c532
Further work
MrAdder Jun 24, 2025
30df409
Merge branch 'main' into booking-system
MrAdder Jun 24, 2025
de6d1ab
Update BookingsPagesController.php
MrAdder Jun 24, 2025
3738a65
Merge branch 'booking-system' of https://github.com/MrAdder/core into…
MrAdder Jun 24, 2025
164d395
Update BookingRepository.php
MrAdder Jun 24, 2025
44bd67f
member_id to name
MrAdder Jun 24, 2025
d549136
Update BookingRepository.php
MrAdder Jun 24, 2025
748e40e
Merge branch 'main' into booking-system
MrAdder Jun 30, 2025
043da30
Merge branch 'main' into booking-system
MrAdder Jul 10, 2025
dff0f6b
Merge branch 'main' into booking-system
MrAdder Aug 8, 2025
7749e81
Merge branch 'main' into booking-system
MrAdder Aug 10, 2025
aba2b10
Further Progress
MrAdder Aug 10, 2025
ff2c654
Merge branch 'VATSIM-UK:main' into booking-system
MrAdder Aug 18, 2025
5de4cc0
Further improvements
MrAdder Aug 18, 2025
34e103e
Forgot to commit Booking
MrAdder Aug 18, 2025
b284488
Bookings now display
MrAdder Aug 18, 2025
621bab5
Linting
MrAdder Aug 18, 2025
faa0a74
Linting again
MrAdder Aug 18, 2025
9471adf
Need to fix the Tests
MrAdder Aug 18, 2025
cc296c1
Merge branch 'main' into booking-system
MrAdder Aug 22, 2025
c69cbec
Merge branch 'main' into booking-system
MrAdder Aug 29, 2025
4cb355a
Changes
MrAdder Aug 31, 2025
4e5797b
Got rid of the Emoji
MrAdder Aug 31, 2025
6503715
Merge branch 'main' into booking-system
MrAdder Sep 5, 2025
8ac8366
Merge branch 'main' into booking-system
MrAdder Oct 12, 2025
f6ba9bd
Finally working
MrAdder Oct 12, 2025
02d8bef
Tidying up the blade file
MrAdder Oct 12, 2025
6ea42b4
EOF issue
MrAdder Oct 12, 2025
083d993
Tests and Navigation
MrAdder Oct 12, 2025
f977c6d
Merge branch 'main' into booking-system
MrAdder Nov 12, 2025
726d0f9
Most of Changes Rejected
MrAdder Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
72 changes: 72 additions & 0 deletions app/Http/Controllers/Site/BookingsPagesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace App\Http\Controllers\Site;

use App\Models\Cts\Booking;
use App\Repositories\Cts\BookingRepository;
use Carbon\Carbon;

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->name ?? 'Unknown',
'from' => $booking->from,
'to' => $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();

$start = $date->copy()->startOfWeek();
$end = $date->copy()->endOfMonth()->endOfWeek();

$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++) {
$dayBookings = $bookings->filter(function ($booking) use ($current) {
return $booking->date->isSameDay($current);
});

$week[] = [
'date' => $current->copy(),
'bookings' => $dayBookings,
];
$current->addDay();
}
$calendar[] = $week;
}

$prevMonth = $date->copy()->subMonth();
$nextMonth = $date->copy()->addMonth();

return view('site.bookings.index', compact('calendar', 'date', 'prevMonth', 'nextMonth'));
}
}
26 changes: 26 additions & 0 deletions app/Libraries/Bookings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Libraries;

use Carbon\Carbon;

class Bookings
{
/**
* Determine if a booking should be considered past (ended) in UTC.
*/
public static function isPastUtc(Carbon $dayDate, $bookingEnd, Carbon $nowUtc): bool
{
$endUtc = Carbon::parse($bookingEnd)->setTimezone('UTC');

if ($dayDate->lt($nowUtc->copy()->startOfDay())) {
return true;
}

if ($dayDate->isSameDay($nowUtc)) {
return $endUtc->lte($nowUtc);
}

return false;
}
}
15 changes: 15 additions & 0 deletions app/Models/Cts/Booking.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,19 @@ 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');
}

public function exams()
{
return $this->belongsTo(\App\Models\Cts\Exams::class, 'type_id', 'id');
}
}
24 changes: 24 additions & 0 deletions app/Models/Cts/Exams.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Models\Cts;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Exams extends Model
{
use HasFactory;

protected $connection = 'cts';

protected $table = 'exam_book';

public $timestamps = false;

public $incrementing = false;

public function examiner()
{
return $this->belongsTo(Member::class, 'exmr_id', 'id');
}
}
61 changes: 57 additions & 4 deletions app/Repositories/Cts/BookingRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@

class BookingRepository
{
public function getBookingsBetween($startDate, $endDate)
{
return Booking::with(['member', 'session.mentor'])
->whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()])
->get()->each(function ($booking) {
$booking->date = Carbon::parse($booking->date);
});
}

public function getBookings(Carbon $date)
{
$bookings = Booking::where('date', '=', $date->toDateString())
->with('member')
->with(['member', 'session.mentor'])
->orderBy('from')
->get();

Expand All @@ -21,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();

Expand All @@ -32,7 +41,7 @@ public function getTodaysLiveAtcBookings()
{
$bookings = Booking::where('date', '=', Carbon::now()->toDateString())
->networkAtc()
->with('member')
->with(['member', 'session.mentor'])
->orderBy('from')
->get();

Expand All @@ -44,7 +53,7 @@ public function getTodaysLiveAtcBookingsWithoutEvents()
$bookings = Booking::where('date', '=', Carbon::now()->toDateString())
->notEvent()
->networkAtc()
->with('member')
->with(['member', 'session.mentor'])
->orderBy('from')
->get();

Expand All @@ -60,6 +69,50 @@ 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,
];
}

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;
});

Expand Down
100 changes: 100 additions & 0 deletions resources/assets/js/bookings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
document.addEventListener('DOMContentLoaded', function () {
// ---- Display old bookings: toggle a CSS class on <body> ----
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}<br>${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);
}
});
Loading