@fleetbase/fleetops-engine 0.6.50 → 0.6.51

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 (159) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/details.hbs +1 -2
  4. package/addon/components/customer/form.hbs +63 -51
  5. package/addon/components/customer/form.js +27 -1
  6. package/addon/components/customer/orders.hbs +48 -0
  7. package/addon/components/customer/orders.js +67 -5
  8. package/addon/components/driver/form.hbs +1 -1
  9. package/addon/components/home/getting-started-guidance.hbs +113 -0
  10. package/addon/components/home/getting-started-guidance.js +107 -0
  11. package/addon/components/issue/details/comments.hbs +3 -0
  12. package/addon/components/issue/details/comments.js +3 -0
  13. package/addon/components/issue/details/correspondence.hbs +9 -0
  14. package/addon/components/issue/details/correspondence.js +83 -0
  15. package/addon/components/issue/details/documents.hbs +69 -0
  16. package/addon/components/issue/details/documents.js +85 -0
  17. package/addon/components/issue/details.hbs +220 -59
  18. package/addon/components/issue/details.js +245 -0
  19. package/addon/components/issue/form.hbs +19 -1
  20. package/addon/components/issue/panel-header.hbs +43 -0
  21. package/addon/components/issue/panel-header.js +22 -0
  22. package/addon/components/issue/timeline.hbs +40 -0
  23. package/addon/components/issue/timeline.js +45 -0
  24. package/addon/components/leaflet-tracking-marker.js +3 -0
  25. package/addon/components/map/drawer/position-listing.js +3 -0
  26. package/addon/components/map/leaflet-live-map.hbs +2 -2
  27. package/addon/components/map/leaflet-live-map.js +17 -0
  28. package/addon/components/map/order-list-overlay.hbs +4 -4
  29. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  30. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  31. package/addon/components/modals/issue-assign.hbs +20 -0
  32. package/addon/components/modals/issue-assign.js +49 -0
  33. package/addon/components/modals/issue-close.hbs +30 -0
  34. package/addon/components/modals/issue-close.js +73 -0
  35. package/addon/components/modals/issue-status.hbs +20 -0
  36. package/addon/components/modals/issue-status.js +43 -0
  37. package/addon/components/modals/reset-customer-credentials.js +12 -6
  38. package/addon/components/modals/update-order-activity.hbs +51 -17
  39. package/addon/components/modals/update-order-activity.js +59 -5
  40. package/addon/components/order/details/activity.js +2 -2
  41. package/addon/components/order/details/proof.hbs +40 -38
  42. package/addon/components/order/details/proof.js +31 -4
  43. package/addon/components/order/details.hbs +5 -5
  44. package/addon/components/order/details.js +19 -1
  45. package/addon/components/order/header.hbs +1 -1
  46. package/addon/components/route-list.hbs +0 -12
  47. package/addon/components/service-rate/details.hbs +1 -1
  48. package/addon/components/service-rate/form.hbs +1 -1
  49. package/addon/components/service-rate/form.js +4 -0
  50. package/addon/components/vendor/personnel-manager.hbs +166 -0
  51. package/addon/components/vendor/personnel-manager.js +143 -0
  52. package/addon/components/widget/live-fleet.hbs +1 -0
  53. package/addon/components/widget/live-fleet.js +2 -7
  54. package/addon/components/widget/revenue-trend.hbs +1 -1
  55. package/addon/components/widget/revenue-trend.js +75 -2
  56. package/addon/controllers/management/contacts/customers/details.js +31 -13
  57. package/addon/controllers/management/contacts/customers.js +16 -5
  58. package/addon/controllers/management/contacts/index/details.js +31 -13
  59. package/addon/controllers/management/contacts/index.js +7 -0
  60. package/addon/controllers/management/issues/index/details.js +52 -6
  61. package/addon/controllers/management/vendors/index/details.js +4 -0
  62. package/addon/controllers/operations/orders/index/details.js +11 -3
  63. package/addon/controllers/operations/orders/index.js +1 -1
  64. package/addon/extension.js +19 -12
  65. package/addon/helpers/format-duration.js +1 -38
  66. package/addon/initializers/load-leaflet-assets.js +5 -6
  67. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  68. package/addon/routes/management/issues/index/details.js +1 -1
  69. package/addon/routes.js +1 -0
  70. package/addon/services/contact-actions.js +231 -1
  71. package/addon/services/getting-started.js +44 -0
  72. package/addon/services/issue-actions.js +71 -0
  73. package/addon/services/leaflet-map-manager.js +2 -0
  74. package/addon/services/map-adapter/leaflet.js +19 -0
  75. package/addon/services/service-areas.js +3 -0
  76. package/addon/styles/fleetops-engine.css +1036 -0
  77. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  78. package/addon/templates/management/contacts/index/details.hbs +1 -1
  79. package/addon/templates/management/contacts.hbs +2 -2
  80. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  81. package/addon/templates/management/issues/index/details.hbs +3 -2
  82. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  83. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  84. package/addon/templates/operations/orders/index.hbs +1 -1
  85. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  86. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  87. package/addon/templates/settings/payments/onboard.hbs +8 -8
  88. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  89. package/addon/utils/leaflet-plugin-loader.js +219 -0
  90. package/addon/utils/setup-customer-portal.js +4 -64
  91. package/app/components/home/getting-started-guidance.js +1 -0
  92. package/app/components/issue/details/comments.js +1 -0
  93. package/app/components/issue/details/correspondence.js +1 -0
  94. package/app/components/issue/details/documents.js +1 -0
  95. package/app/components/issue/panel-header.js +1 -0
  96. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  97. package/app/components/modals/issue-assign.js +1 -0
  98. package/app/components/modals/issue-close.js +1 -0
  99. package/app/components/modals/issue-status.js +1 -0
  100. package/app/components/vendor/personnel-manager.js +1 -0
  101. package/app/helpers/format-duration.js +1 -1
  102. package/app/services/getting-started.js +1 -0
  103. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  104. package/app/utils/leaflet-plugin-loader.js +1 -0
  105. package/composer.json +1 -1
  106. package/extension.json +1 -1
  107. package/package.json +5 -5
  108. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  109. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  110. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  111. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  112. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  113. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  114. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  115. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  116. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -25
  117. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  118. package/server/src/Http/Controllers/Internal/v1/ContactController.php +135 -0
  119. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  120. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  121. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  122. package/server/src/Http/Controllers/Internal/v1/OrderController.php +172 -97
  123. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  124. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  125. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  126. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  127. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  128. package/server/src/Http/Resources/v1/Issue.php +25 -0
  129. package/server/src/Http/Resources/v1/Order.php +20 -18
  130. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  131. package/server/src/Models/Contact.php +30 -51
  132. package/server/src/Models/Issue.php +47 -6
  133. package/server/src/Models/Order.php +33 -6
  134. package/server/src/Models/ServiceArea.php +1 -1
  135. package/server/src/Models/VendorPersonnel.php +10 -1
  136. package/server/src/Models/Zone.php +1 -1
  137. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  138. package/server/src/Observers/ZoneObserver.php +56 -0
  139. package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
  140. package/server/src/Support/Analytics/TopDrivers.php +9 -1
  141. package/server/src/Support/GettingStarted.php +144 -0
  142. package/server/src/Support/PlaceSearch.php +282 -0
  143. package/server/src/Support/ResolvesOrderServiceStops.php +22 -12
  144. package/server/src/routes.php +33 -1
  145. package/server/tests/AnalyticsRoutesTest.php +8 -0
  146. package/server/tests/CustomerContactIdentitySafetyTest.php +41 -0
  147. package/server/tests/DriverVehicleStatusDefaultTest.php +2 -2
  148. package/server/tests/GettingStartedStatusTest.php +21 -0
  149. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  150. package/server/tests/ServiceAreaZoneApiShapeTest.php +44 -0
  151. package/translations/ar-ae.yml +9 -0
  152. package/translations/bg-bg.yaml +9 -0
  153. package/translations/en-us.yaml +11 -0
  154. package/translations/es-pa.yaml +9 -0
  155. package/translations/fr-fr.yaml +9 -0
  156. package/translations/mn-mn.yaml +9 -0
  157. package/translations/pt-br.yaml +9 -0
  158. package/translations/ru-ru.yaml +9 -0
  159. package/translations/vi-vn.yaml +9 -0
