💄 Shared Trip: Trip sync
This commit is contained in:
parent
93b63df79a
commit
164f3d5a15
@ -72,7 +72,8 @@
|
|||||||
text />
|
text />
|
||||||
<p-button label="Highlight" pTooltip="Show itinerary on map" icon="pi pi-directions"
|
<p-button label="Highlight" pTooltip="Show itinerary on map" icon="pi pi-directions"
|
||||||
[severity]="tripMapAntLayerDayID == -1 ? 'help' : 'primary'" (click)="toggleTripDaysHighlight()" text />
|
[severity]="tripMapAntLayerDayID == -1 ? 'help' : 'primary'" (click)="toggleTripDaysHighlight()" text />
|
||||||
<p-button pTooltip="Pretty Print" icon="pi pi-print" (click)="togglePrint()" text />
|
<p-button icon="pi pi-ellipsis-v" label="Export" (click)="menuTripExport.toggle($event)" text />
|
||||||
|
<p-menu #menuTripExport [model]="menuTripExportItems" appendTo="body" [popup]="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex md:hidden items-center">
|
<div class="flex md:hidden items-center">
|
||||||
@ -239,6 +240,7 @@
|
|||||||
<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)="toggleTripDayHighlight(tripitem.day_id); $event.stopPropagation()">
|
(click)="toggleTripDayHighlight(tripitem.day_id); $event.stopPropagation()">
|
||||||
|
<div class="text-xs text-gray-500">{{ tripitem.td_date | date: 'd MMM, y' }}</div>
|
||||||
<div class="truncate">{{ tripitem.td_label }}</div>
|
<div class="truncate">{{ tripitem.td_label }}</div>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
@ -262,7 +264,7 @@
|
|||||||
<div [style.background]="tripitem.place.category.color + '1A'"
|
<div [style.background]="tripitem.place.category.color + '1A'"
|
||||||
class="inline-flex items-center gap-2 text-gray-800 font-medium px-1 py-1 pr-3 rounded-full">
|
class="inline-flex items-center gap-2 text-gray-800 font-medium px-1 py-1 pr-3 rounded-full">
|
||||||
<div class="size-6 flex items-center justify-center bg-white rounded-full overflow-hidden flex-shrink-0">
|
<div class="size-6 flex items-center justify-center bg-white rounded-full overflow-hidden flex-shrink-0">
|
||||||
<img [src]="tripitem.place.image" class="size-full object-cover" />
|
<img [src]="tripitem.place.image || tripitem.place.category.image" class="size-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm truncate min-w-0">{{ tripitem.place.name }}</span>
|
<span class="text-sm truncate min-w-0">{{ tripitem.place.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -559,6 +561,9 @@
|
|||||||
{{ d.label }}
|
{{ d.label }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 flex-none">
|
<div class="flex items-center gap-2 flex-none">
|
||||||
|
<span
|
||||||
|
class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit flex items-center group-hover:hidden dark:bg-gray-100/85"><i
|
||||||
|
class="pi pi-calendar"></i> {{ (d.dt | date: 'd MMM, y' ) || '-' }}</span>
|
||||||
<span class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit dark:bg-gray-100/85">{{
|
<span class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit dark:bg-gray-100/85">{{
|
||||||
getDayStats(d).price || '-' }}
|
getDayStats(d).price || '-' }}
|
||||||
@if (getDayStats(d).price) {
|
@if (getDayStats(d).price) {
|
||||||
@ -678,22 +683,25 @@
|
|||||||
class="flex items-center gap-2 w-full cursor-pointer">
|
class="flex items-center gap-2 w-full cursor-pointer">
|
||||||
<p-checkbox disabled [binary]="true" [inputId]="item.id.toString()" [(ngModel)]="item.packed" />
|
<p-checkbox disabled [binary]="true" [inputId]="item.id.toString()" [(ngModel)]="item.packed" />
|
||||||
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
||||||
@if (item.qt) {
|
@if (item.qt) {<span class="text-gray-400 mr-0.5">{{ item.qt }}</span>}
|
||||||
<span class="text-gray-400 mr-0.5">{{ item.qt }}</span>
|
|
||||||
}
|
|
||||||
<span>{{ item.text }}</span>
|
<span>{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</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)]="checklistDialogVisible" styleClass="w-[95%] md:w-[80%]">
|
||||||
<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="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) {
|
||||||
@ -792,7 +800,7 @@
|
|||||||
<div
|
<div
|
||||||
class="inline-flex items-center gap-2 bg-gray-100 text-gray-800 font-medium px-1 py-1 pr-3 rounded-full">
|
class="inline-flex items-center gap-2 bg-gray-100 text-gray-800 font-medium px-1 py-1 pr-3 rounded-full">
|
||||||
<div class="size-6 flex items-center justify-center bg-white rounded-full overflow-hidden flex-shrink-0">
|
<div class="size-6 flex items-center justify-center bg-white rounded-full overflow-hidden flex-shrink-0">
|
||||||
<img [src]="item.place.image" class="size-full object-cover" />
|
<img [src]="item.place.image || item.place.category.image" class="size-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm">{{ item.place.name }}</span>
|
<span class="text-sm">{{ item.place.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,6 +25,8 @@ import { InputTextModule } from 'primeng/inputtext';
|
|||||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { calculateDistanceBetween } from '../../shared/haversine';
|
import { calculateDistanceBetween } from '../../shared/haversine';
|
||||||
import { orderByPipe } from '../../shared/order-by.pipe';
|
import { orderByPipe } from '../../shared/order-by.pipe';
|
||||||
|
import { generateTripICSFile } from '../trip/ics';
|
||||||
|
import { generateTripCSVFile } from '../trip/csv';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-shared-trip',
|
selector: 'app-shared-trip',
|
||||||
@ -168,6 +170,30 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
readonly menuTripExportItems: MenuItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Actions',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Calendar (.ics)',
|
||||||
|
icon: 'pi pi-calendar',
|
||||||
|
command: () => generateTripICSFile(this.flattenedTripItems, this.trip?.name, this.utilsService),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CSV',
|
||||||
|
icon: 'pi pi-file',
|
||||||
|
command: () => generateTripCSVFile(this.flattenedTripItems, this.trip?.name),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pretty Print',
|
||||||
|
icon: 'pi pi-print',
|
||||||
|
command: () => {
|
||||||
|
this.togglePrint();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
readonly tripTableColumns: string[] = [
|
readonly tripTableColumns: string[] = [
|
||||||
'day',
|
'day',
|
||||||
'time',
|
'time',
|
||||||
@ -299,52 +325,62 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flattenTripDayItems(searchValue?: string) {
|
flattenTripDayItems(searchValue?: string) {
|
||||||
|
const searchLower = (searchValue || '').toLowerCase();
|
||||||
let prevLat: number, prevLng: number;
|
let prevLat: number, prevLng: number;
|
||||||
this.flattenedTripItems = this.trip!.days.flatMap((day) =>
|
this.flattenedTripItems = this.trip!.days.flatMap((day) => day.items.map((item) => ({ item, day })))
|
||||||
[...day.items]
|
.filter(
|
||||||
.filter((item) =>
|
({ item }) =>
|
||||||
searchValue
|
!searchLower ||
|
||||||
? item.text.toLowerCase().includes(searchValue) ||
|
item.text.toLowerCase().includes(searchLower) ||
|
||||||
item.place?.name.toLowerCase().includes(searchValue) ||
|
item.place?.name.toLowerCase().includes(searchLower) ||
|
||||||
item.comment?.toLowerCase().includes(searchValue)
|
item.comment?.toLowerCase().includes(searchLower),
|
||||||
: true,
|
)
|
||||||
)
|
.sort((a, b) => {
|
||||||
.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0))
|
const dateA = a.day.dt;
|
||||||
.map((item) => {
|
const dateB = b.day.dt;
|
||||||
const lat = item.lat ?? (item.place ? item.place.lat : undefined);
|
if (dateA && dateB) return dateA.localeCompare(dateB) || (a.item.time || '').localeCompare(b.item.time || '');
|
||||||
const lng = item.lng ?? (item.place ? item.place.lng : undefined);
|
if (!dateA && !dateB) {
|
||||||
|
return (
|
||||||
|
(a.day.label || '').localeCompare(b.day.label || '') || (a.item.time || '').localeCompare(b.item.time || '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return dateA ? -1 : 1;
|
||||||
|
})
|
||||||
|
.map(({ item, day }) => {
|
||||||
|
const lat = item.lat ?? item.place?.lat;
|
||||||
|
const lng = item.lng ?? item.place?.lng;
|
||||||
|
|
||||||
let distance: number | undefined;
|
let distance: number | undefined;
|
||||||
if (lat && lng) {
|
if (lat && lng) {
|
||||||
if (prevLat && prevLng) {
|
if (prevLat && prevLng) {
|
||||||
const d = calculateDistanceBetween(prevLat, prevLng, lat, lng);
|
const d = calculateDistanceBetween(prevLat, prevLng, lat, lng);
|
||||||
distance = +(Math.round(d * 1000) / 1000).toFixed(2);
|
distance = +(Math.round(d * 1000) / 1000).toFixed(2);
|
||||||
}
|
|
||||||
prevLat = lat;
|
|
||||||
prevLng = lng;
|
|
||||||
}
|
}
|
||||||
|
prevLat = lat;
|
||||||
|
prevLng = lng;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
td_id: day.id,
|
td_id: day.id,
|
||||||
td_label: day.label,
|
td_label: day.label,
|
||||||
id: item.id,
|
td_date: day.dt,
|
||||||
time: item.time,
|
id: item.id,
|
||||||
text: item.text,
|
time: item.time,
|
||||||
status: this.statusToTripStatus(item.status as string),
|
text: item.text,
|
||||||
comment: item.comment,
|
status: this.statusToTripStatus(item.status as string),
|
||||||
price: item.price || undefined,
|
comment: item.comment,
|
||||||
day_id: item.day_id,
|
price: item.price || undefined,
|
||||||
place: item.place,
|
day_id: item.day_id,
|
||||||
image: item.image,
|
place: item.place,
|
||||||
image_id: item.image_id,
|
image: item.image,
|
||||||
gpx: item.gpx,
|
image_id: item.image_id,
|
||||||
lat,
|
gpx: item.gpx,
|
||||||
lng,
|
lat,
|
||||||
distance,
|
lng,
|
||||||
paid_by: item.paid_by,
|
distance,
|
||||||
};
|
paid_by: item.paid_by,
|
||||||
}),
|
};
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
computePlacesUsedInTable() {
|
computePlacesUsedInTable() {
|
||||||
@ -779,11 +815,11 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openChecklist() {
|
openChecklist() {
|
||||||
if (!this.trip) return;
|
if (!this.token) return;
|
||||||
|
|
||||||
if (!this.checklistItems.length)
|
if (!this.checklistItems.length)
|
||||||
this.apiService
|
this.apiService
|
||||||
.getChecklist(this.trip.id)
|
.getSharedTripChecklist(this.token)
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (items) => {
|
next: (items) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user