from pathlib import Path from typing import Any, Dict from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from backend.app import models from backend.app.db import get_db from backend.app.schemas import ( ContractGenerateRequest, ContractGenerateResponse, ProjectRead, ProjectUpdate, QuoteGenerateResponse, RequirementAnalyzeRequest, RequirementAnalyzeResponse, ) from backend.app.services.ai_service import analyze_requirement from backend.app.services.doc_service import ( generate_contract_word, generate_quote_excel, generate_quote_pdf_from_data, ) router = APIRouter(prefix="/projects", tags=["projects"]) def _build_markdown_from_analysis(data: Dict[str, Any]) -> str: """ Convert structured AI analysis JSON into a human-editable Markdown document. """ lines: list[str] = [] lines.append("# 项目方案草稿") lines.append("") total_hours = data.get("total_estimated_hours") total_amount = data.get("total_amount") if total_hours is not None or total_amount is not None: lines.append("## 概要") if total_hours is not None: lines.append(f"- 建议总工时:**{total_hours}**") if total_amount is not None: lines.append(f"- 建议总报价:**{total_amount}**") lines.append("") modules = data.get("modules") or [] if modules: lines.append("## 功能模块与技术实现") for idx, module in enumerate(modules, start=1): name = module.get("name", f"模块 {idx}") desc = module.get("description") or "" tech = module.get("technical_approach") or "" hours = module.get("estimated_hours") unit_price = module.get("unit_price") subtotal = module.get("subtotal") lines.append(f"### {idx}. {name}") if desc: lines.append(desc) lines.append("") if tech: lines.append("**技术实现思路:**") lines.append(tech) lines.append("") if hours is not None or unit_price is not None or subtotal is not None: lines.append("**工时与报价:**") if hours is not None: lines.append(f"- 预估工时:{hours}") if unit_price is not None: lines.append(f"- 单价:{unit_price}") if subtotal is not None: lines.append(f"- 小计:{subtotal}") lines.append("") notes = data.get("notes") if notes: lines.append("## 备注") lines.append(notes) lines.append("") return "\n".join(lines) @router.get("/", response_model=list[ProjectRead]) async def list_projects(db: Session = Depends(get_db)): projects = ( db.query(models.Project) .order_by(models.Project.created_at.desc()) .all() ) return projects @router.get("/{project_id}", response_model=ProjectRead) async def get_project(project_id: int, db: Session = Depends(get_db)): project = db.query(models.Project).get(project_id) if not project: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") return project @router.patch("/{project_id}", response_model=ProjectRead) async def update_project( project_id: int, payload: ProjectUpdate, db: Session = Depends(get_db), ): project = db.query(models.Project).get(project_id) if not project: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") if payload.ai_solution_md is not None: project.ai_solution_md = payload.ai_solution_md if payload.status is not None: project.status = payload.status db.commit() db.refresh(project) return project @router.post("/analyze", response_model=RequirementAnalyzeResponse) async def analyze_project_requirement( payload: RequirementAnalyzeRequest, db: Session = Depends(get_db), ): # Ensure customer exists customer = db.query(models.Customer).get(payload.customer_id) if not customer: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found") analysis = await analyze_requirement(payload.raw_text) ai_solution_md = _build_markdown_from_analysis(analysis) project = models.Project( customer_id=payload.customer_id, raw_requirement=payload.raw_text, ai_solution_md=ai_solution_md, status="draft", ) db.add(project) db.commit() db.refresh(project) return RequirementAnalyzeResponse( project_id=project.id, ai_solution_md=ai_solution_md, ai_solution_json=analysis, ) @router.post("/{project_id}/generate_quote", response_model=QuoteGenerateResponse) async def generate_project_quote( project_id: int, db: Session = Depends(get_db), ): project = db.query(models.Project).get(project_id) if not project: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") # Re-analyze to get fresh structured data (the UI will allow user to edit Markdown separately). analysis = await analyze_requirement(project.raw_requirement) # Paths base_dir = Path("data/quotes") base_dir.mkdir(parents=True, exist_ok=True) excel_path = base_dir / f"quote_project_{project.id}.xlsx" pdf_path = base_dir / f"quote_project_{project.id}.pdf" template_path = Path("templates/quote_template.xlsx") if not template_path.exists(): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Quote template file not found on server.", ) await generate_quote_excel(analysis, str(template_path), str(excel_path)) await generate_quote_pdf_from_data(analysis, str(pdf_path)) total_amount = float(analysis.get("total_amount") or 0.0) quote = models.Quote( project_id=project.id, total_amount=total_amount, file_path=str(pdf_path), ) db.add(quote) db.commit() db.refresh(quote) return QuoteGenerateResponse( quote_id=quote.id, project_id=project.id, total_amount=total_amount, excel_path=str(excel_path), pdf_path=str(pdf_path), ) @router.post( "/{project_id}/generate_contract", response_model=ContractGenerateResponse, ) async def generate_project_contract( project_id: int, payload: ContractGenerateRequest, db: Session = Depends(get_db), ): project = db.query(models.Project).get(project_id) if not project: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") customer = project.customer if not customer: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Project has no associated customer" ) # Use the latest quote for this project to determine total price. latest_quote = ( db.query(models.Quote) .filter(models.Quote.project_id == project.id) .order_by(models.Quote.created_at.desc()) .first() ) if not latest_quote: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No quote found for this project.", ) contracts_dir = Path("data/contracts") contracts_dir.mkdir(parents=True, exist_ok=True) output_path = contracts_dir / f"contract_project_{project.id}.docx" template_path = Path("templates/contract_template.docx") if not template_path.exists(): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Contract template file not found on server.", ) # Build placeholder mapping mapping: Dict[str, str] = { "{{CUSTOMER_NAME}}": customer.name, "{{TOTAL_PRICE}}": str(latest_quote.total_amount), "{{DELIVERY_DATE}}": payload.delivery_date, } # Allow arbitrary additional placeholders for key, value in payload.extra_placeholders.items(): mapping[key] = value await generate_contract_word(mapping, str(template_path), str(output_path)) return ContractGenerateResponse(project_id=project.id, contract_path=str(output_path))