// filepath: frontend/src/pages/TripChecklist.tsx import React, { useState, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import { getTripItems, toggleTripItem, updateTrip, getTags } from "../api"; import TagAutocompleteInput from "../components/TagAutocompleteInput"; 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); const [addingTag, setAddingTag] = useState(false); const inputRef = useRef(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); // Items neu laden if (id) { const updated = await getTripItems(id); setItems(updated); } } 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); // Items neu laden if (id) { const updated = await getTripItems(id); setItems(updated); } } 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(""); // Items neu laden if (id) { const updated = await getTripItems(id); setItems(updated); } } if (!trip) return
Trip not found
; // 1. Items ohne Tag const itemsWithoutTag = items.filter((item) => !item.tag); // 2. Items mit Tag, gruppiert nach tag.id const itemsByTag: Record = {}; 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); }); // Nur markierte Tag-Gruppen const markedTagIds = new Set(markedTags.map((t: any) => t.id)); const markedTagGroups = Object.values(itemsByTag) .filter((g) => markedTagIds.has(g.tag.id)) .sort((a, b) => a.tag.name.localeCompare(b.tag.name)); // Hilfsfunktion für Kategorie-Gruppierung function renderItemsWithCategories(items: any[]) { const slashCategoryMap: Record = {}; const normalItems: any[] = []; items.forEach((item) => { const name = item.name_calculated || ""; if (name.includes("/")) { const [cat, sub] = name.split("/", 2); if (!slashCategoryMap[cat]) slashCategoryMap[cat] = []; slashCategoryMap[cat].push({ ...item, _sub: sub }); } else { normalItems.push(item); } }); return ( <> {/* Haupt-Items als Grid */} {normalItems.length > 0 && (
    {normalItems.map((item) => (
  • { await toggleTripItem(item.id); const updated = await getTripItems(id!); setItems(updated); }} > 0 ? item.item.tags .map( (tag: any) => `#${tag.name}${tag.mandatory ? " *" : ""}` ) .join(" ") : "" } > {item.name_calculated} {/* Tags NICHT mehr direkt anzeigen */}
  • ))}
)} {/* Slash-Kategorien wie gehabt */} {Object.entries(slashCategoryMap).map(([cat, catItems]) => (
  • {cat.charAt(0).toUpperCase() + cat.slice(1)}

    • {catItems.map((item) => (
    • { await toggleTripItem(item.id); const updated = await getTripItems(id!); setItems(updated); }} > 0 ? item.item.tags .map( (tag: any) => `#${tag.name}${tag.mandatory ? " *" : ""}` ) .join(" ") : "" } > {item._sub} {/* Tags NICHT mehr direkt anzeigen */}
    • ))}
    ))} ); } function formatAsHeading(str: string) { if (!str) return ""; // Replace hyphens with spaces and capitalize first letter const withSpaces = str.replace(/-/g, " "); return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); } // Progress calculation const totalItems = items.length; const checkedItems = items.filter((item) => item.checked).length; const progress = totalItems > 0 ? Math.round((checkedItems / totalItems) * 100) : 0; return (
    {/* Trip-Titel und Zeitraum */}

    {trip.name}

    {trip.start_date} – {trip.end_date}
    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" link and dropdown */} {!addingTag ? ( ) : ( { handleAddTag(tagId); if (viaTab) { setTimeout(() => { setAddingTag(true); inputRef.current?.focus(); }, 0); } else { setAddingTag(false); } }} onEscape={() => setAddingTag(false)} placeholder="Tag suchen..." /> )}
    {/* Progressbar integriert am unteren Rand */}
    Fortschritt {checkedItems} / {totalItems} erledigt
    {/* gray overlay for unfilled part */}
    {progress}%
    {/* ...existing code... */}
      {/* 1. Ohne Tag */} {renderItemsWithCategories(itemsWithoutTag)} {/* 2. Markierte Tag-Gruppen */} {markedTagGroups.map(({ tag, items }) => (
    • {formatAsHeading(tag.name)}

        {renderItemsWithCategories(items)}
    • ))}
    ); }