@fleetbase/ember-ui 0.3.10 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/SCHEDULING_COMPONENTS.md +228 -0
  2. package/addon/components/availability-editor.js +81 -0
  3. package/addon/components/filter/multi-input.hbs +16 -0
  4. package/addon/components/filter/multi-input.js +68 -0
  5. package/addon/components/filter/range.hbs +52 -0
  6. package/addon/components/filter/range.js +74 -0
  7. package/addon/components/filters-picker.js +13 -6
  8. package/addon/components/layout/resource/tabular.hbs +2 -0
  9. package/addon/components/layout/resource/tabular.js +28 -0
  10. package/addon/components/modals/query-builder-computed-column-editor.hbs +80 -0
  11. package/addon/components/modals/query-builder-computed-column-editor.js +156 -0
  12. package/addon/components/query-builder/computed-columns.hbs +59 -0
  13. package/addon/components/query-builder/computed-columns.js +106 -0
  14. package/addon/components/query-builder.hbs +10 -0
  15. package/addon/components/query-builder.js +5 -0
  16. package/addon/components/report-builder.hbs +1 -0
  17. package/addon/components/schedule-calendar.hbs +37 -0
  18. package/addon/components/schedule-calendar.js +166 -0
  19. package/addon/components/schedule-item-card.hbs +39 -0
  20. package/addon/components/schedule-item-card.js +74 -0
  21. package/addon/components/table/cell/dropdown.hbs +2 -0
  22. package/addon/components/table/cell/dropdown.js +11 -0
  23. package/addon/components/table/cell/resizer.js +1 -1
  24. package/addon/components/table/cell.hbs +15 -2
  25. package/addon/components/table/cell.js +24 -0
  26. package/addon/components/table/foot.js +1 -1
  27. package/addon/components/table/td.hbs +2 -2
  28. package/addon/components/table/td.js +66 -1
  29. package/addon/components/table/th.hbs +15 -1
  30. package/addon/components/table/th.js +84 -0
  31. package/addon/components/table.hbs +2 -2
  32. package/addon/components/table.js +288 -0
  33. package/addon/styles/components/badge.css +8 -0
  34. package/addon/styles/components/input.css +8 -0
  35. package/addon/styles/components/table.css +126 -0
  36. package/addon/styles/layout/next.css +245 -28
  37. package/app/components/filter/multi-input.js +1 -0
  38. package/app/components/filter/range.js +1 -0
  39. package/app/components/modals/query-builder-computed-column-editor.js +1 -0
  40. package/app/components/query-builder/computed-columns.js +1 -0
  41. package/app/components/schedule-calendar.js +1 -0
  42. package/package.json +1 -1
