From 0b7af7ce9966ab4ee83a6554342691a1fcd09b3e Mon Sep 17 00:00:00 2001 From: itskovacs Date: Wed, 6 Aug 2025 21:10:34 +0200 Subject: [PATCH] :zap: Trip performance and code improvements --- src/src/app/components/trip/trip.component.ts | 819 +++++++++--------- 1 file changed, 415 insertions(+), 404 deletions(-) diff --git a/src/src/app/components/trip/trip.component.ts b/src/src/app/components/trip/trip.component.ts index bb0cf4e..2f594c3 100644 --- a/src/src/app/components/trip/trip.component.ts +++ b/src/src/app/components/trip/trip.component.ts @@ -28,7 +28,14 @@ import { TripPlaceSelectModalComponent } from "../../modals/trip-place-select-mo import { TripCreateDayModalComponent } from "../../modals/trip-create-day-modal/trip-create-day-modal.component"; import { TripCreateDayItemModalComponent } from "../../modals/trip-create-day-item-modal/trip-create-day-item-modal.component"; import { TripCreateItemsModalComponent } from "../../modals/trip-create-items-modal/trip-create-items-modal.component"; -import { combineLatest, forkJoin, map, Observable, tap } from "rxjs"; +import { + combineLatest, + forkJoin, + Observable, + switchMap, + take, + tap, +} from "rxjs"; import { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.component"; import { UtilsService } from "../../services/utils.service"; import { TripCreateModalComponent } from "../../modals/trip-create-modal/trip-create-modal.component"; @@ -37,6 +44,7 @@ import { MenuItem } from "primeng/api"; import { MenuModule } from "primeng/menu"; import { LinkifyPipe } from "../../shared/linkify.pipe"; import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place-create-modal.component"; +import { Settings } from "../../types/settings"; @Component({ selector: "app-trip", @@ -57,32 +65,92 @@ import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place styleUrls: ["./trip.component.scss"], }) export class TripComponent implements AfterViewInit { - map: any; - markerClusterGroup: any; - selectedItem: (TripItem & { status?: TripStatus }) | undefined; - statuses: TripStatus[] = []; - hoveredElement: HTMLElement | undefined; currency$: Observable; - placesUsedInTable = new Set(); - - trip: Trip | undefined; - tripMapAntLayer: L.LayerGroup | undefined; - tripMapAntLayerDayID: number | undefined; - isMapFullscreen: boolean = false; - - totalPrice: number = 0; - dayStatsCache = new Map(); - + statuses: TripStatus[] = []; + trip?: Trip; places: Place[] = []; flattenedTripItems: FlattenedTripItem[] = []; - menuTripActionsItems: MenuItem[] = []; + selectedItem?: TripItem & { status?: TripStatus }; - menuTripDayActionsItems: MenuItem[] = []; - selectedTripDayForMenu: TripDay | undefined; + isMapFullscreen = false; + totalPrice = 0; + collapsedTripDays = false; + collapsedTripPlaces = false; + collapsedTripStatuses = false; - collapsedTripPlaces: boolean = false; - collapsedTripDays: boolean = false; - collapsedTripStatuses: boolean = false; + map?: L.Map; + markerClusterGroup?: L.MarkerClusterGroup; + hoveredElement?: HTMLElement; + tripMapAntLayer?: L.LayerGroup; + tripMapAntLayerDayID?: number; + + readonly menuTripActionsItems: MenuItem[] = [ + { + label: "Actions", + items: [ + { + label: "Edit", + icon: "pi pi-pencil", + iconClass: "text-blue-500!", + command: () => { + this.editTrip(); + }, + }, + { + label: "Archive", + icon: "pi pi-box", + iconClass: "text-orange-500!", + command: () => { + this.toggleArchiveTrip(); + }, + }, + { + label: "Delete", + icon: "pi pi-trash", + iconClass: "text-red-500!", + command: () => { + this.deleteTrip(); + }, + }, + ], + }, + ]; + readonly menuTripDayActionsItems: MenuItem[] = [ + { + label: "Actions", + items: [ + { + label: "Item", + icon: "pi pi-plus", + iconClass: "text-blue-500!", + command: () => { + this.addItem(); + }, + }, + { + label: "Edit", + icon: "pi pi-pencil", + command: () => { + if (!this.selectedTripDayForMenu) return; + this.editDay(this.selectedTripDayForMenu); + }, + }, + { + label: "Delete", + icon: "pi pi-trash", + iconClass: "text-red-500!", + command: () => { + if (!this.selectedTripDayForMenu) return; + this.deleteDay(this.selectedTripDayForMenu); + }, + }, + ], + }, + ]; + selectedTripDayForMenu?: TripDay; + + dayStatsCache = new Map(); + placesUsedInTable = new Set(); constructor( private apiService: ApiService, @@ -93,70 +161,55 @@ export class TripComponent implements AfterViewInit { ) { this.currency$ = this.utilsService.currency$; this.statuses = this.utilsService.statuses; + } - this.menuTripActionsItems = [ + ngAfterViewInit(): void { + this.route.paramMap + .pipe( + take(1), + tap((params) => { + const id = params.get("id"); + if (id) this.loadTripData(+id); + }), + ) + .subscribe(); + } + + loadTripData(id: number): void { + combineLatest({ + trip: this.apiService.getTrip(+id), + settings: this.apiService.getSettings(), + }) + .pipe( + take(1), + tap(({ trip, settings }) => { + this.trip = trip; + this.flattenTripDayItems(); + this.updateTotalPrice(); + this.setupMap(settings); + }), + ) + .subscribe(); + } + + setupMap(settings: Settings): void { + const contentMenuItems = [ { - label: "Actions", - items: [ - { - label: "Edit", - icon: "pi pi-pencil", - iconClass: "text-blue-500!", - command: () => { - this.editTrip(); - }, - }, - { - label: "Archive", - icon: "pi pi-box", - iconClass: "text-orange-500!", - command: () => { - this.toggleArchiveTrip(); - }, - }, - { - label: "Delete", - icon: "pi pi-trash", - iconClass: "text-red-500!", - command: () => { - this.deleteTrip(); - }, - }, - ], - }, - ]; - this.menuTripDayActionsItems = [ - { - label: "Actions", - items: [ - { - label: "Item", - icon: "pi pi-plus", - iconClass: "text-blue-500!", - command: () => { - this.addItem(); - }, - }, - { - label: "Edit", - icon: "pi pi-pencil", - command: () => { - if (!this.selectedTripDayForMenu) return; - this.editDay(this.selectedTripDayForMenu); - }, - }, - { - label: "Delete", - icon: "pi pi-trash", - iconClass: "text-red-500!", - command: () => { - if (!this.selectedTripDayForMenu) return; - this.deleteDay(this.selectedTripDayForMenu); - }, - }, - ], + text: "Copy coordinates", + callback: (e: any) => { + const latlng = e.latlng; + navigator.clipboard.writeText( + `${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`, + ); + }, }, ]; + this.map = createMap(contentMenuItems, settings.tile_layer); + this.markerClusterGroup = createClusterGroup().addTo(this.map); + this.setPlacesAndMarkers(); + + this.map.setView([settings.map_lat, settings.map_lng]); + this.resetMapBounds(); } back() { @@ -170,52 +223,12 @@ export class TripComponent implements AfterViewInit { }, 30); } - ngAfterViewInit(): void { - this.route.paramMap.subscribe((params) => { - const id = params.get("id"); - if (id) { - combineLatest({ - trip: this.apiService.getTrip(+id), - settings: this.apiService.getSettings(), - }) - .pipe( - tap(({ trip, settings }) => { - this.trip = trip; - this.flattenTripDayItems(); - this.updateTotalPrice(); - - let contentMenuItems = [ - { - text: "Copy coordinates", - callback: (e: any) => { - const latlng = e.latlng; - navigator.clipboard.writeText( - `${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`, - ); - }, - }, - ]; - this.map = createMap(contentMenuItems, settings.tile_layer); - this.markerClusterGroup = createClusterGroup().addTo(this.map); - this.setPlacesAndMarkers(); - - this.map.setView([48.107, -2.988]); - this.resetMapBounds(); - }), - ) - .subscribe(); - } - }); - } - sortTripDays() { this.trip?.days.sort((a, b) => a.label.localeCompare(b.label)); } getDayStats(day: TripDay): { price: number; places: number } { - if (this.dayStatsCache.has(day.id)) { - return this.dayStatsCache.get(day.id)!; - } + if (this.dayStatsCache.has(day.id)) return this.dayStatsCache.get(day.id)!; const stats = day.items.reduce( (acc, item) => { @@ -225,7 +238,6 @@ export class TripComponent implements AfterViewInit { }, { price: 0, places: 0 }, ); - this.dayStatsCache.set(day.id, stats); return stats; } @@ -233,17 +245,16 @@ export class TripComponent implements AfterViewInit { get getWatchlistData(): (TripItem & { status: TripStatus })[] { if (!this.trip?.days) return []; - const data = this.trip!.days.map((day) => - day.items.filter((item) => - ["constraint", "pending"].includes(item.status as string), - ), - ).flat(); - if (!data.length) return []; - - return data.map((item) => ({ - ...item, - status: this.statusToTripStatus(item.status as string), - })) as (TripItem & { status: TripStatus })[]; + return this.trip.days + .flatMap((day) => + day.items.filter((item) => + ["constraint", "pending"].includes(item.status as string), + ), + ) + .map((item) => ({ + ...item, + status: this.statusToTripStatus(item.status as string), + })) as (TripItem & { status: TripStatus })[]; } isPlaceUsed(id: number): boolean { @@ -252,7 +263,7 @@ export class TripComponent implements AfterViewInit { statusToTripStatus(status?: string): TripStatus | undefined { if (!status) return undefined; - return this.statuses.find((s) => s.label == status) as TripStatus; + return this.statuses.find((s) => s.label == status); } flattenTripDayItems() { @@ -279,16 +290,16 @@ export class TripComponent implements AfterViewInit { computePlacesUsedInTable() { this.placesUsedInTable.clear(); - this.flattenedTripItems.forEach((i) => { - if (i.place?.id) this.placesUsedInTable.add(i.place.id); + this.flattenedTripItems.forEach((item) => { + if (item.place?.id) this.placesUsedInTable.add(item.place.id); }); } setPlacesAndMarkers() { this.computePlacesUsedInTable(); - this.places = this.trip?.places || []; - this.places.sort((a, b) => a.name.localeCompare(b.name)); - + this.places = [...(this.trip?.places ?? [])].sort((a, b) => + a.name.localeCompare(b.name), + ); this.markerClusterGroup?.clearLayers(); this.places.forEach((p) => { const marker = placeToMarker(p, false, !this.placesUsedInTable.has(p.id)); @@ -298,7 +309,7 @@ export class TripComponent implements AfterViewInit { resetMapBounds() { if (!this.places.length) return; - this.map.fitBounds( + this.map?.fitBounds( this.places.map((p) => [p.lat, p.lng]), { padding: [30, 30] }, ); @@ -309,35 +320,33 @@ export class TripComponent implements AfterViewInit { document.body.classList.toggle("overflow-hidden"); setTimeout(() => { - this.map.invalidateSize(); + this.map?.invalidateSize(); this.resetMapBounds(); }, 50); } updateTotalPrice(n?: number) { - if (n) this.totalPrice += n; - else - this.totalPrice = - this.trip?.days - .flatMap((d) => d.items) - .reduce( - (price, item) => price + (item.price ?? item.place?.price ?? 0), - 0, - ) ?? 0; + if (n) { + this.totalPrice += n; + return; + } + this.totalPrice = + this.trip?.days + .flatMap((d) => d.items) + .reduce( + (price, item) => price + (item.price ?? item.place?.price ?? 0), + 0, + ) ?? 0; } resetPlaceHighlightMarker() { - if (this.hoveredElement) { - this.hoveredElement.classList.remove("listHover"); - this.hoveredElement = undefined; - } + if (!this.hoveredElement) return; + this.hoveredElement.classList.remove("listHover"); + this.hoveredElement = undefined; } placeHighlightMarker(lat: number, lng: number) { - if (this.hoveredElement) { - this.hoveredElement.classList.remove("listHover"); - this.hoveredElement = undefined; - } + this.resetPlaceHighlightMarker(); let marker: L.Marker | undefined; this.markerClusterGroup?.eachLayer((layer: any) => { @@ -347,14 +356,13 @@ export class TripComponent implements AfterViewInit { }); if (!marker) return; - let markerElement = marker.getElement() as HTMLElement; // search for Marker. If 'null', is inside Cluster - + const markerElement = marker.getElement() as HTMLElement; // search for Marker. If 'null', is inside Cluster if (markerElement) { // marker, not clustered markerElement.classList.add("listHover"); this.hoveredElement = markerElement; } else { - // marker , clustered + // marker is clustered const parentCluster = (this.markerClusterGroup as any).getVisibleParent( marker, ); @@ -368,37 +376,11 @@ export class TripComponent implements AfterViewInit { } } - toggleTripDaysHighlight() { - if (this.tripMapAntLayerDayID == -1) { - this.map.removeLayer(this.tripMapAntLayer); - this.tripMapAntLayerDayID = undefined; - this.resetMapBounds(); - return; - } - - if (!this.trip) return; - - const items = this.trip.days - .flatMap((day) => day.items.sort((a, b) => a.time.localeCompare(b.time))) - .map((item) => { - if (item.lat && item.lng) - return { - text: item.text, - lat: item.lat, - lng: item.lng, - isPlace: !!item.place, - }; - if (item.place && item.place) - return { - text: item.text, - lat: item.place.lat, - lng: item.place.lng, - isPlace: true, - }; - return undefined; - }) - .filter((n) => n !== undefined); - + highlightTripPath( + items: { text: string; lat: number; lng: number; isPlace: boolean }[], + layerId: number, + antDelay = 400, + ): void { if (items.length < 2) { this.utilsService.toast( "info", @@ -408,7 +390,7 @@ export class TripComponent implements AfterViewInit { return; } - this.map.fitBounds( + this.map?.fitBounds( items.map((c) => [c.lat, c.lng]), { padding: [30, 30] }, ); @@ -416,7 +398,7 @@ export class TripComponent implements AfterViewInit { const path = antPath( items.map((c) => [c.lat, c.lng]), { - delay: 600, + delay: antDelay, dashArray: [10, 20], weight: 5, color: "#0000FF", @@ -434,34 +416,65 @@ export class TripComponent implements AfterViewInit { }); if (this.tripMapAntLayer) { - this.map.removeLayer(this.tripMapAntLayer); + this.map?.removeLayer(this.tripMapAntLayer); this.tripMapAntLayerDayID = undefined; } - // UX setTimeout(() => { - layGroup.addTo(this.map); + layGroup.addTo(this.map!); }, 200); this.tripMapAntLayer = layGroup; - this.tripMapAntLayerDayID = -1; //Hardcoded value for global trace + this.tripMapAntLayerDayID = layerId; + } + + toggleTripDaysHighlight() { + if (this.tripMapAntLayerDayID == -1) { + this.map?.removeLayer(this.tripMapAntLayer!); + this.tripMapAntLayerDayID = undefined; + this.resetMapBounds(); + return; + } + if (!this.trip) return; + + const items = this.trip.days + .flatMap((day) => day.items.sort((a, b) => a.time.localeCompare(b.time))) + .map((item) => { + if (item.lat && item.lng) + return { + text: item.text, + lat: item.lat, + lng: item.lng, + isPlace: !!item.place, + }; + if (item.place) + return { + text: item.text, + lat: item.place.lat, + lng: item.place.lng, + isPlace: true, + }; + return undefined; + }) + .filter((n) => n !== undefined); + + this.highlightTripPath(items, -1, 600); //Hardcoded value for global trace } toggleTripDayHighlightPathDay(day_id: number) { // Click on the currently displayed day: remove if (this.tripMapAntLayerDayID == day_id) { - this.map.removeLayer(this.tripMapAntLayer); + 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 idx = this.trip?.days.findIndex((d) => d.id === day_id); + if (!this.trip || idx === undefined || idx == -1) return; + const data = this.trip.days[idx].items.sort((a, b) => + a.time.localeCompare(b.time), + ); const items = data .map((item) => { if (item.lat && item.lng) @@ -482,52 +495,7 @@ export class TripComponent implements AfterViewInit { }) .filter((n) => n !== undefined); - if (items.length < 2) { - this.utilsService.toast( - "info", - "Info", - "Not enough values to map an itinerary", - ); - return; - } - - this.map.fitBounds( - items.map((c) => [c.lat, c.lng]), - { padding: [30, 30] }, - ); - - const path = antPath( - items.map((c) => [c.lat, c.lng]), - { - delay: 400, - dashArray: [10, 20], - weight: 5, - color: "#0000FF", - pulseColor: "#FFFFFF", - paused: false, - reverse: false, - hardwareAccelerated: true, - }, - ); - - const layGroup = L.layerGroup(); - layGroup.addLayer(path); - items.forEach((item) => { - if (!item.isPlace) layGroup.addLayer(tripDayMarker(item)); - }); - - if (this.tripMapAntLayer) { - this.map.removeLayer(this.tripMapAntLayer); - this.tripMapAntLayerDayID = undefined; - } - - // UX - setTimeout(() => { - layGroup.addTo(this.map); - }, 200); - - this.tripMapAntLayer = layGroup; - this.tripMapAntLayerDayID = day_id; + this.highlightTripPath(items, day_id); } onRowClick(item: FlattenedTripItem) { @@ -552,14 +520,17 @@ export class TripComponent implements AfterViewInit { data: `Delete ${this.trip?.name} ? This will delete everything.`, }); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (bool) => { if (bool) - this.apiService.deleteTrip(this.trip?.id!).subscribe({ - next: () => { - this.router.navigateByUrl("/trips"); - }, - }); + this.apiService + .deleteTrip(this.trip?.id!) + .pipe(take(1)) + .subscribe({ + next: () => { + this.router.navigateByUrl("/trips"); + }, + }); }, }); } @@ -581,13 +552,16 @@ export class TripComponent implements AfterViewInit { }, ); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (new_trip: Trip | null) => { if (!new_trip) return; - this.apiService.putTrip(new_trip, this.trip?.id!).subscribe({ - next: (trip: Trip) => (this.trip = trip), - }); + this.apiService + .putTrip(new_trip, this.trip?.id!) + .pipe(take(1)) + .subscribe({ + next: (trip: Trip) => (this.trip = trip), + }); }, }); } @@ -605,11 +579,12 @@ export class TripComponent implements AfterViewInit { data: `${currentArchiveStatus ? "Restore" : "Archive"} ${this.trip?.name} ?${currentArchiveStatus ? "" : " This will make everything read-only."}`, }); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (bool) => { if (bool) this.apiService .putTrip({ archived: !currentArchiveStatus }, this.trip?.id!) + .pipe(take(1)) .subscribe({ next: () => { this.trip!.archived = !currentArchiveStatus; @@ -620,6 +595,8 @@ export class TripComponent implements AfterViewInit { } addDay() { + if (!this.trip) return; + const modal: DynamicDialogRef = this.dialogService.open( TripCreateDayModalComponent, { @@ -629,28 +606,33 @@ export class TripComponent implements AfterViewInit { closable: true, dismissableMask: true, width: "50vw", - data: { days: this.trip?.days }, + data: { days: this.trip.days }, breakpoints: { "640px": "80vw", }, }, ); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (day: TripDay | null) => { if (!day) return; - this.apiService.postTripDay(day, this.trip?.id!).subscribe({ - next: (day) => { - this.trip?.days.push(day); - this.flattenTripDayItems(); - }, - }); + this.apiService + .postTripDay(day, this.trip?.id!) + .pipe(take(1)) + .subscribe({ + next: (day) => { + this.trip!.days.push(day); + this.flattenTripDayItems(); + }, + }); }, }); } editDay(day: TripDay) { + if (!this.trip) return; + const modal: DynamicDialogRef = this.dialogService.open( TripCreateDayModalComponent, { @@ -660,31 +642,36 @@ export class TripComponent implements AfterViewInit { closable: true, dismissableMask: true, width: "50vw", - data: { day: day, days: this.trip?.days }, + data: { day: day, days: this.trip.days }, breakpoints: { "640px": "80vw", }, }, ); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (day: TripDay | null) => { if (!day) return; - this.apiService.putTripDay(day, this.trip?.id!).subscribe({ - next: (day) => { - let index = this.trip?.days.findIndex((d) => d.id == day.id); - if (index != -1) { - this.trip?.days.splice(index as number, 1, day); - this.flattenTripDayItems(); - } - }, - }); + this.apiService + .putTripDay(day, this.trip?.id!) + .pipe(take(1)) + .subscribe({ + next: (day) => { + const idx = this.trip!.days.findIndex((d) => d.id == day.id); + if (idx != -1) { + this.trip?.days.splice(idx, 1, day); + this.flattenTripDayItems(); + } + }, + }); }, }); } deleteDay(day: TripDay) { + if (!this.trip) return; + const modal = this.dialogService.open(YesNoModalComponent, { header: "Confirm deletion", modal: true, @@ -696,22 +683,29 @@ export class TripComponent implements AfterViewInit { data: `Delete ${day.label} ? This will delete everything for this day.`, }); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (bool) => { if (bool) - this.apiService.deleteTripDay(this.trip?.id!, day.id).subscribe({ - next: () => { - let index = this.trip?.days.findIndex((d) => d.id == day.id); - this.trip?.days.splice(index as number, 1); - this.flattenTripDayItems(); - this.setPlacesAndMarkers(); - }, - }); + this.apiService + .deleteTripDay(this.trip?.id!, day.id) + .pipe(take(1)) + .subscribe({ + next: () => { + const idx = this.trip!.days.findIndex((d) => d.id == day.id); + if (idx != -1) { + this.trip!.days.splice(idx, 1); + this.flattenTripDayItems(); + this.setPlacesAndMarkers(); + } + }, + }); }, }); } manageTripPlaces() { + if (!this.trip) return; + const modal: DynamicDialogRef = this.dialogService.open( TripPlaceSelectModalComponent, { @@ -728,12 +722,13 @@ export class TripComponent implements AfterViewInit { }, ); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (places: Place[] | null) => { if (!places) return; this.apiService - .putTrip({ place_ids: places.map((p) => p.id) }, this.trip?.id!) + .putTrip({ place_ids: places.map((p) => p.id) }, this.trip!.id) + .pipe(take(1)) .subscribe({ next: (trip) => { this.trip = trip; @@ -746,6 +741,8 @@ export class TripComponent implements AfterViewInit { } addItem(day_id?: number) { + if (!this.trip) return; + const modal: DynamicDialogRef = this.dialogService.open( TripCreateDayItemModalComponent, { @@ -760,22 +757,25 @@ export class TripComponent implements AfterViewInit { }, data: { places: this.places, - days: this.trip?.days, + days: this.trip.days, selectedDay: day_id, }, }, ); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (item: TripItem | null) => { if (!item) return; this.apiService - .postTripDayItem(item, this.trip?.id!, item.day_id) + .postTripDayItem(item, this.trip!.id!, item.day_id) + .pipe(take(1)) .subscribe({ next: (resp) => { - let index = this.trip?.days.findIndex((d) => d.id == item.day_id); - let td: TripDay = this.trip?.days[index as number]!; + const idx = this.trip!.days.findIndex((d) => d.id == item.day_id); + if (idx === -1) return; + + const td: TripDay = this.trip!.days[idx]; td.items.push(resp); this.flattenTripDayItems(); @@ -792,6 +792,8 @@ export class TripComponent implements AfterViewInit { } editItem(item: TripItem) { + if (!this.trip) return; + const modal: DynamicDialogRef = this.dialogService.open( TripCreateDayItemModalComponent, { @@ -815,57 +817,16 @@ export class TripComponent implements AfterViewInit { }, ); - modal.onClose.subscribe({ - next: (it: TripItem | null) => { - if (!it) return; + modal.onClose.pipe(take(1)).subscribe({ + next: (updated: TripItem | null) => { + if (!updated) return; if (item.place?.id) this.placesUsedInTable.delete(item.place.id); this.apiService - .putTripDayItem(it, this.trip?.id!, item.day_id, item.id) + .putTripDayItem(updated, this.trip!.id, item.day_id, item.id) + .pipe(take(1)) .subscribe({ - next: (new_item) => { - if (item.day_id != new_item.day_id) { - let previousIndex = this.trip?.days.findIndex( - (d) => d.id == item.day_id, - ); - this.trip?.days[previousIndex as number]!.items.splice( - this.trip?.days[previousIndex as number]!.items.findIndex( - (i) => i.id == new_item.id, - ), - 1, - ); - this.dayStatsCache.delete(item.day_id); - } - - let index = this.trip?.days.findIndex( - (d) => d.id == new_item.day_id, - ); - let td: TripDay = this.trip?.days[index as number]!; - td.items.splice( - td.items.findIndex((i) => i.id == new_item.id), - 1, - new_item, - ); - this.flattenTripDayItems(); - if (this.selectedItem && this.selectedItem.id === item.id) - this.selectedItem = { - ...new_item, - status: new_item.status - ? this.statusToTripStatus(new_item.status as string) - : undefined, - }; - this.dayStatsCache.delete(new_item.day_id); - - this.computePlacesUsedInTable(); - const updatedPrice = -(new_item.price || 0) + (item.price || 0); - this.updateTotalPrice(updatedPrice); - - if (this.tripMapAntLayerDayID == new_item.day_id) - this.toggleTripDayHighlightPathDay(new_item.day_id); - - if (new_item.place?.id || item.place?.id) - this.setPlacesAndMarkers(); - }, + next: (new_item) => this.updateItemFromTrip(item, new_item), }); }, }); @@ -883,40 +844,24 @@ export class TripComponent implements AfterViewInit { data: `Delete ${item.text.substring(0, 50)} ? This will delete everything for this day.`, }); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (bool) => { - if (bool) - this.apiService - .deleteTripDayItem(this.trip?.id!, item.day_id, item.id) - .subscribe({ - next: () => { - let index = this.trip?.days.findIndex( - (d) => d.id == item.day_id, - ); - - let td: TripDay = this.trip?.days[index as number]!; - td.items.splice( - td.items.findIndex((i) => i.id == item.id), - 1, - ); - this.flattenTripDayItems(); - - if (item.place?.id) { - this.placesUsedInTable.delete(item.place.id); - if (item.place.price) - this.updateTotalPrice(-item.place.price); - this.setPlacesAndMarkers(); - } - this.dayStatsCache.delete(item.day_id); - this.selectedItem = undefined; - this.resetPlaceHighlightMarker(); - }, - }); + if (!bool) return; + this.apiService + .deleteTripDayItem(this.trip?.id!, item.day_id, item.id) + .pipe(take(1)) + .subscribe({ + next: () => { + this.removeItemFromTrip(item); + }, + }); }, }); } addItems() { + if (!this.trip) return; + const modal: DynamicDialogRef = this.dialogService.open( TripCreateItemsModalComponent, { @@ -929,29 +874,28 @@ export class TripComponent implements AfterViewInit { breakpoints: { "1260px": "90vw", }, - data: { days: this.trip?.days }, + data: { days: this.trip.days }, }, ); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (items: TripItem[] | null) => { if (!items?.length) return; const day_id = items[0].day_id; - const obs$ = items.map((item) => - this.apiService.postTripDayItem(item, this.trip?.id!, item.day_id), + this.apiService.postTripDayItem(item, this.trip!.id!, item.day_id), ); - forkJoin(obs$) - .pipe( - map((items) => { - let index = this.trip?.days.findIndex((d) => d.id == day_id); - let td: TripDay = this.trip?.days[index as number]!; - td.items.push(...items); - this.flattenTripDayItems(); - }), - ) - .subscribe(); + forkJoin(obs$).subscribe({ + next: (items: TripItem[]) => { + const index = this.trip!.days.findIndex((d) => d.id == day_id); + if (index === -1) return; + + const td: TripDay = this.trip!.days[index]!; + td.items.push(...items); + this.flattenTripDayItems(); + }, + }); }, }); } @@ -973,27 +917,94 @@ export class TripComponent implements AfterViewInit { }, ); - modal.onClose.subscribe({ + modal.onClose.pipe(take(1)).subscribe({ next: (place: Place | null) => { if (!place) return; - this.apiService.postPlace(place).subscribe({ - next: (place: Place) => { - this.apiService - .putTrip( - { place_ids: [place, ...this.places].map((p) => p.id) }, + this.apiService + .postPlace(place) + .pipe( + switchMap((createdPlace: Place) => + this.apiService.putTrip( + { place_ids: [createdPlace, ...this.places].map((p) => p.id) }, this.trip?.id!, - ) - .subscribe({ - next: (trip) => { - this.trip = trip; - this.setPlacesAndMarkers(); - this.resetMapBounds(); - }, - }); - }, - }); + ), + ), + take(1), + ) + .subscribe({ + next: (trip) => { + this.trip = trip; + this.setPlacesAndMarkers(); + this.resetMapBounds(); + }, + }); }, }); } + + updateItemFromTrip(old: TripItem, updated: TripItem): void { + if (!this.trip) return; + + if (old.day_id != updated.day_id) { + const prevDayIdx = this.trip.days.findIndex((d) => d.id == old.day_id); + if (prevDayIdx === -1) { + const prevDay = this.trip.days[prevDayIdx]; + const prevItemIdx = prevDay.items.findIndex((i) => i.id == updated.id); + if (prevItemIdx != -1) prevDay.items.splice(prevItemIdx, 1); + this.dayStatsCache.delete(old.day_id); + } + } + + const dayIdx = this.trip.days.findIndex((d) => d.id == updated.day_id); + if (dayIdx != -1) { + const day = this.trip.days[dayIdx]; + const itemIdx = day.items.findIndex((i) => i.id === updated.id); + if (itemIdx !== -1) { + day.items[itemIdx] = updated; + } + } + + this.flattenTripDayItems(); + + if (this.selectedItem && this.selectedItem.id === old.id) + this.selectedItem = { + ...updated, + status: updated.status + ? this.statusToTripStatus(updated.status as string) + : undefined, + }; + this.dayStatsCache.delete(updated.day_id); + this.computePlacesUsedInTable(); + + const updatedPrice = (updated.price || 0) - (old.price || 0); + this.updateTotalPrice(updatedPrice); + + if (this.tripMapAntLayerDayID == updated.day_id) + this.toggleTripDayHighlightPathDay(updated.day_id); + + if (updated.place?.id || old.place?.id) this.setPlacesAndMarkers(); + } + + removeItemFromTrip(item: TripItem): void { + if (!this.trip) return; + const dayIndex = this.trip.days.findIndex((d) => d.id === item.day_id); + if (dayIndex === -1) return; + + const day = this.trip.days[dayIndex]; + const itemIndex = day.items.findIndex((i) => i.id === item.id); + if (itemIndex != -1) { + day.items.splice(itemIndex, 1); + this.flattenTripDayItems(); + } + + if (item.place?.id) { + this.placesUsedInTable.delete(item.place.id); + if (item.place.price) this.updateTotalPrice(-item.place.price); + this.setPlacesAndMarkers(); + } + this.dayStatsCache.delete(item.day_id); + this.selectedItem = undefined; + this.resetPlaceHighlightMarker(); + } }