@@ -1,68 +1,229 @@
1
- <div class="details-wrapper" ...attributes>
2
- <ContentPanel @title={{t "common.details"}} @open={{true}} @wrapperClass="bordered-top">
3
- <div class="grid grid-cols-2 gap-2 text-xs dark:text-gray-100">
4
- <div class="field-info-container">
5
- <div class="field-name">{{t "issue.fields.title"}}</div>
6
- <div class="field-value">{{or @resource.title (concat "Issue reported on " (format-date-fns @model.created_at "dd MMM yy, HH:mm"))}}</div>
1
+ <div class="issue-detail-surface" ...attributes>
2
+ <section class="issue-detail-summary-card">
3
+ <div class="issue-detail-section-header">
4
+ <div>
5
+ <div class="issue-detail-section-title">Issue Details</div>
6
+ <p>Classification, ownership, and reporting date.</p>
7
7
  </div>
8
- <div class="field-info-container">
9
- <div class="field-name">{{t "common.status"}}</div>
10
- <div class="field-value">
11
- <Badge @status={{@resource.status}} />
12
- </div>
13
- </div>
14
- <div class="field-info-container">
15
- <div class="field-name">{{t "issue.fields.reported-by"}}</div>
16
- <div class="field-value">{{n-a @resource.reporter_name}}</div>
17
- </div>
18
- <div class="field-info-container">
19
- <div class="field-name">{{t "issue.fields.assigned-to"}}</div>
20
- <div class="field-value">{{n-a @resource.assignee_name}}</div>
8
+ </div>
9
+ <dl class="issue-detail-summary-grid">
10
+ <div class="issue-detail-summary-item">
11
+ <dt>Type</dt>
12
+ <dd>{{n-a (smart-humanize @resource.type)}}</dd>
21
13
  </div>
