Trip: item image and gpx, Trip: table distance between items

This commit is contained in:
itskovacs 2025-09-21 15:14:32 +02:00
parent 8d9b8f149c
commit 460cdc93fe
6 changed files with 297 additions and 42 deletions

View File

@ -111,6 +111,7 @@
@if (tripTableSelectedColumns.includes('LatLng')) {<th class="w-12" pResizableColumn>LatLng</th>} @if (tripTableSelectedColumns.includes('LatLng')) {<th class="w-12" pResizableColumn>LatLng</th>}
@if (tripTableSelectedColumns.includes('price')) {<th class="w-12" pResizableColumn>Price</th>} @if (tripTableSelectedColumns.includes('price')) {<th class="w-12" pResizableColumn>Price</th>}
@if (tripTableSelectedColumns.includes('status')) {<th class="w-12" pResizableColumn>Status</th>} @if (tripTableSelectedColumns.includes('status')) {<th class="w-12" pResizableColumn>Status</th>}
@if (tripTableSelectedColumns.includes('distance')) {<th pResizableColumn>Distance (km)</th>}
</tr> </tr>
</ng-template> </ng-template>
@if (tableExpandableMode) { @if (tableExpandableMode) {
@ -148,10 +149,19 @@
</div> </div>
} @else {-} } @else {-}
</td>} </td>}
@if (tripTableSelectedColumns.includes('comment')) {<td> @if (tripTableSelectedColumns.includes('comment')) {<td class="relative">
@if (tripitem.image) {
<div
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
<img [src]="tripitem.image"
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" /> {{
tripitem.comment }}
</div>
} @else {
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none"> <div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
{{ tripitem.comment || '-' }} {{ tripitem.comment || '-' }}
</div> </div>
}
</td>} </td>}
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm"> @if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
<div class="print:max-w-full truncate"> <div class="print:max-w-full truncate">
@ -166,6 +176,9 @@
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color" [style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.status.label }}</span>}</td>} tripitem.status.label }}</span>}</td>}
@if (tripTableSelectedColumns.includes('distance')) {<td class="text-sm">
<div class="print:max-w-full truncate">{{ tripitem.distance || '-' }}</div>
</td>}
</tr> </tr>
</ng-template> </ng-template>
} }
@ -192,15 +205,24 @@
@if (tripitem.place) { @if (tripitem.place) {
<div class="ml-7 print:ml-0 max-w-24 truncate print:whitespace-normal"> <div class="ml-7 print:ml-0 max-w-24 truncate print:whitespace-normal">
<img [src]="tripitem.place.image || tripitem.place.category.image" <img [src]="tripitem.place.image || tripitem.place.category.image"
class="absolute left-0 top-1/2 -translate-y-1/2 w-9 rounded-full object-cover print:hidden" /> {{ class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" /> {{
tripitem.place.name }} tripitem.place.name }}
</div> </div>
} @else {-} } @else {-}
</td>} </td>}
@if (tripTableSelectedColumns.includes('comment')) {<td> @if (tripTableSelectedColumns.includes('comment')) {<td class="relative">
@if (tripitem.image) {
<div
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
<img [src]="tripitem.image"
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" /> {{
tripitem.comment }}
</div>
} @else {
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none"> <div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
{{ tripitem.comment || '-' }} {{ tripitem.comment || '-' }}
</div> </div>
}
</td>} </td>}
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm"> @if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
<div class="max-w-20 print:max-w-full truncate"> <div class="max-w-20 print:max-w-full truncate">
@ -215,6 +237,9 @@
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color" [style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.status.label }}</span>}</td>} tripitem.status.label }}</span>}</td>}
@if (tripTableSelectedColumns.includes('distance')) {<td class="text-sm">
<div class="print:max-w-full truncate">{{ tripitem.distance || '-' }}</div>
</td>}
</tr> </tr>
</ng-template> </ng-template>
} }
@ -247,7 +272,7 @@
<div [ngClass]="{ 'grid col-span-full grid-cols-4': isExpanded, 'flex flex-col sticky': !isExpanded }" <div [ngClass]="{ 'grid col-span-full grid-cols-4': isExpanded, 'flex flex-col sticky': !isExpanded }"
class="gap-4 top-4 self-start max-w-screen print:hidden min-w-0"> class="gap-4 top-4 self-start max-w-screen print:hidden min-w-0">
@if (selectedItem) { @if (selectedItem) {
<div class="p-4 w-full max-w-full min-h-20 md:max-h-[600px] rounded-md shadow text-center"> <div class="p-4 w-full max-w-full min-h-20 rounded-md shadow text-center">
<div class="flex justify-between items-center mb-3"> <div class="flex justify-between items-center mb-3">
<div class="p-2 flex items-center gap-4 w-full max-w-full"> <div class="p-2 flex items-center gap-4 w-full max-w-full">
@if (selectedItem.place) { @if (selectedItem.place) {
@ -260,6 +285,9 @@
</h1> </h1>
<div class="flex items-center gap-2 flex-none"> <div class="flex items-center gap-2 flex-none">
@if (selectedItem.gpx) {
<p-button icon="pi pi-compass" (click)="downloadItemGPX()" text />
}
@if (selectedItem.lat && selectedItem.lng) { @if (selectedItem.lat && selectedItem.lng) {
<p-button icon="pi pi-car" (click)="itemToNavigation()" text /> <p-button icon="pi pi-car" (click)="itemToNavigation()" text />
} }
@ -285,9 +313,11 @@
</div> </div>
@if (selectedItem.place) { @if (selectedItem.place) {
<div class="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"
(click)="editPlace(selectedItem.place)" /></div>
</div> </div>
} }
@ -324,6 +354,12 @@
</div> </div>
} }
</div> </div>
@if (selectedItem.image) {
<div class="p-4 px-2 gap-4 overflow-auto w-full min-h-0 max-h-64">
<img [src]="selectedItem.image" class="w-full object-cover rounded-md" />
</div>
}
</div> </div>
} }
@ -379,7 +415,7 @@
@defer { @defer {
@for (p of places; track p.id) { @for (p of places; track p.id) {
<div class="flex items-center gap-4 py-2 px-4 hover:bg-gray-50 rounded-md overflow-auto dark:hover:bg-gray-800" <div class="flex items-center gap-4 py-2 px-4 hover:bg-gray-50 rounded-md overflow-auto dark:hover:bg-gray-800"
(mouseenter)="placeHighlightMarker(p.lat, p.lng)" (mouseleave)="resetPlaceHighlightMarker()"> (mouseenter)="placeHighlightMarker(p)" (mouseleave)="resetPlaceHighlightMarker()">
<img [src]="p.image || p.category.image" class="w-12 rounded-full object-fit"> <img [src]="p.image || p.category.image" class="w-12 rounded-full object-fit">
<div class="flex flex-col gap-1 truncate"> <div class="flex flex-col gap-1 truncate">
@ -546,8 +582,9 @@
<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 justify-center"> <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 />
</div> </div>
<div class="grid gap-2 mt-4 pb-4"> <div class="grid gap-2 mt-4 pb-4">

View File

@ -24,6 +24,7 @@ import {
placeToMarker, placeToMarker,
createClusterGroup, createClusterGroup,
tripDayMarker, tripDayMarker,
gpxToPolyline,
} from "../../shared/map"; } from "../../shared/map";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog"; import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog";
@ -59,6 +60,7 @@ import { CheckboxChangeEvent, CheckboxModule } from "primeng/checkbox";
import { TripCreatePackingModalComponent } from "../../modals/trip-create-packing-modal/trip-create-packing-modal.component"; import { TripCreatePackingModalComponent } from "../../modals/trip-create-packing-modal/trip-create-packing-modal.component";
import { TripCreateChecklistModalComponent } from "../../modals/trip-create-checklist-modal/trip-create-checklist-modal.component"; import { TripCreateChecklistModalComponent } from "../../modals/trip-create-checklist-modal/trip-create-checklist-modal.component";
import { TripInviteMemberModalComponent } from "../../modals/trip-invite-member-modal/trip-invite-member-modal.component"; import { TripInviteMemberModalComponent } from "../../modals/trip-invite-member-modal/trip-invite-member-modal.component";
import { calculateDistanceBetween } from "../../shared/haversine";
@Component({ @Component({
selector: "app-trip", selector: "app-trip",
@ -113,6 +115,7 @@ export class TripComponent implements AfterViewInit {
map?: L.Map; map?: L.Map;
markerClusterGroup?: L.MarkerClusterGroup; markerClusterGroup?: L.MarkerClusterGroup;
tripMapTemporaryMarker?: L.Marker; tripMapTemporaryMarker?: L.Marker;
tripMapGpxLayer?: L.Layer;
tripMapHoveredElement?: HTMLElement; tripMapHoveredElement?: HTMLElement;
tripMapAntLayer?: L.FeatureGroup; tripMapAntLayer?: L.FeatureGroup;
tripMapAntLayerDayID?: number; tripMapAntLayerDayID?: number;
@ -262,6 +265,7 @@ export class TripComponent implements AfterViewInit {
"LatLng", "LatLng",
"price", "price",
"status", "status",
"distance",
]; ];
tripTableSelectedColumns: string[] = [ tripTableSelectedColumns: string[] = [
"day", "day",
@ -409,6 +413,7 @@ export class TripComponent implements AfterViewInit {
flattenTripDayItems(searchValue?: string) { flattenTripDayItems(searchValue?: string) {
this.sortTripDays(); this.sortTripDays();
let prevLat: number, prevLng: number;
this.flattenedTripItems = this.trip!.days.flatMap((day) => this.flattenedTripItems = this.trip!.days.flatMap((day) =>
[...day.items] [...day.items]
.filter((item) => .filter((item) =>
@ -419,20 +424,39 @@ export class TripComponent implements AfterViewInit {
: true, : true,
) )
.sort((a, b) => a.time.localeCompare(b.time)) .sort((a, b) => a.time.localeCompare(b.time))
.map((item) => ({ .map((item) => {
td_id: day.id, const lat = item.lat ?? (item.place ? item.place.lat : undefined);
td_label: day.label, const lng = item.lng ?? (item.place ? item.place.lng : undefined);
id: item.id,
time: item.time, let distance: number | undefined;
text: item.text, if (lat && lng) {
status: this.statusToTripStatus(item.status as string), if (prevLat && prevLng) {
comment: item.comment, const d = calculateDistanceBetween(prevLat, prevLng, lat, lng);
price: item.price || undefined, distance = +(Math.round(d * 1000) / 1000).toFixed(2);
day_id: item.day_id, }
place: item.place, prevLat = lat;
lat: item.lat || (item.place ? item.place.lat : undefined), prevLng = lng;
lng: item.lng || (item.place ? item.place.lng : undefined), }
})),
return {
td_id: day.id,
td_label: day.label,
id: item.id,
time: item.time,
text: item.text,
status: this.statusToTripStatus(item.status as string),
comment: item.comment,
price: item.price || undefined,
day_id: item.day_id,
place: item.place,
image: item.image,
image_id: item.image_id,
gpx: item.gpx,
lat,
lng,
distance,
};
}),
); );
} }
@ -517,30 +541,39 @@ export class TripComponent implements AfterViewInit {
this.map?.removeLayer(this.tripMapTemporaryMarker); this.map?.removeLayer(this.tripMapTemporaryMarker);
this.tripMapTemporaryMarker = undefined; this.tripMapTemporaryMarker = undefined;
} }
if (this.tripMapGpxLayer) {
this.map?.removeLayer(this.tripMapGpxLayer);
this.tripMapGpxLayer = undefined;
}
this.resetMapBounds(); this.resetMapBounds();
} }
placeHighlightMarker(lat: number, lng: number) { placeHighlightMarker(item: any) {
if (this.tripMapHoveredElement || this.tripMapTemporaryMarker) if (this.tripMapHoveredElement || this.tripMapTemporaryMarker)
this.resetPlaceHighlightMarker(); this.resetPlaceHighlightMarker();
let marker: L.Marker | undefined; let marker: L.Marker | undefined;
this.markerClusterGroup?.eachLayer((layer: any) => { this.markerClusterGroup?.eachLayer((layer: any) => {
if (layer.getLatLng && layer.getLatLng().equals([lat, lng])) { if (layer.getLatLng && layer.getLatLng().equals([item.lat, item.lng])) {
marker = layer; marker = layer;
} }
}); });
if (item.gpx) {
this.tripMapGpxLayer = gpxToPolyline(item.gpx);
this.tripMapGpxLayer.addTo(this.map!);
}
if (!marker) { if (!marker) {
// TripItem without place, but latlng // TripItem without place, but latlng
const item = {
text: this.selectedItem?.text || "",
lat: lat,
lng: lng,
time: this.selectedItem?.time || "",
};
this.tripMapTemporaryMarker = tripDayMarker(item).addTo(this.map!); this.tripMapTemporaryMarker = tripDayMarker(item).addTo(this.map!);
this.map?.fitBounds([[lat, lng]], { padding: [60, 60] }); if (this.tripMapGpxLayer) {
this.map?.fitBounds(
[[item.lat, item.lng], (this.tripMapGpxLayer as any).getBounds()],
{ padding: [30, 30] },
);
} else this.map?.fitBounds([[item.lat, item.lng]], { padding: [60, 60] });
return; return;
} }
@ -602,6 +635,7 @@ export class TripComponent implements AfterViewInit {
isPlace: !!item.place, isPlace: !!item.place,
idx: idx, idx: idx,
time: item.time, time: item.time,
gpx: item.gpx,
}; };
if (item.lat && item.lng) if (item.lat && item.lng)
@ -680,8 +714,9 @@ export class TripComponent implements AfterViewInit {
prevPoint = coords[0]; prevPoint = coords[0];
} }
group.forEach((day: any) => { group.forEach((data: any) => {
if (!day.isPlace) layGroup.addLayer(tripDayMarker(day)); if (!data.isPlace) layGroup.addLayer(tripDayMarker(data));
if (data.gpx) layGroup.addLayer(gpxToPolyline(data.gpx));
}); });
}); });
@ -724,6 +759,7 @@ export class TripComponent implements AfterViewInit {
lng: item.lng, lng: item.lng,
isPlace: !!item.place, isPlace: !!item.place,
time: item.time, time: item.time,
gpx: item.gpx,
}; };
if (item.place && item.place) if (item.place && item.place)
return { return {
@ -732,6 +768,7 @@ export class TripComponent implements AfterViewInit {
lng: item.place.lng, lng: item.place.lng,
isPlace: true, isPlace: true,
time: item.time, time: item.time,
gpx: item.gpx,
}; };
return undefined; return undefined;
}) })
@ -769,6 +806,7 @@ export class TripComponent implements AfterViewInit {
layGroup.addLayer(path); layGroup.addLayer(path);
items.forEach((item) => { items.forEach((item) => {
if (!item.isPlace) layGroup.addLayer(tripDayMarker(item)); if (!item.isPlace) layGroup.addLayer(tripDayMarker(item));
if (item.gpx) layGroup.addLayer(gpxToPolyline(item.gpx));
}); });
if (this.tripMapAntLayer) { if (this.tripMapAntLayer) {
@ -790,7 +828,7 @@ export class TripComponent implements AfterViewInit {
this.resetPlaceHighlightMarker(); this.resetPlaceHighlightMarker();
} else { } else {
this.selectedItem = item; this.selectedItem = item;
if (item.lat && item.lng) this.placeHighlightMarker(item.lat, item.lng); if (item.lat && item.lng) this.placeHighlightMarker(item);
} }
} }
@ -809,7 +847,7 @@ export class TripComponent implements AfterViewInit {
this.resetPlaceHighlightMarker(); this.resetPlaceHighlightMarker();
this.selectedItem = item; this.selectedItem = item;
this.placeHighlightMarker(item.lat!, item.lng!); this.placeHighlightMarker(item);
} }
deleteTrip() { deleteTrip() {
@ -942,6 +980,18 @@ export class TripComponent implements AfterViewInit {
window.open(url, "_blank"); window.open(url, "_blank");
} }
downloadItemGPX() {
if (!this.selectedItem?.gpx) return;
const dataBlob = new Blob([this.selectedItem.gpx]);
const downloadURL = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.href = downloadURL;
link.download = `TRIP_${this.trip?.name}_${this.selectedItem.text}.gpx`;
link.click();
link.remove();
URL.revokeObjectURL(downloadURL);
}
tripToNavigation() { tripToNavigation() {
// TODO: More services // TODO: More services
const items = this.flattenedTripItems.filter( const items = this.flattenedTripItems.filter(
@ -1284,6 +1334,47 @@ export class TripComponent implements AfterViewInit {
}); });
} }
editPlace(pEdit: Place) {
const modal: DynamicDialogRef = this.dialogService.open(
PlaceCreateModalComponent,
{
header: "Edit Place",
modal: true,
appendTo: "body",
closable: true,
dismissableMask: true,
width: "55vw",
breakpoints: {
"1920px": "70vw",
"1260px": "90vw",
},
data: {
place: { ...pEdit, category: pEdit.category.id },
},
},
);
modal.onClose.pipe(take(1)).subscribe({
next: (p: Place | null) => {
if (!p) return;
this.apiService
.putPlace(p.id, p)
.pipe(take(1))
.subscribe({
next: (place: Place) => {
const places = [...this.places];
const idx = places.findIndex((p) => p.id == place.id);
if (idx > -1) places.splice(idx, 1, place);
places.push(place);
places.sort((a, b) => a.name.localeCompare(b.name));
if (this.selectedItem?.place) this.selectedItem.place = place;
},
});
},
});
}
updateItemFromTrip(old: TripItem, updated: TripItem): void { updateItemFromTrip(old: TripItem, updated: TripItem): void {
if (!this.trip) return; if (!this.trip) return;

View File

@ -4,7 +4,7 @@
@if (itemForm.get('id')?.value !== -1) { @if (itemForm.get('id')?.value !== -1) {
<p-floatlabel variant="in" class="min-w-0"> <p-floatlabel variant="in" class="min-w-0">
<p-select [options]="days" optionValue="id" optionLabel="label" inputId="day_id" id="day_id" <p-select [options]="days" optionValue="id" optionLabel="label" inputId="day_id" id="day_id"
scrollHeight="320px" appendTo="body" formControlName="day_id" [checkmark]="true" fluid> scrollHeight="320px" appendTo="body" formControlName="day_id" [checkmark]="true" [filter]="true" fluid>
<ng-template let-day #item> <ng-template let-day #item>
<div class="whitespace-normal">{{ day.label }}</div> <div class="whitespace-normal">{{ day.label }}</div>
</ng-template> </ng-template>
@ -14,7 +14,7 @@
} @else { } @else {
<p-multiselect [options]="days" optionValue="id" optionLabel="label" formControlName="day_id" placeholder="Days" <p-multiselect [options]="days" optionValue="id" optionLabel="label" formControlName="day_id" placeholder="Days"
[filter]="false" [showToggleAll]="false" display="chip" styleClass="flex items-center" scrollHeight="320px" [filter]="false" [showToggleAll]="false" display="chip" styleClass="flex items-center" scrollHeight="320px"
appendTo="body" selectedItemsLabel="{0} days selected" fluid> appendTo="body" selectedItemsLabel="{0} days selected" [filter]="true" fluid>
<ng-template let-day #item> <ng-template let-day #item>
<div class="whitespace-normal">{{ day.label }}</div> <div class="whitespace-normal">{{ day.label }}</div>
</ng-template> </ng-template>
@ -34,8 +34,8 @@
</div> </div>
</div> </div>
<div class="mt-4 grid md:grid-cols-7 gap-4"> <div class="mt-4 grid md:grid-cols-7 grid-cols-2 gap-4">
<p-floatlabel variant="in" class="md:col-span-2"> <p-floatlabel variant="in" class="col-span-2">
<p-select [options]="places" appendTo="body" optionValue="id" optionLabel="name" inputId="place" id="place" <p-select [options]="places" appendTo="body" optionValue="id" optionLabel="name" inputId="place" id="place"
[filter]="true" filterBy="name" formControlName="place" [showClear]="true" class="capitalize" fluid> [filter]="true" filterBy="name" formControlName="place" [showClear]="true" class="capitalize" fluid>
@ -84,11 +84,38 @@
</p-floatlabel> </p-floatlabel>
</div> </div>
<div class="mt-4"> <div class="mt-4 grid col-span-full md:grid-cols-7">
<p-floatlabel variant="in" class="w-full"> <p-floatlabel variant="in" class="col-span-full md:col-span-5">
<textarea pTextarea id="comment" formControlName="comment" rows="5" autoResize fluid></textarea> <textarea pTextarea id="comment" formControlName="comment" rows="3" autoResize fluid></textarea>
<label for="comment">Comment</label> <label for="comment">Comment</label>
</p-floatlabel> </p-floatlabel>
<div class="mt-4 md:mt-0 grid place-items-center">
@if (itemForm.get("image")?.value) {
<div class="w-2/3 relative group cursor-pointer" (click)="clearImage()">
<img [src]="itemForm.get('image')?.value"
class="w-full max-h-20 object-cover rounded-md shadow-lg transition-transform duration-300" />
<div
class="absolute inset-0 bg-black/50 rounded-md flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<i class="pi pi-trash text-white text-3xl"></i>
</div>
</div>
} @else {
<p-button text label="Select image" icon="pi pi-image" (click)="imageInput.click()" />
}
<input type="file" accept="image/*" #imageInput class="hidden" (change)="onImageSelected($event)" />
</div>
<div class="mt-4 md:mt-0 grid place-items-center">
<div class="flex justify-center items-center">
@if (itemForm.get('gpx')?.value) {
<p-button text icon="pi pi-times" label="GPX" severity="danger" (click)="clearGPX()" />
} @else {
<p-button text icon="pi pi-paperclip" label="GPX" (click)="gpxInput.click()" />
}
<input type="file" accept=".gpx" #gpxInput class="hidden" (change)="onGPXSelected($event)" />
</div>
</div>
</div> </div>
</div> </div>
@ -97,3 +124,14 @@
!== -1 ? "Update" : "Create" }}</p-button> !== -1 ? "Update" : "Create" }}</p-button>
</div> </div>
</section> </section>
<p-popover #op>
<span class="font-medium block mb-2">Members</span>
<div class="flex flex-col gap-2">
@for (member of members; track member) {
<div class="flex items-center gap-2 p-2 hover:bg-emphasis cursor-pointer rounded-border"
[class.font-semibold]="selectedPriceMember == member.user" (click)="selectPriceMember(member.user)">{{ member.user
}}</div>
}
</div>
</p-popover>

