Skip to content

Commit 899f01a

Browse files
284 update vehicle info windows (#301)
1 parent 449142e commit 899f01a

13 files changed

+359
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<div class="text__title">
2+
{{ vehicle | entityName: 'Vehicle' }}
3+
</div>
4+
<div class="text__caption">
5+
<div class="strong underline">Max Load Limits</div>
6+
<div *ngIf="loadLimitsNames.length; else noLoadLimits">
7+
<div *ngFor="let limit of loadLimitsNames; let i = index">
8+
<span>{{ limit }} - </span>
9+
<span class="strong">{{ loadLimitsValues[i] }}</span>
10+
</div>
11+
</div>
12+
<ng-template #noLoadLimits>
13+
<div>None</div>
14+
</ng-template>
15+
</div>
16+
<div class="text__caption">
17+
<div class="strong underline">Start Windows</div>
18+
<div *ngIf="startTimeWindows.length; else noStartWindows">
19+
<div *ngFor="let window of startTimeWindows">
20+
<span>{{ window }}</span>
21+
</div>
22+
</div>
23+
<ng-template #noStartWindows>
24+
<div>None</div>
25+
</ng-template>
26+
</div>

application/frontend/src/app/core/components/base-pre-solve-vehicle-info-window/base-pre-solve-vehicle-info-window.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
import { ComponentFixture, TestBed } from '@angular/core/testing';
17+
18+
import { BasePreSolveVehicleInfoWindowComponent } from './base-pre-solve-vehicle-info-window.component';
19+
import { ITimeWindow, Vehicle } from '../../models';
20+
import Long from 'long';
21+
import { SharedModule } from 'src/app/shared/shared.module';
22+
import { LOCALE_ID } from '@angular/core';
23+
24+
describe('BasePreSolveVehicleInfoWindowComponent', () => {
25+
let component: BasePreSolveVehicleInfoWindowComponent;
26+
let fixture: ComponentFixture<BasePreSolveVehicleInfoWindowComponent>;
27+
28+
beforeEach(async () => {
29+
await TestBed.configureTestingModule({
30+
imports: [SharedModule],
31+
declarations: [BasePreSolveVehicleInfoWindowComponent],
32+
providers: [{ provide: LOCALE_ID, useValue: 'en-US' }],
33+
}).compileComponents();
34+
35+
fixture = TestBed.createComponent(BasePreSolveVehicleInfoWindowComponent);
36+
component = fixture.componentInstance;
37+
fixture.detectChanges();
38+
});
39+
40+
afterEach(() => {
41+
fixture.destroy();
42+
});
43+
44+
it('should create', () => {
45+
expect(component).toBeTruthy();
46+
});
47+
48+
describe('format time windows', () => {
49+
it('should format time window with start and end times', () => {
50+
const timeWindow: ITimeWindow = {
51+
startTime: { seconds: 1609459200 },
52+
endTime: { seconds: 1609502400 },
53+
};
54+
55+
const result = component.formatTimeWindow(timeWindow);
56+
57+
expect(result).toBe('2021/01/01 12:00 am - 2021/01/01 12:00 pm');
58+
});
59+
60+
it('should format time window according to their timezone offset', () => {
61+
const timeWindow: ITimeWindow = {
62+
startTime: { seconds: 1609459200 },
63+
endTime: { seconds: 1609502400 },
64+
};
65+
component.timezoneOffset = 0;
66+
67+
const result = component.formatTimeWindow(timeWindow);
68+
expect(result).toBe('2021/01/01 12:00 am - 2021/01/01 12:00 pm');
69+
70+
component.timezoneOffset = 3600;
71+
72+
const resultOffset = component.formatTimeWindow(timeWindow);
73+
expect(resultOffset).toBe('2021/01/01 1:00 am - 2021/01/01 1:00 pm');
74+
});
75+
76+
it('should use globalDuration start when startTime is missing', () => {
77+
component.globalDuration = [Long.fromNumber(1609459200), Long.fromNumber(1699999999)];
78+
const timeWindow: ITimeWindow = {
79+
endTime: { seconds: 1609502400 },
80+
};
81+
82+
const result = component.formatTimeWindow(timeWindow);
83+
84+
expect(result).toBe('2021/01/01 12:00 am - 2021/01/01 12:00 pm');
85+
});
86+
87+
it('should use globalDuration start when endTime is missing', () => {
88+
component.globalDuration = [Long.fromNumber(1609000000), Long.fromNumber(1609502400)];
89+
const timeWindow: ITimeWindow = {
90+
startTime: { seconds: 1609459200 },
91+
};
92+
93+
const result = component.formatTimeWindow(timeWindow);
94+
95+
expect(result).toBe('2021/01/01 12:00 am - 2021/01/01 12:00 pm');
96+
});
97+
98+
it('should return an empty array when no start time windows exist', () => {
99+
component.vehicle = { id: 1, startTimeWindows: [] } as Vehicle;
100+
101+
component.getFormattedTimeWindows();
102+
103+
expect(component.startTimeWindows).toEqual([]);
104+
});
105+
106+
it('should skip time windows without hard limits', () => {
107+
component.vehicle = {
108+
id: 1,
109+
startTimeWindows: [{ softStartTime: 1609459200, softEndTime: 1609480500 }],
110+
} as Vehicle;
111+
112+
component.getFormattedTimeWindows();
113+
114+
expect(component.startTimeWindows).toEqual([]);
115+
});
116+
117+
it('should format multiple time windows', () => {
118+
component.vehicle = {
119+
id: 1,
120+
startTimeWindows: [
121+
{
122+
startTime: { seconds: 1609459200 },
123+
endTime: { seconds: 1609470000 },
124+
},
125+
{
126+
startTime: { seconds: 1609488000 },
127+
endTime: { seconds: 1609502400 },
128+
},
129+
],
130+
} as Vehicle;
131+
132+
component.getFormattedTimeWindows();
133+
134+
expect(component.startTimeWindows.length).toBe(2);
135+
expect(component.startTimeWindows[0]).toBe('2021/01/01 12:00 am - 2021/01/01 3:00 am');
136+
expect(component.startTimeWindows[1]).toBe('2021/01/01 8:00 am - 2021/01/01 12:00 pm');
137+
});
138+
139+
it('should clear previous time windows before processing changes', () => {
140+
component.startTimeWindows = [
141+
'2021/01/01 12:00 am - 2021/01/01 3:00 am',
142+
'2021/01/01 8:00 am - 2021/01/01 12:00 pm',
143+
];
144+
component.vehicle = {
145+
id: 1,
146+
startTimeWindows: [],
147+
} as Vehicle;
148+
149+
component.getFormattedTimeWindows();
150+
151+
expect(component.startTimeWindows).toEqual([]);
152+
});
153+
});
154+
155+
describe('change detection', () => {
156+
it('should update startTimeWindows when vehicle changes', () => {
157+
component.vehicle = {
158+
id: 1,
159+
startTimeWindows: [
160+
{
161+
startTime: { seconds: 1609459200 },
162+
endTime: { seconds: 1609502400 },
163+
},
164+
],
165+
} as Vehicle;
166+
167+
component.ngOnChanges({});
168+
169+
expect(component.startTimeWindows.length).toBe(1);
170+
});
171+
});
172+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
import {
17+
ChangeDetectionStrategy,
18+
Component,
19+
Inject,
20+
Input,
21+
LOCALE_ID,
22+
OnChanges,
23+
SimpleChanges,
24+
} from '@angular/core';
25+
import { ITimeWindow, Vehicle } from '../../models';
26+
import { durationSeconds } from 'src/app/util/duration';
27+
import { formatDate } from '@angular/common';
28+
29+
@Component({
30+
selector: 'app-base-pre-solve-vehicle-info-window',
31+
templateUrl: './base-pre-solve-vehicle-info-window.component.html',
32+
styleUrl: './base-pre-solve-vehicle-info-window.component.scss',
33+
changeDetection: ChangeDetectionStrategy.OnPush,
34+
})
35+
export class BasePreSolveVehicleInfoWindowComponent implements OnChanges {
36+
@Input() vehicle: Vehicle;
37+
@Input() timezoneOffset = 0;
38+
@Input() globalDuration: [Long, Long];
39+
40+
loadLimitsNames: string[] = [];
41+
loadLimitsValues: string[] = [];
42+
startTimeWindows: string[] = [];
43+
44+
constructor(@Inject(LOCALE_ID) private locale: string) {}
45+
46+
ngOnChanges(_changes: SimpleChanges): void {
47+
this.getFormattedLoadLimits();
48+
this.getFormattedTimeWindows();
49+
}
50+
51+
getFormattedTimeWindows(): void {
52+
this.startTimeWindows = [];
53+
54+
if (!this.vehicle || !this.vehicle.startTimeWindows) {
55+
return;
56+
}
57+
58+
this.vehicle.startTimeWindows.forEach((timewindow) => {
59+
if (!timewindow.startTime && !timewindow.endTime) {
60+
return;
61+
}
62+
this.startTimeWindows.push(this.formatTimeWindow(timewindow));
63+
});
64+
}
65+
66+
getFormattedLoadLimits(): void {
67+
this.loadLimitsNames = [];
68+
this.loadLimitsValues = [];
69+
70+
if (!this.vehicle) {
71+
return;
72+
}
73+
74+
Object.keys(this.vehicle.loadLimits || {}).forEach((key) => {
75+
if (this.vehicle.loadLimits[key].maxLoad) {
76+
this.loadLimitsNames.push(key);
77+
this.loadLimitsValues.push(`${this.vehicle.loadLimits[key].maxLoad}`);
78+
}
79+
});
80+
}
81+
82+
formatTimeWindow(timewindow: ITimeWindow): string {
83+
const startTime = timewindow.startTime
84+
? durationSeconds(timewindow.startTime).toNumber()
85+
: this.globalDuration[0].toNumber();
86+
const endTime = timewindow.endTime
87+
? durationSeconds(timewindow.endTime).toNumber()
88+
: this.globalDuration[1].toNumber();
89+
90+
const formattedStart = formatDate(
91+
(startTime + this.timezoneOffset) * 1000,
92+
'yyyy/MM/dd h:mm aa',
93+
this.locale,
94+
'UTC'
95+
).toLocaleLowerCase(this.locale);
96+
const formattedEnd = formatDate(
97+
(endTime + this.timezoneOffset) * 1000,
98+
'yyyy/MM/dd h:mm aa',
99+
this.locale,
100+
'UTC'
101+
).toLocaleLowerCase(this.locale);
102+
103+
return `${formattedStart} - ${formattedEnd}`;
104+
}
105+
}

application/frontend/src/app/core/components/base-vehicle-info-window/base-vehicle-info-window.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
<div *ngIf="route" class="text__caption">
88
Route End: <span class="strong">{{ formatDate(route.vehicleEndTime) }}</span>
99
</div>
10+
<div *ngIf="route" class="text__caption">
11+
Route Duration:
12+
<span class="strong">{{ formatRouteDuration(route.metrics.totalDuration) }}</span>
13+
</div>
1014
<div *ngIf="route" class="text__caption">
1115
Route Distance:
1216
<span class="strong">{{ route.metrics.travelDistanceMeters / 1000 | number: '1.1-1' }} km</span>

application/frontend/src/app/core/components/base-vehicle-info-window/base-vehicle-info-window.component.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
LOCALE_ID,
2424
Output,
2525
} from '@angular/core';
26-
import { ITimestamp, ShipmentRoute, Vehicle } from '../../models';
26+
import { IDuration, ITimestamp, ShipmentRoute, Vehicle } from '../../models';
2727
import { formatDate } from '@angular/common';
28-
import { durationSeconds } from 'src/app/util';
28+
import { durationSeconds, formattedDurationSeconds } from 'src/app/util';
2929

3030
@Component({
3131
selector: 'app-base-vehicle-info-window',
@@ -42,6 +42,7 @@ export class BaseVehicleInfoWindowComponent {
4242
@Output() vehicleClick = new EventEmitter<Vehicle>();
4343

4444
ObjectKeys = Object.keys;
45+
formattedDurationSeconds = formattedDurationSeconds;
4546

4647
constructor(@Inject(LOCALE_ID) private locale: string) {}
4748

@@ -57,4 +58,8 @@ export class BaseVehicleInfoWindowComponent {
5758
this.locale
5859
);
5960
}
61+
62+
formatRouteDuration(duration: IDuration): string {
63+
return formattedDurationSeconds(durationSeconds(duration));
64+
}
6065
}

application/frontend/src/app/core/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * from './base-main-nav/base-main-nav.component';
2323
export * from './base-post-solve-message/base-post-solve-message.component';
2424
export * from './base-post-solve-metrics/base-post-solve-metrics.component';
2525
export * from './base-pre-solve-message/base-pre-solve-message.component';
26+
export * from './base-pre-solve-vehicle-info-window/base-pre-solve-vehicle-info-window.component';
2627
export * from './base-regenerate-confirmation-dialog/base-regenerate-confirmation-dialog.component';
2728
export * from './base-shipments-kpis/base-shipments-kpis.component';
2829
export * from './base-storage-api-save-load-dialog/base-storage-api-save-load-dialog.component';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
<app-base-vehicle-info-window
2+
*ngIf="isPostSolve(page$ | async); else preSolve"
23
[vehicle]="vehicle$ | async"
34
[shipmentCount]="shipmentCount$ | async"
45
[navigation]="true"
56
[route]="route$ | async"
67
[timezoneOffset]="timezoneOffset$ | async"
78
(vehicleClick)="onVehicleClick($event)">
89
</app-base-vehicle-info-window>
10+
11+
<ng-template #preSolve>
12+
<app-base-pre-solve-vehicle-info-window
13+
[vehicle]="vehicle$ | async"
14+
[timezoneOffset]="timezoneOffset$ | async"
15+
[globalDuration]="globalDuration$ | async"></app-base-pre-solve-vehicle-info-window>
16+
</ng-template>

0 commit comments

Comments
 (0)