from fastapi import APIRouter, HTTPException, Depends from typing import List import uuid from datetime import date from ..database import get_db_cursor from ..models import ( MeetingCreate, MeetingUpdate, MeetingResponse, MeetingListResponse, ConclusionResponse, ActionItemResponse, ActionItemUpdate, TokenPayload, ) from .auth import get_current_user, is_admin router = APIRouter() def generate_system_code(prefix: str, meeting_date: date, sequence: int) -> str: """Generate system code like C-20251210-01 or A-20251210-01.""" date_str = meeting_date.strftime("%Y%m%d") return f"{prefix}-{date_str}-{sequence:02d}" def get_next_sequence(cursor, prefix: str, date_str: str) -> int: """Get next sequence number for a given prefix and date.""" pattern = f"{prefix}-{date_str}-%" cursor.execute( """ SELECT system_code FROM meeting_conclusions WHERE system_code LIKE %s UNION SELECT system_code FROM meeting_action_items WHERE system_code LIKE %s ORDER BY system_code DESC LIMIT 1 """, (pattern, pattern), ) result = cursor.fetchone() if result: last_code = result["system_code"] last_seq = int(last_code.split("-")[-1]) return last_seq + 1 return 1 @router.post("/meetings", response_model=MeetingResponse) async def create_meeting( meeting: MeetingCreate, current_user: TokenPayload = Depends(get_current_user) ): """Create a new meeting with optional conclusions and action items.""" meeting_uuid = str(uuid.uuid4()) recorder = meeting.recorder or current_user.email meeting_date = meeting.meeting_time.date() date_str = meeting_date.strftime("%Y%m%d") with get_db_cursor(commit=True) as cursor: # Insert meeting record cursor.execute( """ INSERT INTO meeting_records (uuid, subject, meeting_time, location, chairperson, recorder, attendees, transcript_blob, created_by) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( meeting_uuid, meeting.subject, meeting.meeting_time, meeting.location, meeting.chairperson, recorder, meeting.attendees, meeting.transcript_blob, current_user.email, ), ) meeting_id = cursor.lastrowid # Insert conclusions conclusions = [] seq = get_next_sequence(cursor, "C", date_str) for conclusion in meeting.conclusions or []: system_code = generate_system_code("C", meeting_date, seq) cursor.execute( """ INSERT INTO meeting_conclusions (meeting_id, content, system_code) VALUES (%s, %s, %s) """, (meeting_id, conclusion.content, system_code), ) conclusions.append( ConclusionResponse( conclusion_id=cursor.lastrowid, meeting_id=meeting_id, content=conclusion.content, system_code=system_code, ) ) seq += 1 # Insert action items actions = [] seq = get_next_sequence(cursor, "A", date_str) for action in meeting.actions or []: system_code = generate_system_code("A", meeting_date, seq) cursor.execute( """ INSERT INTO meeting_action_items (meeting_id, content, owner, due_date, system_code) VALUES (%s, %s, %s, %s, %s) """, (meeting_id, action.content, action.owner, action.due_date, system_code), ) actions.append( ActionItemResponse( action_id=cursor.lastrowid, meeting_id=meeting_id, content=action.content, owner=action.owner, due_date=action.due_date, status="Open", system_code=system_code, ) ) seq += 1 # Fetch created meeting cursor.execute( "SELECT * FROM meeting_records WHERE meeting_id = %s", (meeting_id,) ) record = cursor.fetchone() return MeetingResponse( meeting_id=record["meeting_id"], uuid=record["uuid"], subject=record["subject"], meeting_time=record["meeting_time"], location=record["location"], chairperson=record["chairperson"], recorder=record["recorder"], attendees=record["attendees"], transcript_blob=record["transcript_blob"], created_by=record["created_by"], created_at=record["created_at"], conclusions=conclusions, actions=actions, ) @router.get("/meetings", response_model=List[MeetingListResponse]) async def list_meetings(current_user: TokenPayload = Depends(get_current_user)): """List meetings. Admin sees all, users see only their own.""" with get_db_cursor() as cursor: if is_admin(current_user): cursor.execute( """ SELECT meeting_id, uuid, subject, meeting_time, chairperson, created_at FROM meeting_records ORDER BY meeting_time DESC """ ) else: cursor.execute( """ SELECT meeting_id, uuid, subject, meeting_time, chairperson, created_at FROM meeting_records WHERE created_by = %s OR recorder = %s OR attendees LIKE %s ORDER BY meeting_time DESC """, ( current_user.email, current_user.email, f"%{current_user.email}%", ), ) records = cursor.fetchall() return [MeetingListResponse(**record) for record in records] @router.get("/meetings/{meeting_id}", response_model=MeetingResponse) async def get_meeting( meeting_id: int, current_user: TokenPayload = Depends(get_current_user) ): """Get meeting details with conclusions and action items.""" with get_db_cursor() as cursor: cursor.execute( "SELECT * FROM meeting_records WHERE meeting_id = %s", (meeting_id,) ) record = cursor.fetchone() if not record: raise HTTPException(status_code=404, detail="Meeting not found") # Check access if not is_admin(current_user): if ( record["created_by"] != current_user.email and record["recorder"] != current_user.email and current_user.email not in (record["attendees"] or "") ): raise HTTPException(status_code=403, detail="Access denied") # Get conclusions cursor.execute( "SELECT * FROM meeting_conclusions WHERE meeting_id = %s", (meeting_id,) ) conclusions = [ConclusionResponse(**c) for c in cursor.fetchall()] # Get action items cursor.execute( "SELECT * FROM meeting_action_items WHERE meeting_id = %s", (meeting_id,) ) actions = [ActionItemResponse(**a) for a in cursor.fetchall()] return MeetingResponse( meeting_id=record["meeting_id"], uuid=record["uuid"], subject=record["subject"], meeting_time=record["meeting_time"], location=record["location"], chairperson=record["chairperson"], recorder=record["recorder"], attendees=record["attendees"], transcript_blob=record["transcript_blob"], created_by=record["created_by"], created_at=record["created_at"], conclusions=conclusions, actions=actions, ) @router.put("/meetings/{meeting_id}", response_model=MeetingResponse) async def update_meeting( meeting_id: int, meeting: MeetingUpdate, current_user: TokenPayload = Depends(get_current_user), ): """Update meeting details.""" with get_db_cursor(commit=True) as cursor: cursor.execute( "SELECT * FROM meeting_records WHERE meeting_id = %s", (meeting_id,) ) record = cursor.fetchone() if not record: raise HTTPException(status_code=404, detail="Meeting not found") # Check access if not is_admin(current_user) and record["created_by"] != current_user.email: raise HTTPException(status_code=403, detail="Access denied") # Build update query dynamically updates = [] values = [] for field in ["subject", "meeting_time", "location", "chairperson", "recorder", "attendees", "transcript_blob"]: value = getattr(meeting, field) if value is not None: updates.append(f"{field} = %s") values.append(value) if updates: values.append(meeting_id) cursor.execute( f"UPDATE meeting_records SET {', '.join(updates)} WHERE meeting_id = %s", values, ) # Update conclusions if provided if meeting.conclusions is not None: cursor.execute( "DELETE FROM meeting_conclusions WHERE meeting_id = %s", (meeting_id,) ) meeting_date = (meeting.meeting_time or record["meeting_time"]).date() if hasattr(meeting.meeting_time or record["meeting_time"], 'date') else date.today() date_str = meeting_date.strftime("%Y%m%d") seq = get_next_sequence(cursor, "C", date_str) for conclusion in meeting.conclusions: system_code = generate_system_code("C", meeting_date, seq) cursor.execute( """ INSERT INTO meeting_conclusions (meeting_id, content, system_code) VALUES (%s, %s, %s) """, (meeting_id, conclusion.content, system_code), ) seq += 1 # Update action items if provided if meeting.actions is not None: cursor.execute( "DELETE FROM meeting_action_items WHERE meeting_id = %s", (meeting_id,) ) meeting_date = (meeting.meeting_time or record["meeting_time"]).date() if hasattr(meeting.meeting_time or record["meeting_time"], 'date') else date.today() date_str = meeting_date.strftime("%Y%m%d") seq = get_next_sequence(cursor, "A", date_str) for action in meeting.actions: system_code = generate_system_code("A", meeting_date, seq) cursor.execute( """ INSERT INTO meeting_action_items (meeting_id, content, owner, due_date, system_code) VALUES (%s, %s, %s, %s, %s) """, (meeting_id, action.content, action.owner, action.due_date, system_code), ) seq += 1 # Return updated meeting return await get_meeting(meeting_id, current_user) @router.delete("/meetings/{meeting_id}") async def delete_meeting( meeting_id: int, current_user: TokenPayload = Depends(get_current_user) ): """Delete meeting and all related data (cascade).""" with get_db_cursor(commit=True) as cursor: cursor.execute( "SELECT * FROM meeting_records WHERE meeting_id = %s", (meeting_id,) ) record = cursor.fetchone() if not record: raise HTTPException(status_code=404, detail="Meeting not found") # Check access - admin or creator can delete if not is_admin(current_user) and record["created_by"] != current_user.email: raise HTTPException(status_code=403, detail="Access denied") # Delete (cascade will handle conclusions and action items) cursor.execute("DELETE FROM meeting_records WHERE meeting_id = %s", (meeting_id,)) return {"message": "Meeting deleted successfully"} @router.put("/meetings/{meeting_id}/actions/{action_id}") async def update_action_item( meeting_id: int, action_id: int, action: ActionItemUpdate, current_user: TokenPayload = Depends(get_current_user), ): """Update a specific action item's status, owner, or due date.""" with get_db_cursor(commit=True) as cursor: cursor.execute( "SELECT * FROM meeting_action_items WHERE action_id = %s AND meeting_id = %s", (action_id, meeting_id), ) record = cursor.fetchone() if not record: raise HTTPException(status_code=404, detail="Action item not found") updates = [] values = [] for field in ["content", "owner", "due_date", "status"]: value = getattr(action, field) if value is not None: updates.append(f"{field} = %s") values.append(value.value if hasattr(value, "value") else value) if updates: values.append(action_id) cursor.execute( f"UPDATE meeting_action_items SET {', '.join(updates)} WHERE action_id = %s", values, ) cursor.execute( "SELECT * FROM meeting_action_items WHERE action_id = %s", (action_id,) ) updated = cursor.fetchone() return ActionItemResponse(**updated)