✨ Trip archive feature, 💄 Trip mobile UI menu
This commit is contained in:
parent
7b9d6142b4
commit
a53e18d024
@ -1 +1 @@
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.1.0"
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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" />
|
||||
|
||||
|
||||
@ -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();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user