Files
dashboard-leads/backend/data_manager.py

272 lines
20 KiB
Python

# backend/data_manager.py
"""
Capa de acceso a datos del módulo LEADS.
- PostgreSQL (Chatwoot): leads de pauta (consolidado en UNA sola consulta).
- SQL Server (Académico): matrículas y cursos.
- GitHub: mapa de campañas (frases -> programa/sede), config editable.
"""
import os
import json
import requests
from datetime import datetime
from dotenv import load_dotenv
load_dotenv()
class DataManager:
def __init__(self):
# PostgreSQL (Chatwoot)
self.pg_host = os.getenv("PG_HOST", "191.98.134.81")
self.pg_db = os.getenv("PG_DATABASE", "chatwoot_production")
self.pg_user = os.getenv("PG_USER", "postgres")
self.pg_pass = os.getenv("PG_PASSWORD", "")
self.pg_port = os.getenv("PG_PORT", "5432")
# SQL Server (Académico)
self.sql_server = os.getenv("SQL_SERVER", "191.98.134.80")
self.sql_db = os.getenv("SQL_DATABASE", "BDUS_CK000040_0001")
self.sql_user = os.getenv("SQL_USERNAME", "")
self.sql_pass = os.getenv("SQL_PASSWORD", "")
# GitHub
base = os.getenv("GITHUB_BASE", "")
self.github_campanias_url = f"{base}/campanias.json" if base else ""
self.campanias = []
self._cargar_campanias()
# ── Conexiones ──────────────────────────────────────────────
def pg_conn(self):
import psycopg2
return psycopg2.connect(
host=self.pg_host, dbname=self.pg_db, user=self.pg_user,
password=self.pg_pass, port=self.pg_port,
)
def sql_conn(self):
import pyodbc
conn_str = (
f"DRIVER={{SQL Server}};SERVER={self.sql_server};"
f"DATABASE={self.sql_db};UID={self.sql_user};PWD={self.sql_pass}"
)
return pyodbc.connect(conn_str)
# ── Config de campañas (GitHub, opcional) ───────────────────
def _cargar_campanias(self):
if not self.github_campanias_url:
self.campanias = []
return
try:
r = requests.get(self.github_campanias_url, timeout=5)
r.raise_for_status()
data = r.json()
self.campanias = data.get("campanias", []) if isinstance(data, dict) else data
except Exception:
self.campanias = []
# ════════════════════════════════════════════════════════════
# CHATWOOT — UN SOLO QUERY CONSOLIDADO DE LEADS
# Reemplaza los 4 queries del PBI (procesados, asignados,
# contactados, etiquetas) por uno solo. El resto se calcula
# en Python (Cantidad_Veces, Ultima_Etiqueta, Estado/Objeción).
# ════════════════════════════════════════════════════════════
def traer_leads_chatwoot(self):
"""Leads de PAUTA (igual que el PBI): un mensaje cuenta como lead solo si su
texto coincide con una FRASE de campaña y está dentro del rango de fechas de
esa campaña. El mapa de campañas está incrustado (como en el Power BI)."""
sql = """
SELECT DISTINCT ON (m.id)
REGEXP_REPLACE(c.phone_number, '[^0-9]', '', 'g') AS telefono,
u.name AS asesor,
(m.created_at - INTERVAL '5 hours') AS fecha_creada,
cv.cached_label_list AS etiquetas,
map.cargo AS programa,
map.sede AS sede,
map.codigo AS codigo,
map.origen AS origen
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
('📚 Me interesa el', 'TERC', '200', 'Arequipa', 'Domingo', 'Pauta_wsp', '2025-10-01', '2025-12-31'),
('📚 Me interesa el', 'TEAC', '227', 'Lima', 'Domingo', 'Pauta_wsp', '2026-01-15', '2026-03-30'),
('Hola! 🚨', 'TEAC', '191', 'Piura', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 💯 Me interesa', 'TERC', '193', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 💼', 'TEAC', '195', 'Trujillo', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-01-31'),
('Hola! 💸', 'TEAC', '197', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
('Hola! ⚡', 'TEAC', '198', 'Trujillo', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-01-31'),
('Hola! 🦺', 'TERC', '199', 'Piura', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
('Hola! 🍾', 'TEAC', '201', 'Arequipa', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
('¡Hola! 📚', 'TERC', '200', 'Arequipa', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-01-31'),
('Hola! 🥶', 'TEAC', '202', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🎯', 'TEAC', '203', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
('Hola! 🌞', 'TERC', '204', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🗿', 'TERC', '205', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
('Hola! 😎', 'TEAC', '206', 'Piura', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-04-01'),
('Hola! 🎓', 'TEAC', '207', 'Arequipa', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-01-31'),
('Hola! 👀', 'TEAC', '208', 'Trujillo', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🎮', 'TERC', '209', 'Piura', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-04-30'),
('Hola! 👾', 'TERC', '210', 'Arequipa', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-02-25'),
('Hola! 🕹️', 'TERC', '211', 'Trujillo', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-04-01'),
('Hola! 🚀', 'TEAC', '212', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🛸', 'TERC', '213', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🦾', 'Amoniaco', '63a', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-04-15'),
('Hola! 🛠️', 'Diseño Chillers', '64a', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-04-30'),
('¡Hola! 🥽', 'VRF', '65a', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
('¡Hola! ❄️ Estoy interesado Seminario', 'Cámaras', '66a', 'Lima', '-', 'Pauta_wsp', '2026-02-01', '2026-05-28'),
('¡Hola! 🤩', 'Diseño de Sistemas', '67a', 'Lima', '-', 'Pauta_wsp', '2026-02-01', '2026-12-31'),
('¡Hola! 🙌🏼', 'Metrado y Costeo', '68a', 'Lima', '-', 'Pauta_wsp', '2026-02-01', '2026-12-31'),
('¡Hola! 🥽', 'VRF', '69a', 'Lima', '-', 'Pauta_wsp', '2026-03-01', '2026-06-30'),
(' 🧊 Más información del Seminario', 'VRF', '71a', 'Arequipa', '-', 'Pauta_wsp', '2026-03-01', '2026-06-30'),
(' 📐 Más información del Seminario', 'VRF', '72a', 'Trujillo', '-', 'Pauta_wsp', '2026-03-01', '2026-06-30'),
(' 👾 Más información del Seminario en Instalación', 'VRF', '73a', 'Piura', '-', 'Pauta_wsp', '2026-03-01', '2026-06-30'),
(' 🖥️ Estoy interesado en sus Seminarios', 'Seminarios', '74a', 'Lima', '-', 'Pauta_wsp', '2026-04-01', '2026-07-30'),
(' 🙋 Deseo más info. del Diplomado ', 'Diplomado REF', '75a', 'Lima', '-', 'Pauta_wsp', '2026-04-01', '2026-07-30'),
('💸 Deseo más info. del Seminario', 'CAD', '76a', 'Lima', '-', 'Pauta_wsp', '2026-04-01', '2026-07-30'),
('🦾 Estoy interesado Instalación de Sistemas', 'VRF', '77a', 'Lima', '-', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
('🙀 Estoy interesado Seminario', 'SUPER.OBRAS', '78a', 'Lima', '-', 'Pauta_wsp', '2026-05-15', '2026-07-30'),
('🛠️ más info del Seminario Diseño de Chillers', 'Diseño', '79a', 'Lima', '-', 'Pauta_wsp', '2026-05-15', '2026-07-30'),
('👾Estoy interesado en el Seminario Refrigeración', 'CO2', '80a', 'Lima', '-', 'Pauta_wsp', '2026-05-15', '2026-07-30'),
('Hola! 👉', 'TEAC', '214', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🤝', 'TEAC', '215', 'Lima', 'Domingo', 'Pauta_wsp', '2025-12-01', '2026-03-15'),
('🆕 Me interesa', 'TERC', '217', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🌬️', 'TEAC', '218', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🌍', 'TERC', '219', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🎟️', 'TEAC', '220', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-03-31'),
('Hola! 🌈', 'TERC', '221', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-04-15'),
('¡Hola! ❄️ Me interesa el programa', 'TEAC', '222', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('Hola! 🔝', 'TERC', '223', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('🔋 Me interesa', 'TEAC', '224', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-04-15'),
('¡Hola! 🌡️', 'TEAC', '225', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
('¡Hola! ☃️', 'TERC', '226', 'Lima', 'Sábado', 'Pauta_wsp', '2026-01-01', '2026-04-15'),
('¡Hola! 📚', 'TERC', '227', 'Lima', 'Semipresencial', 'Pauta_wsp', '2026-02-01', '2026-05-10'),
('¡Hola! 🗺️', 'TEAC', '228', 'Arequipa','Semipresencial', 'Pauta_wsp', '2026-02-01', '2026-05-05'),
('¡Hola! 🌤️', 'TEAC', '229', 'Piura', 'Semipresencial', 'Pauta_wsp', '2026-02-01', '2026-05-30'),
('¡Hola! ⚡', 'TEAC', '230', 'Trujillo','Semipresencial', 'Pauta_wsp', '2026-02-01', '2026-05-07'),
('¡Hola! 🎤', 'TERC', '231', 'Lima', 'Domingo', 'Pauta_wsp', '2026-02-01', '2026-05-30'),
('¡Hola! 👾 Me interesa el programa', 'TEAC', '232', 'Lima', 'Domingo', 'Pauta_wsp', '2026-02-26', '2026-05-30'),
('¡Hola! 🍨', 'TEAC', '233', 'Lima', 'Sábado', 'Pauta_wsp', '2026-02-01', '2026-05-30'),
('¡Hola! 🐶 Me interesa', 'TEAC', '234', 'Piura', 'Sábado', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
('¡Hola! 💸 Me interesa', 'TERC', '235', 'Arequipa', 'Semipresencial', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
('¡Hola! 🗿 Me interesa', 'TERC', '236', 'Trujillo', 'Sábado', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
('¡Hola! 🦺 Me interesa', 'TEAC', '237', 'Lima', 'Sábado', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
('¡Hola! 🍾 Me interesa', 'TEAC', '238', 'Lima', 'Domingo', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
('💡 deseo más info del programa', 'TERC', '239', 'Lima', 'Semipresencial', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
('¡Hola! 🎯 Me interesa el programa', 'TERC', '240', 'Lima', 'Domingo', 'Pauta_wsp', '2026-04-01', '2026-05-30'),
('¡Hola! 🤝 Me interesa el programa', 'TERC', '241', 'Lima', 'Sábado', 'Pauta_wsp', '2026-04-01', '2026-05-30'),
('🪐 Me interesa el programa', 'TERC', '242', 'Piura', 'Domingo', 'Pauta_wsp', '2026-04-01', '2026-05-30'),
('🎓 Me interesa el programa', 'TEAC', '243', 'Arequipa', 'Domingo', 'Pauta_wsp', '2026-04-01', '2026-06-30'),
('💼 Me interesa el programa ', 'TEAC', '244', 'Trujillo', 'Domingo', 'Pauta_wsp', '2026-04-01', '2026-06-30'),
('☃️ Me interesa el programa', 'TEAC', '245', 'Arequipa', 'Sabado', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
('🔋 Me interesa el programa', 'TERC', '246', 'Arequipa', 'Sabado', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
('😎 Me interesa el programa', 'TEAC', '247', 'Arequipa', 'Domingo', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
('🌈Me interesa el programa', 'TEAC', '248', 'Lima', 'Sabado', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
('⭐ Me interesa el programa', 'TEAC', '249', 'Lima', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-07-30'),
('🕵️ Me interesa el programa', 'TEAC', '250', 'Piura', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-07-30'),
('📈 Me interesa el programa', 'TEAC', '251', 'Trujillo', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-07-30'),
('🗺️ Me interesa el programa', 'TEAC', '252', 'Piura', 'Sabado', 'Pauta_wsp', '2026-05-06', '2026-07-30'),
('🎮 Me interesa el programa', 'TEAC', '253', 'Trujillo', 'Sabado', 'Pauta_wsp', '2026-05-01', '2026-07-30'),
('🏞️ Me interesa el programa', 'TERC', '254', 'Arequipa', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-12-31'),
('⚙️ Me interesa el programa', 'TERC', '255', 'Piura', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-12-31'),
('⚡ Me interesa el programa', 'TERC', '256', 'Trujillo', 'Domingo', 'Pauta_wsp', '2026-05-08', '2026-12-31'),
('🎟️ Me interesa el programa', 'TERC', '257', 'Trujillo', 'Sabado', 'Pauta_wsp', '2026-05-01', '2026-12-31'),
('🕑 Me interesa el', 'TERC', '258', 'Piura', 'Sabado', 'Pauta_wsp', '2026-05-01', '2026-12-31'),
('📚 Me interesa el programa', 'TEAC', '259', 'Lima', 'Semipresencial', 'Pauta_wsp', '2026-05-11', '2026-12-31'),
('🤗 Me interesa el programa', 'TEAC', '260', 'Lima', 'Semipresencial', 'Pauta_wsp', '2026-05-11', '2026-12-31'),
('🕹️ Me interesa el programa', 'TERC', '261', 'Lima', 'Domingo', 'Pauta_wsp', '2026-05-11', '2026-12-31'),
('🐶 Me interesa el programa', 'TEAC', '262', 'Piura', 'Sabado', 'Pauta_wsp', '2026-05-11', '2026-08-30'),
('🍨 Me interesa el programa', 'TERC', '263', 'Arequipa', 'Sabado', 'Pauta_wsp', '2026-05-15', '2026-08-30'),
('⛱️ Me interesa el programa de Aire Acondicionado', 'TEAC', '264', 'Lima', 'Sabado', 'Pauta_wsp', '2026-05-15', '2026-08-30'),
('🌤️ Me interesa el programa', 'TEAC', '265', 'Arequipa', 'Sabado', 'Pauta_wsp', '2026-05-15', '2026-08-30'),
('⛱️ Me interesa el programa de Refrigeración comercial', 'TERC', '266', 'Lima', 'Sabado', 'Pauta_wsp', '2026-05-15', '2026-08-30'),
('¡Hola! Vengo de su web, quiero saber', 'TEAC', '0', 'Lima', '-', 'Web_Whatsapp', '2025-11-01', '2026-12-31')
) AS map(frase_busqueda, cargo, codigo, sede, dia, origen, fecha_inicio, fecha_fin)
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'
ORDER BY m.id ASC, m.created_at ASC
"""
conn = self.pg_conn()
cur = conn.cursor()
cur.execute(sql)
cols = [d[0] for d in cur.description]
filas = [dict(zip(cols, row)) for row in cur.fetchall()]
conn.close()
return filas
# ════════════════════════════════════════════════════════════
# SQL SERVER — CURSOS (igual que el PBI: Fact_SQL_Base_Cursos)
# ════════════════════════════════════════════════════════════
def traer_cursos(self):
sql = """
SELECT
rp.num_indice,
rp.dsc_det_programa,
p.dsc_programa,
rp.cod_frecuencia,
rp.fch_inicio,
rp.cod_estado,
(SELECT COUNT(*) FROM sgeca_matricula m
WHERE m.cod_periodo = rp.cod_detalle AND m.num_indice = rp.num_indice
AND m.cod_estado NOT IN ('ANU')) AS Inscritos_Totales,
(SELECT COUNT(*) FROM sgeca_matricula m
WHERE m.cod_periodo = rp.cod_detalle AND m.num_indice = rp.num_indice
AND m.cod_estado = 'RET') AS Inscritos_Retirados,
(SELECT COUNT(*) FROM sgeca_matricula m
WHERE m.cod_periodo = rp.cod_detalle AND m.num_indice = rp.num_indice
AND m.cod_estado NOT IN ('ANU','RET','SUS')) AS Inscritos_Activos
FROM sgede_RP_programa rp
INNER JOIN sgeca_programa p ON rp.cod_programa = p.cod_programa
WHERE YEAR(rp.fch_inicio) IN (2025, 2026)
ORDER BY rp.fch_inicio ASC
"""
conn = self.sql_conn()
cur = conn.cursor()
cur.execute(sql)
cols = [d[0] for d in cur.description]
filas = [dict(zip(cols, row)) for row in cur.fetchall()]
conn.close()
return filas
# ════════════════════════════════════════════════════════════
# SQL SERVER — MATRÍCULAS (Fact_SQL_Base_Matriculas, resumido)
# ════════════════════════════════════════════════════════════
def traer_matriculas(self):
sql = """
SELECT
sgeca_matricula.num_matricula,
sgeca_matricula.num_indice,
sgeca_matricula.fch_matricula,
sgeca_matricula.cod_estado AS estado_matricula,
REPLACE(sgema_alumno.dsc_telefono_1,' ','') AS dsc_telefono_1,
sgeca_programa.dsc_programa,
sgede_RP_programa.dsc_det_programa AS dsc_promocion,
ISNULL((SELECT sgede_cronograma_matricula.imp_saldo FROM sgede_cronograma_matricula
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
AND sgede_cronograma_matricula.num_refinanciamiento = 1
AND sgede_cronograma_matricula.num_cuota = 0), 0) AS imp_saldo_matricula,
ISNULL((SELECT sgede_cronograma_matricula.imp_saldo FROM sgede_cronograma_matricula
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
AND sgede_cronograma_matricula.num_refinanciamiento = 1
AND sgede_cronograma_matricula.num_cuota = 1), 0) AS imp_saldo_cuota1
FROM sgeca_matricula
INNER JOIN sgeca_programa ON sgeca_matricula.cod_programa = sgeca_programa.cod_programa
LEFT JOIN sgede_RP_programa ON sgeca_matricula.cod_periodo = sgede_RP_programa.cod_detalle
AND sgeca_matricula.cod_programa = sgede_RP_programa.cod_programa
AND sgeca_matricula.num_indice = sgede_RP_programa.num_indice
INNER JOIN sgema_alumno ON sgeca_matricula.cod_alumno = sgema_alumno.cod_alumno
WHERE sgeca_matricula.cod_localidad LIKE 'SCENT'
AND sgeca_matricula.fch_matricula BETWEEN '01-11-2024 00:00:00.000' AND '31-12-2026 23:59:00.000'
AND sgeca_matricula.cod_estado <> 'ANU'
"""
conn = self.sql_conn()
cur = conn.cursor()
cur.execute(sql)
cols = [d[0] for d in cur.description]
filas = [dict(zip(cols, row)) for row in cur.fetchall()]
conn.close()
return filas
# --- fin data_manager ---