"""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)))