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
4 changes: 1 addition & 3 deletions addon/components/driver/schedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,7 @@ export default class DriverScheduleComponent extends Component {
});

this.scheduleItems = items.toArray();
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));
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));

// 3. Load schedule exceptions
const exceptions = yield this.store.query('schedule-exception', {
Expand Down
12 changes: 8 additions & 4 deletions addon/components/maintenance-schedule/details.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,14 @@ export default class MaintenanceScheduleDetailsComponent extends Component {
@action downloadIcal(schedule) {
const id = schedule.public_id ?? schedule.id;
this.fetch
.download(`maintenance-schedules/${id}/ical`, {}, {
fileName: `maintenance-schedule-${id}.ics`,
mimeType: 'text/calendar',
})
.download(
`maintenance-schedules/${id}/ical`,
{},
{
fileName: `maintenance-schedule-${id}.ics`,
mimeType: 'text/calendar',
}
)
.catch((error) => {
// eslint-disable-next-line no-console
console.error('Failed to download iCal:', error);
Expand Down
2 changes: 1 addition & 1 deletion addon/components/maintenance/cost-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default class MaintenanceCostPanelComponent extends Component {
* Displayed via `format-currency` which also expects cents.
*/
get draftLineTotal() {
return (parseInt(this.draftQuantity, 10) || 0) * (this._toCents(this.draftUnitCost));
return (parseInt(this.draftQuantity, 10) || 0) * this._toCents(this.draftUnitCost);
}

get isDisabled() {
Expand Down
72 changes: 72 additions & 0 deletions addon/components/modals/bulk-assign-orders.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container">
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">

{{! ── Order summary ─────────────────────────────────────────────── }}
<div class="rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 px-3 py-2">
<p class="font-medium dark:text-gray-100">
<FaIcon @icon="list-check" class="mr-1.5 text-indigo-500" />
{{t "scheduler.bulk-assign-order-count" count=@options.orders.length}}
</p>
<div class="mt-2 max-h-32 overflow-y-auto space-y-1">
{{#each @options.orders as |order|}}
<div class="flex items-center justify-between py-0.5">
<span class="font-mono text-gray-600 dark:text-gray-400">{{order.public_id}}</span>
{{#if order.payload.dropoff.address}}
<span class="text-gray-500 dark:text-gray-400 truncate ml-2 max-w-xs">
{{order.payload.dropoff.address}}
</span>
{{/if}}
</div>
{{/each}}
</div>
</div>

{{! ── Driver selector ────────────────────────────────────────────── }}
<div class="input-group">
<label>{{t "resource.driver"}}</label>
<ModelSelect
@modelName="driver"
@selectedModel={{@options.driver}}
@placeholder={{t "scheduler.select-driver"}}
@triggerClass="form-select form-input"
@infiniteScroll={{false}}
@renderInPlace={{true}}
@allowClear={{true}}
@onChange={{fn (mut @options.driver)}}
as |driver|
>
<div class="flex items-center">
<div class="w-7 flex-shrink-0">
<FaIcon @icon="id-card-alt" />
</div>
<div class="flex-1 flex flex-row truncate">
<span class="uppercase mr-2">{{driver.name}}</span>
<span class="uppercase text-gray-400">{{driver.phone}}</span>
</div>
</div>
</ModelSelect>
</div>

{{! ── Start date/time ─────────────────────────────────────────────── }}
<div class="input-group">
<label>{{t "scheduler.bulk-assign-start-time"}}</label>
<DateTimeInput
@value={{@options.date}}
@onChange={{fn (mut @options.date)}}
placeholder={{t "scheduler.select-start-datetime"}}
class="w-full form-input"
/>
</div>

{{! ── Spacing hint ────────────────────────────────────────────────── }}
<div class="rounded-lg bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-700 px-3 py-2">
<p class="text-indigo-700 dark:text-indigo-300">
<FaIcon @icon="circle-info" class="mr-1.5" />
{{t "scheduler.bulk-assign-spacing-hint"}}
</p>
</div>

</div>
</div>
</Modal::Default>
72 changes: 72 additions & 0 deletions addon/components/modals/scheduling-conflict.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<Modal::Default
@modalIsOpened={{@modalIsOpened}}
@options={{@options}}
@confirm={{@onConfirm}}
@decline={{@onDecline}}
>
<div class="modal-body-container">
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">

{{! ── Warning banner ─────────────────────────────────────────────── }}
<div class="flex items-start gap-3 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-300 dark:border-yellow-700 px-3 py-3">
<FaIcon @icon="triangle-exclamation" class="text-yellow-500 mt-0.5 flex-shrink-0" />
<p class="text-yellow-800 dark:text-yellow-200 leading-relaxed">
{{t "scheduler.conflict-description" driverName=@options.driver.name orderTracking=@options.order.tracking}}
</p>
</div>

{{! ── Conflicting events list ─────────────────────────────────────── }}
<div class="rounded-md border border-yellow-200 dark:border-yellow-700 overflow-hidden">
<div class="bg-yellow-50 dark:bg-yellow-900/20 px-3 py-2 font-semibold text-yellow-800 dark:text-yellow-300 uppercase tracking-wide">
{{t "scheduler.conflicting-orders"}}
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
{{#each @options.conflicts as |conflict|}}
<div class="flex items-center justify-between px-3 py-2">
<div class="flex items-center gap-2">
<FaIcon @icon="box" class="text-gray-400" />
<span class="font-medium">{{conflict.tracking}}</span>
</div>
<span class="text-gray-500 dark:text-gray-400">
{{conflict.scheduledAtTime}}
</span>
</div>
{{/each}}
</div>
</div>

{{! ── Resolution prompt ───────────────────────────────────────────── }}
<p class="text-gray-600 dark:text-gray-400">
{{t "scheduler.conflict-resolution-prompt"}}
</p>

{{! ── Resolution buttons ──────────────────────────────────────────── }}
<div class="flex items-center gap-2 justify-end">
<Button
@type="default"
@text={{t "common.cancel"}}
@icon="times"
@size="sm"
@onClick={{@onDecline}}
/>
<Button
@type="default"
@text={{t "scheduler.auto-adjust"}}
@icon="wand-magic-sparkles"
@size="sm"
@isLoading={{@options.isLoading}}
@onClick={{this.autoAdjust}}
/>
<Button
@type="danger"
@text={{t "scheduler.assign-anyway"}}
@icon="triangle-exclamation"
@size="sm"
@isLoading={{@options.isLoading}}
@onClick={{this.assignAnyway}}
/>
</div>

</div>
</div>
</Modal::Default>
53 changes: 53 additions & 0 deletions addon/components/modals/scheduling-conflict.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

/**
* Modals::SchedulingConflict
*
* Surfaces a scheduling conflict when an order is dragged onto a driver row
* that already has an overlapping event. Provides three resolution paths:
*
* 1. Cancel — close the modal, leave the order unscheduled
* 2. Auto-Adjust — call @options.autoAdjust to find the next available slot
* 3. Assign Anyway — call @options.assignAnyway to force the assignment
*
* Options accepted (set via modalsManager.show):
* - order {Order} The order being scheduled
* - driver {Driver} The target driver
* - conflicts {Array} Existing events that overlap the requested slot
* - scheduledAt {Date} The originally requested start time
* - autoAdjust {Function} (modalsManager, done) => Promise
* - assignAnyway {Function} (modalsManager, done) => Promise
*/
export default class ModalsSchedulingConflictComponent extends Component {
@service modalsManager;

/**
* Trigger the auto-adjust resolution path.
* Calls @options.autoAdjust(modalsManager, done) so the controller can
* start loading, find the best-fit slot, assign, and close the modal.
*/
@action
autoAdjust() {
const { autoAdjust } = this.args.options;
if (typeof autoAdjust === 'function') {
const done = () => this.modalsManager.done();
autoAdjust(this.modalsManager, done);
}
}

/**
* Trigger the assign-anyway resolution path.
* Calls @options.assignAnyway(modalsManager, done) so the controller can
* skip the conflict check and force the assignment.
*/
@action
assignAnyway() {
const { assignAnyway } = this.args.options;
if (typeof assignAnyway === 'function') {
const done = () => this.modalsManager.done();
assignAnyway(this.modalsManager, done);
}
}
}
20 changes: 15 additions & 5 deletions addon/components/order/schedule-card.hbs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
<div class="order-schedule-card border border-gray-200 dark:border-gray-700" ...attributes>
<a href="javascript:;" class="{{@titleClass}} card-title border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900" {{on "click" (fn this.onTitleClick @order)}}>
<span>{{@order.tracking}}</span>
<div>
<div class="order-schedule-card border border-gray-200 dark:border-gray-700 {{if @selected 'ring-2 ring-indigo-500'}}" ...attributes>
<div class="card-title border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 flex items-center gap-2 px-2 py-1.5">
{{#if @selectable}}
<input
type="checkbox"
class="form-checkbox h-3.5 w-3.5 rounded text-indigo-600 border-gray-300 dark:border-gray-600 cursor-pointer flex-shrink-0"
checked={{@selected}}
{{on "click" (fn @onSelect)}}
/>
{{/if}}
<a href="javascript:;" class="{{@titleClass}} flex-1 truncate text-sm" {{on "click" (fn this.onTitleClick @order)}}>
<span>{{@order.tracking}}</span>
</a>
<div class="flex-shrink-0">
<Badge @status={{@order.status}} />
</div>
</a>
</div>
<div class="{{@contentClass}} card-content bg-white dark:bg-gray-900 text-sm space-y-2">
<div class="flex items-center justify-between">
<div class="flex flex-row items-center leading-5">
Expand Down
2 changes: 1 addition & 1 deletion addon/controllers/maintenance/schedules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default class MaintenanceSchedulesIndexController extends Controller {
})
.then((response) => {
// The API returns { events: [...] } — unwrap the array.
const raw = Array.isArray(response) ? response : (response?.events ?? []);
const raw = Array.isArray(response) ? response : response?.events ?? [];
const events = raw.map((event) => ({
id: event.id,
title: event.title,
Expand Down
12 changes: 8 additions & 4 deletions addon/controllers/maintenance/schedules/index/details.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ export default class MaintenanceSchedulesIndexDetailsController extends Controll
@action downloadIcal(schedule) {
const id = schedule.public_id ?? schedule.id;
this.fetch
.download(`maintenance-schedules/${id}/ical`, {}, {
fileName: `maintenance-schedule-${id}.ics`,
mimeType: 'text/calendar',
})
.download(
`maintenance-schedules/${id}/ical`,
{},
{
fileName: `maintenance-schedule-${id}.ics`,
mimeType: 'text/calendar',
}
)
.catch((error) => {
// eslint-disable-next-line no-console
console.error('Failed to download iCal:', error);
Expand Down
11 changes: 6 additions & 5 deletions addon/controllers/management/vehicles/index/details.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,12 @@ export default class ManagementVehiclesIndexDetailsController extends Controller
{
text: this.intl.t('common.delete-resource', { resource: this.intl.t('resource.vehicle') }),
icon: 'trash',
fn: () => this.vehicleActions.delete(this.model, {
onConfirm: () => {
this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index');
},
}),
fn: () =>
this.vehicleActions.delete(this.model, {
onConfirm: () => {
this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index');
},
}),
permission: 'fleet-ops delete vehicle',
class: 'text-red-500 hover:text-red-600',
},
Expand Down
26 changes: 14 additions & 12 deletions addon/controllers/operations/scheduler/fleet-schedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ export default class OperationsSchedulerFleetScheduleController extends Controll
*/
getShiftColor(item) {
const colors = {
scheduled: '#6366f1', // indigo
confirmed: '#22c55e', // green
scheduled: '#6366f1', // indigo
confirmed: '#22c55e', // green
in_progress: '#3b82f6', // blue
completed: '#9ca3af', // gray
cancelled: '#ef4444', // red
no_show: '#f97316', // orange
completed: '#9ca3af', // gray
cancelled: '#ef4444', // red
no_show: '#f97316', // orange
};
return colors[item.status] || '#6366f1';
}
Expand Down Expand Up @@ -268,13 +268,15 @@ export default class OperationsSchedulerFleetScheduleController extends Controll
if (schedules.length > 0) {
schedule = schedules.firstObject;
} else {
schedule = await this.store.createRecord('schedule', {
subject_type: 'fleet-ops:driver',
subject_uuid: targetDriver.id,
name: `${targetDriver.name} Schedule`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
status: 'draft',
}).save();
schedule = await this.store
.createRecord('schedule', {
subject_type: 'fleet-ops:driver',
subject_uuid: targetDriver.id,
name: `${targetDriver.name} Schedule`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
status: 'draft',
})
.save();
}

// Apply the template — core-api materialises ScheduleItems
Expand Down
Loading