4dd1b8265f
Ship export intel with contact filters and CRM push, Herman live digest with Telegram briefing, halal recommendation engine, revenue cockpit, and dashboard polling/WebSocket fixes.
234 lines
8.0 KiB
Python
234 lines
8.0 KiB
Python
"""Export Intel Cockpit 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 RedirectResponse, StreamingResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from pathlib import Path
|
|
|
|
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("/")
|
|
|
|
|
|
async def _proxy(method: str, path: str, **kwargs) -> Any:
|
|
url = f"{TOOLS}/export-intel{path}"
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
r = await client.request(method, url, **kwargs)
|
|
if r.status_code >= 400:
|
|
return {"error": r.text, "status": r.status_code}
|
|
if "text/csv" in r.headers.get("content-type", ""):
|
|
return r
|
|
return r.json()
|
|
|
|
|
|
@router.get("/foodlinkk")
|
|
def foodlinkk_home():
|
|
return RedirectResponse(url="/export-intel", status_code=302)
|
|
|
|
|
|
@router.get("/export-intel")
|
|
def export_intel_page(request: Request):
|
|
return templates.TemplateResponse(
|
|
"export_intel.html",
|
|
{"request": request, "page_title": "Wereldexport"},
|
|
)
|
|
|
|
|
|
@router.get("/api/export-intel/stats")
|
|
async def api_stats(country: Optional[str] = None, region: Optional[str] = None):
|
|
params = {k: v for k, v in {"country": country, "region": region}.items() if v}
|
|
return await _proxy("GET", "/stats", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/regions")
|
|
async def api_regions():
|
|
return await _proxy("GET", "/regions")
|
|
|
|
|
|
@router.get("/api/export-intel/territories")
|
|
async def api_territories(region: Optional[str] = None):
|
|
params = {"region": region} if region else {}
|
|
return await _proxy("GET", "/territories", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/entities")
|
|
async def api_entities(
|
|
country: Optional[str] = None,
|
|
region: Optional[str] = None,
|
|
entity_type: Optional[str] = None,
|
|
entity_types: Optional[str] = None,
|
|
q: Optional[str] = None,
|
|
favorite_only: bool = False,
|
|
crm_linked: Optional[bool] = None,
|
|
limit: int = 200,
|
|
offset: int = 0,
|
|
):
|
|
params = {k: v for k, v in {
|
|
"country": country, "region": region, "entity_type": entity_type,
|
|
"entity_types": entity_types, "q": q, "limit": limit, "offset": offset,
|
|
"favorite_only": favorite_only, "crm_linked": crm_linked,
|
|
}.items() if v is not None and v is not False}
|
|
return await _proxy("GET", "/entities", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/entities/{entity_id}")
|
|
async def api_entity(entity_id: int):
|
|
return await _proxy("GET", f"/entities/{entity_id}")
|
|
|
|
|
|
@router.get("/api/export-intel/contacts")
|
|
async def api_contacts(
|
|
country: Optional[str] = None,
|
|
region: Optional[str] = None,
|
|
entity_type: Optional[str] = None,
|
|
has_email: Optional[bool] = None,
|
|
crm_linked: Optional[bool] = None,
|
|
q: Optional[str] = None,
|
|
limit: int = 200,
|
|
offset: int = 0,
|
|
):
|
|
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
if country:
|
|
params["country"] = country
|
|
if region:
|
|
params["region"] = region
|
|
if entity_type:
|
|
params["entity_type"] = entity_type
|
|
if has_email is not None:
|
|
params["has_email"] = has_email
|
|
if crm_linked is not None:
|
|
params["crm_linked"] = crm_linked
|
|
if q:
|
|
params["q"] = q
|
|
return await _proxy("GET", "/contacts", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/contacts/export.csv")
|
|
async def api_contacts_export(
|
|
country: Optional[str] = None,
|
|
region: Optional[str] = None,
|
|
entity_type: Optional[str] = None,
|
|
has_email: Optional[bool] = None,
|
|
crm_linked: Optional[bool] = None,
|
|
q: Optional[str] = None,
|
|
):
|
|
params = {
|
|
k: v
|
|
for k, v in {
|
|
"country": country,
|
|
"region": region,
|
|
"entity_type": entity_type,
|
|
"has_email": has_email,
|
|
"crm_linked": crm_linked,
|
|
"q": q,
|
|
}.items()
|
|
if v is not None and v != ""
|
|
}
|
|
url = f"{TOOLS}/export-intel/contacts/export.csv"
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
r = await client.get(url, params=params)
|
|
return StreamingResponse(
|
|
iter([r.content]),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=export-intel-contacts.csv"},
|
|
)
|
|
|
|
|
|
@router.get("/api/export-intel/caterers/brands")
|
|
async def api_caterer_brands():
|
|
return await _proxy("GET", "/caterers/brands")
|
|
|
|
|
|
@router.get("/api/export-intel/caterers/presence")
|
|
async def api_caterer_presence(country: Optional[str] = None):
|
|
params = {"country": country} if country else {}
|
|
return await _proxy("GET", "/caterers/presence", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/gov-sources")
|
|
async def api_gov_sources(country: Optional[str] = None):
|
|
params = {"country": country} if country else {}
|
|
return await _proxy("GET", "/gov-sources", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/map/bundle")
|
|
async def api_map_bundle(
|
|
country: Optional[str] = None,
|
|
region: Optional[str] = None,
|
|
entity_type: Optional[str] = None,
|
|
entity_types: Optional[str] = None,
|
|
q: Optional[str] = None,
|
|
halal_min: Optional[float] = None,
|
|
favorite_only: bool = False,
|
|
crm_linked: Optional[bool] = None,
|
|
):
|
|
params = {k: v for k, v in {
|
|
"country": country, "region": region, "entity_type": entity_type,
|
|
"entity_types": entity_types, "q": q, "halal_min": halal_min,
|
|
"favorite_only": favorite_only, "crm_linked": crm_linked,
|
|
}.items() if v is not None and v is not False}
|
|
return await _proxy("GET", "/map/bundle", params=params)
|
|
|
|
|
|
@router.post("/api/export-intel/entities/favorites")
|
|
async def api_set_favorites(request: Request):
|
|
body = await request.json()
|
|
return await _proxy("POST", "/entities/favorites", json=body)
|
|
|
|
|
|
@router.get("/api/export-intel/entities/favorites")
|
|
async def api_list_favorites(country: Optional[str] = None, region: Optional[str] = None, limit: int = 200):
|
|
params = {k: v for k, v in {"country": country, "region": region, "limit": limit}.items() if v is not None}
|
|
return await _proxy("GET", "/entities/favorites", params=params)
|
|
|
|
|
|
@router.post("/api/export-intel/crm/push")
|
|
async def api_crm_push(request: Request):
|
|
body = await request.json()
|
|
return await _proxy("POST", "/crm/push", json=body)
|
|
|
|
|
|
@router.get("/api/export-intel/crm/pipeline")
|
|
async def api_crm_pipeline(country: Optional[str] = None, region: Optional[str] = None, limit: int = 100):
|
|
params = {k: v for k, v in {"country": country, "region": region, "limit": limit}.items() if v is not None}
|
|
return await _proxy("GET", "/crm/pipeline", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/halal/markets")
|
|
async def api_halal_markets(
|
|
region: Optional[str] = None,
|
|
country: Optional[str] = None,
|
|
limit: int = 30,
|
|
):
|
|
params = {k: v for k, v in {"region": region, "country": country, "limit": limit}.items() if v is not None}
|
|
return await _proxy("GET", "/halal/markets", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/tenders")
|
|
async def api_tenders(country: Optional[str] = None, status: Optional[str] = "open", limit: int = 100):
|
|
params = {k: v for k, v in {"country": country, "status": status, "limit": limit}.items() if v is not None}
|
|
return await _proxy("GET", "/tenders", params=params)
|
|
|
|
|
|
@router.post("/api/export-intel/sync/{kind}")
|
|
async def api_sync(kind: str, request: Request):
|
|
body = {}
|
|
if request.headers.get("content-type", "").startswith("application/json"):
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
body = {}
|
|
if kind not in ("contacts", "caterers", "distributors", "customers", "tenders", "all", "world", "region"):
|
|
return {"error": "unknown sync kind"}
|
|
timeout = 7200.0 if kind in ("world", "region", "all") else 600.0
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
url = f"{TOOLS}/export-intel/sync/{kind}"
|
|
r = await client.post(url, json=body)
|
|
return r.json()
|