from fastapi import APIRouter, Query, Request, UploadFile, File from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse from fastapi.templating import Jinja2Templates from fastapi import HTTPException from pydantic import BaseModel, validator from pathlib import Path from typing import Optional import io from ..services import mapping_service, sqlite_service import logging logger = logging.getLogger(__name__) router = APIRouter(tags=["mappings"]) templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) class MappingCreate(BaseModel): sku: str codmat: str cantitate_roa: float = 1 procent_pret: float = 100 @validator('sku', 'codmat') def not_empty(cls, v): if not v or not v.strip(): raise ValueError('nu poate fi gol') return v.strip() class MappingUpdate(BaseModel): cantitate_roa: Optional[float] = None procent_pret: Optional[float] = None activ: Optional[int] = None class MappingEdit(BaseModel): new_sku: str new_codmat: str cantitate_roa: float = 1 procent_pret: float = 100 @validator('new_sku', 'new_codmat') def not_empty(cls, v): if not v or not v.strip(): raise ValueError('nu poate fi gol') return v.strip() class MappingLine(BaseModel): codmat: str cantitate_roa: float = 1 procent_pret: float = 100 class MappingBatchCreate(BaseModel): sku: str mappings: list[MappingLine] # HTML page @router.get("/mappings", response_class=HTMLResponse) async def mappings_page(request: Request): return templates.TemplateResponse("mappings.html", {"request": request}) # API endpoints @router.get("/api/mappings") async def list_mappings(search: str = "", page: int = 1, per_page: int = 50, sort_by: str = "sku", sort_dir: str = "asc", show_deleted: bool = False, pct_filter: str = None): result = mapping_service.get_mappings(search=search, page=page, per_page=per_page, sort_by=sort_by, sort_dir=sort_dir, show_deleted=show_deleted, pct_filter=pct_filter) # Merge product names from web_products (R4) skus = list({m["sku"] for m in result.get("mappings", [])}) product_names = await sqlite_service.get_web_products_batch(skus) for m in result.get("mappings", []): m["product_name"] = product_names.get(m["sku"], "") # Ensure counts key is always present if "counts" not in result: result["counts"] = {"total": 0, "complete": 0, "incomplete": 0} return result @router.post("/api/mappings") async def create_mapping(data: MappingCreate): try: result = mapping_service.create_mapping(data.sku, data.codmat, data.cantitate_roa, data.procent_pret) # Mark SKU as resolved in missing_skus tracking await sqlite_service.resolve_missing_sku(data.sku) return {"success": True, **result} except HTTPException as e: can_restore = e.headers.get("X-Can-Restore") == "true" if e.headers else False resp: dict = {"error": e.detail} if can_restore: resp["can_restore"] = True return JSONResponse(status_code=e.status_code, content=resp) except Exception as e: return {"success": False, "error": str(e)} @router.put("/api/mappings/{sku}/{codmat}") def update_mapping(sku: str, codmat: str, data: MappingUpdate): try: updated = mapping_service.update_mapping(sku, codmat, data.cantitate_roa, data.procent_pret, data.activ) return {"success": updated} except Exception as e: return {"success": False, "error": str(e)} @router.put("/api/mappings/{sku}/{codmat}/edit") def edit_mapping(sku: str, codmat: str, data: MappingEdit): try: result = mapping_service.edit_mapping(sku, codmat, data.new_sku, data.new_codmat, data.cantitate_roa, data.procent_pret) return {"success": result} except Exception as e: return {"success": False, "error": str(e)} @router.delete("/api/mappings/{sku}/{codmat}") def delete_mapping(sku: str, codmat: str): try: deleted = mapping_service.delete_mapping(sku, codmat) return {"success": deleted} except Exception as e: return {"success": False, "error": str(e)} @router.post("/api/mappings/{sku}/{codmat}/restore") def restore_mapping(sku: str, codmat: str): try: restored = mapping_service.restore_mapping(sku, codmat) return {"success": restored} except Exception as e: return {"success": False, "error": str(e)} @router.post("/api/mappings/batch") async def create_batch_mapping(data: MappingBatchCreate): """Create multiple (sku, codmat) rows for complex sets (R11).""" if not data.mappings: return {"success": False, "error": "No mappings provided"} # Validate procent_pret sums to 100 for multi-line sets if len(data.mappings) > 1: total_pct = sum(m.procent_pret for m in data.mappings) if abs(total_pct - 100) > 0.01: return {"success": False, "error": f"Procent pret trebuie sa fie 100% (actual: {total_pct}%)"} try: results = [] for m in data.mappings: r = mapping_service.create_mapping(data.sku, m.codmat, m.cantitate_roa, m.procent_pret) results.append(r) # Mark SKU as resolved in missing_skus tracking await sqlite_service.resolve_missing_sku(data.sku) return {"success": True, "created": len(results)} except Exception as e: return {"success": False, "error": str(e)} @router.post("/api/mappings/import-csv") async def import_csv(file: UploadFile = File(...)): content = await file.read() text = content.decode("utf-8-sig") result = mapping_service.import_csv(text) return result @router.get("/api/mappings/export-csv") def export_csv(): csv_content = mapping_service.export_csv() return StreamingResponse( io.BytesIO(csv_content.encode("utf-8-sig")), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=mappings.csv"} ) @router.get("/api/mappings/csv-template") def csv_template(): content = mapping_service.get_csv_template() return StreamingResponse( io.BytesIO(content.encode("utf-8-sig")), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=mappings_template.csv"} )