Compare commits

...

3 Commits

Author SHA1 Message Date
Ronald A Richardson
f4715d137f fix: update schedule-item model default status from 'pending' to 'scheduled' 2026-04-05 04:29:00 -04:00
Ronald A Richardson
a83f1f7079 fix: use start_at/end_at in schedule-exception model to match backend schema
The backend schedule_exceptions table uses start_at/end_at timestamp columns,
not start_date/end_date. Update the Ember model to match.
2026-04-05 03:51:46 -04:00
Ronald A Richardson
7f5242fff4 feat: update scheduling Ember Data models for robust recurring schedule framework
- schedule-item: add schedule_template_uuid, title, notes, is_exception,
  exception_for_date fields; async relationships to schedule and scheduleTemplate
- schedule: add last_materialized_at, materialization_horizon_days fields;
  add exceptions hasMany relationship; make relationships async
- schedule-template: add break_start_time, break_end_time, color fields;
  add recurrenceSummary computed getter for human-readable RRULE display;
  make company relationship async
- schedule-exception: new model for time-off requests, sick leave, holidays,
  shift swaps and other deviations from recurring schedule patterns;
  includes isPending/isApproved/isRejected and typeLabel getters

These changes align the Ember Data layer with the core-api scheduling
framework refactoring (dev-v1.6.39) that introduces RRULE-based
materialization and exception handling.
2026-04-04 23:46:06 -04:00
4 changed files with 127 additions and 12 deletions

View File

@@ -0,0 +1,69 @@
import Model, { attr, belongsTo } from '@ember-data/model';
/**
* ScheduleException
*
* Represents a deviation from a driver's recurring schedule — time off requests,
* sick leave, holidays, shift swaps, training days, or other exceptions.
*
* An exception is submitted by a driver or dispatcher, then approved or rejected
* by a manager. When approved, all ScheduleItem records that fall within the
* exception's date range are automatically cancelled.
*
* @see core-api ScheduleException model
*/
export default class ScheduleExceptionModel extends Model {
/** @ids */
@attr('string') public_id;
@attr('string') schedule_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
@attr('string') requested_by_uuid;
/** @attributes */
@attr('string', { defaultValue: 'time_off' }) type;
@attr('string', { defaultValue: 'pending' }) status;
@attr('date') start_at;
@attr('date') end_at;
@attr('string') reason;
@attr('string') notes;
@attr('string') rejection_reason;
@attr('date') reviewed_at;
@attr('string') reviewed_by_uuid;
/** @meta */
@attr('object') meta;
/** @relationships */
@belongsTo('schedule', { async: true }) schedule;
/** @dates */
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
/** @computed */
get isPending() {
return this.status === 'pending';
}
get isApproved() {
return this.status === 'approved';
}
get isRejected() {
return this.status === 'rejected';
}
get typeLabel() {
const labels = {
time_off: 'Time Off',
sick_leave: 'Sick Leave',
holiday: 'Holiday',
swap: 'Shift Swap',
training: 'Training',
other: 'Other',
};
return labels[this.type] || this.type;
}
}

View File

@@ -1,8 +1,10 @@
import Model, { attr, belongsTo } from '@ember-data/model';
export default class ScheduleItemModel extends Model {
/** @ids */
@attr('string') public_id;
@attr('string') schedule_uuid;
@attr('string') schedule_template_uuid;
@attr('string') assignee_uuid;
@attr('string') assignee_type;
@attr('string') resource_uuid;
@@ -12,10 +14,20 @@ export default class ScheduleItemModel extends Model {
@attr('number') duration;
@attr('date') break_start_at;
@attr('date') break_end_at;
@attr('string', { defaultValue: 'pending' }) status;
@attr('string') title;
@attr('string', { defaultValue: 'scheduled' }) status;
@attr('string') notes;
/** @exception fields */
@attr('boolean', { defaultValue: false }) is_exception;
@attr('date') exception_for_date;
/** @meta */
@attr('object') meta;
@belongsTo('schedule') schedule;
/** @relationships */
@belongsTo('schedule', { async: true }) schedule;
@belongsTo('schedule-template', { async: true }) scheduleTemplate;
@attr('date') created_at;
@attr('date') updated_at;

View File

@@ -1,22 +1,51 @@
import Model, { attr, belongsTo } from '@ember-data/model';
/**
* ScheduleTemplate
*
* A reusable recurring shift pattern that a manager can apply to one or many
* drivers. Stores the RRULE (e.g. FREQ=WEEKLY;BYDAY=MO,TU,TH) plus the
* daily start/end times. When applied to a driver's Schedule, the
* ScheduleService materialises ScheduleItem records for the rolling horizon.
*/
export default class ScheduleTemplateModel extends Model {
/** @ids */
@attr('string') public_id;
@attr('string') company_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
/** @attributes */
@attr('string') name;
@attr('string') description;
@attr('string') start_time;
@attr('string') end_time;
@attr('number') duration;
@attr('number') break_duration;
@attr('string') rrule;
@attr('string') start_time; // HH:mm — e.g. "08:00"
@attr('string') end_time; // HH:mm — e.g. "16:00"
@attr('string') break_start_time; // HH:mm — e.g. "12:00" (optional)
@attr('string') break_end_time; // HH:mm — e.g. "13:00" (optional)
@attr('number') duration; // computed minutes (end - start)
@attr('number') break_duration; // minutes
@attr('string') rrule; // RFC 5545 RRULE string
@attr('string') color; // hex color for calendar display
@attr('object') meta;
@belongsTo('company') company;
/** @relationships */
@belongsTo('company', { async: true }) company;
/** @dates */
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
/**
* Parse the RRULE string and return a human-readable summary.
* e.g. "Weekly on Mon, Tue, Thu · 08:0016:00"
*/
get recurrenceSummary() {
if (!this.rrule) return null;
const dayMap = { MO: 'Mon', TU: 'Tue', WE: 'Wed', TH: 'Thu', FR: 'Fri', SA: 'Sat', SU: 'Sun' };
const byday = this.rrule.match(/BYDAY=([^;]+)/);
const days = byday ? byday[1].split(',').map((d) => dayMap[d] || d).join(', ') : '';
const freq = this.rrule.match(/FREQ=(\w+)/);
const freqLabel = freq ? freq[1].charAt(0) + freq[1].slice(1).toLowerCase() : '';
const times = this.start_time && this.end_time ? ` · ${this.start_time}${this.end_time}` : '';
return `${freqLabel} on ${days}${times}`;
}
}

View File

@@ -16,9 +16,14 @@ export default class ScheduleModel extends Model {
@attr('string', { defaultValue: 'draft' }) status;
@attr('object') meta;
/** @materialization tracking */
@attr('date') last_materialized_at;
@attr('number', { defaultValue: 60 }) materialization_horizon_days;
/** @relationships */
@hasMany('schedule-item') items;
@belongsTo('company') company;
@hasMany('schedule-item', { async: true }) items;
@hasMany('schedule-exception', { async: true }) exceptions;
@belongsTo('company', { async: true }) company;
/** @dates */
@attr('date') created_at;