22
- <div class="field-info-container">
23
- <div class="field-name">{{t "resource.driver"}}</div>
24
- <div class="field-value">{{n-a @resource.driver_name}}</div>
14
+ <div class="issue-detail-summary-item">
15
+ <dt>Category</dt>
16
+ <dd>{{n-a (smart-humanize @resource.category)}}</dd>
25
17
  </div>
26
- <div class="field-info-container">
27
- <div class="field-name">{{t "resource.vehicle"}}</div>
28
- <div class="field-value">{{n-a @resource.vehicle_name}}</div>
18
+ <div class="issue-detail-summary-item">
19
+ <dt>Reporter</dt>
20
+ <dd><User::Pill @user={{@resource.reporter}} @titleFallback={{this.reporterName}} /></dd>
29
21
  </div>
30
- <div class="field-info-container">
31
- <div class="field-name">{{t "issue.fields.type"}}</div>
32
- <div class="field-value">{{n-a (titleize @resource.type)}}</div>
22
+ <div class="issue-detail-summary-item">
23
+ <dt>Assignee</dt>
24
+ <dd><User::Pill @user={{@resource.assignee}} @titleFallback={{this.assigneeName}} /></dd>
33
25
  </div>
34
- <div class="field-info-container">
35
- <div class="field-name">{{t "issue.fields.category"}}</div>
36
- <div class="field-value">{{n-a @resource.category}}</div>
26
+ <div class="issue-detail-summary-item">
27
+ <dt>Date Reported</dt>
28
+ <dd>{{n-a @resource.createdAt}}</dd>
37
29
  </div>
38
- <div class="field-info-container">
39
- <div class="field-name">{{t "issue.fields.priority"}}</div>
40
- <div class="field-value">
41
- <Badge @status={{@resource.priority}} />
30
+ </dl>
31
+ </section>
32
+
33
+ <div class="issue-detail-grid">
34
+ <main class="issue-detail-main">
35
+ <article class="issue-detail-message issue-detail-message-original">
36
+ <div class="issue-detail-avatar">{{this.reporterInitial}}</div>
37
+ <div class="issue-detail-message-content">
38
+ <div class="issue-detail-message-meta">
39
+ <div>
40
+ <div class="issue-detail-message-author">{{this.reporterName}}</div>
41
+ <div class="issue-detail-message-subtitle">Original report</div>
42
+ </div>
43
+ <time>{{n-a @resource.createdAt}}</time>
44
+ </div>
45
+ <div class="issue-detail-report">{{n-a @resource.report "No report was provided."}}</div>
46
+
47
+ <Issue::Details::Documents @resource={{@resource}} />
42
48
  </div>
