Trip: table filtering

This commit is contained in:
itskovacs 2025-08-16 16:15:32 +02:00
parent adc7b80ecf
commit 0fdd149dc9
2 changed files with 118 additions and 46 deletions

View File

@ -60,6 +60,7 @@
<div class="hidden md:flex items-center gap-2"> <div class="hidden md:flex items-center gap-2">
<p-button pTooltip="Expand table" class="hidden lg:flex" icon="pi pi-arrows-h" <p-button pTooltip="Expand table" class="hidden lg:flex" icon="pi pi-arrows-h"
(click)="isExpanded = !isExpanded" text /> (click)="isExpanded = !isExpanded" text />
<p-button pTooltip="Show filters" icon="pi pi-filter" (click)="toggleFiltering()" text />
<p-button [pTooltip]="tableExpandableMode ? 'f' : 'Switch table mode, allow column resizing'" <p-button [pTooltip]="tableExpandableMode ? 'f' : 'Switch table mode, allow column resizing'"
[icon]="tableExpandableMode ? 'pi pi-arrow-up-right-and-arrow-down-left-from-center' : 'pi pi-arrow-down-left-and-arrow-up-right-to-center'" [icon]="tableExpandableMode ? 'pi pi-arrow-up-right-and-arrow-down-left-from-center' : 'pi pi-arrow-down-left-and-arrow-up-right-to-center'"
(click)="tableExpandableMode = !tableExpandableMode" text /> (click)="tableExpandableMode = !tableExpandableMode" text />
@ -79,20 +80,30 @@
</div> </div>
</div> </div>
@if (isFilteringMode) {
<div class="grid md:grid-cols-2 gap-2 mb-2">
<p-multiselect display="chip" [options]="tripTableColumns" [(ngModel)]="tripTableSelectedColumns"
styleClass="capitalize" selectedItemsLabel="{0} columns selected" placeholder="Choose Columns" />
<input [formControl]="tripTableSearchInput" pInputText placeholder="Search..." />
</div>
}
@defer { @defer {
@if (flattenedTripItems.length) { @if (flattenedTripItems.length) {
<p-table [value]="flattenedTripItems" class="print-striped-rows" styleClass="max-w-[85vw] md:max-w-full" <p-table [value]="flattenedTripItems" class="print-striped-rows" styleClass="max-w-[85vw] md:max-w-full"
[rowGroupMode]="tableExpandableMode ? 'subheader': 'rowspan'" groupRowsBy="td_label"> [rowGroupMode]="tableExpandableMode ? 'subheader': 'rowspan'" groupRowsBy="td_label">
<ng-template #header> <ng-template #header>
<tr> <tr>
<th>Day</th> @if (!tableExpandableMode && tripTableSelectedColumns.includes('day')) {<th class="w-24" pResizableColumn>Day
<th class="w-10">Time</th> </th>}
<th>Text</th> @if (tripTableSelectedColumns.includes('time')) {<th class="w-12" pResizableColumn>Time</th>}
<th class="w-24">Place</th> @if (tripTableSelectedColumns.includes('text')) {<th pResizableColumn>Text</th>}
<th>Comment</th> @if (tripTableSelectedColumns.includes('place')) {<th pResizableColumn>Place</th>}
<th class="w-20">LatLng</th> @if (tripTableSelectedColumns.includes('comment')) {<th pResizableColumn>Comment</th>}
<th class="w-12">Price</th> @if (tripTableSelectedColumns.includes('LatLng')) {<th class="w-12" pResizableColumn>LatLng</th>}
<th class="w-12">Status</th> @if (tripTableSelectedColumns.includes('price')) {<th class="w-12" pResizableColumn>Price</th>}
@if (tripTableSelectedColumns.includes('status')) {<th class="w-12" pResizableColumn>Status</th>}
</tr> </tr>
</ng-template> </ng-template>
@if (tableExpandableMode) { @if (tableExpandableMode) {
@ -113,37 +124,41 @@
<ng-template #expandedrow let-tripitem> <ng-template #expandedrow let-tripitem>
<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)">
<td class="font-mono text-sm max-w-20 truncate">{{ tripitem.td_label }}</td> @if (tripTableSelectedColumns.includes('time')) {<td class="font-mono text-sm">{{ tripitem.time }}</td>}
<td class="font-mono text-sm">{{ tripitem.time }}</td> @if (tripTableSelectedColumns.includes('text')) {<td class="relative">
<td class="relative max-w-60 truncate"> <div class="truncate">
<div class="relative"> @if (tripitem.status) {<div class="block absolute top-3 left-1.5 size-2 rounded-full"
@if (tripitem.status) {<div class="block xl:hidden absolute top-0 -left-1.5 size-1.5 rounded-full"
[style.background]="tripitem.status.color"></div>} [style.background]="tripitem.status.color"></div>}
{{ tripitem.text }} {{ tripitem.text }}
</div> </div>
</td> </td>}
<td class="relative"> @if (tripTableSelectedColumns.includes('place')) {<td class="relative">
@if (tripitem.place) { @if (tripitem.place) {
<div class="ml-7 print:ml-0 max-w-24 truncate print:whitespace-normal"> <div class="ml-7 print:ml-0 truncate print:whitespace-normal">
<img [src]="tripitem.place.image || tripitem.place.category.image" <img [src]="tripitem.place.image || tripitem.place.category.image"
class="absolute left-0 top-1/2 -translate-y-1/2 w-9 rounded-full object-cover print:hidden" /> {{ class="absolute left-0 top-1/2 -translate-y-1/2 w-9 rounded-full object-cover print:hidden" /> {{
tripitem.place.name }} tripitem.place.name }}
</div> </div>
} @else {-} } @else {-}
</td> </td>}
<td class="max-w-20 truncate print:whitespace-pre-line">{{ tripitem.comment || '-' }}</td> @if (tripTableSelectedColumns.includes('comment')) {<td>
<td class="font-mono text-sm"> <div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
<div class="max-w-20 print:max-w-full truncate"> {{ tripitem.comment || '-' }}
</div>
</td>}
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
<div class="print:max-w-full truncate">
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} } @if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} }
@else {-} @else {-}
</div> </div>
</td> </td>}
<td class="truncate">@if (tripitem.price) {<span @if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.price }} {{ currency$ | async }}</span>}</td> tripitem.price }} {{ currency$ | async }}</span>}</td>}
<td class="truncate">@if (tripitem.status) {<span [style.background]="tripitem.status.color+'1A'" @if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span
[style.color]="tripitem.status.color" class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ [style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
tripitem.status.label }}</span>}</td> class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.status.label }}</span>}</td>}
</tr> </tr>
</ng-template> </ng-template>
} }
@ -151,22 +166,22 @@
<ng-template #body let-tripitem 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 (tripTableSelectedColumns.includes('day') && rowgroup) {
<td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-pointer" <td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-pointer"
[class.text-blue-500]="tripMapAntLayerDayID == tripitem.day_id" [class.text-blue-500]="tripMapAntLayerDayID == tripitem.day_id"
(click)="toggleTripDayHighlightPathDay(tripitem.day_id); $event.stopPropagation()"> (click)="toggleTripDayHighlightPathDay(tripitem.day_id); $event.stopPropagation()">
<div class="truncate">{{tripitem.td_label }}</div> <div class="truncate">{{tripitem.td_label }}</div>
</td> </td>
} }
<td class="font-mono text-sm">{{ tripitem.time }}</td> @if (tripTableSelectedColumns.includes('time')) {<td class="font-mono text-sm">{{ tripitem.time }}</td>}
<td class="relative max-w-60 truncate"> @if (tripTableSelectedColumns.includes('text')) {<td class="relative max-w-60">
<div class="relative"> <div class="truncate">
{{ tripitem.text }} @if (tripitem.status) {<div class="block absolute top-3 left-1.5 size-2 rounded-full"
@if (tripitem.status) {<div class="block xl:hidden absolute top-0 -left-1.5 size-1.5 rounded-full"
[style.background]="tripitem.status.color"></div>} [style.background]="tripitem.status.color"></div>}
{{ tripitem.text }}
</div> </div>
</td> </td>}
<td class="relative"> @if (tripTableSelectedColumns.includes('place')) {<td class="relative">
@if (tripitem.place) { @if (tripitem.place) {
<div class="ml-7 print:ml-0 max-w-24 truncate print:whitespace-normal"> <div class="ml-7 print:ml-0 max-w-24 truncate print:whitespace-normal">
<img [src]="tripitem.place.image || tripitem.place.category.image" <img [src]="tripitem.place.image || tripitem.place.category.image"
@ -174,20 +189,25 @@
tripitem.place.name }} tripitem.place.name }}
</div> </div>
} @else {-} } @else {-}
</td> </td>}
<td class="max-w-20 truncate print:whitespace-pre-line">{{ tripitem.comment || '-' }}</td> @if (tripTableSelectedColumns.includes('comment')) {<td>
<td class="font-mono text-sm"> <div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
{{ tripitem.comment || '-' }}
</div>
</td>}
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
<div class="max-w-20 print:max-w-full truncate"> <div class="max-w-20 print:max-w-full truncate">
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} } @if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} }
@else {-} @else {-}
</div> </div>
</td> </td>}
<td class="truncate">@if (tripitem.price) {<span @if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.price }} {{ currency$ | async }}</span>}</td> tripitem.price }} {{ currency$ | async }}</span>}</td>}
<td class="truncate">@if (tripitem.status) {<span [style.background]="tripitem.status.color+'1A'" @if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span
[style.color]="tripitem.status.color" class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ [style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
tripitem.status.label }}</span>}</td> class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.status.label }}</span>}</td>}
</tr> </tr>
</ng-template> </ng-template>
} }

