"""Scenario API endpoints: validate, import-excel, generate-excel, export-json, template.""" import os from io import BytesIO from datetime import date, time as dt_time import pandas as pd from fastapi import APIRouter, UploadFile, File, Form, HTTPException from fastapi.responses import StreamingResponse, FileResponse from app.config import VERSION, MAX_FILE_SIZE_MB from app.models.event import ScenarioDocument, Block, ProgramType, EventInfo from app.models.responses import HealthResponse, ValidationResponse, ImportExcelResponse from app.core.validator import validate_inputs, ValidationError, TemplateError, ScenarsError from app.core.excel_reader import read_excel, parse_inline_schedule, parse_inline_types from app.core.timetable import create_timetable router = APIRouter() @router.get("/health", response_model=HealthResponse) async def health(): return HealthResponse(version=VERSION) @router.post("/validate", response_model=ValidationResponse) async def validate_scenario(doc: ScenarioDocument): """Validate a ScenarioDocument.""" errors = [] if not doc.blocks: errors.append("No blocks defined") if not doc.program_types: errors.append("No program types defined") type_codes = {pt.code for pt in doc.program_types} for i, block in enumerate(doc.blocks): if block.typ not in type_codes: errors.append(f"Block {i+1}: unknown type '{block.typ}'") if block.zacatek >= block.konec: errors.append(f"Block {i+1}: start time must be before end time") return ValidationResponse(valid=len(errors) == 0, errors=errors) @router.post("/import-excel") async def import_excel( file: UploadFile = File(...), title: str = Form("Imported Event"), detail: str = Form("Imported from Excel"), ): """Upload Excel file and return ScenarioDocument JSON.""" content = await file.read() if len(content) > MAX_FILE_SIZE_MB * 1024 * 1024: raise HTTPException(status_code=413, detail=f"File exceeds {MAX_FILE_SIZE_MB}MB limit") try: valid_data, error_rows = read_excel(content) except TemplateError as e: raise HTTPException(status_code=422, detail=str(e)) if valid_data.empty: raise HTTPException(status_code=422, detail="No valid rows found in Excel file") # Extract unique types types_in_data = valid_data["Typ"].dropna().unique().tolist() program_types = [ ProgramType(code=t, description=str(t), color="#0070C0") for t in types_in_data ] # Convert rows to blocks blocks = [] for _, row in valid_data.iterrows(): blocks.append(Block( datum=row["Datum"], zacatek=row["Zacatek"], konec=row["Konec"], program=str(row["Program"]), typ=str(row["Typ"]), garant=str(row["Garant"]) if pd.notna(row.get("Garant")) else None, poznamka=str(row["Poznamka"]) if pd.notna(row.get("Poznamka")) else None, )) doc = ScenarioDocument( event=EventInfo(title=title, detail=detail), program_types=program_types, blocks=blocks, ) warnings = [f"Row {e['index']}: {e.get('error', e.get('Error', 'unknown'))}" for e in error_rows] return ImportExcelResponse( success=True, document=doc.model_dump(mode="json"), warnings=warnings, ) @router.post("/generate-excel") async def generate_excel(doc: ScenarioDocument): """Generate Excel timetable from ScenarioDocument.""" try: validate_inputs(doc.event.title, doc.event.detail, 0) except ValidationError as e: raise HTTPException(status_code=422, detail=str(e)) # Convert to DataFrame rows = [] for block in doc.blocks: rows.append({ 'Datum': block.datum, 'Zacatek': block.zacatek, 'Konec': block.konec, 'Program': block.program, 'Typ': block.typ, 'Garant': block.garant, 'Poznamka': block.poznamka, }) df = pd.DataFrame(rows) if df.empty: raise HTTPException(status_code=422, detail="No blocks provided") # Build program descriptions and colors program_descriptions = {pt.code: pt.description for pt in doc.program_types} program_colors = {pt.code: 'FF' + pt.color.lstrip('#') for pt in doc.program_types} try: wb = create_timetable(df, doc.event.title, doc.event.detail, program_descriptions, program_colors) except ScenarsError as e: raise HTTPException(status_code=422, detail=str(e)) output = BytesIO() wb.save(output) output.seek(0) return StreamingResponse( output, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": "attachment; filename=scenar_timetable.xlsx"} ) @router.post("/export-json") async def export_json(doc: ScenarioDocument): """Export ScenarioDocument as JSON.""" return doc.model_dump(mode="json") @router.get("/template") async def download_template(): """Download the Excel template file.""" template_path = os.path.join(os.path.dirname(__file__), "..", "..", "templates", "scenar_template.xlsx") template_path = os.path.abspath(template_path) if not os.path.exists(template_path): raise HTTPException(status_code=404, detail="Template file not found") return FileResponse( template_path, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename="scenar_template.xlsx" )