Trip archive feature, 💄 Trip mobile UI menu

This commit is contained in:
itskovacs 2025-07-19 12:09:58 +02:00
parent 7b9d6142b4
commit a53e18d024
6 changed files with 157 additions and 33 deletions

View File

@ -1 +1 @@
__version__ = "1.0.0"
__version__ = "1.1.0"

View File

@ -76,6 +76,9 @@ def update_trip(
db_trip = session.get(Trip, trip_id)
verify_exists_and_owns(current_user, db_trip)
if db_trip.archived and (trip.archived is not False):
raise HTTPException(status_code=400, detail="Bad request")
trip_data = trip.model_dump(exclude_unset=True)
if trip_data.get("image"):
try:
@ -136,6 +139,9 @@ def delete_trip(
db_trip = session.get(Trip, trip_id)
verify_exists_and_owns(current_user, db_trip)
if db_trip.archived:
raise HTTPException(status_code=400, detail="Bad request")
if db_trip.image:
try:
remove_image(db_trip.image.filename)
@ -161,6 +167,9 @@ def create_tripday(
db_trip = session.get(Trip, trip_id)
verify_exists_and_owns(current_user, db_trip)
if db_trip.archived:
raise HTTPException(status_code=400, detail="Bad request")
new_day = TripDay(label=td.label, trip_id=trip_id, user=current_user)
session.add(new_day)
@ -180,6 +189,9 @@ def update_tripday(
db_trip = session.get(Trip, trip_id)
verify_exists_and_owns(current_user, db_trip)
if db_trip.archived:
raise HTTPException(status_code=400, detail="Bad request")
db_day = session.get(TripDay, day_id)
verify_exists_and_owns(current_user, db_day)
if db_day.trip_id != trip_id:
@ -205,6 +217,9 @@ def delete_tripday(
db_trip = session.get(Trip, trip_id)
verify_exists_and_owns(current_user, db_trip)
if db_trip.archived:
raise HTTPException(status_code=400, detail="Bad request")
db_day = session.get(TripDay, day_id)
verify_exists_and_owns(current_user, db_day)
if db_day.trip_id != trip_id:
@ -226,6 +241,9 @@ def create_tripitem(
db_trip = session.get(Trip, trip_id)
verify_exists_and_owns(current_user, db_trip)
if db_trip.archived:
raise HTTPException(status_code=400, detail="Bad request")
db_day = session.get(TripDay, day_id)
if db_day.trip_id != trip_id:
raise HTTPException(status_code=400, detail="Bad request")
@ -265,6 +283,9 @@ def update_tripitem(
db_trip = session.get(Trip, trip_id)
verify_exists_and_owns(current_user, db_trip)
if db_trip.archived:
raise HTTPException(status_code=400, detail="Bad request")
db_day = session.get(TripDay, day_id)
if db_day.trip_id != trip_id:
raise HTTPException(status_code=400, detail="Bad request")
@ -303,6 +324,9 @@ def delete_tripitem(
db_trip = session.get(Trip, trip_id)
verify_exists_and_owns(current_user, db_trip)
if db_trip.archived:
raise HTTPException(status_code=400, detail="Bad request")
db_day = session.get(TripDay, day_id)
if db_day.trip_id != trip_id:
raise HTTPException(status_code=400, detail="Bad request")

View File

@ -12,19 +12,38 @@
<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 flex-col md:flex-row items-center gap-2 print:hidden">
<div>
<div class="flex items-center gap-2 print:hidden">
@if (!trip?.archived) {
<div class="hidden md:flex items-center gap-2">
<p-button text (click)="toggleArchiveTrip()" icon="pi pi-box" severity="warn" />
<div class="border-l border-solid border-gray-700 h-4"></div>
<p-button text (click)="deleteTrip()" icon="pi pi-trash" severity="danger" />
<p-button text (click)="editTrip()" icon="pi pi-pencil" />
</div>
<div>
<span class="bg-gray-100 text-gray-800 text-xs md:text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ totalPrice
|| '-' }} {{ currency$ | async }}</span>
<div class="flex md:hidden">
<p-button (click)="menu.toggle($event)" severity="secondary" text icon="pi pi-ellipsis-h" />
<p-menu #menu [model]="menuItems" [popup]="true" />
</div>
}
<span class="bg-gray-100 text-gray-800 text-xs md:text-sm font-medium me-2 px-2.5 py-0.5 rounded min-w-fit">{{
totalPrice
|| '-' }} {{ currency$ | async }}</span>
</div>
</div>
</section>
@if (trip?.archived) {
<div class="mx-auto p-4 my-4 w-fit max-w-[400px] text-center text-orange-800 rounded-lg bg-orange-50">
<div class="flex items-center justify-between">
<div class="font-semibold">Archived</div>
<p-button text icon="pi pi-box" label="Restore" (click)="toggleArchiveTrip()" [size]="'small'" />
</div>
This Trip is archived, you cannot modify it.
</div>
}
<section class="p-4 print:px-1 grid md:grid-cols-3 gap-4 print:block">
<div class="p-4 shadow rounded-md md:col-span-2 max-w-screen print:col-span-full">
<div class="p-2 mb-2 flex justify-between items-center">
@ -36,8 +55,8 @@
<div class="flex items-center gap-2 print:hidden">
<p-button icon="pi pi-print" (click)="printTable()" text />
<div class="border-l border-solid border-gray-700 h-4"></div>
<p-button icon="pi pi-ellipsis-v" (click)="addItems()" text />
<p-button icon="pi pi-plus" (click)="addItem()" text />
<p-button icon="pi pi-ellipsis-v" [disabled]="trip?.archived" (click)="addItems()" text />
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addItem()" text />
</div>
</div>
@ -104,7 +123,7 @@
Add <i>Day</i> to your <i>Trip</i> to start organizing !
</p>
<p-button styleClass="mt-4" label="Add" icon="pi pi-plus" (click)="addDay()" text />
<p-button styleClass="mt-4" label="Add" icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
</div>
</div>
<div class="hidden print:block text-center text-sm text-gray-500 mt-4">
@ -130,9 +149,10 @@
<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" severity="danger" (click)="deleteItem(selectedItem)" text />
<p-button icon="pi pi-pencil" (click)="editItem(selectedItem)" text />
<p-button icon="pi pi-times" (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>
@ -201,7 +221,7 @@
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} days</span>
</div>
<p-button icon="pi pi-plus" (click)="addDay()" text />
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
</div>
<div class="max-h-[20vh] overflow-y-auto">
@ -216,13 +236,13 @@
getDayStats(d).places }}</span>
</div>
<div class="hidden group-hover:flex gap-2 items-center">
<p-button icon="pi pi-trash" severity="danger" (click)="deleteDay(d)" text />
<p-button icon="pi pi-pencil" (click)="editDay(d)" label="Edit" text />
<p-button icon="pi pi-plus" (click)="addItem(d.id)" label="Item" text />
<p-button icon="pi pi-trash" severity="danger" [disabled]="trip?.archived" (click)="deleteDay(d)" text />
<p-button icon="pi pi-pencil" [disabled]="trip?.archived" (click)="editDay(d)" label="Edit" text />
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addItem(d.id)" label="Item" text />
</div>
</div>
} @empty {
<p-button label="Add" icon="pi pi-plus" (click)="addDay()" text />
<p-button label="Add" icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
}
} @placeholder (minimum 0.4s) {
<div class="h-16">
@ -245,7 +265,7 @@
} @placeholder (minimum 0.4s) {
<p-skeleton height="1.75rem" width="2.5rem" class="mr-1" />
}
<p-button icon="pi pi-plus" (click)="manageTripPlaces()" text />
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="manageTripPlaces()" text />
</div>
</div>
@ -280,7 +300,7 @@
</div>
</div>
} @empty {
<p-button label="Add" icon="pi pi-plus" (click)="manageTripPlaces()" text />
<p-button label="Add" icon="pi pi-plus" [disabled]="trip?.archived" (click)="manageTripPlaces()" text />
}
} @placeholder (minimum 0.4s) {
<div class="flex flex-col gap-4">

View File

@ -27,6 +27,8 @@ import { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.comp
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";
interface PlaceWithUsage extends Place {
placeUsage?: boolean;
@ -38,6 +40,7 @@ interface PlaceWithUsage extends Place {
imports: [
FormsModule,
SkeletonModule,
MenuModule,
ReactiveFormsModule,
InputTextModule,
AsyncPipe,
@ -62,6 +65,7 @@ export class TripComponent implements AfterViewInit {
places: PlaceWithUsage[] = [];
flattenedTripItems: FlattenedTripItem[] = [];
menuItems: MenuItem[] = [];
constructor(
private apiService: ApiService,
@ -72,6 +76,38 @@ export class TripComponent implements AfterViewInit {
) {
this.currency$ = this.utilsService.currency$;
this.statuses = this.utilsService.statuses;
this.menuItems = [
{
label: "Actions",
items: [
{
label: "Edit",
icon: "pi pi-pencil",
iconClass: "text-blue-500!",
command: () => {
this.editTrip();
},
},
{
label: "Archive",
icon: "pi pi-box",
iconClass: "text-orange-500!",
command: () => {
this.toggleArchiveTrip();
},
},
{
label: "Delete",
icon: "pi pi-trash",
iconClass: "text-red-500!",
command: () => {
this.deleteTrip();
},
},
],
},
];
}
back() {
@ -292,6 +328,33 @@ export class TripComponent implements AfterViewInit {
});
}
toggleArchiveTrip() {
const currentArchiveStatus = this.trip?.archived;
const modal = this.dialogService.open(YesNoModalComponent, {
header: "Confirm Action",
modal: true,
closable: true,
dismissableMask: true,
breakpoints: {
"640px": "90vw",
},
data: `${currentArchiveStatus ? "Restore" : "Archive"} ${this.trip?.name} ?${currentArchiveStatus ? "" : " This will make everything read-only."}`,
});
modal.onClose.subscribe({
next: (bool) => {
if (bool)
this.apiService
.putTrip({ archived: !currentArchiveStatus }, this.trip?.id!)
.subscribe({
next: () => {
this.trip!.archived = !currentArchiveStatus;
},
});
},
});
}
addDay() {
const modal: DynamicDialogRef = this.dialogService.open(
TripCreateDayModalComponent,

View File

@ -12,7 +12,8 @@
<div class="mt-10 grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
@defer {
@for (trip of trips; track trip.id) {
<div class="group relative rounded-lg overflow-hidden shadow-lg cursor-pointer" (click)="viewTrip(trip.id)">
<div class="group relative rounded-lg overflow-hidden shadow-lg cursor-pointer" [class.grayscale]="trip.archived"
(click)="viewTrip(trip.id)">
<img class="rounded-lg object-cover transform transition-transform duration-300 ease-in-out group-hover:scale-105"
[src]="trip.image" />

View File

@ -26,10 +26,13 @@ export class TripsComponent {
constructor(
private apiService: ApiService,
private dialogService: DialogService,
private router: Router
private router: Router,
) {
this.apiService.getTrips().subscribe({
next: (trips) => (this.trips = trips),
next: (trips) => {
this.trips = trips;
this.sortTrips();
},
});
}
@ -37,22 +40,35 @@ export class TripsComponent {
this.router.navigateByUrl(`/trips/${id}`);
}
sortTrips() {
this.trips = this.trips.sort((a, b) => {
if (!!a.archived !== !!b.archived) {
return Number(!!a.archived) - Number(!!b.archived);
}
return a.name.localeCompare(b.name);
});
}
gotoMap() {
this.router.navigateByUrl("/");
}
addTrip() {
const modal: DynamicDialogRef = this.dialogService.open(TripCreateModalComponent, {
header: "Create Place",
modal: true,
appendTo: "body",
closable: true,
dismissableMask: true,
width: "30vw",
breakpoints: {
"640px": "90vw",
const modal: DynamicDialogRef = this.dialogService.open(
TripCreateModalComponent,
{
header: "Create Place",
modal: true,
appendTo: "body",
closable: true,
dismissableMask: true,
width: "30vw",
breakpoints: {
"640px": "90vw",
},
},
});
);
modal.onClose.subscribe({
next: (trip: TripBase | null) => {
@ -61,7 +77,7 @@ export class TripsComponent {
this.apiService.postTrip(trip).subscribe({
next: (trip: TripBase) => {
this.trips.push(trip);
this.trips.sort((a, b) => a.name.localeCompare(b.name));
this.sortTrips();
},
});
},