43
- </div>
44
- <div class="field-info-container">
45
- <div class="field-name">{{t "common.date-created"}}</div>
46
- <div class="field-value">{{@resource.createdAt}}</div>
47
- </div>
48
- <div class="field-info-container col-span-2">
49
- <div class="field-name">{{t "issue.fields.tags"}}</div>
50
- <div class="field-value flex flex-row items-center space-x-1">
51
- {{#each @resource.tags as |tag|}}
52
- <div class="rounded-xl border text-sm border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-0.5 flex items-center justify-center">{{tag}}</div>
53
- {{/each}}
49
+ </article>
50
+
51
+ <section class="issue-detail-correspondence">
52
+ <div class="issue-detail-section-header">
53
+ <div>
54
+ <div class="issue-detail-section-title">Correspondence</div>
55
+ <p>Keep operator updates, customer replies, and internal follow-up attached to this issue.</p>
56
+ </div>
54
57
  </div>
55
- </div>
56
- <div class="field-info-container col-span-2">
57
- <div class="field-name">{{t "common.coordinates"}}</div>
58
- <div class="field-value">{{point-coordinates @resource.location}}</div>
59
- </div>
60
- <div class="field-info-container col-span-2">
61
- <div class="field-name">{{t "issue.fields.report"}}</div>
62
- <div class="field-value">{{n-a @resource.report}}</div>
63
- </div>
64
- </div>
65
- </ContentPanel>
58
+ <Issue::Details::Correspondence @resource={{@resource}} />
59
+ </section>
60
+
61
+ <section class="issue-detail-side-card">
62
+ <div class="issue-detail-section-title">Timeline</div>
63
+ <Issue::Timeline @resource={{@resource}} />
64
+ </section>
65
+
66
+ <RegistryYield @registry="fleet-ops:component:issue:details" as |RegistryComponent|>
67
+ <RegistryComponent @resource={{@resource}} />
68
+ </RegistryYield>
69
+ </main>
70
+
71
+ <aside class="issue-detail-side-rail">
72
+ <section class="issue-detail-side-card issue-detail-resolution-card">
73
+ <div class="issue-detail-section-title">Resolution Details</div>
74
+ <div class="issue-detail-resolution-state {{if this.isResolved 'is-resolved'}} {{if this.isReopened 'is-reopened'}}">
75
+ <div class="issue-detail-resolution-icon">
76
+ <FaIcon @icon={{this.resolutionIcon}} />
77
+ </div>
78
+ <div>
79
+ <div class="issue-detail-resolution-title">{{this.resolutionTitle}}</div>
80
+ <p>{{this.resolutionDescription}}</p>
81
+ </div>
82
+ </div>
83
+ <dl class="issue-detail-meta-list">
84
+ {{#if this.resolutionDate}}
85
+ <div>
86
+ <dt>{{if this.isReopened "Previous close date" "Closed date"}}</dt>
87
+ <dd>{{this.resolutionDate}}</dd>
88
+ </div>
89
+ {{/if}}
90
+ {{#if this.resolutionActor}}
91
+ <div>
92
+ <dt>{{if this.isReopened "Previously closed by" "Closed by"}}</dt>
93
+ <dd>{{this.resolutionActor}}</dd>
94
+ </div>
95
+ {{/if}}
96
+ {{#if this.resolutionNote}}
97
+ <div>
98
+ <dt>Note</dt>
99
+ <dd>{{this.resolutionNote}}</dd>
100
+ </div>
101
+ {{/if}}
102
+ {{#if this.timeToResolve}}
103
+ <div>
104
+ <dt>Time to resolve</dt>
105
+ <dd>{{this.timeToResolve}}</dd>
106
+ </div>
107
+ {{/if}}
108
+ <div>
109
+ <dt>Status</dt>
110
+ <dd><Badge @status={{@resource.status}} /></dd>
111
+ </div>
112
+ <div>
113
+ <dt>Priority</dt>
114
+ <dd><Badge @status={{@resource.priority}} /></dd>
115
+ </div>
116
+ <div>
117
+ <dt>Type</dt>
118
+ <dd>{{n-a (smart-humanize @resource.type)}}</dd>
119
+ </div>
120
+ <div>
121
+ <dt>Category</dt>
122
+ <dd>{{n-a (smart-humanize @resource.category)}}</dd>
123
+ </div>
124
+ </dl>
125
+ </section>
126
+
127
+ <section class="issue-detail-side-card">
128
+ <div class="issue-detail-section-title">Linked Resources</div>
129
+ <div class="issue-detail-resource-stack">
130
+ <div class="issue-detail-resource-row">
131
+ <div class="issue-detail-resource-label">Vehicle</div>
132
+ <Vehicle::Pill @vehicle={{@resource.vehicle}} @titleFallback={{n-a @resource.vehicle_name "No vehicle linked"}} @onClick={{this.viewVehicle}} />
133
+ </div>
134
+ <div class="issue-detail-resource-row">
135
+ <div class="issue-detail-resource-label">Driver</div>
136
+ <Driver::Pill @driver={{@resource.driver}} @titleFallback={{n-a @resource.driver_name "No driver linked"}} @onClick={{this.viewDriver}} />
137
+ </div>
138
+ </div>
139
+ </section>
140
+
141
+ <section class="issue-detail-side-card">
142
+ <div class="issue-detail-section-title">Linked Order</div>
143
+ {{#if this.order}}
144
+ <button type="button" class="issue-detail-order-card" {{on "click" this.viewOrder}}>
145
+ <div class="issue-detail-order-topline">
146
+ <div class="issue-detail-order-title-block">
147
+ <div class="issue-detail-order-title">{{this.orderLabel}}</div>
148
+ <div class="issue-detail-order-subtitle">{{n-a (smart-humanize this.order.type)}}</div>
149
+ </div>
150
+ <Badge @status={{this.order.status}} />
151
+ </div>
152
+ <div class="issue-detail-order-progress {{this.orderProgressClass}}">
153
+ <span></span>
154
+ </div>
155
+ {{#if this.hasOrderStops}}
156
+ <div class="issue-detail-order-route">
157
+ <div class="issue-detail-order-stop">
158
+ <div class="issue-detail-order-stop-marker pickup">1</div>
159
+ <div class="min-w-0">
160
+ <div class="issue-detail-order-stop-label">Pickup</div>
161
+ <div class="issue-detail-order-stop-address">{{n-a this.orderOriginAddress "No pickup address"}}</div>
162
+ </div>
163
+ </div>
164
+ {{#if this.isMultiStopOrder}}
165
+ <div class="issue-detail-order-stop compact">
166
+ <div class="issue-detail-order-stop-marker middle">+</div>
167
+ <div class="min-w-0">
168
+ <div class="issue-detail-order-stop-label">Waypoints</div>
169
+ <div class="issue-detail-order-stop-address">{{this.middleStopCount}} additional {{if (eq this.middleStopCount 1) "stop" "stops"}}</div>
170
+ </div>
171
+ </div>
172
+ {{/if}}
173
+ <div class="issue-detail-order-stop">
174
+ <div class="issue-detail-order-stop-marker dropoff">{{this.orderStopCount}}</div>
175
+ <div class="min-w-0">
176
+ <div class="issue-detail-order-stop-label">{{if (eq this.orderDestination.type "return") "Return" "Dropoff"}}</div>
177
+ <div class="issue-detail-order-stop-address">{{n-a this.orderDestinationAddress "No dropoff address"}}</div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ {{else}}
182
+ <div class="issue-detail-empty issue-detail-empty-compact">
183
+ <FaIcon @icon="route" />
184
+ <span>No route details available.</span>
185
+ </div>
186
+ {{/if}}
187
+ <div class="issue-detail-order-meta">
188
+ {{#if this.orderScheduledAt}}
189
+ <span><FaIcon @icon="calendar" /> {{format-date-fns this.orderScheduledAt "MMM d, HH:mm"}}</span>
190
+ {{/if}}
191
+ {{#if this.orderDriverLabel}}
192
+ <span><FaIcon @icon="id-card" /> {{this.orderDriverLabel}}</span>
193
+ {{/if}}
194
+ {{#if this.orderVehicleLabel}}
195
+ <span><FaIcon @icon="truck" /> {{this.orderVehicleLabel}}</span>
196
+ {{/if}}
197
+ </div>
198
+ </button>
199
+ {{else}}
200
+ <div class="issue-detail-empty">
201
+ <FaIcon @icon="route" />
202
+ <span>No order is linked to this issue.</span>
203
+ </div>
204
+ {{/if}}
205
+ </section>
206
+
207
+ {{#if this.tags.length}}
208
+ <section class="issue-detail-side-card">
209
+ <div class="issue-detail-section-title">Tags</div>
210
+ <div class="issue-detail-tags">
211
+ {{#each this.tags as |tag|}}
212
+ <Badge @status="info" @text={{tag}} @hideStatusDot={{true}} />
213
+ {{/each}}
214
+ </div>
215
+ </section>
216
+ {{/if}}
217
+
218
+ {{#if this.hasLocation}}
219
+ <section class="issue-detail-side-card">
220
+ <div class="issue-detail-section-title">Location</div>
221
+ <div class="issue-detail-coordinate">{{point-coordinates @resource.location}}</div>
222
+ </section>
223
+ {{/if}}
66
224
 
67
- <CustomField::Yield @subject={{@resource}} @viewMode={{true}} @wrapperClass="bordered-top" />
68
- </div>
225
+ <CustomField::Yield @subject={{@resource}} @viewMode={{true}} @wrapperClass="issue-detail-custom-fields" />
226
+ </aside>
227
+ </div>
228
+ <Spacer @height="200px" />
229
+ </div>
@@ -0,0 +1,245 @@
1
+ import Component from '@glimmer/component';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
4
+ import { formatDistanceStrict, isValid } from 'date-fns';
5
+
6
+ const COMPLETE_ORDER_STATUSES = ['completed', 'delivered'];
7
+ const ACTIVE_ORDER_STATUSES = ['dispatched', 'started', 'in_progress', 'driver_enroute', 'driver_nearby'];
8
+
9
+ export default class IssueDetailsComponent extends Component {
10
+ @service hostRouter;
11
+ @service issueActions;
12
+
13
+ get resource() {
14
+ return this.args.resource;
15
+ }
16
+
17
+ get tags() {
18
+ return Array.isArray(this.resource?.tags) ? this.resource.tags.filter(Boolean) : [];
19
+ }
20
+
21
+ get order() {
22
+ return this.resource?.order;
23
+ }
24
+
25
+ get files() {
26
+ return this.resource?.files ?? [];
27
+ }
28
+
29
+ get hasFiles() {
30
+ return this.files.length > 0;
31
+ }
32
+
33
+ get reporterName() {
34
+ return this.resource?.reporter?.name || this.resource?.reporter_name || 'Unknown reporter';
35
+ }
36
+
37
+ get reporterInitial() {
38
+ return this.reporterName?.charAt(0)?.toUpperCase() || '?';
39
+ }
40
+
41
+ get assigneeName() {
42
+ return this.resource?.assignee?.name || this.resource?.assignee_name || 'Unassigned';
43
+ }
44
+
45
+ get isResolved() {
46
+ return ['resolved', 'completed', 'closed'].includes(this.resource?.status);
47
+ }
48
+
49
+ get isReopened() {
50
+ return this.resource?.status === 're_opened';
51
+ }
52
+
53
+ get resolutionMeta() {
54
+ return this.resource?.meta?.resolution ?? {};
55
+ }
56
+
57
+ get reopenHistory() {
58
+ return Array.isArray(this.resource?.meta?.reopen_history) ? this.resource.meta.reopen_history : [];
59
+ }
60
+
61
+ get lastReopen() {
62
+ return this.reopenHistory[this.reopenHistory.length - 1];
63
+ }
64
+
65
+ get resolutionIcon() {
66
+ if (this.isResolved) {
67
+ return 'circle-check';
68
+ }
69
+
70
+ if (this.isReopened) {
71
+ return 'rotate-left';
72
+ }
73
+
74
+ return 'hourglass-half';
75
+ }
76
+
77
+ get resolutionTitle() {
78
+ if (this.isResolved) {
79
+ return 'Issue closed';
80
+ }
81
+
82
+ if (this.isReopened) {
83
+ return 'Issue re-opened';
84
+ }
85
+
86
+ return 'Awaiting resolution';
87
+ }
88
+
89
+ get resolutionDescription() {
90
+ if (this.isResolved) {
91
+ return this.resolutionMeta.note || 'This issue has been closed and no further action is currently required.';
92
+ }
93
+
94
+ if (this.isReopened) {
95
+ return this.lastReopen?.note || 'This issue was previously closed and has been re-opened for follow-up.';
96
+ }
97
+
98
+ return 'No resolution details have been recorded yet.';
99
+ }
100
+
101
+ get resolutionDate() {
102
+ return this.resolutionMeta.closed_at || this.resource?.resolvedAt || this.resource?.resolved_at;
103
+ }
104
+
105
+ get resolutionActor() {
106
+ return this.resolutionMeta.closed_by_name;
107
+ }
108
+
109
+ get resolutionNote() {
110
+ return this.resolutionMeta.note;
111
+ }
112
+
113
+ get timeToResolve() {
114
+ const createdAt = this.dateFromValue(this.resource?.created_at);
115
+ const resolvedAt = this.dateFromValue(this.resource?.resolved_at);
116
+
117
+ if (!createdAt || !resolvedAt) {
118
+ return null;
119
+ }
120
+
121
+ return formatDistanceStrict(createdAt, resolvedAt);
122
+ }
123
+
124
+ get orderLabel() {
125
+ return this.order?.tracking_number?.tracking_number || this.order?.tracking_number || this.order?.tracking || this.order?.public_id || this.order?.id;
126
+ }
127
+
128
+ get orderStops() {
129
+ const payload = this.order?.payload ?? {};
130
+ const stops = [];
131
+
132
+ if (payload.pickup) {
133
+ stops.push({ type: 'pickup', place: payload.pickup });
134
+ }
135
+
136
+ (payload.waypoints ?? []).forEach((waypoint, index) => {
137
+ stops.push({
138
+ type: waypoint.type ?? 'waypoint',
139
+ place: waypoint.place ?? waypoint,
140
+ index: index + 1,
141
+ });
142
+ });
143
+
144
+ if (payload.dropoff) {
145
+ stops.push({ type: 'dropoff', place: payload.dropoff });
146
+ }
147
+
148
+ if (payload.return) {
149
+ stops.push({ type: 'return', place: payload.return });
150
+ }
151
+
152
+ return stops.filter((stop) => this.addressForPlace(stop.place));
153
+ }
154
+
155
+ get orderOrigin() {
156
+ return this.orderStops[0];
157
+ }
158
+
159
+ get orderDestination() {
160
+ return this.orderStops[this.orderStops.length - 1];
161
+ }
162
+
163
+ get orderOriginAddress() {
164
+ return this.addressForPlace(this.orderOrigin?.place);
165
+ }
166
+
167
+ get orderDestinationAddress() {
168
+ return this.addressForPlace(this.orderDestination?.place);
169
+ }
170
+
171
+ get orderStopCount() {
172
+ return this.orderStops.length;
173
+ }
174
+
175
+ get middleStopCount() {
176
+ return Math.max(this.orderStopCount - 2, 0);
177
+ }
178
+
179
+ get isMultiStopOrder() {
180
+ return this.orderStopCount > 2;
181
+ }
182
+
183
+ get hasOrderStops() {
184
+ return this.orderStopCount > 0;
185
+ }
186
+
187
+ get orderProgressClass() {
188
+ const status = this.order?.status;
189
+
190
+ if (COMPLETE_ORDER_STATUSES.includes(status)) {
191
+ return 'is-complete';
192
+ }
193
+
194
+ if (ACTIVE_ORDER_STATUSES.includes(status)) {
195
+ return 'is-active';
196
+ }
197
+
198
+ return 'is-pending';
199
+ }
200
+
201
+ get orderScheduledAt() {
202
+ return this.order?.scheduled_at || this.order?.created_at || this.order?.createdAt;
203
+ }
204
+
205
+ get orderDriverLabel() {
206
+ return this.order?.driver_assigned?.name || this.order?.driver_name || this.order?.driver?.name;
207
+ }
208
+
209
+ get orderVehicleLabel() {
210
+ return this.order?.vehicle_assigned?.displayName || this.order?.vehicle_assigned?.display_name || this.order?.vehicle_name || this.order?.vehicle?.displayName;
211
+ }
212
+
213
+ get hasLocation() {
214
+ const location = this.resource?.location;
215
+ return Boolean(location?.latitude || location?.longitude || location?.coordinates);
216
+ }
217
+
218
+ addressForPlace(place) {
219
+ return place?.address ?? place?.address_html ?? place?.street1 ?? place?.name;
220
+ }
221
+
222
+ dateFromValue(value) {
223
+ if (!value) {
224
+ return null;
225
+ }
226
+
227
+ const date = value instanceof Date ? value : new Date(value);
228
+
229
+ return isValid(date) ? date : null;
230
+ }
231
+
232
+ @action viewOrder() {
233
+ if (this.order) {
234
+ return this.hostRouter.transitionTo('console.fleet-ops.operations.orders.index.details', this.order);
235
+ }
236
+ }
237
+
238
+ @action viewVehicle() {
239
+ return this.issueActions.viewVehicle(this.resource);
240
+ }
241
+
242
+ @action viewDriver() {
243
+ return this.issueActions.viewDriver(this.resource);
244
+ }
245
+ }
@@ -27,6 +27,7 @@
27
27
  @triggerClass="form-select form-input"
28
28
  @infiniteScroll={{false}}
29
29
  @renderInPlace={{true}}
30
+ @query={{hash is_not_customer=true}}
30
31
  @onChange={{fn (mut @resource.assignee)}}
31
32
  @onChangeId={{fn (mut @resource.assigned_to_uuid)}}
32
33
  @disabled={{cannot-write @resource}}
@@ -70,6 +71,23 @@
70
71
  </ModelSelect>
71
72
  </InputGroup>
72
73
 
74
+ <InputGroup @name={{t "resource.order"}}>
75
+ <ModelSelect
76
+ @modelName="order"
77
+ @selectedModel={{@resource.order}}
78
+ @placeholder={{t "common.select-field" field=(t "resource.order")}}
79
+ @triggerClass="form-select form-input"
80
+ @infiniteScroll={{false}}
81
+ @renderInPlace={{true}}
82
+ @onChange={{fn (mut @resource.order)}}
83
+ @onChangeId={{fn (mut @resource.order_uuid)}}
84
+ @disabled={{cannot-write @resource}}
85
+ as |model|
86
+ >
87
+ {{or model.tracking model.public_id}}
88
+ </ModelSelect>
89
+ </InputGroup>
90
+
73
91
  <InputGroup @name={{t "issue.fields.type"}}>
74
92
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
75
93
  <PowerSelect
@@ -180,4 +198,4 @@
180
198
  <RegistryYield @registry="fleet-ops:component:issue:form" as |RegistryComponent|>
181
199
  <RegistryComponent @resource={{@resource}} @controller={{this.controller}} @permission={{get-write-permission @resource}} />
182
200
  </RegistryYield>
183
- </div>
201
+ </div>
@@ -0,0 +1,43 @@
1
+ <div class="issue-panel-header">
2
+ <div class="issue-panel-header-main">
3
+ <div class="issue-panel-header-icon">
4
+ <FaIcon @icon="triangle-exclamation" />
5
+ </div>
6
+ <div class="issue-panel-header-content">
7
+ <div class="issue-panel-header-title-row">
8
+ <h1 class="issue-panel-header-title">{{this.title}}</h1>
9
+ </div>
10
+ {{#if @resource.public_id}}
11
+ <ClickToCopy @value={{@resource.public_id}} class="issue-panel-header-id">
12
+ <FaIcon @icon="hashtag" />
13
+ <span>{{@resource.public_id}}</span>
14
+ </ClickToCopy>
15
+ {{/if}}
16
+ <div class="issue-panel-header-meta">
17
+ {{#if this.subtitle}}
18
+ <span>{{titleize this.subtitle}}</span>
19
+ {{/if}}
20
+ {{#if @resource.createdAt}}
21
+ <span>Reported {{@resource.createdAt}}</span>
22
+ {{/if}}
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="issue-panel-header-actions">
28
+ <div class="issue-panel-header-badges">
29
+ <Badge @status={{@resource.status}} />
30
+ <Badge @status={{@resource.priority}} />
31
+ </div>
32
+ <Layout::Resource::Panel::HeaderActions
33
+ @resource={{@resource}}
34
+ @saveTask={{@saveTask}}
35
+ @saveOptions={{@saveOptions}}
36
+ @saveDisabled={{@saveDisabled}}
37
+ @pojoResource={{@pojoResource}}
38
+ @authSchema={{@authSchema}}
39
+ @actionButtons={{@actionButtons}}
40
+ @onPressCancel={{@onPressCancel}}
41
+ />
42
+ </div>
43
+ </div>
@@ -0,0 +1,22 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class IssuePanelHeaderComponent extends Component {
4
+ get resource() {
5
+ return this.args.resource;
6
+ }
7
+
8
+ get title() {
9
+ return this.resource?.title || `Issue reported on ${this.resource?.createdAt || 'unknown date'}`;
10
+ }
11
+
12
+ get tags() {
13
+ return Array.isArray(this.resource?.tags) ? this.resource.tags.filter(Boolean) : [];
14
+ }
15
+
16
+ get subtitle() {
17
+ return [this.resource?.type, this.resource?.category]
18
+ .filter(Boolean)
19
+ .map((value) => value.replace(/[-_]/g, ' '))
20
+ .join(' / ');
21
+ }
22
+ }