Availability & Scheduling
The availability engine turns a subject's working hours (minus the time they're already busy) into open booking slots. It is pure, core, and reusable by any feature that needs "who is free when" — bookings is one consumer.
Schedules
A subject has one schedule (Schedule / data/schedule_class.php, table sch_schedules) — it is the subject's availability and the timezone anchor for their windows.
sch_subject_type/sch_subject_id— the owningCalendarSubject, unique together (one schedule per subject).sch_timezone— the IANA timezone the windows are defined in.
Schedule::for_subject(CalendarSubject) and Schedule::get_or_create_for_subject(CalendarSubject) load (or seed) the row.Weekly windows (ScheduleWindow, scw_schedule_windows): scw_day_of_week (0=Sunday…6=Saturday), scw_start_time/scw_end_time (PostgreSQL time). End must be after start; windows do not cross midnight (enter an overnight span as two windows on adjacent days).
Date overrides (ScheduleOverride, sco_schedule_overrides): sco_date plus nullable sco_start_time/sco_end_time. If any override rows exist for a date, they replace the weekly windows for that date — a single row with null start/end means fully unavailable; rows with times are that date's windows.
The wall-clock-time exception
Unlike every other time in the database (UTC), scw_*_time / sco_*_time are wall-clock times in the schedule's timezone, not UTC instants — "9am Monday" must survive DST transitions. The slot generator converts wall-clock windows to UTC instants per concrete date.
SlotGenerator
includes/scheduling/SlotGenerator.php — pure computation, no HTTP/DB, unit-testable.
SlotGenerator::generate(array $params)→ list of['start'=>UTC, 'end'=>UTC]. Params: timezone, windows, overrides, range (UTC),duration_minutes,increment_minutes,buffer_before_minutes,buffer_after_minutes,min_notice_minutes,busy(the busy projection), optionalnow_utc.SlotGenerator::availableIntervals(array $params)→ merged free intervals (windows minus busy), for previews.SlotGenerator::forSchedule($schedule, $start, $end, $opts, $busy)→ loads a Schedule's windows/overrides and generates slots; the caller supplies the busy projection.
Per-period caps (max bookings/day/week) are not the generator's job — the bookings layer counts its own bookings and suppresses slots on capped days, bounded in the schedule's timezone.
Availability editor
/profile/bookings/availability (a bookings-plugin view over the core models) edits the subject's single schedule: weekly windows, date overrides, and timezone. It previews the resulting open availability (green) against existing commitments using the calendar_grid component, fed by /api/v1/action/availability_preview.
Calendar UI components
Two universal HTML5 + vanilla-JS component types in views/components/, rendered viewer-local from UTC data:
calendar_grid— month/week views of timed items.ComponentRenderer::render(null, 'calendar_grid', ['items'=>[...], 'view'=>'month', 'feed_url'=>'...', 'initial_date'=>'Y-m-d']). Withfeed_urlit fetches{items:[...]}per range and pages client-side. Emits acalendardayclickevent (date) for click-to-create.slot_picker— booking time picker: mini-month + the selected day's times + a viewer-timezone selector (auto-detected, overridable). Loads UTC slots fromslots_url({slots:[{start,end}]}) and writes the chosen UTC slot start into a hidden field; emits aslotchosenevent.
Data layer
sch_schedules, scw_schedule_windows, sco_schedule_overrides, and cal_entries are core data-model classes. Schedule and CalendarEntry carry a polymorphic owner — (subject_type, subject_id) rather than a usr_users FK — so each defines a hand-written owner-or-staff authenticate_read/write() keyed on subject_id.