Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions addon/components/geofence-map.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<LeafletMap
class="h-80 w-full rounded-md border border-gray-200 shadow-md dark:border-gray-700"
@lat={{get this.center "0"}}
@lng={{get this.center "1"}}
@zoom={{12}}
@zoomControl={{true}}
@onLoad={{this.didLoadMap}}
...attributes
as |layers|
>
<layers.tile @url={{(leaflet-tile-url)}} />
{{#each this.polygons as |locations index|}}
<layers.polygon @id={{concat @resource.id "-" index}} @record={{@resource}} @locations={{locations}} @fillColor={{@resource.color}} @color={{@resource.stroke_color}} as |polygon|>
{{#if (eq index 0)}}
<polygon.tooltip @permanent={{true}} @sticky={{true}}>{{@label}}</polygon.tooltip>
{{/if}}
</layers.polygon>
{{/each}}
</LeafletMap>
83 changes: 83 additions & 0 deletions addon/components/geofence-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Component from '@glimmer/component';
import { action, get } from '@ember/object';
import { later, cancel } from '@ember/runloop';

/**
* A service area's or zone's boundary on a map, fitted to the whole shape.
*
* The details panels open inside a sliding overlay, so Leaflet first measures
* a container with no size: the tiles stay grey and the view is centred on
* the wrong point. The map is re-measured and re-fitted once the overlay has
* settled, and again whenever its container changes size.
*/
export default class GeofenceMapComponent extends Component {
map = null;
resizeObserver = null;
timer = null;

/** Each polygon's outer ring as Leaflet `[lat, lng]` pairs. */
get polygons() {
const border = this.args.resource ? get(this.args.resource, 'border') : null;
const type = border?.type;
const coordinates = border?.coordinates ?? [];
const rings = type === 'MultiPolygon' ? coordinates.map((polygon) => polygon?.[0] ?? []) : [coordinates[0] ?? []];

return rings.map((ring) => ring.filter((pair) => Array.isArray(pair) && pair.length >= 2).map(([lng, lat]) => [lat, lng])).filter((ring) => ring.length > 0);
}

/** `[[south, west], [north, east]]` around every polygon, or null. */
get bounds() {
const points = this.polygons.flat();
if (!points.length) {
return null;
}

const lats = points.map(([lat]) => lat);
const lngs = points.map(([, lng]) => lng);

return [
[Math.min(...lats), Math.min(...lngs)],
[Math.max(...lats), Math.max(...lngs)],
];
}

get center() {
const bounds = this.bounds;
if (!bounds) {
return [0, 0];
}

return [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2];
}

@action didLoadMap({ target: map }) {
this.map = map;
this.fit();
requestAnimationFrame(() => this.fit());
this.timer = later(this, this.fit, 350);

if (typeof ResizeObserver !== 'undefined' && typeof map.getContainer === 'function') {
this.resizeObserver = new ResizeObserver(() => this.fit());
this.resizeObserver.observe(map.getContainer());
}
}

fit() {
if (this.isDestroying || this.isDestroyed || !this.map) {
return;
}

this.map.invalidateSize();

if (this.bounds) {
this.map.fitBounds(this.bounds, { padding: [24, 24] });
}
}

willDestroy() {
super.willDestroy(...arguments);
cancel(this.timer);
this.resizeObserver?.disconnect();
this.map = null;
}
}
9 changes: 6 additions & 3 deletions addon/components/layout/fleet-ops-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,13 @@ export default class LayoutFleetOpsSidebarComponent extends Component {

get resourcesItems() {
return this.withRegistryItems('management', [
this.createHubItem('Resources Hub', 'layer-group', 'management.index', 'fleet-ops list driver', 'fleet-ops see driver', [
this.createHubItem(this.intl.t('menu.radar'), 'satellite-dish', 'management.index', 'fleet-ops list driver', 'fleet-ops see driver', [
'radar',
'needs attention',
'resources hub',
'resource dashboard',
'resource readiness',
'triage',
'handover',
'shift changes',
]),
this.createItem('menu.drivers', 'id-card', 'management.drivers', 'fleet-ops list driver', 'fleet-ops see driver', ['driver', 'online drivers']),
this.createItem('menu.vehicles', 'truck', 'management.vehicles', 'fleet-ops list vehicle', 'fleet-ops see vehicle', ['vehicle', 'track vehicles', 'online vehicles']),
Expand Down
4 changes: 4 additions & 0 deletions addon/components/maintenance-schedule/form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
@selected={{this.selectedSubjectType}}
@onChange={{this.onSubjectTypeChange}}
@placeholder="Select Asset Type"
@allowClear={{true}}
@triggerClass="form-select form-input"
@disabled={{cannot-write @resource}}
as |option|
Expand All @@ -73,6 +74,7 @@
@selectedItemComponent={{if (resource-component "select-option" this.subjectModelName) (component (resource-component "select-option" this.subjectModelName) compact=true)}}
@selectedModel={{@resource.subject}}
@placeholder="Select Asset"
@allowClear={{true}}
@triggerClass="form-select form-input"
@infiniteScroll={{false}}
@renderInPlace={{true}}
Expand Down Expand Up @@ -214,6 +216,7 @@
@selected={{this.selectedAssigneeType}}
@onChange={{this.onAssigneeTypeChange}}
@placeholder="Select Assignee Type"
@allowClear={{true}}
@triggerClass="form-select form-input"
@disabled={{cannot-write @resource}}
as |option|
Expand All @@ -230,6 +233,7 @@
@selectedItemComponent={{if (resource-component "select-option" this.assigneeModelName) (component (resource-component "select-option" this.assigneeModelName) compact=true)}}
@selectedModel={{@resource.default_assignee}}
@placeholder="Select Default Assignee"
@allowClear={{true}}
@triggerClass="form-select form-input"
@infiniteScroll={{false}}
@renderInPlace={{true}}
Expand Down
12 changes: 6 additions & 6 deletions addon/components/maintenance-schedule/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,25 +141,25 @@ export default class MaintenanceScheduleFormComponent extends Component {
}

@action onSubjectTypeChange(option) {
this.selectedSubjectType = option;
this.selectedSubjectType = option ?? null;
// Clear the subject relationship — user must re-select the asset
this.args.resource.subject = null;
this.subjectModelName = TYPE_TO_MODEL[option.value] ?? null;
this.subjectModelName = TYPE_TO_MODEL[option?.value] ?? null;
}

@action assignSubject(model) {
this.args.resource.subject = model;
this.args.resource.subject = model ?? null;
}

@action onAssigneeTypeChange(option) {
this.selectedAssigneeType = option;
this.selectedAssigneeType = option ?? null;
// Clear the default_assignee relationship — user must re-select
this.args.resource.default_assignee = null;
this.assigneeModelName = TYPE_TO_MODEL[option.value] ?? null;
this.assigneeModelName = TYPE_TO_MODEL[option?.value] ?? null;
}

@action assignDefaultAssignee(model) {
this.args.resource.default_assignee = model;
this.args.resource.default_assignee = model ?? null;
}

@action addReminderOffset(value) {
Expand Down
39 changes: 39 additions & 0 deletions addon/components/maintenance-schedule/work-orders.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<div class="schedule-work-orders-tab p-4" ...attributes>
{{#if this.load.isRunning}}
<div class="flex items-center justify-center py-12"><Spinner /></div>
{{else if this.workOrders.length}}
<div class="space-y-2">
{{#each this.workOrders as |workOrder|}}
<button
type="button"
class="flex w-full items-start justify-between rounded-lg border border-gray-200 bg-white px-2 py-2 text-left text-sm dark:border-gray-700 dark:bg-gray-800"
{{on "click" (fn this.view workOrder)}}
>
<div class="flex items-center gap-3">
<FaIcon @icon="wrench" class="text-gray-400" />
<div>
<div class="font-medium text-gray-900 dark:text-white">{{or workOrder.subject workOrder.code workOrder.public_id}}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{workOrder.public_id}}
{{#if workOrder.opened_at}}
&bull; Opened
{{format-date workOrder.opened_at "dd MMM yyyy"}}
{{/if}}
</div>
</div>
</div>
<div class="flex items-center gap-2">
<Badge @status={{workOrder.status}} @label={{smart-humanize workOrder.status}} />
<Badge @status={{workOrder.priority}} @label={{smart-humanize workOrder.priority}} />
</div>
</button>
{{/each}}
</div>
{{else}}
<div class="flex flex-col items-center justify-center py-12 text-center text-gray-500 dark:text-gray-400">
<FaIcon @icon="clipboard-list" class="mb-3 text-3xl text-gray-300 dark:text-gray-600" />
<p class="text-sm font-medium">No work orders yet</p>
<p class="mt-1 text-xs">Work orders will appear here when this schedule triggers.</p>
</div>
{{/if}}
</div>
37 changes: 37 additions & 0 deletions addon/components/maintenance-schedule/work-orders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';

/**
* The work orders a maintenance schedule has raised, for the schedule's
* context panel. The details route loads the same list in its own route.
*/
export default class MaintenanceScheduleWorkOrdersComponent extends Component {
@service store;
@service workOrderActions;
@tracked workOrders = [];

constructor() {
super(...arguments);
this.load.perform();
}

get schedule() {
return this.args.resource ?? this.args.model;
}

@task *load() {
if (!this.schedule?.id) {
return;
}

const workOrders = yield this.store.query('work-order', { schedule_uuid: this.schedule.id });
this.workOrders = workOrders?.toArray?.() ?? [...(workOrders ?? [])];
}

@action view(workOrder) {
return this.workOrderActions.panel.view(workOrder);
}
}
4 changes: 4 additions & 0 deletions addon/components/maintenance/form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
@selected={{this.selectedMaintainableType}}
@onChange={{this.onMaintainableTypeChange}}
@placeholder="Select asset type (Vehicle, Equipment…)"
@allowClear={{true}}
@triggerClass="form-select form-input"
@disabled={{cannot-write @resource}}
as |option|
Expand All @@ -89,6 +90,7 @@
@selectedItemComponent={{if (resource-component "select-option" this.maintainableModelName) (component (resource-component "select-option" this.maintainableModelName) compact=true)}}
@selectedModel={{@resource.maintainable}}
@placeholder="Select asset"
@allowClear={{true}}
@triggerClass="form-select form-input"
@infiniteScroll={{false}}
@renderInPlace={{true}}
Expand Down Expand Up @@ -117,6 +119,7 @@
@selected={{this.selectedPerformedByType}}
@onChange={{this.onPerformedByTypeChange}}
@placeholder="Select performer type (Vendor, Driver, User…)"
@allowClear={{true}}
@triggerClass="form-select form-input"
@disabled={{cannot-write @resource}}
as |option|
Expand All @@ -132,6 +135,7 @@
@selectedItemComponent={{if (resource-component "select-option" this.performedByModelName) (component (resource-component "select-option" this.performedByModelName) compact=true)}}
@selectedModel={{@resource.performed_by}}
@placeholder="Select performer"
@allowClear={{true}}
@triggerClass="form-select form-input"
@infiniteScroll={{false}}
@renderInPlace={{true}}
Expand Down
8 changes: 4 additions & 4 deletions addon/components/maintenance/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,30 +122,30 @@ export default class MaintenanceFormComponent extends Component {
* maintainable relationship so a stale association is not persisted.
*/
@action onMaintainableTypeChange(option) {
this.selectedMaintainableType = option;
this.selectedMaintainableType = option ?? null;
// Clear the maintainable relationship — user must re-select the asset
this.args.resource.maintainable = null;
this.maintainableModelName = TYPE_TO_MODEL[option?.value] ?? null;
}

/** Assigns the selected maintainable model to the resource. */
@action assignMaintainable(model) {
this.args.resource.maintainable = model;
this.args.resource.maintainable = model ?? null;
}

/**
* Handles a change to the performed-by type selector. Resets the
* performed-by relationship so a stale association is not persisted.
*/
@action onPerformedByTypeChange(option) {
this.selectedPerformedByType = option;
this.selectedPerformedByType = option ?? null;
// Clear the performed_by relationship — user must re-select
this.args.resource.performed_by = null;
this.performedByModelName = TYPE_TO_MODEL[option?.value] ?? null;
}

/** Assigns the selected performer model to the resource. */
@action assignPerformedBy(model) {
this.args.resource.performed_by = model;
this.args.resource.performed_by = model ?? null;
}
}
26 changes: 14 additions & 12 deletions addon/components/maintenance/panel-header.hbs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
<div class="px-4 py-2">
<div class="flex flex-1 flex-row items-start justify-between">
<div class="flex flex-col">
<div class="flex flex-row items-center space-x-2">
<h1 class="text-gray-900 dark:text-white font-semibold text-lg">{{n-a @resource.summary}}</h1>
<Badge @status={{@resource.status}}>{{smart-humanize @resource.status}}</Badge>
<div class="px-4 py-2" ...attributes>
<div class="flex flex-1 flex-row items-start justify-between gap-3">
<div class="flex min-w-0 flex-1 flex-col">
<div class="flex min-w-0 flex-row items-center gap-2">
<h1 class="truncate text-lg font-semibold text-gray-900 dark:text-white" title={{@resource.summary}}>{{n-a @resource.summary}}</h1>
<Badge @status={{@resource.status}} class="flex-shrink-0">{{smart-humanize @resource.status}}</Badge>
</div>
<div class="flex flex-row space-x-3 mt-1">
<div class="text-xs text-gray-400 dark:text-gray-500">{{smart-humanize @resource.type}}</div>
<div class="mt-1 flex min-w-0 flex-row flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-gray-400 dark:text-gray-500">
{{#if @resource.type}}
<span class="truncate">{{smart-humanize @resource.type}}</span>
{{/if}}
{{#if @resource.priority}}
<div class="text-xs text-gray-400 dark:text-gray-500">{{t "column.priority"}}: {{smart-humanize @resource.priority}}</div>
<span class="truncate">{{t "column.priority"}}: {{smart-humanize @resource.priority}}</span>
{{/if}}
{{#if @resource.scheduled_at}}
<div class="text-xs text-gray-400 dark:text-gray-500">{{t "column.scheduled-at"}}: {{format-date @resource.scheduled_at}}</div>
<span class="truncate">{{t "column.scheduled-at"}}: {{format-date @resource.scheduled_at}}</span>
{{/if}}
</div>
</div>
<div class="next-view-header-right">
<div class="next-view-header-right flex-shrink-0 whitespace-nowrap">
<Layout::Resource::Panel::HeaderActions
@resource={{@resource}}
@saveTask={{@saveTask}}
Expand All @@ -28,4 +30,4 @@
/>
</div>
</div>
</div>
</div>
5 changes: 2 additions & 3 deletions addon/components/modals/driver-assign-vehicle.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
<ModelSelect
@modelName="vehicle"
@selectedItemComponent={{component "select-option/vehicle" compact=true}}
@selectedModel={{@options.driver.vehicle_uuid}}
@selectedModel={{this.selectedVehicle}}
@placeholder={{t "modals.driver-assign-vehicle.assign-vehicle"}}
@triggerClass="form-select form-input"
@infiniteScroll={{false}}
@renderInPlace={{true}}
@onChange={{fn (mut @options.driver.vehicle)}}
@onChangeId={{fn (mut @options.driver.vehicle_uuid)}}
@onChange={{this.selectVehicle}}
as |model|
>
<SelectOption::Vehicle @option={{model}} />
Expand Down
Loading
Loading