Initial commit
This commit is contained in:
commit
c909d539bc
7178 files changed
+756624
No files matched your search
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ExternalBusyWindow;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AvailabilityService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $tz = 'Europe/London',
|
||||
private readonly int $bufferMins = 40
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check if a single slot (date + time) is blocked by external busy windows,
|
||||
* considering a travel buffer before start and after end.
|
||||
*/
|
||||
public function isSlotBlockedByExternalBusy(string $dateYmd, string $timeHi, int $durationMins = 60): bool
|
||||
{
|
||||
// Build slot window in UTC
|
||||
$slotStartLocal = Carbon::createFromFormat('Y-m-d H:i', "{$dateYmd} {$timeHi}", $this->tz);
|
||||
$slotStartUtc = $slotStartLocal->clone()->utc();
|
||||
$slotEndUtc = $slotStartUtc->clone()->addMinutes($durationMins);
|
||||
|
||||
// Expand comparison bounds by buffer
|
||||
$slotStartMinusBuffer = $slotStartUtc->clone()->subMinutes($this->bufferMins);
|
||||
$slotEndPlusBuffer = $slotEndUtc->clone()->addMinutes($this->bufferMins);
|
||||
|
||||
// Overlap: busyStart < slotEnd+buffer AND busyEnd > slotStart-buffer
|
||||
return ExternalBusyWindow::query()
|
||||
->where('starts_at', '<', $slotEndPlusBuffer)
|
||||
->where('ends_at', '>', $slotStartMinusBuffer)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* For block-booking: given a set of future candidate dates for a given time,
|
||||
* return the subset that are blocked (with reasons).
|
||||
*
|
||||
* @param array<string> $dateListYmd e.g. ['2025-10-27','2025-11-03', ...]
|
||||
* @return array<int, array{date:string, blocked:bool}>
|
||||
*/
|
||||
public function checkSeriesConflicts(array $dateListYmd, string $timeHi, int $durationMins = 60): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($dateListYmd as $ymd) {
|
||||
$blocked = $this->isSlotBlockedByExternalBusy($ymd, $timeHi, $durationMins);
|
||||
$results[] = [
|
||||
'date' => $ymd,
|
||||
'blocked' => $blocked,
|
||||
];
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ExternalBusyWindow;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class BusyWindowService
|
||||
{
|
||||
public static function getBusyRanges()
|
||||
{
|
||||
// Fetch current + upcoming busy windows
|
||||
return ExternalBusyWindow::where('ends_at', '>=', now())
|
||||
->get()
|
||||
->map(fn($w) => [
|
||||
'start' => Carbon::parse($w->starts_at)->subMinutes(40),
|
||||
'end' => Carbon::parse($w->ends_at)->addMinutes(40),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function isInBusyWindow($slotStart, $busyRanges, $slotLengthMinutes = 60)
|
||||
{
|
||||
$slotEnd = (clone $slotStart)->addMinutes($slotLengthMinutes);
|
||||
|
||||
foreach ($busyRanges as $range) {
|
||||
// If the slot overlaps the busy window in any way
|
||||
if ($slotStart < $range['end'] && $slotEnd > $range['start']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Slot;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CalendarService
|
||||
{
|
||||
/**
|
||||
* Build calendar grid for Mon–Fri with your six daily times.
|
||||
*
|
||||
* @param Carbon $start Monday of the week you want to display
|
||||
* @param Carbon $now Current time (use 'Europe/London')
|
||||
* @param bool $applyShortNotice If true, mark ≤ 120 min as closed (student view)
|
||||
* @return array{headers: array<int, array{date:string,label:string}>, rows: array<int, array{time:string,label:string,cells:array<int,array{date:string,time:string,slot:int|null,status:string,class:string,name:?string}>}>, times: string[]}
|
||||
*/
|
||||
public function build(Carbon $start, Carbon $now, bool $applyShortNotice): array
|
||||
{
|
||||
$end = $start->copy()->addDays(6);
|
||||
|
||||
$weekdays = [1,2,3,4,5]; // Mon..Fri
|
||||
$times = ['09:00:00','10:00:00','11:00:00','14:00:00','15:00:00','16:00:00'];
|
||||
|
||||
// Get all slots with bookings in the visible week
|
||||
$slots = Slot::with(['bookings' => function ($q) use ($start, $end) {
|
||||
$q->whereBetween('date', [$start->toDateString(), $end->toDateString()]);
|
||||
}])->get();
|
||||
|
||||
// Map slots by weekday/time for O(1) lookup in the loop
|
||||
$map = [];
|
||||
foreach ($slots as $s) {
|
||||
$map[(int)$s->weekday][$s->time] = $s;
|
||||
}
|
||||
|
||||
// Column headers
|
||||
$headers = [];
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$d = $start->copy()->addDays($i);
|
||||
$headers[] = [
|
||||
'date' => $d->toDateString(),
|
||||
'label' => $d->format('D d M'),
|
||||
];
|
||||
}
|
||||
|
||||
// Rows (each time)
|
||||
$rows = [];
|
||||
foreach ($times as $time) {
|
||||
$cells = [];
|
||||
foreach ($weekdays as $wd) {
|
||||
$date = $start->copy()->addDays($wd - 1)->toDateString();
|
||||
$slot = $map[$wd][$time] ?? null;
|
||||
$booking = $slot ? $slot->bookings->firstWhere('date', $date) : null;
|
||||
|
||||
// Compute short-notice closure only when required (students)
|
||||
$sessionStart = Carbon::createFromFormat('Y-m-d H:i:s', "{$date} {$time}", $now->timezone);
|
||||
$minutesUntil = $now->diffInMinutes($sessionStart, false);
|
||||
$tooSoon = $applyShortNotice ? ($minutesUntil <= 120) : false;
|
||||
|
||||
// Decide status + cell class
|
||||
$status = 'closed';
|
||||
$class = 'bg-gray-100';
|
||||
$name = null;
|
||||
|
||||
if ($booking) {
|
||||
if ($booking->status === 'booked') { $status = 'booked'; $class = 'bg-red-200'; }
|
||||
elseif ($booking->status === 'blocked') { $status = 'blocked'; $class = 'bg-yellow-200'; }
|
||||
} elseif (!$slot) {
|
||||
$status = 'not_seeded'; // slot wasn’t seeded for this weekday/time
|
||||
} elseif ($minutesUntil < 0 || $tooSoon) {
|
||||
$status = 'closed';
|
||||
} else {
|
||||
$status = 'free';
|
||||
$class = 'bg-green-100';
|
||||
}
|
||||
|
||||
if ($booking && $booking->student_name) {
|
||||
$name = $booking->student_name;
|
||||
}
|
||||
|
||||
$cells[] = [
|
||||
'date' => $date,
|
||||
'time' => $time,
|
||||
'slot' => $slot?->id,
|
||||
'status' => $status, // free | booked | blocked | closed | not_seeded
|
||||
'class' => $class,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'time' => $time,
|
||||
'label' => substr($time, 0, 5),
|
||||
'cells' => $cells,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'headers' => $headers,
|
||||
'rows' => $rows,
|
||||
'times' => $times,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user