Actualizar backend/services.py
This commit is contained in:
@@ -1,33 +1,33 @@
|
||||
# backend/services.py
|
||||
"""Servicios: traen datos crudos (cacheados) y arman la respuesta del módulo Leads."""
|
||||
from data_manager_v2 import DataManager
|
||||
import leads_logic as L
|
||||
from cache_manager import cache_get_or_set, cache_invalidate
|
||||
# backend/services.py
|
||||
"""Servicios: traen datos crudos (cacheados) y arman la respuesta del módulo Leads."""
|
||||
from data_manager_v2 import DataManager
|
||||
import leads_logic as L
|
||||
from cache_manager import cache_get_or_set, cache_invalidate
|
||||
|
||||
_DM = None
|
||||
_DM = None
|
||||
|
||||
|
||||
def get_dm() -> DataManager:
|
||||
def get_dm() -> DataManager:
|
||||
global _DM
|
||||
if _DM is None:
|
||||
_DM = DataManager()
|
||||
return _DM
|
||||
|
||||
|
||||
# ── 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
|
||||
# los 15 min ni recargar todo. NO se toca la logica, solo se refresca el cache.
|
||||
_PREF_CAMPANIAS = (
|
||||
# ── 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
|
||||
# los 15 min ni recargar todo. NO se toca la logica, solo se refresca el cache.
|
||||
_PREF_CAMPANIAS = (
|
||||
"campanias_map", "campanias_sc", "leads_raw", "leads_dash", "leads_filtros",
|
||||
"leads_alertas", "vendedores_dash", "roas_dash", "otros_general",
|
||||
"matriz_always", "matriz_webform", "matriz_asignados", "matriz_plantillas",
|
||||
"tiempo_resp", "importe_pauta_periodo", "resultados_pauta_periodo",
|
||||
"resultados_pauta", "leads_x_codigo", "tel_es_copito", "programa_dash",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _invalidar_por_campanias():
|
||||
def _invalidar_por_campanias():
|
||||
"""Invalida el cache de campañas (Supabase) y de todos los apartados que
|
||||
dependen de las leyendas, para que el cambio se vea de inmediato."""
|
||||
from data_manager_v2 import invalidar_campanias_cache
|
||||
@@ -36,31 +36,31 @@ def _invalidar_por_campanias():
|
||||
cache_invalidate(p)
|
||||
|
||||
|
||||
# ── Datos crudos cacheados (consultas pesadas, una sola vez) ────
|
||||
def _leads_crudos():
|
||||
# ── Datos crudos cacheados (consultas pesadas, una sola vez) ────
|
||||
def _leads_crudos():
|
||||
return cache_get_or_set("leads_raw", ("GLOBAL",),
|
||||
lambda: L.procesar_leads(get_dm().traer_leads_chatwoot()))
|
||||
|
||||
|
||||
def _cursos_crudos():
|
||||
def _cursos_crudos():
|
||||
return cache_get_or_set("cursos_raw", ("GLOBAL",), lambda: get_dm().traer_cursos())
|
||||
|
||||
|
||||
def _matriculas_crudas():
|
||||
def _matriculas_crudas():
|
||||
return cache_get_or_set("mat_raw", ("GLOBAL",), lambda: get_dm().traer_matriculas())
|
||||
|
||||
|
||||
def _pauta_cruda():
|
||||
def _pauta_cruda():
|
||||
return cache_get_or_set("pauta_raw", ("GLOBAL",), lambda: get_dm().traer_pauta_cursos())
|
||||
|
||||
|
||||
def _programa_pautas():
|
||||
def _programa_pautas():
|
||||
"""{num_indice: [pautas]} desde la tabla programa_pautas (muchos-a-muchos)."""
|
||||
return cache_get_or_set("programa_pautas", ("GLOBAL",),
|
||||
lambda: get_dm().traer_programa_pautas())
|
||||
|
||||
|
||||
def _pautas_de_indice():
|
||||
def _pautas_de_indice():
|
||||
"""{num_indice: [pautas]} combinando:
|
||||
- la pauta de basebi_programacion (1 por num_indice), y
|
||||
- las pautas extra de programa_pautas (varias).
|
||||
@@ -82,18 +82,18 @@ def _pautas_de_indice():
|
||||
return cache_get_or_set("pautas_de_indice", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def _campanias_map():
|
||||
def _campanias_map():
|
||||
return cache_get_or_set("campanias_map", ("GLOBAL",),
|
||||
lambda: get_dm().traer_campanias_map())
|
||||
|
||||
|
||||
def _clasif_meta():
|
||||
def _clasif_meta():
|
||||
"""JSON de clasificacion (sede/programa) de GitHub, MISMO que Ventas."""
|
||||
return cache_get_or_set("clasif_meta", ("GLOBAL",),
|
||||
lambda: get_dm().traer_clasificacion_meta())
|
||||
|
||||
|
||||
def clasificar_sede_meta(dsc_programa):
|
||||
def clasificar_sede_meta(dsc_programa):
|
||||
"""Sede segun el JSON (clasificacion_sedes). Default LIMA. Igual que Ventas."""
|
||||
up = str(dsc_programa or "").upper()
|
||||
data = _clasif_meta().get("clasificacion_sedes", {})
|
||||
@@ -106,7 +106,7 @@ def clasificar_sede_meta(dsc_programa):
|
||||
return data.get("DEFAULT", "LIMA")
|
||||
|
||||
|
||||
def clasificar_prog_meta(dsc_programa):
|
||||
def clasificar_prog_meta(dsc_programa):
|
||||
"""Programa (TEAC/TERC/SEMINARIOS/OTROS) segun clasificacion_filtro_programa
|
||||
del JSON. Respeta el ORDEN del JSON (primero que coincide gana). Igual que Ventas."""
|
||||
up = str(dsc_programa or "").upper()
|
||||
@@ -120,7 +120,7 @@ def clasificar_prog_meta(dsc_programa):
|
||||
return data.get("DEFAULT", "OTROS")
|
||||
|
||||
|
||||
def meta_de_curso(dsc_programa):
|
||||
def meta_de_curso(dsc_programa):
|
||||
"""Meta de inscritos segun sede+programa (JSON), como acordado:
|
||||
LIMA TEAC/TERC=22, provincia TEAC/TERC=18, SEMINARIOS/OTROS=15."""
|
||||
sede = clasificar_sede_meta(dsc_programa)
|
||||
@@ -130,22 +130,22 @@ def meta_de_curso(dsc_programa):
|
||||
return 15
|
||||
|
||||
|
||||
# Conecta los clasificadores del JSON a leads_logic (metas + tipo de matricula).
|
||||
L.set_clasificadores(clasificar_sede_meta, clasificar_prog_meta)
|
||||
# Conecta los clasificadores del JSON a leads_logic (metas + tipo de matricula).
|
||||
L.set_clasificadores(clasificar_sede_meta, clasificar_prog_meta)
|
||||
|
||||
|
||||
def _conjunto_pauta():
|
||||
def _conjunto_pauta():
|
||||
return cache_get_or_set("conjunto_pauta", ("GLOBAL",),
|
||||
lambda: get_dm().traer_conjunto_pauta())
|
||||
|
||||
|
||||
def _meta_filas():
|
||||
def _meta_filas():
|
||||
"""Filas crudas del CSV de Meta: [{conjunto, importe, inicio}, ...]."""
|
||||
return cache_get_or_set("meta_filas", ("GLOBAL",),
|
||||
lambda: get_dm().traer_meta_importe())
|
||||
|
||||
|
||||
def _importe_por_pauta():
|
||||
def _importe_por_pauta():
|
||||
"""{pauta: importe_gastado_total} = suma del importe de TODAS las filas de los
|
||||
conjuntos ligados a esa pauta (conjunto_pauta × Meta CSV). Sin filtro de fecha."""
|
||||
def _load():
|
||||
@@ -162,11 +162,11 @@ def _importe_por_pauta():
|
||||
return cache_get_or_set("importe_pauta", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
# Canales que cuentan como Copito (ademas de origen_base=POSTGRE).
|
||||
_CANALES_COPITO = {"PAUTA_WSP", "PAUTA_WSP_FACE", "COPITO"}
|
||||
# Canales que cuentan como Copito (ademas de origen_base=POSTGRE).
|
||||
_CANALES_COPITO = {"PAUTA_WSP", "PAUTA_WSP_FACE", "COPITO"}
|
||||
|
||||
|
||||
def _tel_es_copito():
|
||||
def _tel_es_copito():
|
||||
"""{telefono: True/False} = si el telefono es COPITO segun su FILA MAS ANTIGUA
|
||||
en la cartera. Copito = (origen_base=POSTGRE) OR (canal en _CANALES_COPITO).
|
||||
Se calcula una vez sobre toda la cartera (cacheado)."""
|
||||
@@ -188,7 +188,7 @@ def _tel_es_copito():
|
||||
return cache_get_or_set("tel_es_copito", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def _leads_por_codigo_periodo(ano, mes, dia):
|
||||
def _leads_por_codigo_periodo(ano, mes, dia):
|
||||
"""{codigo(pauta): {"rec": set(tels recibidos del periodo),
|
||||
"proc": set(tels procesados del periodo)}}.
|
||||
Recibido = lead unico con ese codigo cuya fecha cae en el periodo.
|
||||
@@ -215,7 +215,7 @@ def _leads_por_codigo_periodo(ano, mes, dia):
|
||||
return cache_get_or_set("leads_x_codigo", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
def _resultados_por_pauta():
|
||||
def _resultados_por_pauta():
|
||||
"""{pauta: resultados_totales} = suma de Resultados de TODAS las filas de los
|
||||
conjuntos ligados a esa pauta. Sin filtro de fecha (analogo a _importe_por_pauta)."""
|
||||
def _load():
|
||||
@@ -232,7 +232,7 @@ def _resultados_por_pauta():
|
||||
return cache_get_or_set("resultados_pauta", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def _importe_por_pauta_periodo(ano, mes, dia):
|
||||
def _importe_por_pauta_periodo(ano, mes, dia):
|
||||
"""{pauta: importe} sumando solo las filas del CSV cuyo 'Inicio del informe'
|
||||
cae dentro del periodo filtrado (año/mes/día). Respeta el filtro de arriba."""
|
||||
def _load():
|
||||
@@ -255,7 +255,7 @@ def _importe_por_pauta_periodo(ano, mes, dia):
|
||||
return cache_get_or_set("importe_pauta_periodo", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
def _resultados_por_pauta_periodo(ano, mes, dia):
|
||||
def _resultados_por_pauta_periodo(ano, mes, dia):
|
||||
"""{pauta: resultados} sumando solo las filas del CSV cuyo 'Inicio del informe'
|
||||
cae en el periodo filtrado. Analogo a _importe_por_pauta_periodo pero con Resultados."""
|
||||
def _load():
|
||||
@@ -275,12 +275,12 @@ def _resultados_por_pauta_periodo(ano, mes, dia):
|
||||
return cache_get_or_set("resultados_pauta_periodo", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
def _campanias_sede_cargo():
|
||||
def _campanias_sede_cargo():
|
||||
return cache_get_or_set("campanias_sc", ("GLOBAL",),
|
||||
lambda: get_dm().traer_campanias_sede_cargo())
|
||||
|
||||
|
||||
def matriz_always(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa="TODOS"):
|
||||
def matriz_always(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa="TODOS"):
|
||||
"""Matriz de gasto Meta Ads de pautas ALWAYS (con gasto pero SIN num_indice).
|
||||
Agrupa Sede -> Programa (TEAC/TERC/SEMINARIOS) con Importe, Resultados,
|
||||
Leads Nuevos (fecha_creada en el mes) y Leads Nuevos Asignados (con asesor).
|
||||
@@ -426,7 +426,7 @@ def matriz_always(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa=
|
||||
return cache_get_or_set("matriz_always", (ano, mes, dia, sede, programa), _load)
|
||||
|
||||
|
||||
def matriz_web_formulario(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa="TODOS"):
|
||||
def matriz_web_formulario(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa="TODOS"):
|
||||
"""Matriz de leads con canal WEB_FORMULARIO (cartera_junta), agrupados Sede -> Programa.
|
||||
Valores: Leads Recibidos (todos), Leads Procesados (con asesor), Matriculas y
|
||||
Valor Venta (cruce por telefono, fch_matricula >= fecha_creada del lead)."""
|
||||
@@ -521,23 +521,23 @@ def matriz_web_formulario(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", p
|
||||
return cache_get_or_set("matriz_webform", (ano, mes, dia, sede, programa), _load)
|
||||
|
||||
|
||||
def _asignacion_respuesta_crudos():
|
||||
def _asignacion_respuesta_crudos():
|
||||
return cache_get_or_set("asig_resp", ("GLOBAL",),
|
||||
lambda: get_dm().traer_asignacion_respuesta())
|
||||
|
||||
|
||||
def _leads_asignados_crudos():
|
||||
def _leads_asignados_crudos():
|
||||
# UNIFICADO: usa la MISMA consulta/cache que el grafico (asignacion+respuesta+etiquetas),
|
||||
# asi la tarjeta, la matriz de asignados y el grafico nunca se desfasan.
|
||||
return _asignacion_respuesta_crudos()
|
||||
|
||||
|
||||
def _actividad_asesores_crudos():
|
||||
def _actividad_asesores_crudos():
|
||||
return cache_get_or_set("act_asesores", ("GLOBAL",),
|
||||
lambda: get_dm().traer_actividad_asesores())
|
||||
|
||||
|
||||
def matriz_asignados(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
def matriz_asignados(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
"""Matriz pivot: filas = asesor (user_name) con sus telefonos; columnas = dias del
|
||||
mes; valor = cantidad de telefonos asignados a ese asesor ese dia.
|
||||
Fecha = created_at del mensaje de asignacion (ya con -5h)."""
|
||||
@@ -584,12 +584,12 @@ def matriz_asignados(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
return cache_get_or_set("matriz_asignados", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
def _plantillas_crudas():
|
||||
def _plantillas_crudas():
|
||||
return cache_get_or_set("plantillas_raw", ("GLOBAL",),
|
||||
lambda: get_dm().traer_plantillas())
|
||||
|
||||
|
||||
def matriz_plantillas(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
def matriz_plantillas(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
"""Matriz de plantillas COBRADAS por plantilla. Columnas:
|
||||
ENVIADAS = Mas_de_24h=SI, created_at_peru en el periodo filtrado
|
||||
RESPONDIDAS= de esas, con respuesta del contacto (siguiente_mensaje_1)
|
||||
@@ -683,13 +683,13 @@ def matriz_plantillas(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
return cache_get_or_set("matriz_plantillas", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
def _norm_ni(x):
|
||||
def _norm_ni(x):
|
||||
"""Normaliza num_indice a texto (quita el '.0' si viniera como float)."""
|
||||
s = str(x or "").strip()
|
||||
return s[:-2] if s.endswith(".0") else s
|
||||
|
||||
|
||||
def _mat_por_indice():
|
||||
def _mat_por_indice():
|
||||
"""{num_indice: [(telefono_resuelto, fecha_matricula), ...]} desde las matrículas.
|
||||
Teléfono resuelto = dsc_telefono_1 si existe; si no, dsc_telefono_2."""
|
||||
def _load():
|
||||
@@ -705,14 +705,14 @@ def _mat_por_indice():
|
||||
return cache_get_or_set("mat_por_indice", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def _cartera_rows_cache():
|
||||
def _cartera_rows_cache():
|
||||
"""Filas crudas de cartera_junta cacheadas (se bajan de Supabase UNA sola vez).
|
||||
Evita re-descargar las 86k filas en cada llamada/cambio de mes."""
|
||||
return cache_get_or_set("cartera_rows", ("GLOBAL",),
|
||||
lambda: get_dm().traer_cartera_rows())
|
||||
|
||||
|
||||
def _cartera_idx():
|
||||
def _cartera_idx():
|
||||
"""Devuelve {'all':..., 'asig':...}, cada uno {(sede,programa):{telefono:fecha_min}}.
|
||||
'all' = todos los teléfonos; 'asig' = solo los que tienen asesor (no vacío).
|
||||
Se baja la cartera UNA vez y queda cacheado."""
|
||||
@@ -783,8 +783,8 @@ def _cartera_idx():
|
||||
return cache_get_or_set("cartera_idx", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
# ── Respuesta del dashboard de Leads (con filtros) ──────────────
|
||||
def leads_dashboard(ano="TODOS", mes="TODOS", dia="TODOS", programa="TODOS", sede="TODOS"):
|
||||
# ── Respuesta del dashboard de Leads (con filtros) ──────────────
|
||||
def leads_dashboard(ano="TODOS", mes="TODOS", dia="TODOS", programa="TODOS", sede="TODOS"):
|
||||
def _load():
|
||||
leads = _leads_crudos()
|
||||
cursos = _cursos_crudos()
|
||||
@@ -840,18 +840,18 @@ def leads_dashboard(ano="TODOS", mes="TODOS", dia="TODOS", programa="TODOS", sed
|
||||
return cache_get_or_set("leads_dash", (ano, mes, dia, programa, sede), _load)
|
||||
|
||||
|
||||
def _asesores():
|
||||
def _asesores():
|
||||
"""{'map': {alias_norm: correcto}, 'lista': [los 8 vendedores]}."""
|
||||
return cache_get_or_set("asesores", ("GLOBAL",), lambda: get_dm().traer_asesores())
|
||||
|
||||
|
||||
def _norm_asesor(nombre, mapa):
|
||||
def _norm_asesor(nombre, mapa):
|
||||
"""Normaliza un nombre de asesor a su 'correcto' via el diccionario."""
|
||||
k = " ".join(str(nombre or "").upper().split())
|
||||
return mapa.get(k) # None si no es uno de los vendedores
|
||||
|
||||
|
||||
def _norm_tel_pe(v):
|
||||
def _norm_tel_pe(v):
|
||||
"""Normaliza telefono igual que la cartera: quita simbolos y el '51' inicial
|
||||
de celulares peruanos (rango 51900000000-51999999999). Asi cruzan con cartera_junta."""
|
||||
s = str(v or "")
|
||||
@@ -863,7 +863,7 @@ def _norm_tel_pe(v):
|
||||
return s
|
||||
|
||||
|
||||
def tiempo_respuesta_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
def tiempo_respuesta_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
"""Tiempo de respuesta LABORAL (minutos) del asesor tras la asignacion.
|
||||
- Turno detectado por dia/asesor segun donde hay MAS actividad:
|
||||
tarde (13-18) -> Turno 1 (09-18, refrigerio 13-14 excluido).
|
||||
@@ -1081,7 +1081,7 @@ def tiempo_respuesta_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
return cache_get_or_set("tiempo_resp", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
def tiempo_respuesta_detalle(dia_click, modo="asignacion", vendedor="TODOS",
|
||||
def tiempo_respuesta_detalle(dia_click, modo="asignacion", vendedor="TODOS",
|
||||
ano="TODOS", mes="TODOS"):
|
||||
"""Detalle de los leads que forman el promedio de UN día del gráfico
|
||||
'Tiempo de Respuesta por Día'. Reproduce la MISMA lógica de turnos/minutos.
|
||||
@@ -1222,7 +1222,7 @@ def tiempo_respuesta_detalle(dia_click, modo="asignacion", vendedor="TODOS",
|
||||
return cache_get_or_set("tiempo_resp_det", (dia_click, modo, vendedor, ano, mes), _load)
|
||||
|
||||
|
||||
def vendedores_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
def vendedores_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
"""Por vendedor (los 8 de alias_normalizacion). Filtro Año/Mes/Día:
|
||||
- Cartera (total/copito/otros): filtra por fecha_creada de cartera_junta.
|
||||
- Matriculas: se cruzan por TELEFONO con los leads del mes (es_origen=SI,
|
||||
@@ -1454,17 +1454,17 @@ def vendedores_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
return cache_get_or_set("vendedores_dash", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
def _conjunto_sede_prog():
|
||||
def _conjunto_sede_prog():
|
||||
"""{conjunto_UPPER: {sede, programa}} asignaciones manuales (tabla Supabase)."""
|
||||
return cache_get_or_set("conjunto_sede_prog", ("GLOBAL",),
|
||||
lambda: get_dm().traer_conjunto_sede_programa())
|
||||
|
||||
|
||||
# Programas cuyos RESULTADOS no deben sumar (son de otro tipo: alcance/marca).
|
||||
_PROG_SIN_RESULTADO = {"BRANDING", "COPEAIRE"}
|
||||
# Programas cuyos RESULTADOS no deben sumar (son de otro tipo: alcance/marca).
|
||||
_PROG_SIN_RESULTADO = {"BRANDING", "COPEAIRE"}
|
||||
|
||||
|
||||
def _conjuntos_branding():
|
||||
def _conjuntos_branding():
|
||||
"""Set de CONJUNTOS (UPPER) asignados manualmente como BRANDING/COPEAIRE.
|
||||
Sus resultados NO deben contarse (su importe sí)."""
|
||||
def _load():
|
||||
@@ -1474,7 +1474,7 @@ def _conjuntos_branding():
|
||||
return cache_get_or_set("conjuntos_branding", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def _codigos_branding():
|
||||
def _codigos_branding():
|
||||
"""Set de codigos(pauta) cuyos conjuntos estan asignados manualmente como
|
||||
BRANDING/COPEAIRE. Sirve para NO contar sus resultados en las tablas."""
|
||||
def _load():
|
||||
@@ -1493,7 +1493,7 @@ def _codigos_branding():
|
||||
return cache_get_or_set("codigos_branding", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def conjuntos_sin_sede():
|
||||
def conjuntos_sin_sede():
|
||||
"""Lista los conjuntos del Meta CSV que NO estan ligados a ninguna sede
|
||||
(ni por pauta->campania, ni por asignacion manual). Para el apartado de
|
||||
asignacion. Sin filtro de mes. Incluye la sede/programa YA asignados (si tiene).
|
||||
@@ -1531,7 +1531,7 @@ def conjuntos_sin_sede():
|
||||
return cache_get_or_set("conjuntos_sin_sede", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def guardar_conjunto_sede(cambios):
|
||||
def guardar_conjunto_sede(cambios):
|
||||
"""Guarda asignaciones conjunto->sede/programa e invalida cachés dependientes."""
|
||||
r = get_dm().guardar_conjunto_sede_programa(cambios)
|
||||
for pref in ("conjunto_sede_prog", "conjuntos_sin_sede", "roas_dash"):
|
||||
@@ -1539,7 +1539,7 @@ def guardar_conjunto_sede(cambios):
|
||||
return r
|
||||
|
||||
|
||||
def borrar_conjunto_sede(conjunto):
|
||||
def borrar_conjunto_sede(conjunto):
|
||||
"""Borra la asignacion de un conjunto e invalida cachés dependientes."""
|
||||
r = get_dm().borrar_conjunto_sede_programa(conjunto)
|
||||
for pref in ("conjunto_sede_prog", "conjuntos_sin_sede", "roas_dash"):
|
||||
@@ -1547,7 +1547,7 @@ def borrar_conjunto_sede(conjunto):
|
||||
return r
|
||||
|
||||
|
||||
def roas_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
def roas_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
"""Matriz ROAS por SEDE (LIMA, PIURA, AREQUIPA, TRUJILLO): Importe Gastado y
|
||||
Resultados del Google Sheet (Meta Ads), filtrado por Año/Mes (Inicio del informe).
|
||||
Cadena: conjunto -> pauta (conjunto_pauta) -> sede (campanias). Conjuntos sin
|
||||
@@ -1693,10 +1693,10 @@ def roas_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
return cache_get_or_set("roas_dash", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
_CURSOS_EXCLUIDOS = {"1121", "1154"}
|
||||
_CURSOS_EXCLUIDOS = {"1121", "1154"}
|
||||
|
||||
|
||||
def _curso_excluido(c, pmap):
|
||||
def _curso_excluido(c, pmap):
|
||||
"""Mismo criterio que 'Detalle por Curso': excluye cursos en la lista fija,
|
||||
o con contar=NO / estado=SUSPENDIDO en basebi."""
|
||||
ni = str(c.get("num_indice", "")).strip()
|
||||
@@ -1708,7 +1708,7 @@ def _curso_excluido(c, pmap):
|
||||
return info.get("contar") == "NO" or info.get("estado") == "SUSPENDIDO"
|
||||
|
||||
|
||||
def programa_opciones(ano="TODOS", mes="TODOS"):
|
||||
def programa_opciones(ano="TODOS", mes="TODOS"):
|
||||
"""Lista de programas (dsc_det_programa) cuyos cursos tienen fch_inicio en el
|
||||
periodo. Para el selector del apartado PROGRAMA. Ordenada por fch_inicio ASC."""
|
||||
def _load():
|
||||
@@ -1731,7 +1731,7 @@ def programa_opciones(ano="TODOS", mes="TODOS"):
|
||||
return cache_get_or_set("programa_opciones", (ano, mes), _load)
|
||||
|
||||
|
||||
def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
||||
def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
||||
"""Datos agregados de UN programa (dsc_det_programa) en el periodo:
|
||||
Cartera Total/Copito/Otros, Leads Recibidos/Procesados/Contactados y %.
|
||||
- Filtro por fch_inicio del curso para elegir los cursos del programa.
|
||||
@@ -1778,6 +1778,7 @@ def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
||||
# de las pautas del programa. Telefonos unicos por etiqueta.
|
||||
from collections import defaultdict as _dd
|
||||
etiq_tels = _dd(set)
|
||||
etiq_ase_tels = _dd(lambda: _dd(set)) # estado -> asesor -> {telefonos}
|
||||
cods_set0 = set(cods)
|
||||
for l in _leads_crudos():
|
||||
if not l.get("es_unico") or not l.get("asesor"):
|
||||
@@ -1788,9 +1789,16 @@ def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
||||
if not tel:
|
||||
continue
|
||||
ult = l.get("ultima") or "NO TRABAJADO"
|
||||
ase = l.get("asesor") or "SIN ASESOR"
|
||||
etiq_tels[ult].add(tel)
|
||||
etiq_ase_tels[ult][ase].add(tel)
|
||||
matriz_etiq = sorted(
|
||||
[{"estado": k, "cantidad": len(v)} for k, v in etiq_tels.items()],
|
||||
[{"estado": k, "cantidad": len(v),
|
||||
"asesores": sorted(
|
||||
[{"asesor": a, "cantidad": len(t), "telefonos": sorted(t)}
|
||||
for a, t in etiq_ase_tels[k].items()],
|
||||
key=lambda x: -x["cantidad"])}
|
||||
for k, v in etiq_tels.items()],
|
||||
key=lambda x: -x["cantidad"])
|
||||
|
||||
# 3) CARTERA TOTAL: MISMA logica que 'Detalle por Curso'. Por cada curso,
|
||||
@@ -1994,7 +2002,7 @@ def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
||||
return cache_get_or_set("programa_dash", (ano, mes, programa), _load)
|
||||
|
||||
|
||||
def otros_general(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
def otros_general(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
"""Solo las 4 matrices de la pagina 'Otros General' (sin KPIs/graficos/tabla_pauta).
|
||||
Mas liviano que leads_dashboard -> filtrar por mes es mas rapido."""
|
||||
def _load():
|
||||
@@ -2007,12 +2015,12 @@ def otros_general(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||
return cache_get_or_set("otros_general", (ano, mes, dia), _load)
|
||||
|
||||
|
||||
def conjuntos_sin_pauta():
|
||||
def conjuntos_sin_pauta():
|
||||
"""Lista de conjuntos de anuncios sin pauta (para el desplegable del pop-up)."""
|
||||
return get_dm().traer_conjuntos_sin_pauta()
|
||||
|
||||
|
||||
def _pautas_de_curso_ni(ni):
|
||||
def _pautas_de_curso_ni(ni):
|
||||
"""Pautas (codigos) de un num_indice = pauta de basebi + programa_pautas."""
|
||||
cds = set()
|
||||
for p in _pautas_de_indice().get(ni, []):
|
||||
@@ -2023,7 +2031,7 @@ def _pautas_de_curso_ni(ni):
|
||||
return cds
|
||||
|
||||
|
||||
def alertas_dashboard():
|
||||
def alertas_dashboard():
|
||||
"""ALERTAS: cursos que inician en los proximos 30 dias (con pauta, importe>0).
|
||||
Estado (CRITICO/ADVERTENCIA/BUEN RUMBO) segun MATRICULAS vs meta ESPERADA por
|
||||
dias que faltan. CPL alto / pocos leads son SENALES (no cambian el estado).
|
||||
@@ -2315,7 +2323,7 @@ def alertas_dashboard():
|
||||
return res
|
||||
|
||||
|
||||
def programas_ocultos():
|
||||
def programas_ocultos():
|
||||
"""Programas con contar=NO en basebi_programacion (ocultos de la matriz).
|
||||
Cada item: {num_indice, dsc_det_programa, fch_inicio, pauta}."""
|
||||
pm = _pauta_cruda()
|
||||
@@ -2337,7 +2345,7 @@ def programas_ocultos():
|
||||
return {"filas": filas}
|
||||
|
||||
|
||||
def encender_programas(num_indices):
|
||||
def encender_programas(num_indices):
|
||||
"""Pone contar=SI a los num_indice dados en basebi_programacion (los reactiva)."""
|
||||
from cache_manager import cache_invalidate
|
||||
dm = get_dm()
|
||||
@@ -2348,7 +2356,7 @@ def encender_programas(num_indices):
|
||||
return {"encendidos": len(num_indices)}
|
||||
|
||||
|
||||
def programas_disponibles():
|
||||
def programas_disponibles():
|
||||
"""Lista de dsc_det_programa de cursos con fch_inicio >= 2026-01-01 (para el
|
||||
desplegable de Programa en la Leyenda). Cada item: {num_indice, dsc_det_programa}."""
|
||||
from datetime import date
|
||||
@@ -2370,30 +2378,30 @@ def programas_disponibles():
|
||||
return {"programas": filas}
|
||||
|
||||
|
||||
# ── LEYENDA (campanias de Supabase): listar / agregar / borrar ──
|
||||
def campanias_listar():
|
||||
# ── LEYENDA (campanias de Supabase): listar / agregar / borrar ──
|
||||
def campanias_listar():
|
||||
return get_dm().listar_campanias()
|
||||
|
||||
|
||||
def campania_agregar(datos):
|
||||
def campania_agregar(datos):
|
||||
res = get_dm().agregar_campania(datos)
|
||||
_invalidar_por_campanias() # la leyenda nueva se refleja de inmediato en todo el dashboard
|
||||
return res
|
||||
|
||||
|
||||
def campania_editar(cid, datos):
|
||||
def campania_editar(cid, datos):
|
||||
res = get_dm().editar_campania(cid, datos)
|
||||
_invalidar_por_campanias() # el cambio se refleja de inmediato en todo el dashboard
|
||||
return res
|
||||
|
||||
|
||||
def campania_borrar(cid):
|
||||
def campania_borrar(cid):
|
||||
res = get_dm().borrar_campania(cid)
|
||||
_invalidar_por_campanias()
|
||||
return res
|
||||
|
||||
|
||||
def leyenda_anuncios():
|
||||
def leyenda_anuncios():
|
||||
"""Tabla: todos los conjuntos del Sheet con su pauta (si tiene) y el programa
|
||||
(dsc_det_programa) del num_indice que usa esa pauta. Cacheado (usa insumos ya
|
||||
en memoria) para que abrir la Leyenda sea rapido."""
|
||||
@@ -2431,7 +2439,7 @@ def leyenda_anuncios():
|
||||
return cache_get_or_set("leyenda_anuncios", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def guardar_leyenda(cambios):
|
||||
def guardar_leyenda(cambios):
|
||||
"""cambios = [{conjunto, pauta, num_indice?}].
|
||||
- conjunto -> pauta: upsert en conjunto_pauta.
|
||||
- si viene num_indice: conecta ese num_indice a la pauta en basebi_programacion.
|
||||
@@ -2460,7 +2468,7 @@ def guardar_leyenda(cambios):
|
||||
return {"guardados": len(cambios)}
|
||||
|
||||
|
||||
def uso_de_pauta(pauta, excluir_num_indice=None):
|
||||
def uso_de_pauta(pauta, excluir_num_indice=None):
|
||||
"""Verifica si una pauta ya esta en uso. Devuelve los cursos (dsc_det_programa)
|
||||
y conjuntos que la usan (para la advertencia antes de guardar)."""
|
||||
dm = get_dm()
|
||||
@@ -2480,7 +2488,7 @@ def uso_de_pauta(pauta, excluir_num_indice=None):
|
||||
"cursos": detalle, "num_indices": nis, "conjuntos": conjuntos}
|
||||
|
||||
|
||||
def guardar_edicion_curso(num_indice, pauta, conjunto=None, contar=None):
|
||||
def guardar_edicion_curso(num_indice, pauta, conjunto=None, contar=None):
|
||||
"""Guarda pauta y contar (SI/NO) en basebi_programacion (upsert) y, si se eligió,
|
||||
vincula el conjunto a esa pauta en conjunto_pauta. Invalida SOLO los cachés que
|
||||
dependen de pauta/conjunto (NO la cartera, para que la recarga sea rápida)."""
|
||||
@@ -2498,7 +2506,7 @@ def guardar_edicion_curso(num_indice, pauta, conjunto=None, contar=None):
|
||||
return res
|
||||
|
||||
|
||||
def _tipo_de_curso(c):
|
||||
def _tipo_de_curso(c):
|
||||
"""Categoría de tipo de programa para el filtro (PROGRAMAS/PROVINCIA/SEMINARIOS/OTROS)."""
|
||||
dscp = c.get("dsc_programa", "")
|
||||
otros = L.clasificar_tipo_programa(dscp)
|
||||
@@ -2511,8 +2519,8 @@ def _tipo_de_curso(c):
|
||||
return L.tipo_programa_cohorte(sede, cat)
|
||||
|
||||
|
||||
# ── Listas para los filtros (años, tipos de programa) ───────────
|
||||
def opciones_filtros():
|
||||
# ── Listas para los filtros (años, tipos de programa) ───────────
|
||||
def opciones_filtros():
|
||||
def _load():
|
||||
cursos = _cursos_crudos()
|
||||
anos = sorted({L._to_date(c.get("fch_inicio")).year for c in cursos
|
||||
@@ -2523,14 +2531,14 @@ def opciones_filtros():
|
||||
return cache_get_or_set("leads_filtros", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
# ── Alertas: valores de programa/sede no identificados en cartera_junta ──
|
||||
def alertas():
|
||||
# ── Alertas: valores de programa/sede no identificados en cartera_junta ──
|
||||
def alertas():
|
||||
return cache_get_or_set("leads_alertas", ("GLOBAL",),
|
||||
lambda: get_dm().traer_no_identificados())
|
||||
|
||||
|
||||
# ── Diagnóstico de cursos (para cuadrar ocupabilidad) ───────────
|
||||
def debug_cursos(ano, mes):
|
||||
# ── Diagnóstico de cursos (para cuadrar ocupabilidad) ───────────
|
||||
def debug_cursos(ano, mes):
|
||||
cursos = _cursos_crudos()
|
||||
periodo = [c for c in cursos if L._en_periodo(L._to_date(c.get("fch_inicio")), ano, mes, "TODOS")]
|
||||
detalle = []
|
||||
@@ -2560,8 +2568,8 @@ def debug_cursos(ano, mes):
|
||||
}
|
||||
|
||||
|
||||
# ── Precarga / refresco ─────────────────────────────────────────
|
||||
def _periodos_actual_anterior():
|
||||
# ── Precarga / refresco ─────────────────────────────────────────
|
||||
def _periodos_actual_anterior():
|
||||
"""[(ano, mes), ...] = mes actual y mes anterior (segun fecha de hoy)."""
|
||||
from datetime import date
|
||||
hoy = date.today()
|
||||
@@ -2573,11 +2581,11 @@ def _periodos_actual_anterior():
|
||||
return [(str(ano), str(mes)), (str(prev[0]), str(prev[1]))]
|
||||
|
||||
|
||||
import os as _os
|
||||
_LOG_CARGA = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "carga_tiempos.log")
|
||||
import os as _os
|
||||
_LOG_CARGA = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "carga_tiempos.log")
|
||||
|
||||
|
||||
def _log_carga(linea, reset=False):
|
||||
def _log_carga(linea, reset=False):
|
||||
"""Escribe una linea en carga_tiempos.log (lo lee la ventana de tiempos)."""
|
||||
try:
|
||||
with open(_LOG_CARGA, "w" if reset else "a", encoding="utf-8") as f:
|
||||
@@ -2586,13 +2594,13 @@ def _log_carga(linea, reset=False):
|
||||
pass
|
||||
|
||||
|
||||
import threading as _thr
|
||||
_TIEMPOS = {} # etiqueta -> segundos (para el resumen final)
|
||||
_TIEMPOS_LOCK = _thr.Lock()
|
||||
_HILOS_LISTOS = set() # {"leads","otros"} — cuando estan los 2, se imprime resumen
|
||||
import threading as _thr
|
||||
_TIEMPOS = {} # etiqueta -> segundos (para el resumen final)
|
||||
_TIEMPOS_LOCK = _thr.Lock()
|
||||
_HILOS_LISTOS = set() # {"leads","otros"} — cuando estan los 2, se imprime resumen
|
||||
|
||||
|
||||
def _cron(etiqueta, base, fn, apartado=None):
|
||||
def _cron(etiqueta, base, fn, apartado=None):
|
||||
"""Ejecuta fn() midiendo el tiempo. Imprime y ademas guarda en el log:
|
||||
paso, base de datos y segundos. Se ve en la ventana del BACKEND y en la
|
||||
ventana de TIEMPOS al iniciar, para saber que carga cada cosa.
|
||||
@@ -2615,7 +2623,7 @@ def _cron(etiqueta, base, fn, apartado=None):
|
||||
return r
|
||||
|
||||
|
||||
def _hilo_termino(nombre):
|
||||
def _hilo_termino(nombre):
|
||||
"""Marca un hilo como listo. Cuando ambos ("leads" y "otros") terminan,
|
||||
imprime el resumen final por apartado en el log."""
|
||||
with _TIEMPOS_LOCK:
|
||||
@@ -2639,7 +2647,7 @@ def _hilo_termino(nombre):
|
||||
_log_carga("=" * 60)
|
||||
|
||||
|
||||
def precargar_todo():
|
||||
def precargar_todo():
|
||||
"""Precarga en segundo plano: insumos crudos + el dashboard del mes actual y
|
||||
anterior (con filtros en TODOS), para que el usuario no espere al abrir/filtrar."""
|
||||
try:
|
||||
@@ -2677,7 +2685,7 @@ def precargar_todo():
|
||||
_hilo_termino("leads")
|
||||
|
||||
|
||||
def precargar_otros_general():
|
||||
def precargar_otros_general():
|
||||
"""Precarga (aparte, mas pesada) las 4 matrices de Otros General del mes actual
|
||||
y anterior. Se llama en su propio hilo para no bloquear."""
|
||||
try:
|
||||
@@ -2697,26 +2705,29 @@ def precargar_otros_general():
|
||||
_hilo_termino("otros")
|
||||
|
||||
|
||||
# Hora de la última actualización (hora Perú)
|
||||
_ULTIMA_ACTUALIZACION = None
|
||||
# Hora de la última actualización (hora Perú)
|
||||
_ULTIMA_ACTUALIZACION = None
|
||||
|
||||
|
||||
def _marcar_actualizacion():
|
||||
def _marcar_actualizacion():
|
||||
global _ULTIMA_ACTUALIZACION
|
||||
from datetime import datetime, timezone, timedelta
|
||||
peru = timezone(timedelta(hours=-5))
|
||||
_ULTIMA_ACTUALIZACION = datetime.now(peru).strftime("%d/%m/%Y %H:%M")
|
||||
|
||||
|
||||
def ultima_actualizacion():
|
||||
def ultima_actualizacion():
|
||||
return {"hora": _ULTIMA_ACTUALIZACION}
|
||||
|
||||
|
||||
def refrescar_todo():
|
||||
from cache_manager import cache_invalidate
|
||||
def refrescar_todo():
|
||||
from cache_manager import set_forzar
|
||||
from data_manager_v2 import invalidar_campanias_cache
|
||||
cache_invalidate() # vaciar y recargar TODO
|
||||
invalidar_campanias_cache() # tambien las campañas (cache aparte)
|
||||
set_forzar(True) # recalcula y sobrescribe (no lee cache viejo)
|
||||
try:
|
||||
precargar_todo() # leads (mes actual/anterior)
|
||||
precargar_otros_general() # cartera + Otros General (para que no quede vacia)
|
||||
precargar_otros_general() # cartera + Otros General
|
||||
finally:
|
||||
set_forzar(False)
|
||||
_marcar_actualizacion()
|
||||
|
||||
Reference in New Issue
Block a user