Files
foodlinkk-command-center/cockpit/app/routes/packaging.py
T
2026-06-09 10:41:13 +00:00

249 lines
8.5 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any, Optional
import httpx
from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import Response
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, Field
from app.config import settings
from app.services import packaging_nas, projects
router = APIRouter(tags=["packaging"])
BASE_DIR = Path(__file__).resolve().parent.parent.parent
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
TOOLS = settings.TOOLS_API_URL.rstrip("/")
class PackagingGenerateBody(BaseModel):
type: str = Field(default="folding_box")
width_mm: float = Field(default=120, gt=0)
height_mm: float = Field(default=80, gt=0)
depth_mm: float = Field(default=40, ge=0)
bleed_mm: float = Field(default=3, ge=0)
elements: dict[str, bool] = Field(default_factory=dict)
brand: dict[str, str] = Field(default_factory=dict)
text: dict[str, Any] = Field(default_factory=dict)
barcode_value: str | None = None
qr_value: str | None = None
design_name: str | None = None
project_id: int | None = None
project_name: str | None = None
client_id: int | None = None
created_by: str = "ceo"
class PackagingEmailBody(BaseModel):
to: list[str] = Field(..., min_length=1)
subject: str = Field(..., min_length=1)
body: str = ""
cc: list[str] = Field(default_factory=list)
client_id: int | None = None
class CopyToProjectBody(BaseModel):
target_project_id: int
async def _tools_request(method: str, path: str, **kwargs) -> httpx.Response:
async with httpx.AsyncClient(timeout=120.0) as client:
r = await client.request(method, f"{TOOLS}{path}", **kwargs)
return r
def _tools_error(exc: httpx.HTTPStatusError) -> HTTPException:
detail = exc.response.text[:500] if exc.response else str(exc)
return HTTPException(status_code=exc.response.status_code if exc.response else 502, detail=detail)
async def _ensure_project(body: PackagingGenerateBody) -> PackagingGenerateBody:
if body.project_id or not body.project_name:
return body
proj = projects.create_project(
body.project_name.strip(),
client_id=body.client_id,
description="Packaging design project",
project_type="packaging",
ensure_nas=True,
)
data = body.model_dump()
data["project_id"] = proj.get("id")
return PackagingGenerateBody(**data)
async def _post_nas_export(result: dict[str, Any]) -> dict[str, Any]:
pid = result.get("cockpit_project_id")
packaging_id = result.get("id")
svg = result.get("svg") or ""
spec = result.get("spec") or {}
if not pid or not packaging_id or not svg:
return result
png_bytes = pdf_bytes = None
try:
png_r = await _tools_request("GET", f"/packaging/download/{packaging_id}", params={"format": "png"})
if png_r.status_code < 400:
png_bytes = png_r.content
pdf_r = await _tools_request("GET", f"/packaging/download/{packaging_id}", params={"format": "pdf"})
if pdf_r.status_code < 400:
pdf_bytes = pdf_r.content
except Exception:
pass
try:
nas_info = packaging_nas.export_packaging_files(packaging_id, int(pid), svg, spec, png_bytes, pdf_bytes)
result["nas"] = nas_info
except Exception as exc:
result["nas"] = {"ok": False, "error": str(exc)}
return result
@router.get("/packaging")
async def packaging_page(request: Request, project_id: Optional[int] = None):
return templates.TemplateResponse(
"packaging.html",
{"request": request, "page_title": "Packaging Studio", "initial_project_id": project_id},
)
@router.get("/api/packaging/types")
async def proxy_packaging_types():
try:
r = await _tools_request("GET", "/packaging/types")
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.post("/api/packaging/preview")
async def proxy_packaging_preview(body: PackagingGenerateBody):
try:
r = await _tools_request("POST", "/packaging/preview", json=body.model_dump())
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.post("/api/packaging/generate")
async def proxy_packaging_generate(body: PackagingGenerateBody):
try:
body = await _ensure_project(body)
r = await _tools_request("POST", "/packaging/generate", json=body.model_dump())
r.raise_for_status()
result = r.json()
return await _post_nas_export(result)
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.get("/api/packaging/projects")
async def proxy_packaging_projects(limit: int = 30):
try:
r = await _tools_request("GET", "/packaging/projects", params={"limit": limit})
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.get("/api/packaging/download/{project_id}")
async def proxy_packaging_download(project_id: str, format: str = "svg"):
try:
r = await _tools_request("GET", f"/packaging/download/{project_id}", params={"format": format})
r.raise_for_status()
media = r.headers.get("content-type", "application/octet-stream")
disposition = r.headers.get("content-disposition")
headers = {"content-disposition": disposition} if disposition else {}
return Response(content=r.content, media_type=media, headers=headers)
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.get("/api/packaging/{packaging_id}")
async def proxy_packaging_get(packaging_id: str):
try:
r = await _tools_request("GET", f"/packaging/{packaging_id}")
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.patch("/api/packaging/{packaging_id}")
async def proxy_packaging_update(packaging_id: str, body: PackagingGenerateBody):
try:
r = await _tools_request("PATCH", f"/packaging/{packaging_id}", json={"spec": body.model_dump()})
r.raise_for_status()
result = r.json()
return await _post_nas_export(result)
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.post("/api/packaging/{packaging_id}/duplicate")
async def proxy_packaging_duplicate(packaging_id: str):
try:
r = await _tools_request("POST", f"/packaging/{packaging_id}/duplicate")
r.raise_for_status()
result = r.json()
return await _post_nas_export(result)
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.post("/api/packaging/{packaging_id}/copy-to-project")
async def proxy_packaging_copy(packaging_id: str, body: CopyToProjectBody):
try:
r = await _tools_request(
"POST",
f"/packaging/{packaging_id}/copy-to-project",
json=body.model_dump(),
)
r.raise_for_status()
result = r.json()
return await _post_nas_export(result)
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.delete("/api/packaging/{packaging_id}")
async def proxy_packaging_delete(packaging_id: str):
try:
r = await _tools_request("DELETE", f"/packaging/{packaging_id}")
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc
@router.post("/api/packaging/{packaging_id}/email")
async def proxy_packaging_email(packaging_id: str, body: PackagingEmailBody):
base = settings.COCKPIT_PUBLIC_URL if hasattr(settings, "COCKPIT_PUBLIC_URL") else "http://10.4.7.18:8600"
links = "\n".join(
f"- {fmt.upper()}: {base}/api/packaging/download/{packaging_id}?format={fmt}"
for fmt in ("svg", "png", "pdf")
)
email_body = (body.body or "").strip()
if email_body:
email_body += "\n\n"
email_body += f"Packaging downloads:\n{links}"
try:
r = await _tools_request(
"POST",
"/emails/send",
json={
"to": body.to,
"cc": body.cc,
"subject": body.subject,
"body": email_body,
"client_id": body.client_id,
},
)
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as exc:
raise _tools_error(exc) from exc