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

View File

@ -12,19 +12,38 @@
<img src="favicon.png" class="size-20"> <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 class="flex gap-2 items-center text-xs text-gray-500"><i class="pi pi-github"></i>itskovacs/trip</div>
</div> </div>
<div class="flex flex-col md:flex-row items-center gap-2 print:hidden"> <div class="flex items-center gap-2 print:hidden">
<div> @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)="deleteTrip()" icon="pi pi-trash" severity="danger" />
<p-button text (click)="editTrip()" icon="pi pi-pencil" /> <p-button text (click)="editTrip()" icon="pi pi-pencil" />
</div> </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 <div class="flex md:hidden">
|| '-' }} {{ currency$ | async }}</span> <p-button (click)="menu.toggle($event)" severity="secondary" text icon="pi pi-ellipsis-h" />
<p-menu #menu [model]="menuItems" [popup]="true" />
</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 min-w-fit">{{
totalPrice
|| '-' }} {{ currency$ | async }}</span>
</div> </div>
</div> </div>
</section> </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"> <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-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"> <div class="p-2 mb-2 flex justify-between items-center">
@ -36,8 +55,8 @@
<div class="flex items-center gap-2 print:hidden"> <div class="flex items-center gap-2 print:hidden">
<p-button icon="pi pi-print" (click)="printTable()" text /> <p-button icon="pi pi-print" (click)="printTable()" text />
<div class="border-l border-solid border-gray-700 h-4"></div> <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-ellipsis-v" [disabled]="trip?.archived" (click)="addItems()" text />
<p-button icon="pi pi-plus" (click)="addItem()" text /> <p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addItem()" text />
</div> </div>
</div> </div>
@ -104,7 +123,7 @@
Add <i>Day</i> to your <i>Trip</i> to start organizing ! Add <i>Day</i> to your <i>Trip</i> to start organizing !
</p> </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> </div>
<div class="hidden print:block text-center text-sm text-gray-500 mt-4"> <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> <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"> <div class="flex items-center gap-2">
<p-button icon="pi pi-trash" severity="danger" (click)="deleteItem(selectedItem)" text /> <p-button icon="pi pi-trash" [disabled]="trip?.archived" severity="danger" (click)="deleteItem(selectedItem)"
<p-button icon="pi pi-pencil" (click)="editItem(selectedItem)" text /> text />
<p-button icon="pi pi-times" (click)="selectedItem = undefined" 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>
@ -201,7 +221,7 @@
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} days</span> <span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} days</span>
</div> </div>
<p-button icon="pi pi-plus" (click)="addDay()" text /> <p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
</div> </div>
<div class="max-h-[20vh] overflow-y-auto"> <div class="max-h-[20vh] overflow-y-auto">
@ -216,13 +236,13 @@
getDayStats(d).places }}</span> getDayStats(d).places }}</span>
</div> </div>
<div class="hidden group-hover:flex gap-2 items-center"> <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-trash" severity="danger" [disabled]="trip?.archived" (click)="deleteDay(d)" text />
<p-button icon="pi pi-pencil" (click)="editDay(d)" label="Edit" text /> <p-button icon="pi pi-pencil" [disabled]="trip?.archived" (click)="editDay(d)" label="Edit" text />
<p-button icon="pi pi-plus" (click)="addItem(d.id)" label="Item" text /> <p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addItem(d.id)" label="Item" text />
</div> </div>
</div> </div>
} @empty { } @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) { } @placeholder (minimum 0.4s) {
<div class="h-16"> <div class="h-16">
@ -245,7 +265,7 @@
} @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" (click)="manageTripPlaces()" text /> <p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="manageTripPlaces()" text />
</div> </div>
</div> </div>
@ -280,7 +300,7 @@
</div> </div>
</div> </div>
} @empty { } @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) { } @placeholder (minimum 0.4s) {
<div class="flex flex-col gap-4"> <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 { 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 { MenuModule } from "primeng/menu";
interface PlaceWithUsage extends Place { interface PlaceWithUsage extends Place {
placeUsage?: boolean; placeUsage?: boolean;
@ -38,6 +40,7 @@ interface PlaceWithUsage extends Place {
imports: [ imports: [
FormsModule, FormsModule,
SkeletonModule, SkeletonModule,
MenuModule,
ReactiveFormsModule, ReactiveFormsModule,
InputTextModule, InputTextModule,
AsyncPipe, AsyncPipe,
@ -62,6 +65,7 @@ export class TripComponent implements AfterViewInit {
places: PlaceWithUsage[] = []; places: PlaceWithUsage[] = [];
flattenedTripItems: FlattenedTripItem[] = []; flattenedTripItems: FlattenedTripItem[] = [];
menuItems: MenuItem[] = [];
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
@ -72,6 +76,38 @@ export class TripComponent implements AfterViewInit {
) { ) {
this.currency$ = this.utilsService.currency$; this.currency$ = this.utilsService.currency$;
this.statuses = this.utilsService.statuses; 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() { 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() { addDay() {
const modal: DynamicDialogRef = this.dialogService.open( const modal: DynamicDialogRef = this.dialogService.open(
TripCreateDayModalComponent, 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"> <div class="mt-10 grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
@defer { @defer {
@for (trip of trips; track trip.id) { @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" <img class="rounded-lg object-cover transform transition-transform duration-300 ease-in-out group-hover:scale-105"
[src]="trip.image" /> [src]="trip.image" />

View File

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