Skip to content

Fix schedule availabilities API#200

Open
roncodes wants to merge 13 commits intomainfrom
dev-v1.6.39
Open

Fix schedule availabilities API#200
roncodes wants to merge 13 commits intomainfrom
dev-v1.6.39

Conversation

@roncodes
Copy link
Copy Markdown
Member

@roncodes roncodes commented Apr 5, 2026

No description provided.

roncodes and others added 13 commits April 5, 2026 10:53
… model, template apply

- Add rlanvin/php-rrule to composer.json for RRULE parsing and expansion
- Add migration: schedule_items.schedule_template_uuid, is_exception, exception_for_date columns
- Add migration: schedule_exceptions table (time_off, sick, holiday, swap, training, other types)
- Add migration: schedules.last_materialized_at, materialization_horizon columns
- Rewrite Schedule model: add exceptions(), appliedTemplates(), activeShiftFor(), isDriverAvailableAt()
- Rewrite ScheduleItem model: add scheduleTemplate(), scopeForDate(), scopeActiveOn(), isException()
- Rewrite ScheduleTemplate model: add schedule(), items(), expandOccurrences(), generateItems()
- Add ScheduleException model: polymorphic subject, approve/reject workflow, overlapping/coveringDate scopes
- Rewrite ScheduleService: full RRULE materialization engine with exception-aware shift generation
  - materializeTemplate(): expand RRULE, skip exception-covered dates, upsert ScheduleItems
  - applyTemplateToSchedule(): copy library template to driver schedule and materialize
  - approveException()/rejectException(): workflow transitions + cancel covered shifts on approval
  - getExceptionsForSubject(): filtered query helper
