Files
foodlinkk-command-center/cockpit/app/routes/retail.py
T
Aissa 5d60d33db1 Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering,
reclamefolder filters, Proxmox monitoring en documentatie.
2026-06-09 00:41:27 +00:00

334 lines
11 KiB
Python

"""Retail intelligence map page + API proxy."""
from __future__ import annotations
import os
from typing import Any, Optional
import httpx
from fastapi import APIRouter, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from pydantic import BaseModel
router = APIRouter()
BASE = Path(__file__).resolve().parent.parent.parent
templates = Jinja2Templates(directory=str(BASE / "templates"))
TOOLS = os.getenv("TOOLS_API_URL", "http://tools-api:8700").rstrip("/")
class CrmLinkBody(BaseModel):
client_id: int
deal_id: Optional[int] = None
relationship_type: str = "prospect"
partnership_status: Optional[str] = None
notes: Optional[str] = None
class NoteBody(BaseModel):
body: str
title: Optional[str] = None
note_type: str = "general"
class MilestoneBody(BaseModel):
title: str
milestone_type: str = "custom"
client_id: Optional[int] = None
deal_id: Optional[int] = None
target_date: Optional[str] = None
value_eur: Optional[float] = None
notes: Optional[str] = None
class OwnershipBody(BaseModel):
new_owner: str
previous_owner: Optional[str] = None
change_type: str = "acquisition"
effective_date: Optional[str] = None
source: Optional[str] = None
notes: Optional[str] = None
class CalendarBody(BaseModel):
title: str
starts_at: str
description: Optional[str] = None
ends_at: Optional[str] = None
client_id: Optional[int] = None
deal_id: Optional[int] = None
location: Optional[str] = None
class MediaBody(BaseModel):
filename: str
storage_path: str
content_type: str = "image/jpeg"
caption: Optional[str] = None
async def _tools_get(path: str, params: Optional[dict] = None) -> Any:
async with httpx.AsyncClient(timeout=120) as client:
resp = await client.get(f"{TOOLS}{path}", params=params or {})
resp.raise_for_status()
return resp.json()
async def _tools_post(path: str, params: Optional[dict] = None, json_body: Optional[dict] = None) -> Any:
async with httpx.AsyncClient(timeout=300) as client:
resp = await client.post(f"{TOOLS}{path}", params=params or {}, json=json_body)
resp.raise_for_status()
return resp.json()
async def _tools_delete(path: str) -> Any:
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.delete(f"{TOOLS}{path}")
resp.raise_for_status()
return resp.json()
@router.get("/retail")
async def retail_page(request: Request):
stats = await _tools_get("/retail/stats")
filters = await _tools_get("/retail/filters")
schema = await _tools_get("/retail/schema")
trends = await _tools_get("/retail/trends", {"limit": 8})
crm = await _tools_get("/retail/crm/options")
return templates.TemplateResponse(
"retail.html",
{
"request": request,
"stats": stats,
"filters": filters,
"schema": schema,
"trends": trends.get("items", []),
"crm_options": crm,
},
)
@router.get("/api/retail/stats")
async def api_retail_stats(request: Request):
return JSONResponse(await _tools_get("/retail/stats", dict(request.query_params)))
@router.get("/api/retail/filters")
async def api_retail_filters():
return JSONResponse(await _tools_get("/retail/filters"))
@router.get("/api/retail/map")
async def api_retail_map(request: Request):
return JSONResponse(await _tools_get("/retail/map", dict(request.query_params)))
@router.get("/api/retail/list")
async def api_retail_list(request: Request):
return JSONResponse(await _tools_get("/retail/supermarkets", dict(request.query_params)))
@router.get("/api/retail/supermarkets/{store_id}")
async def api_retail_store(store_id: int):
return JSONResponse(await _tools_get(f"/retail/supermarkets/{store_id}"))
@router.get("/api/retail/opportunities")
async def api_retail_opportunities(request: Request):
return JSONResponse(await _tools_get("/retail/opportunities", dict(request.query_params)))
@router.get("/api/retail/trends")
async def api_retail_trends(request: Request):
return JSONResponse(await _tools_get("/retail/trends", dict(request.query_params)))
@router.get("/api/retail/crm/options")
async def api_crm_options():
return JSONResponse(await _tools_get("/retail/crm/options"))
@router.post("/api/retail/supermarkets/{store_id}/link")
async def api_link_store(store_id: int, body: CrmLinkBody):
return JSONResponse(await _tools_post(f"/retail/supermarkets/{store_id}/link", json_body=body.model_dump()))
@router.post("/api/retail/enrich")
async def api_retail_enrich(limit: int = Query(100, ge=1, le=200)):
return JSONResponse(await _tools_post("/retail/enrich", params={"limit": limit}))
@router.post("/api/retail/sync/{action}")
async def api_retail_sync(action: str, limit: int = Query(100, ge=1, le=300)):
paths = {
"halal": "/retail/sync/halal",
"contacts": f"/retail/sync/contacts?limit={limit}",
"trends": "/retail/sync/trends",
"opportunities": "/retail/compute-opportunities",
}
if action not in paths:
return JSONResponse({"error": "unknown action"}, status_code=400)
return JSONResponse(await _tools_post(paths[action]))
@router.get("/api/retail/export")
async def api_retail_export(request: Request):
async with httpx.AsyncClient(timeout=120) as client:
resp = await client.get(f"{TOOLS}/retail/export", params=dict(request.query_params))
resp.raise_for_status()
return StreamingResponse(
iter([resp.text]),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=retail_export.csv"},
)
# --- 360 workspace proxies ---
@router.get("/api/retail/360/{store_id}")
async def api_retail_360(store_id: int):
return JSONResponse(await _tools_get(f"/retail/360/{store_id}"))
@router.post("/api/retail/360/{store_id}/notes")
async def api_retail_note(store_id: int, body: NoteBody):
return JSONResponse(await _tools_post(f"/retail/360/{store_id}/notes", json_body=body.model_dump()))
@router.post("/api/retail/360/{store_id}/milestones")
async def api_retail_milestone(store_id: int, body: MilestoneBody):
return JSONResponse(await _tools_post(f"/retail/360/{store_id}/milestones", json_body=body.model_dump()))
@router.post("/api/retail/360/{store_id}/ownership")
async def api_retail_ownership(store_id: int, body: OwnershipBody):
return JSONResponse(await _tools_post(f"/retail/360/{store_id}/ownership", json_body=body.model_dump()))
@router.post("/api/retail/360/{store_id}/calendar")
async def api_retail_calendar(store_id: int, body: CalendarBody):
return JSONResponse(await _tools_post(f"/retail/360/{store_id}/calendar", json_body=body.model_dump()))
@router.post("/api/retail/360/{store_id}/media")
async def api_retail_media(store_id: int, body: MediaBody):
return JSONResponse(await _tools_post(f"/retail/360/{store_id}/media", json_body=body.model_dump()))
@router.get("/api/retail/cities")
async def api_retail_cities(request: Request):
return JSONResponse(await _tools_get("/retail/cities", dict(request.query_params)))
@router.post("/api/retail/cities/sync")
async def api_retail_cities_sync(limit: int = Query(50, ge=1, le=200)):
return JSONResponse(await _tools_post("/retail/cities/sync", params={"limit": limit}))
@router.get("/api/retail/wholesalers")
async def api_retail_wholesalers(request: Request):
return JSONResponse(await _tools_get("/retail/wholesalers", dict(request.query_params)))
@router.post("/api/retail/wholesalers/import")
async def api_retail_wholesalers_import():
return JSONResponse(await _tools_post("/retail/wholesalers/import"))
@router.get("/api/retail/rss/live")
async def api_retail_rss(request: Request):
return JSONResponse(await _tools_get("/retail/rss/live", dict(request.query_params)))
@router.post("/api/retail/rss/refresh")
async def api_retail_rss_refresh():
return JSONResponse(await _tools_post("/retail/rss/refresh"))
@router.get("/api/retail/rss/bookmarks")
async def api_rss_bookmarks(request: Request):
return JSONResponse(await _tools_get("/retail/rss/bookmarks", dict(request.query_params)))
@router.post("/api/retail/rss/bookmarks")
async def api_rss_bookmark_add(request: Request):
body = await request.json()
return JSONResponse(await _tools_post("/retail/rss/bookmarks", json_body=body))
@router.delete("/api/retail/rss/bookmarks/{rss_item_id}")
async def api_rss_bookmark_delete(rss_item_id: int):
return JSONResponse(await _tools_delete(f"/retail/rss/bookmarks/{rss_item_id}"))
@router.get("/api/retail/wholesalers/meta")
async def api_wholesalers_meta():
return JSONResponse(await _tools_get("/retail/wholesalers/meta"))
@router.get("/api/retail/wholesalers/{wh_id}/contacts")
async def api_wholesaler_contacts(wh_id: int):
return JSONResponse(await _tools_get(f"/retail/wholesalers/{wh_id}/contacts"))
@router.post("/api/retail/wholesalers/{wh_id}/contacts")
async def api_wholesaler_contact_add(wh_id: int, request: Request):
body = await request.json()
return JSONResponse(await _tools_post(f"/retail/wholesalers/{wh_id}/contacts", json_body=body))
@router.get("/api/retail/promo-campaigns")
async def api_promo_campaigns(request: Request):
return JSONResponse(await _tools_get("/retail/promo-campaigns", dict(request.query_params)))
@router.post("/api/retail/promo-campaigns")
async def api_promo_campaign_add(request: Request):
body = await request.json()
return JSONResponse(await _tools_post("/retail/promo-campaigns", json_body=body))
@router.post("/api/retail/reclamefolder/refresh")
async def api_reclamefolder_refresh():
return JSONResponse(await _tools_post("/retail/reclamefolder/refresh", json_body={}))
@router.get("/api/retail/reclamefolder/live")
async def api_reclamefolder_live(request: Request):
return JSONResponse(await _tools_get("/retail/reclamefolder/live", dict(request.query_params)))
@router.get("/api/retail/reclamefolder/chains")
async def api_reclamefolder_chains():
return JSONResponse(await _tools_get("/retail/reclamefolder/chains"))
@router.get("/api/retail/market/supermarkets")
async def api_supermarket_market():
return JSONResponse(await _tools_get("/retail/market/supermarkets"))
@router.get("/api/retail/market/food-trends")
async def api_food_trends():
return JSONResponse(await _tools_get("/retail/market/food-trends"))
@router.get("/api/retail/market/concepts")
async def api_market_concepts(request: Request):
return JSONResponse(await _tools_get("/retail/market/concepts", dict(request.query_params)))
@router.get("/api/retail/live-dashboard")
async def api_retail_live_dashboard():
return JSONResponse(await _tools_get("/retail/live-dashboard"))
@router.get("/api/retail/market/stocks")
async def api_retail_market_stocks():
return JSONResponse(await _tools_get("/retail/market/stocks"))
@router.get("/api/retail/regulations")
async def api_retail_regulations(request: Request):
return JSONResponse(await _tools_get("/retail/regulations", dict(request.query_params)))