diff --git a/backend/routes/trip_items.py b/backend/routes/trip_items.py index 9744033..66a7a76 100644 --- a/backend/routes/trip_items.py +++ b/backend/routes/trip_items.py @@ -1,6 +1,6 @@ - from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session, joinedload +from sqlalchemy import func from uuid import UUID from backend.database import get_db from backend import models @@ -17,6 +17,13 @@ def list_trip_items(trip_id: UUID, db: Session = Depends(get_db)): db.query(models.TripItem) .options(joinedload(models.TripItem.tag)) .filter(models.TripItem.trip_id == trip_id) + .order_by( + func.regexp_replace( + models.TripItem.name_calculated, + r'^\s*\d+\s*', + '' + ).asc() + ) .all() ) return [ diff --git a/backend/routes/trips.py b/backend/routes/trips.py index 45c634a..c34588c 100644 --- a/backend/routes/trips.py +++ b/backend/routes/trips.py @@ -102,26 +102,28 @@ def reconfigure_trip(trip_id: UUID, payload: TripUpdate, db: Session = Depends(g trip.end_date = payload.end_date db.flush() - for tag_id in payload.selected_tag_ids or []: + # Always use a list, never None + selected_tag_ids = payload.selected_tag_ids or [] + marked_tag_ids = payload.marked_tag_ids or [] + + for tag_id in selected_tag_ids: tag = db.query(models.Tag).get(tag_id) if tag and tag not in trip.selected_tags: trip.selected_tags.append(tag) - for tag_id in payload.marked_tag_ids or []: + for tag_id in marked_tag_ids: tag = db.query(models.Tag).get(tag_id) if tag and tag not in trip.marked_tags: trip.marked_tags.append(tag) # remove tags not in the new list - if payload.selected_tag_ids is not None: - trip.selected_tags = [tag for tag in trip.selected_tags if tag.id in payload.selected_tag_ids] - if payload.marked_tag_ids is not None: - trip.marked_tags = [tag for tag in trip.marked_tags if tag.id in payload.marked_tag_ids] + trip.selected_tags = [tag for tag in trip.selected_tags if tag.id in selected_tag_ids] + trip.marked_tags = [tag for tag in trip.marked_tags if tag.id in marked_tag_ids] db.flush() created_ids, deleted_checked = generate_trip_items( - db, trip=trip, selected_tag_ids=payload.selected_tag_ids, marked_tag_ids=payload.marked_tag_ids, + db, trip=trip, selected_tag_ids=selected_tag_ids, marked_tag_ids=marked_tag_ids, ) db.commit() return { diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 3da39ee..6e465a4 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -109,3 +109,22 @@ export async function createTrip(data: { if (!res.ok) throw new Error("Failed to create trip"); return res.json(); } + +export async function updateTrip( + tripId: string, + data: { + name: string; + start_date: string; + end_date: string; + selected_tag_ids: string[]; + marked_tag_ids: string[]; + } +): Promise { + const res = await fetch(`${API_BASE}/trips/${tripId}/reconfigure`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Failed to update trip"); + return res.json(); +} diff --git a/frontend/src/pages/TripChecklist.tsx b/frontend/src/pages/TripChecklist.tsx index d4c135c..1fa29b7 100644 --- a/frontend/src/pages/TripChecklist.tsx +++ b/frontend/src/pages/TripChecklist.tsx @@ -1,39 +1,183 @@ // filepath: frontend/src/pages/TripChecklist.tsx import React, { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; -import { getTripItems, toggleTripItem } from "../api"; +import { getTripItems, toggleTripItem, updateTrip, getTags } from "../api"; export default function TripChecklist({ trips }: { trips: any[] }) { const { id } = useParams(); const [items, setItems] = useState([]); + const [allTags, setAllTags] = useState([]); + const [tagInput, setTagInput] = useState(""); + const [hoveredTag, setHoveredTag] = useState(null); useEffect(() => { if (id) { getTripItems(id).then(setItems); + getTags().then(setAllTags); } }, [id]); const trip = trips.find((t) => t.id === id); + const [selectedTags, setSelectedTags] = useState([]); + const [markedTags, setMarkedTags] = useState([]); + + useEffect(() => { + if (trip) { + setSelectedTags(trip.selected_tags); + setMarkedTags(trip.marked_tags); + } + }, [trip]); + + async function handleToggleMark(tagId: string) { + const isMarked = markedTags.some((t) => t.id === tagId); + const newMarked = isMarked + ? markedTags.filter((t) => t.id !== tagId) + : [...markedTags, selectedTags.find((t) => t.id === tagId)]; + await updateTrip(trip.id, { + name: trip.name, + start_date: trip.start_date, + end_date: trip.end_date, + selected_tag_ids: selectedTags.map((t) => t.id), + marked_tag_ids: newMarked.map((t) => t.id), + }); + setMarkedTags(newMarked); + } + + async function handleRemoveTag(tagId: string) { + const newSelected = selectedTags.filter((t) => t.id !== tagId); + const newMarked = markedTags.filter((t) => t.id !== tagId); + await updateTrip(trip.id, { + name: trip.name, + start_date: trip.start_date, + end_date: trip.end_date, + selected_tag_ids: newSelected.map((t) => t.id), + marked_tag_ids: newMarked.map((t) => t.id), + }); + setSelectedTags(newSelected); + setMarkedTags(newMarked); + } + + async function handleAddTag(tagId: string) { + if (selectedTags.some((t) => t.id === tagId)) return; + const tagObj = allTags.find((t) => t.id === tagId); + const newSelected = [...selectedTags, tagObj]; + await updateTrip(trip.id, { + name: trip.name, + start_date: trip.start_date, + end_date: trip.end_date, + selected_tag_ids: newSelected.map((t) => t.id), + marked_tag_ids: markedTags.map((t) => t.id), + }); + setSelectedTags(newSelected); + setTagInput(""); + } if (!trip) return
Trip not found
; return (
-

{trip.name}

-

{trip.start_date} – {trip.end_date}

+ {/* Tag-Liste */} +
+ Tags: + {selectedTags.length === 0 ? ( + keine + ) : ( + selectedTags.map((tag: any) => { + const isMarked = markedTags.some((mt: any) => mt.id === tag.id); + return ( + handleToggleMark(tag.id)} + onMouseEnter={() => setHoveredTag(tag.id)} + onMouseLeave={() => setHoveredTag(null)} + > + #{tag.name} + {hoveredTag === tag.id && ( + + )} + + ); + }) + )} + {/* Tag hinzufügen */} +
+ setTagInput(e.target.value)} + className="border rounded px-2 py-0.5 text-sm ml-2" + list="tag-suggestions" + /> + + {allTags + .filter( + (t) => + t.name.toLowerCase().includes(tagInput.toLowerCase()) && + !selectedTags.some((st) => st.id === t.id) + ) + .map((t) => ( + + {tagInput && + allTags + .filter( + (t) => + t.name.toLowerCase() === tagInput.toLowerCase() && + !selectedTags.some((st) => st.id === t.id) + ) + .map((t) => ( + + ))} +
+
+ {/* ...Rest der Checklist... */}
    {items.map((item) => ( -
  • +
  • { + await toggleTripItem(item.id); + const updated = await getTripItems(id!); + setItems(updated); + }} + > { - await toggleTripItem(item.id); - const updated = await getTripItems(id!); - setItems(updated); - }} + readOnly + tabIndex={-1} + className="pointer-events-none" /> - {item.name_calculated} + + {item.name_calculated} +
  • ))}
diff --git a/frontend/src/pages/TripsPage.tsx b/frontend/src/pages/TripsPage.tsx index f4e760b..a7e9b28 100644 --- a/frontend/src/pages/TripsPage.tsx +++ b/frontend/src/pages/TripsPage.tsx @@ -77,7 +77,12 @@ export default function TripsPage() { {trips.map(trip => ( -
+

{trip.name}

@@ -86,21 +91,18 @@ export default function TripsPage() {

- - Packliste anzeigen -
-
+ ))}
);