Show route on day click in trip table

This commit is contained in:
itskovacs 2025-07-19 17:44:52 +02:00
parent 2ffbcbf57d
commit 5ae894c577
4 changed files with 111 additions and 25 deletions

View File

@ -1 +1 @@
__version__ = "1.2.0" __version__ = "1.3.0"

View File

@ -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>

View File

@ -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();
} }
}, },
}); });

View File

@ -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);