Trip: attachments, archived QoL, empty lists placeholders

This commit is contained in:
itskovacs 2025-10-15 23:39:58 +02:00
parent 0a2d0d5c6f
commit 85495f55e8
2 changed files with 280 additions and 138 deletions

View File

@ -1,5 +1,5 @@
<section class="mt-4" [class.prettyprint]="isPrinting"> <section class="mt-4" [class.prettyprint]="isPrinting">
<div class="p-4 print:p-0 flex flex-wrap items-center justify-between"> <div class="p-4 print:p-0 flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p-button text icon="pi pi-chevron-left" class="print:hidden" (click)="back()" severity="secondary" /> <p-button text icon="pi pi-chevron-left" class="print:hidden" (click)="back()" severity="secondary" />
<div class="flex flex-col max-w-[55vw] md:max-w-full"> <div class="flex flex-col max-w-[55vw] md:max-w-full">
@ -10,12 +10,24 @@
trip?.days?.length }} {{ trip?.days!.length > 1 ? 'days' : trip?.days?.length }} {{ trip?.days!.length > 1 ? 'days' :
'day'}}</span> 'day'}}</span>
<span <span
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{ class="bg-gray-100 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{
(totalPrice | number:'1.0-2') || '-' }} {{ trip?.currency }}</span> (totalPrice | number:'1.0-2') || '-' }} {{ trip?.currency }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="hidden print:flex flex-col items-center">
<img src="favicon.png" class="size-20">
<div class="flex gap-2 items-center text-xs text-gray-500"><i class="pi pi-github"></i>itskovacs/trip</div>
</div>
<div class="flex items-center gap-2 print:hidden">
<div class="flex">
<p-button (click)="openMenuTripActionsItems($event)" severity="secondary" text icon="pi pi-ellipsis-h" />
<p-menu #menuTripActions [model]="menuTripActionsItems" [popup]="true" />
</div>
</div>
</div>
@if (trip?.archived) { @if (trip?.archived) {
<div class="mx-auto p-4 mt-4 md:mt-0 w-full md:w-fit text-orange-800 rounded-md bg-orange-50" <div class="mx-auto p-4 mt-4 md:mt-0 w-full md:w-fit text-orange-800 rounded-md bg-orange-50"
[class.prettyprint]="isPrinting"> [class.prettyprint]="isPrinting">
@ -28,27 +40,12 @@
<p-button text icon="pi pi-box" label="Restore" severity="success" (click)="openUnarchiveTripModal()" /> <p-button text icon="pi pi-box" label="Restore" severity="success" (click)="openUnarchiveTripModal()" />
</div> </div>
</div> </div>
}
<div class="hidden print:flex flex-col items-center">
<img src="favicon.png" class="size-20">
<div class="flex gap-2 items-center text-xs text-gray-500"><i class="pi pi-github"></i>itskovacs/trip</div>
</div>
<div class="flex items-center gap-2 print:hidden">
@if (!trip?.archived) {
<div class="flex">
<p-button (click)="menuTripActions.toggle($event)" severity="secondary" text icon="pi pi-ellipsis-h" />
<p-menu #menuTripActions [model]="menuTripActionsItems" [popup]="true" />
</div>
}
</div>
</div>
@if (isArchivalReviewDisplayed) { @if (isArchivalReviewDisplayed) {
<div <div
class="m-4 whitespace-pre-line text-gray-800 dark:text-gray-200 max-h-[600px] overflow-y-auto p-4 rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800"> class="m-4 whitespace-pre-line text-gray-800 dark:text-gray-200 max-h-[600px] overflow-y-auto p-4 rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800">
{{ trip?.archival_review }}</div> {{ trip?.archival_review }}</div>
} }
}
</section> </section>
<section class="p-4 print:px-1 grid lg:grid-cols-3 gap-4 print:block" [class.prettyprint]="isPrinting"> <section class="p-4 print:px-1 grid lg:grid-cols-3 gap-4 print:block" [class.prettyprint]="isPrinting">
@ -320,8 +317,8 @@
<div class="relative rounded-md shadow p-4"> <div class="relative rounded-md shadow p-4">
<p class="font-bold mb-1">Place</p> <p class="font-bold mb-1">Place</p>
<div class="text-sm text-gray-500 truncate">{{ selectedItem.place.name }}</div> <div class="text-sm text-gray-500 truncate">{{ selectedItem.place.name }}</div>
<div class="absolute top-2 right-2"><p-button severity="help" text icon="pi pi-pencil" <div class="absolute top-2 right-2"><p-button severity="help" pTooltip="Edit place details" text
(click)="editPlace(selectedItem.place)" /></div> icon="pi pi-pencil" (click)="editPlace(selectedItem.place)" /></div>
</div> </div>
} }
@ -395,8 +392,8 @@
<div <div
class="bg-white rounded py-2 absolute top-1/2 -translate-y-1/2 left-0 hidden group-hover:block slide-x dark:bg-surface-900"> class="bg-white rounded py-2 absolute top-1/2 -translate-y-1/2 left-0 hidden group-hover:block slide-x dark:bg-surface-900">
<p-button [icon]="collapsedTripPlaces ? 'pi pi-chevron-down' : 'pi pi-chevron-up'" text <p-button [icon]="isCollapsedTripPlaces ? 'pi pi-chevron-down' : 'pi pi-chevron-up'" text
(click)="collapsedTripPlaces = !collapsedTripPlaces" /> (click)="isCollapsedTripPlaces = !isCollapsedTripPlaces" />
</div> </div>
</div> </div>
@ -414,7 +411,7 @@
</div> </div>
</div> </div>
@if (!collapsedTripPlaces) { @if (!isCollapsedTripPlaces) {
<div [class.max-h-64!]="!isExpanded" class="max-h-[340px] overflow-y-auto"> <div [class.max-h-64!]="!isExpanded" class="max-h-[340px] overflow-y-auto">
@defer { @defer {
@for (p of places; track p.id) { @for (p of places; track p.id) {
@ -475,15 +472,15 @@
<div <div
class="bg-white rounded py-2 absolute top-1/2 -translate-y-1/2 left-0 hidden group-hover:block slide-x dark:bg-surface-900"> class="bg-white rounded py-2 absolute top-1/2 -translate-y-1/2 left-0 hidden group-hover:block slide-x dark:bg-surface-900">
<p-button [icon]="collapsedTripDays ? 'pi pi-chevron-down' : 'pi pi-chevron-up'" text <p-button [icon]="isCollapsedTripDays ? 'pi pi-chevron-down' : 'pi pi-chevron-up'" text
(click)="collapsedTripDays = !collapsedTripDays" /> (click)="isCollapsedTripDays = !isCollapsedTripDays" />
</div> </div>
</div> </div>
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text /> <p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
</div> </div>
@if (!collapsedTripDays) { @if (!isCollapsedTripDays) {
<div [class.max-h-64!]="!isExpanded" class="max-h-[340px] overflow-y-auto"> <div [class.max-h-64!]="!isExpanded" class="max-h-[340px] overflow-y-auto">
@defer { @defer {
@for (d of trip?.days; track d.id) { @for (d of trip?.days; track d.id) {
@ -568,9 +565,9 @@
</div> </div>
} }
<p-dialog header="Share" [draggable]="false" [dismissableMask]="true" [modal]="true" [(visible)]="shareDialogVisible" <p-dialog header="Share" [draggable]="false" [dismissableMask]="true" [modal]="true" [(visible)]="isShareDialogVisible"
[style]="{ width: '25rem' }"> [style]="{ width: '25rem' }">
@if (shareDialogVisible) { @if (isShareDialogVisible) {
<ng-container> <ng-container>
@if (trip?.shared) { @if (trip?.shared) {
<div class="flex items-center flex-col gap-2"> <div class="flex items-center flex-col gap-2">
@ -596,10 +593,10 @@
<p-menu #menuTripPacking [model]="menuTripPackingItems" attachTo="body" [popup]="true" /> <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)]="isPackingDialogVisible" 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()" [disabled]="trip?.archived" icon="pi pi-plus" label="Add item" text />
<p-button (click)="menuTripPacking.toggle($event)" icon="pi pi-ellipsis-h" text /> <p-button (click)="menuTripPacking.toggle($event)" icon="pi pi-ellipsis-h" text />
</div> </div>
@ -621,39 +618,52 @@
</label> </label>
<div <div
class="md:opacity-0 absolute right-0 top-1/2 -translate-y-1/2 md:group-hover:opacity-100 bg-white md:bg-gray-100 rounded"> class="md:opacity-0 absolute right-0 top-1/2 -translate-y-1/2 md:group-hover:opacity-100 bg-white md:bg-gray-100 rounded">
<p-button size="small" text icon="pi pi-trash" (click)="deletePackingItem(item)" severity="danger" /> <p-button size="small" [disabled]="trip?.archived" text icon="pi pi-trash" (click)="deletePackingItem(item)"
severity="danger" />
</div> </div>
</div> </div>
} }
</div> </div>
} @empty {
<div class="flex flex-col items-center justify-center mt-4 text-center select-none">
<i style="font-size: 3rem" class="pi pi-inbox mb-2 text-gray-300 dark:text-gray-700"></i>
<p class="text-sm text-gray-500 dark:text-gray-400">No item</p>
</div>
} }
</div> </div>
</section> </section>
</p-dialog> </p-dialog>
<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)]="isChecklistDialogVisible" 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 items-center justify-center gap-4"> <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()" [disabled]="trip?.archived" icon="pi pi-plus" label="Add item" text />
</div> </div>
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-2 mt-2 pb-4"> <div class="grid md:grid-cols-2 xl:grid-cols-3 gap-2 mt-2 pb-4">
@for (item of checklistItems; track item.id) { @for (item of checklistItems; track item.id) {
<div class="relative group flex items-center gap-3 rounded-md p-2 hover:bg-gray-100 dark:hover:bg-white/5"> <div
class="relative group flex items-center gap-3 rounded-md py-1 min-w-0 hover:bg-gray-100 dark:hover:bg-white/5">
<label [for]="item.id" [pTooltip]="item.text" [class.line-through]="item.checked" <label [for]="item.id" [pTooltip]="item.text" [class.line-through]="item.checked"
class="flex items-center gap-2 w-full cursor-pointer"> class="flex items-center gap-2 w-full cursor-pointer">
<p-checkbox (onChange)="onCheckChecklistItem($event, item.id)" [binary]="true" [inputId]="item.id.toString()" <p-checkbox (onChange)="onCheckChecklistItem($event, item.id)" [binary]="true"
[(ngModel)]="item.checked" /> [disabled]="!!this.trip?.archived" [inputId]="item.id.toString()" [(ngModel)]="item.checked" />
<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>
<div <div
class="md:opacity-0 absolute right-0 top-1/2 -translate-y-1/2 md:group-hover:opacity-100 bg-white md:bg-gray-100 rounded"> class="md:opacity-0 absolute right-0 top-1/2 -translate-y-1/2 md:group-hover:opacity-100 bg-white md:bg-transparent rounded">
<p-button size="small" text icon="pi pi-trash" (click)="deleteChecklistItem(item)" severity="danger" /> <p-button size="small" text icon="pi pi-trash" [disabled]="trip?.archived" (click)="deleteChecklistItem(item)"
severity="danger" />
</div> </div>
</div> </div>
} @empty {
<div class="col-span-full flex flex-col items-center justify-center mt-4 text-center select-none">
<i style="font-size: 3rem" class="pi pi-inbox mb-2 text-gray-300 dark:text-gray-700"></i>
<p class="text-sm text-gray-500 dark:text-gray-400">No item</p>
</div>
} }
</div> </div>
@ -685,16 +695,21 @@
</div> </div>
</label> </label>
</div> </div>
} @empty {
<div class="col-span-full flex flex-col items-center justify-center mt-4 text-center select-none">
<i style="font-size: 3rem" class="pi pi-inbox mb-2 text-gray-300 dark:text-gray-700"></i>
<p class="text-sm text-gray-500 dark:text-gray-400">No item</p>
</div>
} }
</div> </div>
</section> </section>
</p-dialog> </p-dialog>
<p-dialog header="Members" [draggable]="false" [dismissableMask]="true" [modal]="true" <p-dialog header="Members" [draggable]="false" [dismissableMask]="true" [modal]="true"
[(visible)]="membersDialogVisible" styleClass="w-[95%] md:w-[40%] lg:w-[30%]"> [(visible)]="isMembersDialogVisible" styleClass="w-[95%] md:w-[40%] 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 justify-center">
<p-button (click)="addMember()" icon="pi pi-plus" label="Member" text /> <p-button (click)="addMember()" [disabled]="trip?.archived" icon="pi pi-plus" label="Member" text />
</div> </div>
<div class="divide-y divide-gray-100 mt-4 pb-4"> <div class="divide-y divide-gray-100 mt-4 pb-4">
@ -738,7 +753,8 @@
m.balance || '-' }} {{ trip?.currency }}</span> m.balance || '-' }} {{ trip?.currency }}</span>
@if (m.invited_at) { @if (m.invited_at) {
<p-button text (click)="deleteMember(m.user)" icon="pi pi-trash" severity="danger" /> <p-button text (click)="deleteMember(m.user)" [disabled]="trip?.archived" icon="pi pi-trash"
severity="danger" />
} }
</div> </div>
</div> </div>
@ -747,6 +763,52 @@
</section> </section>
</p-dialog> </p-dialog>
<p-dialog header="Attachments" [draggable]="false" [dismissableMask]="true" [modal]="true"
[(visible)]="isAttachmentsDialogVisible" 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)="fileUpload.click()" [disabled]="trip?.archived" icon="pi pi-file-import" label="Add document"
text />
<input type="file" style="display: none;" (change)="onFileUploadInputChange($event)" #fileUpload>
</div>
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-2 mt-2 pb-4">
@for (attachment of trip?.attachments; track attachment.id) {
<div (click)="downloadAttachment(attachment)"
class="group relative cursor-pointer flex items-center gap-3 rounded-lg border border-gray-200 bg-white hover:bg-gray-100 dark:hover:bg-white/5 p-3 transition-all dark:border-gray-800 dark:bg-gray-950">
<div
class="group relative flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-md bg-red-100 transition-colors dark:bg-red-900">
<i class="pi pi-file text-red-600 transition-opacity group-hover:opacity-0 dark:text-red-400"></i>
<i
class="pi pi-arrow-down absolute -translate-y-4 text-red-600 opacity-0 transition-none group-hover:translate-y-0 group-hover:opacity-100 group-hover:transition-all group-hover:duration-300 dark:text-red-400"></i>
</div>
<div class="min-w-0 flex-1">
<div class="truncate font-mono text-md text-gray-900 dark:text-gray-100">{{ attachment.filename }}</div>
<div class="flex items-center gap-2 mt-1 text-xs font-mono text-gray-500 dark:text-gray-400">
<span>{{ attachment.file_size | fileSize }}</span>
<span
class="bg-gray-100 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{
attachment.uploaded_by }}</span>
</div>
</div>
<div
class="md:opacity-0 absolute right-2 top-1/2 -translate-y-1/2 translate-x-2 group-hover:translate-x-0 md:group-hover:opacity-100 group-hover:transition-all group-hover:duration-300 bg-white md:bg-transparent rounded">
<p-button text icon="pi pi-trash" [disabled]="trip?.archived"
(click)="deleteAttachment(attachment.id); $event.stopPropagation()" severity="danger" />
</div>
</div>
} @empty {
<div class="col-span-full flex flex-col items-center justify-center mt-4 text-center select-none">
<i style="font-size: 3rem" class="pi pi-inbox mb-2 text-gray-300 dark:text-gray-700"></i>
<p class="text-sm text-gray-500 dark:text-gray-400">No attachments</p>
</div>
}
</div>
</section>
</p-dialog>
@if (isPrinting) { @if (isPrinting) {
<section class="hidden print:block"> <section class="hidden print:block">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Component } from '@angular/core'; import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
@ -17,6 +17,7 @@ import {
PackingItem, PackingItem,
ChecklistItem, ChecklistItem,
TripMember, TripMember,
TripAttachment,
} from '../../types/trip'; } from '../../types/trip';
import { Place } from '../../types/poi'; import { Place } from '../../types/poi';
import { createMap, placeToMarker, createClusterGroup, tripDayMarker, gpxToPolyline } from '../../shared/map'; import { createMap, placeToMarker, createClusterGroup, tripDayMarker, gpxToPolyline } from '../../shared/map';
@ -32,7 +33,7 @@ import { UtilsService } from '../../services/utils.service';
import { TripCreateModalComponent } from '../../modals/trip-create-modal/trip-create-modal.component'; import { TripCreateModalComponent } from '../../modals/trip-create-modal/trip-create-modal.component';
import { AsyncPipe, CommonModule, DecimalPipe } from '@angular/common'; import { AsyncPipe, CommonModule, DecimalPipe } from '@angular/common';
import { MenuItem } from 'primeng/api'; import { MenuItem } from 'primeng/api';
import { MenuModule } from 'primeng/menu'; import { Menu, MenuModule } from 'primeng/menu';
import { LinkifyPipe } from '../../shared/linkify.pipe'; 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';
@ -49,6 +50,7 @@ import { calculateDistanceBetween } from '../../shared/haversine';
import { orderByPipe } from '../../shared/order-by.pipe'; import { orderByPipe } from '../../shared/order-by.pipe';
import { TripNotesModalComponent } from '../../modals/trip-notes-modal/trip-notes-modal.component'; import { TripNotesModalComponent } from '../../modals/trip-notes-modal/trip-notes-modal.component';
import { TripArchiveModalComponent } from '../../modals/trip-archive-modal/trip-archive-modal.component'; import { TripArchiveModalComponent } from '../../modals/trip-archive-modal/trip-archive-modal.component';
import { FileSizePipe } from '../../shared/filesize.pipe';
@Component({ @Component({
selector: 'app-trip', selector: 'app-trip',
@ -72,11 +74,13 @@ import { TripArchiveModalComponent } from '../../modals/trip-archive-modal/trip-
MultiSelectModule, MultiSelectModule,
CheckboxModule, CheckboxModule,
orderByPipe, orderByPipe,
FileSizePipe,
], ],
templateUrl: './trip.component.html', templateUrl: './trip.component.html',
styleUrls: ['./trip.component.scss'], styleUrls: ['./trip.component.scss'],
}) })
export class TripComponent implements AfterViewInit { export class TripComponent implements AfterViewInit {
@ViewChild('menuTripActions') menuTripActions!: Menu;
tripSharedURL$?: Observable<string>; tripSharedURL$?: Observable<string>;
statuses: TripStatus[] = []; statuses: TripStatus[] = [];
trip?: Trip; trip?: Trip;
@ -87,21 +91,22 @@ export class TripComponent implements AfterViewInit {
isPrinting = false; isPrinting = false;
isArchivalReviewDisplayed = false; isArchivalReviewDisplayed = false;
totalPrice = 0;
isMapFullscreen = false; isMapFullscreen = false;
isMapFullscreenDays = false; isMapFullscreenDays = false;
totalPrice = 0; isCollapsedTripDays = false;
collapsedTripDays = false; isCollapsedTripPlaces = false;
collapsedTripPlaces = false; isShareDialogVisible = false;
shareDialogVisible = false; isPackingDialogVisible = false;
packingDialogVisible = false; isMembersDialogVisible = false;
isAttachmentsDialogVisible = false;
isChecklistDialogVisible = false;
isExpanded = false; isExpanded = false;
isFilteringMode = false; isFilteringMode = false;
packingList: PackingItem[] = []; packingList: PackingItem[] = [];
dispPackingList: Record<string, PackingItem[]> = {}; dispPackingList: Record<string, PackingItem[]> = {};
checklistDialogVisible = false;
checklistItems: ChecklistItem[] = []; checklistItems: ChecklistItem[] = [];
dispchecklist: ChecklistItem[] = []; dispchecklist: ChecklistItem[] = [];
membersDialogVisible = false;
tripMembers: TripMember[] = []; tripMembers: TripMember[] = [];
map?: L.Map; map?: L.Map;
@ -112,86 +117,7 @@ export class TripComponent implements AfterViewInit {
tripMapAntLayer?: L.FeatureGroup; tripMapAntLayer?: L.FeatureGroup;
tripMapAntLayerDayID?: number; tripMapAntLayerDayID?: number;
readonly menuTripActionsItems: MenuItem[] = [ menuTripActionsItems: MenuItem[] = [];
{
label: 'Lists',
items: [
{
label: 'Checklist',
icon: 'pi pi-list-check',
command: () => {
this.openChecklist();
},
},
{
label: 'Packing list',
icon: 'pi pi-briefcase',
command: () => {
this.openPackingList();
},
},
],
},
{
label: 'Collaboration',
items: [
{
label: 'Members',
icon: 'pi pi-users',
command: () => {
this.openMembersDialog();
},
},
{
label: 'Share',
icon: 'pi pi-share-alt',
command: () => {
this.shareDialogVisible = true;
},
},
],
},
{
label: 'Trip',
items: [
{
label: 'Pretty Print',
icon: 'pi pi-print',
command: () => {
this.togglePrint();
},
},
{
label: 'Notes',
icon: 'pi pi-info-circle',
command: () => {
this.openTripNotesModal();
},
},
{
label: 'Archive',
icon: 'pi pi-box',
command: () => {
this.openArchiveTripModal();
},
},
{
label: 'Edit',
icon: 'pi pi-pencil',
command: () => {
this.editTrip();
},
},
{
label: 'Delete',
icon: 'pi pi-trash',
command: () => {
this.deleteTrip();
},
},
],
},
];
readonly menuTripTableActionsItems: MenuItem[] = [ readonly menuTripTableActionsItems: MenuItem[] = [
{ {
label: 'Actions', label: 'Actions',
@ -913,6 +839,12 @@ export class TripComponent implements AfterViewInit {
}); });
} }
toggleArchiveTrip() {
if (!this.trip) return;
if (this.trip.archived) this.openUnarchiveTripModal();
else this.openArchiveTripModal();
}
openArchiveTripModal() { openArchiveTripModal() {
if (!this.trip) return; if (!this.trip) return;
const currentArchiveStatus = this.trip?.archived; const currentArchiveStatus = this.trip?.archived;
@ -1455,7 +1387,7 @@ export class TripComponent implements AfterViewInit {
.subscribe({ .subscribe({
next: () => { next: () => {
this.trip!.shared = false; this.trip!.shared = false;
this.shareDialogVisible = false; this.isShareDialogVisible = false;
}, },
}); });
}, },
@ -1481,7 +1413,7 @@ export class TripComponent implements AfterViewInit {
label: `Quick Paste (${this.utilsService.packingListToCopy.length})`, label: `Quick Paste (${this.utilsService.packingListToCopy.length})`,
icon: 'pi pi-copy', icon: 'pi pi-copy',
command: () => this.pastePackingList(), command: () => this.pastePackingList(),
disabled: !this.utilsService.packingListToCopy.length, disabled: this.trip?.archived || !this.utilsService.packingListToCopy.length,
}, },
], ],
}, },
@ -1501,7 +1433,7 @@ export class TripComponent implements AfterViewInit {
this.computeDispPackingList(); this.computeDispPackingList();
}, },
}); });
this.packingDialogVisible = true; this.isPackingDialogVisible = true;
} }
addPackingItem() { addPackingItem() {
@ -1677,7 +1609,7 @@ export class TripComponent implements AfterViewInit {
this.computeDispChecklistList(); this.computeDispChecklistList();
}, },
}); });
this.checklistDialogVisible = true; this.isChecklistDialogVisible = true;
} }
addChecklistItem() { addChecklistItem() {
@ -1784,7 +1716,7 @@ export class TripComponent implements AfterViewInit {
}, },
}); });
setTimeout(() => { setTimeout(() => {
this.membersDialogVisible = true; this.isMembersDialogVisible = true;
}, 100); }, 100);
} }
@ -1874,7 +1806,7 @@ export class TripComponent implements AfterViewInit {
'1024px': '70vw', '1024px': '70vw',
'640px': '90vw', '640px': '90vw',
}, },
data: this.trip?.notes, data: this.trip,
})!; })!;
modal.onClose.pipe(take(1)).subscribe({ modal.onClose.pipe(take(1)).subscribe({
@ -1889,4 +1821,152 @@ export class TripComponent implements AfterViewInit {
}, },
}); });
} }
openMenuTripActionsItems(event: any) {
const lists = {
label: 'Lists',
items: [
{
label: 'Attachments',
icon: 'pi pi-paperclip',
command: () => {
this.openAttachmentsModal();
},
},
{
label: 'Checklist',
icon: 'pi pi-list-check',
command: () => {
this.openChecklist();
},
},
{
label: 'Packing list',
icon: 'pi pi-briefcase',
command: () => {
this.openPackingList();
},
},
],
};
const collaboration = {
label: 'Collaboration',
items: [
{
label: 'Members',
icon: 'pi pi-users',
command: () => {
this.openMembersDialog();
},
},
{
label: 'Share',
icon: 'pi pi-share-alt',
command: () => {
this.isShareDialogVisible = true;
},
},
],
};
const actions = {
label: 'Trip',
items: [
{
label: 'Pretty Print',
icon: 'pi pi-print',
command: () => {
this.togglePrint();
},
},
{
label: 'Notes',
icon: 'pi pi-info-circle',
command: () => {
this.openTripNotesModal();
},
},
{
label: this.trip?.archived ? 'Unarchive' : 'Archive',
icon: 'pi pi-box',
command: () => {
this.toggleArchiveTrip();
},
},
{
label: 'Edit',
icon: 'pi pi-pencil',
disabled: this.trip?.archived,
command: () => {
this.editTrip();
},
},
{
label: 'Delete',
icon: 'pi pi-trash',
disabled: this.trip?.archived,
command: () => {
this.deleteTrip();
},
},
],
};
this.menuTripActionsItems = [lists, collaboration, actions];
this.menuTripActions.toggle(event);
}
openAttachmentsModal() {
if (!this.trip) return;
this.isAttachmentsDialogVisible = true;
}
onFileUploadInputChange(event: Event) {
if (!this.trip) return;
const input = event.target as HTMLInputElement;
if (!input.files?.length) return;
const formdata = new FormData();
formdata.append('file', input.files[0]);
this.apiService
.postTripAttachment(this.trip?.id, formdata)
.pipe(take(1))
.subscribe({
next: (attachment) => (this.trip!.attachments = [...this.trip!.attachments!, attachment]),
});
}
downloadAttachment(attachment: TripAttachment) {
if (!this.trip) return;
this.apiService
.downloadTripAttachment(this.trip.id, attachment.id)
.pipe(take(1))
.subscribe({
next: (data) => {
const blob = new Blob([data], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.download = attachment.filename;
anchor.href = url;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
window.URL.revokeObjectURL(url);
},
});
}
deleteAttachment(attachmentId: number) {
if (!this.trip) return;
this.apiService
.deleteTripAttachment(this.trip.id, attachmentId)
.pipe(take(1))
.subscribe({
next: () => {
this.trip!.attachments = this.trip?.attachments?.filter((att) => att.id != attachmentId);
},
});
}
} }