From 4dd334302600cae6a59f8cedd3fd6156f3bc03f3 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 21 Jun 2026 15:35:57 -0400 Subject: [PATCH] add event editing --- drb-c2-core/app/models.py | 12 +++++ drb-c2-core/app/routers/trips.py | 41 +++++++++++++++- drb-frontend/app/trips/[id]/page.tsx | 70 ++++++++++++++++++++++------ drb-frontend/lib/c2api.ts | 2 + 4 files changed, 111 insertions(+), 14 deletions(-) diff --git a/drb-c2-core/app/models.py b/drb-c2-core/app/models.py index 0b9ea89..e82e797 100644 --- a/drb-c2-core/app/models.py +++ b/drb-c2-core/app/models.py @@ -161,6 +161,18 @@ class TripEventCreate(BaseModel): tags: List[str] = [] # tag labels applied to this event +class TripEventUpdate(BaseModel): + title: Optional[str] = None + date: Optional[str] = None + start_time: Optional[str] = None + end_time: Optional[str] = None + location: Optional[str] = None + maps_link: Optional[str] = None + place_id: Optional[str] = None + notes: Optional[str] = None + tags: Optional[List[str]] = None + + class AttendeeAction(BaseModel): discord_user_id: str discord_username: Optional[str] = None diff --git a/drb-c2-core/app/routers/trips.py b/drb-c2-core/app/routers/trips.py index a9d9b5f..3c4a420 100644 --- a/drb-c2-core/app/routers/trips.py +++ b/drb-c2-core/app/routers/trips.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel -from app.models import TripCreate, TripEventCreate, AttendeeAction +from app.models import TripCreate, TripEventCreate, TripEventUpdate, AttendeeAction from app.internal import firestore as fstore from app.config import settings from app.internal.logger import logger @@ -319,6 +319,45 @@ async def create_event(trip_id: str, body: TripEventCreate): return doc +@router.patch("/{trip_id}/events/{event_id}") +async def update_event(trip_id: str, event_id: str, body: TripEventUpdate): + event = await fstore.doc_get("trip_events", event_id) + if not event or event.get("trip_id") != trip_id: + raise HTTPException(404, f"Event '{event_id}' not found in trip '{trip_id}'.") + + trip = await fstore.doc_get("trips", trip_id) + if not trip: + raise HTTPException(404, f"Trip '{trip_id}' not found.") + + updates: dict = {} + if body.title is not None: + updates["title"] = body.title + if body.date is not None: + if not (trip["start_date"] <= body.date <= trip["end_date"]): + raise HTTPException(400, f"Event date {body.date} is outside the trip range.") + updates["date"] = body.date + if body.start_time is not None: + updates["start_time"] = body.start_time or None + if body.end_time is not None: + updates["end_time"] = body.end_time or None + if body.location is not None: + updates["location"] = body.location + updates["location_inherited"] = False + if body.maps_link is not None: + updates["maps_link"] = body.maps_link or None + if body.place_id is not None: + updates["place_id"] = body.place_id or None + if body.notes is not None: + updates["notes"] = body.notes or None + if body.tags is not None: + updates["tags"] = body.tags + + if updates: + await fstore.doc_update("trip_events", event_id, updates) + + return {**event, **updates} + + @router.delete("/{trip_id}/events/{event_id}") async def delete_event( trip_id: str, diff --git a/drb-frontend/app/trips/[id]/page.tsx b/drb-frontend/app/trips/[id]/page.tsx index e72be93..c246361 100644 --- a/drb-frontend/app/trips/[id]/page.tsx +++ b/drb-frontend/app/trips/[id]/page.tsx @@ -180,12 +180,15 @@ function AddEventModal({ onClose, onAdd, prefill, + editEventId, }: { trip: TripRecord; onClose: () => void; onAdd: (body: object) => Promise; prefill?: Partial; + editEventId?: string; }) { + const isEdit = !!editEventId; const availableTags = trip.available_tags ?? []; const [title, setTitle] = useState(prefill?.title ?? ""); const [date, setDate] = useState(prefill?.date ?? trip.start_date); @@ -224,7 +227,7 @@ function AddEventModal({ }); onClose(); } catch { - setError("Failed to add event. Check the date is within the trip range."); + setError(isEdit ? "Failed to save changes." : "Failed to add event. Check the date is within the trip range."); } finally { setSaving(false); } @@ -236,7 +239,7 @@ function AddEventModal({ onSubmit={handleSubmit} className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-lg space-y-4 max-h-[90vh] overflow-y-auto" > -

