feat: extract TripItemList to component and add TripEdit page
This commit is contained in:
parent
c7b720ba4a
commit
14905dd033
4 changed files with 379 additions and 166 deletions
|
|
@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route, Link, useLocation, Navigate, us
|
||||||
import { getSeed, getTrips, getNextTripId } from "./api";
|
import { getSeed, getTrips, getNextTripId } from "./api";
|
||||||
import ItemsPage from "./pages/ItemsPage";
|
import ItemsPage from "./pages/ItemsPage";
|
||||||
import TripChecklist from "./pages/TripChecklist";
|
import TripChecklist from "./pages/TripChecklist";
|
||||||
|
import TripEdit from "./pages/TripEdit";
|
||||||
import TripsPage from "./pages/TripsPage";
|
import TripsPage from "./pages/TripsPage";
|
||||||
import TagsPage from "./pages/TagsPage";
|
import TagsPage from "./pages/TagsPage";
|
||||||
import logo from "./assets/logo.svg";
|
import logo from "./assets/logo.svg";
|
||||||
|
|
@ -173,6 +174,7 @@ export default function App() {
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/trips" element={<TripsPage />} />
|
<Route path="/trips" element={<TripsPage />} />
|
||||||
<Route path="/trips/:id" element={<TripChecklist trips={trips} />} />
|
<Route path="/trips/:id" element={<TripChecklist trips={trips} />} />
|
||||||
|
<Route path="/trips/:id/edit" element={<TripEdit trips={trips} />} />
|
||||||
<Route path="/items" element={<ItemsPage />} />
|
<Route path="/items" element={<ItemsPage />} />
|
||||||
<Route path="/tags" element={<TagsPage />} />
|
<Route path="/tags" element={<TagsPage />} />
|
||||||
<Route path="/" element={<NextTripRedirect trips={trips} />} />
|
<Route path="/" element={<NextTripRedirect trips={trips} />} />
|
||||||
|
|
|
||||||
172
frontend/src/components/TripItemList.tsx
Normal file
172
frontend/src/components/TripItemList.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
import React from "react";
|
||||||
|
import { toggleTripItem, getTripItems } from "../api";
|
||||||
|
|
||||||
|
export function TripItemList({
|
||||||
|
tripId,
|
||||||
|
items,
|
||||||
|
markedTags,
|
||||||
|
reloadItems,
|
||||||
|
}: {
|
||||||
|
tripId: string;
|
||||||
|
items: any[];
|
||||||
|
markedTags: any[];
|
||||||
|
reloadItems: () => Promise<void>;
|
||||||
|
}) {
|
||||||
|
// 1. Items ohne Tag
|
||||||
|
const itemsWithoutTag = items.filter((item) => !item.tag);
|
||||||
|
|
||||||
|
// 2. Items mit Tag, gruppiert nach tag.id
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsfunktion für Kategorie-Gruppierung
|
||||||
|
function renderItemsWithCategories(items: any[]) {
|
||||||
|
const slashCategoryMap: Record<string, any[]> = {};
|
||||||
|
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 && (
|
||||||
|
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 mb-4">
|
||||||
|
{normalItems.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className={
|
||||||
|
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded group " +
|
||||||
|
(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" : ""}
|
||||||
|
title={
|
||||||
|
item.item && item.item.tags && item.item.tags.length > 0
|
||||||
|
? item.item.tags
|
||||||
|
.map(
|
||||||
|
(tag: any) =>
|
||||||
|
`#${tag.name}${tag.mandatory ? " *" : ""}`
|
||||||
|
)
|
||||||
|
.join(" ")
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.name_calculated}
|
||||||
|
</span>
|
||||||
|
{/* Tags NICHT mehr direkt anzeigen */}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{/* Slash-Kategorien wie gehabt */}
|
||||||
|
{Object.entries(slashCategoryMap).map(([cat, catItems]) => (
|
||||||
|
<React.Fragment key={cat}>
|
||||||
|
<li className="mt-2 mb-1 flex items-center gap-2">
|
||||||
|
<h3 className="text-base font-semibold text-gray-600 m-0">
|
||||||
|
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4">
|
||||||
|
{catItems.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className={
|
||||||
|
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded group " +
|
||||||
|
(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" : ""}
|
||||||
|
title={
|
||||||
|
item.item && item.item.tags && item.item.tags.length > 0
|
||||||
|
? item.item.tags
|
||||||
|
.map(
|
||||||
|
(tag: any) =>
|
||||||
|
`#${tag.name}${tag.mandatory ? " *" : ""}`
|
||||||
|
)
|
||||||
|
.join(" ")
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item._sub}
|
||||||
|
</span>
|
||||||
|
{/* Tags NICHT mehr direkt anzeigen */}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="grid grid-cols-1 gap-x-8 gap-y-2">
|
||||||
|
{renderItemsWithCategories(itemsWithoutTag)}
|
||||||
|
{markedTagGroups.map(({ tag, items }) => (
|
||||||
|
<li key={tag.id} className="mt-6 mb-6 p-4 rounded-lg border-2 border-yellow-200 bg-yellow-50 shadow-sm">
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<h2 className="text-lg font-bold text-yellow-700 m-0">{formatAsHeading(tag.name)}</h2>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{renderItemsWithCategories(items)}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { getTripItems, toggleTripItem, updateTrip, getTags } from "../api";
|
||||||
import TagAutocompleteInput from "../components/TagAutocompleteInput";
|
import TagAutocompleteInput from "../components/TagAutocompleteInput";
|
||||||
import TripTag from "../components/Tag";
|
import TripTag from "../components/Tag";
|
||||||
import Tag from "../components/Tag";
|
import Tag from "../components/Tag";
|
||||||
|
import { TripItemList } from "../components/TripItemList";
|
||||||
|
|
||||||
export default function TripChecklist({ trips }: { trips: any[] }) {
|
export default function TripChecklist({ trips }: { trips: any[] }) {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
@ -94,148 +95,6 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
|
||||||
|
|
||||||
if (!trip) return <div>Trip not found</div>;
|
if (!trip) return <div>Trip not found</div>;
|
||||||
|
|
||||||
// 1. Items ohne Tag
|
|
||||||
const itemsWithoutTag = items.filter((item) => !item.tag);
|
|
||||||
|
|
||||||
// 2. Items mit Tag, gruppiert nach tag.id
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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<string, any[]> = {};
|
|
||||||
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 && (
|
|
||||||
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 mb-4">
|
|
||||||
{normalItems.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item.id}
|
|
||||||
className={
|
|
||||||
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded group " +
|
|
||||||
(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" : ""}
|
|
||||||
title={
|
|
||||||
item.item && item.item.tags && item.item.tags.length > 0
|
|
||||||
? item.item.tags
|
|
||||||
.map(
|
|
||||||
(tag: any) =>
|
|
||||||
`#${tag.name}${tag.mandatory ? " *" : ""}`
|
|
||||||
)
|
|
||||||
.join(" ")
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item.name_calculated}
|
|
||||||
</span>
|
|
||||||
{/* Tags NICHT mehr direkt anzeigen */}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
{/* Slash-Kategorien wie gehabt */}
|
|
||||||
{Object.entries(slashCategoryMap).map(([cat, catItems]) => (
|
|
||||||
<React.Fragment key={cat}>
|
|
||||||
<li className="mt-2 mb-1 flex items-center gap-2">
|
|
||||||
<h3 className="text-base font-semibold text-gray-600 m-0">
|
|
||||||
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
||||||
</h3>
|
|
||||||
</li>
|
|
||||||
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4">
|
|
||||||
{catItems.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item.id}
|
|
||||||
className={
|
|
||||||
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded group " +
|
|
||||||
(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" : ""}
|
|
||||||
title={
|
|
||||||
item.item && item.item.tags && item.item.tags.length > 0
|
|
||||||
? item.item.tags
|
|
||||||
.map(
|
|
||||||
(tag: any) =>
|
|
||||||
`#${tag.name}${tag.mandatory ? " *" : ""}`
|
|
||||||
)
|
|
||||||
.join(" ")
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item._sub}
|
|
||||||
</span>
|
|
||||||
{/* Tags NICHT mehr direkt anzeigen */}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// Progress calculation
|
||||||
const totalItems = items.length;
|
const totalItems = items.length;
|
||||||
const checkedItems = items.filter((item) => item.checked).length;
|
const checkedItems = items.filter((item) => item.checked).length;
|
||||||
|
|
@ -335,30 +194,16 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* ...restliche Seite... */}
|
{/* ...restliche Seite... */}
|
||||||
<ul
|
|
||||||
className="grid grid-cols-1 gap-x-8 gap-y-2"
|
|
||||||
>
|
|
||||||
{/* 1. Ohne Tag */}
|
|
||||||
{renderItemsWithCategories(itemsWithoutTag)}
|
|
||||||
|
|
||||||
{/* 2. Markierte Tag-Gruppen */}
|
<TripItemList
|
||||||
{markedTagGroups.map(({ tag, items }) => (
|
tripId={trip.id}
|
||||||
<li
|
items={items}
|
||||||
key={tag.id}
|
markedTags={markedTags}
|
||||||
className="mt-6 mb-6 p-4 rounded-lg border-2 border-yellow-200 bg-yellow-50 shadow-sm"
|
reloadItems={async () => {
|
||||||
style={{ listStyle: "none" }}
|
const updated = await getTripItems(id!);
|
||||||
>
|
setItems(updated);
|
||||||
<div className="mb-2 flex items-center gap-2">
|
}}
|
||||||
<h2 className="text-lg font-bold text-yellow-700 m-0">
|
/>
|
||||||
{formatAsHeading(tag.name)}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
{renderItemsWithCategories(items)}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
194
frontend/src/pages/TripEdit.tsx
Normal file
194
frontend/src/pages/TripEdit.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { getTrips, updateTrip, getTags, getTripItems } from "../api";
|
||||||
|
import TagAutocompleteInput from "../components/TagAutocompleteInput";
|
||||||
|
import Tag from "../components/Tag";
|
||||||
|
import { TripItemList } from "../components/TripItemList";
|
||||||
|
|
||||||
|
export default function TripEdit({ trips }: { trips: any[] }) {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const trip = trips.find((t) => t.id === id);
|
||||||
|
|
||||||
|
const [name, setName] = useState(trip?.name || "");
|
||||||
|
const [startDate, setStartDate] = useState(trip?.start_date || "");
|
||||||
|
const [endDate, setEndDate] = useState(trip?.end_date || "");
|
||||||
|
const [selectedTags, setSelectedTags] = useState<any[]>(trip?.selected_tags || []);
|
||||||
|
const [markedTags, setMarkedTags] = useState<any[]>(trip?.marked_tags || []);
|
||||||
|
const [allTags, setAllTags] = useState<any[]>([]);
|
||||||
|
const [addingTag, setAddingTag] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [items, setItems] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getTags().then(setAllTags);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (trip?.id) {
|
||||||
|
getTripItems(trip.id).then(setItems);
|
||||||
|
}
|
||||||
|
}, [trip]);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
await updateTrip(trip.id, {
|
||||||
|
name,
|
||||||
|
start_date: startDate,
|
||||||
|
end_date: endDate,
|
||||||
|
selected_tag_ids: selectedTags.map((t) => t.id),
|
||||||
|
marked_tag_ids: markedTags.map((t) => t.id),
|
||||||
|
});
|
||||||
|
navigate(`/trips/${trip.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)];
|
||||||
|
setMarkedTags(newMarked);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemoveTag(tagId: string) {
|
||||||
|
setSelectedTags(selectedTags.filter((t) => t.id !== tagId));
|
||||||
|
setMarkedTags(markedTags.filter((t) => t.id !== tagId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddTag(tagId: string) {
|
||||||
|
if (selectedTags.some((t) => t.id === tagId)) return;
|
||||||
|
const tagObj = allTags.find((t) => t.id === tagId);
|
||||||
|
setSelectedTags([...selectedTags, tagObj]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsWithoutTag = items.filter((item) => !item.tag);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
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));
|
||||||
|
|
||||||
|
function renderItemsWithCategories(items: any[]) {
|
||||||
|
return items.map((item) => (
|
||||||
|
<div key={item.id} className="py-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-gray-700">{item.name}</div>
|
||||||
|
<div className="text-xs text-gray-500">{item.date}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadItems() {
|
||||||
|
if (trip?.id) {
|
||||||
|
const updated = await getTripItems(trip.id);
|
||||||
|
setItems(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trip) return <div>Trip not found</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-4 max-w-5xl mx-auto">
|
||||||
|
<div className="mb-6 px-4 py-3 pb-6 rounded-xl border shadow flex flex-wrap items-center gap-4 relative">
|
||||||
|
<div className="flex flex-col flex-1 min-w-[180px]" style={{ position: "relative", zIndex: 2 }}>
|
||||||
|
<div className="flex flex-nowrap items-end gap-2">
|
||||||
|
<input
|
||||||
|
className="text-xl font-bold text-gray-900 bg-transparent border-b border-gray-300 px-2 py-1 mr-2"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
placeholder="Trip Name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="text-xs text-gray-500 px-2 py-0.5 rounded font-semibold border border-gray-300"
|
||||||
|
value={startDate}
|
||||||
|
onChange={e => setStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="mx-1 text-gray-500">–</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="text-xs text-gray-500 px-2 py-0.5 rounded font-semibold border border-gray-300"
|
||||||
|
value={endDate}
|
||||||
|
onChange={e => setEndDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Tags kompakt rechts */}
|
||||||
|
<div className="flex flex-wrap gap-2 items-center min-w-[180px]" style={{ position: "relative", zIndex: 2 }}>
|
||||||
|
{selectedTags.length === 0 ? (
|
||||||
|
<span className="text-gray-400 text-sm">keine</span>
|
||||||
|
) : (
|
||||||
|
selectedTags.map((tag: any) => (
|
||||||
|
<Tag
|
||||||
|
key={tag.id}
|
||||||
|
tag={tag}
|
||||||
|
isMarked={markedTags.some((mt: any) => mt.id === tag.id)}
|
||||||
|
onToggleMark={handleToggleMark}
|
||||||
|
onRemoveTag={handleRemoveTag}
|
||||||
|
onMouseEnter={() => {}}
|
||||||
|
onMouseLeave={() => {}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{!addingTag ? (
|
||||||
|
<button
|
||||||
|
className="text-blue-500 underline text-xs ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
setAddingTag(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
+ Tag
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<TagAutocompleteInput
|
||||||
|
ref={inputRef}
|
||||||
|
allTags={allTags}
|
||||||
|
selectedTags={selectedTags}
|
||||||
|
onAddTag={(tagId, viaTab) => {
|
||||||
|
handleAddTag(tagId);
|
||||||
|
if (viaTab) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setAddingTag(true);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
setAddingTag(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onEscape={() => setAddingTag(false)}
|
||||||
|
placeholder="Tag suchen..."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="ml-4 px-4 py-2 rounded bg-green-500 text-white font-semibold shadow"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Optional: Vorschau der Items wie in TripChecklist */}
|
||||||
|
<TripItemList
|
||||||
|
tripId={trip.id}
|
||||||
|
items={items}
|
||||||
|
renderItemsWithCategories={renderItemsWithCategories}
|
||||||
|
markedTagGroups={markedTagGroups}
|
||||||
|
itemsWithoutTag={itemsWithoutTag}
|
||||||
|
reloadItems={reloadItems}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue