mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 16:24:50 +00:00
backend: client profile API (/api/clients, /api/lead)
NEW HANDLERS: - _handle_clients: groups manager's Leads by client (name or tg_id), returns list with leads_count, last_lead_at, client_phone (extracted from checklist), and full leads array per client. Sorted by recency desc. - _handle_lead: fetches single lead with parsed checklist + ai_response JSON. Validates ownership (manager_tg_id matches caller). NEW ENDPOINTS: - POST /api/clients — list of manager's clients with summary - POST /api/lead — single lead detail with ai_response for re-render Both accept initData auth, only manager role can call. Apps Script compat: ?path=clients and ?path=lead also work.
This commit is contained in:
parent
efa2046a97
commit
9a2dcbc3fe
@ -87,6 +87,8 @@ async def _dispatch_post(request: Request):
|
||||
"me": _handle_me,
|
||||
"measurement": _handle_measurement,
|
||||
"podbor": _handle_podbor,
|
||||
"clients": _handle_clients,
|
||||
"lead": _handle_lead,
|
||||
"ping": lambda b: {"pong": True, "time": _now_iso()},
|
||||
"seed_admin": lambda b: _handle_seed_admin(),
|
||||
"test_ai": lambda b: _handle_test_ai(),
|
||||
@ -135,6 +137,18 @@ async def api_podbor(request: Request):
|
||||
return await asyncio.to_thread(_handle_podbor, body)
|
||||
|
||||
|
||||
@app.post("/api/clients")
|
||||
async def api_clients(request: Request):
|
||||
body = await _safe_json(request)
|
||||
return _handle_clients(body)
|
||||
|
||||
|
||||
@app.post("/api/lead")
|
||||
async def api_lead(request: Request):
|
||||
body = await _safe_json(request)
|
||||
return _handle_lead(body)
|
||||
|
||||
|
||||
@app.get("/api/test_ai")
|
||||
async def api_test_ai():
|
||||
return _handle_test_ai()
|
||||
@ -526,6 +540,127 @@ def _enrich_ai_marketplaces(ai_result: dict[str, Any]) -> None:
|
||||
cat_data["models"] = parsers.enrich_models(models, delay_sec=0.4)
|
||||
|
||||
|
||||
def _handle_clients(body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Возвращает список клиентов менеджера со сводкой по подборам."""
|
||||
cfg = get_config()
|
||||
auth = verify_init_data(body.get("initData") or "", cfg.bot_token)
|
||||
if not auth or not auth.get("user"):
|
||||
return {"error": "invalid_init_data"}
|
||||
tg_id = auth["user"]["id"]
|
||||
user = sheets.find_user(tg_id)
|
||||
if not user or user.get("role") != "manager":
|
||||
return {"error": "only_manager"}
|
||||
|
||||
try:
|
||||
ws = sheets.sheet("Leads")
|
||||
rows = ws.get_all_values()
|
||||
except Exception as e:
|
||||
log.warning("Failed to read Leads: %s", e)
|
||||
return {"ok": True, "clients": []}
|
||||
|
||||
if not rows or len(rows) < 2:
|
||||
return {"ok": True, "clients": []}
|
||||
|
||||
headers = rows[0]
|
||||
by_client: dict[str, dict[str, Any]] = {}
|
||||
|
||||
for r in rows[1:]:
|
||||
row = dict(zip(headers, r + [""] * (len(headers) - len(r))))
|
||||
if str(row.get("manager_tg_id", "")) != str(tg_id):
|
||||
continue
|
||||
client_name = (row.get("client_name") or "").strip()
|
||||
client_tg_id = (row.get("client_tg_id") or "").strip()
|
||||
# Ключ для группировки: tg_id если есть, иначе имя
|
||||
key = client_tg_id or client_name.lower()
|
||||
if not key:
|
||||
continue
|
||||
if key not in by_client:
|
||||
by_client[key] = {
|
||||
"client_name": client_name,
|
||||
"client_tg_id": client_tg_id or None,
|
||||
"client_phone": "",
|
||||
"leads_count": 0,
|
||||
"last_lead_at": "",
|
||||
"last_lead_id": "",
|
||||
"leads": [],
|
||||
}
|
||||
c = by_client[key]
|
||||
c["leads_count"] += 1
|
||||
lead_id = row.get("id", "")
|
||||
created_at = row.get("created_at", "")
|
||||
status = row.get("status", "")
|
||||
c["leads"].append({
|
||||
"id": lead_id,
|
||||
"created_at": created_at,
|
||||
"status": status,
|
||||
})
|
||||
# Обновляем «последний»
|
||||
if created_at > c["last_lead_at"]:
|
||||
c["last_lead_at"] = created_at
|
||||
c["last_lead_id"] = lead_id
|
||||
# Достаём телефон из checklist JSON
|
||||
checklist_str = row.get("checklist", "")
|
||||
if checklist_str:
|
||||
try:
|
||||
cl = json.loads(checklist_str)
|
||||
if cl.get("client_phone"):
|
||||
c["client_phone"] = cl["client_phone"]
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Сортируем по дате последнего подбора (новые сверху)
|
||||
clients = sorted(by_client.values(), key=lambda x: x["last_lead_at"], reverse=True)
|
||||
# Внутри каждого клиента — leads тоже по дате desc
|
||||
for c in clients:
|
||||
c["leads"].sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
||||
|
||||
return {"ok": True, "count": len(clients), "clients": clients}
|
||||
|
||||
|
||||
def _handle_lead(body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Возвращает детали одного лида (включая AI-ответ и checklist)."""
|
||||
cfg = get_config()
|
||||
auth = verify_init_data(body.get("initData") or "", cfg.bot_token)
|
||||
if not auth or not auth.get("user"):
|
||||
return {"error": "invalid_init_data"}
|
||||
tg_id = auth["user"]["id"]
|
||||
|
||||
lead_id = body.get("lead_id") or body.get("id")
|
||||
if not lead_id:
|
||||
return {"error": "missing_lead_id"}
|
||||
|
||||
row = sheets.find_row("Leads", "id", lead_id)
|
||||
if not row:
|
||||
return {"error": "lead_not_found"}
|
||||
|
||||
# Проверяем что это лид этого менеджера
|
||||
if str(row.get("manager_tg_id", "")) != str(tg_id):
|
||||
return {"error": "forbidden"}
|
||||
|
||||
# Парсим JSONы
|
||||
try:
|
||||
checklist = json.loads(row.get("checklist") or "{}")
|
||||
except (ValueError, TypeError):
|
||||
checklist = {}
|
||||
ai_response = row.get("ai_response") or ""
|
||||
try:
|
||||
ai_json = json.loads(ai_response) if ai_response else None
|
||||
except (ValueError, TypeError):
|
||||
ai_json = None
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"id": lead_id,
|
||||
"created_at": row.get("created_at"),
|
||||
"client_name": row.get("client_name"),
|
||||
"client_tg_id": row.get("client_tg_id"),
|
||||
"checklist": checklist,
|
||||
"ai": ai_json,
|
||||
"ai_text": ai_response if not ai_json else None,
|
||||
"status": row.get("status", ""),
|
||||
}
|
||||
|
||||
|
||||
def _handle_test_ai() -> dict[str, Any]:
|
||||
cfg = get_config()
|
||||
res = ai.call_ai("Скажи одной фразой: что за фабрика ЗОВ?",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user