💄 Create place from a Trip, Display status indicator, Minor fixes

This commit is contained in:
itskovacs 2025-08-05 18:21:32 +02:00
parent 25cf9128fc
commit f4e5c7e4d7
2 changed files with 175 additions and 130 deletions

View File

@ -45,8 +45,8 @@
</div> </div>
} }
<section class="p-4 print:px-1 grid md:grid-cols-3 gap-4 print:block"> <section class="p-4 print:px-1 grid lg:grid-cols-3 gap-4 print:block">
<div class="p-4 shadow self-start rounded-md md:col-span-2 max-w-screen print:col-span-full"> <div class="p-4 shadow self-start rounded-md lg:col-span-2 max-w-screen print:col-span-full">
<div [class.sticky]="!isMapFullscreen" <div [class.sticky]="!isMapFullscreen"
class="top-0 z-10 bg-white p-2 mb-2 flex justify-between items-center dark:bg-surface-900"> class="top-0 z-10 bg-white p-2 mb-2 flex justify-between items-center dark:bg-surface-900">
<div> <div>
@ -91,7 +91,13 @@
</td> </td>
} }
<td class="font-mono text-sm">{{ tripitem.time }}</td> <td class="font-mono text-sm">{{ tripitem.time }}</td>
<td class="max-w-60 truncate">{{ tripitem.text }}</td> <td class="relative max-w-60 truncate">
<div class="relative">
{{ tripitem.text }}
@if (tripitem.status) {<div class="block xl:hidden absolute top-0 -left-1.5 size-1.5 rounded-full"
[style.background]="tripitem.status.color"></div>}
</div>
</td>
<td class="relative"> <td class="relative">
@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">
@ -144,21 +150,25 @@
<div class="flex flex-col gap-4 sticky top-4 self-start max-w-screen print:hidden"> <div class="flex flex-col gap-4 sticky top-4 self-start max-w-screen print:hidden">
@if (selectedItem) { @if (selectedItem) {
<div class="p-4 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 md:max-h-[600px] rounded-md shadow text-center">
<div class="flex items-center justify-between px-2"> <div class="flex justify-between items-center mb-3">
<div class="hidden md:flex h-20 w-32"> <div class="p-2 flex items-center gap-4 w-full max-w-full">
@if (selectedItem.place) { @if (selectedItem.place) {
<img [src]="selectedItem.place.image || selectedItem.place.category.image" <img [src]="selectedItem.place.image || selectedItem.place.category.image"
class="h-full w-full rounded-md object-cover" /> class="hidden md:flex object-cover rounded-md h-20 w-32" />
} }
</div>
<h2 class="text-xl md:text-3xl font-semibold mb-0 truncate max-w-96 md:mx-auto">{{ selectedItem.text }}</h2> <div class="flex items-center justify-between gap-2 w-full">
<div class="flex items-center gap-2"> <h1 class="text-start font-semibold mb-0 line-clamp-1 md:text-xl">{{ selectedItem.text }}
<p-button icon="pi pi-trash" [disabled]="trip?.archived" severity="danger" (click)="deleteItem(selectedItem)" </h1>
text />
<p-button icon="pi pi-pencil" [disabled]="trip?.archived" (click)="editItem(selectedItem)" text /> <div class="flex items-center gap-2 flex-none">
<p-button icon="pi pi-times" [disabled]="trip?.archived" (click)="selectedItem = undefined" text /> <p-button icon="pi pi-trash" [disabled]="trip?.archived" severity="danger"
(click)="deleteItem(selectedItem)" text />
<p-button icon="pi pi-pencil" [disabled]="trip?.archived" (click)="editItem(selectedItem)" text />
<p-button icon="pi pi-times" [disabled]="trip?.archived" (click)="selectedItem = undefined" text />
</div>
</div>
</div> </div>
</div> </div>
@ -176,28 +186,21 @@
@if (selectedItem.place) { @if (selectedItem.place) {
<div class="rounded-md shadow p-4"> <div class="rounded-md shadow p-4">
<p class="font-bold mb-1">Place</p> <p class="font-bold mb-1">Place</p>
<div class="truncate">{{ selectedItem.place.name }}</div> <div class="text-sm text-gray-500 truncate">{{ selectedItem.place.name }}</div>
</div> </div>
} }
@if (selectedItem.comment) { @if (selectedItem.comment) {
<div class="md:col-span-2 rounded-md shadow p-4"> <div class="md:col-span-2 rounded-md shadow p-4">
<p class="font-bold mb-1">Comment</p> <p class="font-bold mb-1">Comment</p>
<p class="text-sm text-gray-500 whitespace-pre-line">{{ selectedItem.comment }}</p> <p class="text-sm text-gray-500 whitespace-pre-line" [innerHTML]="selectedItem.comment | linkify"></p>
</div> </div>
} }
@if (selectedItem.lat) { @if (selectedItem.lat) {
<div class="rounded-md shadow p-4"> <div class="rounded-md shadow p-4">
<p class="font-bold mb-1">Latitude</p> <p class="font-bold mb-1 truncate">Latitude, Longitude</p>
<p class="text-sm text-gray-500">{{ selectedItem.lat }}</p> <p class="text-sm text-gray-500 truncate">{{ selectedItem.lat }}, {{ selectedItem.lng }}</p>
</div>
}
@if (selectedItem.lng) {
<div class="rounded-md shadow p-4">
<p class="font-bold mb-1">Longitude</p>
<p class="text-sm text-gray-500">{{ selectedItem.lng }}</p>
</div> </div>
} }
@ -258,7 +261,8 @@
} @placeholder (minimum 0.4s) { } @placeholder (minimum 0.4s) {
<p-skeleton height="1.75rem" width="2.5rem" class="mr-1" /> <p-skeleton height="1.75rem" width="2.5rem" class="mr-1" />
} }
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="manageTripPlaces()" text /> <p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addPlace()" text />
<p-button icon="pi pi-list-check" [disabled]="trip?.archived" (click)="manageTripPlaces()" text />
</div> </div>
</div> </div>
@ -275,8 +279,8 @@
<span class="text-xs text-gray-500 truncate">{{ p.place }}</span> <span class="text-xs text-gray-500 truncate">{{ p.place }}</span>
<div class="flex gap-0.5"> <div class="flex gap-0.5">
<span <span [style.color]="p.category.color" [style.background-color]="p.category.color + '1A'"
class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded flex gap-2 items-center truncate dark:bg-blue-100/85"><i class="text-xs font-medium me-2 px-2.5 py-0.5 rounded flex gap-2 items-center truncate"><i
class="pi pi-box text-xs"></i>{{ p.category.name }}</span> class="pi pi-box text-xs"></i>{{ p.category.name }}</span>
@if (isPlaceUsed(p.id)) { @if (isPlaceUsed(p.id)) {

View File

@ -28,13 +28,15 @@ import { TripPlaceSelectModalComponent } from "../../modals/trip-place-select-mo
import { TripCreateDayModalComponent } from "../../modals/trip-create-day-modal/trip-create-day-modal.component"; import { TripCreateDayModalComponent } from "../../modals/trip-create-day-modal/trip-create-day-modal.component";
import { TripCreateDayItemModalComponent } from "../../modals/trip-create-day-item-modal/trip-create-day-item-modal.component"; import { TripCreateDayItemModalComponent } from "../../modals/trip-create-day-item-modal/trip-create-day-item-modal.component";
import { TripCreateItemsModalComponent } from "../../modals/trip-create-items-modal/trip-create-items-modal.component"; import { TripCreateItemsModalComponent } from "../../modals/trip-create-items-modal/trip-create-items-modal.component";
import { forkJoin, map, Observable } from "rxjs"; import { combineLatest, forkJoin, map, Observable, tap } from "rxjs";
import { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.component"; import { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.component";
import { UtilsService } from "../../services/utils.service"; 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 } from "@angular/common"; import { AsyncPipe } from "@angular/common";
import { MenuItem } from "primeng/api"; import { MenuItem } from "primeng/api";
import { MenuModule } from "primeng/menu"; import { MenuModule } from "primeng/menu";
import { LinkifyPipe } from "../../shared/linkify.pipe";
import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place-create-modal.component";
@Component({ @Component({
selector: "app-trip", selector: "app-trip",
@ -46,6 +48,7 @@ import { MenuModule } from "primeng/menu";
ReactiveFormsModule, ReactiveFormsModule,
InputTextModule, InputTextModule,
AsyncPipe, AsyncPipe,
LinkifyPipe,
FloatLabelModule, FloatLabelModule,
TableModule, TableModule,
ButtonModule, ButtonModule,
@ -171,33 +174,36 @@ export class TripComponent implements AfterViewInit {
this.route.paramMap.subscribe((params) => { this.route.paramMap.subscribe((params) => {
const id = params.get("id"); const id = params.get("id");
if (id) { if (id) {
this.apiService.getTrip(+id).subscribe({ combineLatest({
next: (trip) => { trip: this.apiService.getTrip(+id),
this.trip = trip; settings: this.apiService.getSettings(),
this.sortTripDays(); })
this.flattenedTripItems = this.flattenTripDayItems(trip.days); .pipe(
tap(({ trip, settings }) => {
this.trip = trip;
this.flattenTripDayItems();
this.updateTotalPrice();
this.updateTotalPrice(); let contentMenuItems = [
{
let contentMenuItems = [ text: "Copy coordinates",
{ callback: (e: any) => {
text: "Copy coordinates", const latlng = e.latlng;
callback: (e: any) => { navigator.clipboard.writeText(
const latlng = e.latlng; `${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`,
navigator.clipboard.writeText( );
`${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`, },
);
}, },
}, ];
]; this.map = createMap(contentMenuItems, settings.tile_layer);
this.map = createMap(contentMenuItems); this.markerClusterGroup = createClusterGroup().addTo(this.map);
this.markerClusterGroup = createClusterGroup().addTo(this.map); this.setPlacesAndMarkers();
this.setPlacesAndMarkers();
this.map.setView([48.107, -2.988]); this.map.setView([48.107, -2.988]);
this.resetMapBounds(); this.resetMapBounds();
}, }),
}); )
.subscribe();
} }
}); });
} }
@ -249,8 +255,9 @@ export class TripComponent implements AfterViewInit {
return this.statuses.find((s) => s.label == status) as TripStatus; return this.statuses.find((s) => s.label == status) as TripStatus;
} }
flattenTripDayItems(days: TripDay[]): FlattenedTripItem[] { flattenTripDayItems() {
return days.flatMap((day) => this.sortTripDays();
this.flattenedTripItems = this.trip!.days.flatMap((day) =>
[...day.items] [...day.items]
.sort((a, b) => a.time.localeCompare(b.time)) .sort((a, b) => a.time.localeCompare(b.time))
.map((item) => ({ .map((item) => ({
@ -270,7 +277,7 @@ export class TripComponent implements AfterViewInit {
); );
} }
makePlacesUsedInTable() { computePlacesUsedInTable() {
this.placesUsedInTable.clear(); this.placesUsedInTable.clear();
this.flattenedTripItems.forEach((i) => { this.flattenedTripItems.forEach((i) => {
if (i.place?.id) this.placesUsedInTable.add(i.place.id); if (i.place?.id) this.placesUsedInTable.add(i.place.id);
@ -278,7 +285,7 @@ export class TripComponent implements AfterViewInit {
} }
setPlacesAndMarkers() { setPlacesAndMarkers() {
this.makePlacesUsedInTable(); this.computePlacesUsedInTable();
this.places = this.trip?.places || []; this.places = this.trip?.places || [];
this.places.sort((a, b) => a.name.localeCompare(b.name)); this.places.sort((a, b) => a.name.localeCompare(b.name));
@ -636,8 +643,7 @@ export class TripComponent implements AfterViewInit {
this.apiService.postTripDay(day, this.trip?.id!).subscribe({ this.apiService.postTripDay(day, this.trip?.id!).subscribe({
next: (day) => { next: (day) => {
this.trip?.days.push(day); this.trip?.days.push(day);
this.sortTripDays(); this.flattenTripDayItems();
this.flattenedTripItems.push(...this.flattenTripDayItems([day]));
}, },
}); });
}, },
@ -670,11 +676,7 @@ export class TripComponent implements AfterViewInit {
let index = this.trip?.days.findIndex((d) => d.id == day.id); let index = this.trip?.days.findIndex((d) => d.id == day.id);
if (index != -1) { if (index != -1) {
this.trip?.days.splice(index as number, 1, day); this.trip?.days.splice(index as number, 1, day);
this.sortTripDays(); this.flattenTripDayItems();
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!,
);
this.dayStatsCache.delete(day.id);
} }
}, },
}); });
@ -700,14 +702,9 @@ export class TripComponent implements AfterViewInit {
this.apiService.deleteTripDay(this.trip?.id!, day.id).subscribe({ this.apiService.deleteTripDay(this.trip?.id!, day.id).subscribe({
next: () => { next: () => {
let index = this.trip?.days.findIndex((d) => d.id == day.id); let index = this.trip?.days.findIndex((d) => d.id == day.id);
if (index != -1) { this.trip?.days.splice(index as number, 1);
this.trip?.days.splice(index as number, 1); this.flattenTripDayItems();
this.flattenedTripItems = this.flattenTripDayItems( this.setPlacesAndMarkers();
this.trip?.days!,
);
this.dayStatsCache.delete(day.id);
this.makePlacesUsedInTable();
}
}, },
}); });
}, },
@ -778,18 +775,15 @@ export class TripComponent implements AfterViewInit {
.subscribe({ .subscribe({
next: (resp) => { next: (resp) => {
let index = this.trip?.days.findIndex((d) => d.id == item.day_id); let index = this.trip?.days.findIndex((d) => d.id == item.day_id);
if (index != -1) { let td: TripDay = this.trip?.days[index as number]!;
let td: TripDay = this.trip?.days[index as number]!; td.items.push(resp);
td.items.push(resp); this.flattenTripDayItems();
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!, if (resp.price) this.updateTotalPrice(resp.price);
); if (resp.place?.id) {
} this.placesUsedInTable.add(resp.place.id);
if (item.price) this.updateTotalPrice(item.price); this.dayStatsCache.delete(resp.day_id);
if (item.place?.id) {
this.placesUsedInTable.add(item.place.id);
this.setPlacesAndMarkers(); this.setPlacesAndMarkers();
this.dayStatsCache.delete(item.day_id);
} }
}, },
}); });
@ -829,36 +823,48 @@ export class TripComponent implements AfterViewInit {
this.apiService this.apiService
.putTripDayItem(it, this.trip?.id!, item.day_id, item.id) .putTripDayItem(it, this.trip?.id!, item.day_id, item.id)
.subscribe({ .subscribe({
next: (item) => { next: (new_item) => {
let index = this.trip?.days.findIndex((d) => d.id == item.day_id); if (item.day_id != new_item.day_id) {
if (index != -1) { let previousIndex = this.trip?.days.findIndex(
let td: TripDay = this.trip?.days[index as number]!; (d) => d.id == item.day_id,
td.items.splice( );
td.items.findIndex((i) => i.id == item.id), this.trip?.days[previousIndex as number]!.items.splice(
this.trip?.days[previousIndex as number]!.items.findIndex(
(i) => i.id == new_item.id,
),
1, 1,
item,
); );
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!,
);
if (this.selectedItem && this.selectedItem.id === item.id)
this.selectedItem = {
...item,
status: item.status
? this.statusToTripStatus(item.status as string)
: undefined,
};
this.dayStatsCache.delete(item.day_id); this.dayStatsCache.delete(item.day_id);
} }
if (item.place?.id) this.placesUsedInTable.add(item.place.id); let index = this.trip?.days.findIndex(
const updatedPrice = -(item.price || 0) + (it.price || 0); (d) => d.id == new_item.day_id,
);
let td: TripDay = this.trip?.days[index as number]!;
td.items.splice(
td.items.findIndex((i) => i.id == new_item.id),
1,
new_item,
);
this.flattenTripDayItems();
if (this.selectedItem && this.selectedItem.id === item.id)
this.selectedItem = {
...new_item,
status: new_item.status
? this.statusToTripStatus(new_item.status as string)
: undefined,
};
this.dayStatsCache.delete(new_item.day_id);
this.computePlacesUsedInTable();
const updatedPrice = -(new_item.price || 0) + (item.price || 0);
this.updateTotalPrice(updatedPrice); this.updateTotalPrice(updatedPrice);
if (this.tripMapAntLayerDayID == item.day_id) if (this.tripMapAntLayerDayID == new_item.day_id)
this.toggleTripDayHighlightPathDay(item.day_id); this.toggleTripDayHighlightPathDay(new_item.day_id);
if (item.place?.id || it.place?.id) this.setPlacesAndMarkers(); if (new_item.place?.id || item.place?.id)
this.setPlacesAndMarkers();
}, },
}); });
}, },
@ -887,25 +893,23 @@ export class TripComponent implements AfterViewInit {
let index = this.trip?.days.findIndex( let index = this.trip?.days.findIndex(
(d) => d.id == item.day_id, (d) => d.id == item.day_id,
); );
if (index != -1) {
let td: TripDay = this.trip?.days[index as number]!; let td: TripDay = this.trip?.days[index as number]!;
td.items.splice( td.items.splice(
td.items.findIndex((i) => i.id == item.id), td.items.findIndex((i) => i.id == item.id),
1, 1,
); );
this.flattenedTripItems = this.flattenTripDayItems( this.flattenTripDayItems();
this.trip?.days!,
); if (item.place?.id) {
if (item.place?.id) { this.placesUsedInTable.delete(item.place.id);
this.placesUsedInTable.delete(item.place.id); if (item.place.price)
if (item.place.price) this.updateTotalPrice(-item.place.price);
this.updateTotalPrice(-item.place.price); this.setPlacesAndMarkers();
this.setPlacesAndMarkers();
}
this.dayStatsCache.delete(item.day_id);
this.selectedItem = undefined;
this.resetPlaceHighlightMarker();
} }
this.dayStatsCache.delete(item.day_id);
this.selectedItem = undefined;
this.resetPlaceHighlightMarker();
}, },
}); });
}, },
@ -942,17 +946,54 @@ export class TripComponent implements AfterViewInit {
.pipe( .pipe(
map((items) => { map((items) => {
let index = this.trip?.days.findIndex((d) => d.id == day_id); let index = this.trip?.days.findIndex((d) => d.id == day_id);
if (index != -1) { let td: TripDay = this.trip?.days[index as number]!;
let td: TripDay = this.trip?.days[index as number]!; td.items.push(...items);
td.items.push(...items); this.flattenTripDayItems();
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!,
);
}
}), }),
) )
.subscribe(); .subscribe();
}, },
}); });
} }
addPlace() {
const modal: DynamicDialogRef = this.dialogService.open(
PlaceCreateModalComponent,
{
header: "Create Place",
modal: true,
appendTo: "body",
closable: true,
dismissableMask: true,
width: "55vw",
breakpoints: {
"1920px": "70vw",
"1260px": "90vw",
},
},
);
modal.onClose.subscribe({
next: (place: Place | null) => {
if (!place) return;
this.apiService.postPlace(place).subscribe({
next: (place: Place) => {
this.apiService
.putTrip(
{ place_ids: [place, ...this.places].map((p) => p.id) },
this.trip?.id!,
)
.subscribe({
next: (trip) => {
this.trip = trip;
this.setPlacesAndMarkers();
this.resetMapBounds();
},
});
},
});
},
});
}
} }