Trip performance and code improvements

This commit is contained in:
itskovacs 2025-08-06 21:10:34 +02:00
parent 39d69199fe
commit 0b7af7ce99

View File

@ -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 { 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 { 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 { 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 { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.component";
import { UtilsService } from "../../services/utils.service"; import { UtilsService } from "../../services/utils.service";
import { TripCreateModalComponent } from "../../modals/trip-create-modal/trip-create-modal.component"; 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 { MenuModule } from "primeng/menu";
import { LinkifyPipe } from "../../shared/linkify.pipe"; import { LinkifyPipe } from "../../shared/linkify.pipe";
import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place-create-modal.component"; import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place-create-modal.component";
import { Settings } from "../../types/settings";
@Component({ @Component({
selector: "app-trip", selector: "app-trip",
@ -57,44 +65,26 @@ import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place
styleUrls: ["./trip.component.scss"], styleUrls: ["./trip.component.scss"],
}) })
export class TripComponent implements AfterViewInit { export class TripComponent implements AfterViewInit {
map: any;
markerClusterGroup: any;
selectedItem: (TripItem & { status?: TripStatus }) | undefined;
statuses: TripStatus[] = [];
hoveredElement: HTMLElement | undefined;
currency$: Observable<string>; currency$: Observable<string>;
placesUsedInTable = new Set<number>(); statuses: TripStatus[] = [];
trip?: Trip;
trip: Trip | undefined;
tripMapAntLayer: L.LayerGroup<any> | undefined;
tripMapAntLayerDayID: number | undefined;
isMapFullscreen: boolean = false;
totalPrice: number = 0;
dayStatsCache = new Map<number, { price: number; places: number }>();
places: Place[] = []; places: Place[] = [];
flattenedTripItems: FlattenedTripItem[] = []; flattenedTripItems: FlattenedTripItem[] = [];
menuTripActionsItems: MenuItem[] = []; selectedItem?: TripItem & { status?: TripStatus };
menuTripDayActionsItems: MenuItem[] = []; isMapFullscreen = false;
selectedTripDayForMenu: TripDay | undefined; totalPrice = 0;
collapsedTripDays = false;
collapsedTripPlaces = false;
collapsedTripStatuses = false;
collapsedTripPlaces: boolean = false; map?: L.Map;
collapsedTripDays: boolean = false; markerClusterGroup?: L.MarkerClusterGroup;
collapsedTripStatuses: boolean = false; hoveredElement?: HTMLElement;
tripMapAntLayer?: L.LayerGroup<any>;
tripMapAntLayerDayID?: number;
constructor( readonly menuTripActionsItems: MenuItem[] = [
private apiService: ApiService,
private router: Router,
private dialogService: DialogService,
private utilsService: UtilsService,
private route: ActivatedRoute,
) {
this.currency$ = this.utilsService.currency$;
this.statuses = this.utilsService.statuses;
this.menuTripActionsItems = [
{ {
label: "Actions", label: "Actions",
items: [ items: [
@ -125,7 +115,7 @@ export class TripComponent implements AfterViewInit {
], ],
}, },
]; ];
this.menuTripDayActionsItems = [ readonly menuTripDayActionsItems: MenuItem[] = [
{ {
label: "Actions", label: "Actions",
items: [ items: [
@ -157,34 +147,53 @@ export class TripComponent implements AfterViewInit {
], ],
}, },
]; ];
} selectedTripDayForMenu?: TripDay;
back() { dayStatsCache = new Map<number, { price: number; places: number }>();
this.router.navigateByUrl("/trips"); placesUsedInTable = new Set<number>();
}
printTable() { constructor(
this.selectedItem = undefined; private apiService: ApiService,
setTimeout(() => { private router: Router,
window.print(); private dialogService: DialogService,
}, 30); private utilsService: UtilsService,
private route: ActivatedRoute,
) {
this.currency$ = this.utilsService.currency$;
this.statuses = this.utilsService.statuses;
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.route.paramMap.subscribe((params) => { this.route.paramMap
.pipe(
take(1),
tap((params) => {
const id = params.get("id"); const id = params.get("id");
if (id) { if (id) this.loadTripData(+id);
}),
)
.subscribe();
}
loadTripData(id: number): void {
combineLatest({ combineLatest({
trip: this.apiService.getTrip(+id), trip: this.apiService.getTrip(+id),
settings: this.apiService.getSettings(), settings: this.apiService.getSettings(),
}) })
.pipe( .pipe(
take(1),
tap(({ trip, settings }) => { tap(({ trip, settings }) => {
this.trip = trip; this.trip = trip;
this.flattenTripDayItems(); this.flattenTripDayItems();
this.updateTotalPrice(); this.updateTotalPrice();
this.setupMap(settings);
}),
)
.subscribe();
}
let contentMenuItems = [ setupMap(settings: Settings): void {
const contentMenuItems = [
{ {
text: "Copy coordinates", text: "Copy coordinates",
callback: (e: any) => { callback: (e: any) => {
@ -199,13 +208,19 @@ export class TripComponent implements AfterViewInit {
this.markerClusterGroup = createClusterGroup().addTo(this.map); this.markerClusterGroup = createClusterGroup().addTo(this.map);
this.setPlacesAndMarkers(); this.setPlacesAndMarkers();
this.map.setView([48.107, -2.988]); this.map.setView([settings.map_lat, settings.map_lng]);
this.resetMapBounds(); this.resetMapBounds();
}),
)
.subscribe();
} }
});
back() {
this.router.navigateByUrl("/trips");
}
printTable() {
this.selectedItem = undefined;
setTimeout(() => {
window.print();
}, 30);
} }
sortTripDays() { sortTripDays() {
@ -213,9 +228,7 @@ export class TripComponent implements AfterViewInit {
} }
getDayStats(day: TripDay): { price: number; places: number } { getDayStats(day: TripDay): { price: number; places: number } {
if (this.dayStatsCache.has(day.id)) { if (this.dayStatsCache.has(day.id)) return this.dayStatsCache.get(day.id)!;
return this.dayStatsCache.get(day.id)!;
}
const stats = day.items.reduce( const stats = day.items.reduce(
(acc, item) => { (acc, item) => {
@ -225,7 +238,6 @@ export class TripComponent implements AfterViewInit {
}, },
{ price: 0, places: 0 }, { price: 0, places: 0 },
); );
this.dayStatsCache.set(day.id, stats); this.dayStatsCache.set(day.id, stats);
return stats; return stats;
} }
@ -233,14 +245,13 @@ export class TripComponent implements AfterViewInit {
get getWatchlistData(): (TripItem & { status: TripStatus })[] { get getWatchlistData(): (TripItem & { status: TripStatus })[] {
if (!this.trip?.days) return []; if (!this.trip?.days) return [];
const data = this.trip!.days.map((day) => return this.trip.days
.flatMap((day) =>
day.items.filter((item) => day.items.filter((item) =>
["constraint", "pending"].includes(item.status as string), ["constraint", "pending"].includes(item.status as string),
), ),
).flat(); )
if (!data.length) return []; .map((item) => ({
return data.map((item) => ({
...item, ...item,
status: this.statusToTripStatus(item.status as string), status: this.statusToTripStatus(item.status as string),
})) as (TripItem & { status: TripStatus })[]; })) as (TripItem & { status: TripStatus })[];
@ -252,7 +263,7 @@ export class TripComponent implements AfterViewInit {
statusToTripStatus(status?: string): TripStatus | undefined { statusToTripStatus(status?: string): TripStatus | undefined {
if (!status) return undefined; if (!status) return undefined;
return this.statuses.find((s) => s.label == status) as TripStatus; return this.statuses.find((s) => s.label == status);
} }
flattenTripDayItems() { flattenTripDayItems() {
@ -279,16 +290,16 @@ export class TripComponent implements AfterViewInit {
computePlacesUsedInTable() { computePlacesUsedInTable() {
this.placesUsedInTable.clear(); this.placesUsedInTable.clear();
this.flattenedTripItems.forEach((i) => { this.flattenedTripItems.forEach((item) => {
if (i.place?.id) this.placesUsedInTable.add(i.place.id); if (item.place?.id) this.placesUsedInTable.add(item.place.id);
}); });
} }
setPlacesAndMarkers() { setPlacesAndMarkers() {
this.computePlacesUsedInTable(); this.computePlacesUsedInTable();
this.places = this.trip?.places || []; this.places = [...(this.trip?.places ?? [])].sort((a, b) =>
this.places.sort((a, b) => a.name.localeCompare(b.name)); a.name.localeCompare(b.name),
);
this.markerClusterGroup?.clearLayers(); this.markerClusterGroup?.clearLayers();
this.places.forEach((p) => { this.places.forEach((p) => {
const marker = placeToMarker(p, false, !this.placesUsedInTable.has(p.id)); const marker = placeToMarker(p, false, !this.placesUsedInTable.has(p.id));
@ -298,7 +309,7 @@ export class TripComponent implements AfterViewInit {
resetMapBounds() { 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]),
{ padding: [30, 30] }, { padding: [30, 30] },
); );
@ -309,14 +320,16 @@ export class TripComponent implements AfterViewInit {
document.body.classList.toggle("overflow-hidden"); document.body.classList.toggle("overflow-hidden");
setTimeout(() => { setTimeout(() => {
this.map.invalidateSize(); this.map?.invalidateSize();
this.resetMapBounds(); this.resetMapBounds();
}, 50); }, 50);
} }
updateTotalPrice(n?: number) { updateTotalPrice(n?: number) {
if (n) this.totalPrice += n; if (n) {
else this.totalPrice += n;
return;
}
this.totalPrice = this.totalPrice =
this.trip?.days this.trip?.days
.flatMap((d) => d.items) .flatMap((d) => d.items)
@ -327,17 +340,13 @@ export class TripComponent implements AfterViewInit {
} }
resetPlaceHighlightMarker() { resetPlaceHighlightMarker() {
if (this.hoveredElement) { if (!this.hoveredElement) return;
this.hoveredElement.classList.remove("listHover"); this.hoveredElement.classList.remove("listHover");
this.hoveredElement = undefined; this.hoveredElement = undefined;
} }
}
placeHighlightMarker(lat: number, lng: number) { placeHighlightMarker(lat: number, lng: number) {
if (this.hoveredElement) { this.resetPlaceHighlightMarker();
this.hoveredElement.classList.remove("listHover");
this.hoveredElement = undefined;
}
let marker: L.Marker | undefined; let marker: L.Marker | undefined;
this.markerClusterGroup?.eachLayer((layer: any) => { this.markerClusterGroup?.eachLayer((layer: any) => {
@ -347,14 +356,13 @@ export class TripComponent implements AfterViewInit {
}); });
if (!marker) return; 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) { if (markerElement) {
// marker, not clustered // marker, not clustered
markerElement.classList.add("listHover"); markerElement.classList.add("listHover");
this.hoveredElement = markerElement; this.hoveredElement = markerElement;
} else { } else {
// marker , clustered // marker is clustered
const parentCluster = (this.markerClusterGroup as any).getVisibleParent( const parentCluster = (this.markerClusterGroup as any).getVisibleParent(
marker, marker,
); );
@ -368,37 +376,11 @@ export class TripComponent implements AfterViewInit {
} }
} }
toggleTripDaysHighlight() { highlightTripPath(
if (this.tripMapAntLayerDayID == -1) { items: { text: string; lat: number; lng: number; isPlace: boolean }[],
this.map.removeLayer(this.tripMapAntLayer); layerId: number,
this.tripMapAntLayerDayID = undefined; antDelay = 400,
this.resetMapBounds(); ): void {
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);
if (items.length < 2) { if (items.length < 2) {
this.utilsService.toast( this.utilsService.toast(
"info", "info",
@ -408,7 +390,7 @@ export class TripComponent implements AfterViewInit {
return; return;
} }
this.map.fitBounds( this.map?.fitBounds(
items.map((c) => [c.lat, c.lng]), items.map((c) => [c.lat, c.lng]),
{ padding: [30, 30] }, { padding: [30, 30] },
); );
@ -416,7 +398,7 @@ export class TripComponent implements AfterViewInit {
const path = antPath( const path = antPath(
items.map((c) => [c.lat, c.lng]), items.map((c) => [c.lat, c.lng]),
{ {
delay: 600, delay: antDelay,
dashArray: [10, 20], dashArray: [10, 20],
weight: 5, weight: 5,
color: "#0000FF", color: "#0000FF",
@ -434,34 +416,65 @@ export class TripComponent implements AfterViewInit {
}); });
if (this.tripMapAntLayer) { if (this.tripMapAntLayer) {
this.map.removeLayer(this.tripMapAntLayer); this.map?.removeLayer(this.tripMapAntLayer);
this.tripMapAntLayerDayID = undefined; this.tripMapAntLayerDayID = undefined;
} }
// UX
setTimeout(() => { setTimeout(() => {
layGroup.addTo(this.map); layGroup.addTo(this.map!);
}, 200); }, 200);
this.tripMapAntLayer = layGroup; 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) { toggleTripDayHighlightPathDay(day_id: number) {
// Click on the currently displayed day: remove // Click on the currently displayed day: remove
if (this.tripMapAntLayerDayID == day_id) { if (this.tripMapAntLayerDayID == day_id) {
this.map.removeLayer(this.tripMapAntLayer); this.map?.removeLayer(this.tripMapAntLayer!);
this.tripMapAntLayerDayID = undefined; this.tripMapAntLayerDayID = undefined;
this.resetMapBounds(); this.resetMapBounds();
return; return;
} }
let index = this.trip?.days.findIndex((d) => d.id === day_id); const idx = this.trip?.days.findIndex((d) => d.id === day_id);
if (!this.trip || index == -1) return; if (!this.trip || idx === undefined || idx == -1) return;
const data = this.trip.days[idx].items.sort((a, b) =>
const data = this.trip.days[index as number].items; a.time.localeCompare(b.time),
);
data.sort((a, b) => a.time.localeCompare(b.time));
const items = data const items = data
.map((item) => { .map((item) => {
if (item.lat && item.lng) if (item.lat && item.lng)
@ -482,52 +495,7 @@ export class TripComponent implements AfterViewInit {
}) })
.filter((n) => n !== undefined); .filter((n) => n !== undefined);
if (items.length < 2) { this.highlightTripPath(items, day_id);
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;
} }
onRowClick(item: FlattenedTripItem) { onRowClick(item: FlattenedTripItem) {
@ -552,10 +520,13 @@ export class TripComponent implements AfterViewInit {
data: `Delete ${this.trip?.name} ? This will delete everything.`, data: `Delete ${this.trip?.name} ? This will delete everything.`,
}); });
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (bool) => { next: (bool) => {
if (bool) if (bool)
this.apiService.deleteTrip(this.trip?.id!).subscribe({ this.apiService
.deleteTrip(this.trip?.id!)
.pipe(take(1))
.subscribe({
next: () => { next: () => {
this.router.navigateByUrl("/trips"); this.router.navigateByUrl("/trips");
}, },
@ -581,11 +552,14 @@ export class TripComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (new_trip: Trip | null) => { next: (new_trip: Trip | null) => {
if (!new_trip) return; if (!new_trip) return;
this.apiService.putTrip(new_trip, this.trip?.id!).subscribe({ this.apiService
.putTrip(new_trip, this.trip?.id!)
.pipe(take(1))
.subscribe({
next: (trip: Trip) => (this.trip = trip), 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."}`, 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) => { next: (bool) => {
if (bool) if (bool)
this.apiService this.apiService
.putTrip({ archived: !currentArchiveStatus }, this.trip?.id!) .putTrip({ archived: !currentArchiveStatus }, this.trip?.id!)
.pipe(take(1))
.subscribe({ .subscribe({
next: () => { next: () => {
this.trip!.archived = !currentArchiveStatus; this.trip!.archived = !currentArchiveStatus;
@ -620,6 +595,8 @@ export class TripComponent implements AfterViewInit {
} }
addDay() { addDay() {
if (!this.trip) return;
const modal: DynamicDialogRef = this.dialogService.open( const modal: DynamicDialogRef = this.dialogService.open(
TripCreateDayModalComponent, TripCreateDayModalComponent,
{ {
@ -629,20 +606,23 @@ export class TripComponent implements AfterViewInit {
closable: true, closable: true,
dismissableMask: true, dismissableMask: true,
width: "50vw", width: "50vw",
data: { days: this.trip?.days }, data: { days: this.trip.days },
breakpoints: { breakpoints: {
"640px": "80vw", "640px": "80vw",
}, },
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (day: TripDay | null) => { next: (day: TripDay | null) => {
if (!day) return; if (!day) return;
this.apiService.postTripDay(day, this.trip?.id!).subscribe({ this.apiService
.postTripDay(day, this.trip?.id!)
.pipe(take(1))
.subscribe({
next: (day) => { next: (day) => {
this.trip?.days.push(day); this.trip!.days.push(day);
this.flattenTripDayItems(); this.flattenTripDayItems();
}, },
}); });
@ -651,6 +631,8 @@ export class TripComponent implements AfterViewInit {
} }
editDay(day: TripDay) { editDay(day: TripDay) {
if (!this.trip) return;
const modal: DynamicDialogRef = this.dialogService.open( const modal: DynamicDialogRef = this.dialogService.open(
TripCreateDayModalComponent, TripCreateDayModalComponent,
{ {
@ -660,22 +642,25 @@ export class TripComponent implements AfterViewInit {
closable: true, closable: true,
dismissableMask: true, dismissableMask: true,
width: "50vw", width: "50vw",
data: { day: day, days: this.trip?.days }, data: { day: day, days: this.trip.days },
breakpoints: { breakpoints: {
"640px": "80vw", "640px": "80vw",
}, },
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (day: TripDay | null) => { next: (day: TripDay | null) => {
if (!day) return; if (!day) return;
this.apiService.putTripDay(day, this.trip?.id!).subscribe({ this.apiService
.putTripDay(day, this.trip?.id!)
.pipe(take(1))
.subscribe({
next: (day) => { next: (day) => {
let index = this.trip?.days.findIndex((d) => d.id == day.id); const idx = this.trip!.days.findIndex((d) => d.id == day.id);
if (index != -1) { if (idx != -1) {
this.trip?.days.splice(index as number, 1, day); this.trip?.days.splice(idx, 1, day);
this.flattenTripDayItems(); this.flattenTripDayItems();
} }
}, },
@ -685,6 +670,8 @@ export class TripComponent implements AfterViewInit {
} }
deleteDay(day: TripDay) { deleteDay(day: TripDay) {
if (!this.trip) return;
const modal = this.dialogService.open(YesNoModalComponent, { const modal = this.dialogService.open(YesNoModalComponent, {
header: "Confirm deletion", header: "Confirm deletion",
modal: true, modal: true,
@ -696,15 +683,20 @@ export class TripComponent implements AfterViewInit {
data: `Delete ${day.label} ? This will delete everything for this day.`, data: `Delete ${day.label} ? This will delete everything for this day.`,
}); });
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (bool) => { next: (bool) => {
if (bool) if (bool)
this.apiService.deleteTripDay(this.trip?.id!, day.id).subscribe({ this.apiService
.deleteTripDay(this.trip?.id!, day.id)
.pipe(take(1))
.subscribe({
next: () => { next: () => {
let index = this.trip?.days.findIndex((d) => d.id == day.id); const idx = this.trip!.days.findIndex((d) => d.id == day.id);
this.trip?.days.splice(index as number, 1); if (idx != -1) {
this.trip!.days.splice(idx, 1);
this.flattenTripDayItems(); this.flattenTripDayItems();
this.setPlacesAndMarkers(); this.setPlacesAndMarkers();
}
}, },
}); });
}, },
@ -712,6 +704,8 @@ export class TripComponent implements AfterViewInit {
} }
manageTripPlaces() { manageTripPlaces() {
if (!this.trip) return;
const modal: DynamicDialogRef = this.dialogService.open( const modal: DynamicDialogRef = this.dialogService.open(
TripPlaceSelectModalComponent, TripPlaceSelectModalComponent,
{ {
@ -728,12 +722,13 @@ export class TripComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (places: Place[] | null) => { next: (places: Place[] | null) => {
if (!places) return; if (!places) return;
this.apiService 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({ .subscribe({
next: (trip) => { next: (trip) => {
this.trip = trip; this.trip = trip;
@ -746,6 +741,8 @@ export class TripComponent implements AfterViewInit {
} }
addItem(day_id?: number) { addItem(day_id?: number) {
if (!this.trip) return;
const modal: DynamicDialogRef = this.dialogService.open( const modal: DynamicDialogRef = this.dialogService.open(
TripCreateDayItemModalComponent, TripCreateDayItemModalComponent,
{ {
@ -760,22 +757,25 @@ export class TripComponent implements AfterViewInit {
}, },
data: { data: {
places: this.places, places: this.places,
days: this.trip?.days, days: this.trip.days,
selectedDay: day_id, selectedDay: day_id,
}, },
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (item: TripItem | null) => { next: (item: TripItem | null) => {
if (!item) return; if (!item) return;
this.apiService this.apiService
.postTripDayItem(item, this.trip?.id!, item.day_id) .postTripDayItem(item, this.trip!.id!, item.day_id)
.pipe(take(1))
.subscribe({ .subscribe({
next: (resp) => { next: (resp) => {
let index = this.trip?.days.findIndex((d) => d.id == item.day_id); const idx = this.trip!.days.findIndex((d) => d.id == item.day_id);
let td: TripDay = this.trip?.days[index as number]!; if (idx === -1) return;
const td: TripDay = this.trip!.days[idx];
td.items.push(resp); td.items.push(resp);
this.flattenTripDayItems(); this.flattenTripDayItems();
@ -792,6 +792,8 @@ export class TripComponent implements AfterViewInit {
} }
editItem(item: TripItem) { editItem(item: TripItem) {
if (!this.trip) return;
const modal: DynamicDialogRef = this.dialogService.open( const modal: DynamicDialogRef = this.dialogService.open(
TripCreateDayItemModalComponent, TripCreateDayItemModalComponent,
{ {
@ -815,57 +817,16 @@ export class TripComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (it: TripItem | null) => { next: (updated: TripItem | null) => {
if (!it) return; if (!updated) return;
if (item.place?.id) this.placesUsedInTable.delete(item.place.id); if (item.place?.id) this.placesUsedInTable.delete(item.place.id);
this.apiService 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({ .subscribe({
next: (new_item) => { next: (new_item) => this.updateItemFromTrip(item, 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();
},
}); });
}, },
}); });
@ -883,33 +844,15 @@ export class TripComponent implements AfterViewInit {
data: `Delete ${item.text.substring(0, 50)} ? This will delete everything for this day.`, 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) => { next: (bool) => {
if (bool) if (!bool) return;
this.apiService this.apiService
.deleteTripDayItem(this.trip?.id!, item.day_id, item.id) .deleteTripDayItem(this.trip?.id!, item.day_id, item.id)
.pipe(take(1))
.subscribe({ .subscribe({
next: () => { next: () => {
let index = this.trip?.days.findIndex( this.removeItemFromTrip(item);
(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();
}, },
}); });
}, },
@ -917,6 +860,8 @@ export class TripComponent implements AfterViewInit {
} }
addItems() { addItems() {
if (!this.trip) return;
const modal: DynamicDialogRef = this.dialogService.open( const modal: DynamicDialogRef = this.dialogService.open(
TripCreateItemsModalComponent, TripCreateItemsModalComponent,
{ {
@ -929,29 +874,28 @@ export class TripComponent implements AfterViewInit {
breakpoints: { breakpoints: {
"1260px": "90vw", "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) => { next: (items: TripItem[] | null) => {
if (!items?.length) return; if (!items?.length) return;
const day_id = items[0].day_id; const day_id = items[0].day_id;
const obs$ = items.map((item) => 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$) forkJoin(obs$).subscribe({
.pipe( next: (items: TripItem[]) => {
map((items) => { const index = this.trip!.days.findIndex((d) => d.id == day_id);
let index = this.trip?.days.findIndex((d) => d.id == day_id); if (index === -1) return;
let td: TripDay = this.trip?.days[index as number]!;
const td: TripDay = this.trip!.days[index]!;
td.items.push(...items); td.items.push(...items);
this.flattenTripDayItems(); this.flattenTripDayItems();
}), },
) });
.subscribe();
}, },
}); });
} }
@ -973,16 +917,20 @@ export class TripComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (place: Place | null) => { next: (place: Place | null) => {
if (!place) return; if (!place) return;
this.apiService.postPlace(place).subscribe({
next: (place: Place) => {
this.apiService this.apiService
.putTrip( .postPlace(place)
{ place_ids: [place, ...this.places].map((p) => p.id) }, .pipe(
switchMap((createdPlace: Place) =>
this.apiService.putTrip(
{ place_ids: [createdPlace, ...this.places].map((p) => p.id) },
this.trip?.id!, this.trip?.id!,
),
),
take(1),
) )
.subscribe({ .subscribe({
next: (trip) => { next: (trip) => {
@ -993,7 +941,70 @@ export class TripComponent implements AfterViewInit {
}); });
}, },
}); });
}, }
});
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();
} }
} }