Actualizar backend/services.py

This commit is contained in:
2026-10-09 13:45:50 -05:00
parent 1e867bce05
commit 63ee6c7f24

View File

@@ -187,6 +187,158 @@ def plantillas_estados():
conn.close() conn.close()
# ── EXPORTAR BASE: base de leads por asesor (mismo cruce del PBI) ───────────
# Mapa Asesor -> SEDE_ACT (SWITCH del Power BI). Si el asesor no está aquí, se
# usa la sede de la pauta tal cual (fallback). Comparación normalizada (sin
# tildes/mayúsculas) para no fallar por como escriba Chatwoot el nombre.
_SEDE_ACT_MAP = {
"ALMENDRA PERALTA": "Arequipa",
"JUAN CARLOS AGUILAR": "Piura",
"DIEGO LAZARO": "Trujillo",
"VERONICA LA ROSA": "Lima",
"DAYANA BALABARCA": "Lima",
"MILAGROS VARGAS": "Lima",
"CARMEN MONTOYA": "Lima",
"DIANA CHAVEZ": "Lima",
"COPITO RIVERA": "Lima",
}
def _norm_nombre(s):
import unicodedata
s = unicodedata.normalize("NFKD", str(s or "")).encode("ascii", "ignore").decode()
s = s.replace("_", " ") # etiquetas de Chatwoot usan "_" (contacto_iniciado)
return " ".join(s.upper().split())
def exportar_base_asesores():
"""Lista los asesores (agents) de Chatwoot con id y nombre (en vivo)."""
import os, requests
URL = os.getenv("CHATWOOT_URL", "https://gestor.escueladerefrigeracion.edu.pe")
TOKEN = os.getenv("CHATWOOT_TOKEN", "4anazHvZnvKLtup8biu5Zuoh")
ACC = os.getenv("CHATWOOT_ACCOUNT_ID", "1")
h = {"api_access_token": TOKEN, "Content-Type": "application/json"}
r = requests.get(f"{URL}/api/v1/accounts/{ACC}/agents", headers=h, timeout=15)
r.raise_for_status()
ags = [{"id": a.get("id"), "nombre": a.get("name") or a.get("email")}
for a in (r.json() or []) if a.get("id")]
ags.sort(key=lambda x: str(x["nombre"]).upper())
return {"asesores": ags}
def exportar_base_excel(asesor, etiquetas=None):
"""Base de leads de UN asesor (mismo cruce del Power BI: mensajes de Contact x
pautas de Supabase, LIKE frase dentro del rango). Dedup por teléfono (más
antiguo), SEDE_ACT por asesor y última etiqueta. Si 'etiquetas' trae valores,
filtra por última etiqueta (si está vacío = todas). Devuelve bytes del .xlsx."""
from data_manager_v2 import _campanias_supabase
asesor = str(asesor or "").strip()
if not asesor:
raise ValueError("Falta el asesor.")
# Set de etiquetas a filtrar (normalizadas). Vacío = todas.
filtro_etq = set(_norm_nombre(e) for e in (etiquetas or []) if str(e).strip())
# 1) VALUES con las pautas de Supabase (frase, sede, cargo, codigo, fi, ff)
vals, params = [], []
for (frase, cargo, codigo, sede, dia, origen, fi, ff) in _campanias_supabase():
fr = str(frase or "").strip()
if not fr:
continue
vals.append("(%s,%s,%s,%s,%s,%s)")
params += [fr, str(cargo or ""), str(sede or ""),
str(fi or "2000-01-01")[:10], str(ff or "2100-01-01")[:10],
str(codigo or "")]
if not vals:
raise RuntimeError("No hay campañas en Supabase.")
values_sql = ",".join(vals)
sql = f"""
SELECT c.phone_number, m.created_at, map.sede, map.cargo, cv.cached_label_list
FROM messages m
JOIN conversations cv ON m.conversation_id = cv.id
JOIN contacts c ON cv.contact_id = c.id
LEFT JOIN users u ON cv.assignee_id = u.id
JOIN (VALUES {values_sql})
AS map(frase_busqueda, cargo, sede, fecha_inicio, fecha_fin, codigo)
ON m.content LIKE '%%' || map.frase_busqueda || '%%'
AND m.created_at >= CAST(map.fecha_inicio AS TIMESTAMP)
AND m.created_at <= CAST(map.fecha_fin AS TIMESTAMP) + INTERVAL '1 day'
WHERE m.sender_type = 'Contact'
AND u.name = %s
ORDER BY m.created_at ASC
"""
sede_act = _SEDE_ACT_MAP.get(_norm_nombre(asesor)) # None = usar sede pauta
dm = get_dm()
conn = dm.pg_conn()
try:
cur = conn.cursor()
cur.execute(sql, params + [asesor])
por_tel = {}
for tel, created, sede_p, cargo, etiquetas in cur.fetchall():
# Quitar +51 / + y limpiar (igual que el PBI)
t = str(tel or "").strip().replace("+51", "").replace("+", "").strip()
if not t or t in ("51924374783", "924374783"):
continue
if t in por_tel:
continue # ASC -> ya guardamos el más antiguo
etq = L.ultima_etiqueta(etiquetas)
if filtro_etq and _norm_nombre(etq) not in filtro_etq:
continue # no coincide con las etiquetas elegidas
por_tel[t] = {
"telefono": t,
"fecha": created,
"sede": sede_act or (sede_p or ""),
"programa": cargo or "",
"etiqueta": etq,
}
finally:
conn.close()
filas = sorted(por_tel.values(), key=lambda x: (x["fecha"] or __import__("datetime").datetime.max))
# 2) Armar Excel bonito
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from io import BytesIO
wb = Workbook()
ws = wb.active
ws.title = "Base"
encabezados = ["TELEFONO", "FECHA CREADA", "SEDE", "PROGRAMA", "ULTIMA ETIQUETA"]
ws.append(encabezados)
azul = PatternFill("solid", fgColor="1E3A5F")
blanco = Font(color="FFFFFF", bold=True, size=11)
centro = Alignment(horizontal="center", vertical="center")
borde = Border(bottom=Side(style="thin", color="D9D9D9"))
for c in ws[1]:
c.fill = azul; c.font = blanco; c.alignment = centro
for d in filas:
fecha_val = d["fecha"].date() if d["fecha"] else None
ws.append([int(d["telefono"]) if str(d["telefono"]).isdigit() else d["telefono"],
fecha_val, d["sede"], d["programa"], d["etiqueta"]])
# Formato de la columna fecha (B) como fecha real + alineación
for row in ws.iter_rows(min_row=2, min_col=1, max_col=5):
for c in row:
c.border = borde
if c.column == 2: # FECHA CREADA
c.number_format = "DD/MM/YYYY"
c.alignment = centro
elif c.column in (3, 5): # SEDE, ULTIMA ETIQUETA
c.alignment = centro
anchos = {"A": 16, "B": 15, "C": 14, "D": 16, "E": 26}
for col, w in anchos.items():
ws.column_dimensions[col].width = w
ws.freeze_panes = "A2"
buf = BytesIO()
wb.save(buf)
return buf.getvalue()
# ── Prefijos de cache que dependen del mapa de campañas (leyendas). Al cambiar # ── Prefijos de cache que dependen del mapa de campañas (leyendas). Al cambiar
# una leyenda se invalidan estos para que TODOS los apartados se reconstruyan # una leyenda se invalidan estos para que TODOS los apartados se reconstruyan
# en su proxima peticion (perezoso: solo lo que se pida, ~2-5s), sin esperar # en su proxima peticion (perezoso: solo lo que se pida, ~2-5s), sin esperar