View File

@ -1,6 +1,6 @@
import { AfterViewInit, Component } from "@angular/core"; import { AfterViewInit, Component } from "@angular/core";
import { ApiService } from "../../services/api.service"; import { ApiService } from "../../services/api.service";
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { ButtonModule } from "primeng/button"; import { ButtonModule } from "primeng/button";
import { InputTextModule } from "primeng/inputtext"; import { InputTextModule } from "primeng/inputtext";
import { SkeletonModule } from "primeng/skeleton"; import { SkeletonModule } from "primeng/skeleton";
@ -30,6 +30,7 @@ import { TripCreateDayItemModalComponent } from "../../modals/trip-create-day-it
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 { import {
combineLatest, combineLatest,
debounceTime,
forkJoin, forkJoin,
Observable, Observable,
of, of,
@ -48,16 +49,19 @@ import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place
import { Settings } from "../../types/settings"; import { Settings } from "../../types/settings";
import { DialogModule } from "primeng/dialog"; import { DialogModule } from "primeng/dialog";
import { ClipboardModule } from "@angular/cdk/clipboard"; import { ClipboardModule } from "@angular/cdk/clipboard";
import { TooltipModule } from "primeng/tooltip";
import { MultiSelectModule } from "primeng/multiselect";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: "app-trip", selector: "app-trip",
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
ReactiveFormsModule,
FormsModule, FormsModule,
SkeletonModule, SkeletonModule,
MenuModule, MenuModule,
ReactiveFormsModule,
InputTextModule, InputTextModule,
AsyncPipe, AsyncPipe,
LinkifyPipe, LinkifyPipe,
@ -66,7 +70,9 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
ButtonModule, ButtonModule,
DecimalPipe, DecimalPipe,
DialogModule, DialogModule,
TooltipModule,
ClipboardModule, ClipboardModule,
MultiSelectModule,
], ],
templateUrl: "./trip.component.html", templateUrl: "./trip.component.html",
styleUrls: ["./trip.component.scss"], styleUrls: ["./trip.component.scss"],
@ -88,6 +94,7 @@ export class TripComponent implements AfterViewInit {
collapsedTripStatuses = false; collapsedTripStatuses = false;
shareDialogVisible = false; shareDialogVisible = false;
isExpanded = false; isExpanded = false;
isFilteringMode = false;
map?: L.Map; map?: L.Map;
markerClusterGroup?: L.MarkerClusterGroup; markerClusterGroup?: L.MarkerClusterGroup;
@ -152,6 +159,13 @@ export class TripComponent implements AfterViewInit {
this.tripToNavigation(); this.tripToNavigation();
}, },
}, },
{
label: "Filter",
icon: "pi pi-filter",
command: () => {
this.toggleFiltering();
},
},
{ {
label: "Expand / Group", label: "Expand / Group",
icon: "pi pi-arrow-down-left-and-arrow-up-right-to-center", icon: "pi pi-arrow-down-left-and-arrow-up-right-to-center",
@ -201,6 +215,24 @@ export class TripComponent implements AfterViewInit {
], ],
}, },
]; ];
readonly tripTableColumns: string[] = [
"day",
"time",
"text",
"place",
"comment",
"LatLng",
"price",
"status",
];
tripTableSelectedColumns: string[] = [
"day",
"time",
"text",
"place",
"comment",
];
tripTableSearchInput = new FormControl("");
selectedTripDayForMenu?: TripDay; selectedTripDayForMenu?: TripDay;
dayStatsCache = new Map<number, { price: number; places: number }>(); dayStatsCache = new Map<number, { price: number; places: number }>();
@ -215,6 +247,14 @@ export class TripComponent implements AfterViewInit {
) { ) {
this.currency$ = this.utilsService.currency$; this.currency$ = this.utilsService.currency$;
this.statuses = this.utilsService.statuses; 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 { ngAfterViewInit(): void {
@ -284,6 +324,11 @@ export class TripComponent implements AfterViewInit {
this.trip?.days.sort((a, b) => a.label.localeCompare(b.label)); this.trip?.days.sort((a, b) => a.label.localeCompare(b.label));
} }
toggleFiltering() {
this.isFilteringMode = !this.isFilteringMode;
if (!this.isFilteringMode) this.flattenTripDayItems();
}
getDayStats(day: TripDay): { price: number; places: number } { 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)!;
@ -323,10 +368,17 @@ export class TripComponent implements AfterViewInit {
return this.statuses.find((s) => s.label == status); return this.statuses.find((s) => s.label == status);
} }
flattenTripDayItems() { flattenTripDayItems(searchValue?: string) {
this.sortTripDays(); this.sortTripDays();
this.flattenedTripItems = this.trip!.days.flatMap((day) => this.flattenedTripItems = this.trip!.days.flatMap((day) =>
[...day.items] [...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)) .sort((a, b) => a.time.localeCompare(b.time))
.map((item) => ({ .map((item) => ({
td_id: day.id, td_id: day.id,