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:
|
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()
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue