feat: add delete and add trip functionality and TripsPage component
This commit is contained in:
parent
6e6c23b6ad
commit
6456b320c2
4 changed files with 105 additions and 60 deletions
|
|
@ -142,3 +142,11 @@ def get_next_trip_id(db: Session = Depends(get_db)):
|
|||
if not trip:
|
||||
raise HTTPException(status_code=404, detail="No upcoming trip found")
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route, Link, Navigate, useNavigate } f
|
|||
import { getSeed, getTrips, getNextTripId } from "./api";
|
||||
import ItemsPage from "./pages/ItemsPage";
|
||||
import TripChecklist from "./pages/TripChecklist";
|
||||
import TripsPage from "./pages/TripsPage";
|
||||
|
||||
function NextTripRedirect({ trips }: { trips: any[] }) {
|
||||
const [nextTripId, setNextTripId] = React.useState<string | null>(null);
|
||||
|
|
@ -74,27 +75,8 @@ export default function App() {
|
|||
</div>
|
||||
|
||||
<Routes>
|
||||
<Route
|
||||
path="/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="/trips" element={<TripsPage />} />
|
||||
<Route path="/trips/:id" element={<TripChecklist trips={trips} />} />
|
||||
<Route path="/items" element={<ItemsPage />} />
|
||||
<Route path="/" element={<NextTripRedirect trips={trips} />} />
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -88,3 +88,24 @@ export async function getNextTripId(): Promise<string> {
|
|||
if (!res.ok) throw new Error("No upcoming trip found");
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,40 @@
|
|||
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 [items, setItems] = useState<Record<string, any[]>>({});
|
||||
const [newTrip, setNewTrip] = useState({ name: "", start_date: "", end_date: "" });
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
async function loadTrips() {
|
||||
const data = await getTrips();
|
||||
setTrips(data);
|
||||
}
|
||||
|
||||
async function loadItems(tripId: string) {
|
||||
const data = await getTripItems(tripId);
|
||||
setItems(prev => ({ ...prev, [tripId]: data }));
|
||||
async function handleDeleteTrip(tripId: string) {
|
||||
if (window.confirm("Diesen Trip wirklich löschen?")) {
|
||||
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(() => {
|
||||
|
|
@ -22,15 +44,37 @@ export function TripsPage() {
|
|||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Packlist</h1>
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
|
||||
onClick={async () => {
|
||||
await getSeed();
|
||||
await loadTrips();
|
||||
}}
|
||||
>
|
||||
Seed-Daten erzeugen
|
||||
</button>
|
||||
<form className="mb-4 flex gap-2 flex-wrap items-end" onSubmit={handleCreateTrip}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Trip-Name"
|
||||
value={newTrip.name}
|
||||
onChange={e => setNewTrip(t => ({ ...t, name: e.target.value }))}
|
||||
className="border rounded px-2 py-1"
|
||||
required
|
||||
/>
|
||||
<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 => (
|
||||
<div key={trip.id} className="border rounded p-2 mb-4">
|
||||
|
|
@ -41,31 +85,21 @@ export function TripsPage() {
|
|||
{trip.start_date} – {trip.end_date}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="text-sm text-blue-500 underline"
|
||||
onClick={() => loadItems(trip.id)}
|
||||
>
|
||||
Packliste anzeigen
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
to={`/trips/${trip.id}`}
|
||||
className="text-sm text-blue-500 underline"
|
||||
>
|
||||
Packliste anzeigen
|
||||
</Link>
|
||||
<button
|
||||
className="text-sm text-red-500 underline"
|
||||
onClick={() => handleDeleteTrip(trip.id)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue