feat: add delete and add trip functionality and TripsPage component

This commit is contained in:
Felix Zett 2025-08-30 21:31:11 +02:00
parent 6e6c23b6ad
commit 6456b320c2
4 changed files with 105 additions and 60 deletions

View file

@ -142,3 +142,11 @@ def get_next_trip_id(db: Session = Depends(get_db)):
if not trip: if not trip:
raise HTTPException(status_code=404, detail="No upcoming trip found") raise HTTPException(status_code=404, detail="No upcoming trip found")
return trip.id return trip.id
@router.delete("/{trip_id}", status_code=204)
def delete_trip(trip_id: UUID, db: Session = Depends(get_db)):
trip = db.get(models.Trip, trip_id)
if not trip:
raise HTTPException(status_code=404, detail="Trip not found")
db.delete(trip)
db.commit()

View file

@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route, Link, Navigate, useNavigate } f
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 TripsPage from "./pages/TripsPage";
function NextTripRedirect({ trips }: { trips: any[] }) { function NextTripRedirect({ trips }: { trips: any[] }) {
const [nextTripId, setNextTripId] = React.useState<string | null>(null); const [nextTripId, setNextTripId] = React.useState<string | null>(null);
@ -74,27 +75,8 @@ export default function App() {
</div> </div>
<Routes> <Routes>
<Route <Route path="/trips" element={<TripsPage />} />
path="/trips" <Route path="/trips/:id" element={<TripChecklist trips={trips} />} />
element={
<ul>
{trips.map((trip) => (
<li key={trip.id} className="mb-2">
<Link
to={`/trips/${trip.id}`}
className="text-blue-600 underline"
>
{trip.name} <span className="text-gray-500">({trip.start_date} {trip.end_date})</span>
</Link>
</li>
))}
</ul>
}
/>
<Route
path="/trips/:id"
element={<TripChecklist trips={trips} />}
/>
<Route path="/items" element={<ItemsPage />} /> <Route path="/items" element={<ItemsPage />} />
<Route path="/" element={<NextTripRedirect trips={trips} />} /> <Route path="/" element={<NextTripRedirect trips={trips} />} />
</Routes> </Routes>

View file

@ -88,3 +88,24 @@ export async function getNextTripId(): Promise<string> {
if (!res.ok) throw new Error("No upcoming trip found"); if (!res.ok) throw new Error("No upcoming trip found");
return res.json(); return res.json();
} }
export async function deleteTrip(tripId: string): Promise<void> {
const res = await fetch(`${API_BASE}/trips/${tripId}`, { method: "DELETE" });
if (!res.ok) throw new Error("Failed to delete trip");
}
export async function createTrip(data: {
name: string;
start_date: string;
end_date: string;
selected_tag_ids?: string[];
marked_tag_ids?: string[];
}): Promise<any> {
const res = await fetch(`${API_BASE}/trips/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Failed to create trip");
return res.json();
}

View file

@ -1,18 +1,40 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { getSeed, getTrips, getTripItems, toggleTripItem } from "../api"; import { Link } from "react-router-dom";
import { getSeed, getTrips, deleteTrip, createTrip } from "../api";
export function TripsPage() { export default function TripsPage() {
const [trips, setTrips] = useState<any[]>([]); const [trips, setTrips] = useState<any[]>([]);
const [items, setItems] = useState<Record<string, any[]>>({}); const [newTrip, setNewTrip] = useState({ name: "", start_date: "", end_date: "" });
const [creating, setCreating] = useState(false);
async function loadTrips() { async function loadTrips() {
const data = await getTrips(); const data = await getTrips();
setTrips(data); setTrips(data);
} }
async function loadItems(tripId: string) { async function handleDeleteTrip(tripId: string) {
const data = await getTripItems(tripId); if (window.confirm("Diesen Trip wirklich löschen?")) {
setItems(prev => ({ ...prev, [tripId]: data })); await deleteTrip(tripId);
await loadTrips();
}
}
async function handleCreateTrip(e: React.FormEvent) {
e.preventDefault();
setCreating(true);
try {
await createTrip({
name: newTrip.name,
start_date: newTrip.start_date,
end_date: newTrip.end_date,
selected_tag_ids: [],
marked_tag_ids: [],
});
setNewTrip({ name: "", start_date: "", end_date: "" });
await loadTrips();
} finally {
setCreating(false);
}
} }
useEffect(() => { useEffect(() => {
@ -22,15 +44,37 @@ export function TripsPage() {
return ( return (
<div> <div>
<h1 className="text-2xl font-bold mb-4">Packlist</h1> <h1 className="text-2xl font-bold mb-4">Packlist</h1>
<button <form className="mb-4 flex gap-2 flex-wrap items-end" onSubmit={handleCreateTrip}>
className="bg-blue-500 text-white px-4 py-2 rounded mb-4" <input
onClick={async () => { type="text"
await getSeed(); placeholder="Trip-Name"
await loadTrips(); value={newTrip.name}
}} onChange={e => setNewTrip(t => ({ ...t, name: e.target.value }))}
> className="border rounded px-2 py-1"
Seed-Daten erzeugen required
</button> />
<input
type="date"
value={newTrip.start_date}
onChange={e => setNewTrip(t => ({ ...t, start_date: e.target.value }))}
className="border rounded px-2 py-1"
required
/>
<input
type="date"
value={newTrip.end_date}
onChange={e => setNewTrip(t => ({ ...t, end_date: e.target.value }))}
className="border rounded px-2 py-1"
required
/>
<button
type="submit"
className="bg-green-500 text-white px-4 py-2 rounded"
disabled={creating}
>
{creating ? "Anlegen..." : "Neuen Trip anlegen"}
</button>
</form>
{trips.map(trip => ( {trips.map(trip => (
<div key={trip.id} className="border rounded p-2 mb-4"> <div key={trip.id} className="border rounded p-2 mb-4">
@ -41,31 +85,21 @@ export function TripsPage() {
{trip.start_date} {trip.end_date} {trip.start_date} {trip.end_date}
</p> </p>
</div> </div>
<button <div className="flex gap-2">
className="text-sm text-blue-500 underline" <Link
onClick={() => loadItems(trip.id)} to={`/trips/${trip.id}`}
> className="text-sm text-blue-500 underline"
Packliste anzeigen >
</button> Packliste anzeigen
</Link>
<button
className="text-sm text-red-500 underline"
onClick={() => handleDeleteTrip(trip.id)}
>
Löschen
</button>
</div>
</div> </div>
{items[trip.id] && (
<ul className="mt-2">
{items[trip.id].map(item => (
<li key={item.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={item.checked}
onChange={async () => {
await toggleTripItem(item.id);
await loadItems(trip.id);
}}
/>
<span>{item.name_calculated}</span>
</li>
))}
</ul>
)}
</div> </div>
))} ))}
</div> </div>