View File

@ -46,6 +46,8 @@ export class TripCreateDayItemModalComponent {
days: TripDay[] = []; days: TripDay[] = [];
places: Place[] = []; places: Place[] = [];
statuses: TripStatus[] = []; statuses: TripStatus[] = [];
previous_image_id: number | null = null;
previous_image: string | null = null;
constructor( constructor(
private ref: DynamicDialogRef, private ref: DynamicDialogRef,
@ -72,6 +74,9 @@ export class TripCreateDayItemModalComponent {
place: null, place: null,
status: null, status: null,
price: null, price: null,
image: null,
image_id: null,
gpx: null,
lat: [ lat: [
"", "",
{ {
@ -156,7 +161,64 @@ export class TripCreateDayItemModalComponent {
ret["lat"] = null; ret["lat"] = null;
ret["lng"] = null; ret["lng"] = null;
} }
if (ret["image_id"]) {
delete ret["image"];
delete ret["image_id"];
}
if (ret["gpx"] == "1") delete ret["gpx"];
if (!ret["place"]) delete ret["place"]; if (!ret["place"]) delete ret["place"];
this.ref.close(ret); this.ref.close(ret);
} }
onImageSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files?.length) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = (e) => {
if (this.itemForm.get("image_id")?.value) {
this.previous_image_id = this.itemForm.get("image_id")?.value;
this.previous_image = this.itemForm.get("image")?.value;
this.itemForm.get("image_id")?.setValue(null);
}
this.itemForm.get("image")?.setValue(e.target?.result as string);
this.itemForm.get("image")?.markAsDirty();
};
reader.readAsDataURL(file);
}
}
clearImage() {
this.itemForm.get("image")?.setValue(null);
this.itemForm.get("image_id")?.setValue(null);
this.itemForm.markAsDirty();
if (this.previous_image && this.previous_image_id) {
this.itemForm.get("image_id")?.setValue(this.previous_image_id);
this.itemForm.get("image")?.setValue(this.previous_image);
}
}
onGPXSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = (e) => {
this.itemForm.get("gpx")?.setValue(e.target?.result as string);
this.itemForm.get("gpx")?.markAsDirty();
};
reader.readAsText(file);
}
}
clearGPX() {
this.itemForm.get("gpx")?.setValue(null);
this.itemForm.get("gpx")?.markAsDirty();
}
} }

View File

@ -0,0 +1,20 @@
export function calculateDistanceBetween(
lat1: number,
lon1: number,
lat2: number,
lon2: number,
) {
// returns d in meter
const toRad = (deg: number) => (deg * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const rLat1 = toRad(lat1);
const rLat2 = toRad(lat2);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(rLat1) * Math.cos(rLat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const R = 6371;
return R * c;
}

View File

@ -44,6 +44,9 @@ export interface TripItem {
price?: number; price?: number;
day_id: number; day_id: number;
status?: string | TripStatus; status?: string | TripStatus;
image?: string;
image_id?: number;
gpx?: string;
} }
export interface TripStatus { export interface TripStatus {
@ -64,6 +67,10 @@ export interface FlattenedTripItem {
lng?: number; lng?: number;
day_id: number; day_id: number;
status?: TripStatus; status?: TripStatus;
distance?: number;
image?: string;
image_id?: number;
gpx?: string;
} }
export interface TripMember { export interface TripMember {