feat: enhance TripChecklist to load and display items with associated tags and group items by marked tags
This commit is contained in:
parent
8fe64792d7
commit
d385e40171
3 changed files with 108 additions and 3 deletions
|
|
@ -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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +189,8 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
|
||||||
</div>
|
</div>
|
||||||
{/* ...Rest der Checklist... */}
|
{/* ...Rest der Checklist... */}
|
||||||
<ul>
|
<ul>
|
||||||
{items.map((item) => (
|
{/* Einträge ohne Kategorie */}
|
||||||
|
{itemsWithoutTag.map((item) => (
|
||||||
<li
|
<li
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={
|
className={
|
||||||
|
|
@ -178,8 +213,74 @@ 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>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* 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) => (
|
||||||
|
<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>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue