✨ Trip: packing list
This commit is contained in:
parent
d8cb828963
commit
3cfd33ce94
@ -0,0 +1,45 @@
|
|||||||
|
"""Trip Packing list
|
||||||
|
|
||||||
|
Revision ID: 1181ac441ce5
|
||||||
|
Revises: 77027ac49c26
|
||||||
|
Create Date: 2025-08-16 11:35:34.870999
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel.sql.sqltypes
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "1181ac441ce5"
|
||||||
|
down_revision = "77027ac49c26"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
"trippackinglistitem",
|
||||||
|
sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
|
sa.Column("qt", sa.Integer(), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"category",
|
||||||
|
sa.Enum("CLOTHES", "TOILETRIES", "TECH", "DOCUMENTS", "OTHER", name="packinglistcategoryenum"),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
sa.Column("packed", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("user", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column("trip_id", sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["trip_id"], ["trip.id"], name=op.f("fk_trippackinglistitem_trip_id_trip"), ondelete="CASCADE"
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user"], ["user.username"], name=op.f("fk_trippackinglistitem_user_user"), ondelete="CASCADE"
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_trippackinglistitem")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table("trippackinglistitem")
|
||||||
@ -39,6 +39,14 @@ class TripItemStatusEnum(str, Enum):
|
|||||||
OPTIONAL = "optional"
|
OPTIONAL = "optional"
|
||||||
|
|
||||||
|
|
||||||
|
class PackingListCategoryEnum(str, Enum):
|
||||||
|
CLOTHES = "clothes"
|
||||||
|
TOILETRIES = "toiletries"
|
||||||
|
TECH = "tech"
|
||||||
|
DOCUMENTS = "documents"
|
||||||
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
class TripShareURL(BaseModel):
|
class TripShareURL(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
@ -251,6 +259,7 @@ class Trip(TripBase, table=True):
|
|||||||
places: list["Place"] = Relationship(back_populates="trips", link_model=TripPlaceLink)
|
places: list["Place"] = Relationship(back_populates="trips", link_model=TripPlaceLink)
|
||||||
days: list["TripDay"] = Relationship(back_populates="trip", cascade_delete=True)
|
days: list["TripDay"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||||
shares: list["TripShare"] = Relationship(back_populates="trip", cascade_delete=True)
|
shares: list["TripShare"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||||
|
packing_items: list["TripPackingListItem"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||||
|
|
||||||
|
|
||||||
class TripCreate(TripBase):
|
class TripCreate(TripBase):
|
||||||
@ -401,3 +410,39 @@ class TripShare(SQLModel, table=True):
|
|||||||
|
|
||||||
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE")
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE")
|
||||||
trip: Trip | None = Relationship(back_populates="shares")
|
trip: Trip | None = Relationship(back_populates="shares")
|
||||||
|
|
||||||
|
|
||||||
|
class TripPackingListItemBase(SQLModel):
|
||||||
|
text: str | None = None
|
||||||
|
qt: int | None = None
|
||||||
|
category: PackingListCategoryEnum | None = None
|
||||||
|
packed: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class TripPackingListItem(TripPackingListItemBase, table=True):
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
user: str = Field(foreign_key="user.username", ondelete="CASCADE")
|
||||||
|
|
||||||
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE")
|
||||||
|
trip: Trip | None = Relationship(back_populates="packing_items")
|
||||||
|
|
||||||
|
|
||||||
|
class TripPackingListItemCreate(TripPackingListItemBase):
|
||||||
|
packed: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TripPackingListItemUpdate(TripPackingListItemBase): ...
|
||||||
|
|
||||||
|
|
||||||
|
class TripPackingListItemRead(TripPackingListItemBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def serialize(cls, obj: "TripPackingListItem") -> "TripPackingListItemRead":
|
||||||
|
return cls(
|
||||||
|
id=obj.id,
|
||||||
|
text=obj.text,
|
||||||
|
qt=obj.qt,
|
||||||
|
category=obj.category,
|
||||||
|
packed=obj.packed,
|
||||||
|
)
|
||||||
|
|||||||
@ -8,8 +8,11 @@ from ..deps import SessionDep, get_current_username
|
|||||||
from ..models.models import (Image, Place, Trip, TripCreate, TripDay,
|
from ..models.models import (Image, Place, Trip, TripCreate, TripDay,
|
||||||
TripDayBase, TripDayRead, TripItem,
|
TripDayBase, TripDayRead, TripItem,
|
||||||
TripItemCreate, TripItemRead, TripItemUpdate,
|
TripItemCreate, TripItemRead, TripItemUpdate,
|
||||||
TripPlaceLink, TripRead, TripReadBase, TripShare,
|
TripPackingListItem, TripPackingListItemCreate,
|
||||||
TripShareURL, TripUpdate)
|
TripPackingListItemRead,
|
||||||
|
TripPackingListItemUpdate, TripPlaceLink,
|
||||||
|
TripRead, TripReadBase, TripShare, TripShareURL,
|
||||||
|
TripUpdate)
|
||||||
from ..security import verify_exists_and_owns
|
from ..security import verify_exists_and_owns
|
||||||
from ..utils.utils import (b64img_decode, generate_urlsafe, remove_image,
|
from ..utils.utils import (b64img_decode, generate_urlsafe, remove_image,
|
||||||
save_image_to_file)
|
save_image_to_file)
|
||||||
@ -407,3 +410,88 @@ def delete_shared_trip(
|
|||||||
session.delete(db_share)
|
session.delete(db_share)
|
||||||
session.commit()
|
session.commit()
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{trip_id}/packing", response_model=list[TripPackingListItemRead])
|
||||||
|
def read_packing_list(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> list[TripPackingListItemRead]:
|
||||||
|
p_items = session.exec(
|
||||||
|
select(TripPackingListItem)
|
||||||
|
.where(TripPackingListItem.trip_id == trip_id, TripPackingListItem.user == current_user)
|
||||||
|
.order_by(TripPackingListItem.id.asc())
|
||||||
|
).all()
|
||||||
|
|
||||||
|
return [TripPackingListItemRead.serialize(i) for i in p_items]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trip_id}/packing", response_model=TripPackingListItemRead)
|
||||||
|
def create_packing_item(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
data: TripPackingListItemCreate,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> TripPackingListItemRead:
|
||||||
|
item = TripPackingListItem(
|
||||||
|
**data.model_dump(),
|
||||||
|
trip_id=trip_id,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
session.add(item)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(item)
|
||||||
|
return TripPackingListItemRead.serialize(item)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{trip_id}/packing/{p_id}", response_model=TripPackingListItemRead)
|
||||||
|
def update_packing_item(
|
||||||
|
session: SessionDep,
|
||||||
|
p_item: TripPackingListItemUpdate,
|
||||||
|
trip_id: int,
|
||||||
|
p_id: int,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> TripPackingListItemRead:
|
||||||
|
db_item = session.exec(
|
||||||
|
select(TripPackingListItem).where(
|
||||||
|
TripPackingListItem.id == p_id,
|
||||||
|
TripPackingListItem.trip_id == trip_id,
|
||||||
|
TripPackingListItem.user == current_user,
|
||||||
|
)
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not db_item:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
item_data = p_item.model_dump(exclude_unset=True)
|
||||||
|
for key, value in item_data.items():
|
||||||
|
setattr(db_item, key, value)
|
||||||
|
|
||||||
|
session.add(db_item)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_item)
|
||||||
|
return TripPackingListItemRead.serialize(db_item)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{trip_id}/packing/{p_id}")
|
||||||
|
def delete_packing_item(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
p_id: int,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
):
|
||||||
|
item = session.exec(
|
||||||
|
select(TripPackingListItem).where(
|
||||||
|
TripPackingListItem.id == p_id,
|
||||||
|
TripPackingListItem.trip_id == trip_id,
|
||||||
|
TripPackingListItem.user == current_user,
|
||||||
|
)
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
session.delete(item)
|
||||||
|
session.commit()
|
||||||
|
return {}
|
||||||
|
|||||||
@ -21,6 +21,8 @@
|
|||||||
<p-button pTooltip="Delete Trip" text (click)="deleteTrip()" icon="pi pi-trash" severity="danger" />
|
<p-button pTooltip="Delete Trip" text (click)="deleteTrip()" icon="pi pi-trash" severity="danger" />
|
||||||
<p-button pTooltip="Edit Trip" text (click)="editTrip()" icon="pi pi-pencil" />
|
<p-button pTooltip="Edit Trip" text (click)="editTrip()" icon="pi pi-pencil" />
|
||||||
<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 pTooltip="Packing list" tooltipPosition="left" text (click)="openPackingList()" icon="pi pi-briefcase"
|
||||||
|
severity="help" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex md:hidden">
|
<div class="flex md:hidden">
|
||||||
@ -551,3 +553,38 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
}
|
}
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|
||||||
|
<p-dialog header="Packing list" [draggable]="false" [dismissableMask]="true" [modal]="true"
|
||||||
|
[(visible)]="packingDialogVisible" styleClass="w-[95%] md:w-[70%] lg:w-[50%]">
|
||||||
|
<section class="md:max-w-3/4 md:mx-auto max-h-[80%] md:max-h-[600px]">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<p-button (click)="addPackingItem()" icon="pi pi-plus" label="Add item" text />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2 mt-4 pb-4">
|
||||||
|
@for (c of dispPackingList | keyvalue; track c.key) {
|
||||||
|
<div class="mt-4 text-md font-semibold capitalize">{{ c.key }}</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
@for (item of c.value; track item.id) {
|
||||||
|
<div class="relative group flex items-center gap-3 rounded-md p-2 hover:bg-gray-100 dark:hover:bg-white/5">
|
||||||
|
<label [for]="item.id" [class.line-through]="item.packed"
|
||||||
|
class="flex items-center gap-2 w-full cursor-pointer">
|
||||||
|
<p-checkbox (onChange)="onCheckPackingItem($event, item.id)" [binary]="true" [inputId]="item.id.toString()"
|
||||||
|
[(ngModel)]="item.packed" />
|
||||||
|
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
||||||
|
@if (item.qt) {<span class="text-gray-400 mr-0.5">{{ item.qt }}</span>}
|
||||||
|
<span>{{ item.text }}</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="md:opacity-0 absolute right-0 top-1/2 -translate-y-1/2 md:group-hover:opacity-100 bg-white md:bg-gray-100 rounded">
|
||||||
|
<p-button size="small" text icon="pi pi-trash" (click)="deletePackingItem(item)" severity="danger" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</p-dialog>
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
TripDay,
|
TripDay,
|
||||||
TripItem,
|
TripItem,
|
||||||
TripStatus,
|
TripStatus,
|
||||||
|
PackingItem,
|
||||||
} from "../../types/trip";
|
} from "../../types/trip";
|
||||||
import { Place } from "../../types/poi";
|
import { Place } from "../../types/poi";
|
||||||
import {
|
import {
|
||||||
@ -52,6 +53,8 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
|
|||||||
import { TooltipModule } from "primeng/tooltip";
|
import { TooltipModule } from "primeng/tooltip";
|
||||||
import { MultiSelectModule } from "primeng/multiselect";
|
import { MultiSelectModule } from "primeng/multiselect";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { CheckboxChangeEvent, CheckboxModule } from "primeng/checkbox";
|
||||||
|
import { TripCreatePackingModalComponent } from "../../modals/trip-create-packing-modal/trip-create-packing-modal.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip",
|
selector: "app-trip",
|
||||||
@ -73,6 +76,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|||||||
TooltipModule,
|
TooltipModule,
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
MultiSelectModule,
|
MultiSelectModule,
|
||||||
|
CheckboxModule,
|
||||||
],
|
],
|
||||||
templateUrl: "./trip.component.html",
|
templateUrl: "./trip.component.html",
|
||||||
styleUrls: ["./trip.component.scss"],
|
styleUrls: ["./trip.component.scss"],
|
||||||
@ -93,8 +97,11 @@ export class TripComponent implements AfterViewInit {
|
|||||||
collapsedTripPlaces = false;
|
collapsedTripPlaces = false;
|
||||||
collapsedTripStatuses = false;
|
collapsedTripStatuses = false;
|
||||||
shareDialogVisible = false;
|
shareDialogVisible = false;
|
||||||
|
packingDialogVisible = false;
|
||||||
isExpanded = false;
|
isExpanded = false;
|
||||||
isFilteringMode = false;
|
isFilteringMode = false;
|
||||||
|
packingList: PackingItem[] = [];
|
||||||
|
dispPackingList: Record<string, PackingItem[]> = {};
|
||||||
|
|
||||||
map?: L.Map;
|
map?: L.Map;
|
||||||
markerClusterGroup?: L.MarkerClusterGroup;
|
markerClusterGroup?: L.MarkerClusterGroup;
|
||||||
@ -107,6 +114,14 @@ export class TripComponent implements AfterViewInit {
|
|||||||
{
|
{
|
||||||
label: "Actions",
|
label: "Actions",
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
label: "Packing",
|
||||||
|
icon: "pi pi-briefcase",
|
||||||
|
iconClass: "text-purple-500!",
|
||||||
|
command: () => {
|
||||||
|
this.openPackingList();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
icon: "pi pi-pencil",
|
icon: "pi pi-pencil",
|
||||||
@ -1315,4 +1330,117 @@ export class TripComponent implements AfterViewInit {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openPackingList() {
|
||||||
|
if (!this.trip) return;
|
||||||
|
|
||||||
|
if (!this.packingList.length)
|
||||||
|
this.apiService
|
||||||
|
.getPackingList(this.trip.id)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (items) => {
|
||||||
|
this.packingList = [...items];
|
||||||
|
this.computeDispPackingList();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.packingDialogVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPackingItem() {
|
||||||
|
if (!this.trip) return;
|
||||||
|
|
||||||
|
const modal: DynamicDialogRef = this.dialogService.open(
|
||||||
|
TripCreatePackingModalComponent,
|
||||||
|
{
|
||||||
|
header: "Create Packing",
|
||||||
|
modal: true,
|
||||||
|
appendTo: "body",
|
||||||
|
closable: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
width: "40vw",
|
||||||
|
breakpoints: {
|
||||||
|
"1260px": "70vw",
|
||||||
|
"600px": "90vw",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
|
next: (item: PackingItem | null) => {
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
this.apiService
|
||||||
|
.postPackingItem(this.trip!.id, item)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (item) => {
|
||||||
|
this.packingList.push(item);
|
||||||
|
this.computeDispPackingList();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCheckPackingItem(e: CheckboxChangeEvent, id: number) {
|
||||||
|
if (!this.trip) return;
|
||||||
|
this.apiService
|
||||||
|
.putPackingItem(this.trip.id, id, { packed: e.checked })
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (item) => {
|
||||||
|
const i = this.packingList.find((p) => p.id == item.id);
|
||||||
|
if (i) i.packed = item.packed;
|
||||||
|
this.computeDispPackingList();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePackingItem(item: PackingItem) {
|
||||||
|
const modal = this.dialogService.open(YesNoModalComponent, {
|
||||||
|
header: "Confirm deletion",
|
||||||
|
modal: true,
|
||||||
|
closable: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
breakpoints: {
|
||||||
|
"640px": "90vw",
|
||||||
|
},
|
||||||
|
data: `Delete ${item.text.substring(0, 50)} ?`,
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
|
next: (bool) => {
|
||||||
|
if (!bool) return;
|
||||||
|
this.apiService
|
||||||
|
.deletePackingItem(this.trip!.id, item.id)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
const index = this.packingList.findIndex((p) => p.id == item.id);
|
||||||
|
if (index > -1) this.packingList.splice(index, 1);
|
||||||
|
this.computeDispPackingList();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
computeDispPackingList() {
|
||||||
|
const sorted: PackingItem[] = [...this.packingList].sort((a, b) =>
|
||||||
|
a.packed !== b.packed
|
||||||
|
? a.packed
|
||||||
|
? 1
|
||||||
|
: -1
|
||||||
|
: a.text.localeCompare(b.text),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dispPackingList = sorted.reduce<Record<string, PackingItem[]>>(
|
||||||
|
(acc, item) => {
|
||||||
|
(acc[item.category] ??= []).push(item);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
<div pFocusTrap class="grid items-center gap-4" [formGroup]="packingForm">
|
||||||
|
|
||||||
|
<p-floatlabel variant="in">
|
||||||
|
<p-inputnumber id="qt" formControlName="qt" mode="decimal" [maxFractionDigits]="0" min="0" fluid />
|
||||||
|
<label for="qt">Qt.</label>
|
||||||
|
</p-floatlabel>
|
||||||
|
|
||||||
|
<p-floatlabel variant="in">
|
||||||
|
<input class="col-span-2" id="text" formControlName="text" (keypress.enter)="closeDialog()" pInputText fluid />
|
||||||
|
<label for="text">Text</label>
|
||||||
|
</p-floatlabel>
|
||||||
|
|
||||||
|
<p-floatlabel variant="in">
|
||||||
|
<p-select [options]="packingCategories" id="category" appendTo="body" formControlName="category"
|
||||||
|
optionLabel="dispValue" optionValue="value" placeholder="Select Category" fluid />
|
||||||
|
<label for="category">Category</label>
|
||||||
|
</p-floatlabel>
|
||||||
|
|
||||||
|
<div class="mt-4 text-right">
|
||||||
|
<p-button (click)="closeDialog()" [disabled]="!packingForm.dirty || !packingForm.valid">{{
|
||||||
|
packingForm.get("id")?.value
|
||||||
|
!== -1 ? "Update" : "Create" }}</p-button>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import {
|
||||||
|
FormBuilder,
|
||||||
|
FormGroup,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from "@angular/forms";
|
||||||
|
import { ButtonModule } from "primeng/button";
|
||||||
|
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
||||||
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
|
import { InputTextModule } from "primeng/inputtext";
|
||||||
|
import { FocusTrapModule } from "primeng/focustrap";
|
||||||
|
import { SelectModule } from "primeng/select";
|
||||||
|
import { InputNumberModule } from "primeng/inputnumber";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-trip-create-packing-modal",
|
||||||
|
imports: [
|
||||||
|
FloatLabelModule,
|
||||||
|
InputTextModule,
|
||||||
|
ButtonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FocusTrapModule,
|
||||||
|
SelectModule,
|
||||||
|
InputNumberModule,
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: "./trip-create-packing-modal.component.html",
|
||||||
|
styleUrl: "./trip-create-packing-modal.component.scss",
|
||||||
|
})
|
||||||
|
export class TripCreatePackingModalComponent {
|
||||||
|
packingForm: FormGroup;
|
||||||
|
readonly packingCategories = [
|
||||||
|
{ value: "clothes", dispValue: "Clothes" },
|
||||||
|
{ value: "toiletries", dispValue: "Toiletries" },
|
||||||
|
{ value: "tech", dispValue: "Tech" },
|
||||||
|
{ value: "documents", dispValue: "Documents" },
|
||||||
|
{ value: "other", dispValue: "Other" },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private ref: DynamicDialogRef,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private config: DynamicDialogConfig,
|
||||||
|
) {
|
||||||
|
this.packingForm = this.fb.group({
|
||||||
|
id: -1,
|
||||||
|
qt: null,
|
||||||
|
text: ["", { validators: Validators.required }],
|
||||||
|
category: ["", { validators: Validators.required }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const patchValue = this.config.data?.packing;
|
||||||
|
if (patchValue) {
|
||||||
|
this.packingForm.patchValue(patchValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog() {
|
||||||
|
if (!this.packingForm.valid) return;
|
||||||
|
|
||||||
|
// Normalize data for API POST
|
||||||
|
let ret = this.packingForm.value;
|
||||||
|
this.ref.close(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import { BehaviorSubject, map, Observable, shareReplay, tap } from "rxjs";
|
|||||||
import { Info } from "../types/info";
|
import { Info } from "../types/info";
|
||||||
import { ImportResponse, Settings } from "../types/settings";
|
import { ImportResponse, Settings } from "../types/settings";
|
||||||
import {
|
import {
|
||||||
|
PackingItem,
|
||||||
SharedTripURL,
|
SharedTripURL,
|
||||||
Trip,
|
Trip,
|
||||||
TripBase,
|
TripBase,
|
||||||
@ -232,6 +233,39 @@ export class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPackingList(trip_id: number): Observable<PackingItem[]> {
|
||||||
|
return this.httpClient.get<PackingItem[]>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/packing`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
postPackingItem(
|
||||||
|
trip_id: number,
|
||||||
|
p_item: PackingItem,
|
||||||
|
): Observable<PackingItem> {
|
||||||
|
return this.httpClient.post<PackingItem>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/packing`,
|
||||||
|
p_item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
putPackingItem(
|
||||||
|
trip_id: number,
|
||||||
|
p_id: number,
|
||||||
|
p_item: Partial<PackingItem>,
|
||||||
|
): Observable<PackingItem> {
|
||||||
|
return this.httpClient.put<PackingItem>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`,
|
||||||
|
p_item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePackingItem(trip_id: number, p_id: number): Observable<null> {
|
||||||
|
return this.httpClient.delete<null>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
checkVersion(): Observable<string> {
|
checkVersion(): Observable<string> {
|
||||||
return this.httpClient.get<string>(
|
return this.httpClient.get<string>(
|
||||||
`${this.apiBaseUrl}/settings/checkversion`,
|
`${this.apiBaseUrl}/settings/checkversion`,
|
||||||
|
|||||||
@ -65,3 +65,11 @@ export interface FlattenedTripItem {
|
|||||||
export interface SharedTripURL {
|
export interface SharedTripURL {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PackingItem {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
category: string;
|
||||||
|
qt?: number;
|
||||||
|
packed?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user