import uuid from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, HTTPException from app.models import TripCreate, TripEventCreate, AttendeeAction from app.internal import firestore as fstore router = APIRouter(prefix="/trips", tags=["trips"]) @router.get("") async def list_trips(): return await fstore.collection_list("trips") @router.post("") async def create_trip(body: TripCreate): if body.end_date < body.start_date: raise HTTPException(400, "end_date must be on or after start_date.") trip_id = str(uuid.uuid4()) now = datetime.now(timezone.utc).isoformat() doc = { "trip_id": trip_id, "name": body.name, "location": body.location, "maps_link": body.maps_link, "start_date": body.start_date, "end_date": body.end_date, "attendees": {}, # {discord_user_id: discord_username} "created_at": now, } await fstore.doc_set("trips", trip_id, doc, merge=False) return doc @router.get("/{trip_id}") async def get_trip(trip_id: str): trip = await fstore.doc_get("trips", trip_id) if not trip: raise HTTPException(404, f"Trip '{trip_id}' not found.") events = await fstore.collection_list("trip_events", trip_id=trip_id) events.sort(key=lambda e: (e["date"], e.get("time") or "")) return {**trip, "events": events} @router.delete("/{trip_id}") async def delete_trip(trip_id: str): trip = await fstore.doc_get("trips", trip_id) if not trip: raise HTTPException(404, f"Trip '{trip_id}' not found.") events = await fstore.collection_list("trip_events", trip_id=trip_id) for e in events: await fstore.doc_delete("trip_events", e["event_id"]) await fstore.doc_delete("trips", trip_id) return {"ok": True} @router.post("/{trip_id}/join") async def join_trip(trip_id: str, body: AttendeeAction): trip = await fstore.doc_get("trips", trip_id) if not trip: raise HTTPException(404, f"Trip '{trip_id}' not found.") attendees = trip.get("attendees", {}) attendees[body.discord_user_id] = body.discord_username or body.discord_user_id await fstore.doc_update("trips", trip_id, {"attendees": attendees}) return {"ok": True, "attendees": attendees} @router.post("/{trip_id}/leave") async def leave_trip(trip_id: str, body: AttendeeAction): trip = await fstore.doc_get("trips", trip_id) if not trip: raise HTTPException(404, f"Trip '{trip_id}' not found.") attendees = trip.get("attendees", {}) attendees.pop(body.discord_user_id, None) await fstore.doc_update("trips", trip_id, {"attendees": attendees}) # cascade: remove from all events in this trip events = await fstore.collection_list("trip_events", trip_id=trip_id) for e in events: event_attendees = e.get("attendees", {}) if body.discord_user_id in event_attendees: event_attendees.pop(body.discord_user_id) await fstore.doc_update("trip_events", e["event_id"], {"attendees": event_attendees}) return {"ok": True} @router.post("/{trip_id}/events") async def create_event(trip_id: str, body: TripEventCreate): trip = await fstore.doc_get("trips", trip_id) if not trip: raise HTTPException(404, f"Trip '{trip_id}' not found.") if not (trip["start_date"] <= body.date <= trip["end_date"]): raise HTTPException( 400, f"Event date {body.date} is outside the trip range " f"{trip['start_date']} – {trip['end_date']}.", ) event_id = str(uuid.uuid4()) now = datetime.now(timezone.utc).isoformat() doc = { "event_id": event_id, "trip_id": trip_id, "title": body.title, "date": body.date, "time": body.time, "location": body.location if body.location is not None else trip["location"], "location_inherited": body.location is None, "maps_link": body.maps_link, "notes": body.notes, "attendees": {}, "created_at": now, } await fstore.doc_set("trip_events", event_id, doc, merge=False) return doc @router.delete("/{trip_id}/events/{event_id}") async def delete_event(trip_id: str, event_id: str): 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}'.") await fstore.doc_delete("trip_events", event_id) return {"ok": True} @router.post("/{trip_id}/events/{event_id}/join") async def join_event(trip_id: str, event_id: str, body: AttendeeAction): trip = await fstore.doc_get("trips", trip_id) if not trip: raise HTTPException(404, f"Trip '{trip_id}' not found.") if body.discord_user_id not in trip.get("attendees", {}): raise HTTPException(403, "You must join the trip before joining an event.") 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}'.") attendees = event.get("attendees", {}) attendees[body.discord_user_id] = body.discord_username or body.discord_user_id await fstore.doc_update("trip_events", event_id, {"attendees": attendees}) return {"ok": True, "attendees": attendees} @router.post("/{trip_id}/events/{event_id}/leave") async def leave_event(trip_id: str, event_id: str, body: AttendeeAction): 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}'.") attendees = event.get("attendees", {}) attendees.pop(body.discord_user_id, None) await fstore.doc_update("trip_events", event_id, {"attendees": attendees}) return {"ok": True}