- Add MaterializeSchedulesJob: daily rolling 60-day window materialization for all active schedules
- Add ScheduleExceptionController: approve, reject, forSubject endpoints
- Rewrite ScheduleTemplateController: add apply and materialize endpoints
- Add ScheduleException and ScheduleTemplate HTTP resource classes
- Register schedule-exceptions routes with approve/reject/for-subject actions
- Register schedule-templates apply/materialize routes
- Register MaterializeSchedulesJob as daily:01:00 scheduled job in CoreServiceProvider
The schedule_templates migration was missing two columns that the model
already referenced in $fillable and applyToSchedule():

  - schedule_uuid: links an applied template copy to its parent Schedule
    (NULL for library templates, set when applyToSchedule() is called)
    The absence of this column caused:
    SQLSTATE[42S22]: Unknown column 'schedule_uuid' in 'field list'
    when POST /schedule-templates/{id}/apply was called.

  - color: hex colour string used by the frontend calendar to render
    shift blocks (e.g. #6366f1). Frontend was already sending this field.

Changes:
- migrations/2026_04_05_000001_add_schedule_uuid_color_to_schedule_templates_table.php
  New migration that adds both columns to the existing table.
- src/Models/ScheduleTemplate.php
  Added 'color' to $fillable array, applyToSchedule() copy, and @Property docblock.
  (schedule_uuid was already in $fillable — only the column was missing.)
- ScheduleService::applyTemplateToSchedule() now returns an array
  {template, items_created} instead of just the ScheduleTemplate model,
  so the apply endpoint can report the actual count from materializeTemplate()
  rather than a post-hoc items()->count() which could be stale or zero

- ScheduleTemplateController::apply() updated to destructure the new return
  and pass items_created directly from the materialization result

- Docblock updated to reflect the new return type signature
…y returning null

Previously getRruleInstance() caught all \Exception types, which meant a
'Class RRule\RRule not found' Error (not Exception) would still bubble up
but the catch block would silently return null, causing materializeTemplate()
to produce 0 items with no indication of the root cause.

Changes:
- Add class_exists('RRule\RRule') guard that throws a clear RuntimeException
  with an actionable message: 'Run composer require rlanvin/php-rrule'
- Narrow the catch to \RRule\RRuleException only (invalid RRULE strings)
  so legitimate missing-dependency errors are never swallowed
- Add \Log::warning() for invalid RRULE strings to aid debugging
…el/isPending on ScheduleException

- Add PolymorphicType cast to Schedule.subject_type, ScheduleItem.assignee_type,
  ScheduleTemplate.subject_type so frontend 'fleet-ops:driver' strings are
  stored/resolved as the full PHP class name in the database
- Create ScheduleItemFilter with scheduleUuid() that resolves public_id to UUID
  and startAtBetween()/endAtBetween() for range queries from the frontend
- Create ScheduleExceptionFilter with scheduleUuid() that resolves public_id to UUID
- Add typeLabel and isPending appended attributes to ScheduleException model
- Add debug logging to materializeTemplate for easier diagnosis
The php-rrule library requires the RFC 5545 property format:
  DTSTART:<date>    (colon separator, not equals sign)
  RRULE:<rule>      (RRULE: prefix required)

Previous code was generating:
  DTSTART=20260405T080000     <- wrong: equals sign
  FREQ=WEEKLY;BYDAY=MO,TU    <- wrong: missing RRULE: prefix

This caused InvalidArgumentException: 'Failed to parse RFC line,
missing property name followed by ":"' in RfcParser.php.

Also:
- Use DTSTART;TZID=<tz>:<date> for named timezones (not UTC)
- Use DTSTART:<date>Z for UTC
- Strip any existing RRULE: prefix from stored value to avoid doubling
- Catch InvalidArgumentException (RFC parse errors) in addition to RRuleException
…ve polymorphic type aliases

The GET /schedules?subject_type=fleet-ops:driver query was returning empty
because the DB stores the full PHP class name (Fleetbase\FleetOps\Models\Driver)
via the PolymorphicType cast, but no filter class existed to translate the
short alias 'fleet-ops:driver' into the FQCN before applying the WHERE clause.

This caused loadDriverSchedule() to always create a new duplicate Schedule
instead of finding the existing one, so materialized ScheduleItems were never
visible in the calendar.

Changes:
- Add ScheduleFilter: resolves subject_type alias, filters by subject_uuid/status
- Add ScheduleTemplateFilter: resolves subject_type alias, filters by subject_uuid/schedule_uuid
- Update ScheduleItemFilter: add assigneeType() + assigneeUuid() methods with alias resolution
- Update ScheduleExceptionFilter: add subjectType() + subjectUuid() methods with alias resolution

All four filters use Utils::getMutationType() which handles:
  'fleet-ops:driver' -> 'Fleetbase\FleetOps\Models\Driver'
  'fleet-ops:order'  -> 'Fleetbase\FleetOps\Models\Order'
  etc.
…edule on first item

- materializeTemplate: change default status from 'pending' to 'scheduled' for
  future shifts (clearer semantics — pending implies awaiting approval, scheduled
  means the shift is confirmed and upcoming)
- applyTemplateToSchedule: activate the schedule (draft → active) after the first
  template is successfully applied and items are materialized
- createScheduleItem: activate the parent schedule (draft → active) when the first
  standalone shift item is created directly
The status column was defined as ENUM('pending','confirmed','in_progress',
'completed','cancelled','no_show') — 'scheduled' was not a valid value, causing
MySQL to throw SQLSTATE[01000] Data truncated when materializeTemplate() tried
to insert items with status='scheduled'.

Adds a new migration that:
- Adds 'scheduled' to the ENUM (between 'pending' and 'confirmed')
- Changes the column DEFAULT from 'pending' to 'scheduled'
- Retains 'pending' for backwards compatibility

The down() migration reverts 'scheduled' rows to 'pending' before shrinking
the ENUM back to its original definition.
…hedules table

- New migration adds per-schedule HOS limit columns (nullable, fallback to global defaults)
- hos_source column allows future extensibility (schedule | telematics | manual)
- Schedule model fillable and casts updated accordingly
- New migration adds company_uuid column to schedule_items table
  and backfills from parent schedule
- ScheduleItem model: company_uuid added to fillable/filterParams,
  boot() auto-populates from parent schedule or session on creating
- ScheduleService::materializeTemplate: explicitly sets company_uuid
  from schedule.company_uuid on each created item
- ScheduleItemFilter::queryForInternal: uses company_uuid OR schedule
  join so items without a schedule_uuid (standalone shifts) are still
  included in company-scoped queries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant