From 3f8223b89a65e6c73d37961064e378e3e6f66871 Mon Sep 17 00:00:00 2001 From: Ronald A Richardson Date: Mon, 30 Mar 2026 04:58:22 -0400 Subject: [PATCH 1/2] feat(server): add MorphMany scheduling relationships to Driver model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connects the FleetOps Driver Eloquent model to the core-api scheduling system by adding polymorphic relationships: - schedules(): MorphMany — driver as subject - scheduleItems(): MorphMany — driver as assignee - activeShiftFor(date): ?ScheduleItem — convenience method for the AllocationPayloadBuilder to retrieve the driver's active shift for a given date and inject start_at/end_at as VROOM time_window constraints - availabilities(): MorphMany — time-off and preferred working hour records Also adds required use statements for Schedule, ScheduleItem, MorphMany, and ScheduleAvailability from the core-api namespace. Refs: fleetbase/fleetops#214 --- server/src/Models/Driver.php | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/server/src/Models/Driver.php b/server/src/Models/Driver.php index aa36424dd..1eda511c2 100644 --- a/server/src/Models/Driver.php +++ b/server/src/Models/Driver.php @@ -11,6 +11,9 @@ use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint; use Fleetbase\Models\File; use Fleetbase\Models\Model; +use Fleetbase\Models\Schedule; +use Fleetbase\Models\ScheduleAvailability; +use Fleetbase\Models\ScheduleItem; use Fleetbase\Models\User; use Fleetbase\Traits\HasApiModelBehavior; use Fleetbase\Traits\HasApiModelCache; @@ -25,6 +28,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -300,6 +304,48 @@ public function devices(): HasMany return $this->hasMany(\Fleetbase\Models\UserDevice::class, 'user_uuid', 'user_uuid'); } + /** + * Get all schedules assigned to this driver. + * The driver acts as the polymorphic `subject` on the Schedule model. + */ + public function schedules(): MorphMany + { + return $this->morphMany(Schedule::class, 'subject'); + } + + /** + * Get all individual schedule items (shifts) assigned to this driver. + * The driver acts as the polymorphic `assignee` on the ScheduleItem model. + */ + public function scheduleItems(): MorphMany + { + return $this->morphMany(ScheduleItem::class, 'assignee'); + } + + /** + * Get the active schedule item (shift) for a given date. + * Used by AllocationPayloadBuilder to inject time_window constraints. + */ + public function activeShiftFor(\DateTimeInterface $date = null): ?ScheduleItem + { + $date = $date ?? now(); + + return $this->scheduleItems() + ->whereDate('start_at', $date) + ->whereIn('status', ['pending', 'active']) + ->orderBy('start_at') + ->first(); + } + + /** + * Get all availability records (time-off, preferred hours) for this driver. + * The driver acts as the polymorphic `subject` on the ScheduleAvailability model. + */ + public function availabilities(): MorphMany + { + return $this->morphMany(ScheduleAvailability::class, 'subject'); + } + /** * Get avatar url. */ From 8e7f11823e8d760bb605ad9cbbf933e99efe9a20 Mon Sep 17 00:00:00 2001 From: Ronald A Richardson Date: Mon, 30 Mar 2026 05:04:20 -0400 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20driver=20scheduling=20integration?= =?UTF-8?q?=20=E2=80=94=20connect=20Driver=20model=20to=20core-api=20sched?= =?UTF-8?q?uling=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Backend ### server/src/Models/Driver.php - Add schedules() MorphMany — driver as polymorphic subject on Schedule - Add scheduleItems() MorphMany — driver as polymorphic assignee on ScheduleItem - Add activeShiftFor(date) — convenience method for AllocationPayloadBuilder to retrieve the driver's active shift and inject time_window constraints - Add availabilities() MorphMany — time-off and preferred working hours - Add use statements for Schedule, ScheduleItem, ScheduleAvailability, MorphMany ### server/src/Http/Controllers/Internal/v1/Traits/DriverSchedulingTrait.php (new) - scheduleItems(id, request) — GET /{id}/schedule-items with date range filter - availabilities(id, request) — GET /{id}/availabilities with date range filter - hosStatus(id) — GET /{id}/hos-status, computes daily/weekly hours from shifts - activeShift(id) — GET /{id}/active-shift, used by AllocationPayloadBuilder ### server/src/Http/Controllers/Internal/v1/DriverController.php - Use DriverSchedulingTrait to expose the four scheduling endpoints ### server/src/routes.php - Register four scheduling sub-routes under the internal drivers fleetbaseRoutes group ## Frontend ### addon/components/driver/schedule.js (rewritten) - Inject fetch, intl, store, driverScheduling, modalsManager, notifications - loadDriverSchedule task: queries schedule-items via driverScheduling service - loadAvailability task: queries schedule-availability via store - loadHOSStatus task: calls /drivers/{id}/hos-status, suppresses 404 gracefully - addShift, editShift, deleteShift actions using modals/add-driver-shift and modals/driver-shift - setAvailability, requestTimeOff, deleteAvailability actions using modals/set-driver-availability ### addon/components/driver/schedule.hbs (rewritten) - HOS Status panel (hidden when hosStatus is null) - Upcoming Shifts panel with edit/delete per-shift buttons - Availability & Time Off panel with add/delete availability buttons - Fleetbase UI styling (minimal padding, Tailwind, ContentPanel, Badge, Button) ### addon/components/modals/add-driver-shift.{js,hbs} (new) - Creates a ScheduleItem with assignee_type=driver - Supports both single-driver (from driver panel) and multi-driver (from global scheduler) modes via the hasManyDrivers computed property ### addon/components/modals/driver-shift.{js,hbs} (new) - Edits an existing ScheduleItem, pre-populates fields from the passed item ### addon/components/modals/set-driver-availability.{js,hbs} (new) - Shared modal for both Set Availability (is_available=true) and Request Time Off (is_available=false) flows ### addon/services/driver-actions.js - Add Schedule tab (component: 'driver/schedule') to the driver view panel tabs ### addon/controllers/operations/scheduler/index.js - Add driverAvailabilities tracked property - Enhance events computed to render unavailable availability records as FullCalendar background events (red-300) so dispatchers see time-off blocks - Add loadDriverAvailabilities task - Call loadDriverAvailabilities when switching to 'drivers' view mode - Use intl.t() for addDriverShift modal title and button text Refs: fleetbase/fleetops#214 --- addon/components/driver/schedule.hbs | 136 +++--- addon/components/driver/schedule.js | 393 +++++++++++------- addon/components/modals/add-driver-shift.hbs | 67 +++ addon/components/modals/add-driver-shift.js | 71 ++++ addon/components/modals/driver-shift.hbs | 49 +++ addon/components/modals/driver-shift.js | 60 +++ .../modals/set-driver-availability.hbs | 72 ++++ .../modals/set-driver-availability.js | 60 +++ .../controllers/operations/scheduler/index.js | 43 +- addon/services/driver-actions.js | 4 + .../Internal/v1/DriverController.php | 1 + .../v1/Traits/DriverSchedulingTrait.php | 133 ++++++ server/src/routes.php | 5 + 13 files changed, 876 insertions(+), 218 deletions(-) create mode 100644 addon/components/modals/add-driver-shift.hbs create mode 100644 addon/components/modals/add-driver-shift.js create mode 100644 addon/components/modals/driver-shift.hbs create mode 100644 addon/components/modals/driver-shift.js create mode 100644 addon/components/modals/set-driver-availability.hbs create mode 100644 addon/components/modals/set-driver-availability.js create mode 100644 server/src/Http/Controllers/Internal/v1/Traits/DriverSchedulingTrait.php diff --git a/addon/components/driver/schedule.hbs b/addon/components/driver/schedule.hbs index b78c95121..d5aa2e73e 100644 --- a/addon/components/driver/schedule.hbs +++ b/addon/components/driver/schedule.hbs @@ -1,100 +1,110 @@
- - {{#if this.hosStatus}} -
+ {{! HOS Status Panel — only shown when hosStatus is available }} + {{#if this.hosStatus}} + +
-
Daily Driving Hours
-
{{this.hosStatus.daily_hours}}/11
-
-
- +
{{t "scheduler.daily-driving-hours"}}
+
{{this.hosStatus.daily_hours}}/11h
-
-
Weekly Hours
-
{{this.hosStatus.weekly_hours}}/70
-
-
- +
{{t "scheduler.weekly-hours"}}
+
{{this.hosStatus.weekly_hours}}/70h
-
-
Compliance Status
- - {{this.hosComplianceBadge.text}} +
{{t "scheduler.compliance-status"}}
+ + {{this.hosComplianceBadge.label}}
- {{/if}} -
- - - <:item as |ctx|> -
-
{{ctx.item.title}}
-
{{ctx.item.duration}} min
-
- -
-
+ + {{/if}} - + {{! Upcoming Shifts Panel }} + {{#if this.upcomingShifts.length}} -
+
{{#each this.upcomingShifts as |shift|}} - - <:actions> - - - +
+
+
+
+
{{shift.title}}
+
+ {{format-date-fns shift.start_at "EEE, MMM d · HH:mm"}} + – + {{format-date-fns shift.end_at "HH:mm"}} +
+
+
+
+ +
+
{{/each}}
{{else}} -
- No upcoming shifts scheduled +
+ {{t "scheduler.no-upcoming-shifts"}}
{{/if}} - + {{! Availability & Time Off Panel }} + {{#if this.availability.length}} -
+
{{#each this.availability as |avail|}} -
-
-
- {{#if avail.is_available}} - Available - {{else}} - Unavailable +
+
+
+
+
+ {{#if avail.is_available}} + {{t "scheduler.available"}} + {{else}} + {{t "scheduler.unavailable"}} + {{/if}} +
+
+ {{format-date-fns avail.start_at "MMM d"}} + – + {{format-date-fns avail.end_at "MMM d, yyyy"}} +
+ {{#if avail.reason}} +
{{avail.reason}}
{{/if}}
-
- {{format-date-fns avail.start_at "MMM DD, YYYY"}} - - - {{format-date-fns avail.end_at "MMM DD, YYYY"}} -
- {{#if avail.reason}} -
{{avail.reason}}
- {{/if}}
+
{{/each}}
{{else}} -
- No availability restrictions set +
+ {{t "scheduler.no-availability-restrictions"}}
{{/if}} -
\ No newline at end of file +
diff --git a/addon/components/driver/schedule.js b/addon/components/driver/schedule.js index a26a1eca1..a2fad7743 100644 --- a/addon/components/driver/schedule.js +++ b/addon/components/driver/schedule.js @@ -3,46 +3,83 @@ import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { task } from 'ember-concurrency'; +import { startOfWeek, endOfWeek, addWeeks, formatISO } from 'date-fns'; /** * Driver::Schedule Component * - * Displays and manages a driver's schedule from their detail page. - * Includes HOS compliance tracking, upcoming shifts, and availability management. + * Displays and manages a driver's schedule from their detail panel. + * Renders upcoming shifts in a list view and provides actions to add shifts, + * request time off, and manage availability windows. + * + * Wires directly to the core-api scheduling system via the driverScheduling + * service. The schedule_items relationship on the Driver model uses + * assignee_type='driver' and assignee_uuid=driver.id as the polymorphic key. * * @example - * + * */ export default class DriverScheduleComponent extends Component { @service driverScheduling; @service notifications; @service modalsManager; @service store; + @service fetch; + @service intl; + @tracked scheduleItems = []; @tracked upcomingShifts = []; - @tracked hosStatus = null; @tracked availability = []; - @tracked timeOffRequests = []; - @tracked selectedItem = null; + @tracked hosStatus = null; + @tracked isLoadingSchedule = false; + @tracked isLoadingAvailability = false; - get scheduleActionButtons() { - return [ - { - type: 'default', - text: 'Request Time Off', - icon: 'calendar-times', - iconPrefix: 'fas', - permission: 'fleet-ops update driver', - onClick: this.requestTimeOff, - }, - ]; + constructor() { + super(...arguments); + this.loadDriverSchedule.perform(); + this.loadAvailability.perform(); + this.loadHOSStatus.perform(); + } + + /** + * Start of the current week — used as the lower bound for schedule queries. + */ + get startDate() { + return formatISO(startOfWeek(new Date(), { weekStartsOn: 1 })); + } + + /** + * Four weeks from now — used as the upper bound for schedule queries. + */ + get endDate() { + return formatISO(addWeeks(endOfWeek(new Date(), { weekStartsOn: 1 }), 4)); + } + + /** + * Derive a color-coded HOS compliance badge from the loaded hosStatus. + */ + get hosComplianceBadge() { + if (!this.hosStatus) { + return { color: 'gray', label: 'Unknown' }; + } + const { daily_hours, weekly_hours } = this.hosStatus; + if (daily_hours >= 11 || weekly_hours >= 70) { + return { color: 'red', label: 'At Limit' }; + } + if (daily_hours >= 9 || weekly_hours >= 60) { + return { color: 'yellow', label: 'Approaching Limit' }; + } + return { color: 'green', label: 'Compliant' }; } + /** + * Action buttons rendered in the Upcoming Shifts ContentPanel header. + */ get shiftActionButtons() { return [ { type: 'default', - text: 'Add Shift', + text: this.intl.t('scheduler.add-shift'), icon: 'plus', iconPrefix: 'fas', permission: 'fleet-ops update driver', @@ -51,217 +88,271 @@ export default class DriverScheduleComponent extends Component { ]; } + /** + * Action buttons rendered in the Availability ContentPanel header. + */ get availabilityActionButtons() { return [ { type: 'default', - text: 'Set Availability', + text: this.intl.t('scheduler.set-availability'), icon: 'clock', iconPrefix: 'fas', permission: 'fleet-ops update driver', onClick: this.setAvailability, }, + { + type: 'default', + text: this.intl.t('scheduler.request-time-off'), + icon: 'calendar-times', + iconPrefix: 'fas', + permission: 'fleet-ops update driver', + onClick: this.requestTimeOff, + }, ]; } - constructor() { - super(...arguments); - this.loadDriverSchedule.perform(); - this.loadHOSStatus.perform(); - this.loadAvailability.perform(); - } - /** - * Load driver schedule items + * Load all schedule items (shifts) for this driver within the 4-week window. + * Uses the driverScheduling service which calls the core-api schedule-items endpoint + * with assignee_type=driver and assignee_uuid=driver.id. */ @task *loadDriverSchedule() { + this.isLoadingSchedule = true; try { const items = yield this.driverScheduling.getScheduleItemsForAssignee.perform('driver', this.args.resource.id, { start_at: this.startDate, end_at: this.endDate, }); - this.scheduleItems = items.toArray(); - this.upcomingShifts = this.scheduleItems.filter((item) => new Date(item.start_at) > new Date()).slice(0, 5); - } catch (error) { - console.error('Failed to load driver schedule:', error); - } - } - - /** - * Load HOS status for the driver - */ - @task *loadHOSStatus() { - try { - const response = yield this.fetch.get(`drivers/${this.args.resource.id}/hos-status`); - this.hosStatus = response; + this.upcomingShifts = this.scheduleItems + .filter((item) => new Date(item.start_at) >= new Date()) + .sort((a, b) => new Date(a.start_at) - new Date(b.start_at)); } catch (error) { - console.error('Failed to load HOS status:', error); + this.notifications.serverError(error); + } finally { + this.isLoadingSchedule = false; } } /** - * Load driver availability + * Load availability records (time-off, preferred hours) for this driver. + * Uses the core-api schedule-availability endpoint with subject_type=driver. */ @task *loadAvailability() { + this.isLoadingAvailability = true; try { const availability = yield this.store.query('schedule-availability', { subject_type: 'driver', subject_uuid: this.args.resource.id, - start_at: this.startDate, - end_at: this.endDate, }); - this.availability = availability.toArray(); } catch (error) { - console.error('Failed to load availability:', error); + this.notifications.serverError(error); + } finally { + this.isLoadingAvailability = false; } } /** - * Get start date for schedule query (current week) + * Load HOS (Hours of Service) status from the FleetOps driver endpoint. + * This is a best-effort load — if the endpoint is not yet implemented, + * the hosStatus will remain null and the HOS panel will be hidden. */ - get startDate() { - const now = new Date(); - const dayOfWeek = now.getDay(); - const diff = now.getDate() - dayOfWeek; - return new Date(now.setDate(diff)).toISOString(); - } - - /** - * Get end date for schedule query (4 weeks out) - */ - get endDate() { - const now = new Date(); - return new Date(now.setDate(now.getDate() + 28)).toISOString(); - } - - /** - * Get HOS compliance badge color - */ - get hosComplianceBadge() { - if (!this.hosStatus) { - return { color: 'gray', text: 'Unknown' }; - } - - const { daily_hours, weekly_hours } = this.hosStatus; - - if (daily_hours >= 11 || weekly_hours >= 70) { - return { color: 'red', text: 'At Limit' }; - } - - if (daily_hours >= 9 || weekly_hours >= 60) { - return { color: 'yellow', text: 'Approaching Limit' }; + @task *loadHOSStatus() { + try { + const response = yield this.fetch.get(`fleet-ops/drivers/${this.args.resource.id}/hos-status`); + this.hosStatus = response; + } catch { + // HOS endpoint not yet implemented — suppress error silently + this.hosStatus = null; } - - return { color: 'green', text: 'Compliant' }; - } - - /** - * Handle item click - */ - @action - handleItemClick(item) { - this.selectedItem = item; - this.modalsManager.show('modals/schedule-item-details', { - item, - onEdit: this.editScheduleItem, - onDelete: this.deleteScheduleItem, - }); } /** - * Add new shift + * Open the Add Shift modal. Uses the same modalsManager pattern as the + * global scheduler's addDriverShift() action, reusing modals/add-driver-shift. */ @action addShift() { - this.modalsManager.show('modals/add-shift', { - driver: this.args.resource, - onSave: this.handleShiftAdded, + this.modalsManager.show('modals/add-driver-shift', { + title: this.intl.t('scheduler.add-shift'), + acceptButtonText: this.intl.t('scheduler.create-shift'), + acceptButtonIcon: 'plus', + drivers: [this.args.resource], + selectedDriver: this.args.resource, + confirm: async (modal) => { + modal.startLoading(); + const { startAt, endAt, duration } = modal.getOptions(); + try { + const scheduleItem = this.store.createRecord('schedule-item', { + assignee_type: 'driver', + assignee_uuid: this.args.resource.id, + start_at: startAt, + end_at: endAt, + duration: duration, + status: 'pending', + }); + yield scheduleItem.save(); + this.notifications.success(this.intl.t('scheduler.shift-created')); + yield this.loadDriverSchedule.perform(); + yield this.loadHOSStatus.perform(); + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }, }); } /** - * Edit schedule item + * Open the Edit Shift modal for an existing schedule item. */ @action - async editScheduleItem(item) { - this.modalsManager.show('modals/edit-shift', { + editShift(item) { + this.modalsManager.show('modals/driver-shift', { + title: this.intl.t('scheduler.edit-shift'), + acceptButtonText: this.intl.t('common.save-changes'), + acceptButtonIcon: 'save', item, - driver: this.args.resource, - onSave: this.handleShiftUpdated, + confirm: async (modal) => { + modal.startLoading(); + const { startAt, endAt } = modal.getOptions(); + try { + item.setProperties({ start_at: startAt, end_at: endAt }); + yield item.save(); + this.notifications.success(this.intl.t('scheduler.shift-updated')); + yield this.loadDriverSchedule.perform(); + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }, }); } /** - * Delete schedule item + * Delete a shift after inline confirmation via modalsManager. */ @action - async deleteScheduleItem(item) { - if (confirm('Are you sure you want to delete this shift?')) { - try { - await this.driverScheduling.deleteScheduleItem.perform(item); - await this.loadDriverSchedule.perform(); - } catch (error) { - console.error('Failed to delete shift:', error); - } - } - } - - /** - * Handle shift added - */ - @action - async handleShiftAdded() { - await this.loadDriverSchedule.perform(); - await this.loadHOSStatus.perform(); - } - - /** - * Handle shift updated - */ - @action - async handleShiftUpdated() { - await this.loadDriverSchedule.perform(); - await this.loadHOSStatus.perform(); + deleteShift(item) { + this.modalsManager.confirm({ + title: this.intl.t('scheduler.delete-shift'), + body: this.intl.t('scheduler.delete-shift-confirm'), + acceptButtonText: this.intl.t('common.delete'), + acceptButtonScheme: 'danger', + confirm: async (modal) => { + modal.startLoading(); + try { + yield this.driverScheduling.deleteScheduleItem.perform(item); + yield this.loadDriverSchedule.perform(); + yield this.loadHOSStatus.perform(); + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }, + }); } /** - * Request time off + * Open the Set Availability modal to record preferred working hours. */ @action - requestTimeOff() { - this.modalsManager.show('modals/request-time-off', { + setAvailability() { + this.modalsManager.show('modals/set-driver-availability', { + title: this.intl.t('scheduler.set-availability'), + acceptButtonText: this.intl.t('common.save'), + acceptButtonIcon: 'check', driver: this.args.resource, - onSave: this.handleTimeOffRequested, + isAvailable: true, + confirm: async (modal) => { + modal.startLoading(); + const { startAt, endAt, isAvailable, reason, notes } = modal.getOptions(); + try { + const availability = this.store.createRecord('schedule-availability', { + subject_type: 'driver', + subject_uuid: this.args.resource.id, + start_at: startAt, + end_at: endAt, + is_available: isAvailable, + reason, + notes, + }); + yield availability.save(); + this.notifications.success(this.intl.t('scheduler.availability-set')); + yield this.loadAvailability.perform(); + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }, }); } /** - * Handle time off requested - */ - @action - async handleTimeOffRequested() { - await this.loadAvailability.perform(); - await this.loadDriverSchedule.perform(); - } - - /** - * Set availability + * Open the Request Time Off modal. Creates a schedule-availability record + * with is_available=false to mark the driver as unavailable. */ @action - setAvailability() { - this.modalsManager.show('modals/set-availability', { + requestTimeOff() { + this.modalsManager.show('modals/set-driver-availability', { + title: this.intl.t('scheduler.request-time-off'), + acceptButtonText: this.intl.t('scheduler.submit-request'), + acceptButtonIcon: 'calendar-times', driver: this.args.resource, - onSave: this.handleAvailabilitySet, + isAvailable: false, + confirm: async (modal) => { + modal.startLoading(); + const { startAt, endAt, reason, notes } = modal.getOptions(); + try { + const availability = this.store.createRecord('schedule-availability', { + subject_type: 'driver', + subject_uuid: this.args.resource.id, + start_at: startAt, + end_at: endAt, + is_available: false, + reason, + notes, + }); + yield availability.save(); + this.notifications.success(this.intl.t('scheduler.time-off-requested')); + yield this.loadAvailability.perform(); + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }, }); } /** - * Handle availability set + * Delete an availability record. */ @action - async handleAvailabilitySet() { - await this.loadAvailability.perform(); + deleteAvailability(avail) { + this.modalsManager.confirm({ + title: this.intl.t('scheduler.delete-availability'), + body: this.intl.t('scheduler.delete-availability-confirm'), + acceptButtonText: this.intl.t('common.delete'), + acceptButtonScheme: 'danger', + confirm: async (modal) => { + modal.startLoading(); + try { + yield avail.destroyRecord(); + this.notifications.success(this.intl.t('scheduler.availability-deleted')); + yield this.loadAvailability.perform(); + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }, + }); } } diff --git a/addon/components/modals/add-driver-shift.hbs b/addon/components/modals/add-driver-shift.hbs new file mode 100644 index 000000000..01aaee3bf --- /dev/null +++ b/addon/components/modals/add-driver-shift.hbs @@ -0,0 +1,67 @@ + +