@@ -0,0 +1,228 @@
1
+ # Scheduling Components for Ember UI
2
+
3
+ This package provides reusable scheduling components for the Fleetbase platform.
4
+
5
+ ## Components
6
+
7
+ ### ScheduleCalendar
8
+
9
+ A full-featured calendar component for displaying and managing schedules.
10
+
11
+ **Usage:**
12
+ ```handlebars
13
+ <ScheduleCalendar
14
+ @resources={{this.drivers}}
15
+ @items={{this.scheduleItems}}
16
+ @view="resourceTimeline"
17
+ @onItemClick={{this.handleItemClick}}
18
+ @onItemDrop={{this.handleItemDrop}}
19
+ @onDateClick={{this.handleDateClick}}
20
+ >
21
+ <:item as |item|>
22
+ <div class="custom-event-content">
23
+ {{item.title}}
24
+ </div>
25
+ </:item>
26
+ </ScheduleCalendar>
27
+ ```
28
+
29
+ **Arguments:**
30
+ - `@resources` - Array of resources (e.g., drivers, vehicles)
31
+ - `@items` - Array of schedule items to display
32
+ - `@view` - Calendar view type (default: 'resourceTimeline')
33
+ - `@editable` - Enable drag-and-drop editing (default: true)
34
+ - `@onItemClick` - Callback when an item is clicked
35
+ - `@onItemDrop` - Callback when an item is dragged and dropped
36
+ - `@onDateClick` - Callback when a date is clicked
37
+
38
+ **Named Blocks:**
39
+ - `:item` - Custom rendering for schedule items
40
+ - `:header` - Custom header content
41
+ - `:footer` - Custom footer content
42
+
43
+ ### ScheduleItemCard
44
+
45
+ Displays a schedule item in a card format.
46
+
47
+ **Usage:**
48
+ ```handlebars
49
+ <ScheduleItemCard @item={{this.scheduleItem}} @onClick={{this.handleClick}}>
50
+ <:content as |ctx|>
51
+ <div class="custom-content">
52
+ {{ctx.item.title}}
53
+ </div>
54
+ </:content>
55
+ <:actions as |ctx|>
56
+ <button {{on "click" (fn this.edit ctx.item)}}>Edit</button>
57
+ <button {{on "click" (fn this.delete ctx.item)}}>Delete</button>
58
+ </:actions>
59
+ </ScheduleItemCard>
60
+ ```
61
+
62
+ **Arguments:**
63
+ - `@item` - The schedule item to display
64
+ - `@onClick` - Callback when the card is clicked
65
+
66
+ **Named Blocks:**
67
+ - `:content` - Custom content rendering
68
+ - `:actions` - Custom action buttons
69
+
70
+ ### AvailabilityEditor
71
+
72
+ Allows users to set and manage availability windows.
73
+
74
+ **Usage:**
75
+ ```handlebars
76
+ <AvailabilityEditor
77
+ @subjectType="driver"
78
+ @subjectUuid={{@driver.id}}
79
+ @onSave={{this.handleAvailabilitySave}}
80
+ />
81
+ ```
82
+
83
+ **Arguments:**
84
+ - `@subjectType` - Type of the subject (e.g., 'driver', 'vehicle')
85
+ - `@subjectUuid` - UUID of the subject
86
+ - `@onSave` - Callback when availability is saved
87
+
88
+ ## Models
89
+
90
+ ### Schedule
91
+
92
+ Represents a master schedule.
93
+
94
+ **Attributes:**
95
+ - `name` - Schedule name
96
+ - `description` - Schedule description
97
+ - `start_date` - Start date
98
+ - `end_date` - End date
99
+ - `timezone` - Timezone
100
+ - `status` - Status (draft, published, active, paused, archived)
101
+ - `subject_uuid` - UUID of the subject
102
+ - `subject_type` - Type of the subject
103
+
104
+ **Relationships:**
105
+ - `items` - hasMany schedule-item
106
+ - `company` - belongsTo company
107
+
108
+ ### ScheduleItem
109
+
110
+ Represents an individual scheduled item.
111
+
112
+ **Attributes:**
113
+ - `start_at` - Start datetime
114
+ - `end_at` - End datetime
115
+ - `duration` - Duration in minutes
116
+ - `status` - Status (pending, confirmed, in_progress, completed, cancelled, no_show)
117
+ - `assignee_uuid` - UUID of the assignee
118
+ - `assignee_type` - Type of the assignee
119
+ - `resource_uuid` - UUID of the resource
120
+ - `resource_type` - Type of the resource
121
+
122
+ **Relationships:**
123
+ - `schedule` - belongsTo schedule
124
+
125
+ ### ScheduleTemplate
126
+
127
+ Represents a reusable schedule template.
128
+
129
+ **Attributes:**
130
+ - `name` - Template name
131
+ - `description` - Template description
132
+ - `start_time` - Start time
133
+ - `end_time` - End time
134
+ - `duration` - Duration in minutes
135
+ - `break_duration` - Break duration in minutes
136
+ - `rrule` - RFC 5545 recurrence rule
137
+
138
+ ### ScheduleAvailability
139
+
140
+ Represents availability windows for resources.
141
+
142
+ **Attributes:**
143
+ - `subject_uuid` - UUID of the subject
144
+ - `subject_type` - Type of the subject
145
+ - `start_at` - Start datetime
146
+ - `end_at` - End datetime
147
+ - `is_available` - Availability flag
148
+ - `preference_level` - Preference strength (1-5)
149
+ - `reason` - Reason for unavailability
150
+ - `notes` - Additional notes
151
+ - `rrule` - RFC 5545 recurrence rule
152
+
153
+ ### ScheduleConstraint
154
+
155
+ Represents scheduling constraints.
156
+
157
+ **Attributes:**
158
+ - `name` - Constraint name
159
+ - `description` - Constraint description
160
+ - `type` - Constraint type (hos, labor, business, capacity)
161
+ - `category` - Constraint category (compliance, optimization)
162
+ - `constraint_key` - Constraint key
163
+ - `constraint_value` - Constraint value
164
+ - `jurisdiction` - Jurisdiction (e.g., US-Federal, US-CA)
165
+ - `priority` - Priority (higher = more important)
166
+ - `is_active` - Active flag
167
+
168
+ ## Service
169
+
170
+ ### Scheduling Service
171
+
172
+ Provides methods for interacting with the scheduling API.
173
+
174
+ **Methods:**
175
+
176
+ - `loadSchedule(scheduleId)` - Load a schedule by ID
177
+ - `createSchedule(data)` - Create a new schedule
178
+ - `createScheduleItem(data)` - Create a new schedule item
179
+ - `updateScheduleItem(item, data)` - Update a schedule item
180
+ - `deleteScheduleItem(item)` - Delete a schedule item
181
+ - `getScheduleItemsForAssignee(assigneeType, assigneeUuid, filters)` - Get items for an assignee
182
+ - `checkAvailability(subjectType, subjectUuid, startAt, endAt)` - Check availability
183
+ - `setAvailability(data)` - Set availability
184
+ - `loadConstraints(subjectType, subjectUuid)` - Load constraints
185
+ - `validateScheduleItem(item)` - Validate an item against constraints
186
+
187
+ **Usage:**
188
+ ```javascript
189
+ import { inject as service } from '@ember/service';
190
+
191
+ export default class MyComponent extends Component {
192
+ @service scheduling;
193
+
194
+ async loadDriverSchedule(driverId) {
195
+ const items = await this.scheduling.getScheduleItemsForAssignee.perform(
196
+ 'driver',
197
+ driverId,
198
+ { start_at: '2025-11-15', end_at: '2025-11-22' }
199
+ );
200
+ return items;
201
+ }
202
+ }
203
+ ```
204
+
205
+ ## Styling
206
+
207
+ All components follow Fleetbase UI styling standards:
208
+ - Minimal padding and spacing
209
+ - Tailwind CSS framework
210
+ - Dark mode support
211
+ - Consistent with existing ember-ui components
212
+
213
+ ## Dependencies
214
+
215
+ The ScheduleCalendar component requires FullCalendar to be installed:
216
+
217
+ ```bash
218
+ pnpm add @fullcalendar/core @fullcalendar/resource-timeline @fullcalendar/interaction
219
+ ```
220
+
221
+ ## Future Enhancements
222
+
223
+ - TimeOffForm component for time-off requests
224
+ - ScheduleTemplateBuilder component for creating templates
225
+ - Conflict detection UI
226
+ - RRULE editor for recurring patterns
227
+ - Multi-timezone support improvements
228
+ - Real-time updates via WebSockets
@@ -0,0 +1,81 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { inject as service } from '@ember/service';
5
+
6
+ /**
7
+ * AvailabilityEditor Component
8
+ *
9
+ * Allows users to set and manage availability windows for resources.
10
+ *
11
+ * @example
12
+ * <AvailabilityEditor
13
+ * @subjectType="driver"
14
+ * @subjectUuid={{@driver.id}}
15
+ * @onSave={{this.handleAvailabilitySave}}
16
+ * />
17
+ */
18
+ export default class AvailabilityEditorComponent extends Component {
19
+ @service scheduling;
20
+ @service notifications;
21
+
22
+ @tracked startAt = null;
23
+ @tracked endAt = null;
24
+ @tracked isAvailable = true;
25
+ @tracked preferenceLevel = 3;
26
+ @tracked reason = '';
27
+ @tracked notes = '';
28
+ @tracked rrule = '';
29
+
30
+ /**
31
+ * Save availability
32
+ */
33
+ @action
34
+ async saveAvailability() {
35
+ try {
36
+ const data = {
37
+ subject_type: this.args.subjectType,
38
+ subject_uuid: this.args.subjectUuid,
39
+ start_at: this.startAt,
40
+ end_at: this.endAt,
41
+ is_available: this.isAvailable,
42
+ preference_level: this.preferenceLevel,
43
+ reason: this.reason,
44
+ notes: this.notes,
45
+ rrule: this.rrule,
46
+ };
47
+
48
+ const availability = await this.scheduling.setAvailability.perform(data);
49
+
50
+ if (this.args.onSave) {
51
+ this.args.onSave(availability);
52
+ }
53
+
54
+ this.resetForm();
55
+ } catch (error) {
56
+ console.error('Failed to save availability:', error);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Reset form
62
+ */
63
+ @action
64
+ resetForm() {
65
+ this.startAt = null;
66
+ this.endAt = null;
67
+ this.isAvailable = true;
68
+ this.preferenceLevel = 3;
69
+ this.reason = '';
70
+ this.notes = '';
71
+ this.rrule = '';
72
+ }
73
+
74
+ /**
75
+ * Update field
76
+ */
77
+ @action
78
+ updateField(field, value) {
79
+ this[field] = value;
80
+ }
81
+ }
@@ -0,0 +1,16 @@
1
+ <div class="filter-multi-input" ...attributes>
2
+ <TagInput
3
+ class="form-input form-input-sm flex-1"
4
+ @placeholder={{or @placeholder "Add values..."}}
5
+ @allowSpacesInTags={{or @allowSpacesInTags true}}
6
+ @tags={{this.tags}}
7
+ @addTag={{this.addTag}}
8
+ @removeTagAtIndex={{this.removeTag}}
9
+ as |tag|
10
+ >
11
+ {{tag}}
12
+ </TagInput>
13
+ <button type="button" class="clear-button" disabled={{not this.tags.length}} alt="Clear" {{on "click" this.clear}}>
14
+ <FaIcon @icon="times" />
15
+ </button>
16
+ </div>
@@ -0,0 +1,68 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { isArray } from '@ember/array';
5
+
6
+ export default class FilterMultiInputComponent extends Component {
7
+ @tracked tags = [];
8
+
9
+ constructor() {
10
+ super(...arguments);
11
+ this.tags = this.parseValue(this.args.value);
12
+ }
13
+
14
+ parseValue(value) {
15
+ if (isArray(value)) {
16
+ return value;
17
+ }
18
+
19
+ if (typeof value === 'string' && value.includes(',')) {
20
+ return value
21
+ .split(',')
22
+ .map((v) => v.trim())
23
+ .filter(Boolean);
24
+ }
25
+
26
+ if (value) {
27
+ return [value];
28
+ }
29
+
30
+ return [];
31
+ }
32
+
33
+ buildValue() {
34
+ return this.tags.join(',');
35
+ }
36
+
37
+ @action addTag(tag) {
38
+ const { onChange, filter } = this.args;
39
+
40
+ this.tags.pushObject(tag);
41
+ const value = this.buildValue();
42
+
43
+ if (typeof onChange === 'function') {
44
+ onChange(filter, value);
45
+ }
46
+ }
47
+
48
+ @action removeTag(index) {
49
+ const { onChange, filter } = this.args;
50
+
51
+ this.tags.removeAt(index);
52
+ const value = this.buildValue();
53
+
54
+ if (typeof onChange === 'function') {
55
+ onChange(filter, value);
56
+ }
57
+ }
58
+
59
+ @action clear() {
60
+ const { onClear, filter } = this.args;
61
+
62
+ this.tags = [];
63
+
64
+ if (typeof onClear === 'function') {
65
+ onClear(filter);
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,52 @@
1
+ <div class="filter-range" ...attributes>
2
+ <div class="filter-range-inputs">
3
+ <div class="filter-range-input-group">
4
+ <label class="filter-range-label">{{or @filter.minLabel "Min"}}</label>
5
+ <Input
6
+ @type="number"
7
+ @value={{this.minValue}}
8
+ min={{or @filter.min 0}}
9
+ max={{or @filter.max 100}}
10
+ step={{or @filter.step 1}}
11
+ {{on "input" this.onMinChange}}
12
+ class="form-input form-input-sm"
13
+ />
14
+ </div>
15
+ <div class="filter-range-separator">-</div>
16
+ <div class="filter-range-input-group">
17
+ <label class="filter-range-label">{{or @filter.maxLabel "Max"}}</label>
18
+ <Input
19
+ @type="number"
20
+ @value={{this.maxValue}}
21
+ min={{or @filter.min 0}}
22
+ max={{or @filter.max 100}}
23
+ step={{or @filter.step 1}}
24
+ {{on "input" this.onMaxChange}}
25
+ class="form-input form-input-sm"
26
+ />
27
+ </div>
28
+ </div>
29
+ <div class="filter-range-sliders">
30
+ <input
31
+ type="range"
32
+ value={{this.minValue}}
33
+ min={{or @filter.min 0}}
34
+ max={{or @filter.max 100}}
35
+ step={{or @filter.step 1}}
36
+ {{on "input" this.onMinChange}}
37
+ class="filter-range-slider filter-range-slider-min"
38
+ />
39
+ <input
40
+ type="range"
41
+ value={{this.maxValue}}
42
+ min={{or @filter.min 0}}
43
+ max={{or @filter.max 100}}
44
+ step={{or @filter.step 1}}
45
+ {{on "input" this.onMaxChange}}
46
+ class="filter-range-slider filter-range-slider-max"
47
+ />
48
+ </div>
49
+ <button type="button" class="clear-button" alt="Clear" {{on "click" this.clear}}>
50
+ <FaIcon @icon="times" />
51
+ </button>
52
+ </div>
@@ -0,0 +1,74 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+
5
+ export default class FilterRangeComponent extends Component {
6
+ @tracked minValue;
7
+ @tracked maxValue;
8
+
9
+ constructor() {
10
+ super(...arguments);
11
+ this.parseValue(this.args.value);
12
+ }
13
+
14
+ parseValue(value) {
15
+ const { filter } = this.args;
16
+ const { min = 0, max = 100 } = filter;
17
+
18
+ if (typeof value === 'string' && value.includes(',')) {
19
+ const [minVal, maxVal] = value.split(',').map((v) => parseFloat(v.trim()));
20
+ this.minValue = isNaN(minVal) ? min : minVal;
21
+ this.maxValue = isNaN(maxVal) ? max : maxVal;
22
+ } else {
23
+ this.minValue = min;
24
+ this.maxValue = max;
25
+ }
26
+ }
27
+
28
+ buildValue() {
29
+ return `${this.minValue},${this.maxValue}`;
30
+ }
31
+
32
+ @action onMinChange(event) {
33
+ const { onChange, filter } = this.args;
34
+ const value = parseFloat(event.target.value);
35
+
36
+ this.minValue = value;
37
+
38
+ // Ensure min doesn't exceed max
39
+ if (this.minValue > this.maxValue) {
40
+ this.maxValue = this.minValue;
41
+ }
42
+
43
+ if (typeof onChange === 'function') {
44
+ onChange(filter, this.buildValue());
45
+ }
46
+ }
47
+
48
+ @action onMaxChange(event) {
49
+ const { onChange, filter } = this.args;
50
+ const value = parseFloat(event.target.value);
51
+
52
+ this.maxValue = value;
53
+
54
+ // Ensure max doesn't go below min
55
+ if (this.maxValue < this.minValue) {
56
+ this.minValue = this.maxValue;
57
+ }
58
+
59
+ if (typeof onChange === 'function') {
60
+ onChange(filter, this.buildValue());
61
+ }
62
+ }
63
+
64
+ @action clear() {
65
+ const { onClear, filter } = this.args;
66
+
67
+ this.minValue = filter.min ?? 0;
68
+ this.maxValue = filter.max ?? 100;
69
+
70
+ if (typeof onClear === 'function') {
71
+ onClear(filter);
72
+ }
73
+ }
74
+ }
@@ -91,25 +91,32 @@ export default class FiltersPickerComponent extends Component {
91
91
  this.args.onClear(...args);
92
92
  }
93
93
 
94
- // Build a clean query-param bag
94
+ // Build a qp bag that explicitly clears the filter params
95
95
  const qp = { ...this.hostRouter.currentRoute.queryParams };
96
+
96
97
  (this.args.columns ?? [])
97
98
  .filter((c) => c.filterable)
98
99
  .forEach((c) => {
99
100
  const key = c.filterParam ?? c.valuePath;
100
- delete qp[key];
101
- delete qp[`${key}[]`];
101
+
102
+ // Explicitly clear them instead of deleting
103
+ if (key in qp) {
104
+ qp[key] = null; // will remove from URL if default is null
105
+ }
106
+
107
+ const arrayKey = `${key}[]`;
108
+ if (arrayKey in qp) {
109
+ qp[arrayKey] = null;
110
+ }
102
111
  });
103
112
 
104
- // Transition – routeDidChange listener will rebuild afterwards
105
113
  try {
106
114
  await this.hostRouter.transitionTo(this.hostRouter.currentRouteName, {
107
115
  queryParams: qp,
108
116
  });
109
117
  } catch (error) {
110
- // Ignore only the "transition aborted" case
111
118
  if (error?.name !== 'TransitionAborted') {
112
- throw error; // real error → rethrow
119
+ throw error;
113
120
  }
114
121
  }
115
122
  }
@@ -133,6 +133,8 @@
133
133
  @onPageChange={{@onPageChange}}
134
134
  @tfootVerticalOffset={{@tfootVerticalOffset}}
135
135
  @tfootVerticalOffsetElements={{@tfootVerticalOffsetElements}}
136
+ @onSort={{this.handleSort}}
137
+ @checkboxSticky={{this.checkboxSticky}}
136
138
  />
137
139
  {{/if}}
138
140
  </Layout::Section::Body>
@@ -2,12 +2,20 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
+ import { isNone } from '@ember/utils';
5
6
 
6
7
  export default class LayoutResourceTabularComponent extends Component {
7
8
  @service filters;
8
9
  @tracked table;
9
10
  @tracked columns = [];
10
11
 
12
+ get checkboxSticky() {
13
+ if (!isNone(this.args.checkboxSticky)) return this.args.checkboxSticky;
14
+
15
+ const columns = this.args.columns ?? this.columns;
16
+ return columns.some((c) => !isNone(c?.sticky));
17
+ }
18
+
11
19
  constructor(owner, { columns = [] }) {
12
20
  super(...arguments);
13
21
  this.columns = columns;
@@ -19,4 +27,24 @@ export default class LayoutResourceTabularComponent extends Component {
19
27
  this.args.setupTable(table);
20
28
  }
21
29
  }
30
+
31
+ @action handleSort(sortString, sortColumns) {
32
+ // sortString is comma-delimited format: "created_at,-order_date,status"
33
+ // sortColumns is array format: [{ param: 'created_at', direction: 'asc' }, ...]
34
+
35
+ if (this.args.controller && this.args.controller.sort !== undefined) {
36
+ this.args.controller.sort = sortString;
37
+ }
38
+
39
+ if (typeof this.args.onSort === 'function') {
40
+ this.args.onSort({
41
+ sortString,
42
+ sortColumns,
43
+ // Legacy support for single column callbacks
44
+ sortBy: sortColumns.length > 0 ? sortColumns[0].param : null,
45
+ sortDirection: sortColumns.length > 0 ? sortColumns[0].direction : null,
46
+ sortValue: sortString,
47
+ });
48
+ }
49
+ }
22
50
  }