✨ Show route on day click in trip table
This commit is contained in:
parent
2ffbcbf57d
commit
5ae894c577
@ -1 +1 @@
|
|||||||
__version__ = "1.2.0"
|
__version__ = "1.3.0"
|
||||||
|
|||||||
@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<section class="p-4 print:px-1 grid md:grid-cols-3 gap-4 print:block">
|
<section class="p-4 print:px-1 grid md:grid-cols-3 gap-4 print:block">
|
||||||
<div class="p-4 shadow rounded-md md:col-span-2 max-w-screen print:col-span-full">
|
<div class="p-4 shadow self-start rounded-md md:col-span-2 max-w-screen print:col-span-full">
|
||||||
<div class="p-2 mb-2 flex justify-between items-center">
|
<div class="p-2 mb-2 flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="font-semibold tracking-tight text-xl">Plans</h1>
|
<h1 class="font-semibold tracking-tight text-xl">Plans</h1>
|
||||||
@ -76,12 +76,13 @@
|
|||||||
<th class="w-12">Status</th>
|
<th class="w-12">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #body let-tripitem let-rowIndex="rowIndex" let-rowgroup="rowgroup" let-rowspan="rowspan">
|
<ng-template #body let-tripitem let-rowgroup="rowgroup" let-rowspan="rowspan">
|
||||||
<tr class="h-12 cursor-pointer" [class.font-bold]="selectedItem?.id === tripitem.id"
|
<tr class="h-12 cursor-pointer" [class.font-bold]="selectedItem?.id === tripitem.id"
|
||||||
(click)="onRowClick(tripitem)">
|
(click)="onRowClick(tripitem)">
|
||||||
@if (rowgroup) {
|
@if (rowgroup) {
|
||||||
<td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-default"
|
<td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-pointer"
|
||||||
(click)="$event.stopPropagation()">
|
[class.text-blue-500]="tripMapAntLayerDayID == tripitem.day_id"
|
||||||
|
(click)="toggleTripDayHighlightPath(tripitem.day_id); $event.stopPropagation()">
|
||||||
<div class="truncate">{{tripitem.td_label }}</div>
|
<div class="truncate">{{tripitem.td_label }}</div>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
@ -273,7 +274,7 @@
|
|||||||
@defer {
|
@defer {
|
||||||
@for (p of places; track p.id) {
|
@for (p of places; track p.id) {
|
||||||
<div class="flex items-center gap-4 py-2 px-4 hover:bg-gray-50 rounded-md overflow-auto"
|
<div class="flex items-center gap-4 py-2 px-4 hover:bg-gray-50 rounded-md overflow-auto"
|
||||||
(mouseenter)="highlightMarker(p.lat, p.lng)" (mouseleave)="resetHighlightMarker()">
|
(mouseenter)="placeHighlightMarker(p.lat, p.lng)" (mouseleave)="resetPlaceHighlightMarker()">
|
||||||
<img [src]="p.image" class="w-12 rounded-full object-fit">
|
<img [src]="p.image" class="w-12 rounded-full object-fit">
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 truncate">
|
<div class="flex flex-col gap-1 truncate">
|
||||||
@ -322,7 +323,7 @@
|
|||||||
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} places</span>
|
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} places</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p-button icon="pi pi-refresh" [disabled]="!places.length" (click)="setMapBounds()" text />
|
<p-button icon="pi pi-refresh" [disabled]="!places.length" (click)="resetMapBounds()" text />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="map" class="w-full rounded-md min-h-96 h-1/3 max-h-full"></div>
|
<div id="map" class="w-full rounded-md min-h-96 h-1/3 max-h-full"></div>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { InputTextModule } from "primeng/inputtext";
|
|||||||
import { SkeletonModule } from "primeng/skeleton";
|
import { SkeletonModule } from "primeng/skeleton";
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
|
import { AntPath, antPath } from "leaflet-ant-path";
|
||||||
import { TableModule } from "primeng/table";
|
import { TableModule } from "primeng/table";
|
||||||
import {
|
import {
|
||||||
Trip,
|
Trip,
|
||||||
@ -60,6 +61,9 @@ export class TripComponent implements AfterViewInit {
|
|||||||
currency$: Observable<string>;
|
currency$: Observable<string>;
|
||||||
|
|
||||||
trip: Trip | undefined;
|
trip: Trip | undefined;
|
||||||
|
tripMapAntLayer: undefined;
|
||||||
|
tripMapAntLayerDayID: number | undefined;
|
||||||
|
|
||||||
totalPrice: number = 0;
|
totalPrice: number = 0;
|
||||||
dayStatsCache = new Map<number, { price: number; places: number }>();
|
dayStatsCache = new Map<number, { price: number; places: number }>();
|
||||||
|
|
||||||
@ -137,7 +141,7 @@ export class TripComponent implements AfterViewInit {
|
|||||||
this.setPlacesAndMarkers();
|
this.setPlacesAndMarkers();
|
||||||
|
|
||||||
this.map.setView([48.107, -2.988]);
|
this.map.setView([48.107, -2.988]);
|
||||||
this.setMapBounds();
|
this.resetMapBounds();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -204,7 +208,7 @@ export class TripComponent implements AfterViewInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setMapBounds() {
|
resetMapBounds() {
|
||||||
if (!this.places.length) return;
|
if (!this.places.length) return;
|
||||||
this.map.fitBounds(
|
this.map.fitBounds(
|
||||||
this.places.map((p) => [p.lat, p.lng]),
|
this.places.map((p) => [p.lat, p.lng]),
|
||||||
@ -224,14 +228,14 @@ export class TripComponent implements AfterViewInit {
|
|||||||
) ?? 0;
|
) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetHighlightMarker() {
|
resetPlaceHighlightMarker() {
|
||||||
if (this.hoveredElement) {
|
if (this.hoveredElement) {
|
||||||
this.hoveredElement.classList.remove("listHover");
|
this.hoveredElement.classList.remove("listHover");
|
||||||
this.hoveredElement = undefined;
|
this.hoveredElement = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
highlightMarker(lat: number, lng: number) {
|
placeHighlightMarker(lat: number, lng: number) {
|
||||||
if (this.hoveredElement) {
|
if (this.hoveredElement) {
|
||||||
this.hoveredElement.classList.remove("listHover");
|
this.hoveredElement.classList.remove("listHover");
|
||||||
this.hoveredElement = undefined;
|
this.hoveredElement = undefined;
|
||||||
@ -266,13 +270,61 @@ export class TripComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleTripDayHighlightPath(day_id: number) {
|
||||||
|
// Click on the currently displayed day: remove
|
||||||
|
if (this.tripMapAntLayerDayID == day_id) {
|
||||||
|
this.map.removeLayer(this.tripMapAntLayer);
|
||||||
|
this.tripMapAntLayerDayID = undefined;
|
||||||
|
this.resetMapBounds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = this.trip?.days.findIndex((d) => d.id === day_id);
|
||||||
|
if (!this.trip || index == -1) return;
|
||||||
|
|
||||||
|
const data = this.trip.days[index as number].items;
|
||||||
|
data.sort((a, b) => a.time.localeCompare(b.time));
|
||||||
|
const coords = data
|
||||||
|
.map((item) => {
|
||||||
|
if (item.lat && item.lng) return [item.lat, item.lng];
|
||||||
|
if (item.place && item.place) return [item.place.lat, item.place.lng];
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter((n): n is number[] => n !== undefined);
|
||||||
|
this.map.fitBounds(coords, { padding: [30, 30] });
|
||||||
|
|
||||||
|
const path = antPath(coords, {
|
||||||
|
delay: 400,
|
||||||
|
dashArray: [10, 20],
|
||||||
|
weight: 5,
|
||||||
|
color: "#0000FF",
|
||||||
|
pulseColor: "#FFFFFF",
|
||||||
|
paused: false,
|
||||||
|
reverse: false,
|
||||||
|
hardwareAccelerated: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.tripMapAntLayer) {
|
||||||
|
this.map.removeLayer(this.tripMapAntLayer);
|
||||||
|
this.tripMapAntLayerDayID = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UX
|
||||||
|
setTimeout(() => {
|
||||||
|
this.map.addLayer(path);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
this.tripMapAntLayer = path;
|
||||||
|
this.tripMapAntLayerDayID = day_id;
|
||||||
|
}
|
||||||
|
|
||||||
onRowClick(item: FlattenedTripItem) {
|
onRowClick(item: FlattenedTripItem) {
|
||||||
if (this.selectedItem && this.selectedItem.id === item.id) {
|
if (this.selectedItem && this.selectedItem.id === item.id) {
|
||||||
this.selectedItem = undefined;
|
this.selectedItem = undefined;
|
||||||
this.resetHighlightMarker();
|
this.resetPlaceHighlightMarker();
|
||||||
} else {
|
} else {
|
||||||
this.selectedItem = item;
|
this.selectedItem = item;
|
||||||
if (item.lat && item.lng) this.highlightMarker(item.lat, item.lng);
|
if (item.lat && item.lng) this.placeHighlightMarker(item.lat, item.lng);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,7 +532,7 @@ export class TripComponent implements AfterViewInit {
|
|||||||
next: (trip) => {
|
next: (trip) => {
|
||||||
this.trip = trip;
|
this.trip = trip;
|
||||||
this.setPlacesAndMarkers();
|
this.setPlacesAndMarkers();
|
||||||
this.setMapBounds();
|
this.resetMapBounds();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -543,7 +595,10 @@ export class TripComponent implements AfterViewInit {
|
|||||||
data: {
|
data: {
|
||||||
places: this.places,
|
places: this.places,
|
||||||
days: this.trip?.days,
|
days: this.trip?.days,
|
||||||
item: item,
|
item: {
|
||||||
|
...item,
|
||||||
|
status: item.status ? (item.status as TripStatus).label : null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
"640px": "90vw",
|
"640px": "90vw",
|
||||||
@ -582,6 +637,9 @@ export class TripComponent implements AfterViewInit {
|
|||||||
|
|
||||||
const updatedPrice = -(item.price || 0) + (it.price || 0);
|
const updatedPrice = -(item.price || 0) + (it.price || 0);
|
||||||
this.updateTotalPrice(updatedPrice);
|
this.updateTotalPrice(updatedPrice);
|
||||||
|
|
||||||
|
if (this.tripMapAntLayerDayID == item.day_id)
|
||||||
|
this.toggleTripDayHighlightPath(item.day_id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -621,7 +679,7 @@ export class TripComponent implements AfterViewInit {
|
|||||||
);
|
);
|
||||||
this.dayStatsCache.delete(item.day_id);
|
this.dayStatsCache.delete(item.day_id);
|
||||||
this.selectedItem = undefined;
|
this.selectedItem = undefined;
|
||||||
this.resetHighlightMarker();
|
this.resetPlaceHighlightMarker();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
import {
|
||||||
|
FormBuilder,
|
||||||
|
FormGroup,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from "@angular/forms";
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from "primeng/button";
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
@ -40,30 +45,52 @@ export class TripCreateDayItemModalComponent {
|
|||||||
private ref: DynamicDialogRef,
|
private ref: DynamicDialogRef,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private config: DynamicDialogConfig,
|
private config: DynamicDialogConfig,
|
||||||
private utilsService: UtilsService
|
private utilsService: UtilsService,
|
||||||
) {
|
) {
|
||||||
this.statuses = this.utilsService.statuses;
|
this.statuses = this.utilsService.statuses;
|
||||||
|
|
||||||
this.itemForm = this.fb.group({
|
this.itemForm = this.fb.group({
|
||||||
id: -1,
|
id: -1,
|
||||||
time: ["", { validators: [Validators.required, Validators.pattern(/^([01]\d|2[0-3])(:[0-5]\d)?$/)] }],
|
time: [
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
validators: [
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(/^([01]\d|2[0-3])(:[0-5]\d)?$/),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
text: ["", Validators.required],
|
text: ["", Validators.required],
|
||||||
comment: "",
|
comment: "",
|
||||||
day_id: [null, Validators.required],
|
day_id: [null, Validators.required],
|
||||||
place: null,
|
place: null,
|
||||||
status: null,
|
status: null,
|
||||||
price: 0,
|
price: 0,
|
||||||
lat: ["", { validators: Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)") }],
|
lat: [
|
||||||
lng: ["", { validators: Validators.pattern("-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)") }],
|
"",
|
||||||
|
{
|
||||||
|
validators: Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lng: [
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
validators: Validators.pattern(
|
||||||
|
"-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.config.data) {
|
if (this.config.data) {
|
||||||
const item = this.config.data.item;
|
const item = this.config.data.item;
|
||||||
if (item) this.itemForm.patchValue({ ...item, place: item.place?.id || null });
|
if (item)
|
||||||
|
this.itemForm.patchValue({ ...item, place: item.place?.id || null });
|
||||||
|
|
||||||
this.places = this.config.data.places;
|
this.places = this.config.data.places;
|
||||||
this.days = this.config.data.days;
|
this.days = this.config.data.days;
|
||||||
if (this.config.data.selectedDay) this.itemForm.get("day_id")?.setValue(this.config.data.selectedDay);
|
if (this.config.data.selectedDay)
|
||||||
|
this.itemForm.get("day_id")?.setValue(this.config.data.selectedDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.itemForm.get("place")?.valueChanges.subscribe({
|
this.itemForm.get("place")?.valueChanges.subscribe({
|
||||||
@ -96,8 +123,8 @@ export class TripCreateDayItemModalComponent {
|
|||||||
// Normalize data for API POST
|
// Normalize data for API POST
|
||||||
let ret = this.itemForm.value;
|
let ret = this.itemForm.value;
|
||||||
if (!ret["lat"]) {
|
if (!ret["lat"]) {
|
||||||
delete ret["lat"];
|
ret["lat"] = null;
|
||||||
delete ret["lng"];
|
ret["lng"] = null;
|
||||||
}
|
}
|
||||||
if (!ret["place"]) delete ret["place"];
|
if (!ret["place"]) delete ret["place"];
|
||||||
this.ref.close(ret);
|
this.ref.close(ret);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user