Actualizar backend/services.py
This commit is contained in:
@@ -187,6 +187,158 @@ def plantillas_estados():
|
||||
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
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user