From ad350f0a2a91bc77321563e4089cb4b4a6ac9175 Mon Sep 17 00:00:00 2001 From: itskovacs Date: Sat, 16 Aug 2025 16:23:28 +0200 Subject: [PATCH] :sparkles: Trip: apply changes to shared trips --- .../shared-trip/shared-trip.component.html | 168 ++++++++++++------ .../shared-trip/shared-trip.component.ts | 108 ++++++++++- src/src/app/services/api.service.ts | 6 + 3 files changed, 222 insertions(+), 60 deletions(-) diff --git a/src/src/app/components/shared-trip/shared-trip.component.html b/src/src/app/components/shared-trip/shared-trip.component.html index 869bfd2..69e96a1 100644 --- a/src/src/app/components/shared-trip/shared-trip.component.html +++ b/src/src/app/components/shared-trip/shared-trip.component.html @@ -14,6 +14,8 @@
itskovacs/trip
+ {{ (totalPrice | number:'1.0-2') || '-' }} {{ currency$ | async }} @@ -22,7 +24,8 @@
-
+
@@ -32,14 +35,17 @@
@@ -49,20 +55,31 @@
+ @if (isFilteringMode) { +
+ + + +
+ } + @defer { @if (flattenedTripItems.length) { + [rowGroupMode]="tableExpandableMode ? 'subheader': 'rowspan'" groupRowsBy="td_label" + [resizableColumns]="tableExpandableMode"> - Day - Time - Text - Place - Comment - LatLng - Price - Status + @if (!tableExpandableMode && tripTableSelectedColumns.includes('day')) {Day + } + @if (tripTableSelectedColumns.includes('time')) {Time} + @if (tripTableSelectedColumns.includes('text')) {Text} + @if (tripTableSelectedColumns.includes('place')) {Place} + @if (tripTableSelectedColumns.includes('comment')) {Comment} + @if (tripTableSelectedColumns.includes('LatLng')) {LatLng} + @if (tripTableSelectedColumns.includes('price')) {Price} + @if (tripTableSelectedColumns.includes('status')) {Status} @if (tableExpandableMode) { @@ -83,37 +100,41 @@ - {{ tripitem.td_label }} - {{ tripitem.time }} - -
- @if (tripitem.status) {
{{ tripitem.time }}} + @if (tripTableSelectedColumns.includes('text')) { +
+ @if (tripitem.status) {
} {{ tripitem.text }}
- - + } + @if (tripTableSelectedColumns.includes('place')) { @if (tripitem.place) { -
+
{{ tripitem.place.name }}
} @else {-} - - {{ tripitem.comment || '-' }} - -
+ } + @if (tripTableSelectedColumns.includes('comment')) { +
+ {{ tripitem.comment || '-' }} +
+ } + @if (tripTableSelectedColumns.includes('LatLng')) { +
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} } @else {-}
- - @if (tripitem.price) {} + @if (tripTableSelectedColumns.includes('price')) {@if (tripitem.price) {{{ - tripitem.price }} {{ currency$ | async }}} - @if (tripitem.status) {{{ - tripitem.status.label }}} + tripitem.price }} {{ currency$ | async }}}} + @if (tripTableSelectedColumns.includes('status')) {@if (tripitem.status) {{{ + tripitem.status.label }}}} } @@ -121,22 +142,22 @@ - @if (rowgroup) { + @if (tripTableSelectedColumns.includes('day') && rowgroup) {
{{tripitem.td_label }}
} - {{ tripitem.time }} - -
- {{ tripitem.text }} - @if (tripitem.status) {
{{ tripitem.time }}} + @if (tripTableSelectedColumns.includes('text')) { +
+ @if (tripitem.status) {
} + {{ tripitem.text }}
- - + } + @if (tripTableSelectedColumns.includes('place')) { @if (tripitem.place) {
} @else {-} - - {{ tripitem.comment || '-' }} - + } + @if (tripTableSelectedColumns.includes('comment')) { +
+ {{ tripitem.comment || '-' }} +
+ } + @if (tripTableSelectedColumns.includes('LatLng')) {
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} } @else {-}
- - @if (tripitem.price) {} + @if (tripTableSelectedColumns.includes('price')) {@if (tripitem.price) {{{ - tripitem.price }} {{ currency$ | async }}} - @if (tripitem.status) {{{ - tripitem.status.label }}} + tripitem.price }} {{ currency$ | async }}}} + @if (tripTableSelectedColumns.includes('status')) {@if (tripitem.status) {{{ + tripitem.status.label }}}} } @@ -181,7 +207,8 @@ }
-
+
@if (selectedItem) {
@@ -263,8 +290,9 @@
- - + +
@@ -297,7 +325,7 @@
@if (!collapsedTripPlaces) { -
+
@defer { @for (p of places; track p.id) {
@if (!collapsedTripDays) { -
+
@defer { @for (d of trip.days; track d.id) {
@@ -399,7 +427,7 @@
@if (!collapsedTripStatuses) { -
+
@defer { @for (item of getWatchlistData; track item.id) {
@@ -451,4 +479,30 @@
} -} \ No newline at end of file +} + + +
+
+ @for (c of dispPackingList | keyvalue; track c.key) { +
{{ c.key }}
+ +
+ @for (item of c.value; track item.id) { +
+ +
+ } +
+ } +
+
+
\ No newline at end of file diff --git a/src/src/app/components/shared-trip/shared-trip.component.ts b/src/src/app/components/shared-trip/shared-trip.component.ts index 762daeb..577ec8d 100644 --- a/src/src/app/components/shared-trip/shared-trip.component.ts +++ b/src/src/app/components/shared-trip/shared-trip.component.ts @@ -11,6 +11,7 @@ import { TripDay, TripItem, TripStatus, + PackingItem, } from "../../types/trip"; import { Place } from "../../types/poi"; import { @@ -20,29 +21,45 @@ import { tripDayMarker, } from "../../shared/map"; import { ActivatedRoute } from "@angular/router"; -import { Observable, take, tap } from "rxjs"; +import { debounceTime, Observable, take, tap } from "rxjs"; import { UtilsService } from "../../services/utils.service"; -import { AsyncPipe, DecimalPipe } from "@angular/common"; +import { AsyncPipe, CommonModule, DecimalPipe } from "@angular/common"; import { MenuItem } from "primeng/api"; import { MenuModule } from "primeng/menu"; import { LinkifyPipe } from "../../shared/linkify.pipe"; +import { TooltipModule } from "primeng/tooltip"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { MultiSelectModule } from "primeng/multiselect"; +import { DialogModule } from "primeng/dialog"; +import { CheckboxModule } from "primeng/checkbox"; +import { InputTextModule } from "primeng/inputtext"; @Component({ selector: "app-shared-trip", standalone: true, imports: [ + CommonModule, SkeletonModule, MenuModule, LinkifyPipe, TableModule, ButtonModule, DecimalPipe, + TooltipModule, + DialogModule, + ReactiveFormsModule, + InputTextModule, + FormsModule, + MultiSelectModule, + CheckboxModule, AsyncPipe, ], templateUrl: "./shared-trip.component.html", styleUrls: ["./shared-trip.component.scss"], }) export class SharedTripComponent implements AfterViewInit { + token?: string; currency$: Observable; statuses: TripStatus[] = []; trip?: Trip; @@ -56,6 +73,11 @@ export class SharedTripComponent implements AfterViewInit { collapsedTripDays = false; collapsedTripPlaces = false; collapsedTripStatuses = false; + packingDialogVisible = false; + isExpanded = false; + isFilteringMode = false; + packingList: PackingItem[] = []; + dispPackingList: Record = {}; map?: L.Map; markerClusterGroup?: L.MarkerClusterGroup; @@ -103,6 +125,13 @@ export class SharedTripComponent implements AfterViewInit { this.tripToNavigation(); }, }, + { + label: "Filter", + icon: "pi pi-filter", + command: () => { + this.toggleFiltering(); + }, + }, { label: "Expand / Group", icon: "pi pi-arrow-down-left-and-arrow-up-right-to-center", @@ -120,6 +149,24 @@ export class SharedTripComponent implements AfterViewInit { ], }, ]; + readonly tripTableColumns: string[] = [ + "day", + "time", + "text", + "place", + "comment", + "LatLng", + "price", + "status", + ]; + tripTableSelectedColumns: string[] = [ + "day", + "time", + "text", + "place", + "comment", + ]; + tripTableSearchInput = new FormControl(""); dayStatsCache = new Map(); placesUsedInTable = new Set(); @@ -131,6 +178,14 @@ export class SharedTripComponent implements AfterViewInit { ) { this.currency$ = this.utilsService.currency$; this.statuses = this.utilsService.statuses; + this.tripTableSearchInput.valueChanges + .pipe(takeUntilDestroyed(), debounceTime(300)) + .subscribe({ + next: (value) => { + if (value) this.flattenTripDayItems(value.toLowerCase()); + else this.flattenTripDayItems(); + }, + }); } ngAfterViewInit(): void { @@ -140,6 +195,7 @@ export class SharedTripComponent implements AfterViewInit { tap((params) => { const token = params.get("token"); if (token) { + this.token = token; this.loadTripData(token); } }), @@ -193,6 +249,11 @@ export class SharedTripComponent implements AfterViewInit { this.trip?.days.sort((a, b) => a.label.localeCompare(b.label)); } + toggleFiltering() { + this.isFilteringMode = !this.isFilteringMode; + if (!this.isFilteringMode) this.flattenTripDayItems(); + } + toGithub() { this.utilsService.toGithubTRIP(); } @@ -236,10 +297,17 @@ export class SharedTripComponent implements AfterViewInit { return this.statuses.find((s) => s.label == status); } - flattenTripDayItems() { + flattenTripDayItems(searchValue?: string) { this.sortTripDays(); this.flattenedTripItems = this.trip!.days.flatMap((day) => [...day.items] + .filter((item) => + searchValue + ? item.text.toLowerCase().includes(searchValue) || + item.place?.name.toLowerCase().includes(searchValue) || + item.comment?.toLowerCase().includes(searchValue) + : true, + ) .sort((a, b) => a.time.localeCompare(b.time)) .map((item) => ({ td_id: day.id, @@ -617,4 +685,38 @@ export class SharedTripComponent implements AfterViewInit { const url = `https://www.google.com/maps/dir/${waypoints}`; window.open(url, "_blank"); } + + openPackingList() { + if (!this.token) return; + + if (!this.packingList.length) + this.apiService + .getSharedTripPackingList(this.token) + .pipe(take(1)) + .subscribe({ + next: (items) => { + this.packingList = [...items]; + this.computeDispPackingList(); + }, + }); + this.packingDialogVisible = true; + } + + computeDispPackingList() { + const sorted: PackingItem[] = [...this.packingList].sort((a, b) => + a.packed !== b.packed + ? a.packed + ? 1 + : -1 + : a.text.localeCompare(b.text), + ); + + this.dispPackingList = sorted.reduce>( + (acc, item) => { + (acc[item.category] ??= []).push(item); + return acc; + }, + {}, + ); + } } diff --git a/src/src/app/services/api.service.ts b/src/src/app/services/api.service.ts index 7c41071..88a9074 100644 --- a/src/src/app/services/api.service.ts +++ b/src/src/app/services/api.service.ts @@ -239,6 +239,12 @@ export class ApiService { ); } + getSharedTripPackingList(token: string): Observable { + return this.httpClient.get( + `${this.apiBaseUrl}/trips/shared/${token}/packing`, + ); + } + postPackingItem( trip_id: number, p_item: PackingItem,