From 5ae894c5772e0f46f169505961b59d8b4785914f Mon Sep 17 00:00:00 2001 From: itskovacs Date: Sat, 19 Jul 2025 17:44:52 +0200 Subject: [PATCH] :sparkles: Show route on day click in trip table --- backend/trip/__init__.py | 2 +- .../app/components/trip/trip.component.html | 13 ++-- src/src/app/components/trip/trip.component.ts | 76 ++++++++++++++++--- .../trip-create-day-item-modal.component.ts | 45 ++++++++--- 4 files changed, 111 insertions(+), 25 deletions(-) diff --git a/backend/trip/__init__.py b/backend/trip/__init__.py index c68196d..67bc602 100644 --- a/backend/trip/__init__.py +++ b/backend/trip/__init__.py @@ -1 +1 @@ -__version__ = "1.2.0" +__version__ = "1.3.0" diff --git a/src/src/app/components/trip/trip.component.html b/src/src/app/components/trip/trip.component.html index 6abd759..86e8ebc 100644 --- a/src/src/app/components/trip/trip.component.html +++ b/src/src/app/components/trip/trip.component.html @@ -45,7 +45,7 @@ }
-
+

Plans

@@ -76,12 +76,13 @@ Status - + @if (rowgroup) { - +
{{tripitem.td_label }}
} @@ -273,7 +274,7 @@ @defer { @for (p of places; track p.id) {
+ (mouseenter)="placeHighlightMarker(p.lat, p.lng)" (mouseleave)="resetPlaceHighlightMarker()">
@@ -322,7 +323,7 @@ {{ trip?.name }} places
- +
diff --git a/src/src/app/components/trip/trip.component.ts b/src/src/app/components/trip/trip.component.ts index b3fc74b..5d0f7d7 100644 --- a/src/src/app/components/trip/trip.component.ts +++ b/src/src/app/components/trip/trip.component.ts @@ -6,6 +6,7 @@ import { InputTextModule } from "primeng/inputtext"; import { SkeletonModule } from "primeng/skeleton"; import { FloatLabelModule } from "primeng/floatlabel"; import * as L from "leaflet"; +import { AntPath, antPath } from "leaflet-ant-path"; import { TableModule } from "primeng/table"; import { Trip, @@ -60,6 +61,9 @@ export class TripComponent implements AfterViewInit { currency$: Observable; trip: Trip | undefined; + tripMapAntLayer: undefined; + tripMapAntLayerDayID: number | undefined; + totalPrice: number = 0; dayStatsCache = new Map(); @@ -137,7 +141,7 @@ export class TripComponent implements AfterViewInit { this.setPlacesAndMarkers(); 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; this.map.fitBounds( this.places.map((p) => [p.lat, p.lng]), @@ -224,14 +228,14 @@ export class TripComponent implements AfterViewInit { ) ?? 0; } - resetHighlightMarker() { + resetPlaceHighlightMarker() { if (this.hoveredElement) { this.hoveredElement.classList.remove("listHover"); this.hoveredElement = undefined; } } - highlightMarker(lat: number, lng: number) { + placeHighlightMarker(lat: number, lng: number) { if (this.hoveredElement) { this.hoveredElement.classList.remove("listHover"); 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) { if (this.selectedItem && this.selectedItem.id === item.id) { this.selectedItem = undefined; - this.resetHighlightMarker(); + this.resetPlaceHighlightMarker(); } else { 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) => { this.trip = trip; this.setPlacesAndMarkers(); - this.setMapBounds(); + this.resetMapBounds(); }, }); }, @@ -543,7 +595,10 @@ export class TripComponent implements AfterViewInit { data: { places: this.places, days: this.trip?.days, - item: item, + item: { + ...item, + status: item.status ? (item.status as TripStatus).label : null, + }, }, breakpoints: { "640px": "90vw", @@ -582,6 +637,9 @@ export class TripComponent implements AfterViewInit { const updatedPrice = -(item.price || 0) + (it.price || 0); 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.selectedItem = undefined; - this.resetHighlightMarker(); + this.resetPlaceHighlightMarker(); } }, }); diff --git a/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.ts b/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.ts index 2e285cf..03c3809 100644 --- a/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.ts +++ b/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.ts @@ -1,5 +1,10 @@ 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 { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog"; import { FloatLabelModule } from "primeng/floatlabel"; @@ -40,30 +45,52 @@ export class TripCreateDayItemModalComponent { private ref: DynamicDialogRef, private fb: FormBuilder, private config: DynamicDialogConfig, - private utilsService: UtilsService + private utilsService: UtilsService, ) { this.statuses = this.utilsService.statuses; this.itemForm = this.fb.group({ 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], comment: "", day_id: [null, Validators.required], place: null, status: null, price: 0, - lat: ["", { validators: Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)") }], - lng: ["", { validators: Validators.pattern("-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)") }], + lat: [ + "", + { + 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) { 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.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({ @@ -96,8 +123,8 @@ export class TripCreateDayItemModalComponent { // Normalize data for API POST let ret = this.itemForm.value; if (!ret["lat"]) { - delete ret["lat"]; - delete ret["lng"]; + ret["lat"] = null; + ret["lng"] = null; } if (!ret["place"]) delete ret["place"]; this.ref.close(ret);