add event editing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -180,12 +180,15 @@ function AddEventModal({
|
||||
onClose,
|
||||
onAdd,
|
||||
prefill,
|
||||
editEventId,
|
||||
}: {
|
||||
trip: TripRecord;
|
||||
onClose: () => void;
|
||||
onAdd: (body: object) => Promise<void>;
|
||||
prefill?: Partial<TripEvent>;
|
||||
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"
|
||||
>
|
||||
<h2 className="text-white font-bold text-lg">Add Event</h2>
|
||||
<h2 className="text-white font-bold text-lg">{isEdit ? "Edit Event" : "Add Event"}</h2>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Title</label>
|
||||
@@ -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")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -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({
|
||||
</a>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => onDelete(e.event_id)}
|
||||
className="text-gray-600 hover:text-red-400 text-base leading-none opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
onClick={() => onEdit(e)}
|
||||
className="text-gray-600 hover:text-indigo-400 text-xs opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(e.event_id)}
|
||||
className="text-gray-600 hover:text-red-400 text-base leading-none opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,10 +539,16 @@ function DayTimeline({
|
||||
</a>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<button onClick={() => onDelete(e.event_id)}
|
||||
className="text-gray-600 hover:text-red-400 text-sm opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
×
|
||||
</button>
|
||||
<>
|
||||
<button onClick={() => onEdit(e)}
|
||||
className="text-xs text-gray-600 hover:text-indigo-400 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Edit
|
||||
</button>
|
||||
<button onClick={() => onDelete(e.event_id)}
|
||||
className="text-gray-600 hover:text-red-400 text-sm opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
×
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -842,6 +861,7 @@ export default function TripDetailPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedDay, setSelectedDay] = useState<string>("");
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [editEvent, setEditEvent] = useState<TripEvent | null>(null);
|
||||
const [driveTimes, setDriveTimes] = useState<Record<string, { fromId: string; toId: string; text: string }[]>>({});
|
||||
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 && (
|
||||
<AddEventModal
|
||||
trip={trip}
|
||||
onClose={() => setEditEvent(null)}
|
||||
onAdd={handleUpdateEvent}
|
||||
prefill={editEvent}
|
||||
editEventId={editEvent.event_id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<import("@/lib/types").TripEvent>(`/trips/${tripId}/events`, { method: "POST", body: JSON.stringify(body) }),
|
||||
updateTripEvent: (tripId: string, eventId: string, body: object) =>
|
||||
request<import("@/lib/types").TripEvent>(`/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 }[]) =>
|
||||
|
||||
Reference in New Issue
Block a user