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

401 lines
13 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 SupermarketCreateBody(BaseModel):
name: str
chain: str
address: str
postcode: str = "0000AA"
city: str
province: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
manager_name: Optional[str] = None
employee_count: Optional[int] = None
store_type: Optional[str] = None
partnership_status: str = "none"
halal_certified: bool = False
has_halal_section: bool = False
halal_certifier: Optional[str] = None
class SupermarketPatchBody(BaseModel):
name: Optional[str] = None
chain: Optional[str] = None
address: Optional[str] = None
postcode: Optional[str] = None
city: Optional[str] = None
province: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
manager_name: Optional[str] = None
employee_count: Optional[int] = None
store_type: Optional[str] = None
size_m2: Optional[int] = None
partnership_status: Optional[str] = None
halal_certified: Optional[bool] = None
has_halal_section: Optional[bool] = None
halal_certifier: Optional[str] = None
halal_certificate_number: Optional[str] = None
organic_section: Optional[bool] = None
alcohol_section: Optional[bool] = 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()
async def _tools_patch(path: str, json_body: Optional[dict] = None) -> Any:
async with httpx.AsyncClient(timeout=120) as client:
resp = await client.patch(f"{TOOLS}{path}", json=json_body or {})
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/supermarkets")
async def api_create_supermarket(body: SupermarketCreateBody):
return JSONResponse(await _tools_post("/retail/supermarkets", json_body=body.model_dump()))
@router.patch("/api/retail/supermarkets/{store_id}")
async def api_patch_supermarket(store_id: int, body: SupermarketPatchBody):
return JSONResponse(await _tools_patch(
f"/retail/supermarkets/{store_id}",
json_body=body.model_dump(exclude_unset=True),
))
@router.delete("/api/retail/supermarkets/{store_id}/link/{client_id}")
async def api_unlink_store(store_id: int, client_id: int):
return JSONResponse(await _tools_delete(f"/retail/supermarkets/{store_id}/link/{client_id}"))
@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)))