From 2187045addcb5972c29cb210102864fb1efeeb65 Mon Sep 17 00:00:00 2001 From: Felix Zett Date: Sat, 13 Sep 2025 22:42:44 +0200 Subject: [PATCH] feat: add trip-specific items including trip_id in item creation and listing --- backend/crud.py | 26 +++++++++------- backend/models.py | 5 +++- backend/routes/items.py | 4 +-- backend/schemas.py | 2 ++ frontend/src/api.ts | 4 ++- frontend/src/components/ItemList.tsx | 4 ++- frontend/src/components/ItemRow.tsx | 15 +++++++++- frontend/src/pages/ItemsPage.tsx | 44 ++++++++++++++++++++++++++-- 8 files changed, 83 insertions(+), 21 deletions(-) diff --git a/backend/crud.py b/backend/crud.py index 58f7680..d592091 100644 --- a/backend/crud.py +++ b/backend/crud.py @@ -48,8 +48,8 @@ def render_name(name_template: str, start: Optional[date], end: Optional[date]) return _placeholder_re.sub(repl, name_template) -def items_for_trip(db: Session, user_id: UUID_t, selected_tag_ids: List[UUID_t]) -> List[models.Item]: - # Items without tags (always) + items with any of the selected_tags, +def items_for_trip(db: Session, user_id: UUID_t, trip: models.Trip, selected_tag_ids: List[UUID_t]) -> List[models.Item]: + # Items without trip_id and tags (always) + items without trip_id and with any of the selected_tags + items with trip_id equal to the current trip # but: if an item has a mandatory tag, it is only included if at least one of its mandatory tags is selected. q = ( db.query(models.Item) @@ -61,16 +61,20 @@ def items_for_trip(db: Session, user_id: UUID_t, selected_tag_ids: List[UUID_t]) selected_set = set(selected_tag_ids) result: List[models.Item] = [] for it in items: - item_tag_ids = {tag.id for tag in it.tags} - mandatory_tag_ids = {tag.id for tag in it.tags if getattr(tag, "mandatory", False)} - if not item_tag_ids: - result.append(it) - elif mandatory_tag_ids: - # Only include if at least one mandatory tag is selected - if selected_set & mandatory_tag_ids: + if it.trip_id is None: + item_tag_ids = {tag.id for tag in it.tags} + mandatory_tag_ids = {tag.id for tag in it.tags if getattr(tag, "mandatory", False)} + if not item_tag_ids: result.append(it) - elif selected_set & item_tag_ids: + elif mandatory_tag_ids: + # Only include if at least one mandatory tag is selected + if selected_set & mandatory_tag_ids: + result.append(it) + elif selected_set & item_tag_ids: + result.append(it) + elif it.trip_id == trip.id: result.append(it) + return result @@ -93,7 +97,7 @@ def generate_trip_items( db.delete(ti) db.flush() - items = items_for_trip(db, trip.user_id, selected_tag_ids) + items = items_for_trip(db, trip.user_id, trip, selected_tag_ids) created_ids: List[UUID_t] = [] marked_set = set(marked_tag_ids) diff --git a/backend/models.py b/backend/models.py index ba4e2b4..f1fd289 100644 --- a/backend/models.py +++ b/backend/models.py @@ -62,11 +62,14 @@ class Item(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id = Column(UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False) name = Column(String, nullable=False) + # Optional association to a specific trip + trip_id = Column(UUID(as_uuid=True), ForeignKey("trip.id"), nullable=True) user = relationship("User", backref="items") tags = relationship("Tag", secondary=item_tag_table, backref="items") trip_items = relationship("TripItem", back_populates="item", cascade="all, delete-orphan") - + trip = relationship("Trip", backref="special_items") + class Trip(Base): __tablename__ = "trip" diff --git a/backend/routes/items.py b/backend/routes/items.py index f2afda1..b296dc9 100644 --- a/backend/routes/items.py +++ b/backend/routes/items.py @@ -16,7 +16,6 @@ def list_items(db: Session = Depends(get_db)): @router.post("/", response_model=ItemOut) def create_item(payload: ItemCreate, db: Session = Depends(get_db)): - # Demo: use first user or create one if none exists user = db.query(models.User).first() if not user: from uuid import uuid4 @@ -24,8 +23,7 @@ def create_item(payload: ItemCreate, db: Session = Depends(get_db)): db.add(user) db.flush() - # Create the item - item = models.Item(user_id=user.id, name=payload.name) + item = models.Item(user_id=user.id, name=payload.name, trip_id=payload.trip_id) # Attach tags if provided if payload.tag_ids: diff --git a/backend/schemas.py b/backend/schemas.py index 268f589..54b69d1 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -23,10 +23,12 @@ class ItemBase(BaseModel): class ItemCreate(ItemBase): tag_ids: List[UUID] = [] + trip_id: Optional[UUID] = None class ItemOut(ItemBase): id: UUID tags: List[TagOut] = [] + trip_id: Optional[UUID] = None class Config: orm_mode = True diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 3c90876..d18ef42 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -8,6 +8,7 @@ export interface Item { id: string; name: string; tags: Tag[]; + trip_id: string; } const API_BASE = "http://localhost:8000"; // ggf. anpassen @@ -74,13 +75,14 @@ export async function addItemTag(itemId: string, tagId: string): Promise { return res.json(); } -export async function createItem(name: string, tagIds: string[]): Promise { +export async function createItem(name: string, tagIds: string[], tripId?: string): Promise { const res = await fetch(`${API_BASE}/items/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, tag_ids: tagIds, + trip_id: tripId || null, }), }); if (!res.ok) throw new Error("Failed to create item"); diff --git a/frontend/src/components/ItemList.tsx b/frontend/src/components/ItemList.tsx index 22511f2..7115de7 100644 --- a/frontend/src/components/ItemList.tsx +++ b/frontend/src/components/ItemList.tsx @@ -14,11 +14,12 @@ interface ItemListProps { export default function ItemList({ items, allTags, + trips, onUpdateName, onDeleteTag, onAddTag, onDeleteItem, -}: ItemListProps) { +}: ItemListProps & { trips: { id: string; name: string }[] }) { if (items.length === 0) { return

Keine Items gefunden.

; } @@ -30,6 +31,7 @@ export default function ItemList({ key={item.id} item={item} allTags={allTags} + trips={trips} onUpdateName={onUpdateName} onDeleteTag={onDeleteTag} onAddTag={onAddTag} diff --git a/frontend/src/components/ItemRow.tsx b/frontend/src/components/ItemRow.tsx index ec6e5eb..cf992df 100644 --- a/frontend/src/components/ItemRow.tsx +++ b/frontend/src/components/ItemRow.tsx @@ -13,11 +13,12 @@ interface ItemRowProps { export default function ItemRow({ item, allTags, + trips, onUpdateName, onDeleteTag, onAddTag, onDeleteItem, -}: ItemRowProps) { +}: ItemRowProps & { trips: { id: string; name: string }[] }) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(item.name); const [hover, setHover] = useState(false); @@ -49,6 +50,12 @@ export default function ItemRow({ ); } + // Trip-Namen lookup + const tripName = + item.trip_id && trips + ? trips.find((t) => t.id === item.trip_id)?.name + : null; + return (
  • ))} + {tripName && ( + + {tripName} + + )} + {hover && !addingTag && (