from fastapi import APIRouter, HTTPException, Depends from fastapi.responses import StreamingResponse from openpyxl import Workbook, load_workbook from openpyxl.styles import Font, Alignment, Border, Side import io import os from ..database import get_db_cursor from ..models import TokenPayload from .auth import get_current_user, is_admin router = APIRouter() TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "..", "templates") def create_default_workbook(meeting: dict, conclusions: list, actions: list) -> Workbook: """Create Excel workbook with meeting data.""" wb = Workbook() ws = wb.active ws.title = "Meeting Record" # Styles header_font = Font(bold=True, size=14) label_font = Font(bold=True) thin_border = Border( left=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin"), ) # Title ws.merge_cells("A1:F1") ws["A1"] = "Meeting Record" ws["A1"].font = Font(bold=True, size=16) ws["A1"].alignment = Alignment(horizontal="center") # Metadata section row = 3 metadata = [ ("Subject", meeting.get("subject", "")), ("Date/Time", str(meeting.get("meeting_time", ""))), ("Location", meeting.get("location", "")), ("Chairperson", meeting.get("chairperson", "")), ("Recorder", meeting.get("recorder", "")), ("Attendees", meeting.get("attendees", "")), ] for label, value in metadata: ws[f"A{row}"] = label ws[f"A{row}"].font = label_font ws.merge_cells(f"B{row}:F{row}") ws[f"B{row}"] = value row += 1 # Conclusions section row += 1 ws.merge_cells(f"A{row}:F{row}") ws[f"A{row}"] = "Conclusions" ws[f"A{row}"].font = header_font row += 1 ws[f"A{row}"] = "Code" ws[f"B{row}"] = "Content" ws[f"A{row}"].font = label_font ws[f"B{row}"].font = label_font row += 1 for c in conclusions: ws[f"A{row}"] = c.get("system_code", "") ws.merge_cells(f"B{row}:F{row}") ws[f"B{row}"] = c.get("content", "") row += 1 # Action Items section row += 1 ws.merge_cells(f"A{row}:F{row}") ws[f"A{row}"] = "Action Items" ws[f"A{row}"].font = header_font row += 1 headers = ["Code", "Content", "Owner", "Due Date", "Status"] for col, header in enumerate(headers, 1): cell = ws.cell(row=row, column=col, value=header) cell.font = label_font cell.border = thin_border row += 1 for a in actions: ws.cell(row=row, column=1, value=a.get("system_code", "")).border = thin_border ws.cell(row=row, column=2, value=a.get("content", "")).border = thin_border ws.cell(row=row, column=3, value=a.get("owner", "")).border = thin_border ws.cell(row=row, column=4, value=str(a.get("due_date", "") or "")).border = thin_border ws.cell(row=row, column=5, value=a.get("status", "")).border = thin_border row += 1 # Adjust column widths ws.column_dimensions["A"].width = 18 ws.column_dimensions["B"].width = 40 ws.column_dimensions["C"].width = 15 ws.column_dimensions["D"].width = 12 ws.column_dimensions["E"].width = 12 ws.column_dimensions["F"].width = 12 return wb @router.get("/meetings/{meeting_id}/export") async def export_meeting( meeting_id: int, current_user: TokenPayload = Depends(get_current_user) ): """Export meeting to Excel file.""" with get_db_cursor() as cursor: cursor.execute( "SELECT * FROM meeting_records WHERE meeting_id = %s", (meeting_id,) ) meeting = cursor.fetchone() if not meeting: raise HTTPException(status_code=404, detail="Meeting not found") # Check access if not is_admin(current_user): if ( meeting["created_by"] != current_user.email and meeting["recorder"] != current_user.email and current_user.email not in (meeting["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 = cursor.fetchall() # Get action items cursor.execute( "SELECT * FROM meeting_action_items WHERE meeting_id = %s", (meeting_id,) ) actions = cursor.fetchall() # Check for custom template template_path = os.path.join(TEMPLATE_DIR, "template.xlsx") if os.path.exists(template_path): wb = load_workbook(template_path) ws = wb.active # Replace placeholders for row in ws.iter_rows(): for cell in row: if cell.value and isinstance(cell.value, str): cell.value = ( cell.value.replace("{{subject}}", meeting.get("subject", "")) .replace("{{time}}", str(meeting.get("meeting_time", ""))) .replace("{{location}}", meeting.get("location", "")) .replace("{{chair}}", meeting.get("chairperson", "")) .replace("{{recorder}}", meeting.get("recorder", "")) .replace("{{attendees}}", meeting.get("attendees", "")) ) else: # Use default template wb = create_default_workbook(meeting, conclusions, actions) # Save to bytes buffer buffer = io.BytesIO() wb.save(buffer) buffer.seek(0) filename = f"meeting_{meeting.get('uuid', meeting_id)}.xlsx" return StreamingResponse( buffer, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, )