from fastapi import APIRouter, Depends, Query, UploadFile, File from fastapi.responses import StreamingResponse from typing import Optional import io from sqlalchemy.ext.asyncio import AsyncSession from auth.dependencies import require_permission from auth.models import TokenPayload from crm.quotation_models import ( NextNumberResponse, QuotationCreate, QuotationInDB, QuotationListResponse, QuotationUpdate, ) from crm import quotations_service as svc from database.postgres import get_pg_session from shared.audit import log_action router = APIRouter(prefix="/api/crm/quotations", tags=["crm-quotations"]) # IMPORTANT: Static paths must come BEFORE /{id} to avoid route collision in FastAPI @router.get("/next-number", response_model=NextNumberResponse) async def get_next_number( _user: TokenPayload = Depends(require_permission("crm", "view")), ): """Returns the next available quotation number (preview only — does not commit).""" next_num = await svc.get_next_number() return NextNumberResponse(next_number=next_num) @router.get("/all", response_model=list[dict]) async def list_all_quotations( _user: TokenPayload = Depends(require_permission("crm", "view")), ): """Returns all quotations across all customers, each including customer_name.""" return await svc.list_all_quotations() @router.get("/customer/{customer_id}", response_model=QuotationListResponse) async def list_quotations_for_customer( customer_id: str, _user: TokenPayload = Depends(require_permission("crm", "view")), ): quotations = await svc.list_quotations(customer_id) return QuotationListResponse(quotations=quotations, total=len(quotations)) @router.get("/{quotation_id}/pdf") async def proxy_quotation_pdf( quotation_id: str, _user: TokenPayload = Depends(require_permission("crm", "view")), ): """Proxy the quotation PDF from Nextcloud to bypass browser cookie restrictions.""" pdf_bytes = await svc.get_quotation_pdf_bytes(quotation_id) return StreamingResponse( io.BytesIO(pdf_bytes), media_type="application/pdf", headers={"Content-Disposition": "inline"}, ) @router.get("/{quotation_id}", response_model=QuotationInDB) async def get_quotation( quotation_id: str, _user: TokenPayload = Depends(require_permission("crm", "view")), ): return await svc.get_quotation(quotation_id) @router.post("", response_model=QuotationInDB, status_code=201) async def create_quotation( body: QuotationCreate, generate_pdf: bool = Query(False), _user: TokenPayload = Depends(require_permission("crm", "edit")), db: AsyncSession = Depends(get_pg_session), ): """ Create a quotation. Pass ?generate_pdf=true to immediately generate and upload the PDF. """ q = await svc.create_quotation(body, generate_pdf=generate_pdf) await log_action(db, _user.sub, _user.name or _user.email, "CREATE", "quotation", str(q.id), q.quotation_number or str(q.id)) return q @router.put("/{quotation_id}", response_model=QuotationInDB) async def update_quotation( quotation_id: str, body: QuotationUpdate, generate_pdf: bool = Query(False), _user: TokenPayload = Depends(require_permission("crm", "edit")), db: AsyncSession = Depends(get_pg_session), ): """ Update a quotation. Pass ?generate_pdf=true to regenerate the PDF. """ old = await svc.get_quotation(quotation_id) q = await svc.update_quotation(quotation_id, body, generate_pdf=generate_pdf) _SKIP = {"updated_at", "id", "items", "pdf_path"} changes = { k: {"old": getattr(old, k, None), "new": getattr(q, k, None)} for k in body.model_fields_set if k not in _SKIP and getattr(old, k, None) != getattr(q, k, None) } await log_action(db, _user.sub, _user.name or _user.email, "UPDATE", "quotation", quotation_id, q.quotation_number or quotation_id, changes=changes or None) return q @router.delete("/{quotation_id}", status_code=204) async def delete_quotation( quotation_id: str, _user: TokenPayload = Depends(require_permission("crm", "edit")), db: AsyncSession = Depends(get_pg_session), ): q = await svc.get_quotation(quotation_id) await svc.delete_quotation(quotation_id) await log_action(db, _user.sub, _user.name or _user.email, "DELETE", "quotation", quotation_id, q.quotation_number if q else quotation_id) @router.post("/{quotation_id}/regenerate-pdf", response_model=QuotationInDB) async def regenerate_pdf( quotation_id: str, _user: TokenPayload = Depends(require_permission("crm", "edit")), ): """Force PDF regeneration and re-upload to Nextcloud.""" return await svc.regenerate_pdf(quotation_id) @router.post("/{quotation_id}/legacy-pdf", response_model=QuotationInDB) async def upload_legacy_pdf( quotation_id: str, file: UploadFile = File(...), _user: TokenPayload = Depends(require_permission("crm", "edit")), ): """Upload a PDF file for a legacy quotation and store its Nextcloud path.""" pdf_bytes = await file.read() filename = file.filename or f"legacy-{quotation_id}.pdf" return await svc.upload_legacy_pdf(quotation_id, pdf_bytes, filename)