✨ Trip multi-users - BETA
This commit is contained in:
parent
cc729722df
commit
9cbbfd9065
@ -0,0 +1,91 @@
|
|||||||
|
"""Trip multi-users
|
||||||
|
|
||||||
|
Revision ID: 26c89b7466f2
|
||||||
|
Revises: 60a9bb641d8a
|
||||||
|
Create Date: 2025-08-18 23:19:37.457354
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel.sql.sqltypes
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "26c89b7466f2"
|
||||||
|
down_revision = "60a9bb641d8a"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
"tripmember",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("user", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column("invited_by", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
|
sa.Column("invited_at", sa.DateTime(), nullable=False),
|
||||||
|
sa.Column("joined_at", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("trip_id", sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["invited_by"],
|
||||||
|
["user.username"],
|
||||||
|
name=op.f("fk_tripmember_invited_by_user"),
|
||||||
|
ondelete="SET NULL",
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["trip_id"], ["trip.id"], name=op.f("fk_tripmember_trip_id_trip"), ondelete="CASCADE"
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user"], ["user.username"], name=op.f("fk_tripmember_user_user"), ondelete="CASCADE"
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_tripmember")),
|
||||||
|
)
|
||||||
|
with op.batch_alter_table("tripchecklistitem", schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(batch_op.f("fk_tripchecklistitem_user_user"), type_="foreignkey")
|
||||||
|
batch_op.drop_column("user")
|
||||||
|
|
||||||
|
with op.batch_alter_table("trippackinglistitem", schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(batch_op.f("fk_trippackinglistitem_user_user"), type_="foreignkey")
|
||||||
|
batch_op.drop_column("user")
|
||||||
|
|
||||||
|
with op.batch_alter_table("tripitem", schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(batch_op.f("fk_tripitem_day_id_tripday"), type_="foreignkey")
|
||||||
|
|
||||||
|
with op.batch_alter_table("tripday", schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(batch_op.f("fk_tripday_user_user"), type_="foreignkey")
|
||||||
|
batch_op.drop_column("user")
|
||||||
|
|
||||||
|
with op.batch_alter_table("tripitem", schema=None) as batch_op:
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
batch_op.f("fk_tripitem_day_id_tripday"),
|
||||||
|
"tripday",
|
||||||
|
["day_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table("trippackinglistitem", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("user", sa.VARCHAR(), nullable=False))
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
batch_op.f("fk_trippackinglistitem_user_user"),
|
||||||
|
"user",
|
||||||
|
["user"],
|
||||||
|
["username"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
|
||||||
|
with op.batch_alter_table("tripday", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("user", sa.VARCHAR(), nullable=False))
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
batch_op.f("fk_tripday_user_user"), "user", ["user"], ["username"], ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
with op.batch_alter_table("tripchecklistitem", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("user", sa.VARCHAR(), nullable=False))
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
batch_op.f("fk_tripchecklistitem_user_user"), "user", ["user"], ["username"], ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_table("tripmember")
|
||||||
@ -261,6 +261,7 @@ class Trip(TripBase, table=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)
|
checklist_items: list["TripChecklistItem"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||||
|
memberships: list["TripMember"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||||
|
|
||||||
|
|
||||||
class TripCreate(TripBase):
|
class TripCreate(TripBase):
|
||||||
@ -279,6 +280,7 @@ class TripReadBase(TripBase):
|
|||||||
image: str | None
|
image: str | None
|
||||||
image_id: int | None
|
image_id: int | None
|
||||||
days: int
|
days: int
|
||||||
|
collaborators: list["TripMemberRead"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serialize(cls, obj: Trip) -> "TripRead":
|
def serialize(cls, obj: Trip) -> "TripRead":
|
||||||
@ -289,6 +291,7 @@ class TripReadBase(TripBase):
|
|||||||
image=_prefix_assets_url(obj.image.filename) if obj.image else None,
|
image=_prefix_assets_url(obj.image.filename) if obj.image else None,
|
||||||
image_id=obj.image_id,
|
image_id=obj.image_id,
|
||||||
days=len(obj.days),
|
days=len(obj.days),
|
||||||
|
collaborators=[TripMemberRead.serialize(m) for m in obj.memberships],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -298,6 +301,7 @@ class TripRead(TripBase):
|
|||||||
image_id: int | None
|
image_id: int | None
|
||||||
days: list["TripDayRead"]
|
days: list["TripDayRead"]
|
||||||
places: list["PlaceRead"]
|
places: list["PlaceRead"]
|
||||||
|
collaborators: list["TripMemberRead"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serialize(cls, obj: Trip) -> "TripRead":
|
def serialize(cls, obj: Trip) -> "TripRead":
|
||||||
@ -309,17 +313,49 @@ 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],
|
||||||
|
collaborators=[TripMemberRead.serialize(m) for m in obj.memberships],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TripMember(SQLModel, table=True):
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
user: str = Field(foreign_key="user.username", ondelete="CASCADE")
|
||||||
|
invited_by: str | None = Field(default=None, foreign_key="user.username", ondelete="SET NULL")
|
||||||
|
invited_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||||
|
joined_at: datetime | None = None
|
||||||
|
|
||||||
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE")
|
||||||
|
trip: Trip | None = Relationship(back_populates="memberships")
|
||||||
|
|
||||||
|
|
||||||
|
class TripMemberCreate(BaseModel):
|
||||||
|
user: str
|
||||||
|
|
||||||
|
|
||||||
|
class TripMemberRead(BaseModel):
|
||||||
|
user: str
|
||||||
|
invited_by: str | None = None
|
||||||
|
invited_at: datetime | None = None
|
||||||
|
joined_at: datetime | None = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def serialize(cls, obj: TripMember) -> "TripMemberRead":
|
||||||
|
return cls(
|
||||||
|
user=obj.user, invited_by=obj.invited_by, invited_at=obj.invited_at, joined_at=obj.joined_at
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TripInvitationRead(TripReadBase):
|
||||||
|
invited_by: str | None = None
|
||||||
|
invited_at: datetime
|
||||||
|
|
||||||
|
|
||||||
class TripDayBase(SQLModel):
|
class TripDayBase(SQLModel):
|
||||||
label: str
|
label: str
|
||||||
|
|
||||||
|
|
||||||
class TripDay(TripDayBase, table=True):
|
class TripDay(TripDayBase, table=True):
|
||||||
id: int | None = Field(default=None, primary_key=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_id: int = Field(foreign_key="trip.id", ondelete="CASCADE")
|
||||||
trip: Trip | None = Relationship(back_populates="days")
|
trip: Trip | None = Relationship(back_populates="days")
|
||||||
|
|
||||||
@ -420,7 +456,6 @@ class TripPackingListItemBase(SQLModel):
|
|||||||
|
|
||||||
class TripPackingListItem(TripPackingListItemBase, table=True):
|
class TripPackingListItem(TripPackingListItemBase, table=True):
|
||||||
id: int | None = Field(default=None, primary_key=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_id: int = Field(foreign_key="trip.id", ondelete="CASCADE")
|
||||||
trip: Trip | None = Relationship(back_populates="packing_items")
|
trip: Trip | None = Relationship(back_populates="packing_items")
|
||||||
|
|||||||
@ -8,33 +8,120 @@ from ..deps import SessionDep, get_current_username
|
|||||||
from ..models.models import (Image, Place, Trip, TripChecklistItem,
|
from ..models.models import (Image, Place, Trip, TripChecklistItem,
|
||||||
TripChecklistItemCreate, TripChecklistItemRead,
|
TripChecklistItemCreate, TripChecklistItemRead,
|
||||||
TripChecklistItemUpdate, TripCreate, TripDay,
|
TripChecklistItemUpdate, TripCreate, TripDay,
|
||||||
TripDayBase, TripDayRead, TripItem,
|
TripDayBase, TripDayRead, TripInvitationRead,
|
||||||
TripItemCreate, TripItemRead, TripItemUpdate,
|
TripItem, TripItemCreate, TripItemRead,
|
||||||
TripPackingListItem, TripPackingListItemCreate,
|
TripItemUpdate, TripMember, TripMemberCreate,
|
||||||
TripPackingListItemRead, TripPackingListItemUpdate,
|
TripMemberRead, TripPackingListItem,
|
||||||
TripRead, TripReadBase, TripShare,
|
TripPackingListItemCreate,
|
||||||
TripShareURL, TripUpdate)
|
TripPackingListItemRead,
|
||||||
from ..security import verify_exists_and_owns
|
TripPackingListItemUpdate, TripRead, TripReadBase,
|
||||||
|
TripShare, TripShareURL, TripUpdate, User)
|
||||||
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, utc_now)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/trips", tags=["trips"])
|
router = APIRouter(prefix="/api/trips", tags=["trips"])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_trip_or_404(session, trip_id: int) -> Trip:
|
||||||
|
trip = session.get(Trip, trip_id)
|
||||||
|
if not trip:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return trip
|
||||||
|
|
||||||
|
|
||||||
|
def _trip_from_token_or_404(session, token: str) -> TripShare:
|
||||||
|
share = session.exec(select(TripShare).where(TripShare.token == token)).first()
|
||||||
|
if not share:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return share
|
||||||
|
|
||||||
|
|
||||||
|
def _trip_usernames(session, trip_id: int) -> set[str]:
|
||||||
|
owner = session.exec(select(Trip.user).where(Trip.id == trip_id)).first()
|
||||||
|
members = session.exec(select(TripMember.user).where(TripMember.trip_id == trip_id)).all()
|
||||||
|
return {owner} | set(members)
|
||||||
|
|
||||||
|
|
||||||
|
def _can_access_trip(session, trip_id: int, username: str) -> bool:
|
||||||
|
# TODO: Optimize, if trip does not exist, trip_owner is none and we keep iterating on members despite trip not found
|
||||||
|
trip_owner = session.exec(select(Trip.user).where(Trip.id == trip_id)).first()
|
||||||
|
if trip_owner == username:
|
||||||
|
return True
|
||||||
|
|
||||||
|
is_member = session.exec(
|
||||||
|
select(TripMember.id)
|
||||||
|
.where(TripMember.trip_id == trip_id, TripMember.user == username, TripMember.joined_at.is_not(None))
|
||||||
|
.limit(1)
|
||||||
|
).first()
|
||||||
|
return bool(is_member)
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_trip_member(session, trip_id: int, username: str) -> None:
|
||||||
|
if not _can_access_trip(session, trip_id, username):
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[TripReadBase])
|
@router.get("", response_model=list[TripReadBase])
|
||||||
def read_trips(
|
def read_trips(
|
||||||
session: SessionDep, current_user: Annotated[str, Depends(get_current_username)]
|
session: SessionDep, current_user: Annotated[str, Depends(get_current_username)]
|
||||||
) -> list[TripReadBase]:
|
) -> list[TripReadBase]:
|
||||||
trips = session.exec(select(Trip).filter(Trip.user == current_user))
|
trips = session.exec(
|
||||||
|
select(Trip)
|
||||||
|
.join(TripMember, isouter=True)
|
||||||
|
.where(
|
||||||
|
(Trip.user == current_user)
|
||||||
|
| ((TripMember.user == current_user) & (TripMember.joined_at.is_not(None)))
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
return [TripReadBase.serialize(trip) for trip in trips]
|
return [TripReadBase.serialize(trip) for trip in trips]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/invitations", response_model=list[TripInvitationRead])
|
||||||
|
def read_pending_invitations(
|
||||||
|
session: SessionDep,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> list[TripInvitationRead]:
|
||||||
|
pending_inviattions = session.exec(
|
||||||
|
select(TripMember, Trip)
|
||||||
|
.join(Trip, Trip.id == TripMember.trip_id)
|
||||||
|
.where(
|
||||||
|
TripMember.user == current_user,
|
||||||
|
TripMember.joined_at.is_(None),
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
invitations: list[TripInvitationRead] = []
|
||||||
|
for tm, trip in pending_inviattions:
|
||||||
|
base = TripReadBase.serialize(trip)
|
||||||
|
invitations.append(
|
||||||
|
TripInvitationRead(
|
||||||
|
**base.model_dump(),
|
||||||
|
invited_by=tm.invited_by,
|
||||||
|
invited_at=tm.invited_at,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return invitations
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/invitations/pending", response_model=bool)
|
||||||
|
def has_pending_invitations(
|
||||||
|
session: SessionDep,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> bool:
|
||||||
|
pending = session.exec(
|
||||||
|
select(TripMember.id).where(TripMember.user == current_user, TripMember.joined_at.is_(None)).limit(1)
|
||||||
|
).first()
|
||||||
|
return bool(pending)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{trip_id}", response_model=TripRead)
|
@router.get("/{trip_id}", response_model=TripRead)
|
||||||
def read_trip(
|
def read_trip(
|
||||||
session: SessionDep, trip_id: int, current_user: Annotated[str, Depends(get_current_username)]
|
session: SessionDep, trip_id: int, current_user: Annotated[str, Depends(get_current_username)]
|
||||||
) -> TripRead:
|
) -> TripRead:
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
return TripRead.serialize(db_trip)
|
return TripRead.serialize(db_trip)
|
||||||
|
|
||||||
|
|
||||||
@ -42,10 +129,7 @@ def read_trip(
|
|||||||
def create_trip(
|
def create_trip(
|
||||||
trip: TripCreate, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)]
|
trip: TripCreate, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)]
|
||||||
) -> TripReadBase:
|
) -> TripReadBase:
|
||||||
new_trip = Trip(
|
new_trip = Trip(name=trip.name, user=current_user)
|
||||||
name=trip.name,
|
|
||||||
user=current_user,
|
|
||||||
)
|
|
||||||
|
|
||||||
if trip.image:
|
if trip.image:
|
||||||
image_bytes = b64img_decode(trip.image)
|
image_bytes = b64img_decode(trip.image)
|
||||||
@ -55,17 +139,10 @@ def create_trip(
|
|||||||
|
|
||||||
image = Image(filename=filename, user=current_user)
|
image = Image(filename=filename, user=current_user)
|
||||||
session.add(image)
|
session.add(image)
|
||||||
session.commit()
|
session.flush()
|
||||||
session.refresh(image)
|
session.refresh(image)
|
||||||
new_trip.image_id = image.id
|
new_trip.image_id = image.id
|
||||||
|
|
||||||
if trip.place_ids:
|
|
||||||
for place_id in trip.place_ids:
|
|
||||||
db_place = session.get(Place, place_id)
|
|
||||||
verify_exists_and_owns(current_user, db_place)
|
|
||||||
session.add(TripPlaceLink(trip_id=new_trip.id, place_id=db_place.id))
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
session.add(new_trip)
|
session.add(new_trip)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(new_trip)
|
session.refresh(new_trip)
|
||||||
@ -79,8 +156,8 @@ def update_trip(
|
|||||||
trip: TripUpdate,
|
trip: TripUpdate,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripRead:
|
) -> TripRead:
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
if db_trip.archived and (trip.archived is not False):
|
if db_trip.archived and (trip.archived is not False):
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
@ -100,8 +177,8 @@ def update_trip(
|
|||||||
|
|
||||||
image = Image(filename=filename, user=current_user)
|
image = Image(filename=filename, user=current_user)
|
||||||
session.add(image)
|
session.add(image)
|
||||||
session.commit()
|
|
||||||
session.refresh(image)
|
session.refresh(image)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
if db_trip.image_id:
|
if db_trip.image_id:
|
||||||
old_image = session.get(Image, db_trip.image_id)
|
old_image = session.get(Image, db_trip.image_id)
|
||||||
@ -117,11 +194,16 @@ def update_trip(
|
|||||||
|
|
||||||
place_ids = trip_data.pop("place_ids", None)
|
place_ids = trip_data.pop("place_ids", None)
|
||||||
if place_ids is not None: # Could be empty [], so 'in'
|
if place_ids is not None: # Could be empty [], so 'in'
|
||||||
db_trip.places.clear()
|
allowed_users = _trip_usernames(session, trip_id)
|
||||||
|
new_places = []
|
||||||
for place_id in place_ids:
|
for place_id in place_ids:
|
||||||
db_place = session.get(Place, place_id)
|
db_place = session.get(Place, place_id)
|
||||||
verify_exists_and_owns(current_user, db_place)
|
if not db_place:
|
||||||
db_trip.places.append(db_place)
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
if db_place.user not in allowed_users:
|
||||||
|
raise HTTPException(status_code=403, detail="Place not accessible by trip members")
|
||||||
|
new_places.append(db_place)
|
||||||
|
db_trip.places = new_places
|
||||||
|
|
||||||
item_place_ids = {
|
item_place_ids = {
|
||||||
item.place.id for day in db_trip.days for item in day.items if item.place is not None
|
item.place.id for day in db_trip.days for item in day.items if item.place is not None
|
||||||
@ -143,8 +225,8 @@ def update_trip(
|
|||||||
def delete_trip(
|
def delete_trip(
|
||||||
session: SessionDep, trip_id: int, current_user: Annotated[str, Depends(get_current_username)]
|
session: SessionDep, trip_id: int, current_user: Annotated[str, Depends(get_current_username)]
|
||||||
):
|
):
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
if db_trip.archived:
|
if db_trip.archived:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
@ -171,13 +253,13 @@ def create_tripday(
|
|||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripDayRead:
|
) -> TripDayRead:
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
if db_trip.archived:
|
if db_trip.archived:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
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)
|
||||||
|
|
||||||
session.add(new_day)
|
session.add(new_day)
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -193,15 +275,14 @@ def update_tripday(
|
|||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripDayRead:
|
) -> TripDayRead:
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
if db_trip.archived:
|
if db_trip.archived:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
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)
|
if not db_day or (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")
|
||||||
|
|
||||||
td_data = td.model_dump(exclude_unset=True)
|
td_data = td.model_dump(exclude_unset=True)
|
||||||
@ -221,15 +302,14 @@ def delete_tripday(
|
|||||||
day_id: int,
|
day_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
):
|
):
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
if db_trip.archived:
|
if db_trip.archived:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
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)
|
if not db_day or (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")
|
||||||
|
|
||||||
session.delete(db_day)
|
session.delete(db_day)
|
||||||
@ -245,14 +325,14 @@ def create_tripitem(
|
|||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripItemRead:
|
) -> TripItemRead:
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
if db_trip.archived:
|
if db_trip.archived:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
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 not db_day or (db_day.trip_id != trip_id):
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
|
|
||||||
new_item = TripItem(
|
new_item = TripItem(
|
||||||
@ -266,7 +346,7 @@ def create_tripitem(
|
|||||||
status=item.status,
|
status=item.status,
|
||||||
)
|
)
|
||||||
|
|
||||||
if item.place and item.place != "":
|
if item.place is not None:
|
||||||
place_in_trip = any(place.id == item.place for place in db_trip.places)
|
place_in_trip = any(place.id == item.place for place in db_trip.places)
|
||||||
if not place_in_trip:
|
if not place_in_trip:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
@ -287,18 +367,18 @@ def update_tripitem(
|
|||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripItemRead:
|
) -> TripItemRead:
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
if db_trip.archived:
|
if db_trip.archived:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
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 not db_day or (db_day.trip_id != trip_id):
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
|
|
||||||
db_item = session.get(TripItem, item_id)
|
db_item = session.get(TripItem, item_id)
|
||||||
if db_item.day_id != day_id:
|
if not db_item or (db_item.day_id != day_id):
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
|
|
||||||
item_data = item.model_dump(exclude_unset=True)
|
item_data = item.model_dump(exclude_unset=True)
|
||||||
@ -327,18 +407,18 @@ def delete_tripitem(
|
|||||||
item_id: int,
|
item_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
):
|
):
|
||||||
db_trip = session.get(Trip, trip_id)
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
if db_trip.archived:
|
if db_trip.archived:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
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 not db_day or (db_day.trip_id != trip_id):
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
|
|
||||||
db_item = session.get(TripItem, item_id)
|
db_item = session.get(TripItem, item_id)
|
||||||
if db_item.day_id != day_id:
|
if not db_item or (db_item.day_id != day_id):
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
|
|
||||||
session.delete(db_item)
|
session.delete(db_item)
|
||||||
@ -351,11 +431,7 @@ def read_shared_trip(
|
|||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
token: str,
|
token: str,
|
||||||
) -> TripRead:
|
) -> TripRead:
|
||||||
share = session.exec(select(TripShare).where(TripShare.token == token)).first()
|
db_trip = session.get(Trip, _trip_from_token_or_404(session, token).trip_id)
|
||||||
if not share:
|
|
||||||
raise HTTPException(status_code=404, detail="Not found")
|
|
||||||
|
|
||||||
db_trip = session.get(Trip, share.trip_id)
|
|
||||||
return TripRead.serialize(db_trip)
|
return TripRead.serialize(db_trip)
|
||||||
|
|
||||||
|
|
||||||
@ -365,8 +441,7 @@ def get_shared_trip_url(
|
|||||||
trip_id: int,
|
trip_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripShareURL:
|
) -> TripShareURL:
|
||||||
db_trip = session.get(Trip, trip_id)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
|
||||||
|
|
||||||
share = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
share = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
||||||
if not share:
|
if not share:
|
||||||
@ -381,15 +456,14 @@ def create_shared_trip(
|
|||||||
trip_id: int,
|
trip_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripShareURL:
|
) -> TripShareURL:
|
||||||
db_trip = session.get(Trip, trip_id)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
|
||||||
|
|
||||||
shared = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
shared = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
||||||
if shared:
|
if shared:
|
||||||
raise HTTPException(status_code=409, detail="The resource already exists")
|
raise HTTPException(status_code=409, detail="The resource already exists")
|
||||||
|
|
||||||
token = generate_urlsafe()
|
token = generate_urlsafe()
|
||||||
trip_share = TripShare(token=token, trip_id=trip_id, user=current_user)
|
trip_share = TripShare(token=token, trip_id=trip_id)
|
||||||
session.add(trip_share)
|
session.add(trip_share)
|
||||||
session.commit()
|
session.commit()
|
||||||
return {"url": f"/s/t/{token}"}
|
return {"url": f"/s/t/{token}"}
|
||||||
@ -401,8 +475,7 @@ def delete_shared_trip(
|
|||||||
trip_id: int,
|
trip_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
):
|
):
|
||||||
db_trip = session.get(Trip, trip_id)
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
verify_exists_and_owns(current_user, db_trip)
|
|
||||||
|
|
||||||
db_share = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
db_share = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
||||||
if not db_share:
|
if not db_share:
|
||||||
@ -419,15 +492,25 @@ def read_packing_list(
|
|||||||
trip_id: int,
|
trip_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> list[TripPackingListItemRead]:
|
) -> list[TripPackingListItemRead]:
|
||||||
p_items = session.exec(
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
select(TripPackingListItem)
|
p_items = session.exec(select(TripPackingListItem).where(TripPackingListItem.trip_id == trip_id))
|
||||||
.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]
|
return [TripPackingListItemRead.serialize(i) for i in p_items]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/shared/{token}/packing", response_model=list[TripPackingListItemRead])
|
||||||
|
def read_shared_trip_packing_list(
|
||||||
|
session: SessionDep,
|
||||||
|
token: str,
|
||||||
|
) -> list[TripPackingListItemRead]:
|
||||||
|
p_items = session.exec(
|
||||||
|
select(TripPackingListItem).where(
|
||||||
|
TripPackingListItem.trip_id == _trip_from_token_or_404(session, token).trip_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return [TripPackingListItemRead.serialize(i) for i in p_items]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{trip_id}/packing", response_model=TripPackingListItemRead)
|
@router.post("/{trip_id}/packing", response_model=TripPackingListItemRead)
|
||||||
def create_packing_item(
|
def create_packing_item(
|
||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
@ -435,11 +518,8 @@ def create_packing_item(
|
|||||||
data: TripPackingListItemCreate,
|
data: TripPackingListItemCreate,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripPackingListItemRead:
|
) -> TripPackingListItemRead:
|
||||||
item = TripPackingListItem(
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
**data.model_dump(),
|
item = TripPackingListItem(**data.model_dump(), trip_id=trip_id)
|
||||||
trip_id=trip_id,
|
|
||||||
user=current_user,
|
|
||||||
)
|
|
||||||
session.add(item)
|
session.add(item)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(item)
|
session.refresh(item)
|
||||||
@ -454,11 +534,10 @@ def update_packing_item(
|
|||||||
p_id: int,
|
p_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripPackingListItemRead:
|
) -> TripPackingListItemRead:
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
db_item = session.exec(
|
db_item = session.exec(
|
||||||
select(TripPackingListItem).where(
|
select(TripPackingListItem).where(
|
||||||
TripPackingListItem.id == p_id,
|
TripPackingListItem.id == p_id, TripPackingListItem.trip_id == trip_id
|
||||||
TripPackingListItem.trip_id == trip_id,
|
|
||||||
TripPackingListItem.user == current_user,
|
|
||||||
)
|
)
|
||||||
).one_or_none()
|
).one_or_none()
|
||||||
|
|
||||||
@ -482,11 +561,10 @@ def delete_packing_item(
|
|||||||
p_id: int,
|
p_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
):
|
):
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
item = session.exec(
|
item = session.exec(
|
||||||
select(TripPackingListItem).where(
|
select(TripPackingListItem).where(
|
||||||
TripPackingListItem.id == p_id,
|
TripPackingListItem.id == p_id, TripPackingListItem.trip_id == trip_id
|
||||||
TripPackingListItem.trip_id == trip_id,
|
|
||||||
TripPackingListItem.user == current_user,
|
|
||||||
)
|
)
|
||||||
).one_or_none()
|
).one_or_none()
|
||||||
|
|
||||||
@ -504,11 +582,8 @@ def read_checklist(
|
|||||||
trip_id: int,
|
trip_id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> list[TripChecklistItemRead]:
|
) -> list[TripChecklistItemRead]:
|
||||||
items = session.exec(
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
select(TripChecklistItem).where(
|
items = session.exec(select(TripChecklistItem).where(TripChecklistItem.trip_id == trip_id))
|
||||||
TripChecklistItem.trip_id == trip_id, TripChecklistItem.user == current_user
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return [TripChecklistItemRead.serialize(i) for i in items]
|
return [TripChecklistItemRead.serialize(i) for i in items]
|
||||||
|
|
||||||
|
|
||||||
@ -532,11 +607,8 @@ def create_checklist_item(
|
|||||||
data: TripChecklistItemCreate,
|
data: TripChecklistItemCreate,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripChecklistItemRead:
|
) -> TripChecklistItemRead:
|
||||||
item = TripChecklistItem(
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
**data.model_dump(),
|
item = TripChecklistItem(**data.model_dump(), trip_id=trip_id)
|
||||||
trip_id=trip_id,
|
|
||||||
user=current_user,
|
|
||||||
)
|
|
||||||
session.add(item)
|
session.add(item)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(item)
|
session.refresh(item)
|
||||||
@ -551,12 +623,9 @@ def update_checklist_item(
|
|||||||
id: int,
|
id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
) -> TripChecklistItemRead:
|
) -> TripChecklistItemRead:
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
db_item = session.exec(
|
db_item = session.exec(
|
||||||
select(TripChecklistItem).where(
|
select(TripChecklistItem).where(TripChecklistItem.id == id, TripChecklistItem.trip_id == trip_id)
|
||||||
TripChecklistItem.id == id,
|
|
||||||
TripChecklistItem.trip_id == trip_id,
|
|
||||||
TripChecklistItem.user == current_user,
|
|
||||||
)
|
|
||||||
).one_or_none()
|
).one_or_none()
|
||||||
|
|
||||||
if not db_item:
|
if not db_item:
|
||||||
@ -579,11 +648,11 @@ def delete_checklist_item(
|
|||||||
id: int,
|
id: int,
|
||||||
current_user: Annotated[str, Depends(get_current_username)],
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
):
|
):
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
item = session.exec(
|
item = session.exec(
|
||||||
select(TripChecklistItem).where(
|
select(TripChecklistItem).where(
|
||||||
TripChecklistItem.id == id,
|
TripChecklistItem.id == id,
|
||||||
TripChecklistItem.trip_id == trip_id,
|
TripChecklistItem.trip_id == trip_id,
|
||||||
TripChecklistItem.user == current_user,
|
|
||||||
)
|
)
|
||||||
).one_or_none()
|
).one_or_none()
|
||||||
|
|
||||||
@ -593,3 +662,122 @@ def delete_checklist_item(
|
|||||||
session.delete(item)
|
session.delete(item)
|
||||||
session.commit()
|
session.commit()
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{trip_id}/members", response_model=list[TripMemberRead])
|
||||||
|
def read_trip_members(
|
||||||
|
session: SessionDep, trip_id: int, current_user: Annotated[str, Depends(get_current_username)]
|
||||||
|
) -> list[TripMemberRead]:
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
|
members: list[TripMemberRead] = []
|
||||||
|
owner = session.exec(select(Trip.user).where(Trip.id == trip_id)).first()
|
||||||
|
members.append(TripMemberRead(user=owner, invited_by=None, invited_at=None, joined_at=None))
|
||||||
|
|
||||||
|
db_members = session.exec(select(TripMember).where(TripMember.trip_id == trip_id)).all()
|
||||||
|
members.extend(TripMemberRead.serialize(m) for m in db_members)
|
||||||
|
return members
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trip_id}/members", response_model=TripMemberRead)
|
||||||
|
def invite_trip_member(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
data: TripMemberCreate,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
) -> TripMemberRead:
|
||||||
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
|
if db_trip.user == data.user:
|
||||||
|
raise HTTPException(status_code=409, detail="The resource already exists")
|
||||||
|
|
||||||
|
exists = session.exec(
|
||||||
|
select(TripMember.id)
|
||||||
|
.where(
|
||||||
|
TripMember.trip_id == trip_id,
|
||||||
|
TripMember.user == data.user,
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
).first()
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=409, detail="The resource already exists")
|
||||||
|
|
||||||
|
db_user = session.get(User, data.user)
|
||||||
|
print(data.user, db_user)
|
||||||
|
if not db_user:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
new_member = TripMember(trip_id=trip_id, user=data.user, invited_by=current_user)
|
||||||
|
session.add(new_member)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(new_member)
|
||||||
|
return TripMemberRead.serialize(new_member)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{trip_id}/members/{username}")
|
||||||
|
def delete_trip_member(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
username: str,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
):
|
||||||
|
print("yo")
|
||||||
|
db_trip = _get_trip_or_404(session, trip_id)
|
||||||
|
_verify_trip_member(session, trip_id, current_user)
|
||||||
|
|
||||||
|
if current_user == db_trip.user and current_user == username:
|
||||||
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
|
|
||||||
|
if current_user != db_trip.user and current_user != username:
|
||||||
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
|
||||||
|
member = session.exec(
|
||||||
|
select(TripMember).where(
|
||||||
|
TripMember.user == username,
|
||||||
|
TripMember.trip_id == trip_id,
|
||||||
|
)
|
||||||
|
).one_or_none()
|
||||||
|
if not member:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
session.delete(member)
|
||||||
|
session.commit()
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trip_id}/members/accept")
|
||||||
|
def accept_invite(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
):
|
||||||
|
db_member = session.exec(
|
||||||
|
select(TripMember).where(TripMember.trip_id == trip_id, TripMember.user == current_user)
|
||||||
|
).one_or_none()
|
||||||
|
if not db_member:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
if db_member.joined_at:
|
||||||
|
raise HTTPException(status_code=409, detail="Already a member")
|
||||||
|
db_member.joined_at = utc_now()
|
||||||
|
session.add(db_member)
|
||||||
|
session.commit()
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trip_id}/members/decline")
|
||||||
|
def decline_invite(
|
||||||
|
session: SessionDep,
|
||||||
|
trip_id: int,
|
||||||
|
current_user: Annotated[str, Depends(get_current_username)],
|
||||||
|
):
|
||||||
|
db_member = session.exec(
|
||||||
|
select(TripMember).where(TripMember.trip_id == trip_id, TripMember.user == current_user)
|
||||||
|
).one_or_none()
|
||||||
|
if not db_member:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
if db_member.joined_at:
|
||||||
|
raise HTTPException(status_code=409, detail="Already a member")
|
||||||
|
session.delete(db_member)
|
||||||
|
session.commit()
|
||||||
|
return {}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
from datetime import date
|
from datetime import UTC, date, datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
@ -48,6 +48,10 @@ def remove_image(path: str):
|
|||||||
raise Exception("Error deleting image:", exc, path)
|
raise Exception("Error deleting image:", exc, path)
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now():
|
||||||
|
return datetime.now(UTC)
|
||||||
|
|
||||||
|
|
||||||
def parse_str_or_date_to_date(cdate: str | date) -> date:
|
def parse_str_or_date_to_date(cdate: str | date) -> date:
|
||||||
if isinstance(cdate, str):
|
if isinstance(cdate, str):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
<div class="flex items-center gap-2 print:hidden">
|
<div class="flex items-center gap-2 print:hidden">
|
||||||
@if (!trip?.archived) {
|
@if (!trip?.archived) {
|
||||||
<div class="hidden md:flex items-center gap-2">
|
<div class="hidden md:flex items-center gap-2">
|
||||||
|
<p-button pTooltip="Manage users" text (click)="openMembersDialog()" icon="pi pi-users" />
|
||||||
<p-button pTooltip="Share Trip" text (click)="shareDialogVisible = true" icon="pi pi-share-alt" />
|
<p-button pTooltip="Share Trip" text (click)="shareDialogVisible = true" icon="pi pi-share-alt" />
|
||||||
<p-button pTooltip="Archive Trip" text (click)="toggleArchiveTrip()" icon="pi pi-box" severity="warn" />
|
<p-button pTooltip="Archive Trip" text (click)="toggleArchiveTrip()" icon="pi pi-box" severity="warn" />
|
||||||
<div class="border-l border-solid border-gray-700 h-4"></div>
|
<div class="border-l border-solid border-gray-700 h-4"></div>
|
||||||
@ -616,3 +617,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|
||||||
|
<p-dialog header="Members" [draggable]="false" [dismissableMask]="true" [modal]="true"
|
||||||
|
[(visible)]="membersDialogVisible" styleClass="w-[95%] md:w-[40%] 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)="addMember()" icon="pi pi-plus" label="Member" text />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divide-y divide-gray-100 mt-4 pb-4">
|
||||||
|
@for (m of tripMembers; track m.user) {
|
||||||
|
<div class="flex items-center justify-between gap-x-6 py-5">
|
||||||
|
<div class="flex items-center min-w-0 gap-x-4">
|
||||||
|
<div class="size-12 flex flex-none rounded-full items-center justify-center" [ngClass]="{
|
||||||
|
'bg-red-100': !m.invited_at,
|
||||||
|
'bg-gray-50': m.invited_at && !m.joined_at,
|
||||||
|
'bg-blue-100': m.invited_at && m.joined_at
|
||||||
|
}">
|
||||||
|
<i class="pi pi-user"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex min-w-0 gap-x-4">
|
||||||
|
<span class="capitalize truncate font-semibold text-gray-900">{{ m.user }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="text-center bg-gray-100 text-gray-800 text-xs px-2.5 py-0.5 rounded-md group-hover:hidden dark:bg-gray-100/85">-
|
||||||
|
{{ currency$ | async }}</span>
|
||||||
|
|
||||||
|
<div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end">
|
||||||
|
@if (!m.invited_at) {
|
||||||
|
<span
|
||||||
|
class="text-center bg-red-100 text-red-800 text-xs px-2.5 py-0.5 rounded-md group-hover:hidden dark:bg-red-100/85">Owner</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (m.invited_at && !m.joined_at) {
|
||||||
|
<span
|
||||||
|
class="text-center bg-gray-100 text-gray-800 text-xs px-2.5 py-0.5 rounded-md group-hover:hidden dark:bg-gray-100/85">Invited</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (m.joined_at) {
|
||||||
|
<span
|
||||||
|
class="text-center bg-blue-100 text-blue-800 text-xs px-2.5 py-0.5 rounded-md group-hover:hidden dark:bg-blue-100/85">Member</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (m.invited_at) {
|
||||||
|
<p-button text (click)="deleteMember(m.user)" icon="pi pi-trash" severity="danger" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</p-dialog>
|
||||||
@ -16,6 +16,7 @@ import {
|
|||||||
TripStatus,
|
TripStatus,
|
||||||
PackingItem,
|
PackingItem,
|
||||||
ChecklistItem,
|
ChecklistItem,
|
||||||
|
TripMember,
|
||||||
} from "../../types/trip";
|
} from "../../types/trip";
|
||||||
import { Place } from "../../types/poi";
|
import { Place } from "../../types/poi";
|
||||||
import {
|
import {
|
||||||
@ -57,6 +58,7 @@ 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";
|
import { TripCreateChecklistModalComponent } from "../../modals/trip-create-checklist-modal/trip-create-checklist-modal.component";
|
||||||
|
import { TripInviteMemberModalComponent } from "../../modals/trip-invite-member-modal/trip-invite-member-modal.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip",
|
selector: "app-trip",
|
||||||
@ -106,6 +108,8 @@ export class TripComponent implements AfterViewInit {
|
|||||||
checklistDialogVisible = false;
|
checklistDialogVisible = false;
|
||||||
checklistItems: ChecklistItem[] = [];
|
checklistItems: ChecklistItem[] = [];
|
||||||
dispchecklist: ChecklistItem[] = [];
|
dispchecklist: ChecklistItem[] = [];
|
||||||
|
membersDialogVisible = false;
|
||||||
|
tripMembers: TripMember[] = [];
|
||||||
|
|
||||||
map?: L.Map;
|
map?: L.Map;
|
||||||
markerClusterGroup?: L.MarkerClusterGroup;
|
markerClusterGroup?: L.MarkerClusterGroup;
|
||||||
@ -134,6 +138,14 @@ export class TripComponent implements AfterViewInit {
|
|||||||
this.openChecklist();
|
this.openChecklist();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Members",
|
||||||
|
icon: "pi pi-users",
|
||||||
|
iconClass: "text-blue-500!",
|
||||||
|
command: () => {
|
||||||
|
this.openMembersDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
icon: "pi pi-pencil",
|
icon: "pi pi-pencil",
|
||||||
@ -1552,4 +1564,84 @@ export class TripComponent implements AfterViewInit {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openMembersDialog() {
|
||||||
|
if (!this.trip) return;
|
||||||
|
|
||||||
|
if (!this.tripMembers.length)
|
||||||
|
this.apiService
|
||||||
|
.getTripMembers(this.trip.id)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (items) => {
|
||||||
|
this.tripMembers = [...items];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.membersDialogVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMember() {
|
||||||
|
if (!this.trip) return;
|
||||||
|
|
||||||
|
const modal: DynamicDialogRef = this.dialogService.open(
|
||||||
|
TripInviteMemberModalComponent,
|
||||||
|
{
|
||||||
|
header: "Invite member",
|
||||||
|
modal: true,
|
||||||
|
appendTo: "body",
|
||||||
|
closable: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
width: "40vw",
|
||||||
|
breakpoints: {
|
||||||
|
"1260px": "70vw",
|
||||||
|
"600px": "90vw",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
|
next: (user: string | null) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
this.apiService
|
||||||
|
.inviteTripMember(this.trip!.id, user)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (member) => {
|
||||||
|
this.tripMembers = [...this.tripMembers, member];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteMember(username: string) {
|
||||||
|
const modal = this.dialogService.open(YesNoModalComponent, {
|
||||||
|
header: "Confirm deletion",
|
||||||
|
modal: true,
|
||||||
|
closable: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
breakpoints: {
|
||||||
|
"640px": "90vw",
|
||||||
|
},
|
||||||
|
data: `Delete ${username.substring(0, 50)} from Trip ?`,
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
|
next: (bool) => {
|
||||||
|
if (!bool) return;
|
||||||
|
this.apiService
|
||||||
|
.deleteTripMember(this.trip!.id, username)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
const index = this.tripMembers.findIndex(
|
||||||
|
(p) => p.user == username,
|
||||||
|
);
|
||||||
|
if (index > -1) this.tripMembers.splice(index, 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,10 @@
|
|||||||
<img src="favicon.png" (click)="gotoMap()" class="cursor-pointer w-24" />
|
<img src="favicon.png" (click)="gotoMap()" class="cursor-pointer w-24" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<p-button icon="pi pi-map" (click)="gotoMap()" text />
|
<div class="relative">
|
||||||
|
<p-button icon="pi pi-bell" (click)="toggleInvitations()" text />
|
||||||
|
@if(hasPendingInvitations) {<div class="absolute top-1 right-2 size-2 bg-red-400 rounded-full"></div>}
|
||||||
|
</div>
|
||||||
<p-button icon="pi pi-plus" (click)="addTrip()" text />
|
<p-button icon="pi pi-plus" (click)="addTrip()" text />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -19,7 +22,13 @@
|
|||||||
|
|
||||||
<div class="absolute inset-0 bg-black/5 flex flex-col justify-end p-4 text-white">
|
<div class="absolute inset-0 bg-black/5 flex flex-col justify-end p-4 text-white">
|
||||||
<h3 class="text-lg font-semibold line-clamp-2">{{ trip.name }}</h3>
|
<h3 class="text-lg font-semibold line-clamp-2">{{ trip.name }}</h3>
|
||||||
<p class="text-sm ">{{ trip.days || 0 }} {{ trip.days > 1 ? 'days' : 'day'}}</p>
|
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<p>{{ trip.days || 0 }} {{ trip.days > 1 ? 'days' : 'day'}}</p>
|
||||||
|
@if (trip.collaborators.length) {- <p>{{ trip.collaborators.length + 1 }} user{{ trip.collaborators.length > 0
|
||||||
|
? 's'
|
||||||
|
: '' }}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<i
|
<i
|
||||||
class="pi pi-arrow-right text-xl absolute right-4 bottom-4 h-4 opacity-0 -translate-x-4 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300"></i>
|
class="pi pi-arrow-right text-xl absolute right-4 bottom-4 h-4 opacity-0 -translate-x-4 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300"></i>
|
||||||
@ -49,3 +58,39 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p-dialog header="Invitations" [draggable]="false" [dismissableMask]="true" [modal]="true"
|
||||||
|
[(visible)]="invitationsDialogVisible" styleClass="w-[95%] md:w-[40%] lg:w-[30%]">
|
||||||
|
<section class="p-4 max-w-full max-h-[80%] md:max-h-[600px]">
|
||||||
|
<div class="divide-y divide-gray-100">
|
||||||
|
@for (inv of invitations; track inv.id) {
|
||||||
|
<div class="flex items-center justify-between gap-x-6 py-5">
|
||||||
|
<div class="flex items-center min-w-0 gap-x-4">
|
||||||
|
<img [src]="inv.image" class="w-20 rounded-full object-cover">
|
||||||
|
|
||||||
|
<div class="flex flex-col min-w-0 gap-1">
|
||||||
|
<div class="capitalize truncate text-md font-semibold text-gray-900">{{ inv.name }}</div>
|
||||||
|
<div class="text-xs md:text-sm text-gray-500">On <i>{{ inv.invited_at | date: 'short'}}</i>, <i>{{
|
||||||
|
inv.invited_by
|
||||||
|
}}</i> invited you to collaborate </div>
|
||||||
|
|
||||||
|
<div class="flex justify-center md:hidden gap-4">
|
||||||
|
<p-button text (click)="acceptInvitation(inv.id)" icon="pi pi-check" severity="success" />
|
||||||
|
<p-button text (click)="declineInvitation(inv.id)" icon="pi pi-times" severity="danger" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:flex gap-4">
|
||||||
|
<p-button text (click)="acceptInvitation(inv.id)" icon="pi pi-check" severity="success" />
|
||||||
|
<p-button text (click)="declineInvitation(inv.id)" icon="pi pi-times" severity="danger" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @empty {
|
||||||
|
<div class="text-center">
|
||||||
|
No invitation
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</p-dialog>
|
||||||
@ -2,11 +2,13 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { ApiService } from "../../services/api.service";
|
import { ApiService } from "../../services/api.service";
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from "primeng/button";
|
||||||
import { SkeletonModule } from "primeng/skeleton";
|
import { SkeletonModule } from "primeng/skeleton";
|
||||||
import { TripBase } from "../../types/trip";
|
import { TripBase, TripInvitation } from "../../types/trip";
|
||||||
import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog";
|
||||||
import { TripCreateModalComponent } from "../../modals/trip-create-modal/trip-create-modal.component";
|
import { TripCreateModalComponent } from "../../modals/trip-create-modal/trip-create-modal.component";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { forkJoin, take } from "rxjs";
|
import { forkJoin, take } from "rxjs";
|
||||||
|
import { DatePipe } from "@angular/common";
|
||||||
|
import { DialogModule } from "primeng/dialog";
|
||||||
|
|
||||||
interface TripBaseWithDates extends TripBase {
|
interface TripBaseWithDates extends TripBase {
|
||||||
from?: Date;
|
from?: Date;
|
||||||
@ -16,12 +18,15 @@ interface TripBaseWithDates extends TripBase {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "app-trips",
|
selector: "app-trips",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SkeletonModule, ButtonModule],
|
imports: [SkeletonModule, ButtonModule, DialogModule, DatePipe],
|
||||||
templateUrl: "./trips.component.html",
|
templateUrl: "./trips.component.html",
|
||||||
styleUrls: ["./trips.component.scss"],
|
styleUrls: ["./trips.component.scss"],
|
||||||
})
|
})
|
||||||
export class TripsComponent implements OnInit {
|
export class TripsComponent implements OnInit {
|
||||||
trips: TripBase[] = [];
|
trips: TripBase[] = [];
|
||||||
|
hasPendingInvitations = false;
|
||||||
|
invitations: TripInvitation[] = [];
|
||||||
|
invitationsDialogVisible = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
@ -39,6 +44,12 @@ export class TripsComponent implements OnInit {
|
|||||||
this.sortTrips();
|
this.sortTrips();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.apiService
|
||||||
|
.getHasTripsInvitations()
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (bool) => (this.hasPendingInvitations = bool),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
gotoMap() {
|
gotoMap() {
|
||||||
@ -137,4 +148,50 @@ export class TripsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleInvitations() {
|
||||||
|
if (!this.invitations.length)
|
||||||
|
this.apiService
|
||||||
|
.getTripsInvitations()
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (items) => {
|
||||||
|
this.invitations = [...items];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.invitationsDialogVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeInvitationAndHide(trip_id: number) {
|
||||||
|
this.invitations = this.invitations.filter((inv) => inv.id != trip_id);
|
||||||
|
this.hasPendingInvitations = !!this.invitations.length;
|
||||||
|
this.invitationsDialogVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptInvitation(trip_id: number) {
|
||||||
|
this.apiService
|
||||||
|
.acceptTripMemberInvite(trip_id)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
const index = this.invitations.findIndex((inv) => inv.id == trip_id);
|
||||||
|
if (index > -1) {
|
||||||
|
this.trips = [...this.trips, this.invitations[index]];
|
||||||
|
this.sortTrips();
|
||||||
|
}
|
||||||
|
this.removeInvitationAndHide(trip_id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
declineInvitation(trip_id: number) {
|
||||||
|
this.apiService
|
||||||
|
.declineTripMemberInvite(trip_id)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.removeInvitationAndHide(trip_id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
<div pFocusTrap class="grid items-center gap-4">
|
||||||
|
<p-floatlabel variant="in">
|
||||||
|
<input class="col-span-2" id="name" [formControl]="memberForm" (keypress.enter)="closeDialog()" pInputText fluid />
|
||||||
|
<label for="name">Name</label>
|
||||||
|
</p-floatlabel>
|
||||||
|
|
||||||
|
<div class="mt-4 text-right">
|
||||||
|
<p-button (click)="closeDialog()" [disabled]="!memberForm.value">Invite</p-button>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { FormControl, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { ButtonModule } from "primeng/button";
|
||||||
|
import { DynamicDialogRef } from "primeng/dynamicdialog";
|
||||||
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
|
import { InputTextModule } from "primeng/inputtext";
|
||||||
|
import { FocusTrapModule } from "primeng/focustrap";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-trip-invite-member-modal",
|
||||||
|
imports: [
|
||||||
|
FloatLabelModule,
|
||||||
|
InputTextModule,
|
||||||
|
ButtonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FocusTrapModule,
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: "./trip-invite-member-modal.component.html",
|
||||||
|
styleUrl: "./trip-invite-member-modal.component.scss",
|
||||||
|
})
|
||||||
|
export class TripInviteMemberModalComponent {
|
||||||
|
memberForm = new FormControl("");
|
||||||
|
constructor(private ref: DynamicDialogRef) {}
|
||||||
|
|
||||||
|
closeDialog() {
|
||||||
|
if (!this.memberForm.value) return;
|
||||||
|
|
||||||
|
// Normalize data for API POST
|
||||||
|
this.ref.close(this.memberForm.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { inject, Injectable } from "@angular/core";
|
import { inject, Injectable } from "@angular/core";
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient, HttpHeaders } 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";
|
||||||
@ -11,7 +11,9 @@ import {
|
|||||||
Trip,
|
Trip,
|
||||||
TripBase,
|
TripBase,
|
||||||
TripDay,
|
TripDay,
|
||||||
|
TripInvitation,
|
||||||
TripItem,
|
TripItem,
|
||||||
|
TripMember,
|
||||||
} from "../types/trip";
|
} from "../types/trip";
|
||||||
|
|
||||||
const NO_AUTH_HEADER = {
|
const NO_AUTH_HEADER = {
|
||||||
@ -312,6 +314,51 @@ export class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHasTripsInvitations(): Observable<boolean> {
|
||||||
|
return this.httpClient.get<boolean>(
|
||||||
|
`${this.apiBaseUrl}/trips/invitations/pending`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTripsInvitations(): Observable<TripInvitation[]> {
|
||||||
|
return this.httpClient.get<TripInvitation[]>(
|
||||||
|
`${this.apiBaseUrl}/trips/invitations`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTripMembers(trip_id: number): Observable<TripMember[]> {
|
||||||
|
return this.httpClient.get<TripMember[]>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/members`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTripMember(trip_id: number, username: string): Observable<null> {
|
||||||
|
return this.httpClient.delete<null>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/members/${username}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteTripMember(trip_id: number, user: string): Observable<TripMember> {
|
||||||
|
return this.httpClient.post<TripMember>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/members`,
|
||||||
|
{ user },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptTripMemberInvite(trip_id: number): Observable<null> {
|
||||||
|
return this.httpClient.post<null>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/members/accept`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
declineTripMemberInvite(trip_id: number): Observable<null> {
|
||||||
|
return this.httpClient.post<null>(
|
||||||
|
`${this.apiBaseUrl}/trips/${trip_id}/members/decline`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
checkVersion(): Observable<string> {
|
checkVersion(): Observable<string> {
|
||||||
return this.httpClient.get<string>(
|
return this.httpClient.get<string>(
|
||||||
`${this.apiBaseUrl}/settings/checkversion`,
|
`${this.apiBaseUrl}/settings/checkversion`,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export interface TripBase {
|
|||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
user: string;
|
user: string;
|
||||||
days: number;
|
days: number;
|
||||||
|
collaborators: TripMember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Trip {
|
export interface Trip {
|
||||||
@ -16,6 +17,7 @@ export interface Trip {
|
|||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
user: string;
|
user: string;
|
||||||
days: TripDay[];
|
days: TripDay[];
|
||||||
|
collaborators: TripMember[];
|
||||||
|
|
||||||
// POST / PUT
|
// POST / PUT
|
||||||
places: Place[];
|
places: Place[];
|
||||||
@ -62,6 +64,18 @@ export interface FlattenedTripItem {
|
|||||||
status?: TripStatus;
|
status?: TripStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TripMember {
|
||||||
|
user: string;
|
||||||
|
invited_by: string;
|
||||||
|
invited_at: string;
|
||||||
|
joined_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TripInvitation extends TripBase {
|
||||||
|
invited_by: string;
|
||||||
|
invited_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SharedTripURL {
|
export interface SharedTripURL {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user