feat: enhance TripChecklist to load and display items with associated tags and group items by marked tags

This commit is contained in:
Felix Zett 2025-08-31 19:21:38 +02:00
parent 8fe64792d7
commit d385e40171
3 changed files with 108 additions and 3 deletions

View file

@ -15,7 +15,10 @@ def list_trip_items(trip_id: UUID, db: Session = Depends(get_db)):
raise HTTPException(status_code=404, detail="Trip not found") raise HTTPException(status_code=404, detail="Trip not found")
items = ( items = (
db.query(models.TripItem) db.query(models.TripItem)
.options(joinedload(models.TripItem.tag)) .options(
joinedload(models.TripItem.tag),
joinedload(models.TripItem.item).joinedload(models.Item.tags), # <--- Item mit Tags laden
)
.filter(models.TripItem.trip_id == trip_id) .filter(models.TripItem.trip_id == trip_id)
.order_by( .order_by(
func.regexp_replace( func.regexp_replace(
@ -34,6 +37,7 @@ def list_trip_items(trip_id: UUID, db: Session = Depends(get_db)):
name_calculated=ti.name_calculated, name_calculated=ti.name_calculated,
checked=ti.checked, checked=ti.checked,
tag=ti.tag, tag=ti.tag,
item=ti.item,
) for ti in items ) for ti in items
] ]

View file

@ -1,4 +1,3 @@
from typing import List, Optional from typing import List, Optional
from uuid import UUID from uuid import UUID
from datetime import date from datetime import date
@ -60,6 +59,7 @@ class TripItemOut(BaseModel):
name_calculated: str name_calculated: str
checked: bool checked: bool
tag: Optional[TagOut] = None tag: Optional[TagOut] = None
item: Optional[ItemOut] = None
class Config: class Config:
orm_mode = True orm_mode = True

View file

@ -41,6 +41,11 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
marked_tag_ids: newMarked.map((t) => t.id), marked_tag_ids: newMarked.map((t) => t.id),
}); });
setMarkedTags(newMarked); setMarkedTags(newMarked);
// Items neu laden
if (id) {
const updated = await getTripItems(id);
setItems(updated);
}
} }
async function handleRemoveTag(tagId: string) { async function handleRemoveTag(tagId: string) {
@ -55,6 +60,11 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
}); });
setSelectedTags(newSelected); setSelectedTags(newSelected);
setMarkedTags(newMarked); setMarkedTags(newMarked);
// Items neu laden
if (id) {
const updated = await getTripItems(id);
setItems(updated);
}
} }
async function handleAddTag(tagId: string) { async function handleAddTag(tagId: string) {
@ -70,10 +80,34 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
}); });
setSelectedTags(newSelected); setSelectedTags(newSelected);
setTagInput(""); setTagInput("");
// Items neu laden
if (id) {
const updated = await getTripItems(id);
setItems(updated);
}
} }
if (!trip) return <div>Trip not found</div>; if (!trip) return <div>Trip not found</div>;
// Gruppiere Items nach Kategorie (item.tag)
const itemsWithoutTag = items.filter((item) => !item.tag);
// Map: tagId -> { tag, items: [...] }
const itemsByTag: Record<string, { tag: any; items: any[] }> = {};
items
.filter((item) => item.tag)
.forEach((item) => {
const tagId = item.tag.id;
if (!itemsByTag[tagId]) {
itemsByTag[tagId] = { tag: item.tag, items: [] };
}
itemsByTag[tagId].items.push(item);
});
// Sortiere Kategorien alphabetisch nach Tag-Name
const sortedTagGroups = Object.values(itemsByTag).sort((a, b) =>
a.tag.name.localeCompare(b.tag.name)
);
return ( return (
<div> <div>
{/* Tag-Liste */} {/* Tag-Liste */}
@ -155,6 +189,56 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
</div> </div>
{/* ...Rest der Checklist... */} {/* ...Rest der Checklist... */}
<ul> <ul>
{/* Einträge ohne Kategorie */}
{itemsWithoutTag.map((item) => (
<li
key={item.id}
className={
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded " +
(item.checked ? "bg-green-100" : "hover:bg-gray-100")
}
onClick={async () => {
await toggleTripItem(item.id);
const updated = await getTripItems(id!);
setItems(updated);
}}
>
<input
type="checkbox"
checked={item.checked}
readOnly
tabIndex={-1}
className="pointer-events-none"
/>
<span className={item.checked ? "line-through text-gray-400" : ""}>
{item.name_calculated}
</span>
{item.item && item.item.tags && item.item.tags.length > 0 && (
<span className="ml-2 flex gap-1">
{[...item.item.tags]
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((tag: any) => (
<span
key={tag.id}
className="text-xs text-gray-400 bg-gray-100 rounded px-1 py-0.5"
>
#{tag.name}
</span>
))}
</span>
)}
</li>
))}
{/* Einträge mit Kategorie */}
{sortedTagGroups.map(({ tag, items }) => (
<React.Fragment key={tag.id}>
<li className="mt-4 mb-1 font-semibold text-gray-700 flex items-center gap-2">
<span className="text-xs bg-gray-200 text-gray-700 rounded px-2 py-0.5">
#{tag.name}
</span>
</li>
{items.map((item) => ( {items.map((item) => (
<li <li
key={item.id} key={item.id}
@ -178,8 +262,25 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
<span className={item.checked ? "line-through text-gray-400" : ""}> <span className={item.checked ? "line-through text-gray-400" : ""}>
{item.name_calculated} {item.name_calculated}
</span> </span>
{item.item && item.item.tags && item.item.tags.length > 0 && (
<span className="ml-2 flex gap-1">
{[...item.item.tags]
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((tag: any) => (
<span
key={tag.id}
className="text-xs text-gray-400 bg-gray-100 rounded px-1 py-0.5"
>
#{tag.name}
</span>
))}
</span>
)}
</li> </li>
))} ))}
</React.Fragment>
))}
</ul> </ul>
</div> </div>
); );