✨ 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-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>
|
||||||
|
|||||||
@ -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();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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' },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user