Add Event

+

{isEdit ? "Edit Event" : "Add Event"}

@@ -341,7 +344,7 @@ function AddEventModal({ type="submit" disabled={saving} className="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white text-sm rounded-lg px-4 py-2" > - {saving ? "Adding…" : "Add Event"} + {saving ? (isEdit ? "Saving…" : "Adding…") : (isEdit ? "Save Changes" : "Add Event")}
@@ -359,12 +362,14 @@ function DayTimeline({ events, isAdmin, onDelete, + onEdit, driveSegments, availableTags, }: { events: TripEvent[]; isAdmin: boolean; onDelete: (id: string) => void; + onEdit: (event: TripEvent) => void; driveSegments: { fromId: string; toId: string; text: string }[]; availableTags: string[]; }) { @@ -472,12 +477,20 @@ function DayTimeline({ )} {isAdmin && ( - + <> + + + )} @@ -526,10 +539,16 @@ function DayTimeline({ )} {isAdmin && ( - + <> + + + )} @@ -842,6 +861,7 @@ export default function TripDetailPage() { const [loading, setLoading] = useState(true); const [selectedDay, setSelectedDay] = useState(""); const [showAdd, setShowAdd] = useState(false); + const [editEvent, setEditEvent] = useState(null); const [driveTimes, setDriveTimes] = useState>({}); const [tagInput, setTagInput] = useState(""); @@ -901,6 +921,18 @@ export default function TripDetailPage() { } catch (e) { console.error(e); } } + async function handleUpdateEvent(body: object) { + if (!trip || !editEvent) return; + const updated = await c2api.updateTripEvent(trip.trip_id, editEvent.event_id, body); + setTrip((prev) => { + if (!prev) return prev; + const events = prev.events.map((e) => e.event_id === updated.event_id ? updated : e) + .sort((a, b) => a.date.localeCompare(b.date) || (a.start_time ?? "").localeCompare(b.start_time ?? "")); + return { ...prev, events }; + }); + setEditEvent(null); + } + async function handleAddEvent(body: object) { if (!trip) return; const event = await c2api.createTripEvent(trip.trip_id, body); @@ -1079,6 +1111,7 @@ export default function TripDetailPage() { events={dayEvents} isAdmin={isAdmin} onDelete={handleDeleteEvent} + onEdit={setEditEvent} driveSegments={driveTimes[selectedDay] ?? []} availableTags={trip.available_tags ?? []} /> @@ -1100,6 +1133,17 @@ export default function TripDetailPage() { prefill={selectedDay ? { date: selectedDay } : undefined} /> )} + + {/* Edit event modal */} + {editEvent && ( + setEditEvent(null)} + onAdd={handleUpdateEvent} + prefill={editEvent} + editEventId={editEvent.event_id} + /> + )} ); } diff --git a/drb-frontend/lib/c2api.ts b/drb-frontend/lib/c2api.ts index 43e3718..e893fe3 100644 --- a/drb-frontend/lib/c2api.ts +++ b/drb-frontend/lib/c2api.ts @@ -146,6 +146,8 @@ export const c2api = { request<{ available_tags: string[] }>(`/trips/${id}/tags`, { method: "PUT", body: JSON.stringify({ available_tags }) }), createTripEvent: (tripId: string, body: object) => request(`/trips/${tripId}/events`, { method: "POST", body: JSON.stringify(body) }), + updateTripEvent: (tripId: string, eventId: string, body: object) => + request(`/trips/${tripId}/events/${eventId}`, { method: "PATCH", body: JSON.stringify(body) }), deleteTripEvent: (tripId: string, eventId: string) => request(`/trips/${tripId}/events/${eventId}`, { method: "DELETE" }), tripChat: (tripId: string, message: string, history: { role: string; content: string }[]) =>