Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
"""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)))
|
||||
Reference in New Issue
Block a user