✨ Trip packing list: copy to clipboard or to another Trip (quick copy, quick paste), 💄 QoL Packing list, 💄 QoL checklist
This commit is contained in:
parent
818b5c3753
commit
51a36ea09c
@ -594,12 +594,13 @@
|
||||
}
|
||||
</p-dialog>
|
||||
|
||||
<p-menu #menuTripPacking [model]="menuTripPackingItems" attachTo="body" [popup]="true" />
|
||||
<p-dialog header="Packing list" [draggable]="false" [dismissableMask]="true" [modal]="true"
|
||||
[(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]">
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<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 class="grid gap-2 mt-4 pb-4">
|
||||
@ -633,7 +634,7 @@
|
||||
<p-dialog header="Checklist" [draggable]="false" [dismissableMask]="true" [modal]="true"
|
||||
[(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]">
|
||||
<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 />
|
||||
</div>
|
||||
|
||||
@ -656,16 +657,28 @@
|
||||
}
|
||||
</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) {
|
||||
<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">
|
||||
<div class="relative">
|
||||
@if (item.status) {<div class="z-50 block absolute top-0 left-3 size-2.5 rounded-full"
|
||||
[style.background]="item.status.color"></div>}
|
||||
<p-checkbox disabled />
|
||||
</div>
|
||||
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
||||
<div class="truncate select-none">
|
||||
<span>{{ item.text }}</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@ -37,7 +37,7 @@ import { LinkifyPipe } from '../../shared/linkify.pipe';
|
||||
import { PlaceCreateModalComponent } from '../../modals/place-create-modal/place-create-modal.component';
|
||||
import { Settings } from '../../types/settings';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import { MultiSelectModule } from 'primeng/multiselect';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@ -118,13 +118,13 @@ export class TripComponent implements AfterViewInit {
|
||||
items: [
|
||||
{
|
||||
label: 'Checklist',
|
||||
icon: 'pi pi-check-square',
|
||||
icon: 'pi pi-list-check',
|
||||
command: () => {
|
||||
this.openChecklist();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Packing',
|
||||
label: 'Packing list',
|
||||
icon: 'pi pi-briefcase',
|
||||
command: () => {
|
||||
this.openPackingList();
|
||||
@ -287,6 +287,7 @@ export class TripComponent implements AfterViewInit {
|
||||
'status',
|
||||
'distance',
|
||||
];
|
||||
menuTripPackingItems: MenuItem[] = [];
|
||||
tripTableSelectedColumns: string[] = ['day', 'time', 'text', 'place', 'comment'];
|
||||
tripTableSearchInput = new FormControl('');
|
||||
selectedTripDayForMenu?: TripDay;
|
||||
@ -300,6 +301,7 @@ export class TripComponent implements AfterViewInit {
|
||||
private dialogService: DialogService,
|
||||
private utilsService: UtilsService,
|
||||
private route: ActivatedRoute,
|
||||
private clipboard: Clipboard,
|
||||
) {
|
||||
this.statuses = this.utilsService.statuses;
|
||||
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() {
|
||||
if (!this.trip) return;
|
||||
|
||||
this.computeMenuTripPackingItems();
|
||||
if (!this.packingList.length)
|
||||
this.apiService
|
||||
.getPackingList(this.trip.id)
|
||||
@ -1478,7 +1506,7 @@ export class TripComponent implements AfterViewInit {
|
||||
if (!this.trip) return;
|
||||
|
||||
const modal: DynamicDialogRef = this.dialogService.open(TripCreatePackingModalComponent, {
|
||||
header: 'Create Packing',
|
||||
header: 'Create packing item',
|
||||
modal: true,
|
||||
appendTo: 'body',
|
||||
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() {
|
||||
if (!this.trip) return;
|
||||
|
||||
@ -1571,6 +1672,7 @@ export class TripComponent implements AfterViewInit {
|
||||
.subscribe({
|
||||
next: (items) => {
|
||||
this.checklistItems = [...items];
|
||||
this.computeDispChecklistList();
|
||||
},
|
||||
});
|
||||
this.checklistDialogVisible = true;
|
||||
@ -1580,7 +1682,7 @@ export class TripComponent implements AfterViewInit {
|
||||
if (!this.trip) return;
|
||||
|
||||
const modal: DynamicDialogRef = this.dialogService.open(TripCreateChecklistModalComponent, {
|
||||
header: 'Create item',
|
||||
header: 'Create checklist item',
|
||||
modal: true,
|
||||
appendTo: 'body',
|
||||
closable: true,
|
||||
@ -1602,12 +1704,19 @@ export class TripComponent implements AfterViewInit {
|
||||
.subscribe({
|
||||
next: (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) {
|
||||
if (!this.trip) return;
|
||||
this.apiService
|
||||
@ -1617,6 +1726,7 @@ export class TripComponent implements AfterViewInit {
|
||||
next: (item) => {
|
||||
const i = this.checklistItems.find((p) => p.id == item.id);
|
||||
if (i) i.checked = item.checked;
|
||||
this.computeDispChecklistList();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { TripStatus } from '../types/trip';
|
||||
import { PackingItem, TripStatus } from '../types/trip';
|
||||
import { ApiService } from './api.service';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
@ -12,6 +12,7 @@ type ToastSeverity = 'info' | 'warn' | 'error' | 'success';
|
||||
export class UtilsService {
|
||||
private apiService = inject(ApiService);
|
||||
currency$ = this.apiService.settings$.pipe(map((s) => s?.currency ?? '€'));
|
||||
packingListToCopy: Partial<PackingItem>[] = [];
|
||||
|
||||
readonly statuses: TripStatus[] = [
|
||||
{ label: 'pending', color: '#3258A8' },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user