💄 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>
}
<section class="p-4 print:px-1 grid md: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">
<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 lg:col-span-2 max-w-screen print:col-span-full">
<div [class.sticky]="!isMapFullscreen"
class="top-0 z-10 bg-white p-2 mb-2 flex justify-between items-center dark:bg-surface-900">
<div>
@ -91,7 +91,13 @@
</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">
@if (tripitem.place) {
<div class="ml-7 print:ml-0 max-w-24 truncate print:whitespace-normal">
@ -144,23 +150,27 @@
<div class="flex flex-col gap-4 sticky top-4 self-start max-w-screen print:hidden">
@if (selectedItem) {
<div class="p-4 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="hidden md:flex h-20 w-32">
<div class="p-4 w-full max-w-full min-h-20 md:max-h-[600px] rounded-md shadow text-center">
<div class="flex justify-between items-center mb-3">
<div class="p-2 flex items-center gap-4 w-full max-w-full">
@if (selectedItem.place) {
<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 gap-2">
<p-button icon="pi pi-trash" [disabled]="trip?.archived" severity="danger" (click)="deleteItem(selectedItem)"
text />
<div class="flex items-center justify-between gap-2 w-full">
<h1 class="text-start font-semibold mb-0 line-clamp-1 md:text-xl">{{ selectedItem.text }}
</h1>
<div class="flex items-center gap-2 flex-none">
<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 class="p-4 px-2 grid md:grid-cols-3 gap-4 overflow-auto w-full">
<div class="rounded-md shadow p-4 w-full">
@ -176,28 +186,21 @@
@if (selectedItem.place) {
<div class="rounded-md shadow p-4">
<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>
}
@if (selectedItem.comment) {
<div class="md:col-span-2 rounded-md shadow p-4">
<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>
}
@if (selectedItem.lat) {
<div class="rounded-md shadow p-4">
<p class="font-bold mb-1">Latitude</p>
<p class="text-sm text-gray-500">{{ selectedItem.lat }}</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>
<p class="font-bold mb-1 truncate">Latitude, Longitude</p>
<p class="text-sm text-gray-500 truncate">{{ selectedItem.lat }}, {{ selectedItem.lng }}</p>
</div>
}
@ -258,7 +261,8 @@
} @placeholder (minimum 0.4s) {
<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>
@ -275,8 +279,8 @@
<span class="text-xs text-gray-500 truncate">{{ p.place }}</span>
<div class="flex gap-0.5">
<span
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
<span [style.color]="p.category.color" [style.background-color]="p.category.color + '1A'"
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>
@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 { 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 { 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 { UtilsService } from "../../services/utils.service";
import { TripCreateModalComponent } from "../../modals/trip-create-modal/trip-create-modal.component";
import { AsyncPipe } from "@angular/common";
import { MenuItem } from "primeng/api";
import { MenuModule } from "primeng/menu";
import { LinkifyPipe } from "../../shared/linkify.pipe";
import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place-create-modal.component";
@Component({
selector: "app-trip",
@ -46,6 +48,7 @@ import { MenuModule } from "primeng/menu";
ReactiveFormsModule,
InputTextModule,
AsyncPipe,
LinkifyPipe,
FloatLabelModule,
TableModule,
ButtonModule,
@ -171,12 +174,14 @@ export class TripComponent implements AfterViewInit {
this.route.paramMap.subscribe((params) => {
const id = params.get("id");
if (id) {
this.apiService.getTrip(+id).subscribe({
next: (trip) => {
combineLatest({
trip: this.apiService.getTrip(+id),
settings: this.apiService.getSettings(),
})
.pipe(
tap(({ trip, settings }) => {
this.trip = trip;
this.sortTripDays();
this.flattenedTripItems = this.flattenTripDayItems(trip.days);
this.flattenTripDayItems();
this.updateTotalPrice();
let contentMenuItems = [
@ -190,14 +195,15 @@ export class TripComponent implements AfterViewInit {
},
},
];
this.map = createMap(contentMenuItems);
this.map = createMap(contentMenuItems, settings.tile_layer);
this.markerClusterGroup = createClusterGroup().addTo(this.map);
this.setPlacesAndMarkers();
this.map.setView([48.107, -2.988]);
this.resetMapBounds();
},
});
}),
)
.subscribe();
}
});
}
@ -249,8 +255,9 @@ export class TripComponent implements AfterViewInit {
return this.statuses.find((s) => s.label == status) as TripStatus;
}
flattenTripDayItems(days: TripDay[]): FlattenedTripItem[] {
return days.flatMap((day) =>
flattenTripDayItems() {
this.sortTripDays();
this.flattenedTripItems = this.trip!.days.flatMap((day) =>
[...day.items]
.sort((a, b) => a.time.localeCompare(b.time))
.map((item) => ({
@ -270,7 +277,7 @@ export class TripComponent implements AfterViewInit {
);
}
makePlacesUsedInTable() {
computePlacesUsedInTable() {
this.placesUsedInTable.clear();
this.flattenedTripItems.forEach((i) => {
if (i.place?.id) this.placesUsedInTable.add(i.place.id);
@ -278,7 +285,7 @@ export class TripComponent implements AfterViewInit {
}
setPlacesAndMarkers() {
this.makePlacesUsedInTable();
this.computePlacesUsedInTable();
this.places = this.trip?.places || [];
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({
next: (day) => {
this.trip?.days.push(day);
this.sortTripDays();
this.flattenedTripItems.push(...this.flattenTripDayItems([day]));
this.flattenTripDayItems();
},
});
},
@ -670,11 +676,7 @@ export class TripComponent implements AfterViewInit {
let index = this.trip?.days.findIndex((d) => d.id == day.id);
if (index != -1) {
this.trip?.days.splice(index as number, 1, day);
this.sortTripDays();
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!,
);
this.dayStatsCache.delete(day.id);
this.flattenTripDayItems();
}
},
});
@ -700,14 +702,9 @@ export class TripComponent implements AfterViewInit {
this.apiService.deleteTripDay(this.trip?.id!, day.id).subscribe({
next: () => {
let index = this.trip?.days.findIndex((d) => d.id == day.id);
if (index != -1) {
this.trip?.days.splice(index as number, 1);
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!,
);
this.dayStatsCache.delete(day.id);
this.makePlacesUsedInTable();
}
this.flattenTripDayItems();
this.setPlacesAndMarkers();
},
});
},
@ -778,18 +775,15 @@ export class TripComponent implements AfterViewInit {
.subscribe({
next: (resp) => {
let index = this.trip?.days.findIndex((d) => d.id == item.day_id);
if (index != -1) {
let td: TripDay = this.trip?.days[index as number]!;
td.items.push(resp);
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!,
);
}
if (item.price) this.updateTotalPrice(item.price);
if (item.place?.id) {
this.placesUsedInTable.add(item.place.id);
this.flattenTripDayItems();
if (resp.price) this.updateTotalPrice(resp.price);
if (resp.place?.id) {
this.placesUsedInTable.add(resp.place.id);
this.dayStatsCache.delete(resp.day_id);
this.setPlacesAndMarkers();
this.dayStatsCache.delete(item.day_id);
}
},
});
@ -829,36 +823,48 @@ export class TripComponent implements AfterViewInit {
this.apiService
.putTripDayItem(it, this.trip?.id!, item.day_id, item.id)
.subscribe({
next: (item) => {
let index = this.trip?.days.findIndex((d) => d.id == item.day_id);
if (index != -1) {
let td: TripDay = this.trip?.days[index as number]!;
td.items.splice(
td.items.findIndex((i) => i.id == item.id),
next: (new_item) => {
if (item.day_id != new_item.day_id) {
let previousIndex = this.trip?.days.findIndex(
(d) => d.id == item.day_id,
);
this.trip?.days[previousIndex as number]!.items.splice(
this.trip?.days[previousIndex as number]!.items.findIndex(
(i) => i.id == new_item.id,
),
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);
}
if (item.place?.id) this.placesUsedInTable.add(item.place.id);
const updatedPrice = -(item.price || 0) + (it.price || 0);
let index = this.trip?.days.findIndex(
(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);
if (this.tripMapAntLayerDayID == item.day_id)
this.toggleTripDayHighlightPathDay(item.day_id);
if (this.tripMapAntLayerDayID == new_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,15 +893,14 @@ export class TripComponent implements AfterViewInit {
let index = this.trip?.days.findIndex(
(d) => d.id == item.day_id,
);
if (index != -1) {
let td: TripDay = this.trip?.days[index as number]!;
td.items.splice(
td.items.findIndex((i) => i.id == item.id),
1,
);
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!,
);
this.flattenTripDayItems();
if (item.place?.id) {
this.placesUsedInTable.delete(item.place.id);
if (item.place.price)
@ -905,7 +910,6 @@ export class TripComponent implements AfterViewInit {
this.dayStatsCache.delete(item.day_id);
this.selectedItem = undefined;
this.resetPlaceHighlightMarker();
}
},
});
},
@ -942,17 +946,54 @@ export class TripComponent implements AfterViewInit {
.pipe(
map((items) => {
let index = this.trip?.days.findIndex((d) => d.id == day_id);
if (index != -1) {
let td: TripDay = this.trip?.days[index as number]!;
td.items.push(...items);
this.flattenedTripItems = this.flattenTripDayItems(
this.trip?.days!,
);
}
this.flattenTripDayItems();
}),
)
.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();
},
});
},
});
},
});
}
}