Actualizar backend/services.py
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user