Trip packing list: copy to clipboard or to another Trip (quick copy, quick paste), 💄 QoL Packing list, 💄 QoL checklist

This commit is contained in:
itskovacs 2025-10-12 16:11:11 +02:00
parent 818b5c3753
commit 51a36ea09c
3 changed files with 136 additions and 12 deletions

View File

@ -594,12 +594,13 @@
} }
</p-dialog> </p-dialog>
<p-menu #menuTripPacking [model]="menuTripPackingItems" attachTo="body" [popup]="true" />
<p-dialog header="Packing list" [draggable]="false" [dismissableMask]="true" [modal]="true" <p-dialog header="Packing list" [draggable]="false" [dismissableMask]="true" [modal]="true"
[(visible)]="packingDialogVisible" styleClass="w-[95%] md:w-[70%] lg:w-[50%]"> [(visible)]="packingDialogVisible" styleClass="w-[95%] md:w-[70%] lg:w-[50%]">
<section class="md:max-w-3/4 md:mx-auto max-h-[80%] md:max-h-[600px]"> <section class="md:max-w-3/4 md:mx-auto max-h-[80%] md:max-h-[600px]">
<div class="flex items-center justify-center gap-4"> <div class="flex items-center justify-center gap-4">
<p-button (click)="addPackingItem()" icon="pi pi-plus" label="Add item" text /> <p-button (click)="addPackingItem()" icon="pi pi-plus" label="Add item" text />
<p-button icon="pi pi-ellipsis-h" text /> <p-button (click)="menuTripPacking.toggle($event)" icon="pi pi-ellipsis-h" text />
</div> </div>
<div class="grid gap-2 mt-4 pb-4"> <div class="grid gap-2 mt-4 pb-4">
@ -633,7 +634,7 @@
<p-dialog header="Checklist" [draggable]="false" [dismissableMask]="true" [modal]="true" <p-dialog header="Checklist" [draggable]="false" [dismissableMask]="true" [modal]="true"
[(visible)]="checklistDialogVisible" styleClass="w-[95%] md:w-[50%] lg:w-[30%]"> [(visible)]="checklistDialogVisible" styleClass="w-[95%] md:w-[50%] lg:w-[30%]">
<section class="p-4 max-w-full max-h-[80%] md:max-h-[600px]"> <section class="p-4 max-w-full max-h-[80%] md:max-h-[600px]">
<div class="flex justify-center"> <div class="flex items-center justify-center gap-4">
<p-button (click)="addChecklistItem()" icon="pi pi-plus" label="Add item" text /> <p-button (click)="addChecklistItem()" icon="pi pi-plus" label="Add item" text />
</div> </div>
@ -656,16 +657,28 @@
} }
</div> </div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 mt-4 pb-4"> <div class="flex items-center justify-center flex-wrap xl:justify-between gap-1 mt-4">
<div class="font-semibold tracking-tight text-md">Items with status</div>
<div class="flex justify-center md:justify-end gap-1">
@for (status of statuses; track status.label) {
<div class="relative">
<div class="z-50 block absolute top-0.5 left-1 size-2.5 rounded-full" [style.background]="status.color"></div>
<span [style.background]="status.color+'1A'" [style.color]="status.color"
class="text-xs md:text-sm font-medium me-2 px-2.5 py-1 rounded">{{ status.label }}</span>
</div>
}
</div>
</div>
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-2 mt-2 pb-4">
@for (item of getWatchlistData; track item.id) { @for (item of getWatchlistData; track item.id) {
<div class="flex items-center gap-3 rounded-md p-2 hover:bg-gray-100 dark:hover:bg-white/5"> <div class="rounded-md py-1 min-w-0 hover:bg-gray-100 dark:hover:bg-white/5">
<label [for]="item.id" [pTooltip]="item.text" class="flex items-center gap-2 w-full"> <label [for]="item.id" [pTooltip]="item.text" class="flex items-center gap-2 w-full">
<div class="relative"> <div class="relative">
@if (item.status) {<div class="z-50 block absolute top-0 left-3 size-2.5 rounded-full" @if (item.status) {<div class="z-50 block absolute top-0 left-3 size-2.5 rounded-full"
[style.background]="item.status.color"></div>} [style.background]="item.status.color"></div>}
<p-checkbox disabled /> <p-checkbox disabled />
</div> </div>
<div class="pr-6 md:pr-0 truncate select-none flex-1"> <div class="truncate select-none">
<span>{{ item.text }}</span> <span>{{ item.text }}</span>
</div> </div>
</label> </label>

View File

@ -37,7 +37,7 @@ 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'; import { Settings } from '../../types/settings';
import { DialogModule } from 'primeng/dialog'; import { DialogModule } from 'primeng/dialog';
import { ClipboardModule } from '@angular/cdk/clipboard'; import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
import { TooltipModule } from 'primeng/tooltip'; import { TooltipModule } from 'primeng/tooltip';
import { MultiSelectModule } from 'primeng/multiselect'; import { MultiSelectModule } from 'primeng/multiselect';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -118,13 +118,13 @@ export class TripComponent implements AfterViewInit {
items: [ items: [
{ {
label: 'Checklist', label: 'Checklist',
icon: 'pi pi-check-square', icon: 'pi pi-list-check',
command: () => { command: () => {
this.openChecklist(); this.openChecklist();
}, },
}, },
{ {
label: 'Packing', label: 'Packing list',
icon: 'pi pi-briefcase', icon: 'pi pi-briefcase',
command: () => { command: () => {
this.openPackingList(); this.openPackingList();
@ -287,6 +287,7 @@ export class TripComponent implements AfterViewInit {
'status', 'status',
'distance', 'distance',
]; ];
menuTripPackingItems: MenuItem[] = [];
tripTableSelectedColumns: string[] = ['day', 'time', 'text', 'place', 'comment']; tripTableSelectedColumns: string[] = ['day', 'time', 'text', 'place', 'comment'];
tripTableSearchInput = new FormControl(''); tripTableSearchInput = new FormControl('');
selectedTripDayForMenu?: TripDay; selectedTripDayForMenu?: TripDay;
@ -300,6 +301,7 @@ export class TripComponent implements AfterViewInit {
private dialogService: DialogService, private dialogService: DialogService,
private utilsService: UtilsService, private utilsService: UtilsService,
private route: ActivatedRoute, private route: ActivatedRoute,
private clipboard: Clipboard,
) { ) {
this.statuses = this.utilsService.statuses; this.statuses = this.utilsService.statuses;
this.tripTableSearchInput.valueChanges.pipe(debounceTime(300), takeUntilDestroyed()).subscribe({ this.tripTableSearchInput.valueChanges.pipe(debounceTime(300), takeUntilDestroyed()).subscribe({
@ -1458,9 +1460,35 @@ export class TripComponent implements AfterViewInit {
}); });
} }
computeMenuTripPackingItems() {
this.menuTripPackingItems = [
{
label: 'Actions',
items: [
{
label: 'Copy to clipboard (text)',
icon: 'pi pi-clipboard',
command: () => this.copyPackingListToClipboard(),
},
{
label: 'Quick Copy',
icon: 'pi pi-copy',
command: () => this.copyPackingListToService(),
},
{
label: `Quick Paste (${this.utilsService.packingListToCopy.length})`,
icon: 'pi pi-copy',
command: () => this.pastePackingList(),
disabled: !this.utilsService.packingListToCopy.length,
},
],
},
];
}
openPackingList() { openPackingList() {
if (!this.trip) return; if (!this.trip) return;
this.computeMenuTripPackingItems();
if (!this.packingList.length) if (!this.packingList.length)
this.apiService this.apiService
.getPackingList(this.trip.id) .getPackingList(this.trip.id)
@ -1478,7 +1506,7 @@ export class TripComponent implements AfterViewInit {
if (!this.trip) return; if (!this.trip) return;
const modal: DynamicDialogRef = this.dialogService.open(TripCreatePackingModalComponent, { const modal: DynamicDialogRef = this.dialogService.open(TripCreatePackingModalComponent, {
header: 'Create Packing', header: 'Create packing item',
modal: true, modal: true,
appendTo: 'body', appendTo: 'body',
closable: true, closable: true,
@ -1561,6 +1589,79 @@ export class TripComponent implements AfterViewInit {
}, {}); }, {});
} }
copyPackingListToClipboard() {
const content = this.packingList
.sort((a, b) =>
a.category !== b.category
? a.category.localeCompare(b.category)
: a.text < b.text
? -1
: a.text > b.text
? 1
: 0,
)
.map((item) => `[${item.category}] ${item.qt ? item.qt + ' ' : ''}${item.text}`)
.join('\n');
const success = this.clipboard.copy(content);
if (success) this.utilsService.toast('success', 'Success', `Content copied to clipboard`);
else this.utilsService.toast('error', 'Error', 'Content could not be copied to clipboard');
}
copyPackingListToService() {
const content: Partial<PackingItem>[] = this.packingList.map((item) => ({
qt: item.qt,
text: item.text,
category: item.category,
}));
this.utilsService.packingListToCopy = content;
this.utilsService.toast(
'success',
'Ready to Paste',
`${content.length} item${content.length > 1 ? 's' : ''} copied. Go to another Trip and use Quick Paste`,
);
this.computeMenuTripPackingItems();
}
pastePackingList() {
const content: Partial<PackingItem>[] = this.utilsService.packingListToCopy;
const modal = this.dialogService.open(YesNoModalComponent, {
header: 'Confirm Paste',
modal: true,
closable: true,
dismissableMask: true,
breakpoints: {
'640px': '90vw',
},
data: `Paste ${content.length} packing item${content.length > 1 ? 's' : ''} in ${this.trip?.name} ?`,
})!;
modal.onClose.pipe(take(1)).subscribe({
next: (bool) => {
if (!bool) return;
const obs$ = content.map((packingItem) =>
this.apiService.postPackingItem(this.trip!.id, packingItem as PackingItem),
);
forkJoin(obs$)
.pipe(take(1))
.subscribe({
next: (items: PackingItem[]) => {
this.packingList = [...this.packingList, ...items];
this.computeDispPackingList();
this.utilsService.toast(
'success',
'Success',
`Added ${content.length} item${content.length > 1 ? 's' : ''}`,
);
this.utilsService.packingListToCopy = [];
this.computeMenuTripPackingItems();
},
});
},
});
}
openChecklist() { openChecklist() {
if (!this.trip) return; if (!this.trip) return;
@ -1571,6 +1672,7 @@ export class TripComponent implements AfterViewInit {
.subscribe({ .subscribe({
next: (items) => { next: (items) => {
this.checklistItems = [...items]; this.checklistItems = [...items];
this.computeDispChecklistList();
}, },
}); });
this.checklistDialogVisible = true; this.checklistDialogVisible = true;
@ -1580,7 +1682,7 @@ export class TripComponent implements AfterViewInit {
if (!this.trip) return; if (!this.trip) return;
const modal: DynamicDialogRef = this.dialogService.open(TripCreateChecklistModalComponent, { const modal: DynamicDialogRef = this.dialogService.open(TripCreateChecklistModalComponent, {
header: 'Create item', header: 'Create checklist item',
modal: true, modal: true,
appendTo: 'body', appendTo: 'body',
closable: true, closable: true,
@ -1602,12 +1704,19 @@ export class TripComponent implements AfterViewInit {
.subscribe({ .subscribe({
next: (item) => { next: (item) => {
this.checklistItems = [...this.checklistItems, item]; this.checklistItems = [...this.checklistItems, item];
this.computeDispChecklistList();
}, },
}); });
}, },
}); });
} }
computeDispChecklistList() {
this.checklistItems = [...this.checklistItems].sort((a, b) =>
a.checked !== b.checked ? (a.checked ? 1 : -1) : b.id - a.id,
);
}
onCheckChecklistItem(e: CheckboxChangeEvent, id: number) { onCheckChecklistItem(e: CheckboxChangeEvent, id: number) {
if (!this.trip) return; if (!this.trip) return;
this.apiService this.apiService
@ -1617,6 +1726,7 @@ export class TripComponent implements AfterViewInit {
next: (item) => { next: (item) => {
const i = this.checklistItems.find((p) => p.id == item.id); const i = this.checklistItems.find((p) => p.id == item.id);
if (i) i.checked = item.checked; if (i) i.checked = item.checked;
this.computeDispChecklistList();
}, },
}); });
} }

View File

@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { MessageService } from 'primeng/api'; import { MessageService } from 'primeng/api';
import { TripStatus } from '../types/trip'; import { PackingItem, TripStatus } from '../types/trip';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { map } from 'rxjs'; import { map } from 'rxjs';
@ -12,6 +12,7 @@ type ToastSeverity = 'info' | 'warn' | 'error' | 'success';
export class UtilsService { export class UtilsService {
private apiService = inject(ApiService); private apiService = inject(ApiService);
currency$ = this.apiService.settings$.pipe(map((s) => s?.currency ?? '€')); currency$ = this.apiService.settings$.pipe(map((s) => s?.currency ?? '€'));
packingListToCopy: Partial<PackingItem>[] = [];
readonly statuses: TripStatus[] = [ readonly statuses: TripStatus[] = [
{ label: 'pending', color: '#3258A8' }, { label: 'pending', color: '#3258A8' },