✨ Trip: checklist dialog
This commit is contained in:
parent
b4936c5b53
commit
63c9b751eb
39
backend/trip/alembic/versions/60a9bb641d8a_trip_checklist.py
Normal file
39
backend/trip/alembic/versions/60a9bb641d8a_trip_checklist.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""Trip Checklist
|
||||||
|
|
||||||
|
Revision ID: 60a9bb641d8a
|
||||||
|
Revises: 1181ac441ce5
|
||||||
|
Create Date: 2025-08-17 21:12:41.336514
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel.sql.sqltypes
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "60a9bb641d8a"
|
||||||
|
down_revision = "1181ac441ce5"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
"tripchecklistitem",
|
||||||
|
sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
|
sa.Column("checked", 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_tripchecklistitem_trip_id_trip"), ondelete="CASCADE"
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user"], ["user.username"], name=op.f("fk_tripchecklistitem_user_user"), ondelete="CASCADE"
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_tripchecklistitem")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table("tripchecklistitem")
|
||||||
@ -260,6 +260,7 @@ class Trip(TripBase, table=True):
|
|||||||
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)
|
packing_items: list["TripPackingListItem"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||||
|
checklist_items: list["TripChecklistItem"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||||
|
|
||||||
|
|
||||||
class TripCreate(TripBase):
|
class TripCreate(TripBase):
|
||||||
@ -297,7 +298,6 @@ class TripRead(TripBase):
|
|||||||
image_id: int | None
|
image_id: int | None
|
||||||
days: list["TripDayRead"]
|
days: list["TripDayRead"]
|
||||||
places: list["PlaceRead"]
|
places: list["PlaceRead"]
|
||||||
shared: bool
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serialize(cls, obj: Trip) -> "TripRead":
|
def serialize(cls, obj: Trip) -> "TripRead":
|
||||||
@ -309,7 +309,6 @@ class TripRead(TripBase):
|
|||||||
image_id=obj.image_id,
|
image_id=obj.image_id,
|
||||||
days=[TripDayRead.serialize(day) for day in obj.days],
|
days=[TripDayRead.serialize(day) for day in obj.days],
|
||||||
places=[PlaceRead.serialize(place) for place in obj.places],
|
places=[PlaceRead.serialize(place) for place in obj.places],
|
||||||
shared=bool(obj.shares),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -446,3 +445,34 @@ class TripPackingListItemRead(TripPackingListItemBase):
|
|||||||
category=obj.category,
|
category=obj.category,
|
||||||
packed=obj.packed,
|
packed=obj.packed,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TripChecklistItemBase(SQLModel):
|
||||||
|
text: str | None = None
|
||||||
|
checked: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class TripChecklistItem(TripChecklistItemBase, table=True):
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
|
||||||
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE")
|
||||||
|
trip: Trip | None = Relationship(back_populates="checklist_items")
|
||||||
|
|
||||||
|
|
||||||
|
class TripChecklistItemCreate(TripChecklistItemBase):
|
||||||
|
checked: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TripChecklistItemUpdate(TripChecklistItemBase): ...
|
||||||
|
|
||||||
|
|
||||||
|
class TripChecklistItemRead(TripChecklistItemBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def serialize(cls, obj: "TripChecklistItem") -> "TripChecklistItemRead":
|
||||||
|
return cls(
|
||||||
|
id=obj.id,
|
||||||
|
text=obj.text,
|
||||||
|
checked=obj.checked,
|
||||||
|
)
|
||||||
|
|||||||
@ -5,14 +5,15 @@ from sqlmodel import select
|
|||||||
|
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..deps import SessionDep, get_current_username
|
from ..deps import SessionDep, get_current_username
|
||||||
from ..models.models import (Image, Place, Trip, TripCreate, TripDay,
|
from ..models.models import (Image, Place, Trip, TripChecklistItem,
|
||||||
|
TripChecklistItemCreate, TripChecklistItemRead,
|
||||||
|
TripChecklistItemUpdate, TripCreate, TripDay,
|
||||||
TripDayBase, TripDayRead, TripItem,
|
TripDayBase, TripDayRead, TripItem,
|
||||||
TripItemCreate, TripItemRead, TripItemUpdate,
|
TripItemCreate, TripItemRead, TripItemUpdate,
|
||||||
TripPackingListItem, TripPackingListItemCreate,
|
TripPackingListItem, TripPackingListItemCreate,
|
||||||
TripPackingListItemRead,
|
TripPackingListItemRead, TripPackingListItemUpdate,
|
||||||
TripPackingListItemUpdate, TripPlaceLink,
|
TripRead, TripReadBase, TripShare,
|
||||||
TripRead, TripReadBase, TripShare, TripShareURL,
|
TripShareURL, TripUpdate)
|
||||||
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)
|
||||||
@ -495,3 +496,91 @@ def delete_packing_item(
|
|||||||
session.delete(item)
|
session.delete(item)
|
||||||
session.commit()
|
session.commit()
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{trip_id}/checklist", response_model=list[TripChecklistItemRead])
|
||||||
|
def read_checklist(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> list[TripChecklistItemRead]:
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
items = session.exec(select(TripChecklistItem).where(TripChecklistItem.trip_id == trip_id))
|
||||||
|
return [TripChecklistItemRead.serialize(i) for i in items]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/shared/{token}/checklist", response_model=list[TripChecklistItemRead])
|
||||||
|
def read_shared_trip_checklist(
|
||||||
|
session: SessionDep,
|
||||||
|
token: str,
|
||||||
|
) -> list[TripChecklistItemRead]:
|
||||||
|
items = session.exec(
|
||||||
|
select(TripChecklistItem).where(
|
||||||
|
TripChecklistItem.trip_id == _trip_from_token_or_404(session, token).trip_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return [TripChecklistItemRead.serialize(i) for i in items]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trip_id}/checklist", response_model=TripChecklistItemRead)
|
||||||
|
def create_checklist_item(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
data: TripChecklistItemCreate,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> TripChecklistItemRead:
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
item = TripChecklistItem(**data.model_dump(), trip_id=trip_id)
|
||||||
|
session.add(item)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(item)
|
||||||
|
return TripChecklistItemRead.serialize(item)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{trip_id}/checklist/{id}", response_model=TripChecklistItemRead)
|
||||||
|
def update_checklist_item(
|
||||||
|
session: SessionDep,
|
||||||
|
item: TripChecklistItemUpdate,
|
||||||
|
trip_id: int,
|
||||||
|
id: int,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> TripChecklistItemRead:
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
db_item = session.exec(
|
||||||
|
select(TripChecklistItem).where(TripChecklistItem.id == id, TripChecklistItem.trip_id == trip_id)
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not db_item:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
item_data = 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 TripChecklistItemRead.serialize(db_item)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{trip_id}/checklist/{id}")
|
||||||
|
def delete_checklist_item(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
id: int,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
):
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
item = session.exec(
|
||||||
|
select(TripChecklistItem).where(
|
||||||
|
TripChecklistItem.id == id,
|
||||||
|
TripChecklistItem.trip_id == trip_id,
|
||||||
|
)
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
session.delete(item)
|
||||||
|
session.commit()
|
||||||
|
return {}
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
<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="Checklist" text (click)="openChecklist()" icon="pi pi-check-square" severity="help" />
|
||||||
<p-button pTooltip="Packing list" tooltipPosition="left" text (click)="openPackingList()" icon="pi pi-briefcase"
|
<p-button pTooltip="Packing list" tooltipPosition="left" text (click)="openPackingList()" icon="pi pi-briefcase"
|
||||||
severity="help" />
|
severity="help" />
|
||||||
</div>
|
</div>
|
||||||
@ -121,7 +122,7 @@
|
|||||||
<span class="font-bold ml-2">{{ tripitem.td_label }}</span>
|
<span class="font-bold ml-2">{{ tripitem.td_label }}</span>
|
||||||
<p-button class="ml-2" text icon="pi pi-directions"
|
<p-button class="ml-2" text icon="pi pi-directions"
|
||||||
[severity]="tripMapAntLayerDayID == tripitem.day_id ? 'help' : 'primary'"
|
[severity]="tripMapAntLayerDayID == tripitem.day_id ? 'help' : 'primary'"
|
||||||
(click)="toggleTripDayHighlightPathDay(tripitem.day_id)" />
|
(click)="toggleTripDayHighlight(tripitem.day_id)" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -174,7 +175,7 @@
|
|||||||
@if (tripTableSelectedColumns.includes('day') && rowgroup) {
|
@if (tripTableSelectedColumns.includes('day') && rowgroup) {
|
||||||
<td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-pointer"
|
<td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-pointer"
|
||||||
[class.text-blue-500]="tripMapAntLayerDayID == tripitem.day_id"
|
[class.text-blue-500]="tripMapAntLayerDayID == tripitem.day_id"
|
||||||
(click)="toggleTripDayHighlightPathDay(tripitem.day_id); $event.stopPropagation()">
|
(click)="toggleTripDayHighlight(tripitem.day_id); $event.stopPropagation()">
|
||||||
<div class="truncate">{{tripitem.td_label }}</div>
|
<div class="truncate">{{tripitem.td_label }}</div>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
@ -476,44 +477,6 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 shadow rounded-md w-full min-h-20">
|
|
||||||
<div class="group relative p-2 mb-2 flex flex-col items-start">
|
|
||||||
<h1 class="font-semibold tracking-tight text-xl">Watchlist</h1>
|
|
||||||
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} pending/constraints</span>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="bg-white rounded py-2 absolute top-1/2 -translate-y-1/2 left-0 hidden group-hover:block slide-x dark:bg-surface-900">
|
|
||||||
<p-button [icon]="collapsedTripStatuses ? 'pi pi-chevron-down' : 'pi pi-chevron-up'" text
|
|
||||||
(click)="collapsedTripStatuses = !collapsedTripStatuses" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!collapsedTripStatuses) {
|
|
||||||
<div [class.max-h-40!]="!isExpanded" class="max-h-[340px] overflow-y-auto">
|
|
||||||
@defer {
|
|
||||||
@for (item of getWatchlistData; track item.id) {
|
|
||||||
<div class="flex items-center gap-2 h-10 px-4 py-2 w-full max-w-full">
|
|
||||||
<div class="flex flex-none">
|
|
||||||
<span [style.background]="item.status.color+'1A'" [style.color]="item.status.color"
|
|
||||||
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
|
|
||||||
item.status.label }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="line-clamp-1">{{ item.text }}</div>
|
|
||||||
</div>
|
|
||||||
} @empty {
|
|
||||||
<p class="p-4 font-light text-gray-500">
|
|
||||||
Nothing there
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
} @placeholder (minimum 0.4s) {
|
|
||||||
<div class="h-16">
|
|
||||||
<p-skeleton height="100%" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -590,3 +553,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|
||||||
|
<p-dialog header="Checklist" [draggable]="false" [dismissableMask]="true" [modal]="true"
|
||||||
|
[(visible)]="checklistDialogVisible" styleClass="w-[95%] md:w-[50%] lg:w-[30%]">
|
||||||
|
<section class="p-4 max-w-full max-h-[80%] md:max-h-[600px]">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<p-button (click)="addChecklistItem()" icon="pi pi-plus" label="Add item" text />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 mt-4 pb-4">
|
||||||
|
@for (item of checklistItems; 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.checked"
|
||||||
|
class="flex items-center gap-2 w-full cursor-pointer">
|
||||||
|
<p-checkbox (onChange)="onCheckChecklistItem($event, item.id)" [binary]="true" [inputId]="item.id.toString()"
|
||||||
|
[(ngModel)]="item.checked" />
|
||||||
|
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
||||||
|
<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)="deleteChecklistItem(item)" severity="danger" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 mt-4 pb-4">
|
||||||
|
@for (item of getWatchlistData; track item.id) {
|
||||||
|
<div class="flex items-center gap-3 rounded-md p-2 hover:bg-gray-100 dark:hover:bg-white/5">
|
||||||
|
<label [for]="item.id" class="flex items-center gap-2 w-full">
|
||||||
|
<div class="relative">
|
||||||
|
@if (item.status) {<div class="z-50 block absolute top-0 left-3 size-2.5 rounded-full"
|
||||||
|
[style.background]="item.status.color"></div>}
|
||||||
|
<p-checkbox disabled />
|
||||||
|
</div>
|
||||||
|
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
||||||
|
<span>{{ item.text }}</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</p-dialog>
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
TripItem,
|
TripItem,
|
||||||
TripStatus,
|
TripStatus,
|
||||||
PackingItem,
|
PackingItem,
|
||||||
|
ChecklistItem,
|
||||||
} from "../../types/trip";
|
} from "../../types/trip";
|
||||||
import { Place } from "../../types/poi";
|
import { Place } from "../../types/poi";
|
||||||
import {
|
import {
|
||||||
@ -55,6 +56,7 @@ 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 { CheckboxChangeEvent, CheckboxModule } from "primeng/checkbox";
|
||||||
import { TripCreatePackingModalComponent } from "../../modals/trip-create-packing-modal/trip-create-packing-modal.component";
|
import { TripCreatePackingModalComponent } from "../../modals/trip-create-packing-modal/trip-create-packing-modal.component";
|
||||||
|
import { TripCreateChecklistModalComponent } from "../../modals/trip-create-checklist-modal/trip-create-checklist-modal.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip",
|
selector: "app-trip",
|
||||||
@ -95,13 +97,15 @@ export class TripComponent implements AfterViewInit {
|
|||||||
totalPrice = 0;
|
totalPrice = 0;
|
||||||
collapsedTripDays = false;
|
collapsedTripDays = false;
|
||||||
collapsedTripPlaces = false;
|
collapsedTripPlaces = false;
|
||||||
collapsedTripStatuses = false;
|
|
||||||
shareDialogVisible = false;
|
shareDialogVisible = false;
|
||||||
packingDialogVisible = false;
|
packingDialogVisible = false;
|
||||||
isExpanded = false;
|
isExpanded = false;
|
||||||
isFilteringMode = false;
|
isFilteringMode = false;
|
||||||
packingList: PackingItem[] = [];
|
packingList: PackingItem[] = [];
|
||||||
dispPackingList: Record<string, PackingItem[]> = {};
|
dispPackingList: Record<string, PackingItem[]> = {};
|
||||||
|
checklistDialogVisible = false;
|
||||||
|
checklistItems: ChecklistItem[] = [];
|
||||||
|
dispchecklist: ChecklistItem[] = [];
|
||||||
|
|
||||||
map?: L.Map;
|
map?: L.Map;
|
||||||
markerClusterGroup?: L.MarkerClusterGroup;
|
markerClusterGroup?: L.MarkerClusterGroup;
|
||||||
@ -122,6 +126,14 @@ export class TripComponent implements AfterViewInit {
|
|||||||
this.openPackingList();
|
this.openPackingList();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Checklist",
|
||||||
|
icon: "pi pi-check-square",
|
||||||
|
iconClass: "text-purple-500!",
|
||||||
|
command: () => {
|
||||||
|
this.openChecklist();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
icon: "pi pi-pencil",
|
icon: "pi pi-pencil",
|
||||||
@ -666,7 +678,7 @@ export class TripComponent implements AfterViewInit {
|
|||||||
this.tripMapAntLayerDayID = -1; //Hardcoded value for global trace
|
this.tripMapAntLayerDayID = -1; //Hardcoded value for global trace
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTripDayHighlightPathDay(day_id: number) {
|
toggleTripDayHighlight(day_id: number) {
|
||||||
// Click on the currently displayed day: remove
|
// Click on the currently displayed day: remove
|
||||||
if (this.tripMapAntLayerDayID == day_id) {
|
if (this.tripMapAntLayerDayID == day_id) {
|
||||||
this.resetDayHighlight();
|
this.resetDayHighlight();
|
||||||
@ -1447,4 +1459,97 @@ export class TripComponent implements AfterViewInit {
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openChecklist() {
|
||||||
|
if (!this.trip) return;
|
||||||
|
|
||||||
|
if (!this.checklistItems.length)
|
||||||
|
this.apiService
|
||||||
|
.getChecklist(this.trip.id)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (items) => {
|
||||||
|
this.checklistItems = [...items];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.checklistDialogVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addChecklistItem() {
|
||||||
|
if (!this.trip) return;
|
||||||
|
|
||||||
|
const modal: DynamicDialogRef = this.dialogService.open(
|
||||||
|
TripCreateChecklistModalComponent,
|
||||||
|
{
|
||||||
|
header: "Create item",
|
||||||
|
modal: true,
|
||||||
|
appendTo: "body",
|
||||||
|
closable: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
width: "40vw",
|
||||||
|
breakpoints: {
|
||||||
|
"1260px": "70vw",
|
||||||
|
"600px": "90vw",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
|
next: (item: ChecklistItem | null) => {
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
this.apiService
|
||||||
|
.postChecklistItem(this.trip!.id, item)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (item) => {
|
||||||
|
this.checklistItems = [...this.checklistItems, item];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCheckChecklistItem(e: CheckboxChangeEvent, id: number) {
|
||||||
|
if (!this.trip) return;
|
||||||
|
this.apiService
|
||||||
|
.putChecklistItem(this.trip.id, id, { checked: e.checked })
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (item) => {
|
||||||
|
const i = this.checklistItems.find((p) => p.id == item.id);
|
||||||
|
if (i) i.checked = item.checked;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChecklistItem(item: ChecklistItem) {
|
||||||
|
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
|
||||||
|
.deleteChecklistItem(this.trip!.id, item.id)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
const index = this.checklistItems.findIndex(
|
||||||
|
(p) => p.id == item.id,
|
||||||
|
);
|
||||||
|
if (index > -1) this.checklistItems.splice(index, 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
<div pFocusTrap class="grid items-center gap-4" [formGroup]="checklistForm">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="mt-4 text-right">
|
||||||
|
<p-button (click)="closeDialog()" [disabled]="!checklistForm.dirty || !checklistForm.valid">{{
|
||||||
|
checklistForm.get("id")?.value
|
||||||
|
!== -1 ? "Update" : "Create" }}</p-button>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-trip-create-checklist-modal",
|
||||||
|
imports: [
|
||||||
|
FloatLabelModule,
|
||||||
|
InputTextModule,
|
||||||
|
ButtonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FocusTrapModule,
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: "./trip-create-checklist-modal.component.html",
|
||||||
|
styleUrl: "./trip-create-checklist-modal.component.scss",
|
||||||
|
})
|
||||||
|
export class TripCreateChecklistModalComponent {
|
||||||
|
checklistForm: FormGroup;
|
||||||
|
constructor(
|
||||||
|
private ref: DynamicDialogRef,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private config: DynamicDialogConfig,
|
||||||
|
) {
|
||||||
|
this.checklistForm = this.fb.group({
|
||||||
|
id: -1,
|
||||||
|
text: ["", { validators: Validators.required }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const patchValue = this.config.data?.packing;
|
||||||
|
if (patchValue) {
|
||||||
|
this.checklistForm.patchValue(patchValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog() {
|
||||||
|
if (!this.checklistForm.valid) return;
|
||||||
|
|
||||||
|
// Normalize data for API POST
|
||||||
|
let ret = this.checklistForm.value;
|
||||||
|
this.ref.close(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import { inject, Injectable } from "@angular/core";
|
import { inject, Injectable } from "@angular/core";
|
||||||
import { HttpClient, HttpHeaders } from "@angular/common/http";
|
import { HttpClient } from "@angular/common/http";
|
||||||
import { Category, Place } from "../types/poi";
|
import { Category, Place } from "../types/poi";
|
||||||
import { BehaviorSubject, map, Observable, shareReplay, tap } from "rxjs";
|
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 {
|
||||||
|
ChecklistItem,
|
||||||
PackingItem,
|
PackingItem,
|
||||||
SharedTripURL,
|
SharedTripURL,
|
||||||
Trip,
|
Trip,
|
||||||
@ -272,6 +273,45 @@ export class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChecklist(trip_id: number): Observable<ChecklistItem[]> {
|
||||||
|
return this.httpClient.get<ChecklistItem[]>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/checklist`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSharedTripChecklist(token: string): Observable<ChecklistItem[]> {
|
||||||
|
return this.httpClient.get<ChecklistItem[]>(
|
||||||
|
`${this.apiBaseUrl}/trips/shared/${token}/checklist`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
postChecklistItem(
|
||||||
|
trip_id: number,
|
||||||
|
item: ChecklistItem,
|
||||||
|
): Observable<ChecklistItem> {
|
||||||
|
return this.httpClient.post<ChecklistItem>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/checklist`,
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
putChecklistItem(
|
||||||
|
trip_id: number,
|
||||||
|
id: number,
|
||||||
|
item: Partial<ChecklistItem>,
|
||||||
|
): Observable<ChecklistItem> {
|
||||||
|
return this.httpClient.put<ChecklistItem>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/checklist/${id}`,
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChecklistItem(trip_id: number, id: number): Observable<null> {
|
||||||
|
return this.httpClient.delete<null>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/checklist/${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`,
|
||||||
|
|||||||
@ -16,11 +16,11 @@ export interface Trip {
|
|||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
user: string;
|
user: string;
|
||||||
days: TripDay[];
|
days: TripDay[];
|
||||||
shared?: boolean;
|
|
||||||
|
|
||||||
// POST / PUT
|
// POST / PUT
|
||||||
places: Place[];
|
places: Place[];
|
||||||
place_ids: number[];
|
place_ids: number[];
|
||||||
|
shared?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TripDay {
|
export interface TripDay {
|
||||||
@ -73,3 +73,9 @@ export interface PackingItem {
|
|||||||
qt?: number;
|
qt?: number;
|
||||||
packed?: boolean;
|
packed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChecklistItem {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
checked?: boolean;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user