commit e90dae89b68a3f59797af0a871e1c1c5d5be3473 Author: Panchito Date: Tue Aug 18 11:48:08 2026 -0500 Initial commit - dashboard leads diff --git a/FUTUROS_CAMBIOS.md b/FUTUROS_CAMBIOS.md new file mode 100644 index 0000000..4a4dd82 --- /dev/null +++ b/FUTUROS_CAMBIOS.md @@ -0,0 +1,173 @@ +# FUTUROS CAMBIOS — Dashboard LEADS + +Lista de cosas pendientes que el usuario quiere hacer más adelante. +Cuando el usuario pida "futuros cambios", recordarle TODO esto. + +--- + +## 1. Mapa de campañas (frases de pauta) → mover a GitHub o Supabase +- Hoy está incrustado en el código: `backend/data_manager_v2.py`, lista `CAMPANIAS` + (las ~90 frases con emojis + programa/sede/código/rango de fechas). +- Objetivo: sacarlo a un JSON en GitHub (o tabla en Supabase) para editarlo + sin tocar el código (igual que se hace en el otro dashboard con las clasificaciones). + +## 2. Leyenda de los códigos de pauta → a GitHub/Supabase +- Los códigos de campaña (191, 193, 63a, etc.) y su significado. +- Mismo objetivo: que sea editable fuera del código. + +## 3. Excluir cursos (num_indice) puntuales del cálculo +- Por ahora EXCLUIR del cálculo de Ocupabilidad/cursos: **num_indice 1154 y 1121**. +- Motivo: el usuario indicó que no deben contar (ej. reprogramados a otro mes). +- Idea a futuro: manejar esta lista de exclusión desde GitHub/Supabase, no en código. +- (Relacionado: el PBI filtra cursos por FECHA_MOSTRAR_COL = fecha con reprogramación + de SharePoint; aquí no tenemos esa columna todavía → Cursos Reprogramados quedó en fase 2.) + +## 4. Reprogramación de cursos (SharePoint) — FASE 2 [DEPENDE DE LA FUENTE] +La fecha ORIGINAL planificada de cada curso venía de SharePoint +(BaseBI_Programacion: FECHA CALENDARIO vs FECHA REPROGRAMADO). Sin esa fuente, +varios KPIs no se pueden calcular exacto. Pendiente: traerla de Google Sheets / Supabase / JSON +y construir FECHA_MOSTRAR_COL, FECHA_CUADRO_BI y ESTADO_CUADRO_BI. + +KPIs/cosas que dependen de esto (hoy aproximados o en 0): +- **Cursos Reprogramados** → hoy SALE 0 (no hay fecha original para comparar). +- **Cursos Programados** → hoy usa solo `fch_inicio` (el PBI usa planificada + reprogramada). + Diferencia chica, se cuadrará en fase 2. +- **Cursos Iniciados / Suspendidos** → hoy por `fch_inicio` y `cod_estado`; revisar con reprogramación. +- La exclusión manual de cursos (1154, 1121) es justo por reprogramación: cuando llegue + la fuente, esto debería resolverse solo (ya no haría falta la lista manual). + +## 5. Matriz por Curso (num_indice) con métricas de Leads por Pauta — NUEVA +El usuario quiere una MATRIZ debajo de la tabla por Sede, con: +- FILA: Fact_SQL_Base_Cursos[Personalizado], separada por cada num_indice (cursos se repiten). +- VALORES: Fecha_Inicio_Visual, Leads_Acumulados_Historico, Cant_Leads_Nuevos, + Cant_Leads_Nuevos_Con_Asesor, Cant_Leads_Nuevo_x_Mes, Cant_Leads_Nuevos_x_Mes_Con_Asesor. + +DEPENDE DE: el campo **Pauta** (código de campaña) de CADA curso, que en el PBI venía de +SharePoint (BaseBI_Programacion → PAUTA_CODIGO). El cruce de TODAS esas medidas es por +TREATAS(Cursos[Pauta], Leads[codigo]) — o sea por CÓDIGO, no por teléfono. + +Lo que se necesita para construirla (traer a Google Sheets / Supabase / JSON): +- Por cada num_indice (curso): su **código(s) de Pauta** asociado. +- Idealmente también FECHA_CUADRO_BI (fecha real/reprogramada) — mismo paquete que el punto 4. + +Medidas (ya tengo las fórmulas DAX exactas guardadas): +- Cant_Leads_Nuevos = leads únicos (Cantidad_Veces=1) con codigo = Pauta del curso, histórico. +- ..._Con_Asesor = + asesor no vacío. +- ..._x_Mes = igual pero respeta filtro de fecha del mes. +- ..._x_Mes_Con_Asesor = del mes + con asesor. +- Leads_Acumulados_Historico = suma de Cant_Leads_Nuevos de cursos del mismo Personalizado + con fecha <= la del curso actual. +- Fecha_Inicio_Visual = FECHA_CUADRO_BI del curso. + +ESTADO: ✅ La MATRIZ YA ESTÁ CONSTRUIDA en el dashboard (backend `leads_logic.matriz_cursos`, +frontend "Detalle por Curso"). Hoy muestra Programa + Fecha Inicio reales; las 5 columnas de +leads salen en 0 porque FALTA el código de Pauta por curso. Cuando llegue la fuente, solo se +rellena el cruce y se activan las 5 medidas. + +### Fuente SharePoint que hay que conectar (acordado) +Archivo: Base_BI_2.xlsx +Ruta: https://escuelarefrigeracion.sharepoint.com/sites/ASESORASCOMERCIALES/ + Documentos compartidos/2. BASE PROSPECTOS/BASE GENERAL/Patricia/Base_BI_2.xlsx +Tabla: BaseBI_Programacion +Columnas: NUM_INDICE, FECHA CALENDARIO, FECHA REPROGRAMADO, ESTADO, CONTAR, PAUTA_CODIGO +Cruce: Cursos[NUM_INDICE] → BaseBI_Programacion[NUM_INDICE] ⇒ PAUTA_CODIGO + (en el PBI: columna calculada Pauta = RELATED(BaseBI_Programacion[PAUTA_CODIGO])) + Luego: TREATAS(Cursos[Pauta], Leads[codigo]) para contar leads. + +Cómo leerla (3 opciones, de menos a más esfuerzo): +1) (Recomendado) Que Patricia/usuario suba a Supabase una tabla `cursos_pauta` + (num_indice, pauta_codigo, fecha_cuadro_bi). El backend ya tiene credenciales Supabase. + → cero auth de Microsoft, instantáneo, editable. +2) Exportar esa hoja a un Google Sheet / CSV en GitHub (GITHUB_BASE ya configurado). +3) Lectura directa SharePoint con Office365-REST-Python-Client (requiere usuario+clave M365 + o app registration). Más frágil; dejarlo como última opción. + +Cuando exista la fuente, en `data_manager_v2.py` agregar `traer_cursos_pauta()` y mapear +num_indice→pauta sobre los cursos; en `leads_logic.matriz_cursos` reemplazar los 0 por el +conteo de leads cruzado por codigo (las fórmulas DAX de arriba ya están claras). + +## 6. Base Junta (identificador de canal de origen por teléfono) — FUTURO +Objetivo: cuando un número se MATRICULA, saber su CANAL DE ORIGEN (y sede/programa/código) +para clasificarlo. Se usará como "memoria" de consulta por teléfono. + +Fuente/lógica (ya PROBADA y validada con el usuario): +- Une leads de POSTGRE (Chatwoot + mapa de campañas CAMPANIAS → canal/sede/programa/código) + + SUPABASE `datos_unificados` (Telefono, Fechacreada, Canal, Sede, Programa, Codigo). +- Teléfono NORMALIZADO: quita '+51', '+', espacios, '-', '(', ')'. +- DEDUP por teléfono → se queda con el MÁS ANTIGUO (solo por FECHA, sin hora). +- Desempate misma fecha: 1) PAUTA_WSP 2) PAUTA_WSP_FACE 3) orden alfabético del canal. +- Texto en MAYÚSCULAS; '-' de Supabase → vacío. +- Resultado prueba: ~45,054 teléfonos únicos. + +Script de prueba (standalone, corre en cualquier carpeta): `base_junta_full.py` +(genera base_junta_full.xlsx con 6 columnas: Telefono, Canal, Fecha Creada, Sede, +Programa, Codigo). NO está conectado al dashboard todavía. + +Cómo conectarlo a futuro (acordado, debe ser LIVIANO): +- Subir la base junta a una tabla en Supabase (ej. `base_junta`). +- El backend la carga UNA vez en un dict {telefono → datos}, cacheado (refresco ~15 min). +- Al cruzar una matrícula por teléfono → se obtiene su canal de origen y se clasifica. +- 44-73k filas en dict = instantáneo, sin impacto en la carga. + +ESTADO: pendiente. Cuando se retome: crear tabla Supabase + traer_base_junta() en backend ++ cruce por teléfono en el módulo que corresponda. + +## 7. Simplificar/normalizar nombres de programa (Personalizado) — MEJORA OPCIONAL +Los nombres largos de los cursos se acortan con una lista de reemplazos de texto +en `backend/leads_logic.py` → `_PERS_REEMPLAZOS` (hardcoded). + +Problemas/mejoras: +- Está en el código: cada nombre nuevo/largo hay que agregarlo a mano. +- Algunos reemplazos quedan SIN sede ni frecuencia consistente + (ej. "VOLUMEN VARIABLE VRF" quedó sin "SEDE -" ni "- FREC"). +- Ideal: formato uniforme tipo "SEDE - CORTO - FREC" para todos. + +Ideas (opcional, cuando haya tiempo): +1. Mover `_PERS_REEMPLAZOS` a un JSON en GitHub o tabla Supabase → editable sin tocar código. +2. Normalizar TODOS los nombres con un mismo criterio (sede + nombre corto + frecuencia), + no solo los VRF/CO2 ya hechos. + +ESTADO: opcional. No urgente; los reemplazos actuales funcionan. + +## 8. Manual de tablas de Supabase — PENDIENTE +Mantener UN solo documento que liste cada tabla de Supabase y para qué sirve +(evitar olvidarse). Ej.: +- `datos_unificados` → leads (proyecto ogzjtkxnfs...). +- `basebi_programacion` → pauta/estado/contar por num_indice (proyecto uztqscimts...). +- `cartera_junta` → cartera total combinada por teléfono + es_origen (proyecto uztqscimts...). +- (futuras: `programa_alias` → diccionario de normalización). +Cada vez que se cree una tabla nueva, agregarla aquí. + +## 9. Normalización de programa/sede en cartera_junta + alerta de valores nuevos — PENDIENTE +Problema: en `cartera_junta` hay valores repetidos con distinta escritura +(CÁMARAS/CAMARAS, GESTION/GESTIÓN, VRF / VRV vs VRF, SUP. DE OBRAS/SUPER.OBRAS/ +SUPERVISIÓN DE OBRAS, DIPLONADO REF typo, LINA→LIMA) y basura (vacío, "0"). +Como la tabla se regenera a diario (GitHub Actions), limpiar a mano NO sirve +(se sobrescribe). Hay que normalizar en la GENERACIÓN. + +Plan acordado (mínimo de tablas): +- En `carga_cartera.py`: (a) reglas automáticas → MAYÚSCULAS + sin acentos + sin + espacios extra (une CÁMARAS=CAMARAS, GESTIÓN=GESTION, etc. sin mantener nada); + (b) UNA tabla-diccionario `programa_alias` (y sede) en Supabase con + alias → valor_correcto para los sinónimos/typos que las reglas no resuelven. +- Detección de NUEVOS: NO usar tabla de pendientes. Calcular al vuelo en el + dashboard, comparando los valores únicos de cartera_junta vs el diccionario. +- Alerta: 🔔 campanita/panel de notificaciones en el dashboard. Silencioso si todo + es conocido; si aparece sede/programa nuevo no reconocido, se enciende con la + lista para que el usuario decida (unir como alias o dejar como nuevo real). +- Decidir aparte: valores vacíos y "0" → dejar o convertir a SIN PROGRAMA/SIN SEDE. + +## 10. Clasificar valores no identificados DESDE la campanita — MEJORA OPCIONAL +Hoy la campanita 🔔 solo AVISA de los programas/sedes no identificados; para +corregirlos hay que ir a Supabase y agregar la fila en `alias_normalizacion`. +Mejora: permitir hacerlo desde la misma web, en la notificación: +- Al lado de cada valor no identificado, un desplegable con los programas/sedes + YA EXISTENTES (canónicos) + botón "Asignar". +- Al asignar, el backend inserta la fila (alias → correcto) en `alias_normalizacion` + directamente desde la web (nuevo endpoint POST), sin entrar a Supabase. +- Así se limpia al instante y en la siguiente corrida del workflow ya sale unido. +ESTADO: opcional. Hoy funciona avisando; esto solo agiliza la corrección. + +--- + +(Agregar aquí cualquier otro pendiente que vaya saliendo.) diff --git a/INICIAR_LEADS.bat b/INICIAR_LEADS.bat new file mode 100644 index 0000000..d8581c8 --- /dev/null +++ b/INICIAR_LEADS.bat @@ -0,0 +1,13 @@ +@echo off +REM ============================================================ +REM INICIAR DASHBOARD LEADS - Backend (8001) + Frontend (5174) +REM ============================================================ +echo Iniciando BACKEND y FRONTEND de LEADS... + +start "LEADS - BACKEND" cmd /k ""%~dp0_run_backend.bat"" +start "LEADS - FRONTEND" cmd /k ""%~dp0_run_frontend.bat"" +start "LEADS - TIEMPOS" cmd /k ""%~dp0_run_tiempos.bat"" + +echo Listo. Se abrieron 3 ventanas (Backend, Frontend y Tiempos de carga). +echo Cuando cargue, abre el navegador en: http://localhost:5174 +timeout /t 4 >nul diff --git a/OPTIMIZACION_CACHE.md b/OPTIMIZACION_CACHE.md new file mode 100644 index 0000000..a9603b5 --- /dev/null +++ b/OPTIMIZACION_CACHE.md @@ -0,0 +1,78 @@ +# OPTIMIZACIÓN — Caché de descargas pesadas (reutilizable en Ventas, Comisiones, etc.) + +## El problema (síntoma) +Al cambiar un filtro (mes, sede, etc.) el dashboard se demora 15-130 segundos, +aunque los datos "ya deberían estar cargados". Se siente como si cada filtro +volviera a descargar todo desde cero. + +## La causa raíz +Una función que **descarga datos de una fuente externa** (Supabase, SQL Server, +Postgre, API) se llama SIN caché. Cada vez que el usuario filtra, esa función +**vuelve a descargar TODO** desde internet. + +En LEADS el culpable fue `traer_cartera_rows()` (bajaba 86,000 filas de Supabase +de 1000 en 1000 = 86 pedidos en serie ≈ 52-130s) y se re-descargaba en CADA +cambio de mes, porque `matriz_web_formulario` la llamaba directo: + +```python +# ANTES (malo): re-descarga en cada llamada +def _load(): + rows = get_dm().traer_cartera_rows() # baja 86k filas CADA vez + ... +``` + +## Cómo diagnosticarlo (script de tiempos) +Medir cada parte por separado para encontrar QUÉ tarda. Clave: medir un cambio +de mes CON los insumos ya cargados (2a vez). Si sigue lento, algo se re-descarga. + +```python +import time, services as S +def cron(label, fn): + t0 = time.time(); fn(); print(f" {label:38} {time.time()-t0:6.1f}s") + +cron("dashboard MES1 (1a vez)", lambda: S.mi_funcion("2026","1",...)) +cron("dashboard MES2 (2a vez)", lambda: S.mi_funcion("2026","2",...)) # <-- debe ser rapido +# Si MES2 sale lento, desglosar: medir cada traer_* y cada matriz por separado +# hasta ver cual linea se lleva los segundos (casi siempre un traer_* sin cache). +``` + +## La solución (2 líneas, NO toca lógica ni resultados) +Envolver la descarga pesada en `cache_get_or_set` con clave GLOBAL, y que TODAS +las funciones usen ese helper cacheado en vez de descargar directo. + +```python +# 1) Helper cacheado (se baja UNA vez, se reutiliza) +def _cartera_rows_cache(): + return cache_get_or_set("cartera_rows", ("GLOBAL",), + lambda: get_dm().traer_cartera_rows()) + +# 2) Reemplazar TODAS las llamadas directas: +# get_dm().traer_cartera_rows() -> _cartera_rows_cache() +``` + +Resultado en LEADS: +- Cambiar de mes: de 69-133s -> 0.4s (solo filtra lo que ya está en memoria) +- La descarga pesada solo se paga 1 vez (en la precarga de arranque, en background). + +## Regla general para CUALQUIER dashboard (Ventas, Comisiones, Rentabilidad...) +1. Toda función `traer_*` / consulta a SQL/Supabase/API que se use en varias + vistas o en cada filtro, debe estar cacheada con `cache_get_or_set`. +2. NUNCA llamar `get_dm().traer_xxx()` directo dentro de un `_load` que depende + de filtros; usar el helper cacheado. +3. El filtro (mes/sede/programa) debe operar sobre datos YA en memoria, no + re-descargar. Filtrar en memoria = milisegundos. +4. Precargar en segundo plano (hilo daemon al startup) el mes actual/anterior + para que el usuario no espere la 1a carga. +5. El refresco periódico (cada 15 min) invalida caché -> considerar bajar datos + que cambian 1 vez al día (ej. cartera) con menos frecuencia, o recargar en + background sin dejar hueco. + +## Mejora opcional pendiente (no aplicada aún) +La 1a descarga de la cartera es lenta (100-155s) porque pagina de 1000 en 1000. +Subir el tamaño de lote (ej. limit=10000) reduce los viajes: de ~155s a ~10-15s. +Aplica a cualquier tabla grande que se baje paginada. + +## Verificación (que no cambien números) +Antes de optimizar, guardar una "línea base" con los totales actuales +(diag_base.json). Tras optimizar, comparar: deben ser IDÉNTICOS. La optimización +solo cambia CUÁNDO/CÓMO se descarga, nunca el cálculo. diff --git a/PRUEBAS_EXPORT/.env b/PRUEBAS_EXPORT/.env new file mode 100644 index 0000000..5d367a1 --- /dev/null +++ b/PRUEBAS_EXPORT/.env @@ -0,0 +1,5 @@ +# Credenciales del Supabase donde vive la tabla cartera_junta +# (proyecto uztqscimtsihrzgybsyb — service_role) +CARTERA_URL=https://uztqscimtsihrzgybsyb.supabase.co +CARTERA_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV6dHFzY2ltdHNpaHJ6Z3lic3liIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2MTMyMzQ0MCwiZXhwIjoyMDc2ODk5NDQwfQ.pvqzwJ7fBxggNka3oga7SdhiADIgIVKGEuxGbzHQj0E +CARTERA_TABLA=cartera_junta diff --git a/PRUEBAS_EXPORT/cartera_junta.xlsx b/PRUEBAS_EXPORT/cartera_junta.xlsx new file mode 100644 index 0000000..6d15692 Binary files /dev/null and b/PRUEBAS_EXPORT/cartera_junta.xlsx differ diff --git a/PRUEBAS_EXPORT/export_cartera_junta.py b/PRUEBAS_EXPORT/export_cartera_junta.py new file mode 100644 index 0000000..22cb7db --- /dev/null +++ b/PRUEBAS_EXPORT/export_cartera_junta.py @@ -0,0 +1,107 @@ +# PRUEBAS_EXPORT/export_cartera_junta.py +""" +Script de PRUEBA e independiente del dashboard. +Exporta la tabla cartera_junta de Supabase a un Excel para revisarla. + +Autodetecta en cuál proyecto Supabase vive la tabla, probando las credenciales +que ya tienes configuradas (en el .env de esta carpeta y en el .env del backend). +Lee TODAS las filas paginando (Supabase devuelve máx. 1000 por consulta). + +Uso: py -3.12 export_cartera_junta.py +""" +import os +from dotenv import load_dotenv + +_AQUI = os.path.dirname(__file__) +# Carga el .env de esta carpeta y también el del backend (para reutilizar credenciales) +load_dotenv(os.path.join(_AQUI, ".env")) +load_dotenv(os.path.join(_AQUI, "..", "backend", ".env")) + +PAGINA = 1000 +TABLA = os.getenv("CARTERA_TABLA", "cartera_junta") + + +def _valida(url): + """Descarta URLs vacías o de plantilla (XXXX).""" + if not url: + return False + u = url.strip().lower() + return u.startswith("http") and "xxxx" not in u + + +def _candidatos(): + """Pares (nombre, url, key) a probar, en orden de prioridad.""" + pares = [ + ("CARTERA", os.getenv("CARTERA_URL"), os.getenv("CARTERA_KEY")), + ("SUPABASE", os.getenv("SUPABASE_URL"), os.getenv("SUPABASE_KEY")), + ("SUPABASE_PAUTA", os.getenv("SUPABASE_PAUTA_URL"),os.getenv("SUPABASE_PAUTA_KEY")), + ] + vistos, out = set(), [] + for nombre, url, key in pares: + if _valida(url) and key and (url, key) not in vistos: + vistos.add((url, key)) + out.append((nombre, url.strip(), key.strip())) + return out + + +def main(): + from supabase import create_client + + candidatos = _candidatos() + if not candidatos: + print("❌ No hay credenciales válidas. Pon CARTERA_URL/CARTERA_KEY en el .env de esta carpeta.") + return + + # Buscar en qué proyecto existe la tabla + sb = None + for nombre, url, key in candidatos: + try: + cli = create_client(url, key) + cli.table(TABLA).select("*").limit(1).execute() # prueba + sb = cli + print(f"✅ Tabla '{TABLA}' encontrada en el proyecto: {nombre} ({url})") + break + except Exception as e: + print(f" · {nombre}: no sirvió ({str(e)[:80]})") + + if sb is None: + print(f"\n❌ No encontré la tabla '{TABLA}' en ninguno de tus proyectos Supabase.") + print(" Copia CARTERA_URL y CARTERA_KEY (secrets de GitHub) al .env de esta carpeta.") + return + + # Traer TODAS las filas paginando de a 1000 + filas = [] + inicio = 0 + while True: + res = sb.table(TABLA).select("*").range(inicio, inicio + PAGINA - 1).execute() + lote = res.data or [] + if not lote: + break + filas.extend(lote) + print(f" leídas {len(filas)} filas...") + if len(lote) < PAGINA: + break + inicio += PAGINA + + if not filas: + print(f"⚠️ La tabla '{TABLA}' está vacía.") + return + + columnas = list(filas[0].keys()) + + import openpyxl + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "cartera_junta" + ws.append(columnas) + for r in filas: + ws.append([r.get(c, "") for c in columnas]) + + salida = os.path.join(_AQUI, "cartera_junta.xlsx") + wb.save(salida) + print(f"\n✅ Exportado: {salida}") + print(f" Filas: {len(filas)} | Columnas: {', '.join(columnas)}") + + +if __name__ == "__main__": + main() diff --git a/_run_backend.bat b/_run_backend.bat new file mode 100644 index 0000000..ecf8d40 --- /dev/null +++ b/_run_backend.bat @@ -0,0 +1,11 @@ +@echo off +REM Lanzador interno del BACKEND de LEADS +cd /d "%~dp0backend" +echo ==================================== +echo LEADS - BACKEND (FastAPI) puerto 8001 +echo Carpeta: %CD% +echo ==================================== +py -3.12 main.py +echo. +echo (El backend termino o fallo. Revisa los mensajes de arriba.) +pause diff --git a/_run_frontend.bat b/_run_frontend.bat new file mode 100644 index 0000000..3f7d2da --- /dev/null +++ b/_run_frontend.bat @@ -0,0 +1,12 @@ +@echo off +cd /d "%~dp0frontend" +echo ==================================== +echo LEADS - FRONTEND (Vite) puerto 5174 +echo Carpeta: %CD% +echo ==================================== +if not exist "node_modules\.bin\vite.cmd" call npm install +echo Iniciando servidor de desarrollo... +call npm run dev +echo. +echo (npm run dev termino o fallo.) +pause diff --git a/_run_tiempos.bat b/_run_tiempos.bat new file mode 100644 index 0000000..5f48b66 --- /dev/null +++ b/_run_tiempos.bat @@ -0,0 +1,21 @@ +@echo off +REM Ventana que muestra EN VIVO los tiempos de carga del backend de LEADS. +REM Lee el archivo backend\carga_tiempos.log que el backend va escribiendo. +title LEADS - TIEMPOS DE CARGA +cd /d "%~dp0backend" +echo ============================================================ +echo LEADS - TIEMPOS DE CARGA (en vivo) +echo Esperando que el backend empiece a cargar... +echo ============================================================ +echo. + +REM Espera a que exista el log (el backend lo crea al arrancar la precarga) +:esperar +if not exist "carga_tiempos.log" ( + timeout /t 1 >nul + goto esperar +) + +REM Muestra el log en vivo (se actualiza solo conforme el backend carga) +powershell -NoProfile -Command "Get-Content -Path 'carga_tiempos.log' -Wait -Encoding UTF8" +pause diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..6bf4899 --- /dev/null +++ b/backend/.env @@ -0,0 +1,22 @@ +PG_HOST=191.98.134.81 +PG_DATABASE=chatwoot_production +PG_USER=postgres +PG_PASSWORD=2165$%sd3%DFG +PG_PORT=5432 +SQL_SERVER=191.98.134.80 +SQL_DATABASE=BDUS_CK000040_0001 +SQL_USERNAME=ASEBASTIAN +SQL_PASSWORD=24MY36$z>&Uf +GITHUB_BASE=https://raw.githubusercontent.com/aron1798/prueba1/main/DASHBOARD_LEADS +SUPABASE_URL=https://ogzjtkxnfswpbmnbhnjd.supabase.co +SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9nemp0a3huZnN3cGJtbmJobmpkIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3NjYzNDk4MywiZXhwIjoyMDkyMjEwOTgzfQ.D7XB6GIs8UvI97lcMdf6Y6-8ON2ENhh0DOaiMro2f9Y +SUPABASE_TABLA_LEADS=datos_unificados +SUPABASE_PAUTA_URL=https://uztqscimtsihrzgybsyb.supabase.co +SUPABASE_PAUTA_KEY=sb_publishable_aJCh5J6UghfKmSz6Swy4iA_orAqeHb7 +SUPABASE_TABLA_PAUTA=basebi_programacion +CARTERA_URL=https://uztqscimtsihrzgybsyb.supabase.co +CARTERA_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV6dHFzY2ltdHNpaHJ6Z3lic3liIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2MTMyMzQ0MCwiZXhwIjoyMDc2ODk5NDQwfQ.pvqzwJ7fBxggNka3oga7SdhiADIgIVKGEuxGbzHQj0E +SUPABASE_TABLA_CARTERA=cartera_junta +SUPABASE_TABLA_ALIAS=alias_normalizacion +SUPABASE_TABLA_CONJUNTO=conjunto_pauta +META_CSV_URL=https://docs.google.com/spreadsheets/d/e/2PACX-1vQa5BFEeF-p0-6-FuEXDXT4eb_ZK0fkYgLMxK_Ly3EJJPwQBsYDfNsSW09ppMNrH2LIiwVdAnC5q64C/pub?gid=1241390845&single=true&output=csv \ No newline at end of file diff --git a/backend/ROAS_Marzo_2026.xlsx b/backend/ROAS_Marzo_2026.xlsx new file mode 100644 index 0000000..86dfeaf Binary files /dev/null and b/backend/ROAS_Marzo_2026.xlsx differ diff --git a/backend/__pycache__/cache_manager.cpython-310.pyc b/backend/__pycache__/cache_manager.cpython-310.pyc new file mode 100644 index 0000000..6fdbcb0 Binary files /dev/null and b/backend/__pycache__/cache_manager.cpython-310.pyc differ diff --git a/backend/__pycache__/cache_manager.cpython-312.pyc b/backend/__pycache__/cache_manager.cpython-312.pyc new file mode 100644 index 0000000..7f2de5c Binary files /dev/null and b/backend/__pycache__/cache_manager.cpython-312.pyc differ diff --git a/backend/__pycache__/cache_manager.cpython-314.pyc b/backend/__pycache__/cache_manager.cpython-314.pyc new file mode 100644 index 0000000..622f81b Binary files /dev/null and b/backend/__pycache__/cache_manager.cpython-314.pyc differ diff --git a/backend/__pycache__/data_manager.cpython-310.pyc b/backend/__pycache__/data_manager.cpython-310.pyc new file mode 100644 index 0000000..c521459 Binary files /dev/null and b/backend/__pycache__/data_manager.cpython-310.pyc differ diff --git a/backend/__pycache__/data_manager.cpython-312.pyc b/backend/__pycache__/data_manager.cpython-312.pyc new file mode 100644 index 0000000..af0df5b Binary files /dev/null and b/backend/__pycache__/data_manager.cpython-312.pyc differ diff --git a/backend/__pycache__/data_manager_v2.cpython-310.pyc b/backend/__pycache__/data_manager_v2.cpython-310.pyc new file mode 100644 index 0000000..30031f7 Binary files /dev/null and b/backend/__pycache__/data_manager_v2.cpython-310.pyc differ diff --git a/backend/__pycache__/data_manager_v2.cpython-312.pyc b/backend/__pycache__/data_manager_v2.cpython-312.pyc new file mode 100644 index 0000000..5cc65c4 Binary files /dev/null and b/backend/__pycache__/data_manager_v2.cpython-312.pyc differ diff --git a/backend/__pycache__/data_manager_v2.cpython-314.pyc b/backend/__pycache__/data_manager_v2.cpython-314.pyc new file mode 100644 index 0000000..a199e69 Binary files /dev/null and b/backend/__pycache__/data_manager_v2.cpython-314.pyc differ diff --git a/backend/__pycache__/leads_logic.cpython-310.pyc b/backend/__pycache__/leads_logic.cpython-310.pyc new file mode 100644 index 0000000..ffeef2d Binary files /dev/null and b/backend/__pycache__/leads_logic.cpython-310.pyc differ diff --git a/backend/__pycache__/leads_logic.cpython-312.pyc b/backend/__pycache__/leads_logic.cpython-312.pyc new file mode 100644 index 0000000..0340a0f Binary files /dev/null and b/backend/__pycache__/leads_logic.cpython-312.pyc differ diff --git a/backend/__pycache__/leads_logic.cpython-314.pyc b/backend/__pycache__/leads_logic.cpython-314.pyc new file mode 100644 index 0000000..eb55b49 Binary files /dev/null and b/backend/__pycache__/leads_logic.cpython-314.pyc differ diff --git a/backend/__pycache__/main.cpython-310.pyc b/backend/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..ce93ffe Binary files /dev/null and b/backend/__pycache__/main.cpython-310.pyc differ diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..82c30e2 Binary files /dev/null and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/__pycache__/services.cpython-310.pyc b/backend/__pycache__/services.cpython-310.pyc new file mode 100644 index 0000000..a275681 Binary files /dev/null and b/backend/__pycache__/services.cpython-310.pyc differ diff --git a/backend/__pycache__/services.cpython-312.pyc b/backend/__pycache__/services.cpython-312.pyc new file mode 100644 index 0000000..a1fb3fa Binary files /dev/null and b/backend/__pycache__/services.cpython-312.pyc differ diff --git a/backend/__pycache__/services.cpython-314.pyc b/backend/__pycache__/services.cpython-314.pyc new file mode 100644 index 0000000..01d9ac2 Binary files /dev/null and b/backend/__pycache__/services.cpython-314.pyc differ diff --git a/backend/bajar_cartera.py b/backend/bajar_cartera.py new file mode 100644 index 0000000..84214fa --- /dev/null +++ b/backend/bajar_cartera.py @@ -0,0 +1,49 @@ +import os, csv +import requests +from dotenv import load_dotenv +from collections import Counter + +load_dotenv() + +url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") +key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") +t = os.getenv("SUPABASE_TABLA_CARTERA", "cartera_junta") + +assert url and key, "Falta CARTERA_URL / CARTERA_KEY en .env" + +sel = "telefono,sede,programa,fecha_creada,asesor,es_origen,canal,origen_base" +paso, desde = 1000, 0 +out = [] +while True: + hdr = {"apikey": key, "Authorization": f"Bearer {key}", + "Range-Unit": "items", "Range": f"{desde}-{desde + paso - 1}"} + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": sel, "order": "id.asc"}, + headers=hdr, timeout=60) + r.raise_for_status() + data = r.json() + if not data: + break # parar SOLO cuando devuelve 0 + out.extend(data) + desde += len(data) + print(f" descargadas: {desde}", end="\r") + +print(f"\n\nTOTAL filas descargadas: {len(out)}") + +ori = Counter(str(x.get("es_origen") or "").strip().upper() for x in out) +print("es_origen:", ori.most_common(5)) + +ob = Counter(str(x.get("origen_base") or "").strip().upper() for x in out) +print("origen_base:", ob.most_common(5)) + +sede = Counter(str(x.get("sede") or "").strip().upper() for x in out) +print("por SEDE:", sede.most_common(10)) + +# Exporta a CSV para comparar en Excel +salida = "cartera_bajada.csv" +with open(salida, "w", newline="", encoding="utf-8-sig") as f: + w = csv.DictWriter(f, fieldnames=sel.split(",")) + w.writeheader() + for x in out: + w.writerow({k: x.get(k, "") for k in sel.split(",")}) +print(f"\nExportado a: {salida}") diff --git a/backend/bajar_datos_unificados.py b/backend/bajar_datos_unificados.py new file mode 100644 index 0000000..a195f6a --- /dev/null +++ b/backend/bajar_datos_unificados.py @@ -0,0 +1,64 @@ +import os, csv +import requests +from dotenv import load_dotenv +from collections import Counter + +load_dotenv() + +# datos_unificados vive en el proyecto SUPABASE_URL2 / SUPABASE_KEY2 (el del sync) +url = (os.getenv("SUPABASE_URL2") or os.getenv("SUPABASE_URL") + or os.getenv("CARTERA_URL") or "") +key = (os.getenv("SUPABASE_KEY2") or os.getenv("SUPABASE_KEY") + or os.getenv("CARTERA_KEY") or "") +t = "datos_unificados" + +assert url and key, "Falta SUPABASE_URL2 / SUPABASE_KEY2 (o SUPABASE_URL/KEY) en .env" + +sel = "Ejecutivo,Telefono,Fechacreada,Sede,Programa,Canal,Codigo,Codigo_Vendedor" +paso, desde = 1000, 0 +out = [] +while True: + hdr = {"apikey": key, "Authorization": f"Bearer {key}", + "Range-Unit": "items", "Range": f"{desde}-{desde + paso - 1}"} + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": sel, "order": "id.asc"}, + headers=hdr, timeout=60) + r.raise_for_status() + data = r.json() + if not data: + break + out.extend(data) + desde += len(data) + print(f" descargadas: {desde}", end="\r") + +print(f"\n\nTOTAL filas en datos_unificados: {len(out)}") + +def _tel_vacio(v): + s = str(v or "") + for x in ("+51", "+", " ", "-", "(", ")", ".0"): + s = s.replace(x, "") + s = s.strip() + if not s or s in ("-", "nan") or set(s) == {"0"}: + return True + return s.isdigit() and len(s) <= 6 + +vacios = sum(1 for x in out if _tel_vacio(x.get("Telefono"))) +print(f"Filas con teléfono VÁLIDO: {len(out) - vacios}") +print(f"Filas BASURA (sin teléfono): {vacios}") + +# desglose por sede (para ver de qué Excel viene la basura) +sede_basura = Counter() +for x in out: + if _tel_vacio(x.get("Telefono")): + sede_basura[str(x.get("Sede") or "").strip().upper()] += 1 +print("\nBASURA por SEDE:") +for s, n in sede_basura.most_common(12): + print(f" {s if s else '(vacio)':16} {n}") + +salida = "datos_unificados.csv" +with open(salida, "w", newline="", encoding="utf-8-sig") as f: + w = csv.DictWriter(f, fieldnames=sel.split(",")) + w.writeheader() + for x in out: + w.writerow({k: x.get(k, "") for k in sel.split(",")}) +print(f"\nExportado a: {salida}") diff --git a/backend/borrar_branding.py b/backend/borrar_branding.py new file mode 100644 index 0000000..02c4e29 --- /dev/null +++ b/backend/borrar_branding.py @@ -0,0 +1,44 @@ +# borrar_branding.py +# Pone en BLANCO el campo 'programa' de todos los conjuntos que tienen BRANDING +# en la tabla conjunto_sede_programa de Supabase. La SEDE se conserva. +# Ejecutar en backend/: python borrar_branding.py +import os, requests +from dotenv import load_dotenv +load_dotenv() + +URL = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") +KEY = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") +T = "conjunto_sede_programa" +H = {"apikey": KEY, "Authorization": f"Bearer {KEY}"} +assert URL and KEY, "Falta CARTERA_URL/KEY en .env" + +# 1) traer todos y ver cuales tienen BRANDING +r = requests.get(f"{URL}/rest/v1/{T}", params={"select": "*"}, headers=H, timeout=30) +r.raise_for_status() +filas = r.json() +brand = [f for f in filas if "BRANDING" in str(f.get("programa") or "").upper()] + +print(f"\nTotal filas: {len(filas)}") +print(f"Con BRANDING en programa: {len(brand)}") +for f in brand[:60]: + print(f" {f.get('conjunto'):45} sede={f.get('sede'):10} prog={f.get('programa')}") + +if not brand: + print("\nNo hay nada con BRANDING. Listo.") + raise SystemExit + +# 2) actualizar programa='' donde el programa contenga BRANDING +hp = dict(H); hp["Content-Type"] = "application/json"; hp["Prefer"] = "return=minimal" +ok = 0 +for f in brand: + co = f.get("conjunto") + rr = requests.patch(f"{URL}/rest/v1/{T}", + params={"conjunto": f"eq.{co}"}, + headers=hp, json={"programa": ""}, timeout=30) + if rr.ok: + ok += 1 + else: + print(f" ERROR {co}: {rr.status_code} {rr.text[:80]}") + +print(f"\nOK -> {ok} conjuntos actualizados (programa BRANDING borrado, sede conservada).") +print("Ahora en el dashboard: reinicia backend o dale 'Actualizar'.") diff --git a/backend/cache_manager.py b/backend/cache_manager.py new file mode 100644 index 0000000..9f63369 --- /dev/null +++ b/backend/cache_manager.py @@ -0,0 +1,79 @@ +# backend/cache_manager.py +"""Caché global en memoria con refresco en segundo plano (igual que el dashboard de ventas).""" +import time +import threading +from typing import Any, Callable, Dict, Tuple + +_CACHE: Dict[str, Tuple[float, Any]] = {} +_LOCK = threading.Lock() +_DEFAULT_TTL = 900 # 15 min + + +def _make_key(prefix: str, args: tuple) -> str: + return prefix + ":" + "|".join(str(a) for a in args) + + +def cache_get_or_set(prefix: str, args: tuple, loader: Callable[[], Any], ttl: int = _DEFAULT_TTL) -> Any: + key = _make_key(prefix, args) + now = time.time() + with _LOCK: + hit = _CACHE.get(key) + if hit and (now - hit[0] < ttl): + return hit[1] + value = loader() + with _LOCK: + _CACHE[key] = (now, value) + return value + + +def cache_keys(): + with _LOCK: + return list(_CACHE.keys()) + + +def cache_refresh_existing(loader_for_key): + for key in cache_keys(): + try: + nuevo = loader_for_key(key) + if nuevo is not None: + with _LOCK: + _CACHE[key] = (time.time(), nuevo) + except Exception as e: + print(f"[cache refresh] {key}: {e}") + + +def cache_invalidate(prefix: str = None) -> None: + with _LOCK: + if prefix is None: + _CACHE.clear() + else: + for k in list(_CACHE.keys()): + if k.startswith(prefix + ":"): + del _CACHE[k] + + +def cache_stats() -> dict: + with _LOCK: + return {"entradas": len(_CACHE), "claves": list(_CACHE.keys())} + + +_background_started = False + + +def start_background_refresh(refresh_fn: Callable[[], None], interval: int = 900): + global _background_started + if _background_started: + return + _background_started = True + + def _loop(): + while True: + time.sleep(interval) + try: + refresh_fn() + except Exception as e: + print(f"[background refresh] error: {e}") + + t = threading.Thread(target=_loop, daemon=True) + t.start() + print(f"[cache] refresco en segundo plano cada {interval}s iniciado") diff --git a/backend/carga_tiempos.log b/backend/carga_tiempos.log new file mode 100644 index 0000000..f80e456 --- /dev/null +++ b/backend/carga_tiempos.log @@ -0,0 +1,30 @@ +============================================================ +>>> Hilo 2: CARTERA (Supabase) + OTROS GENERAL + CARGA DEL DASHBOARD LEADS — 17/08/2026 16:57:12 +============================================================ +>>> Hilo 1: LEADS (PostgreSQL + SQL Server) + [carga] Plantillas Google Sheet 1.21s (9117 filas) + [carga] Leads (mensajes) PostgreSQL 21.85s (58986 filas) + [carga] Cursos SQL Server 0.00s (289 filas) + [carga] Cartera Supabase 20.88s (106389 filas) + [carga] Matriculas SQL Server 0.34s (3267 filas) + [carga] Leads asignados/respuesta PostgreSQL 4.18s (15885 filas) + [carga] Otros General 2026-8 cache 5.65s (4 filas) + [carga] Otros General 2026-7 cache 0.30s (4 filas) +>>> Hilo 2 (Cartera/Otros) LISTO. + [carga] Apartado Leads 2026-8 cache 4.06s (6 filas) + [carga] Apartado Leads 2026-7 cache 0.38s (6 filas) + [carga] Apartado Vendedores cache 2.47s (13 filas) + [carga] Apartado ROAS cache 0.00s (2 filas) +>>> Hilo 1 (Leads) LISTO. + +============================================================ + RESUMEN — tiempo listo para abrir cada apartado +============================================================ + Leads 4.45s -> listo tras 4.4s + Vendedores 2.47s -> listo tras 2.5s + Otros General 5.96s -> listo tras 6.0s + ROAS 0.00s -> instantaneo al abrir +------------------------------------------------------------ + Ya precargados: al hacer clic se muestran AL INSTANTE (cache). +============================================================ diff --git a/backend/check_choque_campania.py b/backend/check_choque_campania.py new file mode 100644 index 0000000..75ae5f4 --- /dev/null +++ b/backend/check_choque_campania.py @@ -0,0 +1,95 @@ +# check_choque_campania.py +# Revisa si una campaña NUEVA chocaría (duplicaría leads) contra las existentes. +# Regla de choque (igual que el JOIN LIKE del dashboard): +# - una frase es sub-cadena de la otra (contención de texto), Y +# - los rangos de fecha se solapan. +# Un mensaje que caiga en ambas frases + ambas fechas => 2 filas => lead duplicado. +# +# Edita NUEVA abajo y ejecuta: python check_choque_campania.py +import os, requests +from datetime import date +from dotenv import load_dotenv +load_dotenv() + +# ── CAMPAÑA NUEVA A PROBAR (edita aquí) ── +NUEVA = { + "frase": "🎟️ Me interesa el programa de Aire Acondicionado", + "inicio": "2026-08-10", + "fin": "2026-10-10", +} +# ───────────────────────────────────────── + +URL = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") +KEY = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") +TABLA = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") +HEAD = {"apikey": KEY, "Authorization": f"Bearer {KEY}"} +assert URL and KEY, "Falta CARTERA_URL / CARTERA_KEY en .env" + + +def traer_todo(): + filas, paso, desde = [], 1000, 0 + while True: + hdr = dict(HEAD); hdr["Range-Unit"] = "items"; hdr["Range"] = f"{desde}-{desde+paso-1}" + r = requests.get(f"{URL}/rest/v1/{TABLA}", params={"select": "*", "order": "id"}, + headers=hdr, timeout=60) + r.raise_for_status() + data = r.json() + if not data: + break + filas.extend(data); desde += len(data) + if len(data) < paso: + break + return filas + + +def _d(s): + try: + return date.fromisoformat(str(s)[:10]) + except Exception: + return None + + +def _solapan(i1, f1, i2, f2): + if not (i1 and f1 and i2 and f2): + return False + return i1 <= f2 and i2 <= f1 # rangos [i1,f1] y [i2,f2] se cruzan + + +def _norm(t): + return " ".join(str(t or "").lower().split()) + + +filas = traer_todo() +ni, nf = _norm(NUEVA["frase"]), None +n_ini, n_fin = _d(NUEVA["inicio"]), _d(NUEVA["fin"]) + +print("\n" + "="*90) +print(f"CAMPAÑA NUEVA: \"{NUEVA['frase']}\"") +print(f"FECHAS: {NUEVA['inicio']} → {NUEVA['fin']}") +print("="*90) + +choques = [] +for f in filas: + fr = _norm(f.get("frase_busqueda")) + if not fr: + continue + ei, ef = _d(f.get("fecha_inicio")), _d(f.get("fecha_fin")) + contiene = (fr in ni) or (ni in fr) # una es sub-cadena de la otra + fechas = _solapan(n_ini, n_fin, ei, ef) + if contiene and fechas: + choques.append((f, fr)) + +if not choques: + print("\n✅ SIN CHOQUES. Ninguna campaña existente comparte texto contenido") + print(" y fechas solapadas. Tu campaña NO duplicaría leads.\n") +else: + print(f"\n⚠️ {len(choques)} CHOQUE(S) DETECTADO(S) — estas campañas duplicarían leads:\n") + for f, fr in choques: + rel = "existente ⊂ nueva" if fr in ni else "nueva ⊂ existente" + print(f" id {f.get('id')} cod {f.get('codigo')} [{rel}]") + print(f" frase: \"{f.get('frase_busqueda')}\"") + print(f" fechas: {str(f.get('fecha_inicio'))[:10]} → {str(f.get('fecha_fin'))[:10]}") + print() + print(" Un mensaje que contenga ambas frases dentro de estas fechas se") + print(" contaría 2 veces (2 códigos de pauta para el mismo lead).\n") +print("="*90) diff --git a/backend/data_manager.py b/backend/data_manager.py new file mode 100644 index 0000000..cbc7fb9 --- /dev/null +++ b/backend/data_manager.py @@ -0,0 +1,271 @@ +# 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 --- diff --git a/backend/data_manager_v2.py b/backend/data_manager_v2.py new file mode 100644 index 0000000..583e180 --- /dev/null +++ b/backend/data_manager_v2.py @@ -0,0 +1,1040 @@ +# backend/data_manager.py (v2 - reescrito completo) +"""Capa de acceso a datos del módulo LEADS.""" +import os +import time +import requests +from dotenv import load_dotenv + +load_dotenv() + +# ── Mapa de campañas (frase -> programa/sede/rango fechas), igual que el PBI ── +CAMPANIAS = [ + ('📚 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'), + ('📱 Estoy interesado Instalación', 'VRF', '81a', 'Lima', '-', 'Pauta_wsp', '2026-06-25', '2026-08-30'), + ('🧊 Más información del Seminario', 'VRF', '82a', 'Arequipa', '-', 'Pauta_wsp', '2026-07-01', '2026-08-30'), + ('📐 Más información del Seminario', 'VRF', '83a', 'Trujillo', '-', 'Pauta_wsp', '2026-07-01', '2026-08-30'), + ('👾 Más información del Seminario', 'VRF', '84a', 'Piura', '-', 'Pauta_wsp', '2026-07-01', '2026-08-30'), + ('🛠️ más info del Seminario Virtual ', 'Diseño de Chillers', '85a', 'Lima', '-', 'Pauta_wsp', '2026-07-01', '2026-09-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'), +] + + +# Cache en memoria de las campañas leídas de Supabase (expira cada 15 min, +# igual que el dashboard de ventas). Así las campañas nuevas aparecen solas +# sin reiniciar el servidor. +_CAMPANIAS_CACHE = None +_CAMPANIAS_TS = 0.0 +_CAMPANIAS_TTL = 900 # 15 min + + +def _campanias_supabase(): + """Lee las campañas desde la tabla 'campanias' de Supabase (misma fuente que + la cartera). Devuelve lista de tuplas (frase,cargo,codigo,sede,dia,origen,fi,ff). + Si falla o está vacía, cae a la lista hardcodeada CAMPANIAS como respaldo. + El cache expira cada 15 min para tomar campañas nuevas sin reiniciar.""" + global _CAMPANIAS_CACHE, _CAMPANIAS_TS + if _CAMPANIAS_CACHE is not None and (time.time() - _CAMPANIAS_TS < _CAMPANIAS_TTL): + return _CAMPANIAS_CACHE + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") + out = [] + if url and key: + try: + r = requests.get( + f"{url}/rest/v1/{t}", + params={"select": "frase_busqueda,cargo,codigo,sede,dia,origen,fecha_inicio,fecha_fin"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30) + r.raise_for_status() + for row in r.json(): + frase = str(row.get("frase_busqueda") or "") + if not frase: + continue + out.append(( + frase, str(row.get("cargo") or ""), str(row.get("codigo") or ""), + str(row.get("sede") or ""), str(row.get("dia") or ""), + str(row.get("origen") or ""), + str(row.get("fecha_inicio") or "")[:10], + str(row.get("fecha_fin") or "")[:10], + )) + except Exception as e: + print(f"[_campanias_supabase] {e}") + if not out: + print("[_campanias_supabase] sin datos de Supabase -> uso lista hardcodeada") + out = list(CAMPANIAS) + _CAMPANIAS_CACHE = out + _CAMPANIAS_TS = time.time() + return out + + +def invalidar_campanias_cache(): + """Vacía el cache de campañas para forzar re-lectura de Supabase en la + próxima llamada. Se invoca desde refrescar_todo() cada 15 min, para que + las campañas se refresquen junto con el resto del dashboard.""" + global _CAMPANIAS_CACHE, _CAMPANIAS_TS + _CAMPANIAS_CACHE = None + _CAMPANIAS_TS = 0.0 + + +def _values_sql(): + """Construye el bloque VALUES (...) del mapa de campañas para el JOIN. + Lee las campañas desde la tabla 'campanias' de Supabase (ya no hardcodeadas).""" + filas = [] + for frase, cargo, codigo, sede, dia, origen, fi, ff in _campanias_supabase(): + f = frase.replace("'", "''") + filas.append(f"('{f}','{cargo}','{codigo}','{sede}','{dia}','{origen}','{fi}','{ff}')") + return ",\n".join(filas) + + +class DataManager: + def __init__(self): + 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") + 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", "") + + 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) + + # ── CHATWOOT: leads de pauta (mapa de campañas, dedup por mensaje) ── + def traer_leads_chatwoot(self): + sql = f""" + SELECT DISTINCT ON (m.id) + -- Normalización IGUAL que el PBI: quitar '+51' y '+', quedando el número + REPLACE(REPLACE(c.phone_number, '+51', ''), '+', '') AS telefono, + u.name AS asesor, + m.created_at 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 +{_values_sql()} + ) 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 + """ + conn = self.pg_conn() + try: + cur = conn.cursor() + cur.execute(sql) + cols = [d[0] for d in cur.description] + filas = [dict(zip(cols, row)) for row in cur.fetchall()] + finally: + conn.close() + return filas + + # ── SQL SERVER: 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() + try: + cur = conn.cursor() + cur.execute(sql) + cols = [d[0] for d in cur.description] + filas = [dict(zip(cols, row)) for row in cur.fetchall()] + finally: + conn.close() + return filas + + # ── SUPABASE: mapa num_indice → pauta (tabla basebi_programacion) ── + def traer_pauta_cursos(self): + """Devuelve dict {str(num_indice): {pauta, estado, contar}} desde Supabase. + Fuente: tabla BaseBI_Programacion de SharePoint, subida a Supabase.""" + url = os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("SUPABASE_PAUTA_KEY", "") + tabla = os.getenv("SUPABASE_TABLA_PAUTA", "basebi_programacion") + if not url or not key: + return {} + try: + r = requests.get( + f"{url}/rest/v1/{tabla}", + params={"select": "num_indice,pauta_codigo,estado,contar"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, + timeout=30, + ) + r.raise_for_status() + mapa = {} + for row in r.json(): + ni = row.get("num_indice") + if ni is None: + continue + pa = row.get("pauta_codigo") + mapa[str(ni)] = { + "pauta": (str(pa).strip() if pa not in (None, "") else None), + "estado": (str(row.get("estado") or "").strip().upper()), + "contar": (str(row.get("contar") or "").strip().upper()), + } + return mapa + except Exception as e: + print(f"[traer_pauta_cursos] {e}") + return {} + + # ── SUPABASE: valores de programa/sede no identificados (para la campanita) ── + def traer_no_identificados(self): + """Compara cartera_junta vs alias_normalizacion y devuelve los valores de + programa/sede que NO están en el diccionario (correcto). + Retorna {'programa': [{'valor','veces'}], 'sede': [...]}.""" + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t_cart = os.getenv("SUPABASE_TABLA_CARTERA", "cartera_junta") + t_alias = os.getenv("SUPABASE_TABLA_ALIAS", "alias_normalizacion") + vacio = {"programa": [], "sede": []} + if not url or not key: + return vacio + headers = {"apikey": key, "Authorization": f"Bearer {key}"} + try: + # 1) valores conocidos (correcto) por tipo + r = requests.get(f"{url}/rest/v1/{t_alias}", + params={"select": "tipo,correcto"}, headers=headers, timeout=30) + r.raise_for_status() + conocidos = {"PROGRAMA": set(), "SEDE": set()} + for row in r.json(): + tipo = str(row.get("tipo") or "").upper() + corr = " ".join(str(row.get("correcto") or "").upper().split()) + if tipo in conocidos: + conocidos[tipo].add(corr) + + # 2) traer de cartera_junta los valores que NO estén en los conocidos + def _desconocidos(columna, tipo): + conj = conocidos.get(tipo, set()) + if not conj: + return [] + lista = ",".join('"' + v.replace('"', '') + '"' for v in conj) + rr = requests.get( + f"{url}/rest/v1/{t_cart}", + params={"select": columna, columna: f"not.in.({lista})", "limit": "5000"}, + headers=headers, timeout=30) + rr.raise_for_status() + cont = {} + for row in rr.json(): + v = " ".join(str(row.get(columna) or "").upper().split()) + cont[v] = cont.get(v, 0) + 1 + return [{"valor": k, "veces": n} + for k, n in sorted(cont.items(), key=lambda x: -x[1])] + + return {"programa": _desconocidos("programa", "PROGRAMA"), + "sede": _desconocidos("sede", "SEDE")} + except Exception as e: + print(f"[traer_no_identificados] {e}") + return vacio + + # ── SUPABASE: diccionario alias (para normalizar campanias como la cartera) ── + def traer_alias_map(self): + """{'PROGRAMA': {alias:correcto}, 'SEDE': {alias:correcto}}.""" + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_ALIAS", "alias_normalizacion") + out = {"PROGRAMA": {}, "SEDE": {}} + if not url or not key: + return out + try: + r = requests.get(f"{url}/rest/v1/{t}", params={"select": "tipo,alias,correcto"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30) + r.raise_for_status() + for row in r.json(): + tipo = str(row.get("tipo") or "").upper() + al = " ".join(str(row.get("alias") or "").upper().split()) + co = str(row.get("correcto") or "") + if tipo in out: + out[tipo][al] = co + except Exception as e: + print(f"[traer_alias_map] {e}") + return out + + # ── SUPABASE: asesores -> {alias_norm: correcto} y lista de correctos (los vendedores) ── + def traer_asesores(self): + """Devuelve {'map': {alias_norm: correcto}, 'lista': [correctos ordenados]} + de tipo ASESOR (solo con correcto no vacío).""" + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_ALIAS", "alias_normalizacion") + mapa = {}; correctos = set() + if not url or not key: + return {"map": mapa, "lista": []} + try: + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": "alias,correcto", "tipo": "eq.ASESOR"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30) + r.raise_for_status() + for row in r.json(): + al = " ".join(str(row.get("alias") or "").upper().split()) + co = str(row.get("correcto") or "").strip() + if co: + mapa[al] = co + correctos.add(co) + # el propio correcto tambien es su alias + mapa[" ".join(co.upper().split())] = co + except Exception as e: + print(f"[traer_asesores] {e}") + return {"map": mapa, "lista": sorted(correctos)} + + # ── SUPABASE: mapa codigo → (sede, programa) desde campanias (normalizado) ── + def traer_campanias_map(self): + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") + out = {} + if not url or not key: + return out + alias = self.traer_alias_map() + + def _norm(v, tipo): + s = " ".join(str(v or "").upper().split()) + return alias.get(tipo, {}).get(s, s) + + try: + r = requests.get(f"{url}/rest/v1/{t}", params={"select": "codigo,sede,cargo"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30) + r.raise_for_status() + for row in r.json(): + cod = " ".join(str(row.get("codigo") or "").upper().split()) + if not cod: + continue + out[cod] = (_norm(row.get("sede"), "SEDE"), _norm(row.get("cargo"), "PROGRAMA")) + except Exception as e: + print(f"[traer_campanias_map] {e}") + return out + + # ── SUPABASE: filas de cartera_junta para contar (telefono, sede, programa, fecha) ── + def traer_cartera_rows(self): + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CARTERA", "cartera_junta") + out = [] + if not url or not key: + return out + paso, desde = 1000, 0 + sel = "telefono,sede,programa,fecha_creada,asesor,es_origen,canal,origen_base" + try: + while True: + hdr = {"apikey": key, "Authorization": f"Bearer {key}", + "Range-Unit": "items", "Range": f"{desde}-{desde + paso - 1}"} + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": sel}, + headers=hdr, timeout=60) + r.raise_for_status() + data = r.json() + if not data: + break + out.extend(data) + desde += len(data) # avanza por lo realmente recibido (Supabase puede topar el bloque) + if len(data) < paso: + break + except Exception as e: + print(f"[traer_cartera_rows] {e}") + return out + + # ── SUPABASE: mapa pauta → [conjuntos de anuncios] (tabla conjunto_pauta) ── + def traer_conjunto_pauta(self): + """Devuelve dict {pauta(str): [conjunto, ...]} desde la tabla conjunto_pauta. + Una pauta puede tener varios conjuntos de anuncios.""" + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CONJUNTO", "conjunto_pauta") + out = {} + if not url or not key: + return out + try: + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": "conjunto,pauta"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, + timeout=30) + r.raise_for_status() + for row in r.json(): + pa = str(row.get("pauta") or "").strip() + co = " ".join(str(row.get("conjunto") or "").split()) + if not pa or not co: + continue + out.setdefault(pa, []).append(co) + except Exception as e: + print(f"[traer_conjunto_pauta] {e}") + return out + + # ── SUPABASE: asignacion manual conjunto -> sede + programa (tabla conjunto_sede_programa) ── + def traer_conjunto_sede_programa(self): + """Devuelve {conjunto_UPPER: {"sede":..., "programa":...}} desde la tabla.""" + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = "conjunto_sede_programa" + out = {} + if not url or not key: + return out + try: + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": "conjunto,sede,programa"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, + timeout=30) + r.raise_for_status() + for row in r.json(): + co = " ".join(str(row.get("conjunto") or "").split()) + if not co: + continue + out[co.upper()] = { + "sede": " ".join(str(row.get("sede") or "").upper().split()), + "programa": " ".join(str(row.get("programa") or "").upper().split()), + } + except Exception as e: + print(f"[traer_conjunto_sede_programa] {e}") + return out + + def guardar_conjunto_sede_programa(self, cambios): + """Upsert de asignaciones. cambios = [{conjunto, sede, programa}, ...].""" + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = "conjunto_sede_programa" + if not url or not key or not cambios: + return {"guardados": 0} + headers = {"apikey": key, "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + "Prefer": "resolution=merge-duplicates,return=minimal"} + payload = [] + for c in cambios: + co = " ".join(str(c.get("conjunto") or "").split()) + if not co: + continue + payload.append({ + "conjunto": co, + "sede": " ".join(str(c.get("sede") or "").upper().split()) or None, + "programa": " ".join(str(c.get("programa") or "").upper().split()) or None, + }) + if not payload: + return {"guardados": 0} + r = requests.post(f"{url}/rest/v1/{t}?on_conflict=conjunto", + json=payload, headers=headers, timeout=30) + r.raise_for_status() + return {"guardados": len(payload)} + + def borrar_conjunto_sede_programa(self, conjunto): + """Borra la asignacion de un conjunto (lo deja sin sede/programa).""" + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = "conjunto_sede_programa" + co = " ".join(str(conjunto or "").split()) + if not url or not key or not co: + return {"borrado": 0} + headers = {"apikey": key, "Authorization": f"Bearer {key}"} + r = requests.delete(f"{url}/rest/v1/{t}", + params={"conjunto": f"eq.{co}"}, + headers=headers, timeout=30) + r.raise_for_status() + return {"borrado": 1} + + # ── GOOGLE SHEET (CSV publicado): importe gastado por conjunto de anuncios ── + def traer_meta_importe(self): + """Lee el CSV publicado de Meta_Ads_Adsets y devuelve una lista de dicts: + [{conjunto, importe, inicio}, ...] (una por fila del CSV, inicio = 'Inicio del informe'). + Se conserva la fecha para poder filtrar por periodo (Importe Pauta en el Mes).""" + import csv, io + url = os.getenv("META_CSV_URL", "") + out = [] + if not url: + return out + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + r.encoding = "utf-8" + rows = list(csv.reader(io.StringIO(r.text))) + if not rows: + return out + enc = [" ".join(str(h or "").split()).lower() for h in rows[0]] + + def _idx(*claves): + for k in claves: + for i, h in enumerate(enc): + if k in h: + return i + return -1 + + # OJO: buscar "nombre del conjunto..." primero (hay tambien "ID del conjunto..."). + i_conj = _idx("nombre del conjunto de anuncios", "nombre del conjunto") + i_imp = _idx("importe gastado") + i_ini = _idx("inicio del informe") + i_res = _idx("resultados") + if i_conj < 0 or i_imp < 0: + print(f"[traer_meta_importe] columnas no encontradas: {enc}") + return out + for fila in rows[1:]: + if len(fila) <= max(i_conj, i_imp): + continue + co = " ".join(str(fila[i_conj] or "").split()) + if not co: + continue + raw = str(fila[i_imp] or "").strip().replace(",", "") + try: + val = float(raw) if raw else 0.0 + except ValueError: + val = 0.0 + rraw = str(fila[i_res] or "").strip().replace(",", "") if i_res >= 0 and len(fila) > i_res else "" + try: + res = float(rraw) if rraw else 0.0 + except ValueError: + res = 0.0 + ini = str(fila[i_ini] or "").strip() if i_ini >= 0 and len(fila) > i_ini else "" + out.append({"conjunto": co, "importe": val, "resultados": res, "inicio": ini}) + except Exception as e: + print(f"[traer_meta_importe] {e}") + return out + + # ── CHATWOOT: leads asignados (mensaje "Asignado a..."/auto-asignado, ultimo por telefono) ── + def traer_leads_asignados(self): + """Devuelve [{telefono, fecha_asignada(date, -5h), user_name}] tomando el + mensaje de asignacion mas reciente por telefono.""" + sql = """ + SELECT * FROM ( + SELECT DISTINCT ON (c.phone_number) + REPLACE(REPLACE(c.phone_number, '+51', ''), '+', '') AS telefono, + m.id AS message_id, + (m.created_at - INTERVAL '5 hours') AS created_at, + COALESCE(u.name, 'Sin Asesor') AS user_name, + cv.cached_label_list AS etiquetas + 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 + WHERE ( + m.content LIKE 'Asignado a %' OR + m.content LIKE '% auto-asignado%' + ) + ORDER BY c.phone_number, m.id DESC + ) AS t + ORDER BY message_id DESC + """ + conn = self.pg_conn() + try: + cur = conn.cursor() + cur.execute(sql) + cols = [d[0] for d in cur.description] + filas = [dict(zip(cols, row)) for row in cur.fetchall()] + finally: + conn.close() + return filas + + # ── CHATWOOT: ultima asignacion por telefono + primer mensaje del asesor (respuesta) ── + def traer_asignacion_respuesta(self): + """Por telefono: la ULTIMA asignacion (T1) y el primer mensaje posterior de un + ASESOR real (sender_id 15-25) como respuesta (T2). Fechas en hora Peru (-5h). + Devuelve [{telefono, user_id, user_name, created_at(T1), respuesta_fecha(T2)}]. + Si respuesta_fecha es None -> no respondio.""" + sql = """ + SELECT * FROM ( + SELECT DISTINCT ON (c.phone_number) + REPLACE(REPLACE(c.phone_number, '+51', ''), '+', '') AS telefono, + m.id AS message_id, + (m.created_at - INTERVAL '5 hours') AS created_at, + u.id AS user_id, + COALESCE(u.name, 'Sin Asesor') AS user_name, + cv.cached_label_list AS etiquetas, + (respuesta.created_at - INTERVAL '5 hours') AS respuesta_fecha + 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 + LEFT JOIN LATERAL ( + SELECT m2.created_at + FROM messages m2 + WHERE m2.conversation_id = m.conversation_id + AND m2.id > m.id + AND m2.sender_id BETWEEN 15 AND 25 + ORDER BY m2.id ASC + LIMIT 1 + ) AS respuesta ON TRUE + WHERE ( + m.content LIKE 'Asignado a %' OR + m.content LIKE '% auto-asignado%' + ) + ORDER BY c.phone_number, m.id DESC + ) AS t + ORDER BY message_id DESC + """ + conn = self.pg_conn() + try: + cur = conn.cursor() + cur.execute(sql) + cols = [d[0] for d in cur.description] + filas = [dict(zip(cols, row)) for row in cur.fetchall()] + finally: + conn.close() + return filas + + # ── CHATWOOT: actividad de asesores (para detectar turno del dia). ── + def traer_actividad_asesores(self): + """Mensajes ENVIADOS por asesores reales (sender_id 15-25), en hora Peru (-5h). + Sirve para detectar el turno de cada asesor cada dia por concentracion de + actividad. Devuelve [{user_id, created_at}]. Liviano: solo id + fecha.""" + sql = """ + SELECT + m.sender_id AS user_id, + (m.created_at - INTERVAL '5 hours') AS created_at + FROM messages m + WHERE m.sender_id BETWEEN 15 AND 25 + """ + conn = self.pg_conn() + try: + cur = conn.cursor() + cur.execute(sql) + cols = [d[0] for d in cur.description] + filas = [dict(zip(cols, row)) for row in cur.fetchall()] + finally: + conn.close() + return filas + + # ── SUPABASE: actualizar/insertar la pauta de un conjunto en conjunto_pauta ── + def upsert_conjunto_pauta(self, conjunto, pauta): + """Si el conjunto ya existe en conjunto_pauta, actualiza su pauta; si no, lo crea. + Si pauta viene vacia, elimina el vinculo (deja el conjunto sin pauta).""" + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CONJUNTO", "conjunto_pauta") + h = {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"} + co = str(conjunto).strip(); pa = str(pauta or "").strip() + # ¿existe el conjunto? + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": "conjunto", "conjunto": f"eq.{co}"}, + headers=h, timeout=30) + r.raise_for_status() + existe = len(r.json()) > 0 + if not pa: + # sin pauta -> borrar vinculo si existia + if existe: + requests.delete(f"{url}/rest/v1/{t}", params={"conjunto": f"eq.{co}"}, + headers=h, timeout=30).raise_for_status() + return {"conjunto": co, "pauta": None} + if existe: + requests.patch(f"{url}/rest/v1/{t}", params={"conjunto": f"eq.{co}"}, + headers=h, json={"pauta": pa}, timeout=30).raise_for_status() + else: + requests.post(f"{url}/rest/v1/{t}", headers=h, + json={"conjunto": co, "pauta": pa}, timeout=30).raise_for_status() + return {"conjunto": co, "pauta": pa} + + # ── SUPABASE: conjuntos de anuncios SIN pauta conectada (del Sheet, no en conjunto_pauta) ── + def traer_conjuntos_sin_pauta(self): + """Lista de conjuntos del Google Sheet que NO estan en conjunto_pauta.""" + conj_pauta = self.traer_conjunto_pauta() # {pauta: [conjuntos]} + conectados = set() + for cjs in conj_pauta.values(): + for c in cjs: + conectados.add(" ".join(str(c).split()).upper()) + vistos = {} + for f in self.traer_meta_importe(): + nombre = " ".join(str(f.get("conjunto")).split()) + if not nombre: + continue + if " ".join(nombre.split()).upper() not in conectados: + vistos[nombre] = True + return sorted(vistos.keys()) + + # ── SUPABASE: programa_pautas -> {num_indice: [pautas]} (muchos-a-muchos) ── + def traer_programa_pautas(self): + url = os.getenv("SUPABASE_PAUTA_URL", ""); key = os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_PROGRAMA_PAUTAS", "programa_pautas") + out = {} + if not url or not key: + return out + try: + r = requests.get(f"{url}/rest/v1/{t}", params={"select": "num_indice,pauta"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30) + r.raise_for_status() + for row in r.json(): + ni = str(row.get("num_indice") or "").strip() + pa = str(row.get("pauta") or "").strip() + if ni and pa: + out.setdefault(ni, []).append(pa) + except Exception as e: + print(f"[traer_programa_pautas] {e}") + return out + + # ── SUPABASE: agregar un vinculo num_indice<->pauta en programa_pautas (upsert) ── + def agregar_programa_pauta(self, num_indice, pauta): + url = os.getenv("SUPABASE_PAUTA_URL", ""); key = os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_PROGRAMA_PAUTAS", "programa_pautas") + h = {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"} + ni = str(num_indice).strip(); pa = str(pauta).strip() + # ¿ya existe ese par? + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": "id", "num_indice": f"eq.{ni}", "pauta": f"eq.{pa}"}, + headers=h, timeout=30) + r.raise_for_status() + if len(r.json()) > 0: + return {"num_indice": ni, "pauta": pa, "ya_existia": True} + rr = requests.post(f"{url}/rest/v1/{t}", headers=h, + json={"num_indice": ni, "pauta": pa}, timeout=30) + rr.raise_for_status() + return {"num_indice": ni, "pauta": pa, "ya_existia": False} + + # ── SUPABASE: num_indices que YA usan una pauta en basebi_programacion ── + def num_indices_de_pauta(self, pauta): + url = os.getenv("SUPABASE_PAUTA_URL", ""); key = os.getenv("SUPABASE_PAUTA_KEY", "") + tabla = os.getenv("SUPABASE_TABLA_PAUTA", "basebi_programacion") + try: + r = requests.get(f"{url}/rest/v1/{tabla}", + params={"select": "num_indice", "pauta_codigo": f"eq.{str(pauta).strip()}"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30) + r.raise_for_status() + return [str(x.get("num_indice")).strip() for x in r.json() if x.get("num_indice") is not None] + except Exception as e: + print(f"[num_indices_de_pauta] {e}") + return [] + + # ── SUPABASE: guardar pauta y/o contar de un num_indice en basebi_programacion (upsert) ── + def guardar_pauta_basebi(self, num_indice, pauta, contar=None): + url = os.getenv("SUPABASE_PAUTA_URL", ""); key = os.getenv("SUPABASE_PAUTA_KEY", "") + tabla = os.getenv("SUPABASE_TABLA_PAUTA", "basebi_programacion") + h = {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"} + ni = str(num_indice).strip(); pa = str(pauta or "").strip() + campos = {} + if pa: + campos["pauta_codigo"] = pa # solo actualizar pauta si viene con valor + if contar is not None: + campos["contar"] = str(contar).strip().upper() # "SI" / "NO" + if not campos: + return {"num_indice": ni, "sin_cambios": True} + r = requests.get(f"{url}/rest/v1/{tabla}", + params={"select": "num_indice", "num_indice": f"eq.{ni}"}, + headers=h, timeout=30) + r.raise_for_status() + existe = len(r.json()) > 0 + if existe: + rr = requests.patch(f"{url}/rest/v1/{tabla}", + params={"num_indice": f"eq.{ni}"}, + headers=h, json=campos, timeout=30) + else: + rr = requests.post(f"{url}/rest/v1/{tabla}", + headers=h, json={"num_indice": ni, **campos}, timeout=30) + rr.raise_for_status() + return {"num_indice": ni, "pauta": pa, "contar": campos.get("contar"), "creado": not existe} + + # ── SUPABASE: vincular un conjunto de anuncios a una pauta (insert en conjunto_pauta) ── + def guardar_conjunto_pauta(self, conjunto, pauta): + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CONJUNTO", "conjunto_pauta") + h = {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"} + rr = requests.post(f"{url}/rest/v1/{t}", headers=h, + json={"conjunto": str(conjunto).strip(), "pauta": str(pauta).strip()}, + timeout=30) + rr.raise_for_status() + return {"conjunto": conjunto, "pauta": pauta} + + # ── CHATWOOT: plantillas enviadas (para matriz de plantillas cobradas) ── + def traer_plantillas(self): + """Devuelve [{plantilla, telefono, user_name, created_at_peru, + hora_anterior_contacto, siguiente_mensaje_1}] de mensajes de plantilla + con content_attributes vacio (ISBLANK).""" + sql = """ + SELECT + m.additional_attributes -> 'template_params' ->> 'name' AS plantilla, + REPLACE(REPLACE(c.phone_number, '+51', ''), '+', '') AS telefono, + u.name AS user_name, + (m.created_at - INTERVAL '5 hours') AS created_at_peru, + (SELECT (mp.created_at - INTERVAL '5 hours') FROM messages mp + WHERE mp.conversation_id = m.conversation_id AND mp.id < m.id + AND mp.sender_type = 'Contact' ORDER BY mp.id DESC LIMIT 1) AS hora_anterior_contacto, + (SELECT m2.content FROM messages m2 + WHERE m2.conversation_id = m.conversation_id AND m2.id > m.id + AND m2.sender_type = 'Contact' ORDER BY m2.id ASC LIMIT 1) AS siguiente_mensaje_1 + 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 + WHERE m.sender_type = 'User' + AND m.additional_attributes -> 'template_params' ->> 'name' IS NOT NULL + AND (m.content_attributes IS NULL + OR m.content_attributes::text = '{}' + OR m.content_attributes::text = 'null') + """ + conn = self.pg_conn() + try: + cur = conn.cursor() + cur.execute(sql) + cols = [d[0] for d in cur.description] + filas = [dict(zip(cols, row)) for row in cur.fetchall()] + finally: + conn.close() + return filas + + # ── SUPABASE: campanias -> {codigo(pauta): {sede, cargo}} (crudo, sin normalizar) ── + def traer_campanias_sede_cargo(self): + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") + out = {} + if not url or not key: + return out + try: + r = requests.get(f"{url}/rest/v1/{t}", params={"select": "codigo,sede,cargo"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30) + r.raise_for_status() + for row in r.json(): + cod = str(row.get("codigo") or "").strip() + if cod: + out[cod] = {"sede": str(row.get("sede") or "").strip(), + "cargo": str(row.get("cargo") or "").strip()} + except Exception as e: + print(f"[traer_campanias_sede_cargo] {e}") + return out + + # ── SUPABASE campanias: LISTAR todas (con id) para el apartado Leyenda ── + def listar_campanias(self): + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") + out = [] + if not url or not key: + return out + try: + r = requests.get(f"{url}/rest/v1/{t}", + params={"select": "id,frase_busqueda,cargo,codigo,sede,dia,origen,fecha_inicio,fecha_fin", + "order": "codigo"}, + headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30) + r.raise_for_status() + for row in r.json(): + out.append({ + "id": row.get("id"), + "frase_busqueda": str(row.get("frase_busqueda") or ""), + "cargo": str(row.get("cargo") or ""), + "codigo": str(row.get("codigo") or ""), + "sede": str(row.get("sede") or ""), + "dia": str(row.get("dia") or ""), + "origen": str(row.get("origen") or ""), + "fecha_inicio": str(row.get("fecha_inicio") or "")[:10], + "fecha_fin": str(row.get("fecha_fin") or "")[:10], + }) + except Exception as e: + print(f"[listar_campanias] {e}") + return out + + # ── SUPABASE campanias: AGREGAR una fila ── + def agregar_campania(self, datos): + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") + h = {"apikey": key, "Authorization": f"Bearer {key}", + "Content-Type": "application/json", "Prefer": "return=representation"} + fila = { + "frase_busqueda": str(datos.get("frase_busqueda") or "").strip(), + "cargo": str(datos.get("cargo") or "").strip(), + "codigo": str(datos.get("codigo") or "").strip(), + "sede": str(datos.get("sede") or "").strip(), + "dia": str(datos.get("dia") or "").strip(), + "origen": str(datos.get("origen") or "").strip(), + "fecha_inicio": str(datos.get("fecha_inicio") or "").strip() or None, + "fecha_fin": str(datos.get("fecha_fin") or "").strip() or None, + } + r = requests.post(f"{url}/rest/v1/{t}", headers=h, json=fila, timeout=30) + r.raise_for_status() + return r.json() + + # ── SUPABASE campanias: EDITAR una fila por id ── + def editar_campania(self, cid, datos): + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") + h = {"apikey": key, "Authorization": f"Bearer {key}", + "Content-Type": "application/json", "Prefer": "return=representation"} + fila = { + "frase_busqueda": str(datos.get("frase_busqueda") or "").strip(), + "cargo": str(datos.get("cargo") or "").strip(), + "codigo": str(datos.get("codigo") or "").strip(), + "sede": str(datos.get("sede") or "").strip(), + "dia": str(datos.get("dia") or "").strip(), + "origen": str(datos.get("origen") or "").strip(), + "fecha_inicio": str(datos.get("fecha_inicio") or "").strip() or None, + "fecha_fin": str(datos.get("fecha_fin") or "").strip() or None, + } + r = requests.patch(f"{url}/rest/v1/{t}", params={"id": f"eq.{cid}"}, + headers=h, json=fila, timeout=30) + r.raise_for_status() + return r.json() + + # ── SUPABASE campanias: BORRAR una fila por id ── + def borrar_campania(self, cid): + url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") + key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") + t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") + h = {"apikey": key, "Authorization": f"Bearer {key}"} + r = requests.delete(f"{url}/rest/v1/{t}", params={"id": f"eq.{cid}"}, headers=h, timeout=30) + r.raise_for_status() + return {"id": cid, "borrado": True} + + # ── SQL SERVER: matrículas ── + 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(REPLACE(REPLACE(sgema_alumno.dsc_telefono_1,' ',''),'+51',''),'+','') AS dsc_telefono_1, + REPLACE(REPLACE(REPLACE(sgema_alumno.dsc_telefono_2,' ',''),'+51',''),'+','') AS dsc_telefono_2, + sgeca_matricula.cod_moneda, + ISNULL((SELECT SUM(c.imp_total - ISNULL(c.imp_dscto,0)) + FROM sgede_cronograma_matricula c + WHERE c.cod_localidad = sgeca_matricula.cod_localidad + AND c.num_matricula = sgeca_matricula.num_matricula + AND c.num_refinanciamiento = 1), 0) AS INV_NETA, + (SELECT rhuma_trabajador.dsc_apellido_paterno + ' ' + + SUBSTRING(rhuma_trabajador.dsc_apellido_materno, 1, 1) + '. ' + + rhuma_trabajador.dsc_nombres + FROM rhuma_trabajador + WHERE rhuma_trabajador.cod_trabajador = sgeca_matricula.cod_vendedor) AS dsc_vendedor, + 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() + try: + cur = conn.cursor() + cur.execute(sql) + cols = [d[0] for d in cur.description] + rows = cur.fetchall() + finally: + conn.close() + return [dict(zip(cols, r)) for r in rows] diff --git a/backend/diag_asesores.py b/backend/diag_asesores.py new file mode 100644 index 0000000..4cca0be --- /dev/null +++ b/backend/diag_asesores.py @@ -0,0 +1,30 @@ +# diag_asesores.py — Valores UNICOS de la columna 'asesor' en cartera_junta. +import os, requests +from collections import Counter +from dotenv import load_dotenv +load_dotenv() + +url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") +key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") +t = os.getenv("SUPABASE_TABLA_CARTERA", "cartera_junta") +h = {"apikey": key, "Authorization": f"Bearer {key}"} + +# traer solo la columna asesor (paginado) +cont = Counter() +desde, paso = 0, 1000 +while True: + r = requests.get(f"{url}/rest/v1/{t}", params={"select": "asesor", "offset": str(desde), "limit": str(paso)}, + headers=h, timeout=60) + r.raise_for_status() + data = r.json() + if not data: + break + for row in data: + cont[(row.get("asesor") or "").strip()] += 1 + if len(data) < paso: + break + desde += paso + +print(f"Valores distintos de 'asesor' en cartera_junta: {len(cont)}\n") +for val, n in cont.most_common(): + print(f" {n:>6} {val or '(vacio)'}") diff --git a/backend/diag_asignados_diana_hoy.py b/backend/diag_asignados_diana_hoy.py new file mode 100644 index 0000000..cd073ef --- /dev/null +++ b/backend/diag_asignados_diana_hoy.py @@ -0,0 +1,37 @@ +# diag_asignados_diana_hoy.py +# Leads ASIGNADOS a DIANA el dia 13 (hoy) de agosto 2026, con telefono, hora y +# si respondio o no. Fuente: mensajes de asignacion de Chatwoot. +from datetime import datetime +from data_manager_v2 import DataManager + +ANO, MES, DIA = 2026, 8, 13 +NOMBRE = "DIANA" + +dm = DataManager() + +filas = [] +for r in dm.traer_asignacion_respuesta(): + if NOMBRE not in (r.get("user_name") or "").upper(): + continue + t1 = r.get("created_at") + if not isinstance(t1, datetime): + continue + if t1.year == ANO and t1.month == MES and t1.day == DIA: + filas.append(r) + +print(f"\n=== DIANA — leads ASIGNADOS el {DIA:02d}/{MES:02d}/{ANO} ===\n") +print(f"Total asignados hoy: {len(filas)}\n") +print(f"{'#':3} {'telefono':13} {'asignado':11} {'respondio':11} estado") +print("-"*55) +resp = 0; sinr = 0 +for i, r in enumerate(sorted(filas, key=lambda x: x['created_at']), 1): + t1 = r['created_at']; t2 = r.get('respuesta_fecha') + tel = r.get('telefono', '') + if isinstance(t2, datetime): + resp += 1 + print(f"{i:3} {tel:13} {t1.strftime('%H:%M:%S'):11} {t2.strftime('%H:%M:%S'):11} respondio") + else: + sinr += 1 + print(f"{i:3} {tel:13} {t1.strftime('%H:%M:%S'):11} {'':11} SIN RESPONDER") +print("-"*55) +print(f"Respondidos: {resp} | Sin responder: {sinr} | Total: {len(filas)}") diff --git a/backend/diag_base.json b/backend/diag_base.json new file mode 100644 index 0000000..d5dbe49 --- /dev/null +++ b/backend/diag_base.json @@ -0,0 +1,86 @@ +{ + "2026|2|TODOS|TODOS|TODOS": { + "leads_recibidos": 6186, + "leads_procesados": 1489, + "total_matriculados": 117, + "cursos_programados": 7, + "pauta_total_imp": 4255.16, + "always_total_imp": 1730.51, + "webform_total_rec": 151, + "webform_total_matr": 15, + "asignados_total": 1390, + "matriz_cursos_filas": 6 + }, + "2026|2|TODOS|TEAC|LIMA": { + "leads_recibidos": 2198, + "leads_procesados": 499, + "total_matriculados": 53, + "cursos_programados": 3, + "pauta_total_imp": 4255.16, + "always_total_imp": 810.03, + "webform_total_rec": 94, + "webform_total_matr": 9, + "asignados_total": 1390, + "matriz_cursos_filas": 2 + }, + "2026|2|TODOS|TERC|AREQUIPA": { + "leads_recibidos": 325, + "leads_procesados": 64, + "total_matriculados": 10, + "cursos_programados": 1, + "pauta_total_imp": 4255.16, + "always_total_imp": 0, + "webform_total_rec": 5, + "webform_total_matr": 0, + "asignados_total": 1390, + "matriz_cursos_filas": 1 + }, + "2026|1|TODOS|TODOS|TODOS": { + "leads_recibidos": 7417, + "leads_procesados": 2370, + "total_matriculados": 128, + "cursos_programados": 9, + "pauta_total_imp": 4499.9, + "always_total_imp": 776.0, + "webform_total_rec": 143, + "webform_total_matr": 15, + "asignados_total": 1529, + "matriz_cursos_filas": 5 + }, + "2026|1|TODOS|SEMINARIOS|LIMA": { + "leads_recibidos": 946, + "leads_procesados": 342, + "total_matriculados": 22, + "cursos_programados": 1, + "pauta_total_imp": 4499.9, + "always_total_imp": 0, + "webform_total_rec": 1, + "webform_total_matr": 0, + "asignados_total": 1529, + "matriz_cursos_filas": 0 + }, + "2026|7|TODOS|TODOS|TODOS": { + "leads_recibidos": 1664, + "leads_procesados": 1435, + "total_matriculados": 59, + "cursos_programados": 16, + "pauta_total_imp": 2212.41, + "always_total_imp": 1540.57, + "webform_total_rec": 34, + "webform_total_matr": 1, + "asignados_total": 2275, + "matriz_cursos_filas": 16 + }, + "2026|7|TODOS|TEAC|PIURA": { + "leads_recibidos": 104, + "leads_procesados": 102, + "total_matriculados": 1, + "cursos_programados": 1, + "pauta_total_imp": 2212.41, + "always_total_imp": 172.48, + "webform_total_rec": 0, + "webform_total_matr": 0, + "asignados_total": 2275, + "matriz_cursos_filas": 1 + } +} \ No newline at end of file diff --git a/backend/diag_base.py b/backend/diag_base.py new file mode 100644 index 0000000..ea8de7f --- /dev/null +++ b/backend/diag_base.py @@ -0,0 +1,38 @@ +# diag_base.py — Captura los numeros ACTUALES (antes de optimizar) para comparar despues. +import json +import services as S + +combos = [ + ("2026","2","TODOS","TODOS","TODOS"), + ("2026","2","TODOS","TEAC","LIMA"), + ("2026","2","TODOS","TERC","AREQUIPA"), + ("2026","1","TODOS","TODOS","TODOS"), + ("2026","1","TODOS","SEMINARIOS","LIMA"), + ("2026","7","TODOS","TODOS","TODOS"), + ("2026","7","TODOS","TEAC","PIURA"), +] + +def resumen(d): + k = d["kpis"] + return { + "leads_recibidos": k["leads_recibidos"], + "leads_procesados": k["leads_procesados"], + "total_matriculados": k["total_matriculados"], + "cursos_programados": k["cursos_programados"], + "pauta_total_imp": d["tabla_pauta"]["total"].get("importe"), + "always_total_imp": d["matriz_always"]["total"]["importe"], + "webform_total_rec": d["matriz_webform"]["total"]["recibidos"], + "webform_total_matr": d["matriz_webform"]["total"]["matriculas"], + "asignados_total": d["matriz_asignados"]["total"], + "matriz_cursos_filas": len(d["matriz_cursos"]["filas"]), + } + +out = {} +for c in combos: + d = S.leads_dashboard(*c) + out["|".join(c)] = resumen(d) + print("|".join(c), "->", out["|".join(c)]) + +with open("diag_base.json", "w", encoding="utf-8") as f: + json.dump(out, f, indent=2, ensure_ascii=False) +print("\nGuardado: diag_base.json (linea base ANTES de optimizar)") diff --git a/backend/diag_buscar_numero.py b/backend/diag_buscar_numero.py new file mode 100644 index 0000000..02702a4 --- /dev/null +++ b/backend/diag_buscar_numero.py @@ -0,0 +1,58 @@ +# diag_buscar_numero.py - Rastrea un telefono en cartera_junta: como y donde se cuenta. +# Ejecutar en backend/: python diag_buscar_numero.py +import os, requests +from dotenv import load_dotenv +load_dotenv() + +TELEFONO = "18099166706" # <-- numero a buscar (tal cual y normalizado) + +URL = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") +KEY = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") +T = os.getenv("SUPABASE_TABLA_CARTERA", "cartera_junta") +HEAD = {"apikey": KEY, "Authorization": f"Bearer {KEY}"} + +def _norm(v): + """Misma normalizacion de la cartera: quita +51/+ y el 51 inicial de celulares PE.""" + s = str(v or "") + for x in ("+51","+"," ","-","(",")"): s=s.replace(x,"") + s=s.strip() + if s.isdigit() and 51900000000 <= int(s) <= 51999999999: s=s[2:] + return s + +# variantes a buscar: el numero tal cual y su version normalizada +variantes = {TELEFONO, _norm(TELEFONO)} +print(f"Buscando telefono: {TELEFONO} (variantes: {variantes})\n") + +encontrado = False +for tel in variantes: + r = requests.get(f"{URL}/rest/v1/{T}", + params={"select":"*", "telefono": f"eq.{tel}"}, + headers=HEAD, timeout=60) + r.raise_for_status() + filas = r.json() + if not filas: + continue + encontrado = True + print(f"=== {len(filas)} fila(s) con telefono = {tel} ===") + for i,f in enumerate(filas,1): + print(f"\n Fila {i}:") + for campo in ["telefono","es_origen","canal","origen_base","asesor","sede", + "programa","codigo","fecha_creada","solo_contacto"]: + if campo in f: + print(f" {campo:14}= {f.get(campo)}") + # mostrar cualquier otro campo extra + otros=[k for k in f.keys() if k not in + ("telefono","es_origen","canal","origen_base","asesor","sede", + "programa","codigo","fecha_creada","solo_contacto")] + for k in otros: + print(f" {k:14}= {f.get(k)}") + +if not encontrado: + print("El telefono NO aparece en cartera_junta (ni tal cual ni normalizado).") + print("=> No se esta contando en ninguna pauta/origen de la cartera.") +else: + print("\n--- Como leer esto ---") + print(" es_origen=SI -> esta fila es la que CUENTA como origen del telefono.") + print(" canal -> COPITO / PAUTA_WSP / WEB_FORMULARIO / etc.") + print(" origen_base -> POSTGRE (Chatwoot) o SUPABASE (tu Excel).") + print(" codigo -> la pauta/campania a la que esta asociado (si tiene).") diff --git a/backend/diag_campanias_cruce.py b/backend/diag_campanias_cruce.py new file mode 100644 index 0000000..818f5f0 --- /dev/null +++ b/backend/diag_campanias_cruce.py @@ -0,0 +1,61 @@ +""" +Diagnostico: revisa la tabla 'campanias' y detecta cruces = misma frase_busqueda +(mismo emoji/texto) con rangos de fecha [fecha_inicio, fecha_fin] que se SOLAPAN. +Un mensaje que caiga en el solape haria match con 2 campanias a la vez. +""" +import data_manager_v2 as dm +import leads_logic as L + +# _campanias_supabase() trae tuplas (frase, cargo, codigo, sede, dia, origen, fi, ff) +crudas = dm._campanias_supabase() +campanias = [{"frase_busqueda": t[0], "cargo": t[1], "codigo": t[2], "sede": t[3], + "dia": t[4], "origen": t[5], "fecha_inicio": t[6], "fecha_fin": t[7]} + for t in crudas] +print(f"Total campanias: {len(campanias)}\n") + +# agrupar por frase_busqueda +por_frase = {} +for c in campanias: + frase = " ".join(str(c.get("frase_busqueda") or "").split()) + por_frase.setdefault(frase, []).append(c) + +def _rango(c): + fi = L._to_date(str(c.get("fecha_inicio") or "")[:10]) + ff = L._to_date(str(c.get("fecha_fin") or "")[:10]) + return fi, ff + +cruces = 0 +print("=== CRUCES (misma frase + fechas solapadas) ===\n") +for frase, lista in por_frase.items(): + if len(lista) < 2: + continue + # comparar cada par + for i in range(len(lista)): + for j in range(i + 1, len(lista)): + a, b = lista[i], lista[j] + fia, ffa = _rango(a) + fib, ffb = _rango(b) + if not (fia and ffa and fib and ffb): + continue + # solapan si a.inicio <= b.fin y b.inicio <= a.fin + if fia <= ffb and fib <= ffa: + cruces += 1 + print(f"FRASE: {frase!r}") + print(f" A) cod={a.get('codigo')} sede={a.get('sede')} cargo={a.get('cargo')} " + f"[{fia} -> {ffa}]") + print(f" B) cod={b.get('codigo')} sede={b.get('sede')} cargo={b.get('cargo')} " + f"[{fib} -> {ffb}]") + print(f" SOLAPE: {max(fia,fib)} -> {min(ffa,ffb)}\n") + +if cruces == 0: + print("No se encontraron cruces (ninguna frase repetida con fechas solapadas).") +else: + print(f"TOTAL de cruces detectados: {cruces}") + +# tambien listar frases repetidas (aunque no solapen) por si acaso +print("\n=== Frases que se repiten en varias campanias ===") +rep = {f: l for f, l in por_frase.items() if len(l) > 1} +for f, l in rep.items(): + print(f" {len(l)}x {f!r}") +if not rep: + print(" (ninguna frase repetida)") diff --git a/backend/diag_canal.py b/backend/diag_canal.py new file mode 100644 index 0000000..a9f3ebb --- /dev/null +++ b/backend/diag_canal.py @@ -0,0 +1,28 @@ +# diag_canal.py — Revisa num_indice 1161: pauta en basebi vs conjuntos. +import services as S + +NI = "1161" +pm = S._pauta_cruda() # {num_indice: {pauta, estado, contar}} +cp = S._conjunto_pauta() # {pauta: [conjuntos]} + +info = pm.get(NI) +print(f"num_indice {NI} en basebi_programacion:", info) +cod = info.get("pauta") if info else None +print(f" -> pauta (cod): {cod!r}") +print(f" -> conjuntos por esa pauta: {cp.get(str(cod).strip()) if cod else '(sin cod)'}") + +# Buscar en la matriz esa fila +d = S.leads_dashboard("2026","5","TODOS","TODOS","TODOS")["matriz_cursos"] +for c in d["filas"]: + if str(c["num_indice"]) == NI: + print(f"\nEn la matriz -> pauta={c['pauta']!r} conjuntos={c.get('conjuntos')}") + break +else: + print(f"\n(num_indice {NI} no esta en la matriz de mayo)") + +# Buscar en conjunto_pauta si algun conjunto tiene ese nombre TEAC_TRUJILLO_MAY... +print("\nBuscando conjunto 'TEAC_TRUJILLO_MAY' en conjunto_pauta:") +for pauta, cjs in cp.items(): + for cj in cjs: + if "TEAC_TRUJILLO_MAY" in str(cj).upper(): + print(f" pauta={pauta!r} -> {cj}") diff --git a/backend/diag_cart.py b/backend/diag_cart.py new file mode 100644 index 0000000..9e38e1b --- /dev/null +++ b/backend/diag_cart.py @@ -0,0 +1,15 @@ +import services as S +from collections import Counter + +rows = S._cartera_rows_cache() +print("TOTAL filas cartera:", len(rows)) + +si = [r for r in rows if str(r.get("es_origen") or "").strip().upper() == "SI"] +print("Filas es_origen=SI:", len(si)) +print("Valores es_origen:", Counter(str(r.get("es_origen")) for r in rows).most_common(10)) + +ase = S._asesores(); amap = ase["map"] +cnt_si = sum(1 for r in si if S._norm_asesor(r.get("asesor"), amap) == "ALMENDRA PERALTA") +cnt_all = sum(1 for r in rows if S._norm_asesor(r.get("asesor"), amap) == "ALMENDRA PERALTA") +print("Almendra en es_origen=SI :", cnt_si) +print("Almendra en TODA cartera :", cnt_all) diff --git a/backend/diag_caso_469.py b/backend/diag_caso_469.py new file mode 100644 index 0000000..c36b0c7 --- /dev/null +++ b/backend/diag_caso_469.py @@ -0,0 +1,62 @@ +# diag_caso_469.py - Investiga un telefono puntual: asignacion, respuesta, y +# actividad de la asesora ese dia (para saber si estuvo o no de descanso). +from datetime import datetime +from data_manager_v2 import DataManager + +TEL = "923306769" # telefono a investigar +NOMBRE = "DIANA" +DIA_OBJ = (2026, 8, 10) + +dm = DataManager() + +# 1) Todos los mensajes de ese contacto (asignaciones y respuestas), en hora Peru +conn = dm.pg_conn(); cur = conn.cursor() +cur.execute(""" +SELECT (m.created_at - INTERVAL '5 hours') AS fecha, m.id, m.sender_id, m.sender_type, + LEFT(m.content, 70) +FROM messages m +JOIN conversations cv ON m.conversation_id = cv.id +JOIN contacts c ON cv.contact_id = c.id +WHERE REPLACE(REPLACE(c.phone_number,'+51',''),'+','') LIKE %s +ORDER BY m.id ASC +""", ('%'+TEL,)) +rows = cur.fetchall() +print(f"\n=== Mensajes del contacto {TEL} (hora Peru) ===\n") +print(f"{'fecha':20} {'msg_id':>9} {'sender_id':>9} {'tipo':8} contenido") +print("-"*90) +for fecha, mid, sid, stype, cont in rows: + marca = " <- ASESOR" if (sid is not None and 15 <= sid <= 25) else "" + stype = str(stype or "-"); sid_s = str(sid if sid is not None else "-") + print(f"{str(fecha):26} {str(mid):>9} {sid_s:>9} {stype:8} {str(cont or '')[:40]}{marca}") + +# 2) Que trajo el query de asignacion+respuesta para este telefono +print(f"\n=== Lo que el dashboard tomo como T1 (asignacion) y T2 (respuesta) ===") +for r in dm.traer_asignacion_respuesta(): + if str(r.get('telefono','')).endswith(TEL[-9:]): + print(f" T1 asignacion = {r.get('created_at')}") + print(f" T2 respuesta = {r.get('respuesta_fecha')}") + print(f" asesor = {r.get('user_name')}") + +# 3) Actividad de Diana ese dia (para saber si trabajo o descanso) +print(f"\n=== Actividad de {NOMBRE} el {DIA_OBJ[2]:02d}/{DIA_OBJ[1]:02d}/{DIA_OBJ[0]} (mensajes enviados) ===") +asig = dm.traer_asignacion_respuesta() +uid_diana = None +for r in asig: + if NOMBRE in (r.get('user_name') or '').upper() and r.get('user_id') is not None: + uid_diana = r['user_id']; break +if uid_diana is None: + print(" (no se hallo user_id de Diana)") +else: + hrs = [] + for a in dm.traer_actividad_asesores(): + ts = a.get('created_at') + if a.get('user_id')==uid_diana and isinstance(ts,datetime) and \ + (ts.year,ts.month,ts.day)==DIA_OBJ: + hrs.append(ts.strftime('%H:%M')) + if not hrs: + print(f" SIN actividad ese dia -> probablemente DESCANSO / no trabajo.") + else: + hrs.sort() + print(f" {len(hrs)} mensajes. Primera: {hrs[0]} Ultima: {hrs[-1]}") + print(f" (si hay actividad en la tarde 13-18 = turno 1; si en la noche = turno 2)") +conn.close() diff --git a/backend/diag_comparar_asignados_diana.py b/backend/diag_comparar_asignados_diana.py new file mode 100644 index 0000000..5cf1a30 --- /dev/null +++ b/backend/diag_comparar_asignados_diana.py @@ -0,0 +1,53 @@ +# diag_comparar_asignados_diana.py +# Compara, para DIANA en un mes, los asignados por dia entre: +# FUENTE A = traer_leads_asignados (la de la TABLA de Otros General) +# FUENTE B = traer_asignacion_respuesta (la de la GRAFICA de Vendedores) +# Muestra el conteo por dia y que telefonos difieren. +from datetime import datetime +from data_manager_v2 import DataManager + +ANO, MES = 2026, 8 +NOMBRE = "DIANA" + +dm = DataManager() + +def _to_date(v): + if isinstance(v, datetime): return v.date() + return None + +# FUENTE A: tabla Otros General +A = {} # dia -> set(telefonos) +for r in dm.traer_leads_asignados(): + nom = (r.get("user_name") or "") + if NOMBRE not in nom.upper(): continue + f = _to_date(r.get("created_at")) + if not f or f.year!=ANO or f.month!=MES: continue + A.setdefault(f.day, set()).add(str(r.get("telefono") or "").strip()) + +# FUENTE B: grafica Vendedores +B = {} # dia -> set(telefonos) +for r in dm.traer_asignacion_respuesta(): + nom = (r.get("user_name") or "") + if NOMBRE not in nom.upper(): continue + f = _to_date(r.get("created_at")) + if not f or f.year!=ANO or f.month!=MES: continue + B.setdefault(f.day, set()).add(str(r.get("telefono") or "").strip()) + +dias = sorted(set(list(A.keys())+list(B.keys()))) +print(f"\n=== DIANA — asignados por dia: TABLA(A) vs GRAFICA(B) — {MES}/{ANO} ===\n") +print(f"{'DIA':>4} {'TABLA(A)':>9} {'GRAFICA(B)':>11} {'DIF':>5}") +print("-"*34) +for d in dias: + a=len(A.get(d,set())); b=len(B.get(d,set())) + marca = " <==" if a!=b else "" + print(f"{d:>4} {a:>9} {b:>11} {a-b:>5}{marca}") + +print(f"\n=== Detalle de los dias que DIFIEREN ===") +for d in dias: + sa=A.get(d,set()); sb=B.get(d,set()) + if sa==sb: continue + solo_A = sa - sb # estan en tabla pero NO en grafica + solo_B = sb - sa # estan en grafica pero NO en tabla + print(f"\n DIA {d}:") + if solo_A: print(f" solo en TABLA (A), no en grafica: {sorted(solo_A)}") + if solo_B: print(f" solo en GRAFICA (B), no en tabla: {sorted(solo_B)}") diff --git a/backend/diag_con_msje_sin_campania.py b/backend/diag_con_msje_sin_campania.py new file mode 100644 index 0000000..c79b493 --- /dev/null +++ b/backend/diag_con_msje_sin_campania.py @@ -0,0 +1,57 @@ +""" +Diagnostico: contactos que SI tienen mensajes, pero NINGUNO de campaña. +Muestra sus mensajes (content) para revisar por que no hicieron match. +""" +import os +import psycopg2 +from dotenv import load_dotenv +import data_manager_v2 as dm # reutiliza las frases de campaña + +load_dotenv() + +PG_HOST = os.getenv("PG_HOST") +PG_DB = os.getenv("PG_DATABASE") or os.getenv("PG_DB") +PG_USER = os.getenv("PG_USER") +PG_PASS = os.getenv("PG_PASSWORD") or os.getenv("PG_PASS") +PG_PORT = os.getenv("PG_PORT", "5432") + +FRASES = [c[0] for c in dm._campanias_supabase()] +likes = " OR ".join(["m.content LIKE %s"] * len(FRASES)) +params = [f"%{fr}%" for fr in FRASES] + +conn = psycopg2.connect(host=PG_HOST, dbname=PG_DB, user=PG_USER, + password=PG_PASS, port=PG_PORT, connect_timeout=30) +cur = conn.cursor() + +# Buscar contactos con >=1 mensaje pero 0 de campaña (traemos algunos pocos) +sql = f""" +SELECT c.id, c.phone_number, c.created_at, + COUNT(m.id) AS total_msjes, + SUM(CASE WHEN ({likes}) THEN 1 ELSE 0 END) AS msjes_campania +FROM contacts c +JOIN conversations cv ON cv.contact_id = c.id +JOIN messages m ON m.conversation_id = cv.id AND m.sender_type = 'Contact' +GROUP BY c.id, c.phone_number, c.created_at +HAVING COUNT(m.id) >= 1 AND SUM(CASE WHEN ({likes}) THEN 1 ELSE 0 END) = 0 +ORDER BY c.created_at DESC +LIMIT 5 +""" +cur.execute(sql, params + params) +contactos = cur.fetchall() + +for cid, ph, creado, tot, camp in contactos: + print("=" * 80) + print(f"CONTACTO id={cid} tel={ph} creado={creado} #msjes={tot} campaña={camp}") + # Mostrar sus mensajes (content) del contacto + cur.execute(""" + SELECT m.created_at, LEFT(m.content, 90) + FROM messages m + JOIN conversations cv ON m.conversation_id = cv.id + WHERE cv.contact_id = %s AND m.sender_type = 'Contact' + ORDER BY m.created_at ASC + LIMIT 10 + """, (cid,)) + for fecha, txt in cur.fetchall(): + print(f" [{fecha}] {txt!r}") + +conn.close() diff --git a/backend/diag_content.py b/backend/diag_content.py new file mode 100644 index 0000000..1625e86 --- /dev/null +++ b/backend/diag_content.py @@ -0,0 +1,43 @@ +# diag_content.py — Revisa que valores toma content_attributes (para replicar ISBLANK). +# Ejecutar dentro de backend/: python diag_content.py +from data_manager_v2 import DataManager + +dm = DataManager() +conn = dm.pg_conn() +cur = conn.cursor() + +# Distribucion de content_attributes en los mensajes de plantilla +sql = """ +SELECT + CASE + WHEN m.content_attributes IS NULL THEN '(NULL)' + WHEN m.content_attributes::text = '{}' THEN '(vacio {})' + WHEN m.content_attributes::text = 'null' THEN "(texto 'null')" + ELSE 'CON DATOS' + END AS tipo, + COUNT(*) AS n +FROM messages m +WHERE m.sender_type = 'User' + AND m.additional_attributes -> 'template_params' ->> 'name' IS NOT NULL +GROUP BY 1 +ORDER BY 2 DESC +""" +cur.execute(sql) +print("== content_attributes en mensajes de plantilla ==") +for tipo, n in cur.fetchall(): + print(f" {tipo:16} {n}") + +# Muestra 3 ejemplos de cada tipo con datos +print("\n== ejemplos de content_attributes CON DATOS (primeros 3) ==") +cur.execute(""" +SELECT m.additional_attributes -> 'template_params' ->> 'name', m.content_attributes::text +FROM messages m +WHERE m.sender_type='User' + AND m.additional_attributes -> 'template_params' ->> 'name' IS NOT NULL + AND m.content_attributes IS NOT NULL + AND m.content_attributes::text NOT IN ('{}','null') +LIMIT 3 +""") +for nombre, ca in cur.fetchall(): + print(f" {nombre}: {ca[:120]}") +conn.close() diff --git a/backend/diag_curso_indice.py b/backend/diag_curso_indice.py new file mode 100644 index 0000000..f633dfe --- /dev/null +++ b/backend/diag_curso_indice.py @@ -0,0 +1,32 @@ +""" +Diagnostico: para num_indices dados, muestra las columnas de la matriz +'Detalle por Curso' del apartado LEADS (importe pauta, cartera, leads, matriculas...). +Filtro por defecto: TODOS (sin mes). Cambia ANO/MES/DIA si quieres un periodo. +""" +import services as S + +NUM_INDICES = ["1157", "1174"] +ANO, MES, DIA = "TODOS", "TODOS", "TODOS" # cambia si quieres filtrar (ej "2026","7","TODOS") + +d = S.leads_dashboard(ANO, MES, DIA, "TODOS", "TODOS") +matriz = d.get("matriz_cursos", {}) +filas = matriz.get("filas", []) if isinstance(matriz, dict) else matriz + +def _ni(v): + s = str(v or "").strip() + return s[:-2] if s.endswith(".0") else s + +objetivo = set(NUM_INDICES) +encontrados = [f for f in filas if _ni(f.get("num_indice")) in objetivo] + +if not encontrados: + print("No se encontraron esos num_indice en la matriz. num_indices disponibles (muestra):") + print([_ni(f.get("num_indice")) for f in filas[:40]]) +else: + for f in encontrados: + print("=" * 70) + print(f"NUM_INDICE: {_ni(f.get('num_indice'))} | {f.get('personalizado') or f.get('programa') or ''}") + print("-" * 70) + for k, v in f.items(): + print(f" {k:32}: {v}") + print() diff --git a/backend/diag_dayana_6_7.py b/backend/diag_dayana_6_7.py new file mode 100644 index 0000000..6c4ff37 --- /dev/null +++ b/backend/diag_dayana_6_7.py @@ -0,0 +1,110 @@ +# diag_dayana_6_7.py +# Dias 6 y 7 de AGOSTO 2026 de DAYANA: turno, tiempo asignacion, respuesta y minutos. +# min = tiempo LABORAL. Los del refrigerio (13-15 turno1) llevan -45 si superan 45 (marcado). +from datetime import datetime, timedelta, date +from data_manager_v2 import DataManager + +ANO, MES = 2026, 8 +DIAS = [6, 7] +NOMBRE = "DAYANA" + +dm = DataManager() +asig = dm.traer_asignacion_respuesta() +act_rows = dm.traer_actividad_asesores() + +uid2name = {} +for r in asig: + if r.get("user_id") is not None: + uid2name[r["user_id"]] = r.get("user_name") or "" + +from collections import defaultdict +act = defaultdict(lambda: defaultdict(int)) +for a in act_rows: + uid=a.get("user_id"); ts=a.get("created_at") + if uid not in uid2name or not isinstance(ts,datetime): continue + nom=uid2name[uid].upper(); d=ts.date(); h=ts.hour+ts.minute/60.0; key=(nom,d) + if d.weekday()==6: continue + if d.weekday()==5: + if 9<=h<13: act[key]["sab_manana"]+=1 + elif 14<=h<18: act[key]["sab_tarde"]+=1 + else: + if 13<=h<18: act[key]["tarde"]+=1 + elif 18<=h<22: act[key]["noche"]+=1 + +def turno_dia(nom,d): + if d.weekday()==6: return None + fr=act.get((nom,d)) + if not fr: return None + if d.weekday()==5: + m=fr.get("sab_manana",0); t=fr.get("sab_tarde",0) + if m==0 and t==0: return None + return [(9,13)] if m>=t else [(14,18)] + t=fr.get("tarde",0); n=fr.get("noche",0) + if t==0 and n==0: return None + return [(9,13),(18,22)] if n>t else [(9,13),(14,18)] + +def es_t1(nom,d): + fr=act.get((nom,d)) + if not fr or d.weekday()>=5: return False + t=fr.get("tarde",0); n=fr.get("noche",0) + return (not (n>t)) and (t>0 or n>0) + +def _dt(d,hh): return datetime(d.year,d.month,d.day)+timedelta(hours=hh) + +def mins_laborales(nom,t1,t2): + if not isinstance(t1,datetime) or not isinstance(t2,datetime) or t2<=t1: return 0 + total=0.0; cur=t1; s=0 + while cur=fin: continue + a=max(cur,ini); b=min(t2,fin) + if b>a: total+=(b-a).total_seconds()/60.0 + cur=fin + if cur>=t2: break + if cur.date()==d: cur=_dt(d,24); s+=1 + return round(total) + +for DIA in DIAS: + d1=date(ANO,MES,DIA) + filas=[] + for r in asig: + if NOMBRE not in (r.get("user_name") or "").upper(): continue + t1=r.get("created_at") + if isinstance(t1,datetime) and t1.year==ANO and t1.month==MES and t1.day==DIA: + filas.append(r) + nom_up=(filas[0].get("user_name") or "").upper() if filas else NOMBRE + tt=turno_dia(nom_up,d1) + if tt is None: turno="LIBRE/DESCANSO" + elif tt==[(9,13),(14,18)]: turno="TURNO 1 (9-18)" + elif tt==[(9,13),(18,22)]: turno="TURNO 2 (9-13+18-22)" + else: turno=str(tt) + + print(f"\n{'='*70}") + print(f"DAYANA — {DIA:02d}/{MES:02d}/{ANO} -> {turno} (asignados: {len(filas)})") + print(f"{'='*70}") + print(f"{'#':3} {'telefono':13} {'asignado':11} {'respondio':11} {'min':>5} nota") + print("-"*60) + resp=0; sinr=0; suma=0 + for i,r in enumerate(sorted(filas,key=lambda x:x['created_at']),1): + t1=r['created_at']; t2=r.get('respuesta_fecha'); tel=r.get('telefono','') + h=t1.hour+t1.minute/60.0 + refri = es_t1(nom_up,d1) and (13<=h<15) + if not isinstance(t2,datetime): + sinr+=1 + print(f"{i:3} {tel:13} {t1.strftime('%H:%M:%S'):11} {'':11} {'':>5}") + continue + if refri: + dif=(t2-t1).total_seconds()/60.0 + m=round(dif-45) if dif>45 else round(dif) + if m<0: m=0 + nota=" refrig(-45 si >45)" + else: + m=mins_laborales(nom_up,t1,t2); nota="" + resp+=1; suma+=m + print(f"{i:3} {tel:13} {t1.strftime('%H:%M:%S'):11} {t2.strftime('%H:%M:%S'):11} {m:>5}{nota}") + print("-"*60) + print(f"Respondidos: {resp} | Sin responder: {sinr} | Promedio: {round(suma/resp) if resp else 0} min") diff --git a/backend/diag_dayana_grafica_6_7.py b/backend/diag_dayana_grafica_6_7.py new file mode 100644 index 0000000..ef6575f --- /dev/null +++ b/backend/diag_dayana_grafica_6_7.py @@ -0,0 +1,102 @@ +# diag_dayana_grafica_6_7.py +# Replica EXACTA de las dos series del grafico para DAYANA dias 6 y 7: +# NARANJA = por fecha de ASIGNACION (todos, con -45 refrigerio) +# AZUL = por fecha de RESPUESTA (solo respondidos, -45 refrigerio, y EXCLUYE >960 min) +from datetime import datetime, timedelta, date +from data_manager_v2 import DataManager + +ANO, MES = 2026, 8 +DIAS = [6, 7] +NOMBRE = "DAYANA" +LIMITE = 960 + +dm = DataManager() +asig = dm.traer_asignacion_respuesta() +act_rows = dm.traer_actividad_asesores() +uid2name = {r["user_id"]: (r.get("user_name") or "") for r in asig if r.get("user_id") is not None} + +from collections import defaultdict +act = defaultdict(lambda: defaultdict(int)) +for a in act_rows: + uid=a.get("user_id"); ts=a.get("created_at") + if uid not in uid2name or not isinstance(ts,datetime): continue + nom=uid2name[uid].upper(); d=ts.date(); h=ts.hour+ts.minute/60.0; key=(nom,d) + if d.weekday()==6: continue + if d.weekday()==5: + if 9<=h<13: act[key]["sab_manana"]+=1 + elif 14<=h<18: act[key]["sab_tarde"]+=1 + else: + if 13<=h<18: act[key]["tarde"]+=1 + elif 18<=h<22: act[key]["noche"]+=1 + +def turno_dia(nom,d): + if d.weekday()==6: return None + fr=act.get((nom,d)) + if not fr: return None + if d.weekday()==5: + m=fr.get("sab_manana",0); t=fr.get("sab_tarde",0) + if m==0 and t==0: return None + return [(9,13)] if m>=t else [(14,18)] + t=fr.get("tarde",0); n=fr.get("noche",0) + if t==0 and n==0: return None + return [(9,13),(18,22)] if n>t else [(9,13),(14,18)] + +def es_t1(nom,d): + fr=act.get((nom,d)) + if not fr or d.weekday()>=5: return False + t=fr.get("tarde",0); n=fr.get("noche",0) + return (not (n>t)) and (t>0 or n>0) + +def _dt(d,hh): return datetime(d.year,d.month,d.day)+timedelta(hours=hh) + +def mins_lab(nom,t1,t2): + if not isinstance(t1,datetime) or not isinstance(t2,datetime) or t2<=t1: return 0 + total=0.0; cur=t1; s=0 + while cur=fin: continue + a=max(cur,ini); b=min(t2,fin) + if b>a: total+=(b-a).total_seconds()/60.0 + cur=fin + if cur>=t2: break + if cur.date()==d: cur=_dt(d,24); s+=1 + return round(total) + +def calc_min(nom,t1,t2,d1): + h=t1.hour+t1.minute/60.0 + if es_t1(nom,d1) and (13<=h<15): + dif=(t2-t1).total_seconds()/60.0 + m=round(dif-45) if dif>45 else round(dif) + return max(m,0) + return mins_lab(nom,t1,t2) + +for DIA in DIAS: + d1=date(ANO,MES,DIA) + print(f"\n{'#'*72}") + print(f"DAYANA — DIA {DIA:02d}/{MES:02d}/{ANO}") + print(f"{'#'*72}") + + # ---- AZUL: respondidos ese dia (excluye >960) ---- + print(f"\n>>> GRAFICO AZUL (por fecha de RESPUESTA): respondidos el {DIA} (excluye >960 min)") + print(f"{'telefono':13} {'asignado':14} {'respondio':14} {'min':>6} nota") + print("-"*60) + az=[]; tard=0 + for r in asig: + if NOMBRE not in (r.get("user_name") or "").upper(): continue + t1=r.get("created_at"); t2=r.get("respuesta_fecha") + if not isinstance(t1,datetime) or not isinstance(t2,datetime): continue + if t2.date()!=d1: continue + m=calc_min((r.get('user_name') or '').upper(),t1,t2,t1.date()) + nota = " (asignado otro dia)" if t1.date()!=d1 else "" + if m>LIMITE: + tard+=1 + print(f"{r.get('telefono',''):13} {t1.strftime('%d/%m %H:%M'):14} {t2.strftime('%d/%m %H:%M'):14} {m:>6} TARDIO(>960) fuera{nota}") + continue + az.append(m) + print(f"{r.get('telefono',''):13} {t1.strftime('%d/%m %H:%M'):14} {t2.strftime('%d/%m %H:%M'):14} {m:>6}{nota}") + print("-"*60) + print(f" {len(az)} respondidos | promedio {round(sum(az)/len(az)) if az else 0} min | tardios(>960): {tard}") diff --git a/backend/diag_diana_8_9_10.py b/backend/diag_diana_8_9_10.py new file mode 100644 index 0000000..127afb9 --- /dev/null +++ b/backend/diag_diana_8_9_10.py @@ -0,0 +1,114 @@ +# diag_diana_8_9_10.py +# Dias 8, 9 y 10 de AGOSTO 2026 de DIANA: turno detectado (o LIBRE) y tiempos. +# Aplica la regla del refrigerio (13-15 turno1: diferencia directa, -45 si supera 45). +from datetime import datetime, timedelta, date +from data_manager_v2 import DataManager + +ANO, MES = 2026, 8 +DIAS = [8, 9, 10] +NOMBRE = "DIANA" + +dm = DataManager() +asig = dm.traer_asignacion_respuesta() +act_rows = dm.traer_actividad_asesores() + +uid2name = {} +for r in asig: + if r.get("user_id") is not None: + uid2name[r["user_id"]] = r.get("user_name") or "" + +from collections import defaultdict +act = defaultdict(lambda: defaultdict(int)) +for a in act_rows: + uid = a.get("user_id"); ts = a.get("created_at") + if uid not in uid2name or not isinstance(ts, datetime): continue + nom = uid2name[uid].upper(); d = ts.date(); h = ts.hour + ts.minute/60.0 + key=(nom,d) + if d.weekday()==6: continue + if d.weekday()==5: + if 9<=h<13: act[key]["sab_manana"]+=1 + elif 14<=h<18: act[key]["sab_tarde"]+=1 + else: + if 13<=h<18: act[key]["tarde"]+=1 + elif 18<=h<22: act[key]["noche"]+=1 + +def turno_dia(nom,d): + if d.weekday()==6: return None + fr=act.get((nom,d)) + if not fr: return None + if d.weekday()==5: + m=fr.get("sab_manana",0); t=fr.get("sab_tarde",0) + if m==0 and t==0: return None + return [(9,13)] if m>=t else [(14,18)] + t=fr.get("tarde",0); n=fr.get("noche",0) + if t==0 and n==0: return None + return [(9,13),(18,22)] if n>t else [(9,13),(14,18)] + +def es_t1(nom,d): + fr=act.get((nom,d)) + if not fr or d.weekday()>=5: return False + t=fr.get("tarde",0); n=fr.get("noche",0) + return (not (n>t)) and (t>0 or n>0) + +def _dt(d,hh): return datetime(d.year,d.month,d.day)+timedelta(hours=hh) + +def mins_laborales(nom,t1,t2): + if not isinstance(t1,datetime) or not isinstance(t2,datetime) or t2<=t1: return 0 + total=0.0; cur=t1; s=0 + while cur=fin: continue + a=max(cur,ini); b=min(t2,fin) + if b>a: total+=(b-a).total_seconds()/60.0 + cur=fin + if cur>=t2: break + if cur.date()==d: cur=_dt(d,24); s+=1 + return round(total) + +for DIA in DIAS: + d1=date(ANO,MES,DIA) + filas=[] + for r in asig: + if NOMBRE not in (r.get("user_name") or "").upper(): continue + t1=r.get("created_at") + if not isinstance(t1,datetime): continue + if t1.year==ANO and t1.month==MES and t1.day==DIA: filas.append(r) + # turno del dia (por actividad de Diana) + nom_up = (filas[0].get("user_name") or "").upper() if filas else NOMBRE + tt=turno_dia(nom_up,d1) + if tt is None: + turno="LIBRE / DESCANSO (sin actividad ese dia)" + elif tt==[(9,13),(14,18)]: turno="TURNO 1 (9-18)" + elif tt==[(9,13),(18,22)]: turno="TURNO 2 (9-13 + 18-22)" + else: turno=str(tt) + + print(f"\n{'='*66}") + print(f"DIANA — {DIA:02d}/{MES:02d}/{ANO} -> {turno}") + print(f"Total asignados: {len(filas)}") + print(f"{'='*66}") + print(f"{'#':3} {'telefono':13} {'asignado':11} {'respondio':11} {'min':>5} nota") + print("-"*60) + resp=0; sinr=0; suma=0 + for i,r in enumerate(sorted(filas,key=lambda x:x['created_at']),1): + t1=r['created_at']; t2=r.get('respuesta_fecha'); tel=r.get('telefono','') + h=t1.hour+t1.minute/60.0 + refri = es_t1(nom_up,d1) and (13<=h<15) + if not isinstance(t2,datetime): + sinr+=1 + print(f"{i:3} {tel:13} {t1.strftime('%H:%M:%S'):11} {'':11} {'':>5}") + continue + if refri: + dif=(t2-t1).total_seconds()/60.0 + m=round(dif-45) if dif>45 else round(dif) + if m<0: m=0 + nota=" refrigerio (-45 si >45)" + else: + m=mins_laborales(nom_up,t1,t2); nota="" + resp+=1; suma+=m + print(f"{i:3} {tel:13} {t1.strftime('%H:%M:%S'):11} {t2.strftime('%H:%M:%S'):11} {m:>5}{nota}") + print("-"*60) + print(f"Respondidos: {resp} | Sin responder: {sinr} | Promedio: {round(suma/resp) if resp else 0} min") diff --git a/backend/diag_diana_resp_ago11.py b/backend/diag_diana_resp_ago11.py new file mode 100644 index 0000000..432d0af --- /dev/null +++ b/backend/diag_diana_resp_ago11.py @@ -0,0 +1,100 @@ +# diag_diana_resp_ago11.py +# NUEVO grafico (por FECHA DE RESPUESTA): leads que DIANA RESPONDIO el 11/08/2026, +# sin importar cuando se asignaron. Muestra asignacion, respuesta y minutos. +from datetime import datetime, timedelta, date +from data_manager_v2 import DataManager + +ANO, MES, DIA_RESP = 2026, 8, 11 # dia de RESPUESTA +NOMBRE = "DIANA" + +dm = DataManager() +asig = dm.traer_asignacion_respuesta() +act_rows = dm.traer_actividad_asesores() + +uid2name = {} +for r in asig: + if r.get("user_id") is not None: + uid2name[r["user_id"]] = r.get("user_name") or "" + +from collections import defaultdict +act = defaultdict(lambda: defaultdict(int)) +for a in act_rows: + uid=a.get("user_id"); ts=a.get("created_at") + if uid not in uid2name or not isinstance(ts,datetime): continue + nom=uid2name[uid].upper(); d=ts.date(); h=ts.hour+ts.minute/60.0; key=(nom,d) + if d.weekday()==6: continue + if d.weekday()==5: + if 9<=h<13: act[key]["sab_manana"]+=1 + elif 14<=h<18: act[key]["sab_tarde"]+=1 + else: + if 13<=h<18: act[key]["tarde"]+=1 + elif 18<=h<22: act[key]["noche"]+=1 + +def turno_dia(nom,d): + if d.weekday()==6: return None + fr=act.get((nom,d)) + if not fr: return None + if d.weekday()==5: + m=fr.get("sab_manana",0); t=fr.get("sab_tarde",0) + if m==0 and t==0: return None + return [(9,13)] if m>=t else [(14,18)] + t=fr.get("tarde",0); n=fr.get("noche",0) + if t==0 and n==0: return None + return [(9,13),(18,22)] if n>t else [(9,13),(14,18)] + +def es_t1(nom,d): + fr=act.get((nom,d)) + if not fr or d.weekday()>=5: return False + t=fr.get("tarde",0); n=fr.get("noche",0) + return (not (n>t)) and (t>0 or n>0) + +def _dt(d,hh): return datetime(d.year,d.month,d.day)+timedelta(hours=hh) + +def mins_laborales(nom,t1,t2): + if not isinstance(t1,datetime) or not isinstance(t2,datetime) or t2<=t1: return 0 + total=0.0; cur=t1; s=0 + while cur=fin: continue + a=max(cur,ini); b=min(t2,fin) + if b>a: total+=(b-a).total_seconds()/60.0 + cur=fin + if cur>=t2: break + if cur.date()==d: cur=_dt(d,24); s+=1 + return round(total) + +# filtrar Diana + RESPUESTA el 11 +filas=[] +for r in asig: + if NOMBRE not in (r.get("user_name") or "").upper(): continue + t1=r.get("created_at"); t2=r.get("respuesta_fecha") + if not isinstance(t1,datetime) or not isinstance(t2,datetime): continue # sin respuesta -> fuera + if t2.year==ANO and t2.month==MES and t2.day==DIA_RESP: + filas.append(r) + +print(f"\n=== DIANA — leads RESPONDIDOS el {DIA_RESP:02d}/{MES:02d}/{ANO} (grafico por fecha respuesta) ===\n") +print(f"Total respondidos ese dia: {len(filas)}\n") +print(f"{'#':3} {'telefono':13} {'asignado':17} {'respondio':17} {'min':>5} nota") +print("-"*74) +suma=0 +for i,r in enumerate(sorted(filas, key=lambda x: x['respuesta_fecha']),1): + t1=r['created_at']; t2=r['respuesta_fecha']; tel=r.get('telefono','') + d1=t1.date(); h=t1.hour+t1.minute/60.0 + refri = es_t1((r.get('user_name') or '').upper(), d1) and (13<=h<15) + if refri: + dif=(t2-t1).total_seconds()/60.0 + m=round(dif-45) if dif>45 else round(dif) + if m<0: m=0 + nota=" refrig(-45)" + else: + m=mins_laborales((r.get('user_name') or '').upper(),t1,t2); nota="" + # marca si se asigno OTRO dia + if t1.date()!=t2.date(): nota += f" (asignado {t1.strftime('%d/%m')})" + suma+=m + print(f"{i:3} {tel:13} {t1.strftime('%d/%m %H:%M'):17} {t2.strftime('%d/%m %H:%M'):17} {m:>5}{nota}") +print("-"*74) +print(f"Promedio del dia {DIA_RESP} (por fecha respuesta): {round(suma/len(filas)) if filas else 0} min") diff --git a/backend/diag_diff_asignados_diana13.py b/backend/diag_diff_asignados_diana13.py new file mode 100644 index 0000000..c195e49 --- /dev/null +++ b/backend/diag_diff_asignados_diana13.py @@ -0,0 +1,44 @@ +# diag_diff_asignados_diana13.py +# Compara las 2 fuentes de "leads asignados" para DIANA el dia 13/08/2026: +# A = traer_leads_asignados (la de la TARJETA) +# B = traer_asignacion_respuesta (la del GRAFICO) +# Muestra los telefonos de cada una y cuales difieren. +from datetime import datetime +from data_manager_v2 import DataManager + +ANO, MES, DIA = 2026, 8, 13 +NOMBRE = "DIANA" +dm = DataManager() + +def _d(v): return v.date() if isinstance(v, datetime) else None + +A = {} # telefono -> hora asignacion (TARJETA) +for r in dm.traer_leads_asignados(): + if NOMBRE not in (r.get("user_name") or "").upper(): continue + f = r.get("created_at") + if _d(f) and f.year==ANO and f.month==MES and f.day==DIA: + A[str(r.get("telefono") or "").strip()] = f + +B = {} # telefono -> hora asignacion (GRAFICO) +for r in dm.traer_asignacion_respuesta(): + if NOMBRE not in (r.get("user_name") or "").upper(): continue + f = r.get("created_at") + if _d(f) and f.year==ANO and f.month==MES and f.day==DIA: + B[str(r.get("telefono") or "").strip()] = f + +print(f"\n=== DIANA dia {DIA} — comparacion de fuentes ===\n") +print(f"A) TARJETA (traer_leads_asignados): {len(A)} telefonos") +print(f"B) GRAFICO (traer_asignacion_respuesta): {len(B)} telefonos\n") + +print("Telefonos en A (tarjeta):") +for t in sorted(A): print(f" {t} asignado {A[t].strftime('%H:%M:%S')}") +print("\nTelefonos en B (grafico):") +for t in sorted(B): print(f" {t} asignado {B[t].strftime('%H:%M:%S')}") + +solo_A = set(A) - set(B) +solo_B = set(B) - set(A) +print("\n--- DIFERENCIAS ---") +if solo_A: print(f" Solo en TARJETA (A), no en grafico: {sorted(solo_A)}") +if solo_B: print(f" Solo en GRAFICO (B), no en tarjeta: {sorted(solo_B)}") +if not solo_A and not solo_B: + print(" Los telefonos son IDENTICOS. La diferencia debe ser cache (reinicia backend).") diff --git a/backend/diag_excel_arequipa.py b/backend/diag_excel_arequipa.py new file mode 100644 index 0000000..96780ca --- /dev/null +++ b/backend/diag_excel_arequipa.py @@ -0,0 +1,42 @@ +""" +Diagnostico: baja el Excel de Arequipa de SharePoint y muestra que hojas +y que tablas nombradas detecta openpyxl, y cuantas filas tiene cada una. +Asi sabemos por que el sync cae al fallback (10000 filas). +""" +import os, io, openpyxl, requests, msal + +CLIENT_ID = os.environ["MS_CLIENT_ID"] +TENANT_ID = os.environ["MS_TENANT_ID"] +REFRESH_TOKEN = os.environ["MS_REFRESH_TOKEN"] +SHAREPOINT_SITE = "escuelarefrigeracion.sharepoint.com" +SITE_PATH = "/sites/ASESORASCOMERCIALES" +SUBFOLDER = "2. BASE PROSPECTOS/BASE GENERAL" +SCOPES = ["Sites.Read.All", "Files.Read.All"] + +ARCHIVOS = ["Base Sede Arequipa.xlsx", "Base Diana Chavez.xlsx"] # uno malo + uno bueno para comparar + +app = msal.PublicClientApplication(CLIENT_ID, authority=f"https://login.microsoftonline.com/{TENANT_ID}") +tok = app.acquire_token_by_refresh_token(REFRESH_TOKEN, scopes=SCOPES)["access_token"] +h = {"Authorization": f"Bearer {tok}"} + +sid = requests.get(f"https://graph.microsoft.com/v1.0/sites/{SHAREPOINT_SITE}:{SITE_PATH}", headers=h).json()["id"] +drives = requests.get(f"https://graph.microsoft.com/v1.0/sites/{sid}/drives", headers=h).json()["value"] +did = next((d["id"] for d in drives if "document" in d["name"].lower() or "compartid" in d["name"].lower()), drives[0]["id"]) +items = requests.get(f"https://graph.microsoft.com/v1.0/drives/{did}/root:/{SUBFOLDER}:/children", headers=h).json()["value"] + +for nombre in ARCHIVOS: + it = next((x for x in items if x["name"] == nombre), None) + print("\n" + "="*60) + if not it: + print(f"❌ NO ENCONTRADO en SharePoint: '{nombre}'") + print(" Archivos disponibles:", [x["name"] for x in items if x["name"].endswith(".xlsx")]) + continue + print(f"📄 {nombre}") + cont = requests.get(f"https://graph.microsoft.com/v1.0/drives/{did}/items/{it['id']}/content", headers=h).content + wb = openpyxl.load_workbook(io.BytesIO(cont), data_only=True) + for ws in wb.worksheets: + print(f" Hoja: '{ws.title}' dims={ws.dimensions} max_row={ws.max_row}") + tbls = list(ws.tables.keys()) + print(f" Tablas nombradas: {tbls}") + for tn, tb in ws.tables.items(): + print(f" · {tn} -> ref={tb.ref}") diff --git a/backend/diag_fecha_contacto.py b/backend/diag_fecha_contacto.py new file mode 100644 index 0000000..71e555e --- /dev/null +++ b/backend/diag_fecha_contacto.py @@ -0,0 +1,45 @@ +""" +Diagnostico: para una lista de telefonos, muestra la fecha de creacion del +contacto (contacts.created_at) y la fecha del primer/ultimo mensaje. +""" +import os +import psycopg2 +from dotenv import load_dotenv + +load_dotenv() + +PG_HOST = os.getenv("PG_HOST") +PG_DB = os.getenv("PG_DATABASE") or os.getenv("PG_DB") +PG_USER = os.getenv("PG_USER") +PG_PASS = os.getenv("PG_PASSWORD") or os.getenv("PG_PASS") +PG_PORT = os.getenv("PG_PORT", "5432") + +TELEFONOS = ["948400152", "953932854", "959735396", "972132288"] + +conn = psycopg2.connect(host=PG_HOST, dbname=PG_DB, user=PG_USER, + password=PG_PASS, port=PG_PORT, connect_timeout=30) +cur = conn.cursor() + +print(f"{'TELEFONO':14} {'phone_number':16} {'CONTACTO_CREADO':22} {'1ER_MSJE':22} {'ULT_MSJE':22}") +print("-" * 100) + +for tel in TELEFONOS: + cur.execute(""" + SELECT c.phone_number, c.created_at, + MIN(m.created_at) AS primer_msje, + MAX(m.created_at) AS ultimo_msje + FROM contacts c + LEFT JOIN conversations cv ON cv.contact_id = c.id + LEFT JOIN messages m ON m.conversation_id = cv.id + WHERE REPLACE(REPLACE(c.phone_number, '+51',''), '+','') LIKE %s + GROUP BY c.phone_number, c.created_at + ORDER BY c.created_at ASC + """, (f"%{tel}",)) + rows = cur.fetchall() + if not rows: + print(f"{tel:14} {'(no encontrado)':16}") + continue + for ph, creado, primer, ultimo in rows: + print(f"{tel:14} {str(ph):16} {str(creado):22} {str(primer):22} {str(ultimo):22}") + +conn.close() diff --git a/backend/diag_importe.py b/backend/diag_importe.py new file mode 100644 index 0000000..5e8c0b7 --- /dev/null +++ b/backend/diag_importe.py @@ -0,0 +1,12 @@ +# diag_importe.py — Verifica matriz_asignados (pivot asesor x dia). +import services as S + +d = S.matriz_asignados("2026", "7", "TODOS") +print("MES 7 2026 TOTAL asignados:", d["total"]) +print("dias (columnas):", d["dias"][:5], "...", d["dias"][-3:]) +print("\nPor asesor (total):") +for f in d["filas"]: + dias_con = {k:v for k,v in f["por_dia"].items() if v} + print(f" {f['asesor']:22} total={f['total']:4} dias con datos: {len(dias_con)}") +print("\ntotal_por_dia (primeros 10):", {k:v for k,v in list(d['total_por_dia'].items())[:10]}) +print("OK.") diff --git a/backend/diag_matri_50_vs_51.py b/backend/diag_matri_50_vs_51.py new file mode 100644 index 0000000..edfa294 --- /dev/null +++ b/backend/diag_matri_50_vs_51.py @@ -0,0 +1,58 @@ +# diag_matri_50_vs_51.py +# Busca la matricula de diferencia (dashboard 51 vs cmd 50) en TEAC agosto. +# Replica EXACTAMENTE el bloque de la matriz del dashboard y compara. +from datetime import datetime +from collections import defaultdict +import services as S +import leads_logic as L + +ANO, MES = 2026, 8 +ase = S._asesores(); amap = ase["map"]; lista = ase["lista"] + +# ---- REPLICA EXACTA del bloque de matriz del dashboard ---- +GRUPOS = ["TEAC","TERC","SEMINARIOS","OTROS"] +prog_tot = defaultdict(int) +detalle = defaultdict(list) # grupo -> [matriculas] +for m in S._matriculas_crudas(): + vend = S._norm_asesor(m.get("dsc_vendedor"), amap) + if not vend: + continue + estado = str(m.get("estado_matricula") or "").strip().upper() + if estado == "ANU": + continue + fm = L._to_date(m.get("fch_matricula")) + if not L._en_periodo(fm, str(ANO), str(MES), "TODOS"): + continue + grupo = L.grupo_programa_curso(m.get("dsc_programa")) + prog_tot[grupo] += 1 + detalle[grupo].append(m) + +print("=== Conteo por grupo (replica dashboard) ===") +tot=0 +for g in GRUPOS: + print(f" {g:12} {prog_tot[g]}") + tot+=prog_tot[g] +print(f" TOTAL {tot}") + +# ---- Detalle de TEAC: mostrar TODAS con num_matricula para ver duplicados/algo raro ---- +print(f"\n=== TEAC — {len(detalle['TEAC'])} matriculas (con num_matricula y num_indice) ===") +for m in sorted(detalle['TEAC'], key=lambda x: str(x.get('fch_matricula'))): + fm = L._to_date(m.get('fch_matricula')) + print(f" mat={m.get('num_matricula')} idx={m.get('num_indice')} " + f"{S._norm_asesor(m.get('dsc_vendedor'),amap):16} {str(fm)[:10]} " + f"est={m.get('estado_matricula')} | {str(m.get('dsc_programa'))[:40]}") + +# ---- Ver si hay matriculas cuyo asesor NO cae en los 8 (por eso el dashboard podria contar distinto) ---- +print(f"\n=== Matriculas agosto con asesor que NO es de los 8 (quedan fuera del cmd) ===") +n_fuera=0 +for m in S._matriculas_crudas(): + estado = str(m.get("estado_matricula") or "").strip().upper() + if estado == "ANU": continue + fm = L._to_date(m.get("fch_matricula")) + if not (fm and fm.year==ANO and fm.month==MES): continue + vend = S._norm_asesor(m.get("dsc_vendedor"), amap) + if not vend: + n_fuera += 1 + print(f" mat={m.get('num_matricula')} asesor_original='{m.get('dsc_vendedor')}' " + f"grupo={L.grupo_programa_curso(m.get('dsc_programa'))} est={estado}") +print(f" -> {n_fuera} matriculas con asesor fuera de los 8") diff --git a/backend/diag_matri_51_perdida.py b/backend/diag_matri_51_perdida.py new file mode 100644 index 0000000..98a68cc --- /dev/null +++ b/backend/diag_matri_51_perdida.py @@ -0,0 +1,51 @@ +# diag_matri_51_perdida.py +# Busca la matricula #51: revisa TODAS las de agosto (incluso ANU/RET) para ver +# cual pudo aparecer/desaparecer. Va directo a SQL Server (fresco, sin cache). +from datetime import datetime +import services as S +import leads_logic as L + +ANO, MES = 2026, 8 +ase = S._asesores(); amap = ase["map"] + +# TODAS las matriculas de agosto (sin excluir estado), directo de traer_matriculas +todas = [] +for m in S.get_dm().traer_matriculas(): + fm = L._to_date(m.get("fch_matricula")) + if not (fm and fm.year==ANO and fm.month==MES): continue + todas.append(m) + +print(f"=== TODAS las matriculas de agosto {ANO} (cualquier estado): {len(todas)} ===\n") +from collections import Counter +est = Counter(str(m.get("estado_matricula") or "").strip().upper() for m in todas) +print("Por estado:", dict(est)) + +# las que cuentan (no ANU, asesor de los 8) +cuentan = 0 +no_asesor = 0 +anu = 0 +for m in todas: + e = str(m.get("estado_matricula") or "").strip().upper() + if e == "ANU": + anu += 1; continue + v = S._norm_asesor(m.get("dsc_vendedor"), amap) + if not v: + no_asesor += 1 + print(f" [FUERA 8] mat={m.get('num_matricula')} asesor='{m.get('dsc_vendedor')}' est={e} " + f"grupo={L.grupo_programa_curso(m.get('dsc_programa'))}") + continue + cuentan += 1 + +print(f"\nCuentan (no ANU + asesor de los 8): {cuentan}") +print(f"Anuladas (ANU): {anu}") +print(f"Con asesor fuera de los 8: {no_asesor}") + +# mostrar TODAS con su estado para ver si hay algo raro (RET, SUS, etc.) +print(f"\n=== Detalle: matriculas con estado != ALU/PRE (posibles casos raros) ===") +for m in todas: + e = str(m.get("estado_matricula") or "").strip().upper() + if e not in ("ALU","PRE"): + v = S._norm_asesor(m.get("dsc_vendedor"), amap) + fm = L._to_date(m.get("fch_matricula")) + print(f" mat={m.get('num_matricula')} est={e} asesor={v or m.get('dsc_vendedor')} " + f"{str(fm)[:10]} grupo={L.grupo_programa_curso(m.get('dsc_programa'))}") diff --git a/backend/diag_matriculas_agosto_detalle.py b/backend/diag_matriculas_agosto_detalle.py new file mode 100644 index 0000000..b19ef19 --- /dev/null +++ b/backend/diag_matriculas_agosto_detalle.py @@ -0,0 +1,51 @@ +# diag_matriculas_agosto_detalle.py +# Las 51 matriculas de AGOSTO 2026 (todos los asesores de los 8), con su +# dsc_det_programa (detalle del programa) y grupo. Mismo criterio del dashboard: +# dsc_vendedor de los 8, estado != ANU, fch_matricula en agosto. +from datetime import datetime +from collections import defaultdict, Counter +import services as S +import leads_logic as L + +ANO, MES = 2026, 8 + +ase = S._asesores(); amap = ase["map"] + +filas = [] +for m in S._matriculas_crudas(): + vend = S._norm_asesor(m.get("dsc_vendedor"), amap) + if not vend: + continue + estado = str(m.get("estado_matricula") or "").strip().upper() + if estado == "ANU": + continue + fm = L._to_date(m.get("fch_matricula")) + if not fm or fm.year != ANO or fm.month != MES: + continue + filas.append({ + "vendedor": vend, + "num_matricula": m.get("num_matricula"), + "estado": estado, + "fch": fm.strftime("%d/%m/%Y"), + "dsc_programa": str(m.get("dsc_programa") or ""), + "dsc_det": str(m.get("dsc_promocion") or ""), # dsc_det_programa + "grupo": L.grupo_programa_curso(m.get("dsc_programa")), + }) + +print(f"\n=== MATRICULAS AGOSTO {ANO} — todos los asesores (total: {len(filas)}) ===\n") +print(f"{'#':3} {'vendedor':16} {'estado':6} {'fch':11} {'grupo':10} dsc_det_programa") +print("-"*100) +for i, f in enumerate(sorted(filas, key=lambda x: (x['grupo'], x['vendedor'])), 1): + print(f"{i:3} {f['vendedor'][:16]:16} {f['estado']:6} {f['fch']:11} {f['grupo']:10} {f['dsc_det'][:45]}") + +print("-"*100) +print("\n=== RESUMEN por dsc_det_programa (cuantas matriculas de cada uno) ===") +cnt = Counter(f["dsc_det"] for f in filas) +for prog, n in sorted(cnt.items(), key=lambda x: -x[1]): + print(f" {n:3} {prog}") + +print("\n=== RESUMEN por GRUPO (debe cuadrar con la matriz del dashboard) ===") +gr = Counter(f["grupo"] for f in filas) +for g in ["TEAC","TERC","SEMINARIOS","OTROS"]: + print(f" {g:12} {gr.get(g,0)}") +print(f" {'TOTAL':12} {len(filas)}") diff --git a/backend/diag_multi.py b/backend/diag_multi.py new file mode 100644 index 0000000..b4e4bd0 --- /dev/null +++ b/backend/diag_multi.py @@ -0,0 +1,22 @@ +# diag_multi.py — Revisa programa_pautas y la suma por num_indice. +import services as S +from cache_manager import cache_invalidate + +# forzar leer fresco de supabase +cache_invalidate("programa_pautas") +cache_invalidate("pautas_de_indice") + +pp = S._programa_pautas() # {num_indice: [pautas]} SOLO de la tabla nueva +print("== Tabla programa_pautas (lo que se guardo) ==") +if not pp: + print(" (VACIA - no se guardo nada, o no se lee)") +for ni, ps in pp.items(): + print(f" num_indice {ni} -> {ps}") + +ppi = S._pautas_de_indice() # combinado basebi + nueva +print("\n== num_indices con VARIAS pautas (basebi + nueva) ==") +multi = {ni: ps for ni, ps in ppi.items() if len(ps) > 1} +for ni, ps in list(multi.items())[:15]: + print(f" num_indice {ni} -> {ps}") +if not multi: + print(" (ninguno con varias)") diff --git a/backend/diag_numero_chatwoot.py b/backend/diag_numero_chatwoot.py new file mode 100644 index 0000000..5bc9aa9 --- /dev/null +++ b/backend/diag_numero_chatwoot.py @@ -0,0 +1,54 @@ +# diag_numero_chatwoot.py - Ver los mensajes de un numero en Chatwoot y que campania hace match. +import os +from dotenv import load_dotenv +import psycopg2 +load_dotenv() + +TEL = "8099166706" # buscar por terminacion (sin prefijo pais) + +dm_host=os.getenv("PG_HOST","191.98.134.81"); dm_db=os.getenv("PG_DATABASE","chatwoot_production") +dm_user=os.getenv("PG_USER","postgres"); dm_pass=os.getenv("PG_PASSWORD",""); dm_port=os.getenv("PG_PORT","5432") +conn=psycopg2.connect(host=dm_host,dbname=dm_db,user=dm_user,password=dm_pass,port=dm_port) +cur=conn.cursor() + +# mensajes del contacto (por terminacion del telefono) +cur.execute(""" +SELECT c.phone_number, m.id, m.created_at, m.sender_type, LEFT(m.content, 90) +FROM messages m +JOIN conversations cv ON m.conversation_id=cv.id +JOIN contacts c ON cv.contact_id=c.id +WHERE c.phone_number LIKE %s +ORDER BY m.id ASC +""", ('%'+TEL,)) +rows=cur.fetchall() +print(f"\n=== Mensajes en Chatwoot del numero terminado en {TEL} ===\n") +if not rows: + print(" (no se encontro el numero en Chatwoot)") +for ph,mid,fecha,stype,cont in rows: + print(f" [{ph}] {fecha} {stype:8} | {cont}") + +# ¿el primer mensaje del contacto hace match con alguna campania por texto? +print("\n=== Campanias cuyo texto aparece en algun mensaje de este numero ===") +cur.execute(""" +SELECT map.codigo, map.cargo, map.fecha_inicio, map.fecha_fin, map.frase +FROM (VALUES + (' 🖥️ Estoy interesado en sus Seminarios','Seminarios','74a','2026-04-01','2026-07-30'), + ('👾Estoy interesado en el Seminario Refrigeración','CO2','80a','2026-05-15','2026-07-30') +) AS map(frase, cargo, codigo, fecha_inicio, fecha_fin) +WHERE EXISTS ( + SELECT 1 FROM messages m + JOIN conversations cv ON m.conversation_id=cv.id + JOIN contacts c ON cv.contact_id=c.id + WHERE c.phone_number LIKE %s AND m.sender_type='Contact' + AND m.content LIKE '%%'||map.frase||'%%' +) +""", ('%'+TEL,)) +mt=cur.fetchall() +if not mt: + print(" (ninguna de las 2 frases de ejemplo hizo match textual - revisar frase exacta)") +for cod,cargo,fi,ff,frase in mt: + print(f" codigo={cod} cargo={cargo} rango={fi}..{ff} frase='{frase}'") + +conn.close() +print("\nNOTA: si el match textual SI existe pero la fecha del mensaje esta fuera") +print("del rango de la campania, por eso NO cuenta como pauta (queda Copito).") diff --git a/backend/diag_plantillas.py b/backend/diag_plantillas.py new file mode 100644 index 0000000..0c5d259 --- /dev/null +++ b/backend/diag_plantillas.py @@ -0,0 +1,12 @@ +# diag_plantillas.py — Verifica otros_general (endpoint liviano). +import time, services as S + +for mes in ["TODOS", "1", "2"]: + t0 = time.time() + d = S.otros_general("2026", mes, "TODOS") + seg = time.time() - t0 + p = d["matriz_plantillas"]["total"] + print(f"MES {mes:6} ({seg:.1f}s) plantillas_env={p['enviadas']} matric={p['matriculas']} " + f"always_filas={len(d['matriz_always']['filas'])} webform_filas={len(d['matriz_webform']['filas'])} " + f"asignados_total={d['matriz_asignados']['total']}") +print("OK. (2da vez el mismo mes debe ser instantaneo por cache)") diff --git a/backend/diag_roas_faltantes.py b/backend/diag_roas_faltantes.py new file mode 100644 index 0000000..cb108cc --- /dev/null +++ b/backend/diag_roas_faltantes.py @@ -0,0 +1,58 @@ +""" +Diagnostico ROAS: conjuntos del Meta CSV (enero) que NO se suman en la matriz +por sede, porque no tienen pauta ligada o su pauta no tiene sede en campanias. +""" +import services as S +import leads_logic as L + +ANO, MES = "2026", "1" # enero 2026 +SEDES = {"LIMA", "PIURA", "AREQUIPA", "TRUJILLO"} + +conj_pauta = S._conjunto_pauta() # {pauta: [conjunto,...]} +camp = S._campanias_sede_cargo() # {pauta: {sede, cargo}} + +conj_sede = {} +conj_pauta_map = {} +for pauta, conjuntos in conj_pauta.items(): + sp = camp.get(str(pauta).strip()) + sede = " ".join(str((sp or {}).get("sede") or "").upper().split()) + for co in conjuntos: + k = " ".join(str(co).split()).upper() + conj_pauta_map[k] = str(pauta).strip() + if sp and sede in SEDES: + conj_sede[k] = sede + +sin_sumar = {} +suma_ok = 0.0 +suma_falta = 0.0 +for f in S._meta_filas(): + fx = L._to_date(f.get("inicio")) + if not L._en_periodo(fx, ANO, MES, "TODOS"): + continue + k = " ".join(str(f.get("conjunto")).split()).upper() + imp = float(f.get("importe") or 0.0) + res = int(float(f.get("resultados") or 0.0)) + if k in conj_sede: + suma_ok += imp + continue + if k not in conj_pauta_map: + motivo = "SIN PAUTA" + else: + p = conj_pauta_map[k] + sp = camp.get(p) + motivo = (f"pauta '{p}' SIN campania" if not sp + else f"pauta '{p}' sede='{sp.get('sede')}' (fuera de las 4)") + d = sin_sumar.setdefault(k, {"importe": 0.0, "resultados": 0, "motivo": motivo}) + d["importe"] += imp + d["resultados"] += res + suma_falta += imp + +print("=== ENERO 2026 — conjuntos que NO se suman en ROAS ===\n") +print(f"{'CONJUNTO':45} {'IMPORTE':>12} {'RESULTADOS':>12} MOTIVO") +print("-"*100) +for k, d in sorted(sin_sumar.items(), key=lambda x: -x[1]["importe"]): + print(f"{k[:45]:45} {('$'+format(d['importe'],',.2f')):>12} {d['resultados']:>12,} {d['motivo']}") +print("-"*100) +print(f"{'IMPORTE QUE SÍ SUMA (4 sedes)':45} {('$'+format(suma_ok,',.2f')):>12}") +print(f"{'IMPORTE QUE NO SUMA (faltante)':45} {('$'+format(suma_falta,',.2f')):>12}") +print(f"Conjuntos distintos sin sumar: {len(sin_sumar)}") diff --git a/backend/diag_roas_faltantes_2026.py b/backend/diag_roas_faltantes_2026.py new file mode 100644 index 0000000..b8246d2 --- /dev/null +++ b/backend/diag_roas_faltantes_2026.py @@ -0,0 +1,73 @@ +""" +Diagnostico ROAS por MES (2026): para cada mes, los conjuntos del Meta CSV que +NO se suman en la matriz por sede (sin pauta / sin sede valida), con su importe. +""" +import services as S +import leads_logic as L + +ANO = "2026" +SEDES = {"LIMA", "PIURA", "AREQUIPA", "TRUJILLO"} +MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO", + "SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"] + +conj_pauta = S._conjunto_pauta() # {pauta: [conjunto,...]} +camp = S._campanias_sede_cargo() # {pauta: {sede, cargo}} + +conj_sede = {} +conj_pauta_map = {} +for pauta, conjuntos in conj_pauta.items(): + sp = camp.get(str(pauta).strip()) + sede = " ".join(str((sp or {}).get("sede") or "").upper().split()) + for co in conjuntos: + k = " ".join(str(co).split()).upper() + conj_pauta_map[k] = str(pauta).strip() + if sp and sede in SEDES: + conj_sede[k] = sede + +filas = S._meta_filas() + +for mi in range(1, 13): + mes = str(mi) + sin_sumar = {} + suma_ok = 0.0 + suma_falta = 0.0 + for f in filas: + fx = L._to_date(f.get("inicio")) + if not L._en_periodo(fx, ANO, mes, "TODOS"): + continue + k = " ".join(str(f.get("conjunto")).split()).upper() + imp = float(f.get("importe") or 0.0) + res = int(float(f.get("resultados") or 0.0)) + if k in conj_sede: + suma_ok += imp + continue + if k not in conj_pauta_map: + motivo = "SIN PAUTA" + else: + p = conj_pauta_map[k] + sp = camp.get(p) + motivo = (f"pauta '{p}' SIN campania" if not sp + else f"pauta '{p}' sede='{sp.get('sede')}' (fuera de las 4)") + d = sin_sumar.setdefault(k, {"importe": 0.0, "resultados": 0, "motivo": motivo}) + d["importe"] += imp + d["resultados"] += res + suma_falta += imp + + # solo imprimir meses con algo de data + if suma_ok == 0 and not sin_sumar: + continue + + print("=" * 100) + print(f" {MESES[mi-1]} {ANO}") + print("=" * 100) + if sin_sumar: + print(f"{'CONJUNTO':45} {'IMPORTE':>12} {'RESULTADOS':>12} MOTIVO") + print("-" * 100) + for k, d in sorted(sin_sumar.items(), key=lambda x: -x[1]["importe"]): + print(f"{k[:45]:45} {('$'+format(d['importe'],',.2f')):>12} {d['resultados']:>12,} {d['motivo']}") + else: + print(" (sin conjuntos faltantes)") + print("-" * 100) + print(f"{'IMPORTE QUE SÍ SUMA (4 sedes)':45} {('$'+format(suma_ok,',.2f')):>12}") + print(f"{'IMPORTE QUE NO SUMA (faltante)':45} {('$'+format(suma_falta,',.2f')):>12}") + print(f" Conjuntos distintos sin sumar: {len(sin_sumar)}\n") diff --git a/backend/diag_sin_mensaje.py b/backend/diag_sin_mensaje.py new file mode 100644 index 0000000..79b1b93 --- /dev/null +++ b/backend/diag_sin_mensaje.py @@ -0,0 +1,56 @@ +""" +Diagnostico: contactos de Chatwoot que NO tienen mensaje de campaña +(no entran por el query de Postgre de la cartera). +Muestra: telefono, fecha del contacto (contacts.created_at), +fecha del primer y ultimo mensaje (si tiene), y si tiene algun mensaje. +""" +import os +import psycopg2 +from dotenv import load_dotenv +import data_manager_v2 as dm # para reutilizar las campañas (frases de campaña) + +load_dotenv() + +PG_HOST = os.getenv("PG_HOST") +PG_DB = os.getenv("PG_DATABASE") or os.getenv("PG_DB") +PG_USER = os.getenv("PG_USER") +PG_PASS = os.getenv("PG_PASSWORD") or os.getenv("PG_PASS") +PG_PORT = os.getenv("PG_PORT", "5432") + +# Frases de campaña (mismas que usa la cartera / dashboard) +FRASES = [c[0] for c in dm._campanias_supabase()] + +conn = psycopg2.connect(host=PG_HOST, dbname=PG_DB, user=PG_USER, + password=PG_PASS, port=PG_PORT, connect_timeout=30) +cur = conn.cursor() + +# Construir condicion: contacto que NO tiene NINGUN mensaje que haga match con alguna frase +likes = " OR ".join(["m.content LIKE %s"] * len(FRASES)) +params = [f"%{fr}%" for fr in FRASES] + +sql = f""" +SELECT c.phone_number, c.created_at, + COUNT(m.id) AS total_msjes, + MIN(m.created_at) AS primer_msje, + MAX(m.created_at) AS ultimo_msje, + SUM(CASE WHEN ({likes}) THEN 1 ELSE 0 END) AS msjes_campania +FROM contacts c +LEFT JOIN conversations cv ON cv.contact_id = c.id +LEFT JOIN messages m ON m.conversation_id = cv.id AND m.sender_type = 'Contact' +GROUP BY c.phone_number, c.created_at +HAVING SUM(CASE WHEN ({likes}) THEN 1 ELSE 0 END) = 0 -- SIN mensaje de campaña +ORDER BY c.created_at DESC +LIMIT 100 +""" + +cur.execute(sql, params + params) +rows = cur.fetchall() + +print(f"Contactos SIN mensaje de campaña (primeros 100):") +print(f"{'phone_number':16} {'contacto_creado':22} {'#msjes':>7} {'primer_msje':22} {'ultimo_msje':22}") +print("-" * 95) +for ph, creado, tot, primer, ultimo, _camp in rows: + print(f"{str(ph):16} {str(creado):22} {tot:>7} {str(primer):22} {str(ultimo):22}") + +print(f"\nTotal mostrados: {len(rows)} (limite 100)") +conn.close() diff --git a/backend/diag_tiempo_diana_ago10.py b/backend/diag_tiempo_diana_ago10.py new file mode 100644 index 0000000..0f5474b --- /dev/null +++ b/backend/diag_tiempo_diana_ago10.py @@ -0,0 +1,102 @@ +# diag_tiempo_diana_ago10.py +# Detalle del 10 de AGOSTO 2026 para DIANA CHAVEZ: asignados, hora de asignacion, +# hora de respuesta y minutos laborales (misma logica del dashboard). Promedio al final. +# Ejecutar en backend/: python diag_tiempo_diana_ago10.py +from datetime import datetime, timedelta, date +from data_manager_v2 import DataManager + +ANO, MES, DIA = 2026, 8, 10 +NOMBRE = "DIANA" # user_name contiene esto + +dm = DataManager() +asig = dm.traer_asignacion_respuesta() +act_rows = dm.traer_actividad_asesores() + +uid2name = {} +for r in asig: + if r.get("user_id") is not None: + uid2name[r["user_id"]] = r.get("user_name") or "" + +from collections import defaultdict +act = defaultdict(lambda: defaultdict(int)) +for a in act_rows: + uid = a.get("user_id"); ts = a.get("created_at") + if uid not in uid2name or not isinstance(ts, datetime): continue + nom = uid2name[uid].upper(); d = ts.date(); h = ts.hour + ts.minute/60.0 + key = (nom, d) + if d.weekday() == 6: continue + if d.weekday() == 5: + if 9 <= h < 13: act[key]["sab_manana"] += 1 + elif 14 <= h < 18: act[key]["sab_tarde"] += 1 + else: + if 13 <= h < 18: act[key]["tarde"] += 1 + elif 18 <= h < 22: act[key]["noche"] += 1 + +def turno_dia(nom, d): + if d.weekday() == 6: return None + fr = act.get((nom, d)) + if not fr: return None + if d.weekday() == 5: + m = fr.get("sab_manana",0); t = fr.get("sab_tarde",0) + if m==0 and t==0: return None + return [(9,13)] if m>=t else [(14,18)] + t = fr.get("tarde",0); n = fr.get("noche",0) + if t==0 and n==0: return None + return [(9,13),(18,22)] if n>t else [(9,13),(14,18)] + +def _dt(d, hh): return datetime(d.year,d.month,d.day) + timedelta(hours=hh) + +def mins_laborales(nom, t1, t2): + if not isinstance(t1,datetime) or not isinstance(t2,datetime) or t2<=t1: return 0 + total=0.0; cur=t1; saltos=0 + while cur < t2 and saltos < 60: + d=cur.date(); bloques=turno_dia(nom,d) + if not bloques: + cur=_dt(d,24); saltos+=1; continue + for (hi,hf) in bloques: + ini=_dt(d,hi); fin=_dt(d,hf) + if t2<=ini: break + if cur>=fin: continue + a=max(cur,ini); b=min(t2,fin) + if b>a: total += (b-a).total_seconds()/60.0 + cur=fin + if cur>=t2: break + if cur.date()==d: + cur=_dt(d,24); saltos+=1 + return round(total) + +# filtrar Diana + 10 agosto por fecha de asignacion +filas=[] +for r in asig: + nom=(r.get("user_name") or "") + if NOMBRE not in nom.upper(): continue + t1=r.get("created_at") + if not isinstance(t1,datetime): continue + if not (t1.year==ANO and t1.month==MES and t1.day==DIA): continue + filas.append(r) + +d1=date(ANO,MES,DIA) +turno=None +if filas: + tt=turno_dia((filas[0].get("user_name") or "").upper(), d1) + turno="Turno 1 (9-18)" if tt==[(9,13),(14,18)] else ("Turno 2 (9-13+18-22)" if tt==[(9,13),(18,22)] else str(tt)) + +print(f"\n=== DIANA CHAVEZ — {DIA:02d}/{MES:02d}/{ANO} ===") +print(f"Turno detectado ese dia: {turno}") +print(f"Total asignados ese dia: {len(filas)}\n") +print(f"{'#':3} {'telefono':13} {'asignado':17} {'respondio':17} {'min':>5}") +print("-"*62) +resp=0; sinr=0; suma=0 +for i,r in enumerate(sorted(filas, key=lambda x: x['created_at']),1): + t1=r['created_at']; t2=r.get('respuesta_fecha') + nom_up=(r.get('user_name') or '').upper() + tel=r.get('telefono',''); a_str=t1.strftime('%H:%M:%S') + if not isinstance(t2,datetime): + sinr+=1 + print(f"{i:3} {tel:13} {a_str:17} {'':17} {'':>5}") # sin respuesta -> vacio + continue + m=mins_laborales(nom_up,t1,t2); resp+=1; suma+=m + print(f"{i:3} {tel:13} {a_str:17} {t2.strftime('%H:%M:%S'):17} {m:>5}") +print("-"*62) +print(f"Respondidos: {resp} | Sin responder: {sinr}") +print(f"Promedio (solo respondidos): {round(suma/resp) if resp else 0} min") diff --git a/backend/diag_tiempo_diana_ago3.py b/backend/diag_tiempo_diana_ago3.py new file mode 100644 index 0000000..c5188a5 --- /dev/null +++ b/backend/diag_tiempo_diana_ago3.py @@ -0,0 +1,110 @@ +# diag_tiempo_diana_ago3.py +# Detalle del 3 de AGOSTO 2026 para DIANA CHAVEZ: asignados, hora de asignacion, +# hora de respuesta y minutos. Marca los asignados en refrigerio (13:00-14:00 turno1), +# que son los que la GRAFICA excluye. +from datetime import datetime, timedelta, date +from data_manager_v2 import DataManager + +ANO, MES, DIA = 2026, 8, 3 +NOMBRE = "DIANA" + +dm = DataManager() +asig = dm.traer_asignacion_respuesta() +act_rows = dm.traer_actividad_asesores() + +uid2name = {} +for r in asig: + if r.get("user_id") is not None: + uid2name[r["user_id"]] = r.get("user_name") or "" + +from collections import defaultdict +act = defaultdict(lambda: defaultdict(int)) +for a in act_rows: + uid = a.get("user_id"); ts = a.get("created_at") + if uid not in uid2name or not isinstance(ts, datetime): continue + nom = uid2name[uid].upper(); d = ts.date(); h = ts.hour + ts.minute/60.0 + key = (nom, d) + if d.weekday() == 6: continue + if d.weekday() == 5: + if 9 <= h < 13: act[key]["sab_manana"] += 1 + elif 14 <= h < 18: act[key]["sab_tarde"] += 1 + else: + if 13 <= h < 18: act[key]["tarde"] += 1 + elif 18 <= h < 22: act[key]["noche"] += 1 + +def turno_dia(nom, d): + if d.weekday() == 6: return None + fr = act.get((nom, d)) + if not fr: return None + if d.weekday() == 5: + m = fr.get("sab_manana",0); t = fr.get("sab_tarde",0) + if m==0 and t==0: return None + return [(9,13)] if m>=t else [(14,18)] + t = fr.get("tarde",0); n = fr.get("noche",0) + if t==0 and n==0: return None + return [(9,13),(18,22)] if n>t else [(9,13),(14,18)] + +def es_t1(nom, d): + fr = act.get((nom,d)) + if not fr or d.weekday()>=5: return False + t=fr.get("tarde",0); n=fr.get("noche",0) + return (not (n>t)) and (t>0 or n>0) + +def _dt(d, hh): return datetime(d.year,d.month,d.day) + timedelta(hours=hh) + +def mins_laborales(nom, t1, t2): + if not isinstance(t1,datetime) or not isinstance(t2,datetime) or t2<=t1: return 0 + total=0.0; cur=t1; saltos=0 + while cur < t2 and saltos < 60: + d=cur.date(); bloques=turno_dia(nom,d) + if not bloques: + cur=_dt(d,24); saltos+=1; continue + for (hi,hf) in bloques: + ini=_dt(d,hi); fin=_dt(d,hf) + if t2<=ini: break + if cur>=fin: continue + a=max(cur,ini); b=min(t2,fin) + if b>a: total += (b-a).total_seconds()/60.0 + cur=fin + if cur>=t2: break + if cur.date()==d: + cur=_dt(d,24); saltos+=1 + return round(total) + +filas=[] +for r in asig: + nom=(r.get("user_name") or "") + if NOMBRE not in nom.upper(): continue + t1=r.get("created_at") + if not isinstance(t1,datetime): continue + if not (t1.year==ANO and t1.month==MES and t1.day==DIA): continue + filas.append(r) + +d1=date(ANO,MES,DIA) +tt=turno_dia((filas[0].get("user_name") or "").upper(), d1) if filas else None +turno="Turno 1 (9-18)" if tt==[(9,13),(14,18)] else ("Turno 2 (9-13+18-22)" if tt==[(9,13),(18,22)] else str(tt)) + +print(f"\n=== DIANA CHAVEZ — {DIA:02d}/{MES:02d}/{ANO} ===") +print(f"Turno detectado ese dia: {turno}") +print(f"Total asignados ese dia: {len(filas)}\n") +print(f"{'#':3} {'telefono':13} {'asignado':17} {'respondio':17} {'min':>5} nota") +print("-"*72) +resp=0; sinr=0; suma=0; refri=0 +for i,r in enumerate(sorted(filas, key=lambda x: x['created_at']),1): + t1=r['created_at']; t2=r.get('respuesta_fecha') + nom_up=(r.get('user_name') or '').upper() + tel=r.get('telefono',''); a_str=t1.strftime('%H:%M:%S') + h=t1.hour+t1.minute/60.0 + en_refri = es_t1(nom_up,d1) and (13<=h<14) + nota = " REFRIGERIO (grafica lo excluye)" if en_refri else "" + if en_refri: refri+=1 + if not isinstance(t2,datetime): + sinr+=1 + print(f"{i:3} {tel:13} {a_str:17} {'':17} {'':>5}{nota}") + continue + m=mins_laborales(nom_up,t1,t2); resp+=1; suma+=m + print(f"{i:3} {tel:13} {a_str:17} {t2.strftime('%H:%M:%S'):17} {m:>5}{nota}") +print("-"*72) +print(f"Respondidos: {resp} | Sin responder: {sinr} | En refrigerio (13-14): {refri}") +print(f"Total tabla (todos): {len(filas)} | Total grafica (sin refrigerio): {len(filas)-refri}") +print(f"Promedio (solo respondidos): {round(suma/resp) if resp else 0} min") diff --git a/backend/diag_tiempo_diana_jul1.py b/backend/diag_tiempo_diana_jul1.py new file mode 100644 index 0000000..f59fb21 --- /dev/null +++ b/backend/diag_tiempo_diana_jul1.py @@ -0,0 +1,131 @@ +# diag_tiempo_diana_jul1.py +# Detalle del 1 de JULIO 2026 para DIANA CHAVEZ: los asignados, hora de asignacion, +# si respondieron, hora de respuesta y minutos laborales (misma logica del dashboard). +# Ejecutar en backend/: python diag_tiempo_diana_jul1.py +from datetime import datetime, timedelta +from data_manager_v2 import DataManager + +ANO, MES, DIA = 2026, 7, 1 +NOMBRE = "DIANA" # busca por apellido/nombre que contenga esto en user_name + +dm = DataManager() + +# --- 1) asignacion + respuesta (mismo query del dashboard) --- +asig = dm.traer_asignacion_respuesta() +# --- 2) actividad de asesores (para detectar turno) --- +act_rows = dm.traer_actividad_asesores() + +# mapa user_id -> user_name +uid2name = {} +for r in asig: + if r.get("user_id") is not None: + uid2name[r["user_id"]] = r.get("user_name") or "" + +# actividad por (user_name_upper, fecha) -> franja -> conteo +from collections import defaultdict +act = defaultdict(lambda: defaultdict(int)) +for a in act_rows: + uid = a.get("user_id"); ts = a.get("created_at") + if uid not in uid2name or not isinstance(ts, datetime): + continue + nom = uid2name[uid].upper() + d = ts.date(); h = ts.hour + ts.minute/60.0 + key = (nom, d) + if d.weekday() == 6: + continue + if d.weekday() == 5: + if 9 <= h < 13: act[key]["sab_manana"] += 1 + elif 14 <= h < 18: act[key]["sab_tarde"] += 1 + else: + if 13 <= h < 18: act[key]["tarde"] += 1 + elif 18 <= h < 22: act[key]["noche"] += 1 + +def turno_dia(nom, d): + if d.weekday() == 6: return None + fr = act.get((nom, d)) + if not fr: return None + if d.weekday() == 5: + m = fr.get("sab_manana",0); t = fr.get("sab_tarde",0) + if m==0 and t==0: return None + return [(9,13)] if m>=t else [(14,18)] + t = fr.get("tarde",0); n = fr.get("noche",0) + if t==0 and n==0: return None + return [(9,13),(18,22)] if n>t else [(9,13),(14,18)] + +def _dt(d, hh): return datetime(d.year,d.month,d.day) + timedelta(hours=hh) + +def mins_laborales(nom, t1, t2): + if not isinstance(t1,datetime) or not isinstance(t2,datetime) or t2<=t1: return 0 + total=0.0; cur=t1; saltos=0 + while cur < t2 and saltos < 60: + d=cur.date(); bloques=turno_dia(nom,d) + if not bloques: + cur=_dt(d,24); saltos+=1; continue + for (hi,hf) in bloques: + ini=_dt(d,hi); fin=_dt(d,hf) + if t2<=ini: break + if cur>=fin: continue + a=max(cur,ini); b=min(t2,fin) + if b>a: total += (b-a).total_seconds()/60.0 + cur=fin + if cur>=t2: break + if cur.date()==d: + cur=_dt(d,24); saltos+=1 + return round(total) + +def es_refri_t1(nom, ts): + d=ts.date() + if d.weekday()>=5: return False + fr=act.get((nom,d)) + if not fr: return False + t=fr.get("tarde",0); n=fr.get("noche",0) + es_t1 = not (n>t) and (t>0 or n>0) + h=ts.hour+ts.minute/60.0 + return es_t1 and (13<=h<14) + +# --- filtrar Diana + 1 julio (por fecha de asignacion) --- +filas=[] +for r in asig: + nom = (r.get("user_name") or "") + if NOMBRE not in nom.upper(): + continue + t1 = r.get("created_at") + if not isinstance(t1, datetime): continue + if not (t1.year==ANO and t1.month==MES and t1.day==DIA): continue + filas.append(r) + +# turno detectado ese dia +from datetime import date +d1 = date(ANO,MES,DIA) +turno = None +if filas: + nom_up = (filas[0].get("user_name") or "").upper() + tt = turno_dia(nom_up, d1) + turno = "Turno 1 (9-18)" if tt==[(9,13),(14,18)] else ("Turno 2 (9-13+18-22)" if tt==[(9,13),(18,22)] else str(tt)) + +print(f"\n=== DIANA CHAVEZ — {DIA:02d}/{MES:02d}/{ANO} ===") +print(f"Turno detectado ese dia: {turno}") +print(f"Total asignados ese dia: {len(filas)}\n") +print(f"{'#':3} {'telefono':13} {'asignado':17} {'respondio':17} {'min':>5} nota") +print("-"*75) +resp=0; sinr=0; refri=0; suma=0 +for i,r in enumerate(sorted(filas, key=lambda x: x['created_at']),1): + t1=r['created_at']; t2=r.get('respuesta_fecha') + nom_up=(r.get('user_name') or '').upper() + tel=r.get('telefono','') + a_str=t1.strftime('%H:%M:%S') + if es_refri_t1(nom_up, t1): + refri+=1 + r_str = t2.strftime('%H:%M:%S') if isinstance(t2, datetime) else '(sin resp)' + print(f"{i:3} {tel:13} {a_str:17} {r_str:17} {'-':>5} EXCLUIDO (refrigerio 13-14)") + continue + if not isinstance(t2,datetime): + sinr+=1 + print(f"{i:3} {tel:13} {a_str:17} {'(sin resp)':17} {'-':>5} no respondio") + continue + m=mins_laborales(nom_up,t1,t2) + resp+=1; suma+=m + print(f"{i:3} {tel:13} {a_str:17} {t2.strftime('%H:%M:%S'):17} {m:>5}") +print("-"*75) +print(f"Respondidos: {resp} | Sin responder: {sinr} | Excluidos refrigerio: {refri}") +print(f"Suma minutos: {suma} | Promedio: {round(suma/resp) if resp else 0} min") diff --git a/backend/diag_ultima_etiqueta.py b/backend/diag_ultima_etiqueta.py new file mode 100644 index 0000000..30a60aa --- /dev/null +++ b/backend/diag_ultima_etiqueta.py @@ -0,0 +1,51 @@ +""" +Diagnostico: para una lista de telefonos, muestra su cached_label_list crudo +y la Ultima_Etiqueta calculada (misma logica del DAX / leads_logic). +""" +import os +import psycopg2 +from dotenv import load_dotenv +import leads_logic as L # reutiliza ultima_etiqueta() ya existente + +load_dotenv() + +PG_HOST = os.getenv("PG_HOST") +PG_DB = os.getenv("PG_DATABASE") or os.getenv("PG_DB") +PG_USER = os.getenv("PG_USER") +PG_PASS = os.getenv("PG_PASSWORD") or os.getenv("PG_PASS") +PG_PORT = os.getenv("PG_PORT", "5432") + +TELEFONOS = """ +948400152 953932854 959735396 971615688 972132288 976595677 +56953794663 56961258851 56962820196 59173326356 59175648709 +900079313 900153292 900610393 900779778 902490667 902671008 +902968272 907312317 907818325 +""".split() + +conn = psycopg2.connect(host=PG_HOST, dbname=PG_DB, user=PG_USER, + password=PG_PASS, port=PG_PORT, connect_timeout=30) +cur = conn.cursor() + +print(f"{'TELEFONO':14} {'ULTIMA_ETIQUETA':22} | cached_label_list (crudo)") +print("-" * 90) + +for tel in TELEFONOS: + # busca cualquier variante del numero (con o sin 51 adelante) + cur.execute(""" + SELECT cv.cached_label_list, m.created_at + FROM messages m + JOIN conversations cv ON m.conversation_id = cv.id + JOIN contacts c ON cv.contact_id = c.id + WHERE REPLACE(REPLACE(c.phone_number, '+51',''), '+','') LIKE %s + ORDER BY m.created_at DESC + LIMIT 1 + """, (f"%{tel}",)) + row = cur.fetchone() + if row is None: + print(f"{tel:14} {'(no encontrado)':22} |") + continue + crudo = row[0] + ult = L.ultima_etiqueta(crudo) + print(f"{tel:14} {ult:22} | {crudo}") + +conn.close() diff --git a/backend/diag_vendedor_sql.py b/backend/diag_vendedor_sql.py new file mode 100644 index 0000000..9671d1e --- /dev/null +++ b/backend/diag_vendedor_sql.py @@ -0,0 +1,14 @@ +# diag_vendedor_sql.py — Verifica el apartado Vendedores. +import services as S + +d = S.vendedores_dashboard("2026", "TODOS", "TODOS") +print("Vendedores (filtro):", d["vendedores"]) +print(f"\n{'VENDEDOR':24} {'CartTot':>8} {'Copito':>7} {'Otros':>7} {'MatTot':>7} {'Curso':>6} {'Retir':>6}") +print("-"*72) +for f in d["filas"]: + print(f"{f['vendedor']:24} {f['cartera_total']:>8} {f['cartera_copito']:>7} {f['cartera_otros']:>7} " + f"{f['mat_total']:>7} {f['mat_curso']:>6} {f['mat_retiradas']:>6}") +t = d["total"] +print("-"*72) +print(f"{'TOTAL':24} {t['cartera_total']:>8} {t['cartera_copito']:>7} {t['cartera_otros']:>7} " + f"{t['mat_total']:>7} {t['mat_curso']:>6} {t['mat_retiradas']:>6}") diff --git a/backend/diag_webform.py b/backend/diag_webform.py new file mode 100644 index 0000000..af612ea --- /dev/null +++ b/backend/diag_webform.py @@ -0,0 +1,58 @@ +# diag_webform.py — Compara WEB_FORMULARIO enero: datos_unificados vs cartera_junta (local xlsx). +# Usa el export local para cartera (rapido) y baja solo enero de datos_unificados. +import os, requests +from datetime import datetime +from dotenv import load_dotenv +load_dotenv() + +SUP_URL = os.getenv("SUPABASE_URL"); SUP_KEY = os.getenv("SUPABASE_KEY") + +def _norm_tel(v): + s = str(v or "") + for x in ("+51","+"," ","-","(",")"): s=s.replace(x,"") + s=s.strip() + if not s or set(s)=={"0"}: return "" + if s.isdigit() and len(s)<=6: return "" + return s + +def _ene(v): + for fmt in ("%d/%m/%Y","%Y-%m-%d"): + try: + d=datetime.strptime(str(v)[:10],fmt).date() + return d.year==2026 and d.month==1 + except: pass + return False + +# datos_unificados WEB_FORMULARIO enero +h={"apikey":SUP_KEY,"Authorization":f"Bearer {SUP_KEY}"} +out=[]; desde=0 +while True: + r=requests.get(f"{SUP_URL}/rest/v1/datos_unificados", + params={"select":"Telefono,Canal,Fechacreada","offset":str(desde),"limit":"1000"},headers=h,timeout=60) + r.raise_for_status(); d=r.json() + if not d: break + out.extend(d) + if len(d)<1000: break + desde+=1000 +du=[_norm_tel(r["Telefono"]) for r in out if str(r.get("Canal","")).upper()=="WEB_FORMULARIO" and _ene(r.get("Fechacreada"))] +du=set(t for t in du if t) +print("datos_unificados WEB_FORMULARIO enero (tel unicos):", len(du)) + +# cartera desde el excel local +import openpyxl, glob +xf=max(glob.glob("cartera_junta_export.xlsx"), default=None) +wb=openpyxl.load_workbook("cartera_junta_export.xlsx", read_only=True); ws=wb.active +rows=list(ws.iter_rows(values_only=True)); hdr=rows[0]; ci={h:i for i,h in enumerate(hdr)} +tel_all=set(str(r[ci["telefono"]]).strip() for r in rows[1:]) # todos los tel de cartera +tel_web_ene=set() +for r in rows[1:]: + if str(r[ci["canal"]]).strip().upper()=="WEB_FORMULARIO" and str(r[ci["fecha_creada"]])[:7]=="2026-01": + tel_web_ene.add(str(r[ci["telefono"]]).strip()) +print("cartera WEB_FORMULARIO enero (tel):", len(tel_web_ene)) + +falt_total=[t for t in du if t not in tel_all] +falt_web=[t for t in du if t not in tel_web_ene] +print(f"\nDe {len(du)} tel de datos_unificados enero:") +print(f" NO estan en cartera por NINGUN canal: {len(falt_total)}") +print(f" estan en cartera pero NO como WEB_FORMULARIO enero: {len(falt_web)-len(falt_total)}") +print(" ejemplos NO en cartera:", falt_total[:10]) diff --git a/backend/export_base_junta.py b/backend/export_base_junta.py new file mode 100644 index 0000000..e08e458 --- /dev/null +++ b/backend/export_base_junta.py @@ -0,0 +1,150 @@ +# backend/export_base_junta.py +""" +Une los leads de Postgre (Fact_Leads_Procesados, mapa de campañas) + Supabase, +deduplica por teléfono quedándose con el MÁS ANTIGUO (desempate: Pauta_wsp_face), +y exporta un Excel con: Telefono, Canal, Fecha Creada. + +Uso: py -3.12 export_base_junta.py +""" +import os +from datetime import datetime +from dotenv import load_dotenv +from data_manager_v2 import DataManager + +load_dotenv() + +# Canal preferido en caso de empate de fecha +CANAL_PREFERIDO = "Pauta_wsp_face" + + +def _norm_tel(v): + s = str(v or "") + for x in ("+51", "+", " ", "-", "(", ")"): + s = s.replace(x, "") + return s.strip() + + +def _txt(v): + """Texto en MAYÚSCULAS; '-' o vacío → ''.""" + s = str(v or "").strip() + if s == "-": + s = "" + return s.upper() + + +def _to_dt(v): + if v is None: + return None + if isinstance(v, datetime): + return v + s = str(v)[:19] + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y"): + try: + return datetime.strptime(s[:len(fmt) + 2] if "%H" in fmt else s[:10], fmt) + except Exception: + pass + return None + + +def traer_postgre(dm): + """Mismos leads que Fact_Leads_Procesados (mapa de campañas): tel, fecha, canal(origen).""" + filas = dm.traer_leads_chatwoot() # ya aplica el mapa de campañas y trae 'origen' + out = [] + for f in filas: + tel = _norm_tel(f.get("telefono")) + if not tel: + continue + out.append({ + "telefono": tel, + "fecha": _to_dt(f.get("fecha_creada")), + "canal": _txt(f.get("origen")), + "sede": _txt(f.get("sede")), + "programa": _txt(f.get("programa")), + "codigo": _txt(f.get("codigo")), + }) + return out + + +def traer_supabase(dm): + """Lee la tabla de Supabase con columnas: telefono, fechacreada, canal.""" + try: + from supabase import create_client + url = os.getenv("SUPABASE_URL", "") + key = os.getenv("SUPABASE_KEY", "") + if not url or not key: + print("⚠️ Falta SUPABASE_URL / SUPABASE_KEY en .env — se omite Supabase") + return [] + sb = create_client(url, key) + tabla = os.getenv("SUPABASE_TABLA_LEADS", "leads") # ajustar nombre real si difiere + res = sb.table(tabla).select("Telefono,Fechacreada,Canal,Sede,Programa,Codigo").execute() + out = [] + for r in (res.data or []): + tel = _norm_tel(r.get("Telefono")) + if not tel: + continue + out.append({ + "telefono": tel, + "fecha": _to_dt(r.get("Fechacreada")), + "canal": _txt(r.get("Canal")), + "sede": _txt(r.get("Sede")), + "programa": _txt(r.get("Programa")), + "codigo": _txt(r.get("Codigo")), + }) + return out + except Exception as e: + print(f"⚠️ Error leyendo Supabase: {e}") + return [] + + +def main(): + dm = DataManager() + print("Trayendo Postgre (Fact_Leads_Procesados)...") + pg = traer_postgre(dm) + print(f" Postgre: {len(pg)} filas") + print("Trayendo Supabase...") + sup = traer_supabase(dm) + print(f" Supabase: {len(sup)} filas") + + todos = pg + sup + print(f"Total combinado (con repetidos): {len(todos)}") + + # Dedup por teléfono: quedarse con el MÁS ANTIGUO. + # Desempate (misma fecha): preferir CANAL_PREFERIDO. + FUTURO = datetime(9999, 1, 1) + mejor = {} + for r in todos: + tel = r["telefono"] + f = r["fecha"] or FUTURO + actual = mejor.get(tel) + if actual is None: + mejor[tel] = r + continue + fa = actual["fecha"] or FUTURO + if f < fa: + mejor[tel] = r + elif f == fa: + # empate de fecha → preferir el canal preferido + if r["canal"] == CANAL_PREFERIDO and actual["canal"] != CANAL_PREFERIDO: + mejor[tel] = r + + final = list(mejor.values()) + print(f"Teléfonos únicos (sin repetir): {len(final)}") + + # Exportar a Excel + import openpyxl + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "Base_Junta" + ws.append(["Telefono", "Canal", "Fecha Creada", "Sede", "Programa", "Codigo"]) + for r in sorted(final, key=lambda x: (x["fecha"] or FUTURO)): + fecha = r["fecha"].strftime("%d/%m/%Y") if r["fecha"] and r["fecha"] != FUTURO else "" + ws.append([r["telefono"], r["canal"], fecha, + r.get("sede", ""), r.get("programa", ""), r.get("codigo", "")]) + salida = os.path.join(os.path.dirname(__file__), "base_junta.xlsx") + wb.save(salida) + print(f"\n✅ Exportado: {salida}") + print(f" Filas: {len(final)}") + + +if __name__ == "__main__": + main() diff --git a/backend/export_campanias.py b/backend/export_campanias.py new file mode 100644 index 0000000..37e7ca8 --- /dev/null +++ b/backend/export_campanias.py @@ -0,0 +1,70 @@ +# export_campanias.py — Exporta TODA la tabla campanias de Supabase a Excel/CSV. +# Ejecutar dentro de backend/: python export_campanias.py +import os, csv, requests +from dotenv import load_dotenv +load_dotenv() + +URL = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") +KEY = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") +TABLA = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") +HEAD = {"apikey": KEY, "Authorization": f"Bearer {KEY}"} + +assert URL and KEY, "Falta CARTERA_URL / CARTERA_KEY en .env" + +def traer_todo(): + filas, paso, desde = [], 1000, 0 + while True: + hdr = dict(HEAD) + hdr["Range-Unit"] = "items" + hdr["Range"] = f"{desde}-{desde + paso - 1}" + r = requests.get(f"{URL}/rest/v1/{TABLA}", + params={"select": "*"}, + headers=hdr, timeout=60) + r.raise_for_status() + data = r.json() + if not data: + break + filas.extend(data) + print(f" descargadas: {len(filas)}", end="\r") + desde += len(data) # avanzar por lo realmente recibido + if len(data) < paso: + break + print() + return filas + +print(f"Descargando '{TABLA}' de Supabase...") +filas = traer_todo() +print(f"Total: {len(filas)} filas") + +if not filas: + print("Sin datos. Revisa CARTERA_URL / CARTERA_KEY en .env") + raise SystemExit + +# columnas = union de todas las claves, en orden de la primera fila +cols = list(filas[0].keys()) +for f in filas: + for k in f.keys(): + if k not in cols: + cols.append(k) + +# 1) CSV siempre +csv_path = "campanias_export.csv" +with open(csv_path, "w", newline="", encoding="utf-8-sig") as fout: + w = csv.DictWriter(fout, fieldnames=cols) + w.writeheader() + for f in filas: + w.writerow({c: f.get(c, "") for c in cols}) +print(f"CSV generado: {csv_path}") + +# 2) Excel si openpyxl esta disponible +try: + from openpyxl import Workbook + wb = Workbook(); ws = wb.active; ws.title = "campanias" + ws.append(cols) + for f in filas: + ws.append([f.get(c, "") for c in cols]) + xlsx_path = "campanias_export.xlsx" + wb.save(xlsx_path) + print(f"Excel generado: {xlsx_path}") +except ImportError: + print("(openpyxl no instalado -> solo CSV. Para Excel: pip install openpyxl)") diff --git a/backend/export_cartera.py b/backend/export_cartera.py new file mode 100644 index 0000000..df7ceb8 --- /dev/null +++ b/backend/export_cartera.py @@ -0,0 +1,64 @@ +# export_cartera.py — Exporta TODA la tabla cartera_junta de Supabase a Excel/CSV. +# Ejecutar dentro de backend/: python export_cartera.py +import os, csv, requests +from dotenv import load_dotenv +load_dotenv() + +URL = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") +KEY = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") +TABLA = os.getenv("SUPABASE_TABLA_CARTERA", "cartera_junta") +HEAD = {"apikey": KEY, "Authorization": f"Bearer {KEY}"} + +def traer_todo(): + filas, paso, desde = [], 1000, 0 + while True: + r = requests.get(f"{URL}/rest/v1/{TABLA}", + params={"select": "*", "offset": str(desde), "limit": str(paso)}, + headers=HEAD, timeout=120) + r.raise_for_status() + data = r.json() + if not data: + break + filas.extend(data) + print(f" descargadas {len(filas)} filas...") + if len(data) < paso: + break + desde += paso + return filas + +print(f"Descargando '{TABLA}' de Supabase...") +filas = traer_todo() +print(f"Total: {len(filas)} filas") + +if not filas: + print("Sin datos. Revisa CARTERA_URL / CARTERA_KEY en .env") + raise SystemExit + +# columnas = union de todas las claves, en orden de la primera fila +cols = list(filas[0].keys()) +for f in filas: + for k in f.keys(): + if k not in cols: + cols.append(k) + +# 1) CSV siempre +csv_path = "cartera_junta_export.csv" +with open(csv_path, "w", newline="", encoding="utf-8-sig") as fout: + w = csv.DictWriter(fout, fieldnames=cols) + w.writeheader() + for f in filas: + w.writerow({c: f.get(c, "") for c in cols}) +print(f"CSV generado: {csv_path}") + +# 2) Excel si openpyxl esta disponible +try: + from openpyxl import Workbook + wb = Workbook(); ws = wb.active; ws.title = "cartera_junta" + ws.append(cols) + for f in filas: + ws.append([f.get(c, "") for c in cols]) + xlsx_path = "cartera_junta_export.xlsx" + wb.save(xlsx_path) + print(f"Excel generado: {xlsx_path}") +except ImportError: + print("(openpyxl no instalado -> solo CSV. Para Excel: pip install openpyxl)") diff --git a/backend/export_roas_excel.py b/backend/export_roas_excel.py new file mode 100644 index 0000000..8b7a880 --- /dev/null +++ b/backend/export_roas_excel.py @@ -0,0 +1,73 @@ +# export_roas_excel.py +# Genera un Excel de la tabla "Inversión por Sede" (ROAS) con el filtro +# Año=2026, Mes=Marzo (3), Día=TODOS — igual que el dashboard. +# Columnas: Sede | Programa | Conjunto | Importe Gastado | Resultados +# Usa la MISMA función roas_dashboard() del dashboard (no reinventa la logica). +# Ejecutar en backend/: python export_roas_excel.py +import services as S +from openpyxl import Workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side + +ANO, MES, DIA = "2026", "3", "TODOS" # Marzo 2026 + +data = S.roas_dashboard(ANO, MES, DIA) +filas = data.get("filas", []) + +# ── Aplanar a filas: Sede / Programa / Conjunto / Importe / Resultados ── +rows = [] +for f in filas: + sede = f.get("sede", "") + # nivel sede (total de la sede) + rows.append([sede, "", "", f.get("importe", 0), f.get("resultados", 0), "sede"]) + for p in (f.get("programas") or []): + prog = p.get("programa", "") + # nivel programa (total del programa) + rows.append([sede, prog, "", p.get("importe", 0), p.get("resultados", 0), "prog"]) + for c in (p.get("conjuntos") or []): + rows.append([sede, prog, c.get("conjunto", ""), + c.get("importe", 0), c.get("resultados", 0), "conj"]) + +# ── Excel ── +wb = Workbook() +ws = wb.active +ws.title = "ROAS Marzo 2026" + +azul = PatternFill("solid", fgColor="1E3A5F") +blanco = Font(bold=True, color="FFFFFF") +fill_sede = PatternFill("solid", fgColor="DBEAFE") +fill_prog = PatternFill("solid", fgColor="F1F5F9") +bord = Border(*(Side(style="thin", color="D0D7E2"),)*4) +cen = Alignment(horizontal="center", vertical="center") +izq = Alignment(horizontal="left", vertical="center") + +cab = ["Sede", "Programa", "Conjunto", "Importe Gastado", "Resultados"] +for j, t in enumerate(cab, 1): + c = ws.cell(1, j, t) + c.fill = azul; c.font = blanco; c.alignment = cen; c.border = bord + +r = 2 +for sede, prog, conj, imp, res, nivel in rows: + ws.cell(r, 1, sede) + ws.cell(r, 2, prog) + ws.cell(r, 3, conj) + ci = ws.cell(r, 4, round(float(imp or 0), 2)); ci.number_format = '"$"#,##0.00' + cr = ws.cell(r, 5, int(res or 0)); cr.number_format = '#,##0' + for j in range(1, 6): + cell = ws.cell(r, j); cell.border = bord + cell.alignment = izq if j <= 3 else cen + if nivel == "sede": + cell.fill = fill_sede; cell.font = Font(bold=True) + elif nivel == "prog": + cell.fill = fill_prog; cell.font = Font(bold=True, color="475569") + r += 1 + +# anchos +for col, w in zip("ABCDE", [16, 14, 46, 16, 14]): + ws.column_dimensions[col].width = w +ws.freeze_panes = "A2" + +out = "ROAS_Marzo_2026.xlsx" +wb.save(out) +print(f"\nOK -> {out}") +print(f"Filas totales: {len(rows)} (sedes: {len(filas)})") +print("Columnas: Sede | Programa | Conjunto | Importe Gastado | Resultados") diff --git a/backend/leads_logic.py b/backend/leads_logic.py new file mode 100644 index 0000000..b3efe68 --- /dev/null +++ b/backend/leads_logic.py @@ -0,0 +1,894 @@ +# backend/leads_logic.py +""" +Lógica del módulo LEADS. Traduce las medidas DAX del PBI a Python. +Columnas calculadas replicadas: + - Cantidad_Veces -> 1ª aparición de un teléfono = lead único + - Ultima_Etiqueta / ESTADO/OBJECION -> limpieza de cached_label_list + - Tipo_Programa / Sede -> clasificación por dsc_programa +Medidas replicadas: + - Leads_Totales_Pauta_Unico, Leads_Procesados_Pauta_Unicos, + Leads_Procesados_Contactados_Unicos, % Procesados, % Contactados, + Total_Matriculas, Cant_inscritos_Mes, Ocupabilidad, + Cursos (Inicios / Suspendidos / Ya Iniciados), Matrículas por día, + tabla Estado/Objeción. +""" +from datetime import datetime, date + +# Etiquetas de sistema que se eliminan para hallar el estado/objeción real (igual que el DAX) +ETIQUETAS_SISTEMA = { + "atención_humana", "negociación", "supervisor", "grupo_arequipa", + "grupo_trujillo", "sin_respuesta", +} +META_POR_TIPO = { + "PROGRAMAS TEAC": 22, "PROGRAMAS TERC": 22, + "PROVINCIA TEAC": 18, "PROVINCIA TERC": 18, + "SEMINARIOS": 15, +} + + +# ── Helpers de fecha ──────────────────────────────────────────── +def _to_date(v): + if v is None: + return None + if isinstance(v, datetime): + return v.date() + if isinstance(v, date): + return v + s = str(v)[:10] + for fmt in ("%Y-%m-%d", "%d-%m-%Y", "%d/%m/%Y"): + try: + return datetime.strptime(s, fmt).date() + except Exception: + pass + return None + + +def _en_periodo(d, ano, mes, dia): + """True si la fecha cae en el filtro (ano/mes/dia; 'TODOS' = sin filtrar).""" + if d is None: + return False + if ano not in ("TODOS", None) and d.year != int(ano): + return False + if mes not in ("TODOS", None) and d.month != int(mes): + return False + if dia not in ("TODOS", None) and d.day != int(dia): + return False + return True + + +# ── Clasificadores (igual que columnas calculadas DAX) ────────── +def clasificar_tipo_programa(dsc_programa): + up = str(dsc_programa or "").upper() + otros = ["CERTIFICACIÓN", "CERTIFICACION", "CURSO A MEDIDA", "MASTERCLASS", + "TALLER DE REFRIGERACIÓN DOM", "GESTIÓN DE VENTA", "GESTION DE VENTA"] + for o in otros: + if o.upper() in up: + return "OTROS" + return None # el tipo PROGRAMAS/PROVINCIA se arma con sede+programa abajo + + +def clasificar_sede(dsc_programa): + up = str(dsc_programa or "").upper() + if "AREQUIPA" in up: return "AREQUIPA" + if "TRUJILLO" in up: return "TRUJILLO" + if "PIURA" in up: return "PIURA" + return "LIMA" + + +def sede_por_codigo_map(): + """{codigo: SEDE} tomado del mapa de campañas (la sede que le corresponde a + cada código, SIN reasignar por asesor). Usado por el filtro SEDE de leads, + para que coincida con 'sede del código' (no con sede_act).""" + from data_manager_v2 import CAMPANIAS + out = {} + for fila in CAMPANIAS: + cod = str(fila[2]).strip() # índice 2 = codigo + sede = str(fila[3]).strip().upper() # índice 3 = sede + if cod: + out[cod] = sede + return out + + +# ── Grupo de PROGRAMA para el filtro: TEAC / TERC / SEMINARIOS / OTROS ── +def grupo_programa_curso(dsc_programa): + """Reduce el tipo detallado a 4 grupos para el filtro PROGRAMA (cursos/matrículas). + TEAC = PROGRAMAS TEAC + PROVINCIA TEAC + TERC = PROGRAMAS TERC + PROVINCIA TERC + SEMINARIOS = SEMINARIOS + OTROS = OTROS + CARRERA""" + tp = tipo_programa_curso(dsc_programa) + if tp in ("PROGRAMAS TEAC", "PROVINCIA TEAC"): + return "TEAC" + if tp in ("PROGRAMAS TERC", "PROVINCIA TERC"): + return "TERC" + if tp == "SEMINARIOS": + return "SEMINARIOS" + return "OTROS" # OTROS + CARRERA + + +def grupo_programa_lead(cargo): + """Grupo de programa para un LEAD, según su 'cargo' (del mapa de campañas). + TEAC → TEAC, TERC → TERC, resto (VRF/CO2/DIPLOMADO/etc.) → SEMINARIOS.""" + c = str(cargo or "").strip().upper() + if c == "TEAC": + return "TEAC" + if c == "TERC": + return "TERC" + return "SEMINARIOS" + + +def grupo_tipo_cohorte(tp): + """Reduce 'PROGRAMAS TEAC'/'PROVINCIA TEAC'/... a los 3 grupos de la tabla por Sede: + TEAC = PROGRAMAS/PROVINCIA TEAC, TERC = PROGRAMAS/PROVINCIA TERC, resto = SEMINARIOS.""" + t = str(tp or "").upper() + if "TEAC" in t: + return "TEAC" + if "TERC" in t: + return "TERC" + return "SEMINARIOS" + + +# Réplica del DAX Sede_Act: la sede del lead se decide por el ASESOR; +# si el asesor no está en la lista, usa la sede de la campaña. +_SEDE_POR_ASESOR = { + "almendra peralta": "Arequipa", + "juan carlos aguilar": "Piura", + "diego lázaro": "Trujillo", + "diego lazaro": "Trujillo", + "verónica la rosa": "Lima", + "veronica la rosa": "Lima", + "dayana balabarca": "Lima", + "milagros vargas": "Lima", + "carmen montoya": "Lima", + "diana chávez": "Lima", + "diana chavez": "Lima", + "copito rivera": "Lima", +} + + +def sede_act(asesor, sede_campania): + return _SEDE_POR_ASESOR.get(str(asesor or "").strip().lower(), sede_campania) + + +def tipo_programa_cohorte(sede, programa_cat): + """Igual al DAX 'Tipo Programa': combina sede + (TEAC/TERC) -> categoría.""" + s = str(sede or "").upper() + p = str(programa_cat or "").upper() + if s == "LIMA" and p == "TEAC": return "PROGRAMAS TEAC" + if s == "LIMA" and p == "TERC": return "PROGRAMAS TERC" + if s in ("TRUJILLO", "PIURA", "AREQUIPA") and p == "TEAC": return "PROVINCIA TEAC" + if s in ("TRUJILLO", "PIURA", "AREQUIPA") and p == "TERC": return "PROVINCIA TERC" + return "SEMINARIOS" + + +def tipo_programa_curso(dsc_programa): + """Réplica EXACTA del DAX Tipo_Programa (columna calculada de Fact_SQL_Base_Cursos). + Usa CONTAINSSTRING en el mismo ORDEN que el PBI (el orden importa).""" + p = str(dsc_programa or "") + def has(s): # CONTAINSSTRING es sensible a may/min en DAX; comparamos tal cual + return s in p + + # 1) OTROS + if (has("CERTIFICACIÓN") or has("CURSO A MEDIDA") or has("MASTERCLASS") + or has("TALLER DE REFRIGERACIÓN DOM") + or has("GESTIÓN DE VENTA DE EQUIPOS DE AIRE ACONDICIONADO Y EQUIPOS DE REFRIGERACIÓN")): + return "OTROS" + if (has("CERTIFICACION FUNDAMENTOS DE CHILLER MODULAR INVERT- LG") + or has("PIURA - CERTIFICACIÓN MIDEA: TECNOLOGIA INVERTER") + or has("AREQUIPA - CERTIFICACIÓN MIDEA - TECNOLOGÍA INVERTER") + or has("CERTIFICACIÓN: OPERACIÓN Y MANTENIMIENTO DE REFRIGERADORES MIDEA") + or has("TRUJILLO - CERTIFICACIÓN MIDEA: TECNOLOGÍA INVERTER") + or has("CERTIFICACIÓN MIDEA: AIRE ACOND INVERTER (INTROD., FUNC., INST. Y MANT.)")): + return "OTROS" + # 2) CARRERA + if has("CARRERA TECNICA DE AIRE ACONDICIONADO Y REFRIGERACION"): + return "CARRERA" + # 3) PROVINCIA TEAC + if (has("TRUJILLO - TECNICO ESPECIALISTA EN AIRE ACONDICIONADO") + or has("PIURA - TECNICO ESPECIALISTA EN AIRE ACONDICIONADO") + or has("AREQUIPA - TECNICO ESPECIALISTA EN AIRE ACONDICIONADO") + or has("TRUJILLO - VIRTUAL TECNICO ESPECIALISTA EN AIRE ACONDICIONADO")): + return "PROVINCIA TEAC" + # 4) PROVINCIA TERC + if (has("AREQUIPA - TECNICO ESPECIALISTA EN REFRIGERACION DOMÉSTICA Y COMERCIAL") + or has("PIURA - TECNICO ESPECIALISTA EN REFRIGERACION DOMÉSTICA Y COMERCIAL") + or has("TRUJILLO - TECNICO ESPECIALISTA EN REFRIGERACION DOMÉSTICA Y COMERCIAL")): + return "PROVINCIA TERC" + # 5) SEMINARIOS + if (has("CO2") or has("CERTIF") or has("SEM:") or has("MASTERCLASS") or has("DISEÑO") + or has("DIPLOMADO") or has("SEM.") or has("DUCTOS") or has("SEMINARIO") or has("SEMINARIOS")): + return "SEMINARIOS" + # 6) PROGRAMAS TEAC (Lima) + if (has("MANTENIMIENTO EN AIRE ACONDICIONADO") + or has("TECNICO ESPECIALISTA EN AIRE ACONDICIONADO") + or has("TALLER - TECNICO ESPECIALISTA EN AIRE ACONDICIONADO") + or has("CONTINUIDAD-TECNICO ESPECIALISTA EN AA (4M)") + or has("VIRTUAL TECNICO ESPECIALISTA EN AIRE ACONDICIONADO")): + return "PROGRAMAS TEAC" + # 7) PROGRAMAS TERC (Lima) + if (has("INSTALACION EN REFRIGERACION COMERCIAL") + or has("MANTENIMIENTO EN REFRIGERACIÓN COMERCIAL") + or has("CONTINUIDAD-TECNICO ESPECIALISTA EN REFRIGERACIÓN COMERCIAL (4M)") + or has("TECNICO ESPECIALISTA EN REFRIGERACION COMERCIAL")): + return "PROGRAMAS TERC" + return "SEMINARIOS" + + +def meta_curso(dsc_programa): + """Meta por curso según Tipo_Programa (igual que el DAX Meta_Curso): + PROGRAMAS TEAC/TERC=22, PROVINCIA TEAC/TERC=18, resto=15.""" + tp = tipo_programa_curso(dsc_programa) + if tp in ("PROGRAMAS TEAC", "PROGRAMAS TERC"): + return 22 + if tp in ("PROVINCIA TEAC", "PROVINCIA TERC"): + return 18 + return 15 + + +# ── ESTADO/OBJECION (réplica EXACTA del DAX: cadena de SUBSTITUTE) ── +# El DAX quita comas y espacios (pega todas las etiquetas), elimina las de +# sistema, y luego reduce combinaciones concatenadas a un estado final. +_SUST = [ + ("atención_humana", ""), ("negociación", ""), ("supervisor", ""), + ("grupo_arequipa", ""), ("grupo_trujillo", ""), ("sin_respuesta", ""), + ("interesadovendido", "vendido"), ("próximo_inicio", "proxima_fecha"), + ("inicio", ""), ("aprobación", ""), ("negociacion", ""), ("no_acepta", ""), + ("pendiente", ""), + ("revisando_informaciónsólo_consulta", "sólo_consulta"), + ("contacto_iniciadovendido", "vendido"), + ("revisando_informacióninteresado", "interesado"), + ("revisando_informaciónvendido", "vendido"), + ("sólo_consultainteresado", "interesado"), + ("revisando_informaciónno_califica", "no_califica"), + ("interesadopor_pagar", "por_pagar"), + ("revisando_informaciónprecio_elevado", "precio_elevado"), + ("proxima_fechavendido", "vendido"), + ("sólo_consultavendido", "vendido"), + ("vendidointeresado", "vendido"), + ("revisando_informaciónproxima_fecha", "proxima_fecha"), + ("contacto_iniciadosólo_consulta", "sólo_consulta"), +] + + +def estado_objecion(etiquetas): + """Réplica del DAX ESTADO/OBJECION (versión de 28 sustituciones del PBI).""" + if etiquetas is None: + return "no trabajado" + s = str(etiquetas).replace(",", "").replace(" ", "") + for buscar, reemplazar in _SUST: + s = s.replace(buscar, reemplazar) + s = s.strip() + return s if s else "no trabajado" + + +# ── Ultima_Etiqueta (réplica EXACTA del DAX, usada para "Total Contactados") ── +_ULTIMA_NO_TRABAJADO = { + "", "(en blanco)", "aprobación", "atencion humana", "atención humana", + "grupo arequipa", "grupo trujillo", "importacion masiva", "inicio", + "negociacion", "sin respuesta", "pendiente", +} + + +def ultima_etiqueta(etiquetas): + if etiquetas is None or str(etiquetas).strip() == "": + return "NO TRABAJADO" + partes = str(etiquetas).split(", ") + limpia = partes[-1].replace("_", " ").strip() + if limpia in _ULTIMA_NO_TRABAJADO: + return "NO TRABAJADO" + return limpia if limpia else "NO TRABAJADO" + + +# ════════════════════════════════════════════════════════════════ +# PROCESAMIENTO DE LEADS (calcula Cantidad_Veces y agrupa) +# ════════════════════════════════════════════════════════════════ +def procesar_leads(filas_chatwoot): + """Marca el lead único (1ª vez de cada teléfono) y arma estructura limpia. + Ordenamos por fecha para que la 1ª aparición de cada teléfono sea la 'única'.""" + # Ordenar por fecha ascendente (el query puede venir ordenado por id) + filas_chatwoot = sorted(filas_chatwoot, key=lambda f: (_to_date(f.get("fecha_creada")) or date.min)) + # Teléfonos de prueba a excluir (igual que el PBI: 924374783) + EXCLUIR = {"924374783"} + vistos = set() + out = [] + for f in filas_chatwoot: + tel = str(f.get("telefono") or "").strip() + if not tel or tel in EXCLUIR: + continue + es_unico = tel not in vistos + vistos.add(tel) + fecha = _to_date(f.get("fecha_creada")) + asesor = (f.get("asesor") or "").strip() + sede_campania = (f.get("sede") or "").strip() or "SIN SEDE" + sede = sede_act(asesor, sede_campania) # réplica DAX Sede_Act (reasigna por asesor) + cargo = (f.get("programa") or "").strip() # TEAC / TERC / VRF / etc. (del mapa campañas) + out.append({ + "telefono": tel, + "asesor": asesor, + "fecha": fecha, + "estado": estado_objecion(f.get("etiquetas")), + "ultima": ultima_etiqueta(f.get("etiquetas")), + "sede": sede, + "cargo": cargo, # TEAC / TERC / VRF / CO2 / etc. + "tipo_programa": tipo_programa_cohorte(sede, cargo), # PROGRAMAS/PROVINCIA/SEMINARIOS + "codigo": (f.get("codigo") or "").strip() or "-", # código de campaña + "es_unico": es_unico, + }) + return out + + +# ════════════════════════════════════════════════════════════════ +# KPIs DEL MÓDULO LEADS +# ════════════════════════════════════════════════════════════════ +def kpis_leads(leads, cursos, matriculas, ano, mes, dia): + # Leads en el periodo (solo únicos = Cantidad_Veces == 1) + leads_periodo = [l for l in leads if l["es_unico"] and _en_periodo(l["fecha"], ano, mes, dia)] + + recibidos = len(leads_periodo) + procesados = sum(1 for l in leads_periodo if l["asesor"]) + contactados = sum(1 for l in leads_periodo if l["asesor"] and l["ultima"] != "NO TRABAJADO") + + pct_procesados = (procesados / recibidos) if recibidos > 0 else 0.0 + pct_contactados = (contactados / procesados) if procesados > 0 else 0.0 + + # ── Cursos del periodo (por fecha de inicio) ── + cursos_periodo = [c for c in cursos if _en_periodo(_to_date(c.get("fch_inicio")), ano, mes, dia)] + cursos_programados = len(cursos_periodo) + suspendidos = sum(1 for c in cursos_periodo if str(c.get("cod_estado", "")).strip().upper() == "SUS") + hoy = date.today() + iniciados = sum(1 for c in cursos_periodo + if str(c.get("cod_estado", "")).strip().upper() != "SUS" + and (_to_date(c.get("fch_inicio")) or hoy) <= hoy) + + # ── Total matriculados (= medida Matriculas_Totales del PBI) ── + # Cuenta matrículas ALU/PRE cuya FECHA DE MATRÍCULA cae en el mes, + # excluyendo 2 vendedoras. El Calendario del PBI va por fch_matricula. + VENDEDORES_EXCLUIDOS = {"CALDERON S. LISSA GENA", "CRUZ G. FIORELLA MELISSA"} + total_matriculados = sum( + 1 for m in matriculas + if str(m.get("estado_matricula", "")).strip().upper() in ("ALU", "PRE") + and str(m.get("dsc_vendedor", "")).strip() not in VENDEDORES_EXCLUIDOS + and _en_periodo(_to_date(m.get("fch_matricula")), ano, mes, dia) + ) + # Ocupabilidad (PBI): SUM(Inscritos) / SUM(Meta). + # Excluye cursos suspendidos (SUS) y num_indice excluidos manualmente. + # (Ver FUTUROS_CAMBIOS.md: mover esta lista a GitHub/Supabase) + CURSOS_EXCLUIDOS = {"1154", "1121"} + cursos_ocup = [c for c in cursos_periodo + if str(c.get("cod_estado", "")).strip().upper() != "SUS" + and str(c.get("num_indice", "")).strip() not in CURSOS_EXCLUIDOS] + sum_meta = sum(meta_curso(c.get("dsc_programa")) for c in cursos_ocup) + sum_inscritos_cursos = sum(int(c.get("Inscritos_Totales", 0) or 0) for c in cursos_ocup) + + # ── Matrículas en los cursos del mes ── + # Matrículas ALU/PRE que: (1) se hicieron en el mes (fch_matricula) Y + # (2) son de un curso que INICIA en el mes (dsc_promocion = dsc_det_programa + # de los cursos del periodo). Réplica de Cant_inscritos_Mes del PBI. + promos_periodo = {str(c.get("dsc_det_programa", "")).strip() + for c in cursos_periodo if str(c.get("dsc_det_programa", "")).strip()} + mats_mes = [m for m in matriculas + if str(m.get("dsc_promocion", "")).strip() in promos_periodo + and str(m.get("estado_matricula", "")).strip().upper() in ("ALU", "PRE") + and _en_periodo(_to_date(m.get("fch_matricula")), ano, mes, dia)] + matriculas_mes = len({str(m.get("num_matricula")) for m in mats_mes}) + + ocupabilidad = (sum_inscritos_cursos / sum_meta) if sum_meta > 0 else 0.0 + + return { + "leads_recibidos": recibidos, + "leads_procesados": procesados, + "leads_contactados": contactados, + "pct_procesados": round(pct_procesados * 100, 2), + "pct_contactados": round(pct_contactados * 100, 2), + "total_matriculados": total_matriculados, + "matriculas_mes": matriculas_mes, + "ocupabilidad": round(ocupabilidad * 100, 2), + "cursos_programados": cursos_programados, + "cursos_reprogramados": 0, # requiere SharePoint (fase 2) + "cursos_suspendidos": suspendidos, + "cursos_iniciados": iniciados, + } + + +# ── Tabla Estado/Objeción (agrupa por Ultima_Etiqueta + desglose por asesor) ── +def tabla_estado_objecion(leads, ano, mes, dia): + leads_periodo = [l for l in leads if l["es_unico"] + and l["asesor"] and _en_periodo(l["fecha"], ano, mes, dia)] + agg = {} # estado -> total + por_asesor = {} # estado -> {asesor: cantidad} + telefonos = {} # (estado, asesor) -> [telefonos] + for l in leads_periodo: + est = l["ultima"] or "NO TRABAJADO" + ase = l["asesor"] or "SIN ASESOR" + agg[est] = agg.get(est, 0) + 1 + por_asesor.setdefault(est, {}) + por_asesor[est][ase] = por_asesor[est].get(ase, 0) + 1 + telefonos.setdefault((est, ase), []).append(l["telefono"]) + filas = [] + for estado in sorted(agg.keys(), key=lambda x: -agg[x]): # mayor a menor por cantidad + asesores = sorted(por_asesor[estado].items(), key=lambda x: -x[1]) + filas.append({ + "estado": estado, + "cantidad": agg[estado], + "asesores": [ + {"asesor": a, "cantidad": c, + "telefonos": sorted(telefonos.get((estado, a), []))} + for a, c in asesores + ], + }) + total = sum(agg.values()) + return {"filas": filas, "total": total} + + +# ── Matrículas por día (gráfico de línea) ── +# Cuenta TODAS las matrículas del día (misma base que Total Matriculados): +# estado ALU/PRE, excluyendo las 2 vendedoras, por día de fch_matricula. +def matriculas_por_dia(matriculas, cursos, ano, mes, dia): + VENDEDORES_EXCLUIDOS = {"CALDERON S. LISSA GENA", "CRUZ G. FIORELLA MELISSA"} + conteo = {} # dia -> total + por_tipo = {} # dia -> {tipo_programa: cantidad} + for m in matriculas: + if str(m.get("estado_matricula", "")).strip().upper() not in ("ALU", "PRE"): + continue + if str(m.get("dsc_vendedor", "")).strip() in VENDEDORES_EXCLUIDOS: + continue + d = _to_date(m.get("fch_matricula")) + if not _en_periodo(d, ano, mes, dia): + continue + conteo[d.day] = conteo.get(d.day, 0) + 1 + tp = tipo_programa_curso(m.get("dsc_programa")) # TEAC/TERC/PROVINCIA.../SEMINARIOS/OTROS + por_tipo.setdefault(d.day, {}) + por_tipo[d.day][tp] = por_tipo[d.day].get(tp, 0) + 1 + + def _detalle(day): + items = sorted(por_tipo.get(day, {}).items(), key=lambda x: -x[1]) + return [{"tipo": t, "cantidad": c} for t, c in items] + + import calendar + if mes not in ("TODOS", None) and ano not in ("TODOS", None): + ndias = calendar.monthrange(int(ano), int(mes))[1] + return [{"dia": d, "cantidad": conteo.get(d, 0), "detalle": _detalle(d)} + for d in range(1, ndias + 1)] + return [{"dia": d, "cantidad": conteo[d], "detalle": _detalle(d)} + for d in sorted(conteo.keys())] + + +# ── Gráfico "Leads por Programa": serie diaria Totales vs Procesados ── +def leads_por_dia(leads, ano, mes, dia): + lp = [l for l in leads if l["es_unico"] and _en_periodo(l["fecha"], ano, mes, dia)] + tot = {} # dia -> totales + proc = {} # dia -> procesados (con asesor) + for l in lp: + d = l["fecha"].day + tot[d] = tot.get(d, 0) + 1 + if l["asesor"]: + proc[d] = proc.get(d, 0) + 1 + import calendar + if mes not in ("TODOS", None) and ano not in ("TODOS", None): + ndias = calendar.monthrange(int(ano), int(mes))[1] + dias = range(1, ndias + 1) + else: + dias = sorted(set(list(tot.keys()) + list(proc.keys()))) + return [{"dia": d, "totales": tot.get(d, 0), "procesados": proc.get(d, 0)} for d in dias] + + +# ── Tabla por Sede_Act -> Tipo_Programa con 4 medidas (cohorte de leads) ── +def tabla_pauta(leads, matriculas, ano, mes, dia, importe_por_pauta_periodo=None, + resultados_por_pauta_periodo=None, campanias_sede_cargo=None): + importe_por_pauta_periodo = importe_por_pauta_periodo or {} + resultados_por_pauta_periodo = resultados_por_pauta_periodo or {} + campanias_sede_cargo = campanias_sede_cargo or {} + # 1) Leads únicos del periodo con su teléfono, fecha, sede y tipo + lp = [l for l in leads if l["es_unico"] and _en_periodo(l["fecha"], ano, mes, dia)] + # mapa teléfono -> fecha del lead (para "matrícula posterior al lead") + fecha_lead = {} + for l in lp: + fecha_lead.setdefault(l["telefono"], l["fecha"]) + + # 2) Matrículas que cruzan por teléfono y son POSTERIORES a la fecha del lead + # (réplica de Matriculas_Cohorte_Lead e Inversion_x_Pauta_Cohorte) + mat_por_tel = {} # telefono -> {"mats": set(num_matricula), "inv": float} + for m in matriculas: + tel = str(m.get("dsc_telefono_1") or "").strip() + if tel not in fecha_lead: + continue + fm = _to_date(m.get("fch_matricula")) + fl = fecha_lead[tel] + if not fm or not fl or not (fm > fl): + continue + # INV_NETA_FINAL: si DOL, ×3.34 + try: inv = float(m.get("INV_NETA", 0) or 0) + except: inv = 0.0 + if str(m.get("cod_moneda", "")).strip().upper() == "DOL": + inv *= 3.34 + e = mat_por_tel.setdefault(tel, {"mats": set(), "inv": 0.0}) + e["mats"].add(str(m.get("num_matricula"))) + e["inv"] += inv + + # 3) Agrupar por Sede -> Tipo_Programa -> Código + def _nuevo(): + return {"recibidos": 0, "procesados": 0, "mats": set(), "inv": 0.0} + grupos = {} # sede -> tipo -> codigo -> métricas + for l in lp: + sede = l["sede"]; tp = grupo_tipo_cohorte(l["tipo_programa"]); cod = l.get("codigo", "-"); tel = l["telefono"] + g = grupos.setdefault(sede, {}).setdefault(tp, {}).setdefault(cod, _nuevo()) + g["recibidos"] += 1 + if l["asesor"]: + g["procesados"] += 1 + if tel in mat_por_tel: + g["mats"] |= mat_por_tel[tel]["mats"] + g["inv"] += mat_por_tel[tel]["inv"] + + def _fila(d): + return {"recibidos": d["recibidos"], "procesados": d["procesados"], + "matriculas": len(d["mats"]), "inversion": round(d["inv"], 0)} + + # IMPORTE y RESULTADOS por PAUTA -> se asignan a su (sede, tipo) de CAMPANIAS, + # una sola vez por pauta (no por sede del lead), para NO duplicar. + # imp_st[(sede,tipo)][cod] = importe ; res_st[(sede,tipo)][cod] = resultados + imp_st = {} + res_st = {} + for cod, imp_val in importe_por_pauta_periodo.items(): + info = campanias_sede_cargo.get(str(cod).strip()) + if not info: + continue + se = str(info.get("sede") or "").upper() + tp = grupo_tipo_cohorte(tipo_programa_cohorte(se, info.get("cargo"))) + imp_st.setdefault((se, tp), {})[str(cod).strip()] = float(imp_val or 0.0) + for cod, res_val in resultados_por_pauta_periodo.items(): + info = campanias_sede_cargo.get(str(cod).strip()) + if not info: + continue + se = str(info.get("sede") or "").upper() + tp = grupo_tipo_cohorte(tipo_programa_cohorte(se, info.get("cargo"))) + res_st.setdefault((se, tp), {})[str(cod).strip()] = int(res_val or 0) + + filas = [] + for sede in sorted(grupos.keys()): + subfilas = [] + s_rec = s_proc = 0; s_mats = set(); s_inv = 0.0; s_imp = 0.0; s_res = 0 + for tp in sorted(grupos[sede].keys()): + cods = grupos[sede][tp] + imp_map = imp_st.get((sede.upper(), tp), {}) # importe por pauta de esta sede/tipo + res_map = res_st.get((sede.upper(), tp), {}) + # métricas del tipo (sumando sus códigos) + t_rec = t_proc = 0; t_mats = set(); t_inv = 0.0; t_imp = 0.0; t_res = 0 + codigos = [] + for cod in sorted(cods.keys()): + d = cods[cod] + imp_cod = float(imp_map.get(str(cod).strip(), 0.0)) + res_cod = int(res_map.get(str(cod).strip(), 0)) + codigos.append({"codigo": cod, **_fila(d), "importe": round(imp_cod, 2), + "resultados": res_cod}) + t_rec += d["recibidos"]; t_proc += d["procesados"] + t_mats |= d["mats"]; t_inv += d["inv"]; t_imp += imp_cod; t_res += res_cod + subfilas.append({ + "tipo": tp, "recibidos": t_rec, "procesados": t_proc, + "matriculas": len(t_mats), "inversion": round(t_inv, 0), + "importe": round(t_imp, 2), "resultados": t_res, "codigos": codigos, + }) + s_rec += t_rec; s_proc += t_proc; s_mats |= t_mats; s_inv += t_inv; s_imp += t_imp; s_res += t_res + filas.append({ + "sede": sede, "recibidos": s_rec, "procesados": s_proc, + "matriculas": len(s_mats), "inversion": round(s_inv, 0), + "importe": round(s_imp, 2), "resultados": s_res, "subfilas": subfilas, + }) + # Totales + t_rec = sum(f["recibidos"] for f in filas) + t_proc = sum(f["procesados"] for f in filas) + t_inv = round(sum(f["inversion"] for f in filas), 0) + # Total de importe/resultados: suma de TODAS las pautas que estan en campanias + # (una vez cada una), aunque no tengan fila con leads. Asi el total no se pierde. + t_imp = round(sum(float(v or 0.0) for cod, v in importe_por_pauta_periodo.items() + if str(cod).strip() in campanias_sede_cargo), 2) + t_res = sum(int(v or 0) for cod, v in resultados_por_pauta_periodo.items() + if str(cod).strip() in campanias_sede_cargo) + # matrículas total: unión global + all_mats = set() + for tel, e in mat_por_tel.items(): + all_mats |= e["mats"] + return {"filas": filas, "total": {"recibidos": t_rec, "procesados": t_proc, + "matriculas": len(all_mats), "inversion": t_inv, + "importe": t_imp, "resultados": t_res}} + + +# ── Personalizado: replica la columna Fact_SQL_Base_Cursos[Personalizado] (M) ── +# Base = dsc_det_programa, limpiando saltos de línea, luego cadena de reemplazos. +_PERS_REEMPLAZOS = [ + ("TECNICO ESPECIALISTA EN AIRE ACONDICIONADO", "TEAC"), + ("TECNICO ESPECIALISTA EN REFRIGERACION COMERCIAL", "TERC"), + ("TECNICO ESPECIALISTA EN REFRIGERACION DOMÉSTICA Y COMERCIAL", "TERC"), + ("CARRERA TECNICA DE AIRE ACONDICIONADO Y REFRIGERACION ", "CARRERA"), + ("VIRTUAL SEMINARIO SUPERVISION DE OBRAS EN AIRE ACONDICIONADO", "SUPERVISION DE OBRAS"), + ("SEMINARIO APLICACIÓN DE VARIADORES DE FRECUENCIA EN SISTEMAS HVAC", "VARIADORES DE FRECUENCIA"), + ("VIRTUAL - SISTEMAS DE REFRIGERACION CON CO2 - FASE SUBCRITICA Y TRANSCRITICA", "REFRIGERACION CON CO2"), + ("VIRTUAL SEMINARIO DISEÑO DE CAMARAS DE REFRIGERACION CON SISTEMAS CON FREON", "DISEÑO DE CAMARAS"), + ("VIRTUAL SEM. DETERMINACIÓN DE CAPACIDAD DE EQUIPOS DE AIRE ACONDICIONADO- CARGAS TÉRMICAS -(A/A)", "CARGAS TERMICAS"), + ("VIRTUAL SEMINARIO DIBUJO TECNICO Y DISEÑO ASISTIDO POR COMPUTADORA (CAD) PARA HVAC -(A/A)", "DIBUJO TECNICO (CAD)"), + ("VIRTUAL SEMINARIO: SISTEMAS DE REFRIGERACIÓN INDUSTRIAL POR AMONIACO (NH3)", "AMONIACO (NH3)"), + ("SEMINARIO VIRTUAL METRADO , COSTEO Y PRESUPUESTOS DE HVAC (AA)", "METRADO, COSTEO Y PRESUPUESTOS"), + ("CARRERA TECNICA LIMA GESTION DE EMPRESAS", "GESTION DE EMPRESAS"), + ("VIRTUAL SEMINARIO DISEÑO DE CHILLERS PARA PROCESOS INDUSTRIALES DE REFRIGERACIÓN", "DISEÑO DE CHILLERS"), + ("VIRTUAL SEMINARIO PRACTICO VENTILACION DE SOTANO Y PREZURIZACION DE ESCALERA -(A/A)", "VENTILACION DE SOTANO"), + ("SEMINARIO USO DE SOFTWARE EN CÁLCULOS DE AIRE ACONDICIONADO", "SOFTWARE EN CALCULOS DE A/A"), + ("DISEÑO DE SISTEMAS DE AIRE ACONDICIONADO", "DISEÑO DE SISTEMAS DE A/A"), + ("SEMINARIO INSTALACIÓN DE SISTEMAS DE A/A DE VOLUMEN VARIABLE VRV/VRF - SAB", "VOLUMEN VARIABLE VRF"), + # Seminarios VRF por sede y CO2 → nombre corto (la frecuencia se conserva al final) + ("PIURA - SEMINARIO INSTALACIÓN DE SISTEMAS DE A/A DE VOLUMEN VARIABLE - VRF", "PIURA - VRF"), + ("AREQUIPA - SEMINARIO INSTALACIÓN DE SISTEMAS DE A/A DE VOLUMEN VARIABLE - VRF", "AREQUIPA - VRF"), + ("TRUJILLO - SEMINARIO INSTALACIÓN DE SISTEMAS DE A/A DE VOLUMEN VARIABLE - VRF", "TRUJILLO - VRF"), + ("VIRTUAL - DISEÑO Y OPERACIÓN DE SISTEMAS DE REFRIGERACIÓN CON CO2 EN FASE SUBCRITICA Y TRANSCRITICA", "CO2"), + # Reemplazos sobre Nombre_Programa_Detalle (bloque ductos y otros) + ("VIRTUAL - DIMENSIONAMIENTO DE", ""), + ("METÁLICOS EN AIRE ACONDICIONADO Y VENTILACIÓN", ""), + ("MASTERCLASS: PROGRAMACIÓN DE CONTROLADORES PARA REFRIGERACIÓN", "PROGRAMACION DE CONTROLADORES"), + ("DIPLOMADO INTERNACIONAL DE AIRE ACONDICIONADO", "DIPLOMADO DE A/A"), + # Reemplazos finales (sedes/carreras/masterclass) + ("AREQUIPA - CARRERA TÉCNICA DE AIRE ACONDICIONADO Y REFRIGERACIÓN - TAR", "AREQUIPA - CARRERA - TARDE"), + ("PIURA - CARRERA TÉCNICA DE AIRE ACONDICIONADO Y REFRIGERACIÓN - TAR", "PIURA - CARRERA - TARDE"), + ("TRUJILLO - CARRERA TÉCNICA DE AIRE ACONDICIONADO Y REFRIGERACIÓN - TAR", "TRUJILLO - CARRERA - TARDE"), + ("AREQUIPA - CARRERA TÉCNICA DE AIRE ACONDICIONADO Y REFRIGERACIÓN - MAN", "AREQUIPA - CARRERA - MAÑANA"), + ("PIURA - CARRERA TÉCNICA DE AIRE ACONDICIONADO Y REFRIGERACIÓN - MAN", "PIURA - CARRERA - MAÑANA"), + ("TRUJILLO - CARRERA TÉCNICA DE AIRE ACONDICIONADO Y REFRIGERACIÓN - MAN", "TRUJILLO - CARRERA - MAÑANA"), + ("GESTIÓN DE VENTA DE EQUIPOS DE AIRE ACONDICIONADO Y EQUIPOS DE REFRIGERACIÓN - NOC", "GESTIÓN DE VENTA - NOC"), + ("MASTERCLASS SELECCIÓN E INSTALACIÓN DE TARJETAS ELECTRÓNICAS UNIVERSALES EN EQUIPOS DE A/A .INVERTER - MAN", "MASTERCLASS TARJETAS UNIVERSALES - MAN"), + ("MASTERCLASS: SISTEMAS DE A/A CON VRF - SAB", "MASTERCLASS: A/A CON VRF - SAB"), + ("GESTIÓN DE VENTA DE EQUIPOS DE AIRE ACONDICIONADO Y EQUIPOS DE REFRIGERACIÓN - TAR", "GESTIÓN DE VENTA - TAR"), +] + + +def personalizado_curso(dsc_programa, cod_frecuencia=""): + """Replica Fact_SQL_Base_Cursos[Personalizado] del PBI: + base = dsc_programa & ' - ' & cod_frecuencia (NO dsc_det_programa).""" + s = f"{str(dsc_programa or '')} - {str(cod_frecuencia or '')}" + # Text.Clean + reemplazar saltos de línea por espacio, luego Trim + s = s.replace("\r", " ").replace("\n", " ") + s = "".join(ch for ch in s if ch >= " " or ch == " ") # Text.Clean (quita control) + s = s.strip() + for buscar, reemplazo in _PERS_REEMPLAZOS: + s = s.replace(buscar, reemplazo) + # colapsar espacios múltiples que pudieran quedar de reemplazos vacíos + s = " ".join(s.split()) + return s + + +# ── Matriz por Curso (num_indice): Personalizado + métricas de leads por pauta ── +# NOTA: las métricas de leads cruzan por el CÓDIGO DE PAUTA del curso, que viene de +# SharePoint (no disponible aún). Por eso salen en 0 hasta conectar esa fuente. +# Ver FUTUROS_CAMBIOS.md punto 5. Lo que sí se calcula: Personalizado y Fecha Inicio. +def matriz_cursos(cursos, leads, ano, mes, dia, pauta_map=None, + campanias_map=None, cartera_all=None, cartera_asig=None, + mat_por_indice=None, cartera_tels=None, cartera_origen=None, + importe_por_pauta=None, importe_por_pauta_periodo=None, + cartera_canal=None, conjunto_por_pauta=None, pautas_por_indice=None): + pauta_map = pauta_map or {} + pautas_por_indice = pautas_por_indice or {} # {num_indice: [pautas]} (muchos-a-muchos) + campanias_map = campanias_map or {} + cartera_all = cartera_all or {} + cartera_asig = cartera_asig or {} + mat_por_indice = mat_por_indice or {} + cartera_tels = cartera_tels or set() + cartera_origen = cartera_origen or {} + importe_por_pauta = importe_por_pauta or {} + importe_por_pauta_periodo = importe_por_pauta_periodo or {} + conjunto_por_pauta = conjunto_por_pauta or {} # {pauta: [conjuntos de anuncios]} + # cartera_canal = {"PAUTA": {"all","asig","tels"}, "OTROS": {...}} para el desglose por canal + cartera_canal = cartera_canal or {} + + CURSOS_EXCLUIDOS = {"1121", "1154"} + + def _excluir(c): + ni = str(c.get("num_indice", "")) + if ni in CURSOS_EXCLUIDOS: + return True + info = pauta_map.get(ni) + if not info: + return False # sin info en Supabase → no se excluye + # Excluir SUSPENDIDO o contar = NO + return info.get("contar") == "NO" or info.get("estado") == "SUSPENDIDO" + + cursos_periodo = [c for c in cursos + if _en_periodo(_to_date(c.get("fch_inicio")), ano, mes, dia) + and not _excluir(c)] + + # Leads únicos agrupados por codigo (= Pauta). Históricos y del mes (filtro fecha). + leads_unicos = [l for l in leads if l.get("es_unico")] + hist_por_cod = {} # codigo → set teléfonos + hist_ases_cod = {} # codigo → set teléfonos con asesor + mes_por_cod = {} # codigo → set teléfonos (en periodo filtrado) + mes_ases_cod = {} # codigo → set teléfonos con asesor (en periodo) + for l in leads_unicos: + cod = l.get("codigo", "-") + tel = l["telefono"] + hist_por_cod.setdefault(cod, set()).add(tel) + if l.get("asesor"): + hist_ases_cod.setdefault(cod, set()).add(tel) + if _en_periodo(l["fecha"], ano, mes, dia): + mes_por_cod.setdefault(cod, set()).add(tel) + if l.get("asesor"): + mes_ases_cod.setdefault(cod, set()).add(tel) + + # Construir filas con sus medidas + pre = [] + for c in sorted(cursos_periodo, key=lambda x: (_to_date(x.get("fch_inicio")) or date.min)): + nombre = personalizado_curso(c.get("dsc_programa"), c.get("cod_frecuencia")) + fch = _to_date(c.get("fch_inicio")) + ni = str(c.get("num_indice", "")).strip() + # LISTA de pautas del num_indice (muchos-a-muchos). Si no hay en la tabla nueva, + # usa la de basebi (compatibilidad). + cods = pautas_por_indice.get(ni) + if not cods: + _info = pauta_map.get(ni) + cods = [_info.get("pauta")] if _info and _info.get("pauta") else [] + cods = [str(x).strip() for x in cods if x] + # Leads = union de telefonos de TODAS las pautas (sin duplicar) + def _union(mapa): + s = set() + for cd in cods: + s |= mapa.get(cd, set()) + return len(s) + nuevos = _union(hist_por_cod) + nuevos_a = _union(hist_ases_cod) + nuevos_m = _union(mes_por_cod) + nuevos_ma = _union(mes_ases_cod) + pre.append({ + "num_indice": ni, + "personalizado": nombre, + "fch": fch, + "pauta": cods[0] if cods else None, # pauta principal (para mostrar) + "pautas": cods, # todas las pautas (para sumar) + "fecha_inicio": fch.strftime("%d/%m/%Y") if fch else "", + "leads_nuevos": nuevos, + "leads_nuevos_asesor": nuevos_a, + "leads_nuevos_mes": nuevos_m, + "leads_nuevos_mes_asesor": nuevos_ma, + }) + + # Índice HISTÓRICO completo (TODOS los cursos, sin filtro de fecha) por + # personalizado → lista de (fecha, leads_nuevos). Para el acumulado. + hist_cursos = {} + for c in cursos: + if _excluir(c): + continue + nom = personalizado_curso(c.get("dsc_programa"), c.get("cod_frecuencia")) + f = _to_date(c.get("fch_inicio")) + info = pauta_map.get(str(c.get("num_indice", ""))) + cd = info.get("pauta") if info else None + nv = len(hist_por_cod.get(cd, ())) if cd else 0 + hist_cursos.setdefault(nom, []).append((f, nv)) + + # Leads_Acumulados_Historico: suma de leads_nuevos de TODOS los cursos del + # MISMO personalizado con fecha <= la del curso actual (histórico completo). + filas = [] + for r in pre: + acum = sum(nv for (f, nv) in hist_cursos.get(r["personalizado"], []) + if f and r["fch"] and f <= r["fch"]) + # CARTERA TOTAL: teléfonos únicos de cartera_junta con la misma SEDE+PROGRAMA + # del curso (según su pauta → campanias) cuya fecha_creada <= fch_inicio del curso. + # cartera_total = todos; cartera_total_asig = solo con asesor asignado. + cartera_total = 0 + cartera_total_asig = 0 + cod = r.get("pauta") + cods = r.get("pautas") or ([cod] if cod else []) + if cods and r["fch"]: + # Cartera = union de telefonos de la sede+programa de TODAS las pautas del num_indice + tels_all = {}; tels_asig = {} + for cd in cods: + sp = campanias_map.get(str(cd).upper()) + if not sp: + continue + for tel, fx in cartera_all.get(sp, {}).items(): + if fx and (tel not in tels_all or fx < tels_all[tel]): + tels_all[tel] = fx + for tel, fx in cartera_asig.get(sp, {}).items(): + if fx and (tel not in tels_asig or fx < tels_asig[tel]): + tels_asig[tel] = fx + cartera_total = sum(1 for fx in tels_all.values() if fx <= r["fch"]) + cartera_total_asig = sum(1 for fx in tels_asig.values() if fx <= r["fch"]) + # MATRÍCULAS NO IDENTIFICADAS: matrículas de este curso (num_indice) cuyo teléfono + # (tel_1, o tel_2 si el 1 está vacío) NO aparece en cartera_junta. Sin filtro de fecha. + _ni = r["num_indice"] + _ni = _ni[:-2] if _ni.endswith(".0") else _ni + _mats = mat_por_indice.get(_ni, []) # lista de (telefono, fecha_matricula) + _tels_no_iden = [ph for (ph, fm) in _mats if ph not in cartera_tels] + matriculas_no_iden = len(_tels_no_iden) + # MATRICULAS LEADS NUEVOS: matrícula hecha dentro de 45 días DESPUÉS del origen + # del lead (registro es_origen=SI de ese teléfono en la cartera). + mat_leads_nuevos = 0 + mat_leads_antiguos = 0 + _tels_nuevos = [] + _tels_antiguos = [] + for (ph, fm) in _mats: + if not ph or not fm: + continue + forig = cartera_origen.get(ph) + if forig is None: + continue + dias = (fm - forig).days + if 0 <= dias <= 45: + mat_leads_nuevos += 1 + _tels_nuevos.append(ph) + else: + # >45 días, o matrícula anterior al origen del lead (dias<0) → Antiguo + mat_leads_antiguos += 1 + _tels_antiguos.append(ph) + # IMPORTE PAUTA: gasto de Meta Ads sumando TODAS las pautas del num_indice. + importe_pauta = round(sum(float(importe_por_pauta.get(str(cd), 0.0)) for cd in cods), 2) + # IMPORTE PAUTA EN EL MES: igual, pero solo del periodo filtrado. + importe_pauta_mes = round(sum(float(importe_por_pauta_periodo.get(str(cd), 0.0)) for cd in cods), 2) + + # ── SUBFILAS por CANAL (PAUTA / OTROS) ── mismo criterio que la fila padre + # (SEDE+PROGRAMA vía campanias, misma fecha) + filtro de canal desde cartera. + def _subfila(clave): + cc = cartera_canal.get(clave, {}) + c_all = cc.get("all", {}); c_asig = cc.get("asig", {}); c_tels = cc.get("tels", set()) + ct = ct_asig = 0 + l_rec = l_proc = 0 + l_rec_mes = l_proc_mes = 0 + if cod and r["fch"]: + sp = campanias_map.get(str(cod).upper()) + if sp: + fa = c_all.get(sp, {}) + fg = c_asig.get(sp, {}) + # Cartera Total/Asig del canal: fecha_creada <= inicio del curso (igual que padre) + ct = sum(1 for fx in fa.values() if fx and fx <= r["fch"]) + ct_asig = sum(1 for fx in fg.values() if fx and fx <= r["fch"]) + # L. Recibidos/Procesados del canal = misma cartera del canal (mismo corte) + l_rec = ct + l_proc = ct_asig + # "del Mes" = fecha_creada en el periodo filtrado + l_rec_mes = sum(1 for fx in fa.values() if _en_periodo(fx, ano, mes, dia)) + l_proc_mes = sum(1 for fx in fg.values() if _en_periodo(fx, ano, mes, dia)) + # Matrículas del curso, clasificadas por canal: + # - NO identificadas (tel NO en cartera) -> todas a OTROS. + # - identificadas -> al canal de su telefono (c_tels), regla 45 dias. + mt_noiden = mt_nuevos = mt_antiguos = 0 + for (ph, fm) in _mats: + if not ph: + continue + en_cartera = ph in cartera_tels + if not en_cartera: + # no identificada: solo cuenta en la subfila OTROS + if clave == "OTROS": + mt_noiden += 1 + continue + # identificada: solo cuenta si su tel pertenece a ESTE canal + if ph not in c_tels: + continue + forig = cartera_origen.get(ph) + if fm and forig: + d = (fm - forig).days + if 0 <= d <= 45: mt_nuevos += 1 + else: mt_antiguos += 1 + return {"canal": clave, "cartera_total": ct, "cartera_total_asig": ct_asig, + "leads_nuevos": l_rec, "leads_nuevos_asesor": l_proc, + "leads_nuevos_mes": l_rec_mes, "leads_nuevos_mes_asesor": l_proc_mes, + "matriculas_no_iden": mt_noiden, "mat_leads_nuevos": mt_nuevos, + "mat_leads_antiguos": mt_antiguos} + subfilas = [_subfila("PAUTA"), _subfila("WEB"), _subfila("OTROS")] if cartera_canal else [] + + filas.append({ + "num_indice": r["num_indice"], + "personalizado": r["personalizado"], + "fecha_inicio": r["fecha_inicio"], + "pauta": r.get("pauta"), + "importe_pauta": importe_pauta, + "importe_pauta_mes": importe_pauta_mes, + "cartera_total": cartera_total, + "cartera_total_asig": cartera_total_asig, + "matriculas_no_iden": matriculas_no_iden, + "matriculas_no_iden_tels": _tels_no_iden, + "mat_leads_nuevos": mat_leads_nuevos, + "mat_leads_antiguos": mat_leads_antiguos, + "mat_leads_nuevos_tels": _tels_nuevos, + "mat_leads_antiguos_tels": _tels_antiguos, + "leads_acumulados": acum, + "leads_nuevos": r["leads_nuevos"], + "leads_nuevos_asesor": r["leads_nuevos_asesor"], + "leads_nuevos_mes": r["leads_nuevos_mes"], + "leads_nuevos_mes_asesor": r["leads_nuevos_mes_asesor"], + "subfilas_canal": subfilas, + "conjuntos": [cj for cd in cods for cj in conjunto_por_pauta.get(str(cd).strip(), [])], + "contar": (pauta_map.get(str(r["num_indice"]).strip()) or {}).get("contar") or "SI", + }) + return {"filas": filas} diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..455a3da --- /dev/null +++ b/backend/main.py @@ -0,0 +1,275 @@ +# backend/main.py +"""API REST del Dashboard de LEADS (FastAPI).""" +from fastapi import FastAPI, Query, HTTPException, Body +from fastapi.middleware.cors import CORSMiddleware +from datetime import datetime +import uvicorn + +import services +from cache_manager import start_background_refresh, cache_stats + +app = FastAPI(title="Dashboard Leads API", version="1.0") + +app.add_middleware( + CORSMiddleware, allow_origins=["*"], allow_credentials=True, + allow_methods=["*"], allow_headers=["*"], +) + + +@app.on_event("startup") +def _startup(): + # Precarga en SEGUNDO PLANO (hilo aparte) para no bloquear el arranque del server. + import threading + def _precarga_bg(): + print("[startup] Precargando datos de Leads (2do plano)...") + try: + services.precargar_todo() + print("[startup] Precarga completa.") + except Exception as e: + print(f"[startup] Precarga falló: {e}") + threading.Thread(target=_precarga_bg, daemon=True).start() + # Otros General en un hilo aparte (sus consultas son mas pesadas) + def _precarga_otros_bg(): + try: + services.precargar_otros_general() + except Exception as e: + print(f"[startup] Precarga Otros falló: {e}") + threading.Thread(target=_precarga_otros_bg, daemon=True).start() + start_background_refresh(services.refrescar_todo, interval=900) + + +@app.get("/api/health") +def health(): + return {"status": "ok", "hora": datetime.now().isoformat()} + + +@app.get("/api/cache/stats") +def stats(): + return cache_stats() + + +@app.post("/api/cache/refresh") +def refresh(): + services.refrescar_todo() + return {"status": "refrescado"} + + +@app.get("/api/leads/filtros") +def get_filtros(): + try: + return services.opciones_filtros() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/leads") +def get_leads( + ano: str = Query("TODOS"), + mes: str = Query("TODOS"), + dia: str = Query("TODOS"), + programa: str = Query("TODOS"), + sede: str = Query("TODOS"), +): + try: + return services.leads_dashboard(ano, mes, dia, programa, sede) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/vendedores") +def get_vendedores( + ano: str = Query("TODOS"), + mes: str = Query("TODOS"), + dia: str = Query("TODOS"), +): + try: + return services.vendedores_dashboard(ano, mes, dia) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/roas") +def get_roas( + ano: str = Query("TODOS"), + mes: str = Query("TODOS"), + dia: str = Query("TODOS"), +): + try: + return services.roas_dashboard(ano, mes, dia) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/conjuntos-sin-sede") +def get_conjuntos_sin_sede(): + try: + return services.conjuntos_sin_sede() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/conjuntos-sin-sede/guardar") +def post_conjunto_sede(body: dict = Body(...)): + try: + cambios = body.get("cambios") or [] + return services.guardar_conjunto_sede(cambios) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/conjuntos-sin-sede/borrar") +def post_borrar_conjunto_sede(body: dict = Body(...)): + try: + return services.borrar_conjunto_sede(body.get("conjunto") or "") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/otros-general") +def get_otros_general( + ano: str = Query("TODOS"), + mes: str = Query("TODOS"), + dia: str = Query("TODOS"), +): + try: + return services.otros_general(ano, mes, dia) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/conjuntos-sin-pauta") +def get_conjuntos_sin_pauta(): + try: + return {"conjuntos": services.conjuntos_sin_pauta()} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/programas-ocultos") +def get_programas_ocultos(): + try: + return services.programas_ocultos() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/programas-ocultos/encender") +def post_encender_programas(body: dict = Body(...)): + try: + nis = body.get("num_indices") or [] + return services.encender_programas(nis) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/programas-disponibles") +def get_programas_disponibles(): + try: + return services.programas_disponibles() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/leyenda-anuncios") +def get_leyenda(): + try: + return services.leyenda_anuncios() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/leyenda-anuncios/guardar") +def post_leyenda(body: dict = Body(...)): + try: + cambios = body.get("cambios") or [] + return services.guardar_leyenda(cambios) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# ── LEYENDA (campanias de Supabase): listar / agregar / borrar ── +@app.get("/api/campanias") +def get_campanias(): + try: + return {"filas": services.campanias_listar()} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/campanias/agregar") +def post_campania(body: dict = Body(...)): + try: + return services.campania_agregar(body or {}) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/campanias/editar") +def post_editar_campania(body: dict = Body(...)): + try: + cid = body.get("id") + return services.campania_editar(cid, body or {}) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/campanias/borrar") +def post_borrar_campania(body: dict = Body(...)): + try: + return services.campania_borrar(body.get("id")) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/pauta/uso") +def get_uso_pauta(pauta: str = Query(...), excluir: str = Query("")): + try: + return services.uso_de_pauta(pauta, excluir) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/curso/guardar-pauta") +def post_guardar_pauta(body: dict = Body(...)): + try: + ni = str(body.get("num_indice", "")).strip() + pa = str(body.get("pauta", "")).strip() + cj = body.get("conjunto") or None + contar = body.get("contar") # "SI" / "NO" / None + if not ni: + raise HTTPException(status_code=400, detail="num_indice es obligatorio") + return services.guardar_edicion_curso(ni, pa, cj, contar) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/ultima-actualizacion") +def get_ultima_actualizacion(): + try: + return services.ultima_actualizacion() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/leads/alertas") +def get_alertas(): + """Valores de programa/sede en cartera_junta que NO están en el diccionario.""" + try: + return services.alertas() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/debug/cursos") +def debug_cursos(ano: str = "2026", mes: str = "1"): + """Diagnóstico: lista cursos del periodo con inscritos, tipo y meta.""" + try: + return services.debug_cursos(ano, mes) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=False) diff --git a/backend/medir_tiempos.py b/backend/medir_tiempos.py new file mode 100644 index 0000000..d0ccb01 --- /dev/null +++ b/backend/medir_tiempos.py @@ -0,0 +1,70 @@ +# medir_tiempos.py +# Mide cuánto tarda CADA parte que carga el dashboard de Leads al iniciar. +# Simula lo mismo que hace el backend en su precarga, pero cronometrando cada paso. +# Ejecutar en backend/: python medir_tiempos.py +import time +import services as S + +def cron(nombre, fn): + t0 = time.perf_counter() + ok = "OK" + extra = "" + try: + r = fn() + try: + if isinstance(r, (list, dict)): + extra = f"({len(r)} items)" + except Exception: + pass + except Exception as e: + ok = "ERROR" + extra = str(e)[:60] + dt = time.perf_counter() - t0 + print(f" {nombre:38} {dt:7.2f}s {ok} {extra}") + return dt + +periodos = S._periodos_actual_anterior() +a0, m0 = periodos[0] + +print("\n" + "="*70) +print(" MEDICIÓN DE TIEMPOS DE CARGA — Dashboard LEADS") +print("="*70) + +print("\n[1] INSUMOS CRUDOS (consultas base, se cargan 1 vez)") +tot1 = 0 +tot1 += cron("Leads (Chatwoot/PostgreSQL)", S._leads_crudos) +tot1 += cron("Cursos (SQL Server)", S._cursos_crudos) +tot1 += cron("Matriculas (SQL Server)", S._matriculas_crudas) +tot1 += cron("Leads asignados/respuesta", S._leads_asignados_crudos) +tot1 += cron("Cartera (Supabase)", S._cartera_rows_cache) +tot1 += cron("Plantillas", S._plantillas_crudas) + +print(f"\n --> Subtotal insumos: {tot1:.2f}s") + +print(f"\n[2] APARTADO LEADS (periodo {a0}-{m0}, filtros TODOS)") +tot2 = 0 +tot2 += cron("leads_dashboard", lambda: S.leads_dashboard(a0, m0, "TODOS", "TODOS", "TODOS")) +tot2 += cron("leads_filtros", S.opciones_filtros) +print(f"\n --> Subtotal Leads: {tot2:.2f}s") + +print(f"\n[3] APARTADO VENDEDORES (periodo {a0}-{m0})") +tot3 = 0 +tot3 += cron("vendedores_dashboard", lambda: S.vendedores_dashboard(a0, m0, "TODOS")) +print(f"\n --> Subtotal Vendedores: {tot3:.2f}s") + +print(f"\n[4] APARTADO OTROS GENERAL (periodo {a0}-{m0}) — el mas pesado") +tot4 = 0 +tot4 += cron("otros_general", lambda: S.otros_general(a0, m0, "TODOS")) +print(f"\n --> Subtotal Otros General: {tot4:.2f}s") + +print(f"\n[5] APARTADO ROAS (periodo {a0}-{m0})") +tot5 = 0 +tot5 += cron("roas_dashboard", lambda: S.roas_dashboard(a0, m0, "TODOS")) +print(f"\n --> Subtotal ROAS: {tot5:.2f}s") + +total = tot1 + tot2 + tot3 + tot4 + tot5 +print("\n" + "="*70) +print(f" TIEMPO TOTAL (todo en frio, 1a vez): {total:.2f}s") +print(" Nota: en el arranque real, [1]+[2] van en un hilo y [4] en otro,") +print(" asi que corren EN PARALELO. El tiempo real es ~ el mas lento de los dos.") +print("="*70 + "\n") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..0b80d1a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn +python-dotenv +pyodbc +psycopg2-binary +requests diff --git a/backend/services.py b/backend/services.py new file mode 100644 index 0000000..c12d124 --- /dev/null +++ b/backend/services.py @@ -0,0 +1,1768 @@ +# 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 + + +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 = ( + "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", +) + + +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 + invalidar_campanias_cache() + for p in _PREF_CAMPANIAS: + cache_invalidate(p) + + +# ── 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(): + return cache_get_or_set("cursos_raw", ("GLOBAL",), lambda: get_dm().traer_cursos()) + + +def _matriculas_crudas(): + return cache_get_or_set("mat_raw", ("GLOBAL",), lambda: get_dm().traer_matriculas()) + + +def _pauta_cruda(): + return cache_get_or_set("pauta_raw", ("GLOBAL",), lambda: get_dm().traer_pauta_cursos()) + + +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(): + """{num_indice: [pautas]} combinando: + - la pauta de basebi_programacion (1 por num_indice), y + - las pautas extra de programa_pautas (varias). + Sin duplicados. Un num_indice puede terminar con varias pautas que se SUMAN.""" + def _load(): + pm = _pauta_cruda() # {num_indice: {pauta,...}} + extra = _programa_pautas() # {num_indice: [pautas]} + out = {} + for ni, info in pm.items(): + p = info.get("pauta") + if p: + out.setdefault(str(ni).strip(), []).append(str(p).strip()) + for ni, pautas in extra.items(): + lst = out.setdefault(str(ni).strip(), []) + for p in pautas: + if str(p).strip() not in lst: + lst.append(str(p).strip()) + return out + return cache_get_or_set("pautas_de_indice", ("GLOBAL",), _load) + + +def _campanias_map(): + return cache_get_or_set("campanias_map", ("GLOBAL",), + lambda: get_dm().traer_campanias_map()) + + +def _conjunto_pauta(): + return cache_get_or_set("conjunto_pauta", ("GLOBAL",), + lambda: get_dm().traer_conjunto_pauta()) + + +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(): + """{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(): + conj_pauta = _conjunto_pauta() # {pauta: [conjunto,...]} + imp_norm = {} + for f in _meta_filas(): + k = " ".join(str(f.get("conjunto")).split()).upper() + imp_norm[k] = imp_norm.get(k, 0.0) + float(f.get("importe") or 0.0) + out = {} + for pauta, conjuntos in conj_pauta.items(): + total = sum(imp_norm.get(" ".join(str(co).split()).upper(), 0.0) for co in conjuntos) + out[str(pauta).strip()] = round(total, 2) + return out + return cache_get_or_set("importe_pauta", ("GLOBAL",), _load) + + +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(): + conj_pauta = _conjunto_pauta() + imp_norm = {} + sin_filtro = (ano in ("TODOS", None) and mes in ("TODOS", None) and dia in ("TODOS", None)) + for f in _meta_filas(): + fx = L._to_date(f.get("inicio")) + if sin_filtro: + pass # sin filtro: cuenta todo (incluye filas sin fecha) = total + elif not L._en_periodo(fx, ano, mes, dia): + continue + k = " ".join(str(f.get("conjunto")).split()).upper() + imp_norm[k] = imp_norm.get(k, 0.0) + float(f.get("importe") or 0.0) + out = {} + for pauta, conjuntos in conj_pauta.items(): + total = sum(imp_norm.get(" ".join(str(co).split()).upper(), 0.0) for co in conjuntos) + out[str(pauta).strip()] = round(total, 2) + return out + return cache_get_or_set("importe_pauta_periodo", (ano, mes, dia), _load) + + +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(): + conj_pauta = _conjunto_pauta() + res_norm = {} + sin_filtro = (ano in ("TODOS", None) and mes in ("TODOS", None) and dia in ("TODOS", None)) + for f in _meta_filas(): + if not sin_filtro and not L._en_periodo(L._to_date(f.get("inicio")), ano, mes, dia): + continue + k = " ".join(str(f.get("conjunto")).split()).upper() + res_norm[k] = res_norm.get(k, 0.0) + float(f.get("resultados") or 0.0) + out = {} + for pauta, conjuntos in conj_pauta.items(): + total = sum(res_norm.get(" ".join(str(co).split()).upper(), 0.0) for co in conjuntos) + out[str(pauta).strip()] = int(total) + return out + return cache_get_or_set("resultados_pauta_periodo", (ano, mes, dia), _load) + + +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"): + """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). + Sede/programa salen de la tabla campanias. Pautas sin campanias NO se cuentan.""" + def _load(): + conj_pauta = _conjunto_pauta() # {pauta: [conjuntos]} + camp = _campanias_sede_cargo() # {pauta: {sede,cargo}} + pauta_map = _pauta_cruda() # {num_indice: {pauta,...}} + con_indice = {str(v.get("pauta")).strip() for v in pauta_map.values() if v.get("pauta")} + + # Leads nuevos (unicos) por pauta = codigo, cuya fecha_creada cae en el mes filtrado + leads = _leads_crudos() + leads_nuevos = {} # codigo -> nº leads unicos del mes + leads_nuevos_asig = {} # codigo -> nº leads unicos del mes con asesor + tel_lead_mes = {} # telefono -> (codigo, fecha_creada) del lead unico del mes + for l in leads: + if not l.get("es_unico"): + continue + if not L._en_periodo(l.get("fecha"), ano, mes, dia): + continue + cod = str(l.get("codigo") or "").strip() + if not cod: + continue + leads_nuevos[cod] = leads_nuevos.get(cod, 0) + 1 + if str(l.get("asesor") or "").strip(): + leads_nuevos_asig[cod] = leads_nuevos_asig.get(cod, 0) + 1 + tel = str(l.get("telefono") or "").strip() + if tel: + tel_lead_mes[tel] = (cod, l.get("fecha")) + + # MATRICULAS por pauta: matriculas cuyo telefono = un lead nuevo del mes, y con + # fch_matricula >= fecha_creada del lead. Cuenta CADA matricula (una vez c/u), + # atribuida a la pauta del lead. Cruce por telefono (tel_1, o tel_2 si vacio). + matriculas_pauta = {} # codigo -> nº matriculas + venta_pauta = {} # codigo -> suma INV_NETA en soles (DOL ×3.34) + _vistas = set() # (num_matricula) para no contar 2 veces + for m in _matriculas_crudas(): + t1 = str(m.get("dsc_telefono_1") or "").strip() + t2 = str(m.get("dsc_telefono_2") or "").strip() + ph = t1 if t1 else t2 + if ph not in tel_lead_mes: + continue + cod, f_lead = tel_lead_mes[ph] + fm = L._to_date(m.get("fch_matricula")) + if not fm or not f_lead or fm < f_lead: + continue # matricula anterior al ingreso del lead -> no cuenta + nm = str(m.get("num_matricula")) + if nm in _vistas: + continue + _vistas.add(nm) + matriculas_pauta[cod] = matriculas_pauta.get(cod, 0) + 1 + # INV_NETA en soles: si moneda DOL, ×3.34 (igual que tabla por Sede) + try: + inv = float(m.get("INV_NETA", 0) or 0) + except (TypeError, ValueError): + inv = 0.0 + if str(m.get("cod_moneda", "")).strip().upper() == "DOL": + inv *= 3.34 + venta_pauta[cod] = venta_pauta.get(cod, 0.0) + inv + + # Importe y Resultados por conjunto, filtrando por el MES de arriba + imp = {}; res = {} + sin_filtro = (ano in ("TODOS", None) and mes in ("TODOS", None) and dia in ("TODOS", None)) + for f in _meta_filas(): + if not sin_filtro and not L._en_periodo(L._to_date(f.get("inicio")), ano, mes, dia): + continue + k = " ".join(str(f.get("conjunto")).split()).upper() + imp[k] = imp.get(k, 0.0) + float(f.get("importe") or 0.0) + res[k] = res.get(k, 0.0) + float(f.get("resultados") or 0.0) + + # Acumular por Sede -> Programa -> Pauta (solo pautas ALWAYS que esten en campanias) + from collections import defaultdict + grupos = defaultdict(lambda: defaultdict(lambda: {"importe": 0.0, "resultados": 0.0, + "nuevos": 0, "nuevos_asig": 0, + "matriculas": 0, "venta": 0.0, "pautas": []})) + for pauta, conjuntos in conj_pauta.items(): + p = str(pauta).strip() + if p in con_indice: # tiene num_indice -> NO es always + continue + info = camp.get(p) + if not info: # no esta en campanias -> no se cuenta + continue + se = str(info.get("sede") or "").upper() + gr = L.grupo_programa_lead(info.get("cargo")) # TEAC/TERC/SEMINARIOS + ti = sum(imp.get(" ".join(str(c).split()).upper(), 0.0) for c in conjuntos) + tr = sum(res.get(" ".join(str(c).split()).upper(), 0.0) for c in conjuntos) + if ti == 0 and tr == 0: + continue # sin gasto en el mes -> no aparece + # filtros de arriba + if sede not in ("TODOS", None) and se != str(sede).upper(): + continue + if programa not in ("TODOS", None) and gr != str(programa).upper(): + continue + nv = leads_nuevos.get(p, 0) + na = leads_nuevos_asig.get(p, 0) + mt = matriculas_pauta.get(p, 0) + vv = venta_pauta.get(p, 0.0) + g = grupos[se][gr] + g["importe"] += ti + g["resultados"] += tr + g["nuevos"] += nv + g["nuevos_asig"] += na + g["matriculas"] += mt + g["venta"] += vv + g["pautas"].append({"pauta": p, "importe": round(ti, 2), "resultados": int(tr), + "nuevos": nv, "nuevos_asig": na, "matriculas": mt, + "venta": round(vv, 0)}) + + # Armar filas: Sede -> subfilas por programa -> pautas (solo con gasto) + filas = [] + for se in sorted(grupos.keys()): + subs = [] + s_imp = s_res = 0.0; s_nv = s_na = s_mt = 0; s_vv = 0.0 + for gr in ("TEAC", "TERC", "SEMINARIOS"): + if gr in grupos[se]: + d = grupos[se][gr] + pautas = sorted(d["pautas"], key=lambda x: -x["importe"]) + subs.append({"programa": gr, "importe": round(d["importe"], 2), + "resultados": int(d["resultados"]), + "nuevos": d["nuevos"], "nuevos_asig": d["nuevos_asig"], + "matriculas": d["matriculas"], "venta": round(d["venta"], 0), + "pautas": pautas}) + s_imp += d["importe"]; s_res += d["resultados"] + s_nv += d["nuevos"]; s_na += d["nuevos_asig"]; s_mt += d["matriculas"]; s_vv += d["venta"] + filas.append({"sede": se, "importe": round(s_imp, 2), "resultados": int(s_res), + "nuevos": s_nv, "nuevos_asig": s_na, "matriculas": s_mt, + "venta": round(s_vv, 0), "subfilas": subs}) + tot_i = round(sum(f["importe"] for f in filas), 2) + tot_r = int(sum(f["resultados"] for f in filas)) + tot_nv = sum(f["nuevos"] for f in filas) + tot_na = sum(f["nuevos_asig"] for f in filas) + tot_mt = sum(f["matriculas"] for f in filas) + tot_vv = round(sum(f["venta"] for f in filas), 0) + return {"filas": filas, "total": {"importe": tot_i, "resultados": tot_r, + "nuevos": tot_nv, "nuevos_asig": tot_na, + "matriculas": tot_mt, "venta": tot_vv}} + + 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"): + """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).""" + def _load(): + rows = _cartera_rows_cache() + # 1) leads WEB_FORMULARIO cuya fila es el ORIGEN del telefono en TODA la cartera + # (es_origen=SI) y con fecha_creada en el periodo. Asi NO cuenta si el telefono + # ya vino antes por otro canal (igual criterio que las demas tablas). + vistos = {} # telefono -> {fecha, sede, prog, asesor} + for r in rows: + if str(r.get("canal") or "").strip().upper() != "WEB_FORMULARIO": + continue + if str(r.get("es_origen") or "").strip().upper() != "SI": + continue # solo el origen real del telefono + tel = str(r.get("telefono") or "").strip() + if not tel: + continue + f = L._to_date(r.get("fecha_creada")) + if not L._en_periodo(f, ano, mes, dia): + continue + # sede y programa YA vienen normalizados en cartera_junta. Reducir a 4 grupos. + se = " ".join(str(r.get("sede") or "").upper().split()) or "SIN SEDE" + pr = " ".join(str(r.get("programa") or "").upper().split()) + if pr == "TEAC": + gr = "TEAC" + elif pr == "TERC": + gr = "TERC" + elif pr in ("SEMINARIOS", "VRF", "CO2", "DIPLOMADO", "VENTILACIÓN", "VENTILACION"): + gr = "SEMINARIOS" + else: + gr = "OTROS" + vistos[tel] = {"fecha": f, "sede": se, "prog": gr, + "asesor": str(r.get("asesor") or "").strip()} + + # 2) matriculas por telefono (num_matricula, fecha, inv en soles) + from collections import defaultdict + mat_por_tel = defaultdict(list) + for m in _matriculas_crudas(): + t1 = str(m.get("dsc_telefono_1") or "").strip() + t2 = str(m.get("dsc_telefono_2") or "").strip() + ph = t1 if t1 else t2 + if not ph: + continue + fm = L._to_date(m.get("fch_matricula")) + try: + inv = float(m.get("INV_NETA", 0) or 0) + except (TypeError, ValueError): + inv = 0.0 + if str(m.get("cod_moneda", "")).strip().upper() == "DOL": + inv *= 3.34 + mat_por_tel[ph].append((str(m.get("num_matricula")), fm, inv)) + + # 3) agrupar Sede -> Programa + grupos = defaultdict(lambda: defaultdict(lambda: {"recibidos": 0, "procesados": 0, + "matriculas": 0, "venta": 0.0})) + for tel, info in vistos.items(): + se = info["sede"]; gr = info["prog"]; f_lead = info["fecha"] + if sede not in ("TODOS", None) and se != str(sede).upper(): + continue + if programa not in ("TODOS", None) and gr != str(programa).upper(): + continue + g = grupos[se][gr] + g["recibidos"] += 1 + if info["asesor"]: + g["procesados"] += 1 + for (nm, fm, inv) in mat_por_tel.get(tel, []): + if fm and f_lead and fm >= f_lead: + g["matriculas"] += 1 + g["venta"] += inv + + # 4) filas + filas = [] + for se in sorted(grupos.keys()): + subs = [] + s_rec = s_proc = s_mt = 0; s_vv = 0.0 + for gr in ("TEAC", "TERC", "SEMINARIOS", "OTROS"): + if gr in grupos[se]: + d = grupos[se][gr] + subs.append({"programa": gr, "recibidos": d["recibidos"], + "procesados": d["procesados"], "matriculas": d["matriculas"], + "venta": round(d["venta"], 0)}) + s_rec += d["recibidos"]; s_proc += d["procesados"] + s_mt += d["matriculas"]; s_vv += d["venta"] + filas.append({"sede": se, "recibidos": s_rec, "procesados": s_proc, + "matriculas": s_mt, "venta": round(s_vv, 0), "subfilas": subs}) + tot = {"recibidos": sum(f["recibidos"] for f in filas), + "procesados": sum(f["procesados"] for f in filas), + "matriculas": sum(f["matriculas"] for f in filas), + "venta": round(sum(f["venta"] for f in filas), 0)} + return {"filas": filas, "total": tot} + + return cache_get_or_set("matriz_webform", (ano, mes, dia, sede, programa), _load) + + +def _asignacion_respuesta_crudos(): + return cache_get_or_set("asig_resp", ("GLOBAL",), + lambda: get_dm().traer_asignacion_respuesta()) + + +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(): + return cache_get_or_set("act_asesores", ("GLOBAL",), + lambda: get_dm().traer_actividad_asesores()) + + +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).""" + def _load(): + import calendar + from collections import defaultdict + rows = _leads_asignados_crudos() + # dias del mes filtrado (columnas) + if mes not in ("TODOS", None) and ano not in ("TODOS", None): + ndias = calendar.monthrange(int(ano), int(mes))[1] + dias_cols = list(range(1, ndias + 1)) + else: + dias_cols = list(range(1, 32)) + + # asesores excluidos de esta matriz + EXCLUIR = {"SIN ASESOR", "COPITO RIVERA"} + # asesor -> dia -> [telefonos] + por = defaultdict(lambda: defaultdict(list)) + for r in rows: + f = L._to_date(r.get("created_at")) + if not L._en_periodo(f, ano, mes, dia): + continue + ase = str(r.get("user_name") or "Sin Asesor").strip() or "Sin Asesor" + if ase.upper() in EXCLUIR: + continue + tel = str(r.get("telefono") or "").strip() + por[ase][f.day].append(tel) + + filas = [] + tot_por_dia = defaultdict(int) + for ase in sorted(por.keys()): + pordia = {d: len(por[ase].get(d, [])) for d in dias_cols} + total = sum(pordia.values()) + # telefonos por dia (para expandir con +) + tels_dia = {d: sorted(por[ase].get(d, [])) for d in dias_cols if por[ase].get(d)} + for d, n in pordia.items(): + tot_por_dia[d] += n + filas.append({"asesor": ase, "por_dia": pordia, "total": total, "tels_dia": tels_dia}) + filas.sort(key=lambda x: -x["total"]) + total_gral = sum(f["total"] for f in filas) + return {"dias": dias_cols, "filas": filas, + "total_por_dia": {d: tot_por_dia.get(d, 0) for d in dias_cols}, + "total": total_gral} + return cache_get_or_set("matriz_asignados", (ano, mes, dia), _load) + + +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"): + """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) + ASIGNADAS = de esas, con asesor Y respondida por el contacto + MATRICULAS = matriculas de esos telefonos (fch_matricula >= envio mas antiguo). Sin filtro de mes. + VENTA = INV_NETA (DOL x3.34) de esas matriculas.""" + from datetime import timedelta, datetime + from collections import defaultdict + + def _load(): + rows = _plantillas_crudas() + UMBRAL = timedelta(hours=24) + + def _lleno(s): + return s is not None and str(s).strip() != "" + + enviadas = defaultdict(int) + respondidas = defaultdict(int) + asignadas = defaultdict(int) + prim_por_tel = {} # telefono -> (fecha_envio, plantilla) la cobrada mas antigua DEL PERIODO + + for r in rows: + plantilla = r.get("plantilla") + envio = r.get("created_at_peru") + f = L._to_date(envio) + # ENVIADAS/RESP/ASIG respetan el filtro de mes (por created_at_peru) + if not L._en_periodo(f, ano, mes, dia): + continue + anterior = r.get("hora_anterior_contacto") + sig = r.get("siguiente_mensaje_1") + es_si = False + if plantilla == "descuento_egresados_seminarios": + es_si = True + elif anterior is not None and (envio - anterior) > UMBRAL: + es_si = True + if not es_si: + continue + enviadas[plantilla] += 1 + if _lleno(sig): + respondidas[plantilla] += 1 + if _lleno(r.get("user_name")) and _lleno(sig): + asignadas[plantilla] += 1 + tel = str(r.get("telefono") or "").strip() + if tel and envio is not None: + if tel not in prim_por_tel or envio < prim_por_tel[tel][0]: + prim_por_tel[tel] = (envio, plantilla) + + # MATRICULAS + VENTA de los telefonos filtrados (sin filtro de mes en la matricula) + mat_por_tel = defaultdict(list) + for m in _matriculas_crudas(): + t1 = str(m.get("dsc_telefono_1") or "").strip() + t2 = str(m.get("dsc_telefono_2") or "").strip() + ph = t1 if t1 else t2 + if not ph: + continue + fm = m.get("fch_matricula") + if isinstance(fm, datetime): fm = fm.date() + try: + inv = float(m.get("INV_NETA", 0) or 0) + except (TypeError, ValueError): + inv = 0.0 + if str(m.get("cod_moneda", "")).strip().upper() == "DOL": + inv *= 3.34 + mat_por_tel[ph].append((str(m.get("num_matricula")), fm, inv)) + + matriculas = defaultdict(int) + venta = defaultdict(float) + _vistas = set() + for tel, (fenvio, plantilla) in prim_por_tel.items(): + fenvio_d = fenvio.date() if hasattr(fenvio, "date") else fenvio + for (nm, fm, inv) in mat_por_tel.get(tel, []): + if not fm or nm in _vistas: + continue + if fm >= fenvio_d: + _vistas.add(nm) + matriculas[plantilla] += 1 + venta[plantilla] += inv + + filas = [] + for p in sorted(enviadas.keys(), key=lambda x: -enviadas[x]): + filas.append({"plantilla": p, "enviadas": enviadas[p], + "respondidas": respondidas.get(p, 0), + "asignadas": asignadas.get(p, 0), + "matriculas": matriculas.get(p, 0), + "venta": round(venta.get(p, 0.0), 0)}) + tot = {"enviadas": sum(enviadas.values()), "respondidas": sum(respondidas.values()), + "asignadas": sum(asignadas.values()), "matriculas": sum(matriculas.values()), + "venta": round(sum(venta.values()), 0)} + return {"filas": filas, "total": tot} + + return cache_get_or_set("matriz_plantillas", (ano, mes, dia), _load) + + +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(): + """{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(): + from collections import defaultdict + out = defaultdict(list) + for m in _matriculas_crudas(): + ni = _norm_ni(m.get("num_indice")) + t1 = str(m.get("dsc_telefono_1") or "").strip() + t2 = str(m.get("dsc_telefono_2") or "").strip() + fm = L._to_date(m.get("fch_matricula")) + out[ni].append((t1 if t1 else t2, fm)) + return out + return cache_get_or_set("mat_por_indice", ("GLOBAL",), _load) + + +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(): + """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.""" + def _load(): + from collections import defaultdict + CANAL_PAUTA = {"COPITO", "PAUTA_WSP", "PAUTA_WSP_FACE"} + CANAL_WEB = {"WEB_FORMULARIO", "WEB_WHATSAPP", "WHATSAPP WEB"} + + def _clase(canal): + if canal in CANAL_PAUTA: return "PAUTA" + if canal in CANAL_WEB: return "WEB" + return "OTROS" + + # PASO 1: DEDUP por (sede, prog, telefono) -> quedarse con la fila mas antigua. + # Se guarda la fecha, si tiene asesor, y el CANAL de esa fila ganadora. + # (Asi el canal de PAUTA/OTROS se decide DESPUES de deduplicar, como pide el usuario.) + ganador = {} # (sede, prog, tel) -> {"f":fecha, "asesor":bool, "canal":canal} + tels = set() + tels_canal = {} # telefono -> canal de su fila ganadora GLOBAL (para matriculas) + gan_global = {} # telefono -> fecha ganadora global (para decidir canal de matriculas) + origen = {} + for r in _cartera_rows_cache(): + tel = str(r.get("telefono") or "").strip() + if not tel: + continue + tels.add(tel) + sede = " ".join(str(r.get("sede") or "").upper().split()) + prog = " ".join(str(r.get("programa") or "").upper().split()) + canal = " ".join(str(r.get("canal") or "").upper().split()) + f = L._to_date(r.get("fecha_creada")) + if str(r.get("es_origen") or "").upper() == "SI" and f is not None: + origen[tel] = f + if f is None: + continue + con_asesor = bool(str(r.get("asesor") or "").strip()) + # ganador por sede+prog+tel (para la tabla) + k = (sede, prog, tel) + g = ganador.get(k) + if g is None or f < g["f"]: + ganador[k] = {"f": f, "asesor": con_asesor, "canal": canal} + # ganador global por tel (para clasificar matriculas por canal) + if tel not in gan_global or f < gan_global[tel]: + gan_global[tel] = f + tels_canal[tel] = canal + + # PASO 2: armar indices con la fila ganadora ya deduplicada + idx_all = defaultdict(dict); idx_asig = defaultdict(dict) + # por clase (PAUTA/WEB/OTROS): {clase: {"all":..,"asig":..}} + CLASES = ("PAUTA", "WEB", "OTROS") + idx_cl = {cl: {"all": defaultdict(dict), "asig": defaultdict(dict)} for cl in CLASES} + tels_cl = {cl: set() for cl in CLASES} + for (sede, prog, tel), g in ganador.items(): + f = g["f"]; cl = _clase(g["canal"]) + idx_all[(sede, prog)][tel] = f + if g["asesor"]: idx_asig[(sede, prog)][tel] = f + idx_cl[cl]["all"][(sede, prog)][tel] = f + if g["asesor"]: idx_cl[cl]["asig"][(sede, prog)][tel] = f + # telefonos por clase (segun ganador GLOBAL) para matriculas + for tel, canal in tels_canal.items(): + tels_cl[_clase(canal)].add(tel) + + out = {"all": idx_all, "asig": idx_asig, "tels": tels, "origen": origen} + for cl in CLASES: + out[f"all_{cl.lower()}"] = idx_cl[cl]["all"] + out[f"asig_{cl.lower()}"] = idx_cl[cl]["asig"] + out[f"tels_{cl.lower()}"] = tels_cl[cl] + return out + 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"): + def _load(): + leads = _leads_crudos() + cursos = _cursos_crudos() + mats = _matriculas_crudas() + + # Filtro por PROGRAMA (4 grupos: TEAC / TERC / SEMINARIOS / OTROS). + # Recalcula toda la pantalla filtrando las listas base ANTES de calcular. + # - leads: grupo por 'cargo' del código de campaña + # - cursos y matrículas: grupo por dsc_programa + if programa not in ("TODOS", None): + g = str(programa).upper() + leads = [l for l in leads if L.grupo_programa_lead(l.get("cargo")) == g] + cursos = [c for c in cursos if L.grupo_programa_curso(c.get("dsc_programa")) == g] + mats = [m for m in mats if L.grupo_programa_curso(m.get("dsc_programa")) == g] + + # Filtro por SEDE (recalcula toda la pantalla). Se filtran las listas base + # ANTES de calcular, para no tocar las funciones de cálculo existentes. + # - leads: sede POR CÓDIGO de campaña (NO sede_act; coincide con conteo real) + # - cursos y matrículas: sede por clasificar_sede(dsc_programa) + if sede not in ("TODOS", None): + s = str(sede).upper() + sede_cod = L.sede_por_codigo_map() + leads = [l for l in leads + if sede_cod.get(str(l.get("codigo") or "").strip()) == s] + cursos = [c for c in cursos if L.clasificar_sede(c.get("dsc_programa")) == s] + mats = [m for m in mats if L.clasificar_sede(m.get("dsc_programa")) == s] + + kpis = L.kpis_leads(leads, cursos, mats, ano, mes, dia) + tabla = L.tabla_estado_objecion(leads, ano, mes, dia) + por_dia = L.matriculas_por_dia(mats, cursos, ano, mes, dia) + leads_dia = L.leads_por_dia(leads, ano, mes, dia) + pauta = L.tabla_pauta(leads, mats, ano, mes, dia, + _importe_por_pauta_periodo(ano, mes, dia), + _resultados_por_pauta_periodo(ano, mes, dia), + _campanias_sede_cargo()) + ci = _cartera_idx() + cartera_canal = { + "PAUTA": {"all": ci.get("all_pauta"), "asig": ci.get("asig_pauta"), "tels": ci.get("tels_pauta")}, + "WEB": {"all": ci.get("all_web"), "asig": ci.get("asig_web"), "tels": ci.get("tels_web")}, + "OTROS": {"all": ci.get("all_otros"), "asig": ci.get("asig_otros"), "tels": ci.get("tels_otros")}, + } + matriz = L.matriz_cursos(cursos, leads, ano, mes, dia, _pauta_cruda(), + _campanias_map(), ci.get("all"), ci.get("asig"), + _mat_por_indice(), ci.get("tels"), ci.get("origen"), + _importe_por_pauta(), + _importe_por_pauta_periodo(ano, mes, dia), + cartera_canal, _conjunto_pauta(), _pautas_de_indice()) + return {"kpis": kpis, "estado_objecion": tabla, "matriculas_por_dia": por_dia, + "leads_por_dia": leads_dia, "tabla_pauta": pauta, "matriz_cursos": matriz} + + return cache_get_or_set("leads_dash", (ano, mes, dia, programa, sede), _load) + + +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): + """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): + """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 "") + for x in ("+51", "+", " ", "-", "(", ")"): + s = s.replace(x, "") + s = s.strip() + if s.isdigit() and 51900000000 <= int(s) <= 51999999999: + s = s[2:] + return s + + +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). + noche (18-22) -> Turno 2 (09-13 + 18-22, sin refrigerio). + sabado: manana(09-13) o tarde(14-18), 4h, donde haya mas actividad. + sin actividad ese dia -> no trabajo (se salta). + - El tiempo cuenta SOLO minutos dentro del turno; si la asignacion cae fuera, + el reloj arranca al inicio del siguiente bloque laboral (saltando dias sin + actividad). Leads asignados en refrigerio (13-14) de turno1 se excluyen. + - Serie dia a dia por FECHA DE ASIGNACION: promedio de minutos de los leads + asignados ese dia (solo respondidos). Filtro ano/mes/dia por fecha asignacion. + - No respondidos -> se marcan aparte, NO entran en el promedio.""" + from datetime import datetime, timedelta, time as _time + def _load(): + from collections import defaultdict + ase = _asesores(); amap = ase["map"]; lista = ase["lista"] + + # ── 1) Detectar turno por (asesor_norm, fecha) segun actividad ── + # Mapa user_id -> nombre normalizado (de asignacion_respuesta). + uid2vend = {} + for r in _asignacion_respuesta_crudos(): + uid = r.get("user_id") + v = _norm_asesor(r.get("user_name"), amap) + if uid is not None and v: + uid2vend[uid] = v + + # Contar actividad por (vend, fecha, franja): tarde=13-18, noche=18-22, + # sab_manana=9-13, sab_tarde=14-18. + act = defaultdict(lambda: defaultdict(int)) # (vend,fecha) -> franja -> conteo + for a in _actividad_asesores_crudos(): + uid = a.get("user_id"); ts = a.get("created_at") + if uid not in uid2vend or not isinstance(ts, datetime): + continue + v = uid2vend[uid]; d = ts.date(); h = ts.hour + ts.minute / 60.0 + key = (v, d) + if d.weekday() == 6: # domingo: no trabaja + continue + if d.weekday() == 5: # sabado + if 9 <= h < 13: act[key]["sab_manana"] += 1 + elif 14 <= h < 18: act[key]["sab_tarde"] += 1 + else: # lun-vie + if 13 <= h < 18: act[key]["tarde"] += 1 + elif 18 <= h < 22: act[key]["noche"] += 1 + + def _turno_dia(v, d): + """Devuelve lista de bloques [(h_ini,h_fin), ...] del turno de ese dia, + o None si no trabajo. Refrigerio de turno1 (13-14) NO se incluye.""" + if d.weekday() == 6: + return None + fr = act.get((v, d)) + if not fr: + return None # sin actividad -> no trabajo + if d.weekday() == 5: # sabado (4h) + m = fr.get("sab_manana", 0); t = fr.get("sab_tarde", 0) + if m == 0 and t == 0: + return None + return [(9, 13)] if m >= t else [(14, 18)] + # lun-vie: comparar tarde vs noche + t = fr.get("tarde", 0); n = fr.get("noche", 0) + if t == 0 and n == 0: + return None + if n > t: + return [(9, 13), (18, 22)] # Turno 2 + return [(9, 13), (14, 18)] # Turno 1 (refrigerio 13-14 fuera) + + def _dt(d, hh): + return datetime(d.year, d.month, d.day) + timedelta(hours=hh) + + def _en_refrigerio_t1(v, ts): + """True si ts cae 13-15 y ese dia el asesor es turno 1 (lun-vie). + Estos leads NO se excluyen: cuentan como respondidos, pero su tiempo + (diferencia directa T2-T1) se reduce 45 min si supera 45.""" + d = ts.date() + if d.weekday() >= 5: + return False + fr = act.get((v, d)) + if not fr: + return False + t = fr.get("tarde", 0); n = fr.get("noche", 0) + es_t1 = not (n > t) and (t > 0 or n > 0) + h = ts.hour + ts.minute / 60.0 + return es_t1 and (13 <= h < 15) + + def _minutos_laborales(v, t1, t2): + """Minutos de turno entre t1 (asignacion) y t2 (respuesta), saltando + huecos y dias sin actividad. Devuelve None si no se puede (raro).""" + if not isinstance(t1, datetime) or not isinstance(t2, datetime) or t2 <= t1: + return 0 + total = 0.0 + cur = t1 + saltos = 0 + while cur < t2 and saltos < 60: # tope 60 dias por seguridad + d = cur.date() + bloques = _turno_dia(v, d) + if not bloques: + # dia sin trabajo -> saltar al dia siguiente 00:00 + cur = _dt(d, 24); saltos += 1; continue + avanzo = False + for (hi, hf) in bloques: + ini = _dt(d, hi); fin = _dt(d, hf) + if t2 <= ini: # la respuesta es antes de este bloque + break + if cur >= fin: # ya paso este bloque + continue + a = max(cur, ini) + b = min(t2, fin) + if b > a: + total += (b - a).total_seconds() / 60.0 + cur = fin # avanzar al fin del bloque + avanzo = True + if cur >= t2: + break + # pasar al dia siguiente + if cur.date() == d: + cur = _dt(d, 24); saltos += 1 + return round(total) + + # ── 2) Por lead: minutos laborales, agrupado por FECHA DE ASIGNACION ── + por_dia_sum = defaultdict(float) # (vend,dia) -> suma minutos + por_dia_cnt = defaultdict(int) # (vend,dia) -> nº respondidos + no_resp = defaultdict(int) # (vend,dia) -> nº sin responder + for r in _asignacion_respuesta_crudos(): + v = _norm_asesor(r.get("user_name"), amap) + if not v: + continue + t1 = r.get("created_at") + if not isinstance(t1, datetime): + continue + if not L._en_periodo(t1.date(), ano, mes, dia): + continue # filtro por fecha de asignacion + t2 = r.get("respuesta_fecha") + if not isinstance(t2, datetime): + no_resp[(v, t1.day)] += 1 # no respondio + continue + if _en_refrigerio_t1(v, t1): + # asignado 13-15 en turno1: tiempo = diferencia DIRECTA (T2-T1), + # y si supera 45 min se le restan 45 (nunca negativo). + dif = (t2 - t1).total_seconds() / 60.0 + mins = round(dif - 45) if dif > 45 else round(dif) + if mins < 0: + mins = 0 + else: + mins = _minutos_laborales(v, t1, t2) + por_dia_sum[(v, t1.day)] += mins + por_dia_cnt[(v, t1.day)] += 1 + + # ── 2b) Igual pero agrupado por FECHA DE RESPUESTA (dia en que respondio). + # Solo leads respondidos. Filtro por fecha de respuesta (t2). + # Si el tiempo laboral supera 960 min (2 dias laborales) -> NO entra en + # el promedio; se cuenta aparte como "respuesta tardia". + LIMITE_TARDIA = 960 # 2 dias laborales (8h/dia) + pr_sum = defaultdict(float) # (vend,dia_respuesta) -> suma minutos (validos) + pr_cnt = defaultdict(int) # (vend,dia_respuesta) -> nº validos (<=960) + pr_tardia = defaultdict(int) # (vend,dia_respuesta) -> nº tardios (>960) + for r in _asignacion_respuesta_crudos(): + v = _norm_asesor(r.get("user_name"), amap) + if not v: + continue + t1 = r.get("created_at"); t2 = r.get("respuesta_fecha") + if not isinstance(t1, datetime) or not isinstance(t2, datetime): + continue # sin respuesta -> no entra en este grafico + if not L._en_periodo(t2.date(), ano, mes, dia): + continue # filtro por FECHA DE RESPUESTA + if _en_refrigerio_t1(v, t1): + dif = (t2 - t1).total_seconds() / 60.0 + mins = round(dif - 45) if dif > 45 else round(dif) + if mins < 0: + mins = 0 + else: + mins = _minutos_laborales(v, t1, t2) + if mins > LIMITE_TARDIA: + pr_tardia[(v, t2.day)] += 1 # respuesta tardia -> fuera del promedio + continue + pr_sum[(v, t2.day)] += mins + pr_cnt[(v, t2.day)] += 1 + + # ── 3) Serie dia a dia (promedio min) por vendedor y total ── + def _serie(vends): + out = [] + for dnum in range(1, 32): + s = sum(por_dia_sum.get((v, dnum), 0) for v in vends) + c = sum(por_dia_cnt.get((v, dnum), 0) for v in vends) + nr = sum(no_resp.get((v, dnum), 0) for v in vends) + out.append({"dia": dnum, + "promedio_min": round(s / c) if c else 0, + "respondidos": c, "no_respondidos": nr}) + return out + + def _serie_resp(vends): + out = [] + for dnum in range(1, 32): + s = sum(pr_sum.get((v, dnum), 0) for v in vends) + c = sum(pr_cnt.get((v, dnum), 0) for v in vends) + td = sum(pr_tardia.get((v, dnum), 0) for v in vends) + out.append({"dia": dnum, + "promedio_min": round(s / c) if c else 0, + "respondidos": c, "tardios": td}) + return out + + serie_por_vend = {v: _serie([v]) for v in lista} + serie_total = _serie(lista) + serie_resp_por_vend = {v: _serie_resp([v]) for v in lista} + serie_resp_total = _serie_resp(lista) + return {"serie_por_vend": serie_por_vend, "serie_total": serie_total, + "serie_resp_por_vend": serie_resp_por_vend, + "serie_resp_total": serie_resp_total, + "vendedores": lista} + return cache_get_or_set("tiempo_resp", (ano, mes, dia), _load) + + +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, + fecha_creada en el periodo); la matricula cuenta en CUALQUIER fecha. + - Leads Procesados/Contactados: filtran por fecha del lead (mensaje campaña).""" + def _load(): + from collections import defaultdict + ase = _asesores() + amap = ase["map"]; lista = ase["lista"] + CANAL_COPITO = {"COPITO"} + + # 1) CARTERA por vendedor. Fila ORIGEN de cada telefono (es_origen=SI) cuya + # fecha_creada cae en el periodo filtrado. Cada telefono cuenta para UN solo asesor. + # Ademas guardamos {vendedor: set(telefonos del periodo)} para cruzar matriculas. + cart_total = defaultdict(int); cart_copito = defaultdict(int); cart_otros = defaultdict(int) + tels_por_vend = defaultdict(set) # vendedor -> telefonos (es_origen=SI del periodo) + cop_tels = defaultdict(set) # vendedor -> telefonos ya contados como Copito (sin repetir) + for r in _cartera_rows_cache(): + if str(r.get("es_origen") or "").strip().upper() != "SI": + continue # solo la fila origen de cada telefono + f = L._to_date(r.get("fecha_creada")) + if not L._en_periodo(f, ano, mes, dia): + continue # filtro por fecha_creada de la cartera + v = _norm_asesor(r.get("asesor"), amap) + if not v: # el asesor del origen no es uno de los 8 + continue + tel = str(r.get("telefono") or "").strip() + canal = " ".join(str(r.get("canal") or "").upper().split()) + ob = " ".join(str(r.get("origen_base") or "").upper().split()) + cart_total[v] += 1 + # Copito = union (origen_base=POSTGRE) OR (canal=COPITO), sin repetir telefono + es_copito = (ob == "POSTGRE") or (canal in CANAL_COPITO) + if es_copito: + if tel and tel in cop_tels[v]: + pass # ese telefono ya se conto como Copito -> no repetir + else: + cart_copito[v] += 1 + if tel: + cop_tels[v].add(tel) + else: + cart_otros[v] += 1 + if tel: + tels_por_vend[v].add(tel) + + # 2) MATRICULAS por vendedor. Se CRUZAN por TELEFONO con los leads del periodo + # (tels_por_vend). La matricula cuenta en CUALQUIER fecha (no se filtra por + # fch_matricula). Telefono de matricula: dsc_telefono_1, o _2 si el 1 esta vacio. + # Un telefono puede tener varias matriculas -> se cuentan todas (no unico). + # MATRICULAS: por dsc_vendedor + fch_matricula en el periodo (NO se cruza + # con la cartera por telefono). Filtro Año/Mes/Día sobre fch_matricula. + mat_total = defaultdict(int); mat_curso = defaultdict(int); mat_retir = defaultdict(int) + for m in _matriculas_crudas(): + vend = _norm_asesor(m.get("dsc_vendedor"), amap) + if not vend: + continue + estado = str(m.get("estado_matricula") or "").strip().upper() + if estado == "ANU": + continue # anuladas no cuentan + fm = L._to_date(m.get("fch_matricula")) + if not L._en_periodo(fm, ano, mes, dia): + continue # filtro por fch_matricula + mat_total[vend] += 1 + if estado in ("ALU", "PRE"): + mat_curso[vend] += 1 + elif estado == "RET": + mat_retir[vend] += 1 + + # 3) LEADS Procesados/Contactados por vendedor (leads de Chatwoot). Filtro por + # fecha del lead (mensaje de campaña). Procesados = lead unico con asesor; + # Contactados = de esos, ultima_etiqueta != "NO TRABAJADO". + lead_proc = defaultdict(int); lead_cont = defaultdict(int) + etiq = defaultdict(int) # clave: (vendedor, ultima_etiqueta) -> nº leads procesados + for l in _leads_crudos(): + if not l.get("es_unico"): + continue + if not L._en_periodo(l.get("fecha"), ano, mes, dia): + continue # filtro por fecha del lead + v = _norm_asesor(l.get("asesor"), amap) + if not v: + continue + lead_proc[v] += 1 # tiene asesor de los 8 => procesado + ult = l.get("ultima") or "NO TRABAJADO" + etiq[(v, ult)] += 1 # para la matriz Estado/Objeción + # NOTA: "Contactados" ya NO se cuenta aqui; ahora sale de los ASIGNADOS + # (bloque 3b), usando su ultima etiqueta. + + # 3b) LEADS ASIGNADOS por vendedor (mensajes "Asignado a..." de Chatwoot). + # Se cuenta por telefono (1 asignacion por telefono, la mas reciente). + # Filtro por fecha de asignacion (created_at). Asesor via user_name normalizado. + # CONTACTADOS = de los asignados, cuya ULTIMA ETIQUETA != "NO TRABAJADO". + lead_asig = defaultdict(int) + etiq_asig = defaultdict(int) # (vendedor, ultima_etiqueta) -> nº asignados (para matriz) + for a in _leads_asignados_crudos(): + f = L._to_date(a.get("created_at")) + if not L._en_periodo(f, ano, mes, dia): + continue # filtro por fecha de asignacion + v = _norm_asesor(a.get("user_name"), amap) + if not v: + continue # el asesor asignado no es uno de los 8 + lead_asig[v] += 1 + ult = L.ultima_etiqueta(a.get("etiquetas")) or "NO TRABAJADO" + etiq_asig[(v, ult)] += 1 # para la matriz Estado/Objeción (asignados) + if ult != "NO TRABAJADO": + lead_cont[v] += 1 # contactado = asignado con ultima etiqueta trabajada + + # SERIE DIARIA: Leads Asignados (por fecha de asignacion) y Matriculas (por fch_matricula), + # por vendedor y por dia del mes. clave: (vendedor, dia) -> conteo. + serie_proc = defaultdict(int) # (vendedor, dia) -> leads asignados + serie_mat = defaultdict(int) # (vendedor, dia) -> matriculas + for a in _leads_asignados_crudos(): + f = L._to_date(a.get("created_at")) + if not L._en_periodo(f, ano, mes, dia): + continue + v = _norm_asesor(a.get("user_name"), amap) + if v and f: + serie_proc[(v, f.day)] += 1 + for m in _matriculas_crudas(): + vend = _norm_asesor(m.get("dsc_vendedor"), amap) + if not vend: + continue + estado = str(m.get("estado_matricula") or "").strip().upper() + if estado == "ANU": + continue + fm = L._to_date(m.get("fch_matricula")) + if fm and L._en_periodo(fm, ano, mes, dia): + serie_mat[(vend, fm.day)] += 1 + + def _serie(vendedores): + """Lista 1..31 con asignados y matriculas del conjunto de vendedores.""" + out = [] + for d in range(1, 32): + p = sum(serie_proc.get((v, d), 0) for v in vendedores) + mm = sum(serie_mat.get((v, d), 0) for v in vendedores) + out.append({"dia": d, "asignados": p, "matriculas": mm}) + return out + + serie_por_vend = {v: _serie([v]) for v in lista} + serie_total = _serie(lista) + + # Matriz Estado/Objeción (Ultima_Etiqueta) por vendedor y total. + # Ahora se basa en los LEADS ASIGNADOS (etiq_asig), no en los de campaña. + etiquetas_all = sorted({e for (_, e) in etiq_asig.keys()}, key=lambda x: x.lower()) + + def _matriz_etiq(vendedores): + out = [] + for e in etiquetas_all: + c = sum(etiq_asig.get((v, e), 0) for v in vendedores) + if c > 0: + out.append({"estado": e, "cantidad": c}) + out.sort(key=lambda x: -x["cantidad"]) # mayor a menor por cantidad + return out + + matriz_etiq_por_vend = {v: _matriz_etiq([v]) for v in lista} + matriz_etiq_total = _matriz_etiq(lista) + + # 4) MATRIZ por TIPO DE PROGRAMA (TEAC/TERC/SEMINARIOS/OTROS). + # Filtro por fch_matricula (periodo) y dsc_vendedor. Total y Retiradas (RET). + # Se guarda por (vendedor, grupo) para poder filtrar por vendedor en el front. + GRUPOS_PROG = ["TEAC", "TERC", "SEMINARIOS", "OTROS"] + prog_tot = defaultdict(int); prog_ret = defaultdict(int) # clave: (vendedor, grupo) + for m in _matriculas_crudas(): + vend = _norm_asesor(m.get("dsc_vendedor"), amap) + if not vend: + continue + estado = str(m.get("estado_matricula") or "").strip().upper() + if estado == "ANU": + continue + fm = L._to_date(m.get("fch_matricula")) + if not L._en_periodo(fm, ano, mes, dia): + continue # filtro por fch_matricula + grupo = L.grupo_programa_curso(m.get("dsc_programa")) + prog_tot[(vend, grupo)] += 1 + if estado == "RET": + prog_ret[(vend, grupo)] += 1 + + def _matriz_prog(vendedores): + """Suma total/retiradas por grupo para el conjunto de vendedores dado.""" + out = [] + for g in GRUPOS_PROG: + t = sum(prog_tot.get((v, g), 0) for v in vendedores) + r = sum(prog_ret.get((v, g), 0) for v in vendedores) + out.append({"programa": g, "total": t, "retiradas": r, + "pct_retiradas": round(r / t * 100, 1) if t else 0}) + return out + + # matriz por programa para CADA vendedor y para el TOTAL (todos) + matriz_prog_por_vend = {v: _matriz_prog([v]) for v in lista} + matriz_prog_total = _matriz_prog(lista) + + filas = [] + for v in lista: + proc = lead_proc.get(v, 0); cont = lead_cont.get(v, 0) + asig = lead_asig.get(v, 0) + filas.append({ + "vendedor": v, + "cartera_total": cart_total.get(v, 0), + "cartera_copito": cart_copito.get(v, 0), + "cartera_otros": cart_otros.get(v, 0), + "mat_total": mat_total.get(v, 0), + "mat_curso": mat_curso.get(v, 0), + "mat_retiradas": mat_retir.get(v, 0), + "pct_retiradas": round(mat_retir.get(v, 0) / mat_total.get(v, 0) * 100, 1) + if mat_total.get(v, 0) else 0, + "leads_procesados": proc, + "leads_contactados": cont, + # % Contactados = contactados / ASIGNADOS (ambos de la misma fuente) + "pct_contactados": round(cont / asig * 100, 1) if asig else 0, + "leads_asignados": asig, + }) + tot = {"cartera_total": sum(f["cartera_total"] for f in filas), + "cartera_copito": sum(f["cartera_copito"] for f in filas), + "cartera_otros": sum(f["cartera_otros"] for f in filas), + "mat_total": sum(f["mat_total"] for f in filas), + "mat_curso": sum(f["mat_curso"] for f in filas), + "mat_retiradas": sum(f["mat_retiradas"] for f in filas), + "leads_procesados": sum(f["leads_procesados"] for f in filas), + "leads_contactados": sum(f["leads_contactados"] for f in filas), + "leads_asignados": sum(f["leads_asignados"] for f in filas)} + tot["pct_retiradas"] = (round(tot["mat_retiradas"] / tot["mat_total"] * 100, 1) + if tot["mat_total"] else 0) + tot["pct_contactados"] = (round(tot["leads_contactados"] / tot["leads_asignados"] * 100, 1) + if tot["leads_asignados"] else 0) + # Tiempo de respuesta laboral (min) dia a dia por asesor (por fecha asignacion) + tr = tiempo_respuesta_dashboard(ano, mes, dia) + return {"filas": filas, "total": tot, "vendedores": lista, + "matriz_prog": matriz_prog_por_vend, "matriz_prog_total": matriz_prog_total, + "matriz_etiq": matriz_etiq_por_vend, "matriz_etiq_total": matriz_etiq_total, + "serie_dia": serie_por_vend, "serie_dia_total": serie_total, + "tiempo_resp": tr.get("serie_por_vend", {}), + "tiempo_resp_total": tr.get("serie_total", []), + "tiempo_resp_r": tr.get("serie_resp_por_vend", {}), + "tiempo_resp_r_total": tr.get("serie_resp_total", [])} + return cache_get_or_set("vendedores_dash", (ano, mes, dia), _load) + + +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()) + + +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). + Devuelve [{conjunto, importe, resultados, sede, programa}].""" + def _load(): + SEDES = {"LIMA", "PIURA", "AREQUIPA", "TRUJILLO"} + conj_pauta = _conjunto_pauta() + camp = _campanias_sede_cargo() + manual = _conjunto_sede_prog() + # conjuntos que SÍ tienen sede via pauta -> campania + con_sede = set() + for pauta, conjuntos in conj_pauta.items(): + sp = camp.get(str(pauta).strip()) + sede = " ".join(str((sp or {}).get("sede") or "").upper().split()) + if sp and sede in SEDES: + for co in conjuntos: + con_sede.add(" ".join(str(co).split()).upper()) + agg = {} + for f in _meta_filas(): + co = " ".join(str(f.get("conjunto")).split()) + if not co: + continue + k = co.upper() + if k in con_sede: + continue # ya tiene sede por campania -> no aparece aqui + d = agg.setdefault(k, {"conjunto": co, "importe": 0.0, "resultados": 0, + "sede": manual.get(k, {}).get("sede", ""), + "programa": manual.get(k, {}).get("programa", "")}) + d["importe"] += float(f.get("importe") or 0.0) + d["resultados"] += int(float(f.get("resultados") or 0.0)) + out = sorted(agg.values(), key=lambda x: -x["importe"]) + for d in out: + d["importe"] = round(d["importe"], 2) + return out + return cache_get_or_set("conjuntos_sin_sede", ("GLOBAL",), _load) + + +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"): + cache_invalidate(prefix=pref) + return r + + +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"): + cache_invalidate(prefix=pref) + return r + + +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 + pauta/sede se omiten por ahora.""" + def _load(): + SEDES = ["LIMA", "PIURA", "AREQUIPA", "TRUJILLO", "OTROS"] # OTROS = 5ta fila (manual) + CAMP_SEDES = {"LIMA", "PIURA", "AREQUIPA", "TRUJILLO"} # sedes validas por campania + conj_pauta = _conjunto_pauta() # {pauta: [conjunto,...]} + camp = _campanias_sede_cargo() # {pauta: {sede, cargo}} + + PROGS_OK = {"TEAC", "TERC", "SEMINARIOS", "OTROS"} + + # conjunto (normalizado) -> (sede, programa) via su pauta -> campania + conj_sede = {} # conjunto -> sede + conj_prog = {} # conjunto -> programa (grupo) + for pauta, conjuntos in conj_pauta.items(): + sp = camp.get(str(pauta).strip()) + if not sp: + continue + sede = " ".join(str(sp.get("sede") or "").upper().split()) + if sede not in CAMP_SEDES: + continue + prog = L.grupo_programa_lead(sp.get("cargo")) # TEAC/TERC/SEMINARIOS + for co in conjuntos: + k = " ".join(str(co).split()).upper() + conj_sede[k] = sede + conj_prog[k] = prog + + # + asignaciones MANUALES (sede + programa). Prioridad sobre la campania. + for k, v in _conjunto_sede_prog().items(): + sede = v.get("sede") + if sede in SEDES: + conj_sede[k] = sede + prog = str(v.get("programa") or "").strip().upper() + if prog: # acepta CUALQUIER programa asignado manualmente (no solo los fijos) + conj_prog[k] = prog + + # sumar importe y resultados por sede y por (sede, programa), filtrando por Año/Mes + imp = {s: 0.0 for s in SEDES} + res = {s: 0 for s in SEDES} + imp_pg = {} # (sede, prog) -> importe + res_pg = {} # (sede, prog) -> resultados + # conjuntos por (sede, prog): nombre_conjunto -> {importe, resultados} + conj_det = {} # (sede, prog) -> { conjunto_original: {importe, resultados} } + sin_filtro = (ano in ("TODOS", None) and mes in ("TODOS", None) and dia in ("TODOS", None)) + for f in _meta_filas(): + if not sin_filtro and not L._en_periodo(L._to_date(f.get("inicio")), ano, mes, dia): + continue + co = " ".join(str(f.get("conjunto")).split()) + k = co.upper() + sede = conj_sede.get(k) + if not sede: + continue # conjunto sin pauta/sede -> se omite + i = float(f.get("importe") or 0.0) + rr = int(float(f.get("resultados") or 0.0)) + imp[sede] += i + res[sede] += rr + prog = conj_prog.get(k, "SIN PROGRAMA") + imp_pg[(sede, prog)] = imp_pg.get((sede, prog), 0.0) + i + res_pg[(sede, prog)] = res_pg.get((sede, prog), 0) + rr + dd = conj_det.setdefault((sede, prog), {}) + d = dd.setdefault(co, {"conjunto": co, "importe": 0.0, "resultados": 0}) + d["importe"] += i + d["resultados"] += rr + + ORDEN_PROG = ["TEAC", "TERC", "SEMINARIOS", "OTROS", "SIN PROGRAMA"] + + def _conjuntos(sede, prog): + """Lista de conjuntos de anuncio de un (sede, programa), ordenada por importe.""" + det = list(conj_det.get((sede, prog), {}).values()) + det.sort(key=lambda x: -x["importe"]) + for d in det: + d["importe"] = round(d["importe"], 2) + return det + + def _sub(sede): + """Desglose por programa de una sede (solo los que tienen algo). + Primero los fijos en su orden, luego CUALQUIER otro programa asignado + (ej. BRANDING), y al final SIN PROGRAMA.""" + progs_de_sede = [p for (s, p) in imp_pg.keys() if s == sede] + extras = sorted(set(progs_de_sede) - set(ORDEN_PROG)) + orden = [p for p in ORDEN_PROG if p != "SIN PROGRAMA"] + extras + ["SIN PROGRAMA"] + out = [] + for p in orden: + i = imp_pg.get((sede, p), 0.0); r = res_pg.get((sede, p), 0) + if i or r: + out.append({"programa": p, "importe": round(i, 2), "resultados": r, + "conjuntos": _conjuntos(sede, p)}) + return out + + # Las 4 sedes reales siempre se muestran. OTROS solo si tiene algo (>0). + filas = [] + for s in SEDES: + if s == "OTROS" and imp[s] == 0 and res[s] == 0: + continue + filas.append({"sede": s, "importe": round(imp[s], 2), "resultados": res[s], + "programas": _sub(s)}) + tot = {"importe": round(sum(imp.values()), 2), "resultados": sum(res.values())} + return {"filas": filas, "total": tot} + return cache_get_or_set("roas_dash", (ano, mes, dia), _load) + + +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(): + return { + "matriz_always": matriz_always(ano, mes, dia, "TODOS", "TODOS"), + "matriz_webform": matriz_web_formulario(ano, mes, dia, "TODOS", "TODOS"), + "matriz_asignados": matriz_asignados(ano, mes, dia), + "matriz_plantillas": matriz_plantillas(ano, mes, dia), + } + return cache_get_or_set("otros_general", (ano, mes, dia), _load) + + +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 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() + ocultos_ni = {ni for ni, info in pm.items() if str(info.get("contar") or "").upper() == "NO"} + prog_de_ni = {} + fch_de_ni = {} + for c in _cursos_crudos(): + ni = _norm_ni(c.get("num_indice")) + prog_de_ni[ni] = str(c.get("dsc_det_programa") or c.get("dsc_programa") or "").strip() + f = L._to_date(c.get("fch_inicio")) + fch_de_ni[ni] = f.strftime("%d/%m/%Y") if f else "" + filas = [] + for ni in ocultos_ni: + filas.append({"num_indice": ni, + "dsc_det_programa": prog_de_ni.get(ni, "(sin nombre)"), + "fch_inicio": fch_de_ni.get(ni, ""), + "pauta": (pm.get(ni) or {}).get("pauta") or ""}) + filas.sort(key=lambda x: x["dsc_det_programa"]) + return {"filas": filas} + + +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() + for ni in num_indices: + dm.guardar_pauta_basebi(ni, "", "SI") # pauta vacia -> no toca pauta, solo contar + for pref in ("pauta_raw", "leads_dash", "otros_general", "leads_filtros"): + cache_invalidate(pref) + return {"encendidos": len(num_indices)} + + +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 + corte = date(2026, 1, 1) + vistos = {} + for c in _cursos_crudos(): + f = L._to_date(c.get("fch_inicio")) + if not f or f < corte: + continue + ni = _norm_ni(c.get("num_indice")) + nombre = str(c.get("dsc_det_programa") or c.get("dsc_programa") or "").strip() + if not nombre: + continue + finicio = f.strftime("%d/%m/%Y") + label = f"{nombre} · {finicio}" + vistos[ni] = {"num_indice": ni, "dsc_det_programa": nombre, + "fch_inicio": finicio, "label": label} + filas = sorted(vistos.values(), key=lambda x: x["label"]) + return {"programas": filas} + + +# ── LEYENDA (campanias de Supabase): listar / agregar / borrar ── +def campanias_listar(): + return get_dm().listar_campanias() + + +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): + 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): + res = get_dm().borrar_campania(cid) + _invalidar_por_campanias() + return res + + +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.""" + def _load(): + conj_pauta = _conjunto_pauta() # cacheado + pauta_de_conj = {} + for pa, cjs in conj_pauta.items(): + for c in cjs: + pauta_de_conj[" ".join(str(c).split())] = str(pa).strip() + # pauta -> [num_indices] usando TODAS las vinculaciones (basebi + programa_pautas) + ni_de_pauta = {} + for ni, pautas in _pautas_de_indice().items(): + for p in pautas: + ni_de_pauta.setdefault(str(p).strip(), []).append(str(ni).strip()) + prog_de_ni = {} + for c in _cursos_crudos(): # cacheado + prog_de_ni[_norm_ni(c.get("num_indice"))] = str(c.get("dsc_det_programa") or c.get("dsc_programa") or "").strip() + def _programa(pauta): + if not pauta: + return "" + nis = ni_de_pauta.get(str(pauta).strip(), []) + nombres = [prog_de_ni.get(n, "") for n in nis if prog_de_ni.get(n)] + return " | ".join(sorted(set(nombres))) + filas = [] + vistos = set() + for f in _meta_filas(): # cacheado (CSV de Meta) + co = " ".join(str(f.get("conjunto")).split()) + if not co or co in vistos: + continue + vistos.add(co) + pa = pauta_de_conj.get(co, "") + filas.append({"conjunto": co, "pauta": pa, "programa": _programa(pa)}) + filas.sort(key=lambda x: x["conjunto"]) + return {"filas": filas} + return cache_get_or_set("leyenda_anuncios", ("GLOBAL",), _load) + + +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. + Invalida solo cachés dependientes de pauta/conjunto (no la cartera).""" + from cache_manager import cache_invalidate + dm = get_dm() + for c in cambios: + conjunto = c.get("conjunto") + pauta = (c.get("pauta") or "").strip() + ni = c.get("num_indice") + if conjunto: + dm.upsert_conjunto_pauta(conjunto, pauta) + # si eligió un programa (num_indice), AGREGAR el vínculo num_indice<->pauta + # en programa_pautas (muchos-a-muchos, se SUMA, no reemplaza). + if ni and pauta: + dm.agregar_programa_pauta(str(ni).strip(), pauta) + for pref in ("conjunto_pauta", "pauta_raw", "programa_pautas", "pautas_de_indice", + "meta_filas", "importe_pauta", "importe_pauta_periodo", + "resultados_pauta_periodo", "leads_dash", "otros_general", + "matriz_always", "matriz_plantillas", "leyenda_anuncios"): + cache_invalidate(pref) + return {"guardados": len(cambios)} + + +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() + nis = dm.num_indices_de_pauta(pauta) + excl = str(excluir_num_indice or "").strip() + nis = [n for n in nis if n != excl] + # dsc_det_programa de esos num_indice (desde cursos de SQL, ya cacheados) + cursos = _cursos_crudos() + detalle = [] + for c in cursos: + ni = _norm_ni(c.get("num_indice")) + if ni in nis: + detalle.append({"num_indice": ni, + "dsc_det_programa": str(c.get("dsc_det_programa") or c.get("dsc_programa") or "").strip()}) + conjuntos = _conjunto_pauta().get(str(pauta).strip(), []) + return {"en_uso": bool(nis) or bool(conjuntos), + "cursos": detalle, "num_indices": nis, "conjuntos": conjuntos} + + +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).""" + from cache_manager import cache_invalidate + dm = get_dm() + res = dm.guardar_pauta_basebi(num_indice, pauta, contar) + if conjunto: + dm.guardar_conjunto_pauta(conjunto, pauta) + # invalidar solo lo afectado por pauta/conjunto (la cartera NO se re-baja) + for pref in ("pauta_raw", "pautas_de_indice", "conjunto_pauta", "meta_filas", + "importe_pauta", "importe_pauta_periodo", "resultados_pauta_periodo", + "leads_dash", "otros_general", "matriz_always", "matriz_plantillas", + "leads_filtros", "roas_dash"): + cache_invalidate(pref) + return res + + +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) + if otros == "OTROS": + return "OTROS" + up = str(dscp).upper() + cat = "TEAC" if ("AIRE ACONDICIONADO" in up or "TEAC" in up) else ( + "TERC" if ("REFRIGERACION COMERCIAL" in up or "REFRIGERACIÓN COMERCIAL" in up or "TERC" in up) else "SEM") + sede = L.clasificar_sede(dscp) + return L.tipo_programa_cohorte(sede, cat) + + +# ── 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 + if L._to_date(c.get("fch_inicio"))}, reverse=True) + # Grupos de programa fijos para el filtro PROGRAMA + tipos = ["TEAC", "TERC", "SEMINARIOS", "OTROS"] + return {"anos": anos or [2026], "tipos": tipos} + return cache_get_or_set("leads_filtros", ("GLOBAL",), _load) + + +# ── 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): + cursos = _cursos_crudos() + periodo = [c for c in cursos if L._en_periodo(L._to_date(c.get("fch_inicio")), ano, mes, "TODOS")] + detalle = [] + tot_insc = 0 + tot_meta = 0 + for c in periodo: + insc = int(c.get("Inscritos_Totales", 0) or 0) + meta = L.meta_curso(c.get("dsc_programa")) + tp = L.tipo_programa_curso(c.get("dsc_programa")) + tot_insc += insc + tot_meta += meta + detalle.append({ + "num_indice": c.get("num_indice"), + "dsc_programa": c.get("dsc_programa"), + "fch_inicio": str(c.get("fch_inicio"))[:10], + "cod_estado": c.get("cod_estado"), + "inscritos": insc, + "tipo": tp, + "meta": meta, + }) + return { + "num_cursos": len(periodo), + "suma_inscritos": tot_insc, + "suma_meta": tot_meta, + "ocupabilidad": round((tot_insc / tot_meta * 100), 2) if tot_meta else 0, + "cursos": detalle, + } + + +# ── Precarga / refresco ───────────────────────────────────────── +def _periodos_actual_anterior(): + """[(ano, mes), ...] = mes actual y mes anterior (segun fecha de hoy).""" + from datetime import date + hoy = date.today() + ano, mes = hoy.year, hoy.month + if mes == 1: + prev = (ano - 1, 12) + else: + prev = (ano, mes - 1) + 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") + + +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: + f.write(linea + "\n") + except Exception: + 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 + + +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. + Si 'apartado' se indica, acumula el tiempo para el resumen final.""" + import time as _t + t0 = _t.perf_counter() + r = fn() + dt = _t.perf_counter() - t0 + n = "" + try: + if isinstance(r, (list, dict)): + n = f"({len(r)} filas)" + except Exception: + pass + linea = f" [carga] {etiqueta:32} {base:14} {dt:6.2f}s {n}" + print(linea, flush=True) + _log_carga(linea) + with _TIEMPOS_LOCK: + _TIEMPOS[apartado or etiqueta] = _TIEMPOS.get(apartado or etiqueta, 0.0) + dt + return r + + +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: + _HILOS_LISTOS.add(nombre) + listos = {"leads", "otros"}.issubset(_HILOS_LISTOS) + tiempos = dict(_TIEMPOS) + if not listos: + return + _log_carga("") + _log_carga("=" * 60) + _log_carga(" RESUMEN — tiempo listo para abrir cada apartado") + _log_carga("=" * 60) + orden = ["Leads", "Vendedores", "Otros General", "ROAS"] + for ap in orden: + seg = tiempos.get(ap) + if seg is not None: + estado = "instantaneo al abrir" if seg < 1 else f"listo tras {seg:.1f}s" + _log_carga(f" {ap:16} {seg:6.2f}s -> {estado}") + _log_carga("-" * 60) + _log_carga(" Ya precargados: al hacer clic se muestran AL INSTANTE (cache).") + _log_carga("=" * 60) + + +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: + import time as _t + _log_carga("=" * 60, reset=True) + _log_carga(" CARGA DEL DASHBOARD LEADS — " + + _t.strftime("%d/%m/%Y %H:%M:%S")) + _log_carga("=" * 60) + print(">>> Precargando LEADS (hilo 1)...", flush=True) + _log_carga(">>> Hilo 1: LEADS (PostgreSQL + SQL Server)") + _cron("Leads (mensajes)", "PostgreSQL", _leads_crudos) + _cron("Cursos", "SQL Server", _cursos_crudos) + _cron("Matriculas", "SQL Server", _matriculas_crudas) + _cron("Leads asignados/respuesta", "PostgreSQL", _leads_asignados_crudos) + # Dashboard (Leads) del mes actual y anterior (lo mas usado). Otros General + # NO se precarga aqui (sus consultas son pesadas); se calcula al entrar. + p = _periodos_actual_anterior() + a0, m0 = p[0] + for (a, m) in p: + _cron(f"Apartado Leads {a}-{m}", "cache", + lambda a=a, m=m: leads_dashboard(a, m, "TODOS", "TODOS", "TODOS"), apartado="Leads") + # Vendedores y ROAS del periodo actual: se precargan para abrir al instante. + _cron("Apartado Vendedores", "cache", + lambda: vendedores_dashboard(a0, m0, "TODOS"), apartado="Vendedores") + _cron("Apartado ROAS", "cache", + lambda: roas_dashboard(a0, m0, "TODOS"), apartado="ROAS") + _marcar_actualizacion() + print(">>> Hilo 1 (Leads) LISTO.", flush=True) + _log_carga(">>> Hilo 1 (Leads) LISTO.") + _hilo_termino("leads") + except Exception as e: + print(f"[precarga] {e}", flush=True) + _log_carga(f"[precarga] ERROR: {e}") + _hilo_termino("leads") + + +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: + print(">>> Precargando CARTERA + OTROS GENERAL (hilo 2)...", flush=True) + _log_carga(">>> Hilo 2: CARTERA (Supabase) + OTROS GENERAL") + _cron("Plantillas", "Google Sheet", _plantillas_crudas) + _cron("Cartera", "Supabase", _cartera_rows_cache) # la mas pesada + for (a, m) in _periodos_actual_anterior(): + _cron(f"Otros General {a}-{m}", "cache", + lambda a=a, m=m: otros_general(a, m, "TODOS"), apartado="Otros General") + print(">>> Hilo 2 (Cartera/Otros) LISTO.", flush=True) + _log_carga(">>> Hilo 2 (Cartera/Otros) LISTO.") + _hilo_termino("otros") + except Exception as e: + print(f"[precarga-otros] {e}", flush=True) + _log_carga(f"[precarga-otros] ERROR: {e}") + _hilo_termino("otros") + + +# Hora de la última actualización (hora Perú) +_ULTIMA_ACTUALIZACION = None + + +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(): + return {"hora": _ULTIMA_ACTUALIZACION} + + +def refrescar_todo(): + from cache_manager import cache_invalidate + from data_manager_v2 import invalidar_campanias_cache + cache_invalidate() # vaciar y recargar TODO + invalidar_campanias_cache() # tambien las campañas (cache aparte) + precargar_todo() # leads (mes actual/anterior) + precargar_otros_general() # cartera + Otros General (para que no quede vacia) + _marcar_actualizacion() diff --git a/backend/ver_campanias.py b/backend/ver_campanias.py new file mode 100644 index 0000000..9f26b07 --- /dev/null +++ b/backend/ver_campanias.py @@ -0,0 +1,55 @@ +# ver_campanias.py — Muestra en pantalla TODA la tabla campanias de Supabase. +# Ejecutar dentro de backend/: python ver_campanias.py +import os, requests +from dotenv import load_dotenv +load_dotenv() + +URL = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "") +KEY = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "") +TABLA = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias") +HEAD = {"apikey": KEY, "Authorization": f"Bearer {KEY}"} + +assert URL and KEY, "Falta CARTERA_URL / CARTERA_KEY en .env" + +def traer_todo(): + filas, paso, desde = [], 1000, 0 + while True: + hdr = dict(HEAD) + hdr["Range-Unit"] = "items" + hdr["Range"] = f"{desde}-{desde + paso - 1}" + r = requests.get(f"{URL}/rest/v1/{TABLA}", + params={"select": "*", "order": "codigo"}, + headers=hdr, timeout=60) + r.raise_for_status() + data = r.json() + if not data: + break + filas.extend(data) + desde += len(data) + if len(data) < paso: + break + return filas + +filas = traer_todo() +print(f"\nTotal campanias: {len(filas)}\n") + +if not filas: + print("Sin datos. Revisa CARTERA_URL / CARTERA_KEY en .env") + raise SystemExit + +# columnas +cols = ["frase_busqueda", "cargo", "codigo", "sede", "dia", "origen", "fecha_inicio", "fecha_fin"] +# por si hay columnas extra +for f in filas: + for k in f.keys(): + if k not in cols and k != "id": + cols.append(k) + +# imprime cada campania en pantalla +for i, f in enumerate(filas, 1): + partes = [] + for c in cols: + v = f.get(c, "") + v = "" if v is None else str(v) + partes.append(f"{c}={v}") + print(f"{i:3}. " + " | ".join(partes)) diff --git a/backend/ver_leyenda.py b/backend/ver_leyenda.py new file mode 100644 index 0000000..32f1208 --- /dev/null +++ b/backend/ver_leyenda.py @@ -0,0 +1,24 @@ +# ver_leyenda.py - Muestra en pantalla el mini-apartado LEYENDA (tabla campanias), +# igual que se ve en el dashboard. Solo lectura. +# Ejecutar en backend/: python ver_leyenda.py +from data_manager_v2 import DataManager + +dm = DataManager() +filas = dm.listar_campanias() + +print(f"\n=== LEYENDA (tabla campanias) — {len(filas)} filas ===\n") +h = f"{'id':>4} {'FRASE DE BUSQUEDA':38} {'CARGO':16} {'COD':6} {'SEDE':10} {'DIA':14} {'ORIGEN':13} {'INICIO':11} {'FIN':11}" +print(h) +print("-"*len(h)) +for f in filas: + print(f"{str(f.get('id','')):>4} " + f"{str(f.get('frase_busqueda',''))[:38]:38} " + f"{str(f.get('cargo',''))[:16]:16} " + f"{str(f.get('codigo','')):6} " + f"{str(f.get('sede',''))[:10]:10} " + f"{str(f.get('dia',''))[:14]:14} " + f"{str(f.get('origen',''))[:13]:13} " + f"{str(f.get('fecha_inicio','')):11} " + f"{str(f.get('fecha_fin','')):11}") +print("-"*len(h)) +print(f"Total: {len(filas)} campanias") diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a467813 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Dashboard Leads — Escuela Refrigeración + + +
+ + + diff --git a/frontend/node_modules/.bin/baseline-browser-mapping b/frontend/node_modules/.bin/baseline-browser-mapping new file mode 100644 index 0000000..7e4dd08 --- /dev/null +++ b/frontend/node_modules/.bin/baseline-browser-mapping @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@" +else + exec node "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@" +fi diff --git a/frontend/node_modules/.bin/baseline-browser-mapping.cmd b/frontend/node_modules/.bin/baseline-browser-mapping.cmd new file mode 100644 index 0000000..724ac4d --- /dev/null +++ b/frontend/node_modules/.bin/baseline-browser-mapping.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\baseline-browser-mapping\dist\cli.cjs" %* diff --git a/frontend/node_modules/.bin/baseline-browser-mapping.ps1 b/frontend/node_modules/.bin/baseline-browser-mapping.ps1 new file mode 100644 index 0000000..049fe74 --- /dev/null +++ b/frontend/node_modules/.bin/baseline-browser-mapping.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args + } else { + & "$basedir/node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args + } else { + & "node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/browserslist b/frontend/node_modules/.bin/browserslist new file mode 100644 index 0000000..60e71ad --- /dev/null +++ b/frontend/node_modules/.bin/browserslist @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../browserslist/cli.js" "$@" +else + exec node "$basedir/../browserslist/cli.js" "$@" +fi diff --git a/frontend/node_modules/.bin/browserslist.cmd b/frontend/node_modules/.bin/browserslist.cmd new file mode 100644 index 0000000..f93c251 --- /dev/null +++ b/frontend/node_modules/.bin/browserslist.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\browserslist\cli.js" %* diff --git a/frontend/node_modules/.bin/browserslist.ps1 b/frontend/node_modules/.bin/browserslist.ps1 new file mode 100644 index 0000000..01e10a0 --- /dev/null +++ b/frontend/node_modules/.bin/browserslist.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../browserslist/cli.js" $args + } else { + & "$basedir/node$exe" "$basedir/../browserslist/cli.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../browserslist/cli.js" $args + } else { + & "node$exe" "$basedir/../browserslist/cli.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/esbuild b/frontend/node_modules/.bin/esbuild new file mode 100644 index 0000000..63bb6d4 --- /dev/null +++ b/frontend/node_modules/.bin/esbuild @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@" +else + exec node "$basedir/../esbuild/bin/esbuild" "$@" +fi diff --git a/frontend/node_modules/.bin/esbuild.cmd b/frontend/node_modules/.bin/esbuild.cmd new file mode 100644 index 0000000..cc920c5 --- /dev/null +++ b/frontend/node_modules/.bin/esbuild.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %* diff --git a/frontend/node_modules/.bin/esbuild.ps1 b/frontend/node_modules/.bin/esbuild.ps1 new file mode 100644 index 0000000..81ffbf9 --- /dev/null +++ b/frontend/node_modules/.bin/esbuild.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args + } else { + & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args + } else { + & "node$exe" "$basedir/../esbuild/bin/esbuild" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/jsesc b/frontend/node_modules/.bin/jsesc new file mode 100644 index 0000000..879c413 --- /dev/null +++ b/frontend/node_modules/.bin/jsesc @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../jsesc/bin/jsesc" "$@" +else + exec node "$basedir/../jsesc/bin/jsesc" "$@" +fi diff --git a/frontend/node_modules/.bin/jsesc.cmd b/frontend/node_modules/.bin/jsesc.cmd new file mode 100644 index 0000000..eb41110 --- /dev/null +++ b/frontend/node_modules/.bin/jsesc.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\jsesc\bin\jsesc" %* diff --git a/frontend/node_modules/.bin/jsesc.ps1 b/frontend/node_modules/.bin/jsesc.ps1 new file mode 100644 index 0000000..6007e02 --- /dev/null +++ b/frontend/node_modules/.bin/jsesc.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../jsesc/bin/jsesc" $args + } else { + & "$basedir/node$exe" "$basedir/../jsesc/bin/jsesc" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../jsesc/bin/jsesc" $args + } else { + & "node$exe" "$basedir/../jsesc/bin/jsesc" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/json5 b/frontend/node_modules/.bin/json5 new file mode 100644 index 0000000..abf72a4 --- /dev/null +++ b/frontend/node_modules/.bin/json5 @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../json5/lib/cli.js" "$@" +else + exec node "$basedir/../json5/lib/cli.js" "$@" +fi diff --git a/frontend/node_modules/.bin/json5.cmd b/frontend/node_modules/.bin/json5.cmd new file mode 100644 index 0000000..95c137f --- /dev/null +++ b/frontend/node_modules/.bin/json5.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\json5\lib\cli.js" %* diff --git a/frontend/node_modules/.bin/json5.ps1 b/frontend/node_modules/.bin/json5.ps1 new file mode 100644 index 0000000..8700ddb --- /dev/null +++ b/frontend/node_modules/.bin/json5.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../json5/lib/cli.js" $args + } else { + & "$basedir/node$exe" "$basedir/../json5/lib/cli.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../json5/lib/cli.js" $args + } else { + & "node$exe" "$basedir/../json5/lib/cli.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/loose-envify b/frontend/node_modules/.bin/loose-envify new file mode 100644 index 0000000..076f91b --- /dev/null +++ b/frontend/node_modules/.bin/loose-envify @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../loose-envify/cli.js" "$@" +else + exec node "$basedir/../loose-envify/cli.js" "$@" +fi diff --git a/frontend/node_modules/.bin/loose-envify.cmd b/frontend/node_modules/.bin/loose-envify.cmd new file mode 100644 index 0000000..599576f --- /dev/null +++ b/frontend/node_modules/.bin/loose-envify.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\loose-envify\cli.js" %* diff --git a/frontend/node_modules/.bin/loose-envify.ps1 b/frontend/node_modules/.bin/loose-envify.ps1 new file mode 100644 index 0000000..eb866fc --- /dev/null +++ b/frontend/node_modules/.bin/loose-envify.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../loose-envify/cli.js" $args + } else { + & "$basedir/node$exe" "$basedir/../loose-envify/cli.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../loose-envify/cli.js" $args + } else { + & "node$exe" "$basedir/../loose-envify/cli.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/nanoid b/frontend/node_modules/.bin/nanoid new file mode 100644 index 0000000..46220bd --- /dev/null +++ b/frontend/node_modules/.bin/nanoid @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@" +else + exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@" +fi diff --git a/frontend/node_modules/.bin/nanoid.cmd b/frontend/node_modules/.bin/nanoid.cmd new file mode 100644 index 0000000..9c40107 --- /dev/null +++ b/frontend/node_modules/.bin/nanoid.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %* diff --git a/frontend/node_modules/.bin/nanoid.ps1 b/frontend/node_modules/.bin/nanoid.ps1 new file mode 100644 index 0000000..d8a4d7a --- /dev/null +++ b/frontend/node_modules/.bin/nanoid.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args + } else { + & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args + } else { + & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/parser b/frontend/node_modules/.bin/parser new file mode 100644 index 0000000..7696ad4 --- /dev/null +++ b/frontend/node_modules/.bin/parser @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../@babel/parser/bin/babel-parser.js" "$@" +else + exec node "$basedir/../@babel/parser/bin/babel-parser.js" "$@" +fi diff --git a/frontend/node_modules/.bin/parser.cmd b/frontend/node_modules/.bin/parser.cmd new file mode 100644 index 0000000..1ad5c81 --- /dev/null +++ b/frontend/node_modules/.bin/parser.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@babel\parser\bin\babel-parser.js" %* diff --git a/frontend/node_modules/.bin/parser.ps1 b/frontend/node_modules/.bin/parser.ps1 new file mode 100644 index 0000000..8926517 --- /dev/null +++ b/frontend/node_modules/.bin/parser.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args + } else { + & "$basedir/node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args + } else { + & "node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/rollup b/frontend/node_modules/.bin/rollup new file mode 100644 index 0000000..998fc16 --- /dev/null +++ b/frontend/node_modules/.bin/rollup @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@" +else + exec node "$basedir/../rollup/dist/bin/rollup" "$@" +fi diff --git a/frontend/node_modules/.bin/rollup.cmd b/frontend/node_modules/.bin/rollup.cmd new file mode 100644 index 0000000..b3f110b --- /dev/null +++ b/frontend/node_modules/.bin/rollup.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rollup\dist\bin\rollup" %* diff --git a/frontend/node_modules/.bin/rollup.ps1 b/frontend/node_modules/.bin/rollup.ps1 new file mode 100644 index 0000000..10f657d --- /dev/null +++ b/frontend/node_modules/.bin/rollup.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args + } else { + & "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../rollup/dist/bin/rollup" $args + } else { + & "node$exe" "$basedir/../rollup/dist/bin/rollup" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/semver b/frontend/node_modules/.bin/semver new file mode 100644 index 0000000..97c5327 --- /dev/null +++ b/frontend/node_modules/.bin/semver @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@" +else + exec node "$basedir/../semver/bin/semver.js" "$@" +fi diff --git a/frontend/node_modules/.bin/semver.cmd b/frontend/node_modules/.bin/semver.cmd new file mode 100644 index 0000000..9913fa9 --- /dev/null +++ b/frontend/node_modules/.bin/semver.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %* diff --git a/frontend/node_modules/.bin/semver.ps1 b/frontend/node_modules/.bin/semver.ps1 new file mode 100644 index 0000000..314717a --- /dev/null +++ b/frontend/node_modules/.bin/semver.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args + } else { + & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../semver/bin/semver.js" $args + } else { + & "node$exe" "$basedir/../semver/bin/semver.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/update-browserslist-db b/frontend/node_modules/.bin/update-browserslist-db new file mode 100644 index 0000000..cced63c --- /dev/null +++ b/frontend/node_modules/.bin/update-browserslist-db @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../update-browserslist-db/cli.js" "$@" +else + exec node "$basedir/../update-browserslist-db/cli.js" "$@" +fi diff --git a/frontend/node_modules/.bin/update-browserslist-db.cmd b/frontend/node_modules/.bin/update-browserslist-db.cmd new file mode 100644 index 0000000..2e14905 --- /dev/null +++ b/frontend/node_modules/.bin/update-browserslist-db.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\update-browserslist-db\cli.js" %* diff --git a/frontend/node_modules/.bin/update-browserslist-db.ps1 b/frontend/node_modules/.bin/update-browserslist-db.ps1 new file mode 100644 index 0000000..7abdf26 --- /dev/null +++ b/frontend/node_modules/.bin/update-browserslist-db.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../update-browserslist-db/cli.js" $args + } else { + & "$basedir/node$exe" "$basedir/../update-browserslist-db/cli.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../update-browserslist-db/cli.js" $args + } else { + & "node$exe" "$basedir/../update-browserslist-db/cli.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.bin/vite b/frontend/node_modules/.bin/vite new file mode 100644 index 0000000..014463f --- /dev/null +++ b/frontend/node_modules/.bin/vite @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@" +else + exec node "$basedir/../vite/bin/vite.js" "$@" +fi diff --git a/frontend/node_modules/.bin/vite.cmd b/frontend/node_modules/.bin/vite.cmd new file mode 100644 index 0000000..f62e966 --- /dev/null +++ b/frontend/node_modules/.bin/vite.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %* diff --git a/frontend/node_modules/.bin/vite.ps1 b/frontend/node_modules/.bin/vite.ps1 new file mode 100644 index 0000000..a7759bc --- /dev/null +++ b/frontend/node_modules/.bin/vite.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args + } else { + & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../vite/bin/vite.js" $args + } else { + & "node$exe" "$basedir/../vite/bin/vite.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/frontend/node_modules/.package-lock.json b/frontend/node_modules/.package-lock.json new file mode 100644 index 0000000..4675128 --- /dev/null +++ b/frontend/node_modules/.package-lock.json @@ -0,0 +1,1335 @@ +{ + "name": "dashboard-leads", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.378", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", + "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-2YIMICFJ.js b/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-2YIMICFJ.js new file mode 100644 index 0000000..b0c56c3 --- /dev/null +++ b/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-2YIMICFJ.js @@ -0,0 +1,1935 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// node_modules/react/cjs/react.development.js +var require_react_development = __commonJS({ + "node_modules/react/cjs/react.development.js"(exports, module) { + "use strict"; + if (true) { + (function() { + "use strict"; + if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined" && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart === "function") { + __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error()); + } + var ReactVersion = "18.3.1"; + var REACT_ELEMENT_TYPE = Symbol.for("react.element"); + var REACT_PORTAL_TYPE = Symbol.for("react.portal"); + var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); + var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"); + var REACT_PROFILER_TYPE = Symbol.for("react.profiler"); + var REACT_PROVIDER_TYPE = Symbol.for("react.provider"); + var REACT_CONTEXT_TYPE = Symbol.for("react.context"); + var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"); + var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"); + var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"); + var REACT_MEMO_TYPE = Symbol.for("react.memo"); + var REACT_LAZY_TYPE = Symbol.for("react.lazy"); + var REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen"); + var MAYBE_ITERATOR_SYMBOL = Symbol.iterator; + var FAUX_ITERATOR_SYMBOL = "@@iterator"; + function getIteratorFn(maybeIterable) { + if (maybeIterable === null || typeof maybeIterable !== "object") { + return null; + } + var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]; + if (typeof maybeIterator === "function") { + return maybeIterator; + } + return null; + } + var ReactCurrentDispatcher = { + /** + * @internal + * @type {ReactComponent} + */ + current: null + }; + var ReactCurrentBatchConfig = { + transition: null + }; + var ReactCurrentActQueue = { + current: null, + // Used to reproduce behavior of `batchedUpdates` in legacy mode. + isBatchingLegacy: false, + didScheduleLegacyUpdate: false + }; + var ReactCurrentOwner = { + /** + * @internal + * @type {ReactComponent} + */ + current: null + }; + var ReactDebugCurrentFrame = {}; + var currentExtraStackFrame = null; + function setExtraStackFrame(stack) { + { + currentExtraStackFrame = stack; + } + } + { + ReactDebugCurrentFrame.setExtraStackFrame = function(stack) { + { + currentExtraStackFrame = stack; + } + }; + ReactDebugCurrentFrame.getCurrentStack = null; + ReactDebugCurrentFrame.getStackAddendum = function() { + var stack = ""; + if (currentExtraStackFrame) { + stack += currentExtraStackFrame; + } + var impl = ReactDebugCurrentFrame.getCurrentStack; + if (impl) { + stack += impl() || ""; + } + return stack; + }; + } + var enableScopeAPI = false; + var enableCacheElement = false; + var enableTransitionTracing = false; + var enableLegacyHidden = false; + var enableDebugTracing = false; + var ReactSharedInternals = { + ReactCurrentDispatcher, + ReactCurrentBatchConfig, + ReactCurrentOwner + }; + { + ReactSharedInternals.ReactDebugCurrentFrame = ReactDebugCurrentFrame; + ReactSharedInternals.ReactCurrentActQueue = ReactCurrentActQueue; + } + function warn(format) { + { + { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + printWarning("warn", format, args); + } + } + } + function error(format) { + { + { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + printWarning("error", format, args); + } + } + } + function printWarning(level, format, args) { + { + var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame; + var stack = ReactDebugCurrentFrame2.getStackAddendum(); + if (stack !== "") { + format += "%s"; + args = args.concat([stack]); + } + var argsWithFormat = args.map(function(item) { + return String(item); + }); + argsWithFormat.unshift("Warning: " + format); + Function.prototype.apply.call(console[level], console, argsWithFormat); + } + } + var didWarnStateUpdateForUnmountedComponent = {}; + function warnNoop(publicInstance, callerName) { + { + var _constructor = publicInstance.constructor; + var componentName = _constructor && (_constructor.displayName || _constructor.name) || "ReactClass"; + var warningKey = componentName + "." + callerName; + if (didWarnStateUpdateForUnmountedComponent[warningKey]) { + return; + } + error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.", callerName, componentName); + didWarnStateUpdateForUnmountedComponent[warningKey] = true; + } + } + var ReactNoopUpdateQueue = { + /** + * Checks whether or not this composite component is mounted. + * @param {ReactClass} publicInstance The instance we want to test. + * @return {boolean} True if mounted, false otherwise. + * @protected + * @final + */ + isMounted: function(publicInstance) { + return false; + }, + /** + * Forces an update. This should only be invoked when it is known with + * certainty that we are **not** in a DOM transaction. + * + * You may want to call this when you know that some deeper aspect of the + * component's state has changed but `setState` was not called. + * + * This will not invoke `shouldComponentUpdate`, but it will invoke + * `componentWillUpdate` and `componentDidUpdate`. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {?function} callback Called after component is updated. + * @param {?string} callerName name of the calling function in the public API. + * @internal + */ + enqueueForceUpdate: function(publicInstance, callback, callerName) { + warnNoop(publicInstance, "forceUpdate"); + }, + /** + * Replaces all of the state. Always use this or `setState` to mutate state. + * You should treat `this.state` as immutable. + * + * There is no guarantee that `this.state` will be immediately updated, so + * accessing `this.state` after calling this method may return the old value. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} completeState Next state. + * @param {?function} callback Called after component is updated. + * @param {?string} callerName name of the calling function in the public API. + * @internal + */ + enqueueReplaceState: function(publicInstance, completeState, callback, callerName) { + warnNoop(publicInstance, "replaceState"); + }, + /** + * Sets a subset of the state. This only exists because _pendingState is + * internal. This provides a merging strategy that is not available to deep + * properties which is confusing. TODO: Expose pendingState or don't use it + * during the merge. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} partialState Next partial state to be merged with state. + * @param {?function} callback Called after component is updated. + * @param {?string} Name of the calling function in the public API. + * @internal + */ + enqueueSetState: function(publicInstance, partialState, callback, callerName) { + warnNoop(publicInstance, "setState"); + } + }; + var assign = Object.assign; + var emptyObject = {}; + { + Object.freeze(emptyObject); + } + function Component(props, context, updater) { + this.props = props; + this.context = context; + this.refs = emptyObject; + this.updater = updater || ReactNoopUpdateQueue; + } + Component.prototype.isReactComponent = {}; + Component.prototype.setState = function(partialState, callback) { + if (typeof partialState !== "object" && typeof partialState !== "function" && partialState != null) { + throw new Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables."); + } + this.updater.enqueueSetState(this, partialState, callback, "setState"); + }; + Component.prototype.forceUpdate = function(callback) { + this.updater.enqueueForceUpdate(this, callback, "forceUpdate"); + }; + { + var deprecatedAPIs = { + isMounted: ["isMounted", "Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks."], + replaceState: ["replaceState", "Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236)."] + }; + var defineDeprecationWarning = function(methodName, info) { + Object.defineProperty(Component.prototype, methodName, { + get: function() { + warn("%s(...) is deprecated in plain JavaScript React classes. %s", info[0], info[1]); + return void 0; + } + }); + }; + for (var fnName in deprecatedAPIs) { + if (deprecatedAPIs.hasOwnProperty(fnName)) { + defineDeprecationWarning(fnName, deprecatedAPIs[fnName]); + } + } + } + function ComponentDummy() { + } + ComponentDummy.prototype = Component.prototype; + function PureComponent(props, context, updater) { + this.props = props; + this.context = context; + this.refs = emptyObject; + this.updater = updater || ReactNoopUpdateQueue; + } + var pureComponentPrototype = PureComponent.prototype = new ComponentDummy(); + pureComponentPrototype.constructor = PureComponent; + assign(pureComponentPrototype, Component.prototype); + pureComponentPrototype.isPureReactComponent = true; + function createRef() { + var refObject = { + current: null + }; + { + Object.seal(refObject); + } + return refObject; + } + var isArrayImpl = Array.isArray; + function isArray(a) { + return isArrayImpl(a); + } + function typeName(value) { + { + var hasToStringTag = typeof Symbol === "function" && Symbol.toStringTag; + var type = hasToStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object"; + return type; + } + } + function willCoercionThrow(value) { + { + try { + testStringCoercion(value); + return false; + } catch (e) { + return true; + } + } + } + function testStringCoercion(value) { + return "" + value; + } + function checkKeyStringCoercion(value) { + { + if (willCoercionThrow(value)) { + error("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.", typeName(value)); + return testStringCoercion(value); + } + } + } + function getWrappedName(outerType, innerType, wrapperName) { + var displayName = outerType.displayName; + if (displayName) { + return displayName; + } + var functionName = innerType.displayName || innerType.name || ""; + return functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName; + } + function getContextName(type) { + return type.displayName || "Context"; + } + function getComponentNameFromType(type) { + if (type == null) { + return null; + } + { + if (typeof type.tag === "number") { + error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."); + } + } + if (typeof type === "function") { + return type.displayName || type.name || null; + } + if (typeof type === "string") { + return type; + } + switch (type) { + case REACT_FRAGMENT_TYPE: + return "Fragment"; + case REACT_PORTAL_TYPE: + return "Portal"; + case REACT_PROFILER_TYPE: + return "Profiler"; + case REACT_STRICT_MODE_TYPE: + return "StrictMode"; + case REACT_SUSPENSE_TYPE: + return "Suspense"; + case REACT_SUSPENSE_LIST_TYPE: + return "SuspenseList"; + } + if (typeof type === "object") { + switch (type.$$typeof) { + case REACT_CONTEXT_TYPE: + var context = type; + return getContextName(context) + ".Consumer"; + case REACT_PROVIDER_TYPE: + var provider = type; + return getContextName(provider._context) + ".Provider"; + case REACT_FORWARD_REF_TYPE: + return getWrappedName(type, type.render, "ForwardRef"); + case REACT_MEMO_TYPE: + var outerName = type.displayName || null; + if (outerName !== null) { + return outerName; + } + return getComponentNameFromType(type.type) || "Memo"; + case REACT_LAZY_TYPE: { + var lazyComponent = type; + var payload = lazyComponent._payload; + var init = lazyComponent._init; + try { + return getComponentNameFromType(init(payload)); + } catch (x) { + return null; + } + } + } + } + return null; + } + var hasOwnProperty = Object.prototype.hasOwnProperty; + var RESERVED_PROPS = { + key: true, + ref: true, + __self: true, + __source: true + }; + var specialPropKeyWarningShown, specialPropRefWarningShown, didWarnAboutStringRefs; + { + didWarnAboutStringRefs = {}; + } + function hasValidRef(config) { + { + if (hasOwnProperty.call(config, "ref")) { + var getter = Object.getOwnPropertyDescriptor(config, "ref").get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.ref !== void 0; + } + function hasValidKey(config) { + { + if (hasOwnProperty.call(config, "key")) { + var getter = Object.getOwnPropertyDescriptor(config, "key").get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.key !== void 0; + } + function defineKeyPropWarningGetter(props, displayName) { + var warnAboutAccessingKey = function() { + { + if (!specialPropKeyWarningShown) { + specialPropKeyWarningShown = true; + error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName); + } + } + }; + warnAboutAccessingKey.isReactWarning = true; + Object.defineProperty(props, "key", { + get: warnAboutAccessingKey, + configurable: true + }); + } + function defineRefPropWarningGetter(props, displayName) { + var warnAboutAccessingRef = function() { + { + if (!specialPropRefWarningShown) { + specialPropRefWarningShown = true; + error("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName); + } + } + }; + warnAboutAccessingRef.isReactWarning = true; + Object.defineProperty(props, "ref", { + get: warnAboutAccessingRef, + configurable: true + }); + } + function warnIfStringRefCannotBeAutoConverted(config) { + { + if (typeof config.ref === "string" && ReactCurrentOwner.current && config.__self && ReactCurrentOwner.current.stateNode !== config.__self) { + var componentName = getComponentNameFromType(ReactCurrentOwner.current.type); + if (!didWarnAboutStringRefs[componentName]) { + error('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref', componentName, config.ref); + didWarnAboutStringRefs[componentName] = true; + } + } + } + } + var ReactElement = function(type, key, ref, self, source, owner, props) { + var element = { + // This tag allows us to uniquely identify this as a React Element + $$typeof: REACT_ELEMENT_TYPE, + // Built-in properties that belong on the element + type, + key, + ref, + props, + // Record the component responsible for creating this element. + _owner: owner + }; + { + element._store = {}; + Object.defineProperty(element._store, "validated", { + configurable: false, + enumerable: false, + writable: true, + value: false + }); + Object.defineProperty(element, "_self", { + configurable: false, + enumerable: false, + writable: false, + value: self + }); + Object.defineProperty(element, "_source", { + configurable: false, + enumerable: false, + writable: false, + value: source + }); + if (Object.freeze) { + Object.freeze(element.props); + Object.freeze(element); + } + } + return element; + }; + function createElement(type, config, children) { + var propName; + var props = {}; + var key = null; + var ref = null; + var self = null; + var source = null; + if (config != null) { + if (hasValidRef(config)) { + ref = config.ref; + { + warnIfStringRefCannotBeAutoConverted(config); + } + } + if (hasValidKey(config)) { + { + checkKeyStringCoercion(config.key); + } + key = "" + config.key; + } + self = config.__self === void 0 ? null : config.__self; + source = config.__source === void 0 ? null : config.__source; + for (propName in config) { + if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) { + props[propName] = config[propName]; + } + } + } + var childrenLength = arguments.length - 2; + if (childrenLength === 1) { + props.children = children; + } else if (childrenLength > 1) { + var childArray = Array(childrenLength); + for (var i = 0; i < childrenLength; i++) { + childArray[i] = arguments[i + 2]; + } + { + if (Object.freeze) { + Object.freeze(childArray); + } + } + props.children = childArray; + } + if (type && type.defaultProps) { + var defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === void 0) { + props[propName] = defaultProps[propName]; + } + } + } + { + if (key || ref) { + var displayName = typeof type === "function" ? type.displayName || type.name || "Unknown" : type; + if (key) { + defineKeyPropWarningGetter(props, displayName); + } + if (ref) { + defineRefPropWarningGetter(props, displayName); + } + } + } + return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props); + } + function cloneAndReplaceKey(oldElement, newKey) { + var newElement = ReactElement(oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props); + return newElement; + } + function cloneElement(element, config, children) { + if (element === null || element === void 0) { + throw new Error("React.cloneElement(...): The argument must be a React element, but you passed " + element + "."); + } + var propName; + var props = assign({}, element.props); + var key = element.key; + var ref = element.ref; + var self = element._self; + var source = element._source; + var owner = element._owner; + if (config != null) { + if (hasValidRef(config)) { + ref = config.ref; + owner = ReactCurrentOwner.current; + } + if (hasValidKey(config)) { + { + checkKeyStringCoercion(config.key); + } + key = "" + config.key; + } + var defaultProps; + if (element.type && element.type.defaultProps) { + defaultProps = element.type.defaultProps; + } + for (propName in config) { + if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) { + if (config[propName] === void 0 && defaultProps !== void 0) { + props[propName] = defaultProps[propName]; + } else { + props[propName] = config[propName]; + } + } + } + } + var childrenLength = arguments.length - 2; + if (childrenLength === 1) { + props.children = children; + } else if (childrenLength > 1) { + var childArray = Array(childrenLength); + for (var i = 0; i < childrenLength; i++) { + childArray[i] = arguments[i + 2]; + } + props.children = childArray; + } + return ReactElement(element.type, key, ref, self, source, owner, props); + } + function isValidElement(object) { + return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE; + } + var SEPARATOR = "."; + var SUBSEPARATOR = ":"; + function escape(key) { + var escapeRegex = /[=:]/g; + var escaperLookup = { + "=": "=0", + ":": "=2" + }; + var escapedString = key.replace(escapeRegex, function(match) { + return escaperLookup[match]; + }); + return "$" + escapedString; + } + var didWarnAboutMaps = false; + var userProvidedKeyEscapeRegex = /\/+/g; + function escapeUserProvidedKey(text) { + return text.replace(userProvidedKeyEscapeRegex, "$&/"); + } + function getElementKey(element, index) { + if (typeof element === "object" && element !== null && element.key != null) { + { + checkKeyStringCoercion(element.key); + } + return escape("" + element.key); + } + return index.toString(36); + } + function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) { + var type = typeof children; + if (type === "undefined" || type === "boolean") { + children = null; + } + var invokeCallback = false; + if (children === null) { + invokeCallback = true; + } else { + switch (type) { + case "string": + case "number": + invokeCallback = true; + break; + case "object": + switch (children.$$typeof) { + case REACT_ELEMENT_TYPE: + case REACT_PORTAL_TYPE: + invokeCallback = true; + } + } + } + if (invokeCallback) { + var _child = children; + var mappedChild = callback(_child); + var childKey = nameSoFar === "" ? SEPARATOR + getElementKey(_child, 0) : nameSoFar; + if (isArray(mappedChild)) { + var escapedChildKey = ""; + if (childKey != null) { + escapedChildKey = escapeUserProvidedKey(childKey) + "/"; + } + mapIntoArray(mappedChild, array, escapedChildKey, "", function(c) { + return c; + }); + } else if (mappedChild != null) { + if (isValidElement(mappedChild)) { + { + if (mappedChild.key && (!_child || _child.key !== mappedChild.key)) { + checkKeyStringCoercion(mappedChild.key); + } + } + mappedChild = cloneAndReplaceKey( + mappedChild, + // Keep both the (mapped) and old keys if they differ, just as + // traverseAllChildren used to do for objects as children + escapedPrefix + // $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key + (mappedChild.key && (!_child || _child.key !== mappedChild.key) ? ( + // $FlowFixMe Flow incorrectly thinks existing element's key can be a number + // eslint-disable-next-line react-internal/safe-string-coercion + escapeUserProvidedKey("" + mappedChild.key) + "/" + ) : "") + childKey + ); + } + array.push(mappedChild); + } + return 1; + } + var child; + var nextName; + var subtreeCount = 0; + var nextNamePrefix = nameSoFar === "" ? SEPARATOR : nameSoFar + SUBSEPARATOR; + if (isArray(children)) { + for (var i = 0; i < children.length; i++) { + child = children[i]; + nextName = nextNamePrefix + getElementKey(child, i); + subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback); + } + } else { + var iteratorFn = getIteratorFn(children); + if (typeof iteratorFn === "function") { + var iterableChildren = children; + { + if (iteratorFn === iterableChildren.entries) { + if (!didWarnAboutMaps) { + warn("Using Maps as children is not supported. Use an array of keyed ReactElements instead."); + } + didWarnAboutMaps = true; + } + } + var iterator = iteratorFn.call(iterableChildren); + var step; + var ii = 0; + while (!(step = iterator.next()).done) { + child = step.value; + nextName = nextNamePrefix + getElementKey(child, ii++); + subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback); + } + } else if (type === "object") { + var childrenString = String(children); + throw new Error("Objects are not valid as a React child (found: " + (childrenString === "[object Object]" ? "object with keys {" + Object.keys(children).join(", ") + "}" : childrenString) + "). If you meant to render a collection of children, use an array instead."); + } + } + return subtreeCount; + } + function mapChildren(children, func, context) { + if (children == null) { + return children; + } + var result = []; + var count = 0; + mapIntoArray(children, result, "", "", function(child) { + return func.call(context, child, count++); + }); + return result; + } + function countChildren(children) { + var n = 0; + mapChildren(children, function() { + n++; + }); + return n; + } + function forEachChildren(children, forEachFunc, forEachContext) { + mapChildren(children, function() { + forEachFunc.apply(this, arguments); + }, forEachContext); + } + function toArray(children) { + return mapChildren(children, function(child) { + return child; + }) || []; + } + function onlyChild(children) { + if (!isValidElement(children)) { + throw new Error("React.Children.only expected to receive a single React element child."); + } + return children; + } + function createContext(defaultValue) { + var context = { + $$typeof: REACT_CONTEXT_TYPE, + // As a workaround to support multiple concurrent renderers, we categorize + // some renderers as primary and others as secondary. We only expect + // there to be two concurrent renderers at most: React Native (primary) and + // Fabric (secondary); React DOM (primary) and React ART (secondary). + // Secondary renderers store their context values on separate fields. + _currentValue: defaultValue, + _currentValue2: defaultValue, + // Used to track how many concurrent renderers this context currently + // supports within in a single renderer. Such as parallel server rendering. + _threadCount: 0, + // These are circular + Provider: null, + Consumer: null, + // Add these to use same hidden class in VM as ServerContext + _defaultValue: null, + _globalName: null + }; + context.Provider = { + $$typeof: REACT_PROVIDER_TYPE, + _context: context + }; + var hasWarnedAboutUsingNestedContextConsumers = false; + var hasWarnedAboutUsingConsumerProvider = false; + var hasWarnedAboutDisplayNameOnConsumer = false; + { + var Consumer = { + $$typeof: REACT_CONTEXT_TYPE, + _context: context + }; + Object.defineProperties(Consumer, { + Provider: { + get: function() { + if (!hasWarnedAboutUsingConsumerProvider) { + hasWarnedAboutUsingConsumerProvider = true; + error("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?"); + } + return context.Provider; + }, + set: function(_Provider) { + context.Provider = _Provider; + } + }, + _currentValue: { + get: function() { + return context._currentValue; + }, + set: function(_currentValue) { + context._currentValue = _currentValue; + } + }, + _currentValue2: { + get: function() { + return context._currentValue2; + }, + set: function(_currentValue2) { + context._currentValue2 = _currentValue2; + } + }, + _threadCount: { + get: function() { + return context._threadCount; + }, + set: function(_threadCount) { + context._threadCount = _threadCount; + } + }, + Consumer: { + get: function() { + if (!hasWarnedAboutUsingNestedContextConsumers) { + hasWarnedAboutUsingNestedContextConsumers = true; + error("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?"); + } + return context.Consumer; + } + }, + displayName: { + get: function() { + return context.displayName; + }, + set: function(displayName) { + if (!hasWarnedAboutDisplayNameOnConsumer) { + warn("Setting `displayName` on Context.Consumer has no effect. You should set it directly on the context with Context.displayName = '%s'.", displayName); + hasWarnedAboutDisplayNameOnConsumer = true; + } + } + } + }); + context.Consumer = Consumer; + } + { + context._currentRenderer = null; + context._currentRenderer2 = null; + } + return context; + } + var Uninitialized = -1; + var Pending = 0; + var Resolved = 1; + var Rejected = 2; + function lazyInitializer(payload) { + if (payload._status === Uninitialized) { + var ctor = payload._result; + var thenable = ctor(); + thenable.then(function(moduleObject2) { + if (payload._status === Pending || payload._status === Uninitialized) { + var resolved = payload; + resolved._status = Resolved; + resolved._result = moduleObject2; + } + }, function(error2) { + if (payload._status === Pending || payload._status === Uninitialized) { + var rejected = payload; + rejected._status = Rejected; + rejected._result = error2; + } + }); + if (payload._status === Uninitialized) { + var pending = payload; + pending._status = Pending; + pending._result = thenable; + } + } + if (payload._status === Resolved) { + var moduleObject = payload._result; + { + if (moduleObject === void 0) { + error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))\n\nDid you accidentally put curly braces around the import?", moduleObject); + } + } + { + if (!("default" in moduleObject)) { + error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))", moduleObject); + } + } + return moduleObject.default; + } else { + throw payload._result; + } + } + function lazy(ctor) { + var payload = { + // We use these fields to store the result. + _status: Uninitialized, + _result: ctor + }; + var lazyType = { + $$typeof: REACT_LAZY_TYPE, + _payload: payload, + _init: lazyInitializer + }; + { + var defaultProps; + var propTypes; + Object.defineProperties(lazyType, { + defaultProps: { + configurable: true, + get: function() { + return defaultProps; + }, + set: function(newDefaultProps) { + error("React.lazy(...): It is not supported to assign `defaultProps` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."); + defaultProps = newDefaultProps; + Object.defineProperty(lazyType, "defaultProps", { + enumerable: true + }); + } + }, + propTypes: { + configurable: true, + get: function() { + return propTypes; + }, + set: function(newPropTypes) { + error("React.lazy(...): It is not supported to assign `propTypes` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."); + propTypes = newPropTypes; + Object.defineProperty(lazyType, "propTypes", { + enumerable: true + }); + } + } + }); + } + return lazyType; + } + function forwardRef(render) { + { + if (render != null && render.$$typeof === REACT_MEMO_TYPE) { + error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."); + } else if (typeof render !== "function") { + error("forwardRef requires a render function but was given %s.", render === null ? "null" : typeof render); + } else { + if (render.length !== 0 && render.length !== 2) { + error("forwardRef render functions accept exactly two parameters: props and ref. %s", render.length === 1 ? "Did you forget to use the ref parameter?" : "Any additional parameter will be undefined."); + } + } + if (render != null) { + if (render.defaultProps != null || render.propTypes != null) { + error("forwardRef render functions do not support propTypes or defaultProps. Did you accidentally pass a React component?"); + } + } + } + var elementType = { + $$typeof: REACT_FORWARD_REF_TYPE, + render + }; + { + var ownName; + Object.defineProperty(elementType, "displayName", { + enumerable: false, + configurable: true, + get: function() { + return ownName; + }, + set: function(name) { + ownName = name; + if (!render.name && !render.displayName) { + render.displayName = name; + } + } + }); + } + return elementType; + } + var REACT_MODULE_REFERENCE; + { + REACT_MODULE_REFERENCE = Symbol.for("react.module.reference"); + } + function isValidElementType(type) { + if (typeof type === "string" || typeof type === "function") { + return true; + } + if (type === REACT_FRAGMENT_TYPE || type === REACT_PROFILER_TYPE || enableDebugTracing || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || enableLegacyHidden || type === REACT_OFFSCREEN_TYPE || enableScopeAPI || enableCacheElement || enableTransitionTracing) { + return true; + } + if (typeof type === "object" && type !== null) { + if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object + // types supported by any Flight configuration anywhere since + // we don't know which Flight build this will end up being used + // with. + type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== void 0) { + return true; + } + } + return false; + } + function memo(type, compare) { + { + if (!isValidElementType(type)) { + error("memo: The first argument must be a component. Instead received: %s", type === null ? "null" : typeof type); + } + } + var elementType = { + $$typeof: REACT_MEMO_TYPE, + type, + compare: compare === void 0 ? null : compare + }; + { + var ownName; + Object.defineProperty(elementType, "displayName", { + enumerable: false, + configurable: true, + get: function() { + return ownName; + }, + set: function(name) { + ownName = name; + if (!type.name && !type.displayName) { + type.displayName = name; + } + } + }); + } + return elementType; + } + function resolveDispatcher() { + var dispatcher = ReactCurrentDispatcher.current; + { + if (dispatcher === null) { + error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem."); + } + } + return dispatcher; + } + function useContext(Context) { + var dispatcher = resolveDispatcher(); + { + if (Context._context !== void 0) { + var realContext = Context._context; + if (realContext.Consumer === Context) { + error("Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be removed in a future major release. Did you mean to call useContext(Context) instead?"); + } else if (realContext.Provider === Context) { + error("Calling useContext(Context.Provider) is not supported. Did you mean to call useContext(Context) instead?"); + } + } + } + return dispatcher.useContext(Context); + } + function useState(initialState) { + var dispatcher = resolveDispatcher(); + return dispatcher.useState(initialState); + } + function useReducer(reducer, initialArg, init) { + var dispatcher = resolveDispatcher(); + return dispatcher.useReducer(reducer, initialArg, init); + } + function useRef(initialValue) { + var dispatcher = resolveDispatcher(); + return dispatcher.useRef(initialValue); + } + function useEffect(create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useEffect(create, deps); + } + function useInsertionEffect(create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useInsertionEffect(create, deps); + } + function useLayoutEffect(create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useLayoutEffect(create, deps); + } + function useCallback(callback, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useCallback(callback, deps); + } + function useMemo(create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useMemo(create, deps); + } + function useImperativeHandle(ref, create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useImperativeHandle(ref, create, deps); + } + function useDebugValue(value, formatterFn) { + { + var dispatcher = resolveDispatcher(); + return dispatcher.useDebugValue(value, formatterFn); + } + } + function useTransition() { + var dispatcher = resolveDispatcher(); + return dispatcher.useTransition(); + } + function useDeferredValue(value) { + var dispatcher = resolveDispatcher(); + return dispatcher.useDeferredValue(value); + } + function useId() { + var dispatcher = resolveDispatcher(); + return dispatcher.useId(); + } + function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) { + var dispatcher = resolveDispatcher(); + return dispatcher.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); + } + var disabledDepth = 0; + var prevLog; + var prevInfo; + var prevWarn; + var prevError; + var prevGroup; + var prevGroupCollapsed; + var prevGroupEnd; + function disabledLog() { + } + disabledLog.__reactDisabledLog = true; + function disableLogs() { + { + if (disabledDepth === 0) { + prevLog = console.log; + prevInfo = console.info; + prevWarn = console.warn; + prevError = console.error; + prevGroup = console.group; + prevGroupCollapsed = console.groupCollapsed; + prevGroupEnd = console.groupEnd; + var props = { + configurable: true, + enumerable: true, + value: disabledLog, + writable: true + }; + Object.defineProperties(console, { + info: props, + log: props, + warn: props, + error: props, + group: props, + groupCollapsed: props, + groupEnd: props + }); + } + disabledDepth++; + } + } + function reenableLogs() { + { + disabledDepth--; + if (disabledDepth === 0) { + var props = { + configurable: true, + enumerable: true, + writable: true + }; + Object.defineProperties(console, { + log: assign({}, props, { + value: prevLog + }), + info: assign({}, props, { + value: prevInfo + }), + warn: assign({}, props, { + value: prevWarn + }), + error: assign({}, props, { + value: prevError + }), + group: assign({}, props, { + value: prevGroup + }), + groupCollapsed: assign({}, props, { + value: prevGroupCollapsed + }), + groupEnd: assign({}, props, { + value: prevGroupEnd + }) + }); + } + if (disabledDepth < 0) { + error("disabledDepth fell below zero. This is a bug in React. Please file an issue."); + } + } + } + var ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher; + var prefix; + function describeBuiltInComponentFrame(name, source, ownerFn) { + { + if (prefix === void 0) { + try { + throw Error(); + } catch (x) { + var match = x.stack.trim().match(/\n( *(at )?)/); + prefix = match && match[1] || ""; + } + } + return "\n" + prefix + name; + } + } + var reentry = false; + var componentFrameCache; + { + var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map; + componentFrameCache = new PossiblyWeakMap(); + } + function describeNativeComponentFrame(fn, construct) { + if (!fn || reentry) { + return ""; + } + { + var frame = componentFrameCache.get(fn); + if (frame !== void 0) { + return frame; + } + } + var control; + reentry = true; + var previousPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = void 0; + var previousDispatcher; + { + previousDispatcher = ReactCurrentDispatcher$1.current; + ReactCurrentDispatcher$1.current = null; + disableLogs(); + } + try { + if (construct) { + var Fake = function() { + throw Error(); + }; + Object.defineProperty(Fake.prototype, "props", { + set: function() { + throw Error(); + } + }); + if (typeof Reflect === "object" && Reflect.construct) { + try { + Reflect.construct(Fake, []); + } catch (x) { + control = x; + } + Reflect.construct(fn, [], Fake); + } else { + try { + Fake.call(); + } catch (x) { + control = x; + } + fn.call(Fake.prototype); + } + } else { + try { + throw Error(); + } catch (x) { + control = x; + } + fn(); + } + } catch (sample) { + if (sample && control && typeof sample.stack === "string") { + var sampleLines = sample.stack.split("\n"); + var controlLines = control.stack.split("\n"); + var s = sampleLines.length - 1; + var c = controlLines.length - 1; + while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) { + c--; + } + for (; s >= 1 && c >= 0; s--, c--) { + if (sampleLines[s] !== controlLines[c]) { + if (s !== 1 || c !== 1) { + do { + s--; + c--; + if (c < 0 || sampleLines[s] !== controlLines[c]) { + var _frame = "\n" + sampleLines[s].replace(" at new ", " at "); + if (fn.displayName && _frame.includes("")) { + _frame = _frame.replace("", fn.displayName); + } + { + if (typeof fn === "function") { + componentFrameCache.set(fn, _frame); + } + } + return _frame; + } + } while (s >= 1 && c >= 0); + } + break; + } + } + } + } finally { + reentry = false; + { + ReactCurrentDispatcher$1.current = previousDispatcher; + reenableLogs(); + } + Error.prepareStackTrace = previousPrepareStackTrace; + } + var name = fn ? fn.displayName || fn.name : ""; + var syntheticFrame = name ? describeBuiltInComponentFrame(name) : ""; + { + if (typeof fn === "function") { + componentFrameCache.set(fn, syntheticFrame); + } + } + return syntheticFrame; + } + function describeFunctionComponentFrame(fn, source, ownerFn) { + { + return describeNativeComponentFrame(fn, false); + } + } + function shouldConstruct(Component2) { + var prototype = Component2.prototype; + return !!(prototype && prototype.isReactComponent); + } + function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) { + if (type == null) { + return ""; + } + if (typeof type === "function") { + { + return describeNativeComponentFrame(type, shouldConstruct(type)); + } + } + if (typeof type === "string") { + return describeBuiltInComponentFrame(type); + } + switch (type) { + case REACT_SUSPENSE_TYPE: + return describeBuiltInComponentFrame("Suspense"); + case REACT_SUSPENSE_LIST_TYPE: + return describeBuiltInComponentFrame("SuspenseList"); + } + if (typeof type === "object") { + switch (type.$$typeof) { + case REACT_FORWARD_REF_TYPE: + return describeFunctionComponentFrame(type.render); + case REACT_MEMO_TYPE: + return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn); + case REACT_LAZY_TYPE: { + var lazyComponent = type; + var payload = lazyComponent._payload; + var init = lazyComponent._init; + try { + return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn); + } catch (x) { + } + } + } + } + return ""; + } + var loggedTypeFailures = {}; + var ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame; + function setCurrentlyValidatingElement(element) { + { + if (element) { + var owner = element._owner; + var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null); + ReactDebugCurrentFrame$1.setExtraStackFrame(stack); + } else { + ReactDebugCurrentFrame$1.setExtraStackFrame(null); + } + } + } + function checkPropTypes(typeSpecs, values, location, componentName, element) { + { + var has = Function.call.bind(hasOwnProperty); + for (var typeSpecName in typeSpecs) { + if (has(typeSpecs, typeSpecName)) { + var error$1 = void 0; + try { + if (typeof typeSpecs[typeSpecName] !== "function") { + var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`."); + err.name = "Invariant Violation"; + throw err; + } + error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"); + } catch (ex) { + error$1 = ex; + } + if (error$1 && !(error$1 instanceof Error)) { + setCurrentlyValidatingElement(element); + error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1); + setCurrentlyValidatingElement(null); + } + if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) { + loggedTypeFailures[error$1.message] = true; + setCurrentlyValidatingElement(element); + error("Failed %s type: %s", location, error$1.message); + setCurrentlyValidatingElement(null); + } + } + } + } + } + function setCurrentlyValidatingElement$1(element) { + { + if (element) { + var owner = element._owner; + var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null); + setExtraStackFrame(stack); + } else { + setExtraStackFrame(null); + } + } + } + var propTypesMisspellWarningShown; + { + propTypesMisspellWarningShown = false; + } + function getDeclarationErrorAddendum() { + if (ReactCurrentOwner.current) { + var name = getComponentNameFromType(ReactCurrentOwner.current.type); + if (name) { + return "\n\nCheck the render method of `" + name + "`."; + } + } + return ""; + } + function getSourceInfoErrorAddendum(source) { + if (source !== void 0) { + var fileName = source.fileName.replace(/^.*[\\\/]/, ""); + var lineNumber = source.lineNumber; + return "\n\nCheck your code at " + fileName + ":" + lineNumber + "."; + } + return ""; + } + function getSourceInfoErrorAddendumForProps(elementProps) { + if (elementProps !== null && elementProps !== void 0) { + return getSourceInfoErrorAddendum(elementProps.__source); + } + return ""; + } + var ownerHasKeyUseWarning = {}; + function getCurrentComponentErrorInfo(parentType) { + var info = getDeclarationErrorAddendum(); + if (!info) { + var parentName = typeof parentType === "string" ? parentType : parentType.displayName || parentType.name; + if (parentName) { + info = "\n\nCheck the top-level render call using <" + parentName + ">."; + } + } + return info; + } + function validateExplicitKey(element, parentType) { + if (!element._store || element._store.validated || element.key != null) { + return; + } + element._store.validated = true; + var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType); + if (ownerHasKeyUseWarning[currentComponentErrorInfo]) { + return; + } + ownerHasKeyUseWarning[currentComponentErrorInfo] = true; + var childOwner = ""; + if (element && element._owner && element._owner !== ReactCurrentOwner.current) { + childOwner = " It was passed a child from " + getComponentNameFromType(element._owner.type) + "."; + } + { + setCurrentlyValidatingElement$1(element); + error('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner); + setCurrentlyValidatingElement$1(null); + } + } + function validateChildKeys(node, parentType) { + if (typeof node !== "object") { + return; + } + if (isArray(node)) { + for (var i = 0; i < node.length; i++) { + var child = node[i]; + if (isValidElement(child)) { + validateExplicitKey(child, parentType); + } + } + } else if (isValidElement(node)) { + if (node._store) { + node._store.validated = true; + } + } else if (node) { + var iteratorFn = getIteratorFn(node); + if (typeof iteratorFn === "function") { + if (iteratorFn !== node.entries) { + var iterator = iteratorFn.call(node); + var step; + while (!(step = iterator.next()).done) { + if (isValidElement(step.value)) { + validateExplicitKey(step.value, parentType); + } + } + } + } + } + } + function validatePropTypes(element) { + { + var type = element.type; + if (type === null || type === void 0 || typeof type === "string") { + return; + } + var propTypes; + if (typeof type === "function") { + propTypes = type.propTypes; + } else if (typeof type === "object" && (type.$$typeof === REACT_FORWARD_REF_TYPE || // Note: Memo only checks outer props here. + // Inner props are checked in the reconciler. + type.$$typeof === REACT_MEMO_TYPE)) { + propTypes = type.propTypes; + } else { + return; + } + if (propTypes) { + var name = getComponentNameFromType(type); + checkPropTypes(propTypes, element.props, "prop", name, element); + } else if (type.PropTypes !== void 0 && !propTypesMisspellWarningShown) { + propTypesMisspellWarningShown = true; + var _name = getComponentNameFromType(type); + error("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", _name || "Unknown"); + } + if (typeof type.getDefaultProps === "function" && !type.getDefaultProps.isReactClassApproved) { + error("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead."); + } + } + } + function validateFragmentProps(fragment) { + { + var keys = Object.keys(fragment.props); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (key !== "children" && key !== "key") { + setCurrentlyValidatingElement$1(fragment); + error("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", key); + setCurrentlyValidatingElement$1(null); + break; + } + } + if (fragment.ref !== null) { + setCurrentlyValidatingElement$1(fragment); + error("Invalid attribute `ref` supplied to `React.Fragment`."); + setCurrentlyValidatingElement$1(null); + } + } + } + function createElementWithValidation(type, props, children) { + var validType = isValidElementType(type); + if (!validType) { + var info = ""; + if (type === void 0 || typeof type === "object" && type !== null && Object.keys(type).length === 0) { + info += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports."; + } + var sourceInfo = getSourceInfoErrorAddendumForProps(props); + if (sourceInfo) { + info += sourceInfo; + } else { + info += getDeclarationErrorAddendum(); + } + var typeString; + if (type === null) { + typeString = "null"; + } else if (isArray(type)) { + typeString = "array"; + } else if (type !== void 0 && type.$$typeof === REACT_ELEMENT_TYPE) { + typeString = "<" + (getComponentNameFromType(type.type) || "Unknown") + " />"; + info = " Did you accidentally export a JSX literal instead of a component?"; + } else { + typeString = typeof type; + } + { + error("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", typeString, info); + } + } + var element = createElement.apply(this, arguments); + if (element == null) { + return element; + } + if (validType) { + for (var i = 2; i < arguments.length; i++) { + validateChildKeys(arguments[i], type); + } + } + if (type === REACT_FRAGMENT_TYPE) { + validateFragmentProps(element); + } else { + validatePropTypes(element); + } + return element; + } + var didWarnAboutDeprecatedCreateFactory = false; + function createFactoryWithValidation(type) { + var validatedFactory = createElementWithValidation.bind(null, type); + validatedFactory.type = type; + { + if (!didWarnAboutDeprecatedCreateFactory) { + didWarnAboutDeprecatedCreateFactory = true; + warn("React.createFactory() is deprecated and will be removed in a future major release. Consider using JSX or use React.createElement() directly instead."); + } + Object.defineProperty(validatedFactory, "type", { + enumerable: false, + get: function() { + warn("Factory.type is deprecated. Access the class directly before passing it to createFactory."); + Object.defineProperty(this, "type", { + value: type + }); + return type; + } + }); + } + return validatedFactory; + } + function cloneElementWithValidation(element, props, children) { + var newElement = cloneElement.apply(this, arguments); + for (var i = 2; i < arguments.length; i++) { + validateChildKeys(arguments[i], newElement.type); + } + validatePropTypes(newElement); + return newElement; + } + function startTransition(scope, options) { + var prevTransition = ReactCurrentBatchConfig.transition; + ReactCurrentBatchConfig.transition = {}; + var currentTransition = ReactCurrentBatchConfig.transition; + { + ReactCurrentBatchConfig.transition._updatedFibers = /* @__PURE__ */ new Set(); + } + try { + scope(); + } finally { + ReactCurrentBatchConfig.transition = prevTransition; + { + if (prevTransition === null && currentTransition._updatedFibers) { + var updatedFibersCount = currentTransition._updatedFibers.size; + if (updatedFibersCount > 10) { + warn("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table."); + } + currentTransition._updatedFibers.clear(); + } + } + } + } + var didWarnAboutMessageChannel = false; + var enqueueTaskImpl = null; + function enqueueTask(task) { + if (enqueueTaskImpl === null) { + try { + var requireString = ("require" + Math.random()).slice(0, 7); + var nodeRequire = module && module[requireString]; + enqueueTaskImpl = nodeRequire.call(module, "timers").setImmediate; + } catch (_err) { + enqueueTaskImpl = function(callback) { + { + if (didWarnAboutMessageChannel === false) { + didWarnAboutMessageChannel = true; + if (typeof MessageChannel === "undefined") { + error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."); + } + } + } + var channel = new MessageChannel(); + channel.port1.onmessage = callback; + channel.port2.postMessage(void 0); + }; + } + } + return enqueueTaskImpl(task); + } + var actScopeDepth = 0; + var didWarnNoAwaitAct = false; + function act(callback) { + { + var prevActScopeDepth = actScopeDepth; + actScopeDepth++; + if (ReactCurrentActQueue.current === null) { + ReactCurrentActQueue.current = []; + } + var prevIsBatchingLegacy = ReactCurrentActQueue.isBatchingLegacy; + var result; + try { + ReactCurrentActQueue.isBatchingLegacy = true; + result = callback(); + if (!prevIsBatchingLegacy && ReactCurrentActQueue.didScheduleLegacyUpdate) { + var queue = ReactCurrentActQueue.current; + if (queue !== null) { + ReactCurrentActQueue.didScheduleLegacyUpdate = false; + flushActQueue(queue); + } + } + } catch (error2) { + popActScope(prevActScopeDepth); + throw error2; + } finally { + ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy; + } + if (result !== null && typeof result === "object" && typeof result.then === "function") { + var thenableResult = result; + var wasAwaited = false; + var thenable = { + then: function(resolve, reject) { + wasAwaited = true; + thenableResult.then(function(returnValue2) { + popActScope(prevActScopeDepth); + if (actScopeDepth === 0) { + recursivelyFlushAsyncActWork(returnValue2, resolve, reject); + } else { + resolve(returnValue2); + } + }, function(error2) { + popActScope(prevActScopeDepth); + reject(error2); + }); + } + }; + { + if (!didWarnNoAwaitAct && typeof Promise !== "undefined") { + Promise.resolve().then(function() { + }).then(function() { + if (!wasAwaited) { + didWarnNoAwaitAct = true; + error("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"); + } + }); + } + } + return thenable; + } else { + var returnValue = result; + popActScope(prevActScopeDepth); + if (actScopeDepth === 0) { + var _queue = ReactCurrentActQueue.current; + if (_queue !== null) { + flushActQueue(_queue); + ReactCurrentActQueue.current = null; + } + var _thenable = { + then: function(resolve, reject) { + if (ReactCurrentActQueue.current === null) { + ReactCurrentActQueue.current = []; + recursivelyFlushAsyncActWork(returnValue, resolve, reject); + } else { + resolve(returnValue); + } + } + }; + return _thenable; + } else { + var _thenable2 = { + then: function(resolve, reject) { + resolve(returnValue); + } + }; + return _thenable2; + } + } + } + } + function popActScope(prevActScopeDepth) { + { + if (prevActScopeDepth !== actScopeDepth - 1) { + error("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. "); + } + actScopeDepth = prevActScopeDepth; + } + } + function recursivelyFlushAsyncActWork(returnValue, resolve, reject) { + { + var queue = ReactCurrentActQueue.current; + if (queue !== null) { + try { + flushActQueue(queue); + enqueueTask(function() { + if (queue.length === 0) { + ReactCurrentActQueue.current = null; + resolve(returnValue); + } else { + recursivelyFlushAsyncActWork(returnValue, resolve, reject); + } + }); + } catch (error2) { + reject(error2); + } + } else { + resolve(returnValue); + } + } + } + var isFlushing = false; + function flushActQueue(queue) { + { + if (!isFlushing) { + isFlushing = true; + var i = 0; + try { + for (; i < queue.length; i++) { + var callback = queue[i]; + do { + callback = callback(true); + } while (callback !== null); + } + queue.length = 0; + } catch (error2) { + queue = queue.slice(i + 1); + throw error2; + } finally { + isFlushing = false; + } + } + } + } + var createElement$1 = createElementWithValidation; + var cloneElement$1 = cloneElementWithValidation; + var createFactory = createFactoryWithValidation; + var Children = { + map: mapChildren, + forEach: forEachChildren, + count: countChildren, + toArray, + only: onlyChild + }; + exports.Children = Children; + exports.Component = Component; + exports.Fragment = REACT_FRAGMENT_TYPE; + exports.Profiler = REACT_PROFILER_TYPE; + exports.PureComponent = PureComponent; + exports.StrictMode = REACT_STRICT_MODE_TYPE; + exports.Suspense = REACT_SUSPENSE_TYPE; + exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactSharedInternals; + exports.act = act; + exports.cloneElement = cloneElement$1; + exports.createContext = createContext; + exports.createElement = createElement$1; + exports.createFactory = createFactory; + exports.createRef = createRef; + exports.forwardRef = forwardRef; + exports.isValidElement = isValidElement; + exports.lazy = lazy; + exports.memo = memo; + exports.startTransition = startTransition; + exports.unstable_act = act; + exports.useCallback = useCallback; + exports.useContext = useContext; + exports.useDebugValue = useDebugValue; + exports.useDeferredValue = useDeferredValue; + exports.useEffect = useEffect; + exports.useId = useId; + exports.useImperativeHandle = useImperativeHandle; + exports.useInsertionEffect = useInsertionEffect; + exports.useLayoutEffect = useLayoutEffect; + exports.useMemo = useMemo; + exports.useReducer = useReducer; + exports.useRef = useRef; + exports.useState = useState; + exports.useSyncExternalStore = useSyncExternalStore; + exports.useTransition = useTransition; + exports.version = ReactVersion; + if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined" && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop === "function") { + __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error()); + } + })(); + } + } +}); + +// node_modules/react/index.js +var require_react = __commonJS({ + "node_modules/react/index.js"(exports, module) { + if (false) { + module.exports = null; + } else { + module.exports = require_react_development(); + } + } +}); + +export { + __commonJS, + __export, + __toESM, + require_react +}; +/*! Bundled license information: + +react/cjs/react.development.js: + (** + * @license React + * react.development.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) +*/ +//# sourceMappingURL=chunk-2YIMICFJ.js.map diff --git a/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-2YIMICFJ.js.map b/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-2YIMICFJ.js.map new file mode 100644 index 0000000..3bb38a2 --- /dev/null +++ b/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-2YIMICFJ.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../react/cjs/react.development.js", "../../react/index.js"], + "sourcesContent": ["/**\n * @license React\n * react.development.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n'use strict';\n\nif (process.env.NODE_ENV !== \"production\") {\n (function() {\n\n 'use strict';\n\n/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */\nif (\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart ===\n 'function'\n) {\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());\n}\n var ReactVersion = '18.3.1';\n\n// ATTENTION\n// When adding new symbols to this file,\n// Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols'\n// The Symbol used to tag the ReactElement-like types.\nvar REACT_ELEMENT_TYPE = Symbol.for('react.element');\nvar REACT_PORTAL_TYPE = Symbol.for('react.portal');\nvar REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');\nvar REACT_STRICT_MODE_TYPE = Symbol.for('react.strict_mode');\nvar REACT_PROFILER_TYPE = Symbol.for('react.profiler');\nvar REACT_PROVIDER_TYPE = Symbol.for('react.provider');\nvar REACT_CONTEXT_TYPE = Symbol.for('react.context');\nvar REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref');\nvar REACT_SUSPENSE_TYPE = Symbol.for('react.suspense');\nvar REACT_SUSPENSE_LIST_TYPE = Symbol.for('react.suspense_list');\nvar REACT_MEMO_TYPE = Symbol.for('react.memo');\nvar REACT_LAZY_TYPE = Symbol.for('react.lazy');\nvar REACT_OFFSCREEN_TYPE = Symbol.for('react.offscreen');\nvar MAYBE_ITERATOR_SYMBOL = Symbol.iterator;\nvar FAUX_ITERATOR_SYMBOL = '@@iterator';\nfunction getIteratorFn(maybeIterable) {\n if (maybeIterable === null || typeof maybeIterable !== 'object') {\n return null;\n }\n\n var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];\n\n if (typeof maybeIterator === 'function') {\n return maybeIterator;\n }\n\n return null;\n}\n\n/**\n * Keeps track of the current dispatcher.\n */\nvar ReactCurrentDispatcher = {\n /**\n * @internal\n * @type {ReactComponent}\n */\n current: null\n};\n\n/**\n * Keeps track of the current batch's configuration such as how long an update\n * should suspend for if it needs to.\n */\nvar ReactCurrentBatchConfig = {\n transition: null\n};\n\nvar ReactCurrentActQueue = {\n current: null,\n // Used to reproduce behavior of `batchedUpdates` in legacy mode.\n isBatchingLegacy: false,\n didScheduleLegacyUpdate: false\n};\n\n/**\n * Keeps track of the current owner.\n *\n * The current owner is the component who should own any components that are\n * currently being constructed.\n */\nvar ReactCurrentOwner = {\n /**\n * @internal\n * @type {ReactComponent}\n */\n current: null\n};\n\nvar ReactDebugCurrentFrame = {};\nvar currentExtraStackFrame = null;\nfunction setExtraStackFrame(stack) {\n {\n currentExtraStackFrame = stack;\n }\n}\n\n{\n ReactDebugCurrentFrame.setExtraStackFrame = function (stack) {\n {\n currentExtraStackFrame = stack;\n }\n }; // Stack implementation injected by the current renderer.\n\n\n ReactDebugCurrentFrame.getCurrentStack = null;\n\n ReactDebugCurrentFrame.getStackAddendum = function () {\n var stack = ''; // Add an extra top frame while an element is being validated\n\n if (currentExtraStackFrame) {\n stack += currentExtraStackFrame;\n } // Delegate to the injected renderer-specific implementation\n\n\n var impl = ReactDebugCurrentFrame.getCurrentStack;\n\n if (impl) {\n stack += impl() || '';\n }\n\n return stack;\n };\n}\n\n// -----------------------------------------------------------------------------\n\nvar enableScopeAPI = false; // Experimental Create Event Handle API.\nvar enableCacheElement = false;\nvar enableTransitionTracing = false; // No known bugs, but needs performance testing\n\nvar enableLegacyHidden = false; // Enables unstable_avoidThisFallback feature in Fiber\n// stuff. Intended to enable React core members to more easily debug scheduling\n// issues in DEV builds.\n\nvar enableDebugTracing = false; // Track which Fiber(s) schedule render work.\n\nvar ReactSharedInternals = {\n ReactCurrentDispatcher: ReactCurrentDispatcher,\n ReactCurrentBatchConfig: ReactCurrentBatchConfig,\n ReactCurrentOwner: ReactCurrentOwner\n};\n\n{\n ReactSharedInternals.ReactDebugCurrentFrame = ReactDebugCurrentFrame;\n ReactSharedInternals.ReactCurrentActQueue = ReactCurrentActQueue;\n}\n\n// by calls to these methods by a Babel plugin.\n//\n// In PROD (or in packages without access to React internals),\n// they are left as they are instead.\n\nfunction warn(format) {\n {\n {\n for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n args[_key - 1] = arguments[_key];\n }\n\n printWarning('warn', format, args);\n }\n }\n}\nfunction error(format) {\n {\n {\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n args[_key2 - 1] = arguments[_key2];\n }\n\n printWarning('error', format, args);\n }\n }\n}\n\nfunction printWarning(level, format, args) {\n // When changing this logic, you might want to also\n // update consoleWithStackDev.www.js as well.\n {\n var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n var stack = ReactDebugCurrentFrame.getStackAddendum();\n\n if (stack !== '') {\n format += '%s';\n args = args.concat([stack]);\n } // eslint-disable-next-line react-internal/safe-string-coercion\n\n\n var argsWithFormat = args.map(function (item) {\n return String(item);\n }); // Careful: RN currently depends on this prefix\n\n argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it\n // breaks IE9: https://github.com/facebook/react/issues/13610\n // eslint-disable-next-line react-internal/no-production-logging\n\n Function.prototype.apply.call(console[level], console, argsWithFormat);\n }\n}\n\nvar didWarnStateUpdateForUnmountedComponent = {};\n\nfunction warnNoop(publicInstance, callerName) {\n {\n var _constructor = publicInstance.constructor;\n var componentName = _constructor && (_constructor.displayName || _constructor.name) || 'ReactClass';\n var warningKey = componentName + \".\" + callerName;\n\n if (didWarnStateUpdateForUnmountedComponent[warningKey]) {\n return;\n }\n\n error(\"Can't call %s on a component that is not yet mounted. \" + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {};` ' + 'class property with the desired state in the %s component.', callerName, componentName);\n\n didWarnStateUpdateForUnmountedComponent[warningKey] = true;\n }\n}\n/**\n * This is the abstract API for an update queue.\n */\n\n\nvar ReactNoopUpdateQueue = {\n /**\n * Checks whether or not this composite component is mounted.\n * @param {ReactClass} publicInstance The instance we want to test.\n * @return {boolean} True if mounted, false otherwise.\n * @protected\n * @final\n */\n isMounted: function (publicInstance) {\n return false;\n },\n\n /**\n * Forces an update. This should only be invoked when it is known with\n * certainty that we are **not** in a DOM transaction.\n *\n * You may want to call this when you know that some deeper aspect of the\n * component's state has changed but `setState` was not called.\n *\n * This will not invoke `shouldComponentUpdate`, but it will invoke\n * `componentWillUpdate` and `componentDidUpdate`.\n *\n * @param {ReactClass} publicInstance The instance that should rerender.\n * @param {?function} callback Called after component is updated.\n * @param {?string} callerName name of the calling function in the public API.\n * @internal\n */\n enqueueForceUpdate: function (publicInstance, callback, callerName) {\n warnNoop(publicInstance, 'forceUpdate');\n },\n\n /**\n * Replaces all of the state. Always use this or `setState` to mutate state.\n * You should treat `this.state` as immutable.\n *\n * There is no guarantee that `this.state` will be immediately updated, so\n * accessing `this.state` after calling this method may return the old value.\n *\n * @param {ReactClass} publicInstance The instance that should rerender.\n * @param {object} completeState Next state.\n * @param {?function} callback Called after component is updated.\n * @param {?string} callerName name of the calling function in the public API.\n * @internal\n */\n enqueueReplaceState: function (publicInstance, completeState, callback, callerName) {\n warnNoop(publicInstance, 'replaceState');\n },\n\n /**\n * Sets a subset of the state. This only exists because _pendingState is\n * internal. This provides a merging strategy that is not available to deep\n * properties which is confusing. TODO: Expose pendingState or don't use it\n * during the merge.\n *\n * @param {ReactClass} publicInstance The instance that should rerender.\n * @param {object} partialState Next partial state to be merged with state.\n * @param {?function} callback Called after component is updated.\n * @param {?string} Name of the calling function in the public API.\n * @internal\n */\n enqueueSetState: function (publicInstance, partialState, callback, callerName) {\n warnNoop(publicInstance, 'setState');\n }\n};\n\nvar assign = Object.assign;\n\nvar emptyObject = {};\n\n{\n Object.freeze(emptyObject);\n}\n/**\n * Base class helpers for the updating state of a component.\n */\n\n\nfunction Component(props, context, updater) {\n this.props = props;\n this.context = context; // If a component has string refs, we will assign a different object later.\n\n this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the\n // renderer.\n\n this.updater = updater || ReactNoopUpdateQueue;\n}\n\nComponent.prototype.isReactComponent = {};\n/**\n * Sets a subset of the state. Always use this to mutate\n * state. You should treat `this.state` as immutable.\n *\n * There is no guarantee that `this.state` will be immediately updated, so\n * accessing `this.state` after calling this method may return the old value.\n *\n * There is no guarantee that calls to `setState` will run synchronously,\n * as they may eventually be batched together. You can provide an optional\n * callback that will be executed when the call to setState is actually\n * completed.\n *\n * When a function is provided to setState, it will be called at some point in\n * the future (not synchronously). It will be called with the up to date\n * component arguments (state, props, context). These values can be different\n * from this.* because your function may be called after receiveProps but before\n * shouldComponentUpdate, and this new state, props, and context will not yet be\n * assigned to this.\n *\n * @param {object|function} partialState Next partial state or function to\n * produce next partial state to be merged with current state.\n * @param {?function} callback Called after state is updated.\n * @final\n * @protected\n */\n\nComponent.prototype.setState = function (partialState, callback) {\n if (typeof partialState !== 'object' && typeof partialState !== 'function' && partialState != null) {\n throw new Error('setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.');\n }\n\n this.updater.enqueueSetState(this, partialState, callback, 'setState');\n};\n/**\n * Forces an update. This should only be invoked when it is known with\n * certainty that we are **not** in a DOM transaction.\n *\n * You may want to call this when you know that some deeper aspect of the\n * component's state has changed but `setState` was not called.\n *\n * This will not invoke `shouldComponentUpdate`, but it will invoke\n * `componentWillUpdate` and `componentDidUpdate`.\n *\n * @param {?function} callback Called after update is complete.\n * @final\n * @protected\n */\n\n\nComponent.prototype.forceUpdate = function (callback) {\n this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');\n};\n/**\n * Deprecated APIs. These APIs used to exist on classic React classes but since\n * we would like to deprecate them, we're not going to move them over to this\n * modern base class. Instead, we define a getter that warns if it's accessed.\n */\n\n\n{\n var deprecatedAPIs = {\n isMounted: ['isMounted', 'Instead, make sure to clean up subscriptions and pending requests in ' + 'componentWillUnmount to prevent memory leaks.'],\n replaceState: ['replaceState', 'Refactor your code to use setState instead (see ' + 'https://github.com/facebook/react/issues/3236).']\n };\n\n var defineDeprecationWarning = function (methodName, info) {\n Object.defineProperty(Component.prototype, methodName, {\n get: function () {\n warn('%s(...) is deprecated in plain JavaScript React classes. %s', info[0], info[1]);\n\n return undefined;\n }\n });\n };\n\n for (var fnName in deprecatedAPIs) {\n if (deprecatedAPIs.hasOwnProperty(fnName)) {\n defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);\n }\n }\n}\n\nfunction ComponentDummy() {}\n\nComponentDummy.prototype = Component.prototype;\n/**\n * Convenience component with default shallow equality check for sCU.\n */\n\nfunction PureComponent(props, context, updater) {\n this.props = props;\n this.context = context; // If a component has string refs, we will assign a different object later.\n\n this.refs = emptyObject;\n this.updater = updater || ReactNoopUpdateQueue;\n}\n\nvar pureComponentPrototype = PureComponent.prototype = new ComponentDummy();\npureComponentPrototype.constructor = PureComponent; // Avoid an extra prototype jump for these methods.\n\nassign(pureComponentPrototype, Component.prototype);\npureComponentPrototype.isPureReactComponent = true;\n\n// an immutable object with a single mutable value\nfunction createRef() {\n var refObject = {\n current: null\n };\n\n {\n Object.seal(refObject);\n }\n\n return refObject;\n}\n\nvar isArrayImpl = Array.isArray; // eslint-disable-next-line no-redeclare\n\nfunction isArray(a) {\n return isArrayImpl(a);\n}\n\n/*\n * The `'' + value` pattern (used in in perf-sensitive code) throws for Symbol\n * and Temporal.* types. See https://github.com/facebook/react/pull/22064.\n *\n * The functions in this module will throw an easier-to-understand,\n * easier-to-debug exception with a clear errors message message explaining the\n * problem. (Instead of a confusing exception thrown inside the implementation\n * of the `value` object).\n */\n// $FlowFixMe only called in DEV, so void return is not possible.\nfunction typeName(value) {\n {\n // toStringTag is needed for namespaced types like Temporal.Instant\n var hasToStringTag = typeof Symbol === 'function' && Symbol.toStringTag;\n var type = hasToStringTag && value[Symbol.toStringTag] || value.constructor.name || 'Object';\n return type;\n }\n} // $FlowFixMe only called in DEV, so void return is not possible.\n\n\nfunction willCoercionThrow(value) {\n {\n try {\n testStringCoercion(value);\n return false;\n } catch (e) {\n return true;\n }\n }\n}\n\nfunction testStringCoercion(value) {\n // If you ended up here by following an exception call stack, here's what's\n // happened: you supplied an object or symbol value to React (as a prop, key,\n // DOM attribute, CSS property, string ref, etc.) and when React tried to\n // coerce it to a string using `'' + value`, an exception was thrown.\n //\n // The most common types that will cause this exception are `Symbol` instances\n // and Temporal objects like `Temporal.Instant`. But any object that has a\n // `valueOf` or `[Symbol.toPrimitive]` method that throws will also cause this\n // exception. (Library authors do this to prevent users from using built-in\n // numeric operators like `+` or comparison operators like `>=` because custom\n // methods are needed to perform accurate arithmetic or comparison.)\n //\n // To fix the problem, coerce this object or symbol value to a string before\n // passing it to React. The most reliable way is usually `String(value)`.\n //\n // To find which value is throwing, check the browser or debugger console.\n // Before this exception was thrown, there should be `console.error` output\n // that shows the type (Symbol, Temporal.PlainDate, etc.) that caused the\n // problem and how that type was used: key, atrribute, input value prop, etc.\n // In most cases, this console output also shows the component and its\n // ancestor components where the exception happened.\n //\n // eslint-disable-next-line react-internal/safe-string-coercion\n return '' + value;\n}\nfunction checkKeyStringCoercion(value) {\n {\n if (willCoercionThrow(value)) {\n error('The provided key is an unsupported type %s.' + ' This value must be coerced to a string before before using it here.', typeName(value));\n\n return testStringCoercion(value); // throw (to help callers find troubleshooting comments)\n }\n }\n}\n\nfunction getWrappedName(outerType, innerType, wrapperName) {\n var displayName = outerType.displayName;\n\n if (displayName) {\n return displayName;\n }\n\n var functionName = innerType.displayName || innerType.name || '';\n return functionName !== '' ? wrapperName + \"(\" + functionName + \")\" : wrapperName;\n} // Keep in sync with react-reconciler/getComponentNameFromFiber\n\n\nfunction getContextName(type) {\n return type.displayName || 'Context';\n} // Note that the reconciler package should generally prefer to use getComponentNameFromFiber() instead.\n\n\nfunction getComponentNameFromType(type) {\n if (type == null) {\n // Host root, text node or just invalid type.\n return null;\n }\n\n {\n if (typeof type.tag === 'number') {\n error('Received an unexpected object in getComponentNameFromType(). ' + 'This is likely a bug in React. Please file an issue.');\n }\n }\n\n if (typeof type === 'function') {\n return type.displayName || type.name || null;\n }\n\n if (typeof type === 'string') {\n return type;\n }\n\n switch (type) {\n case REACT_FRAGMENT_TYPE:\n return 'Fragment';\n\n case REACT_PORTAL_TYPE:\n return 'Portal';\n\n case REACT_PROFILER_TYPE:\n return 'Profiler';\n\n case REACT_STRICT_MODE_TYPE:\n return 'StrictMode';\n\n case REACT_SUSPENSE_TYPE:\n return 'Suspense';\n\n case REACT_SUSPENSE_LIST_TYPE:\n return 'SuspenseList';\n\n }\n\n if (typeof type === 'object') {\n switch (type.$$typeof) {\n case REACT_CONTEXT_TYPE:\n var context = type;\n return getContextName(context) + '.Consumer';\n\n case REACT_PROVIDER_TYPE:\n var provider = type;\n return getContextName(provider._context) + '.Provider';\n\n case REACT_FORWARD_REF_TYPE:\n return getWrappedName(type, type.render, 'ForwardRef');\n\n case REACT_MEMO_TYPE:\n var outerName = type.displayName || null;\n\n if (outerName !== null) {\n return outerName;\n }\n\n return getComponentNameFromType(type.type) || 'Memo';\n\n case REACT_LAZY_TYPE:\n {\n var lazyComponent = type;\n var payload = lazyComponent._payload;\n var init = lazyComponent._init;\n\n try {\n return getComponentNameFromType(init(payload));\n } catch (x) {\n return null;\n }\n }\n\n // eslint-disable-next-line no-fallthrough\n }\n }\n\n return null;\n}\n\nvar hasOwnProperty = Object.prototype.hasOwnProperty;\n\nvar RESERVED_PROPS = {\n key: true,\n ref: true,\n __self: true,\n __source: true\n};\nvar specialPropKeyWarningShown, specialPropRefWarningShown, didWarnAboutStringRefs;\n\n{\n didWarnAboutStringRefs = {};\n}\n\nfunction hasValidRef(config) {\n {\n if (hasOwnProperty.call(config, 'ref')) {\n var getter = Object.getOwnPropertyDescriptor(config, 'ref').get;\n\n if (getter && getter.isReactWarning) {\n return false;\n }\n }\n }\n\n return config.ref !== undefined;\n}\n\nfunction hasValidKey(config) {\n {\n if (hasOwnProperty.call(config, 'key')) {\n var getter = Object.getOwnPropertyDescriptor(config, 'key').get;\n\n if (getter && getter.isReactWarning) {\n return false;\n }\n }\n }\n\n return config.key !== undefined;\n}\n\nfunction defineKeyPropWarningGetter(props, displayName) {\n var warnAboutAccessingKey = function () {\n {\n if (!specialPropKeyWarningShown) {\n specialPropKeyWarningShown = true;\n\n error('%s: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://reactjs.org/link/special-props)', displayName);\n }\n }\n };\n\n warnAboutAccessingKey.isReactWarning = true;\n Object.defineProperty(props, 'key', {\n get: warnAboutAccessingKey,\n configurable: true\n });\n}\n\nfunction defineRefPropWarningGetter(props, displayName) {\n var warnAboutAccessingRef = function () {\n {\n if (!specialPropRefWarningShown) {\n specialPropRefWarningShown = true;\n\n error('%s: `ref` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://reactjs.org/link/special-props)', displayName);\n }\n }\n };\n\n warnAboutAccessingRef.isReactWarning = true;\n Object.defineProperty(props, 'ref', {\n get: warnAboutAccessingRef,\n configurable: true\n });\n}\n\nfunction warnIfStringRefCannotBeAutoConverted(config) {\n {\n if (typeof config.ref === 'string' && ReactCurrentOwner.current && config.__self && ReactCurrentOwner.current.stateNode !== config.__self) {\n var componentName = getComponentNameFromType(ReactCurrentOwner.current.type);\n\n if (!didWarnAboutStringRefs[componentName]) {\n error('Component \"%s\" contains the string ref \"%s\". ' + 'Support for string refs will be removed in a future major release. ' + 'This case cannot be automatically converted to an arrow function. ' + 'We ask you to manually fix this case by using useRef() or createRef() instead. ' + 'Learn more about using refs safely here: ' + 'https://reactjs.org/link/strict-mode-string-ref', componentName, config.ref);\n\n didWarnAboutStringRefs[componentName] = true;\n }\n }\n }\n}\n/**\n * Factory method to create a new React element. This no longer adheres to\n * the class pattern, so do not use new to call it. Also, instanceof check\n * will not work. Instead test $$typeof field against Symbol.for('react.element') to check\n * if something is a React Element.\n *\n * @param {*} type\n * @param {*} props\n * @param {*} key\n * @param {string|object} ref\n * @param {*} owner\n * @param {*} self A *temporary* helper to detect places where `this` is\n * different from the `owner` when React.createElement is called, so that we\n * can warn. We want to get rid of owner and replace string `ref`s with arrow\n * functions, and as long as `this` and owner are the same, there will be no\n * change in behavior.\n * @param {*} source An annotation object (added by a transpiler or otherwise)\n * indicating filename, line number, and/or other information.\n * @internal\n */\n\n\nvar ReactElement = function (type, key, ref, self, source, owner, props) {\n var element = {\n // This tag allows us to uniquely identify this as a React Element\n $$typeof: REACT_ELEMENT_TYPE,\n // Built-in properties that belong on the element\n type: type,\n key: key,\n ref: ref,\n props: props,\n // Record the component responsible for creating this element.\n _owner: owner\n };\n\n {\n // The validation flag is currently mutative. We put it on\n // an external backing store so that we can freeze the whole object.\n // This can be replaced with a WeakMap once they are implemented in\n // commonly used development environments.\n element._store = {}; // To make comparing ReactElements easier for testing purposes, we make\n // the validation flag non-enumerable (where possible, which should\n // include every environment we run tests in), so the test framework\n // ignores it.\n\n Object.defineProperty(element._store, 'validated', {\n configurable: false,\n enumerable: false,\n writable: true,\n value: false\n }); // self and source are DEV only properties.\n\n Object.defineProperty(element, '_self', {\n configurable: false,\n enumerable: false,\n writable: false,\n value: self\n }); // Two elements created in two different places should be considered\n // equal for testing purposes and therefore we hide it from enumeration.\n\n Object.defineProperty(element, '_source', {\n configurable: false,\n enumerable: false,\n writable: false,\n value: source\n });\n\n if (Object.freeze) {\n Object.freeze(element.props);\n Object.freeze(element);\n }\n }\n\n return element;\n};\n/**\n * Create and return a new ReactElement of the given type.\n * See https://reactjs.org/docs/react-api.html#createelement\n */\n\nfunction createElement(type, config, children) {\n var propName; // Reserved names are extracted\n\n var props = {};\n var key = null;\n var ref = null;\n var self = null;\n var source = null;\n\n if (config != null) {\n if (hasValidRef(config)) {\n ref = config.ref;\n\n {\n warnIfStringRefCannotBeAutoConverted(config);\n }\n }\n\n if (hasValidKey(config)) {\n {\n checkKeyStringCoercion(config.key);\n }\n\n key = '' + config.key;\n }\n\n self = config.__self === undefined ? null : config.__self;\n source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object\n\n for (propName in config) {\n if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {\n props[propName] = config[propName];\n }\n }\n } // Children can be more than one argument, and those are transferred onto\n // the newly allocated props object.\n\n\n var childrenLength = arguments.length - 2;\n\n if (childrenLength === 1) {\n props.children = children;\n } else if (childrenLength > 1) {\n var childArray = Array(childrenLength);\n\n for (var i = 0; i < childrenLength; i++) {\n childArray[i] = arguments[i + 2];\n }\n\n {\n if (Object.freeze) {\n Object.freeze(childArray);\n }\n }\n\n props.children = childArray;\n } // Resolve default props\n\n\n if (type && type.defaultProps) {\n var defaultProps = type.defaultProps;\n\n for (propName in defaultProps) {\n if (props[propName] === undefined) {\n props[propName] = defaultProps[propName];\n }\n }\n }\n\n {\n if (key || ref) {\n var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;\n\n if (key) {\n defineKeyPropWarningGetter(props, displayName);\n }\n\n if (ref) {\n defineRefPropWarningGetter(props, displayName);\n }\n }\n }\n\n return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);\n}\nfunction cloneAndReplaceKey(oldElement, newKey) {\n var newElement = ReactElement(oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props);\n return newElement;\n}\n/**\n * Clone and return a new ReactElement using element as the starting point.\n * See https://reactjs.org/docs/react-api.html#cloneelement\n */\n\nfunction cloneElement(element, config, children) {\n if (element === null || element === undefined) {\n throw new Error(\"React.cloneElement(...): The argument must be a React element, but you passed \" + element + \".\");\n }\n\n var propName; // Original props are copied\n\n var props = assign({}, element.props); // Reserved names are extracted\n\n var key = element.key;\n var ref = element.ref; // Self is preserved since the owner is preserved.\n\n var self = element._self; // Source is preserved since cloneElement is unlikely to be targeted by a\n // transpiler, and the original source is probably a better indicator of the\n // true owner.\n\n var source = element._source; // Owner will be preserved, unless ref is overridden\n\n var owner = element._owner;\n\n if (config != null) {\n if (hasValidRef(config)) {\n // Silently steal the ref from the parent.\n ref = config.ref;\n owner = ReactCurrentOwner.current;\n }\n\n if (hasValidKey(config)) {\n {\n checkKeyStringCoercion(config.key);\n }\n\n key = '' + config.key;\n } // Remaining properties override existing props\n\n\n var defaultProps;\n\n if (element.type && element.type.defaultProps) {\n defaultProps = element.type.defaultProps;\n }\n\n for (propName in config) {\n if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {\n if (config[propName] === undefined && defaultProps !== undefined) {\n // Resolve default props\n props[propName] = defaultProps[propName];\n } else {\n props[propName] = config[propName];\n }\n }\n }\n } // Children can be more than one argument, and those are transferred onto\n // the newly allocated props object.\n\n\n var childrenLength = arguments.length - 2;\n\n if (childrenLength === 1) {\n props.children = children;\n } else if (childrenLength > 1) {\n var childArray = Array(childrenLength);\n\n for (var i = 0; i < childrenLength; i++) {\n childArray[i] = arguments[i + 2];\n }\n\n props.children = childArray;\n }\n\n return ReactElement(element.type, key, ref, self, source, owner, props);\n}\n/**\n * Verifies the object is a ReactElement.\n * See https://reactjs.org/docs/react-api.html#isvalidelement\n * @param {?object} object\n * @return {boolean} True if `object` is a ReactElement.\n * @final\n */\n\nfunction isValidElement(object) {\n return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;\n}\n\nvar SEPARATOR = '.';\nvar SUBSEPARATOR = ':';\n/**\n * Escape and wrap key so it is safe to use as a reactid\n *\n * @param {string} key to be escaped.\n * @return {string} the escaped key.\n */\n\nfunction escape(key) {\n var escapeRegex = /[=:]/g;\n var escaperLookup = {\n '=': '=0',\n ':': '=2'\n };\n var escapedString = key.replace(escapeRegex, function (match) {\n return escaperLookup[match];\n });\n return '$' + escapedString;\n}\n/**\n * TODO: Test that a single child and an array with one item have the same key\n * pattern.\n */\n\n\nvar didWarnAboutMaps = false;\nvar userProvidedKeyEscapeRegex = /\\/+/g;\n\nfunction escapeUserProvidedKey(text) {\n return text.replace(userProvidedKeyEscapeRegex, '$&/');\n}\n/**\n * Generate a key string that identifies a element within a set.\n *\n * @param {*} element A element that could contain a manual key.\n * @param {number} index Index that is used if a manual key is not provided.\n * @return {string}\n */\n\n\nfunction getElementKey(element, index) {\n // Do some typechecking here since we call this blindly. We want to ensure\n // that we don't block potential future ES APIs.\n if (typeof element === 'object' && element !== null && element.key != null) {\n // Explicit key\n {\n checkKeyStringCoercion(element.key);\n }\n\n return escape('' + element.key);\n } // Implicit key determined by the index in the set\n\n\n return index.toString(36);\n}\n\nfunction mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {\n var type = typeof children;\n\n if (type === 'undefined' || type === 'boolean') {\n // All of the above are perceived as null.\n children = null;\n }\n\n var invokeCallback = false;\n\n if (children === null) {\n invokeCallback = true;\n } else {\n switch (type) {\n case 'string':\n case 'number':\n invokeCallback = true;\n break;\n\n case 'object':\n switch (children.$$typeof) {\n case REACT_ELEMENT_TYPE:\n case REACT_PORTAL_TYPE:\n invokeCallback = true;\n }\n\n }\n }\n\n if (invokeCallback) {\n var _child = children;\n var mappedChild = callback(_child); // If it's the only child, treat the name as if it was wrapped in an array\n // so that it's consistent if the number of children grows:\n\n var childKey = nameSoFar === '' ? SEPARATOR + getElementKey(_child, 0) : nameSoFar;\n\n if (isArray(mappedChild)) {\n var escapedChildKey = '';\n\n if (childKey != null) {\n escapedChildKey = escapeUserProvidedKey(childKey) + '/';\n }\n\n mapIntoArray(mappedChild, array, escapedChildKey, '', function (c) {\n return c;\n });\n } else if (mappedChild != null) {\n if (isValidElement(mappedChild)) {\n {\n // The `if` statement here prevents auto-disabling of the safe\n // coercion ESLint rule, so we must manually disable it below.\n // $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key\n if (mappedChild.key && (!_child || _child.key !== mappedChild.key)) {\n checkKeyStringCoercion(mappedChild.key);\n }\n }\n\n mappedChild = cloneAndReplaceKey(mappedChild, // Keep both the (mapped) and old keys if they differ, just as\n // traverseAllChildren used to do for objects as children\n escapedPrefix + ( // $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key\n mappedChild.key && (!_child || _child.key !== mappedChild.key) ? // $FlowFixMe Flow incorrectly thinks existing element's key can be a number\n // eslint-disable-next-line react-internal/safe-string-coercion\n escapeUserProvidedKey('' + mappedChild.key) + '/' : '') + childKey);\n }\n\n array.push(mappedChild);\n }\n\n return 1;\n }\n\n var child;\n var nextName;\n var subtreeCount = 0; // Count of children found in the current subtree.\n\n var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;\n\n if (isArray(children)) {\n for (var i = 0; i < children.length; i++) {\n child = children[i];\n nextName = nextNamePrefix + getElementKey(child, i);\n subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback);\n }\n } else {\n var iteratorFn = getIteratorFn(children);\n\n if (typeof iteratorFn === 'function') {\n var iterableChildren = children;\n\n {\n // Warn about using Maps as children\n if (iteratorFn === iterableChildren.entries) {\n if (!didWarnAboutMaps) {\n warn('Using Maps as children is not supported. ' + 'Use an array of keyed ReactElements instead.');\n }\n\n didWarnAboutMaps = true;\n }\n }\n\n var iterator = iteratorFn.call(iterableChildren);\n var step;\n var ii = 0;\n\n while (!(step = iterator.next()).done) {\n child = step.value;\n nextName = nextNamePrefix + getElementKey(child, ii++);\n subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback);\n }\n } else if (type === 'object') {\n // eslint-disable-next-line react-internal/safe-string-coercion\n var childrenString = String(children);\n throw new Error(\"Objects are not valid as a React child (found: \" + (childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString) + \"). \" + 'If you meant to render a collection of children, use an array ' + 'instead.');\n }\n }\n\n return subtreeCount;\n}\n\n/**\n * Maps children that are typically specified as `props.children`.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrenmap\n *\n * The provided mapFunction(child, index) will be called for each\n * leaf child.\n *\n * @param {?*} children Children tree container.\n * @param {function(*, int)} func The map function.\n * @param {*} context Context for mapFunction.\n * @return {object} Object containing the ordered map of results.\n */\nfunction mapChildren(children, func, context) {\n if (children == null) {\n return children;\n }\n\n var result = [];\n var count = 0;\n mapIntoArray(children, result, '', '', function (child) {\n return func.call(context, child, count++);\n });\n return result;\n}\n/**\n * Count the number of children that are typically specified as\n * `props.children`.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrencount\n *\n * @param {?*} children Children tree container.\n * @return {number} The number of children.\n */\n\n\nfunction countChildren(children) {\n var n = 0;\n mapChildren(children, function () {\n n++; // Don't return anything\n });\n return n;\n}\n\n/**\n * Iterates through children that are typically specified as `props.children`.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrenforeach\n *\n * The provided forEachFunc(child, index) will be called for each\n * leaf child.\n *\n * @param {?*} children Children tree container.\n * @param {function(*, int)} forEachFunc\n * @param {*} forEachContext Context for forEachContext.\n */\nfunction forEachChildren(children, forEachFunc, forEachContext) {\n mapChildren(children, function () {\n forEachFunc.apply(this, arguments); // Don't return anything.\n }, forEachContext);\n}\n/**\n * Flatten a children object (typically specified as `props.children`) and\n * return an array with appropriately re-keyed children.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrentoarray\n */\n\n\nfunction toArray(children) {\n return mapChildren(children, function (child) {\n return child;\n }) || [];\n}\n/**\n * Returns the first child in a collection of children and verifies that there\n * is only one child in the collection.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrenonly\n *\n * The current implementation of this function assumes that a single child gets\n * passed without a wrapper, but the purpose of this helper function is to\n * abstract away the particular structure of children.\n *\n * @param {?object} children Child collection structure.\n * @return {ReactElement} The first and only `ReactElement` contained in the\n * structure.\n */\n\n\nfunction onlyChild(children) {\n if (!isValidElement(children)) {\n throw new Error('React.Children.only expected to receive a single React element child.');\n }\n\n return children;\n}\n\nfunction createContext(defaultValue) {\n // TODO: Second argument used to be an optional `calculateChangedBits`\n // function. Warn to reserve for future use?\n var context = {\n $$typeof: REACT_CONTEXT_TYPE,\n // As a workaround to support multiple concurrent renderers, we categorize\n // some renderers as primary and others as secondary. We only expect\n // there to be two concurrent renderers at most: React Native (primary) and\n // Fabric (secondary); React DOM (primary) and React ART (secondary).\n // Secondary renderers store their context values on separate fields.\n _currentValue: defaultValue,\n _currentValue2: defaultValue,\n // Used to track how many concurrent renderers this context currently\n // supports within in a single renderer. Such as parallel server rendering.\n _threadCount: 0,\n // These are circular\n Provider: null,\n Consumer: null,\n // Add these to use same hidden class in VM as ServerContext\n _defaultValue: null,\n _globalName: null\n };\n context.Provider = {\n $$typeof: REACT_PROVIDER_TYPE,\n _context: context\n };\n var hasWarnedAboutUsingNestedContextConsumers = false;\n var hasWarnedAboutUsingConsumerProvider = false;\n var hasWarnedAboutDisplayNameOnConsumer = false;\n\n {\n // A separate object, but proxies back to the original context object for\n // backwards compatibility. It has a different $$typeof, so we can properly\n // warn for the incorrect usage of Context as a Consumer.\n var Consumer = {\n $$typeof: REACT_CONTEXT_TYPE,\n _context: context\n }; // $FlowFixMe: Flow complains about not setting a value, which is intentional here\n\n Object.defineProperties(Consumer, {\n Provider: {\n get: function () {\n if (!hasWarnedAboutUsingConsumerProvider) {\n hasWarnedAboutUsingConsumerProvider = true;\n\n error('Rendering is not supported and will be removed in ' + 'a future major release. Did you mean to render instead?');\n }\n\n return context.Provider;\n },\n set: function (_Provider) {\n context.Provider = _Provider;\n }\n },\n _currentValue: {\n get: function () {\n return context._currentValue;\n },\n set: function (_currentValue) {\n context._currentValue = _currentValue;\n }\n },\n _currentValue2: {\n get: function () {\n return context._currentValue2;\n },\n set: function (_currentValue2) {\n context._currentValue2 = _currentValue2;\n }\n },\n _threadCount: {\n get: function () {\n return context._threadCount;\n },\n set: function (_threadCount) {\n context._threadCount = _threadCount;\n }\n },\n Consumer: {\n get: function () {\n if (!hasWarnedAboutUsingNestedContextConsumers) {\n hasWarnedAboutUsingNestedContextConsumers = true;\n\n error('Rendering is not supported and will be removed in ' + 'a future major release. Did you mean to render instead?');\n }\n\n return context.Consumer;\n }\n },\n displayName: {\n get: function () {\n return context.displayName;\n },\n set: function (displayName) {\n if (!hasWarnedAboutDisplayNameOnConsumer) {\n warn('Setting `displayName` on Context.Consumer has no effect. ' + \"You should set it directly on the context with Context.displayName = '%s'.\", displayName);\n\n hasWarnedAboutDisplayNameOnConsumer = true;\n }\n }\n }\n }); // $FlowFixMe: Flow complains about missing properties because it doesn't understand defineProperty\n\n context.Consumer = Consumer;\n }\n\n {\n context._currentRenderer = null;\n context._currentRenderer2 = null;\n }\n\n return context;\n}\n\nvar Uninitialized = -1;\nvar Pending = 0;\nvar Resolved = 1;\nvar Rejected = 2;\n\nfunction lazyInitializer(payload) {\n if (payload._status === Uninitialized) {\n var ctor = payload._result;\n var thenable = ctor(); // Transition to the next state.\n // This might throw either because it's missing or throws. If so, we treat it\n // as still uninitialized and try again next time. Which is the same as what\n // happens if the ctor or any wrappers processing the ctor throws. This might\n // end up fixing it if the resolution was a concurrency bug.\n\n thenable.then(function (moduleObject) {\n if (payload._status === Pending || payload._status === Uninitialized) {\n // Transition to the next state.\n var resolved = payload;\n resolved._status = Resolved;\n resolved._result = moduleObject;\n }\n }, function (error) {\n if (payload._status === Pending || payload._status === Uninitialized) {\n // Transition to the next state.\n var rejected = payload;\n rejected._status = Rejected;\n rejected._result = error;\n }\n });\n\n if (payload._status === Uninitialized) {\n // In case, we're still uninitialized, then we're waiting for the thenable\n // to resolve. Set it as pending in the meantime.\n var pending = payload;\n pending._status = Pending;\n pending._result = thenable;\n }\n }\n\n if (payload._status === Resolved) {\n var moduleObject = payload._result;\n\n {\n if (moduleObject === undefined) {\n error('lazy: Expected the result of a dynamic imp' + 'ort() call. ' + 'Instead received: %s\\n\\nYour code should look like: \\n ' + // Break up imports to avoid accidentally parsing them as dependencies.\n 'const MyComponent = lazy(() => imp' + \"ort('./MyComponent'))\\n\\n\" + 'Did you accidentally put curly braces around the import?', moduleObject);\n }\n }\n\n {\n if (!('default' in moduleObject)) {\n error('lazy: Expected the result of a dynamic imp' + 'ort() call. ' + 'Instead received: %s\\n\\nYour code should look like: \\n ' + // Break up imports to avoid accidentally parsing them as dependencies.\n 'const MyComponent = lazy(() => imp' + \"ort('./MyComponent'))\", moduleObject);\n }\n }\n\n return moduleObject.default;\n } else {\n throw payload._result;\n }\n}\n\nfunction lazy(ctor) {\n var payload = {\n // We use these fields to store the result.\n _status: Uninitialized,\n _result: ctor\n };\n var lazyType = {\n $$typeof: REACT_LAZY_TYPE,\n _payload: payload,\n _init: lazyInitializer\n };\n\n {\n // In production, this would just set it on the object.\n var defaultProps;\n var propTypes; // $FlowFixMe\n\n Object.defineProperties(lazyType, {\n defaultProps: {\n configurable: true,\n get: function () {\n return defaultProps;\n },\n set: function (newDefaultProps) {\n error('React.lazy(...): It is not supported to assign `defaultProps` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');\n\n defaultProps = newDefaultProps; // Match production behavior more closely:\n // $FlowFixMe\n\n Object.defineProperty(lazyType, 'defaultProps', {\n enumerable: true\n });\n }\n },\n propTypes: {\n configurable: true,\n get: function () {\n return propTypes;\n },\n set: function (newPropTypes) {\n error('React.lazy(...): It is not supported to assign `propTypes` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');\n\n propTypes = newPropTypes; // Match production behavior more closely:\n // $FlowFixMe\n\n Object.defineProperty(lazyType, 'propTypes', {\n enumerable: true\n });\n }\n }\n });\n }\n\n return lazyType;\n}\n\nfunction forwardRef(render) {\n {\n if (render != null && render.$$typeof === REACT_MEMO_TYPE) {\n error('forwardRef requires a render function but received a `memo` ' + 'component. Instead of forwardRef(memo(...)), use ' + 'memo(forwardRef(...)).');\n } else if (typeof render !== 'function') {\n error('forwardRef requires a render function but was given %s.', render === null ? 'null' : typeof render);\n } else {\n if (render.length !== 0 && render.length !== 2) {\n error('forwardRef render functions accept exactly two parameters: props and ref. %s', render.length === 1 ? 'Did you forget to use the ref parameter?' : 'Any additional parameter will be undefined.');\n }\n }\n\n if (render != null) {\n if (render.defaultProps != null || render.propTypes != null) {\n error('forwardRef render functions do not support propTypes or defaultProps. ' + 'Did you accidentally pass a React component?');\n }\n }\n }\n\n var elementType = {\n $$typeof: REACT_FORWARD_REF_TYPE,\n render: render\n };\n\n {\n var ownName;\n Object.defineProperty(elementType, 'displayName', {\n enumerable: false,\n configurable: true,\n get: function () {\n return ownName;\n },\n set: function (name) {\n ownName = name; // The inner component shouldn't inherit this display name in most cases,\n // because the component may be used elsewhere.\n // But it's nice for anonymous functions to inherit the name,\n // so that our component-stack generation logic will display their frames.\n // An anonymous function generally suggests a pattern like:\n // React.forwardRef((props, ref) => {...});\n // This kind of inner function is not used elsewhere so the side effect is okay.\n\n if (!render.name && !render.displayName) {\n render.displayName = name;\n }\n }\n });\n }\n\n return elementType;\n}\n\nvar REACT_MODULE_REFERENCE;\n\n{\n REACT_MODULE_REFERENCE = Symbol.for('react.module.reference');\n}\n\nfunction isValidElementType(type) {\n if (typeof type === 'string' || typeof type === 'function') {\n return true;\n } // Note: typeof might be other than 'symbol' or 'number' (e.g. if it's a polyfill).\n\n\n if (type === REACT_FRAGMENT_TYPE || type === REACT_PROFILER_TYPE || enableDebugTracing || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || enableLegacyHidden || type === REACT_OFFSCREEN_TYPE || enableScopeAPI || enableCacheElement || enableTransitionTracing ) {\n return true;\n }\n\n if (typeof type === 'object' && type !== null) {\n if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object\n // types supported by any Flight configuration anywhere since\n // we don't know which Flight build this will end up being used\n // with.\n type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== undefined) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction memo(type, compare) {\n {\n if (!isValidElementType(type)) {\n error('memo: The first argument must be a component. Instead ' + 'received: %s', type === null ? 'null' : typeof type);\n }\n }\n\n var elementType = {\n $$typeof: REACT_MEMO_TYPE,\n type: type,\n compare: compare === undefined ? null : compare\n };\n\n {\n var ownName;\n Object.defineProperty(elementType, 'displayName', {\n enumerable: false,\n configurable: true,\n get: function () {\n return ownName;\n },\n set: function (name) {\n ownName = name; // The inner component shouldn't inherit this display name in most cases,\n // because the component may be used elsewhere.\n // But it's nice for anonymous functions to inherit the name,\n // so that our component-stack generation logic will display their frames.\n // An anonymous function generally suggests a pattern like:\n // React.memo((props) => {...});\n // This kind of inner function is not used elsewhere so the side effect is okay.\n\n if (!type.name && !type.displayName) {\n type.displayName = name;\n }\n }\n });\n }\n\n return elementType;\n}\n\nfunction resolveDispatcher() {\n var dispatcher = ReactCurrentDispatcher.current;\n\n {\n if (dispatcher === null) {\n error('Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\\n' + '2. You might be breaking the Rules of Hooks\\n' + '3. You might have more than one copy of React in the same app\\n' + 'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.');\n }\n } // Will result in a null access error if accessed outside render phase. We\n // intentionally don't throw our own error because this is in a hot path.\n // Also helps ensure this is inlined.\n\n\n return dispatcher;\n}\nfunction useContext(Context) {\n var dispatcher = resolveDispatcher();\n\n {\n // TODO: add a more generic warning for invalid values.\n if (Context._context !== undefined) {\n var realContext = Context._context; // Don't deduplicate because this legitimately causes bugs\n // and nobody should be using this in existing code.\n\n if (realContext.Consumer === Context) {\n error('Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' + 'removed in a future major release. Did you mean to call useContext(Context) instead?');\n } else if (realContext.Provider === Context) {\n error('Calling useContext(Context.Provider) is not supported. ' + 'Did you mean to call useContext(Context) instead?');\n }\n }\n }\n\n return dispatcher.useContext(Context);\n}\nfunction useState(initialState) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useState(initialState);\n}\nfunction useReducer(reducer, initialArg, init) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useReducer(reducer, initialArg, init);\n}\nfunction useRef(initialValue) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useRef(initialValue);\n}\nfunction useEffect(create, deps) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useEffect(create, deps);\n}\nfunction useInsertionEffect(create, deps) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useInsertionEffect(create, deps);\n}\nfunction useLayoutEffect(create, deps) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useLayoutEffect(create, deps);\n}\nfunction useCallback(callback, deps) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useCallback(callback, deps);\n}\nfunction useMemo(create, deps) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useMemo(create, deps);\n}\nfunction useImperativeHandle(ref, create, deps) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useImperativeHandle(ref, create, deps);\n}\nfunction useDebugValue(value, formatterFn) {\n {\n var dispatcher = resolveDispatcher();\n return dispatcher.useDebugValue(value, formatterFn);\n }\n}\nfunction useTransition() {\n var dispatcher = resolveDispatcher();\n return dispatcher.useTransition();\n}\nfunction useDeferredValue(value) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useDeferredValue(value);\n}\nfunction useId() {\n var dispatcher = resolveDispatcher();\n return dispatcher.useId();\n}\nfunction useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {\n var dispatcher = resolveDispatcher();\n return dispatcher.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\n// Helpers to patch console.logs to avoid logging during side-effect free\n// replaying on render function. This currently only patches the object\n// lazily which won't cover if the log function was extracted eagerly.\n// We could also eagerly patch the method.\nvar disabledDepth = 0;\nvar prevLog;\nvar prevInfo;\nvar prevWarn;\nvar prevError;\nvar prevGroup;\nvar prevGroupCollapsed;\nvar prevGroupEnd;\n\nfunction disabledLog() {}\n\ndisabledLog.__reactDisabledLog = true;\nfunction disableLogs() {\n {\n if (disabledDepth === 0) {\n /* eslint-disable react-internal/no-production-logging */\n prevLog = console.log;\n prevInfo = console.info;\n prevWarn = console.warn;\n prevError = console.error;\n prevGroup = console.group;\n prevGroupCollapsed = console.groupCollapsed;\n prevGroupEnd = console.groupEnd; // https://github.com/facebook/react/issues/19099\n\n var props = {\n configurable: true,\n enumerable: true,\n value: disabledLog,\n writable: true\n }; // $FlowFixMe Flow thinks console is immutable.\n\n Object.defineProperties(console, {\n info: props,\n log: props,\n warn: props,\n error: props,\n group: props,\n groupCollapsed: props,\n groupEnd: props\n });\n /* eslint-enable react-internal/no-production-logging */\n }\n\n disabledDepth++;\n }\n}\nfunction reenableLogs() {\n {\n disabledDepth--;\n\n if (disabledDepth === 0) {\n /* eslint-disable react-internal/no-production-logging */\n var props = {\n configurable: true,\n enumerable: true,\n writable: true\n }; // $FlowFixMe Flow thinks console is immutable.\n\n Object.defineProperties(console, {\n log: assign({}, props, {\n value: prevLog\n }),\n info: assign({}, props, {\n value: prevInfo\n }),\n warn: assign({}, props, {\n value: prevWarn\n }),\n error: assign({}, props, {\n value: prevError\n }),\n group: assign({}, props, {\n value: prevGroup\n }),\n groupCollapsed: assign({}, props, {\n value: prevGroupCollapsed\n }),\n groupEnd: assign({}, props, {\n value: prevGroupEnd\n })\n });\n /* eslint-enable react-internal/no-production-logging */\n }\n\n if (disabledDepth < 0) {\n error('disabledDepth fell below zero. ' + 'This is a bug in React. Please file an issue.');\n }\n }\n}\n\nvar ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher;\nvar prefix;\nfunction describeBuiltInComponentFrame(name, source, ownerFn) {\n {\n if (prefix === undefined) {\n // Extract the VM specific prefix used by each line.\n try {\n throw Error();\n } catch (x) {\n var match = x.stack.trim().match(/\\n( *(at )?)/);\n prefix = match && match[1] || '';\n }\n } // We use the prefix to ensure our stacks line up with native stack frames.\n\n\n return '\\n' + prefix + name;\n }\n}\nvar reentry = false;\nvar componentFrameCache;\n\n{\n var PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;\n componentFrameCache = new PossiblyWeakMap();\n}\n\nfunction describeNativeComponentFrame(fn, construct) {\n // If something asked for a stack inside a fake render, it should get ignored.\n if ( !fn || reentry) {\n return '';\n }\n\n {\n var frame = componentFrameCache.get(fn);\n\n if (frame !== undefined) {\n return frame;\n }\n }\n\n var control;\n reentry = true;\n var previousPrepareStackTrace = Error.prepareStackTrace; // $FlowFixMe It does accept undefined.\n\n Error.prepareStackTrace = undefined;\n var previousDispatcher;\n\n {\n previousDispatcher = ReactCurrentDispatcher$1.current; // Set the dispatcher in DEV because this might be call in the render function\n // for warnings.\n\n ReactCurrentDispatcher$1.current = null;\n disableLogs();\n }\n\n try {\n // This should throw.\n if (construct) {\n // Something should be setting the props in the constructor.\n var Fake = function () {\n throw Error();\n }; // $FlowFixMe\n\n\n Object.defineProperty(Fake.prototype, 'props', {\n set: function () {\n // We use a throwing setter instead of frozen or non-writable props\n // because that won't throw in a non-strict mode function.\n throw Error();\n }\n });\n\n if (typeof Reflect === 'object' && Reflect.construct) {\n // We construct a different control for this case to include any extra\n // frames added by the construct call.\n try {\n Reflect.construct(Fake, []);\n } catch (x) {\n control = x;\n }\n\n Reflect.construct(fn, [], Fake);\n } else {\n try {\n Fake.call();\n } catch (x) {\n control = x;\n }\n\n fn.call(Fake.prototype);\n }\n } else {\n try {\n throw Error();\n } catch (x) {\n control = x;\n }\n\n fn();\n }\n } catch (sample) {\n // This is inlined manually because closure doesn't do it for us.\n if (sample && control && typeof sample.stack === 'string') {\n // This extracts the first frame from the sample that isn't also in the control.\n // Skipping one frame that we assume is the frame that calls the two.\n var sampleLines = sample.stack.split('\\n');\n var controlLines = control.stack.split('\\n');\n var s = sampleLines.length - 1;\n var c = controlLines.length - 1;\n\n while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {\n // We expect at least one stack frame to be shared.\n // Typically this will be the root most one. However, stack frames may be\n // cut off due to maximum stack limits. In this case, one maybe cut off\n // earlier than the other. We assume that the sample is longer or the same\n // and there for cut off earlier. So we should find the root most frame in\n // the sample somewhere in the control.\n c--;\n }\n\n for (; s >= 1 && c >= 0; s--, c--) {\n // Next we find the first one that isn't the same which should be the\n // frame that called our sample function and the control.\n if (sampleLines[s] !== controlLines[c]) {\n // In V8, the first line is describing the message but other VMs don't.\n // If we're about to return the first line, and the control is also on the same\n // line, that's a pretty good indicator that our sample threw at same line as\n // the control. I.e. before we entered the sample frame. So we ignore this result.\n // This can happen if you passed a class to function component, or non-function.\n if (s !== 1 || c !== 1) {\n do {\n s--;\n c--; // We may still have similar intermediate frames from the construct call.\n // The next one that isn't the same should be our match though.\n\n if (c < 0 || sampleLines[s] !== controlLines[c]) {\n // V8 adds a \"new\" prefix for native classes. Let's remove it to make it prettier.\n var _frame = '\\n' + sampleLines[s].replace(' at new ', ' at '); // If our component frame is labeled \"\"\n // but we have a user-provided \"displayName\"\n // splice it in to make the stack more readable.\n\n\n if (fn.displayName && _frame.includes('')) {\n _frame = _frame.replace('', fn.displayName);\n }\n\n {\n if (typeof fn === 'function') {\n componentFrameCache.set(fn, _frame);\n }\n } // Return the line we found.\n\n\n return _frame;\n }\n } while (s >= 1 && c >= 0);\n }\n\n break;\n }\n }\n }\n } finally {\n reentry = false;\n\n {\n ReactCurrentDispatcher$1.current = previousDispatcher;\n reenableLogs();\n }\n\n Error.prepareStackTrace = previousPrepareStackTrace;\n } // Fallback to just using the name if we couldn't make it throw.\n\n\n var name = fn ? fn.displayName || fn.name : '';\n var syntheticFrame = name ? describeBuiltInComponentFrame(name) : '';\n\n {\n if (typeof fn === 'function') {\n componentFrameCache.set(fn, syntheticFrame);\n }\n }\n\n return syntheticFrame;\n}\nfunction describeFunctionComponentFrame(fn, source, ownerFn) {\n {\n return describeNativeComponentFrame(fn, false);\n }\n}\n\nfunction shouldConstruct(Component) {\n var prototype = Component.prototype;\n return !!(prototype && prototype.isReactComponent);\n}\n\nfunction describeUnknownElementTypeFrameInDEV(type, source, ownerFn) {\n\n if (type == null) {\n return '';\n }\n\n if (typeof type === 'function') {\n {\n return describeNativeComponentFrame(type, shouldConstruct(type));\n }\n }\n\n if (typeof type === 'string') {\n return describeBuiltInComponentFrame(type);\n }\n\n switch (type) {\n case REACT_SUSPENSE_TYPE:\n return describeBuiltInComponentFrame('Suspense');\n\n case REACT_SUSPENSE_LIST_TYPE:\n return describeBuiltInComponentFrame('SuspenseList');\n }\n\n if (typeof type === 'object') {\n switch (type.$$typeof) {\n case REACT_FORWARD_REF_TYPE:\n return describeFunctionComponentFrame(type.render);\n\n case REACT_MEMO_TYPE:\n // Memo may contain any component type so we recursively resolve it.\n return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn);\n\n case REACT_LAZY_TYPE:\n {\n var lazyComponent = type;\n var payload = lazyComponent._payload;\n var init = lazyComponent._init;\n\n try {\n // Lazy may contain any component type so we recursively resolve it.\n return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn);\n } catch (x) {}\n }\n }\n }\n\n return '';\n}\n\nvar loggedTypeFailures = {};\nvar ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame;\n\nfunction setCurrentlyValidatingElement(element) {\n {\n if (element) {\n var owner = element._owner;\n var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);\n ReactDebugCurrentFrame$1.setExtraStackFrame(stack);\n } else {\n ReactDebugCurrentFrame$1.setExtraStackFrame(null);\n }\n }\n}\n\nfunction checkPropTypes(typeSpecs, values, location, componentName, element) {\n {\n // $FlowFixMe This is okay but Flow doesn't know it.\n var has = Function.call.bind(hasOwnProperty);\n\n for (var typeSpecName in typeSpecs) {\n if (has(typeSpecs, typeSpecName)) {\n var error$1 = void 0; // Prop type validation may throw. In case they do, we don't want to\n // fail the render phase where it didn't fail before. So we log it.\n // After these have been cleaned up, we'll let them throw.\n\n try {\n // This is intentionally an invariant that gets caught. It's the same\n // behavior as without this statement except with a better message.\n if (typeof typeSpecs[typeSpecName] !== 'function') {\n // eslint-disable-next-line react-internal/prod-error-codes\n var err = Error((componentName || 'React class') + ': ' + location + ' type `' + typeSpecName + '` is invalid; ' + 'it must be a function, usually from the `prop-types` package, but received `' + typeof typeSpecs[typeSpecName] + '`.' + 'This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.');\n err.name = 'Invariant Violation';\n throw err;\n }\n\n error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED');\n } catch (ex) {\n error$1 = ex;\n }\n\n if (error$1 && !(error$1 instanceof Error)) {\n setCurrentlyValidatingElement(element);\n\n error('%s: type specification of %s' + ' `%s` is invalid; the type checker ' + 'function must return `null` or an `Error` but returned a %s. ' + 'You may have forgotten to pass an argument to the type checker ' + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 'shape all require an argument).', componentName || 'React class', location, typeSpecName, typeof error$1);\n\n setCurrentlyValidatingElement(null);\n }\n\n if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {\n // Only monitor this failure once because there tends to be a lot of the\n // same error.\n loggedTypeFailures[error$1.message] = true;\n setCurrentlyValidatingElement(element);\n\n error('Failed %s type: %s', location, error$1.message);\n\n setCurrentlyValidatingElement(null);\n }\n }\n }\n }\n}\n\nfunction setCurrentlyValidatingElement$1(element) {\n {\n if (element) {\n var owner = element._owner;\n var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);\n setExtraStackFrame(stack);\n } else {\n setExtraStackFrame(null);\n }\n }\n}\n\nvar propTypesMisspellWarningShown;\n\n{\n propTypesMisspellWarningShown = false;\n}\n\nfunction getDeclarationErrorAddendum() {\n if (ReactCurrentOwner.current) {\n var name = getComponentNameFromType(ReactCurrentOwner.current.type);\n\n if (name) {\n return '\\n\\nCheck the render method of `' + name + '`.';\n }\n }\n\n return '';\n}\n\nfunction getSourceInfoErrorAddendum(source) {\n if (source !== undefined) {\n var fileName = source.fileName.replace(/^.*[\\\\\\/]/, '');\n var lineNumber = source.lineNumber;\n return '\\n\\nCheck your code at ' + fileName + ':' + lineNumber + '.';\n }\n\n return '';\n}\n\nfunction getSourceInfoErrorAddendumForProps(elementProps) {\n if (elementProps !== null && elementProps !== undefined) {\n return getSourceInfoErrorAddendum(elementProps.__source);\n }\n\n return '';\n}\n/**\n * Warn if there's no key explicitly set on dynamic arrays of children or\n * object keys are not valid. This allows us to keep track of children between\n * updates.\n */\n\n\nvar ownerHasKeyUseWarning = {};\n\nfunction getCurrentComponentErrorInfo(parentType) {\n var info = getDeclarationErrorAddendum();\n\n if (!info) {\n var parentName = typeof parentType === 'string' ? parentType : parentType.displayName || parentType.name;\n\n if (parentName) {\n info = \"\\n\\nCheck the top-level render call using <\" + parentName + \">.\";\n }\n }\n\n return info;\n}\n/**\n * Warn if the element doesn't have an explicit key assigned to it.\n * This element is in an array. The array could grow and shrink or be\n * reordered. All children that haven't already been validated are required to\n * have a \"key\" property assigned to it. Error statuses are cached so a warning\n * will only be shown once.\n *\n * @internal\n * @param {ReactElement} element Element that requires a key.\n * @param {*} parentType element's parent's type.\n */\n\n\nfunction validateExplicitKey(element, parentType) {\n if (!element._store || element._store.validated || element.key != null) {\n return;\n }\n\n element._store.validated = true;\n var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);\n\n if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {\n return;\n }\n\n ownerHasKeyUseWarning[currentComponentErrorInfo] = true; // Usually the current owner is the offender, but if it accepts children as a\n // property, it may be the creator of the child that's responsible for\n // assigning it a key.\n\n var childOwner = '';\n\n if (element && element._owner && element._owner !== ReactCurrentOwner.current) {\n // Give the component that originally created this child.\n childOwner = \" It was passed a child from \" + getComponentNameFromType(element._owner.type) + \".\";\n }\n\n {\n setCurrentlyValidatingElement$1(element);\n\n error('Each child in a list should have a unique \"key\" prop.' + '%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner);\n\n setCurrentlyValidatingElement$1(null);\n }\n}\n/**\n * Ensure that every element either is passed in a static location, in an\n * array with an explicit keys property defined, or in an object literal\n * with valid key property.\n *\n * @internal\n * @param {ReactNode} node Statically passed child of any type.\n * @param {*} parentType node's parent's type.\n */\n\n\nfunction validateChildKeys(node, parentType) {\n if (typeof node !== 'object') {\n return;\n }\n\n if (isArray(node)) {\n for (var i = 0; i < node.length; i++) {\n var child = node[i];\n\n if (isValidElement(child)) {\n validateExplicitKey(child, parentType);\n }\n }\n } else if (isValidElement(node)) {\n // This element was passed in a valid location.\n if (node._store) {\n node._store.validated = true;\n }\n } else if (node) {\n var iteratorFn = getIteratorFn(node);\n\n if (typeof iteratorFn === 'function') {\n // Entry iterators used to provide implicit keys,\n // but now we print a separate warning for them later.\n if (iteratorFn !== node.entries) {\n var iterator = iteratorFn.call(node);\n var step;\n\n while (!(step = iterator.next()).done) {\n if (isValidElement(step.value)) {\n validateExplicitKey(step.value, parentType);\n }\n }\n }\n }\n }\n}\n/**\n * Given an element, validate that its props follow the propTypes definition,\n * provided by the type.\n *\n * @param {ReactElement} element\n */\n\n\nfunction validatePropTypes(element) {\n {\n var type = element.type;\n\n if (type === null || type === undefined || typeof type === 'string') {\n return;\n }\n\n var propTypes;\n\n if (typeof type === 'function') {\n propTypes = type.propTypes;\n } else if (typeof type === 'object' && (type.$$typeof === REACT_FORWARD_REF_TYPE || // Note: Memo only checks outer props here.\n // Inner props are checked in the reconciler.\n type.$$typeof === REACT_MEMO_TYPE)) {\n propTypes = type.propTypes;\n } else {\n return;\n }\n\n if (propTypes) {\n // Intentionally inside to avoid triggering lazy initializers:\n var name = getComponentNameFromType(type);\n checkPropTypes(propTypes, element.props, 'prop', name, element);\n } else if (type.PropTypes !== undefined && !propTypesMisspellWarningShown) {\n propTypesMisspellWarningShown = true; // Intentionally inside to avoid triggering lazy initializers:\n\n var _name = getComponentNameFromType(type);\n\n error('Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?', _name || 'Unknown');\n }\n\n if (typeof type.getDefaultProps === 'function' && !type.getDefaultProps.isReactClassApproved) {\n error('getDefaultProps is only used on classic React.createClass ' + 'definitions. Use a static property named `defaultProps` instead.');\n }\n }\n}\n/**\n * Given a fragment, validate that it can only be provided with fragment props\n * @param {ReactElement} fragment\n */\n\n\nfunction validateFragmentProps(fragment) {\n {\n var keys = Object.keys(fragment.props);\n\n for (var i = 0; i < keys.length; i++) {\n var key = keys[i];\n\n if (key !== 'children' && key !== 'key') {\n setCurrentlyValidatingElement$1(fragment);\n\n error('Invalid prop `%s` supplied to `React.Fragment`. ' + 'React.Fragment can only have `key` and `children` props.', key);\n\n setCurrentlyValidatingElement$1(null);\n break;\n }\n }\n\n if (fragment.ref !== null) {\n setCurrentlyValidatingElement$1(fragment);\n\n error('Invalid attribute `ref` supplied to `React.Fragment`.');\n\n setCurrentlyValidatingElement$1(null);\n }\n }\n}\nfunction createElementWithValidation(type, props, children) {\n var validType = isValidElementType(type); // We warn in this case but don't throw. We expect the element creation to\n // succeed and there will likely be errors in render.\n\n if (!validType) {\n var info = '';\n\n if (type === undefined || typeof type === 'object' && type !== null && Object.keys(type).length === 0) {\n info += ' You likely forgot to export your component from the file ' + \"it's defined in, or you might have mixed up default and named imports.\";\n }\n\n var sourceInfo = getSourceInfoErrorAddendumForProps(props);\n\n if (sourceInfo) {\n info += sourceInfo;\n } else {\n info += getDeclarationErrorAddendum();\n }\n\n var typeString;\n\n if (type === null) {\n typeString = 'null';\n } else if (isArray(type)) {\n typeString = 'array';\n } else if (type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE) {\n typeString = \"<\" + (getComponentNameFromType(type.type) || 'Unknown') + \" />\";\n info = ' Did you accidentally export a JSX literal instead of a component?';\n } else {\n typeString = typeof type;\n }\n\n {\n error('React.createElement: type is invalid -- expected a string (for ' + 'built-in components) or a class/function (for composite ' + 'components) but got: %s.%s', typeString, info);\n }\n }\n\n var element = createElement.apply(this, arguments); // The result can be nullish if a mock or a custom function is used.\n // TODO: Drop this when these are no longer allowed as the type argument.\n\n if (element == null) {\n return element;\n } // Skip key warning if the type isn't valid since our key validation logic\n // doesn't expect a non-string/function type and can throw confusing errors.\n // We don't want exception behavior to differ between dev and prod.\n // (Rendering will throw with a helpful message and as soon as the type is\n // fixed, the key warnings will appear.)\n\n\n if (validType) {\n for (var i = 2; i < arguments.length; i++) {\n validateChildKeys(arguments[i], type);\n }\n }\n\n if (type === REACT_FRAGMENT_TYPE) {\n validateFragmentProps(element);\n } else {\n validatePropTypes(element);\n }\n\n return element;\n}\nvar didWarnAboutDeprecatedCreateFactory = false;\nfunction createFactoryWithValidation(type) {\n var validatedFactory = createElementWithValidation.bind(null, type);\n validatedFactory.type = type;\n\n {\n if (!didWarnAboutDeprecatedCreateFactory) {\n didWarnAboutDeprecatedCreateFactory = true;\n\n warn('React.createFactory() is deprecated and will be removed in ' + 'a future major release. Consider using JSX ' + 'or use React.createElement() directly instead.');\n } // Legacy hook: remove it\n\n\n Object.defineProperty(validatedFactory, 'type', {\n enumerable: false,\n get: function () {\n warn('Factory.type is deprecated. Access the class directly ' + 'before passing it to createFactory.');\n\n Object.defineProperty(this, 'type', {\n value: type\n });\n return type;\n }\n });\n }\n\n return validatedFactory;\n}\nfunction cloneElementWithValidation(element, props, children) {\n var newElement = cloneElement.apply(this, arguments);\n\n for (var i = 2; i < arguments.length; i++) {\n validateChildKeys(arguments[i], newElement.type);\n }\n\n validatePropTypes(newElement);\n return newElement;\n}\n\nfunction startTransition(scope, options) {\n var prevTransition = ReactCurrentBatchConfig.transition;\n ReactCurrentBatchConfig.transition = {};\n var currentTransition = ReactCurrentBatchConfig.transition;\n\n {\n ReactCurrentBatchConfig.transition._updatedFibers = new Set();\n }\n\n try {\n scope();\n } finally {\n ReactCurrentBatchConfig.transition = prevTransition;\n\n {\n if (prevTransition === null && currentTransition._updatedFibers) {\n var updatedFibersCount = currentTransition._updatedFibers.size;\n\n if (updatedFibersCount > 10) {\n warn('Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + 'Otherwise concurrent mode guarantees are off the table.');\n }\n\n currentTransition._updatedFibers.clear();\n }\n }\n }\n}\n\nvar didWarnAboutMessageChannel = false;\nvar enqueueTaskImpl = null;\nfunction enqueueTask(task) {\n if (enqueueTaskImpl === null) {\n try {\n // read require off the module object to get around the bundlers.\n // we don't want them to detect a require and bundle a Node polyfill.\n var requireString = ('require' + Math.random()).slice(0, 7);\n var nodeRequire = module && module[requireString]; // assuming we're in node, let's try to get node's\n // version of setImmediate, bypassing fake timers if any.\n\n enqueueTaskImpl = nodeRequire.call(module, 'timers').setImmediate;\n } catch (_err) {\n // we're in a browser\n // we can't use regular timers because they may still be faked\n // so we try MessageChannel+postMessage instead\n enqueueTaskImpl = function (callback) {\n {\n if (didWarnAboutMessageChannel === false) {\n didWarnAboutMessageChannel = true;\n\n if (typeof MessageChannel === 'undefined') {\n error('This browser does not have a MessageChannel implementation, ' + 'so enqueuing tasks via await act(async () => ...) will fail. ' + 'Please file an issue at https://github.com/facebook/react/issues ' + 'if you encounter this warning.');\n }\n }\n }\n\n var channel = new MessageChannel();\n channel.port1.onmessage = callback;\n channel.port2.postMessage(undefined);\n };\n }\n }\n\n return enqueueTaskImpl(task);\n}\n\nvar actScopeDepth = 0;\nvar didWarnNoAwaitAct = false;\nfunction act(callback) {\n {\n // `act` calls can be nested, so we track the depth. This represents the\n // number of `act` scopes on the stack.\n var prevActScopeDepth = actScopeDepth;\n actScopeDepth++;\n\n if (ReactCurrentActQueue.current === null) {\n // This is the outermost `act` scope. Initialize the queue. The reconciler\n // will detect the queue and use it instead of Scheduler.\n ReactCurrentActQueue.current = [];\n }\n\n var prevIsBatchingLegacy = ReactCurrentActQueue.isBatchingLegacy;\n var result;\n\n try {\n // Used to reproduce behavior of `batchedUpdates` in legacy mode. Only\n // set to `true` while the given callback is executed, not for updates\n // triggered during an async event, because this is how the legacy\n // implementation of `act` behaved.\n ReactCurrentActQueue.isBatchingLegacy = true;\n result = callback(); // Replicate behavior of original `act` implementation in legacy mode,\n // which flushed updates immediately after the scope function exits, even\n // if it's an async function.\n\n if (!prevIsBatchingLegacy && ReactCurrentActQueue.didScheduleLegacyUpdate) {\n var queue = ReactCurrentActQueue.current;\n\n if (queue !== null) {\n ReactCurrentActQueue.didScheduleLegacyUpdate = false;\n flushActQueue(queue);\n }\n }\n } catch (error) {\n popActScope(prevActScopeDepth);\n throw error;\n } finally {\n ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy;\n }\n\n if (result !== null && typeof result === 'object' && typeof result.then === 'function') {\n var thenableResult = result; // The callback is an async function (i.e. returned a promise). Wait\n // for it to resolve before exiting the current scope.\n\n var wasAwaited = false;\n var thenable = {\n then: function (resolve, reject) {\n wasAwaited = true;\n thenableResult.then(function (returnValue) {\n popActScope(prevActScopeDepth);\n\n if (actScopeDepth === 0) {\n // We've exited the outermost act scope. Recursively flush the\n // queue until there's no remaining work.\n recursivelyFlushAsyncActWork(returnValue, resolve, reject);\n } else {\n resolve(returnValue);\n }\n }, function (error) {\n // The callback threw an error.\n popActScope(prevActScopeDepth);\n reject(error);\n });\n }\n };\n\n {\n if (!didWarnNoAwaitAct && typeof Promise !== 'undefined') {\n // eslint-disable-next-line no-undef\n Promise.resolve().then(function () {}).then(function () {\n if (!wasAwaited) {\n didWarnNoAwaitAct = true;\n\n error('You called act(async () => ...) without await. ' + 'This could lead to unexpected testing behaviour, ' + 'interleaving multiple act calls and mixing their ' + 'scopes. ' + 'You should - await act(async () => ...);');\n }\n });\n }\n }\n\n return thenable;\n } else {\n var returnValue = result; // The callback is not an async function. Exit the current scope\n // immediately, without awaiting.\n\n popActScope(prevActScopeDepth);\n\n if (actScopeDepth === 0) {\n // Exiting the outermost act scope. Flush the queue.\n var _queue = ReactCurrentActQueue.current;\n\n if (_queue !== null) {\n flushActQueue(_queue);\n ReactCurrentActQueue.current = null;\n } // Return a thenable. If the user awaits it, we'll flush again in\n // case additional work was scheduled by a microtask.\n\n\n var _thenable = {\n then: function (resolve, reject) {\n // Confirm we haven't re-entered another `act` scope, in case\n // the user does something weird like await the thenable\n // multiple times.\n if (ReactCurrentActQueue.current === null) {\n // Recursively flush the queue until there's no remaining work.\n ReactCurrentActQueue.current = [];\n recursivelyFlushAsyncActWork(returnValue, resolve, reject);\n } else {\n resolve(returnValue);\n }\n }\n };\n return _thenable;\n } else {\n // Since we're inside a nested `act` scope, the returned thenable\n // immediately resolves. The outer scope will flush the queue.\n var _thenable2 = {\n then: function (resolve, reject) {\n resolve(returnValue);\n }\n };\n return _thenable2;\n }\n }\n }\n}\n\nfunction popActScope(prevActScopeDepth) {\n {\n if (prevActScopeDepth !== actScopeDepth - 1) {\n error('You seem to have overlapping act() calls, this is not supported. ' + 'Be sure to await previous act() calls before making a new one. ');\n }\n\n actScopeDepth = prevActScopeDepth;\n }\n}\n\nfunction recursivelyFlushAsyncActWork(returnValue, resolve, reject) {\n {\n var queue = ReactCurrentActQueue.current;\n\n if (queue !== null) {\n try {\n flushActQueue(queue);\n enqueueTask(function () {\n if (queue.length === 0) {\n // No additional work was scheduled. Finish.\n ReactCurrentActQueue.current = null;\n resolve(returnValue);\n } else {\n // Keep flushing work until there's none left.\n recursivelyFlushAsyncActWork(returnValue, resolve, reject);\n }\n });\n } catch (error) {\n reject(error);\n }\n } else {\n resolve(returnValue);\n }\n }\n}\n\nvar isFlushing = false;\n\nfunction flushActQueue(queue) {\n {\n if (!isFlushing) {\n // Prevent re-entrance.\n isFlushing = true;\n var i = 0;\n\n try {\n for (; i < queue.length; i++) {\n var callback = queue[i];\n\n do {\n callback = callback(true);\n } while (callback !== null);\n }\n\n queue.length = 0;\n } catch (error) {\n // If something throws, leave the remaining callbacks on the queue.\n queue = queue.slice(i + 1);\n throw error;\n } finally {\n isFlushing = false;\n }\n }\n }\n}\n\nvar createElement$1 = createElementWithValidation ;\nvar cloneElement$1 = cloneElementWithValidation ;\nvar createFactory = createFactoryWithValidation ;\nvar Children = {\n map: mapChildren,\n forEach: forEachChildren,\n count: countChildren,\n toArray: toArray,\n only: onlyChild\n};\n\nexports.Children = Children;\nexports.Component = Component;\nexports.Fragment = REACT_FRAGMENT_TYPE;\nexports.Profiler = REACT_PROFILER_TYPE;\nexports.PureComponent = PureComponent;\nexports.StrictMode = REACT_STRICT_MODE_TYPE;\nexports.Suspense = REACT_SUSPENSE_TYPE;\nexports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactSharedInternals;\nexports.act = act;\nexports.cloneElement = cloneElement$1;\nexports.createContext = createContext;\nexports.createElement = createElement$1;\nexports.createFactory = createFactory;\nexports.createRef = createRef;\nexports.forwardRef = forwardRef;\nexports.isValidElement = isValidElement;\nexports.lazy = lazy;\nexports.memo = memo;\nexports.startTransition = startTransition;\nexports.unstable_act = act;\nexports.useCallback = useCallback;\nexports.useContext = useContext;\nexports.useDebugValue = useDebugValue;\nexports.useDeferredValue = useDeferredValue;\nexports.useEffect = useEffect;\nexports.useId = useId;\nexports.useImperativeHandle = useImperativeHandle;\nexports.useInsertionEffect = useInsertionEffect;\nexports.useLayoutEffect = useLayoutEffect;\nexports.useMemo = useMemo;\nexports.useReducer = useReducer;\nexports.useRef = useRef;\nexports.useState = useState;\nexports.useSyncExternalStore = useSyncExternalStore;\nexports.useTransition = useTransition;\nexports.version = ReactVersion;\n /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */\nif (\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop ===\n 'function'\n) {\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error());\n}\n \n })();\n}\n", "'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/react.production.min.js');\n} else {\n module.exports = require('./cjs/react.development.js');\n}\n"], + "mappings": "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", + "names": ["ReactDebugCurrentFrame", "moduleObject", "error", "Component", "returnValue"] +} diff --git a/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-BCXODTBQ.js b/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-BCXODTBQ.js new file mode 100644 index 0000000..15b401e --- /dev/null +++ b/frontend/node_modules/.vite/deps_temp_a4301e0f/chunk-BCXODTBQ.js @@ -0,0 +1,21626 @@ +import { + __commonJS, + require_react +} from "./chunk-2YIMICFJ.js"; + +// node_modules/scheduler/cjs/scheduler.development.js +var require_scheduler_development = __commonJS({ + "node_modules/scheduler/cjs/scheduler.development.js"(exports) { + "use strict"; + if (true) { + (function() { + "use strict"; + if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined" && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart === "function") { + __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error()); + } + var enableSchedulerDebugging = false; + var enableProfiling = false; + var frameYieldMs = 5; + function push(heap, node) { + var index = heap.length; + heap.push(node); + siftUp(heap, node, index); + } + function peek(heap) { + return heap.length === 0 ? null : heap[0]; + } + function pop(heap) { + if (heap.length === 0) { + return null; + } + var first = heap[0]; + var last = heap.pop(); + if (last !== first) { + heap[0] = last; + siftDown(heap, last, 0); + } + return first; + } + function siftUp(heap, node, i) { + var index = i; + while (index > 0) { + var parentIndex = index - 1 >>> 1; + var parent = heap[parentIndex]; + if (compare(parent, node) > 0) { + heap[parentIndex] = node; + heap[index] = parent; + index = parentIndex; + } else { + return; + } + } + } + function siftDown(heap, node, i) { + var index = i; + var length = heap.length; + var halfLength = length >>> 1; + while (index < halfLength) { + var leftIndex = (index + 1) * 2 - 1; + var left = heap[leftIndex]; + var rightIndex = leftIndex + 1; + var right = heap[rightIndex]; + if (compare(left, node) < 0) { + if (rightIndex < length && compare(right, left) < 0) { + heap[index] = right; + heap[rightIndex] = node; + index = rightIndex; + } else { + heap[index] = left; + heap[leftIndex] = node; + index = leftIndex; + } + } else if (rightIndex < length && compare(right, node) < 0) { + heap[index] = right; + heap[rightIndex] = node; + index = rightIndex; + } else { + return; + } + } + } + function compare(a, b) { + var diff = a.sortIndex - b.sortIndex; + return diff !== 0 ? diff : a.id - b.id; + } + var ImmediatePriority = 1; + var UserBlockingPriority = 2; + var NormalPriority = 3; + var LowPriority = 4; + var IdlePriority = 5; + function markTaskErrored(task, ms) { + } + var hasPerformanceNow = typeof performance === "object" && typeof performance.now === "function"; + if (hasPerformanceNow) { + var localPerformance = performance; + exports.unstable_now = function() { + return localPerformance.now(); + }; + } else { + var localDate = Date; + var initialTime = localDate.now(); + exports.unstable_now = function() { + return localDate.now() - initialTime; + }; + } + var maxSigned31BitInt = 1073741823; + var IMMEDIATE_PRIORITY_TIMEOUT = -1; + var USER_BLOCKING_PRIORITY_TIMEOUT = 250; + var NORMAL_PRIORITY_TIMEOUT = 5e3; + var LOW_PRIORITY_TIMEOUT = 1e4; + var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; + var taskQueue = []; + var timerQueue = []; + var taskIdCounter = 1; + var currentTask = null; + var currentPriorityLevel = NormalPriority; + var isPerformingWork = false; + var isHostCallbackScheduled = false; + var isHostTimeoutScheduled = false; + var localSetTimeout = typeof setTimeout === "function" ? setTimeout : null; + var localClearTimeout = typeof clearTimeout === "function" ? clearTimeout : null; + var localSetImmediate = typeof setImmediate !== "undefined" ? setImmediate : null; + var isInputPending = typeof navigator !== "undefined" && navigator.scheduling !== void 0 && navigator.scheduling.isInputPending !== void 0 ? navigator.scheduling.isInputPending.bind(navigator.scheduling) : null; + function advanceTimers(currentTime) { + var timer = peek(timerQueue); + while (timer !== null) { + if (timer.callback === null) { + pop(timerQueue); + } else if (timer.startTime <= currentTime) { + pop(timerQueue); + timer.sortIndex = timer.expirationTime; + push(taskQueue, timer); + } else { + return; + } + timer = peek(timerQueue); + } + } + function handleTimeout(currentTime) { + isHostTimeoutScheduled = false; + advanceTimers(currentTime); + if (!isHostCallbackScheduled) { + if (peek(taskQueue) !== null) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } else { + var firstTimer = peek(timerQueue); + if (firstTimer !== null) { + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); + } + } + } + } + function flushWork(hasTimeRemaining, initialTime2) { + isHostCallbackScheduled = false; + if (isHostTimeoutScheduled) { + isHostTimeoutScheduled = false; + cancelHostTimeout(); + } + isPerformingWork = true; + var previousPriorityLevel = currentPriorityLevel; + try { + if (enableProfiling) { + try { + return workLoop(hasTimeRemaining, initialTime2); + } catch (error) { + if (currentTask !== null) { + var currentTime = exports.unstable_now(); + markTaskErrored(currentTask, currentTime); + currentTask.isQueued = false; + } + throw error; + } + } else { + return workLoop(hasTimeRemaining, initialTime2); + } + } finally { + currentTask = null; + currentPriorityLevel = previousPriorityLevel; + isPerformingWork = false; + } + } + function workLoop(hasTimeRemaining, initialTime2) { + var currentTime = initialTime2; + advanceTimers(currentTime); + currentTask = peek(taskQueue); + while (currentTask !== null && !enableSchedulerDebugging) { + if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) { + break; + } + var callback = currentTask.callback; + if (typeof callback === "function") { + currentTask.callback = null; + currentPriorityLevel = currentTask.priorityLevel; + var didUserCallbackTimeout = currentTask.expirationTime <= currentTime; + var continuationCallback = callback(didUserCallbackTimeout); + currentTime = exports.unstable_now(); + if (typeof continuationCallback === "function") { + currentTask.callback = continuationCallback; + } else { + if (currentTask === peek(taskQueue)) { + pop(taskQueue); + } + } + advanceTimers(currentTime); + } else { + pop(taskQueue); + } + currentTask = peek(taskQueue); + } + if (currentTask !== null) { + return true; + } else { + var firstTimer = peek(timerQueue); + if (firstTimer !== null) { + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); + } + return false; + } + } + function unstable_runWithPriority(priorityLevel, eventHandler) { + switch (priorityLevel) { + case ImmediatePriority: + case UserBlockingPriority: + case NormalPriority: + case LowPriority: + case IdlePriority: + break; + default: + priorityLevel = NormalPriority; + } + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = priorityLevel; + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + } + } + function unstable_next(eventHandler) { + var priorityLevel; + switch (currentPriorityLevel) { + case ImmediatePriority: + case UserBlockingPriority: + case NormalPriority: + priorityLevel = NormalPriority; + break; + default: + priorityLevel = currentPriorityLevel; + break; + } + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = priorityLevel; + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + } + } + function unstable_wrapCallback(callback) { + var parentPriorityLevel = currentPriorityLevel; + return function() { + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = parentPriorityLevel; + try { + return callback.apply(this, arguments); + } finally { + currentPriorityLevel = previousPriorityLevel; + } + }; + } + function unstable_scheduleCallback(priorityLevel, callback, options) { + var currentTime = exports.unstable_now(); + var startTime2; + if (typeof options === "object" && options !== null) { + var delay = options.delay; + if (typeof delay === "number" && delay > 0) { + startTime2 = currentTime + delay; + } else { + startTime2 = currentTime; + } + } else { + startTime2 = currentTime; + } + var timeout; + switch (priorityLevel) { + case ImmediatePriority: + timeout = IMMEDIATE_PRIORITY_TIMEOUT; + break; + case UserBlockingPriority: + timeout = USER_BLOCKING_PRIORITY_TIMEOUT; + break; + case IdlePriority: + timeout = IDLE_PRIORITY_TIMEOUT; + break; + case LowPriority: + timeout = LOW_PRIORITY_TIMEOUT; + break; + case NormalPriority: + default: + timeout = NORMAL_PRIORITY_TIMEOUT; + break; + } + var expirationTime = startTime2 + timeout; + var newTask = { + id: taskIdCounter++, + callback, + priorityLevel, + startTime: startTime2, + expirationTime, + sortIndex: -1 + }; + if (startTime2 > currentTime) { + newTask.sortIndex = startTime2; + push(timerQueue, newTask); + if (peek(taskQueue) === null && newTask === peek(timerQueue)) { + if (isHostTimeoutScheduled) { + cancelHostTimeout(); + } else { + isHostTimeoutScheduled = true; + } + requestHostTimeout(handleTimeout, startTime2 - currentTime); + } + } else { + newTask.sortIndex = expirationTime; + push(taskQueue, newTask); + if (!isHostCallbackScheduled && !isPerformingWork) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } + } + return newTask; + } + function unstable_pauseExecution() { + } + function unstable_continueExecution() { + if (!isHostCallbackScheduled && !isPerformingWork) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } + } + function unstable_getFirstCallbackNode() { + return peek(taskQueue); + } + function unstable_cancelCallback(task) { + task.callback = null; + } + function unstable_getCurrentPriorityLevel() { + return currentPriorityLevel; + } + var isMessageLoopRunning = false; + var scheduledHostCallback = null; + var taskTimeoutID = -1; + var frameInterval = frameYieldMs; + var startTime = -1; + function shouldYieldToHost() { + var timeElapsed = exports.unstable_now() - startTime; + if (timeElapsed < frameInterval) { + return false; + } + return true; + } + function requestPaint() { + } + function forceFrameRate(fps) { + if (fps < 0 || fps > 125) { + console["error"]("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"); + return; + } + if (fps > 0) { + frameInterval = Math.floor(1e3 / fps); + } else { + frameInterval = frameYieldMs; + } + } + var performWorkUntilDeadline = function() { + if (scheduledHostCallback !== null) { + var currentTime = exports.unstable_now(); + startTime = currentTime; + var hasTimeRemaining = true; + var hasMoreWork = true; + try { + hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); + } finally { + if (hasMoreWork) { + schedulePerformWorkUntilDeadline(); + } else { + isMessageLoopRunning = false; + scheduledHostCallback = null; + } + } + } else { + isMessageLoopRunning = false; + } + }; + var schedulePerformWorkUntilDeadline; + if (typeof localSetImmediate === "function") { + schedulePerformWorkUntilDeadline = function() { + localSetImmediate(performWorkUntilDeadline); + }; + } else if (typeof MessageChannel !== "undefined") { + var channel = new MessageChannel(); + var port = channel.port2; + channel.port1.onmessage = performWorkUntilDeadline; + schedulePerformWorkUntilDeadline = function() { + port.postMessage(null); + }; + } else { + schedulePerformWorkUntilDeadline = function() { + localSetTimeout(performWorkUntilDeadline, 0); + }; + } + function requestHostCallback(callback) { + scheduledHostCallback = callback; + if (!isMessageLoopRunning) { + isMessageLoopRunning = true; + schedulePerformWorkUntilDeadline(); + } + } + function requestHostTimeout(callback, ms) { + taskTimeoutID = localSetTimeout(function() { + callback(exports.unstable_now()); + }, ms); + } + function cancelHostTimeout() { + localClearTimeout(taskTimeoutID); + taskTimeoutID = -1; + } + var unstable_requestPaint = requestPaint; + var unstable_Profiling = null; + exports.unstable_IdlePriority = IdlePriority; + exports.unstable_ImmediatePriority = ImmediatePriority; + exports.unstable_LowPriority = LowPriority; + exports.unstable_NormalPriority = NormalPriority; + exports.unstable_Profiling = unstable_Profiling; + exports.unstable_UserBlockingPriority = UserBlockingPriority; + exports.unstable_cancelCallback = unstable_cancelCallback; + exports.unstable_continueExecution = unstable_continueExecution; + exports.unstable_forceFrameRate = forceFrameRate; + exports.unstable_getCurrentPriorityLevel = unstable_getCurrentPriorityLevel; + exports.unstable_getFirstCallbackNode = unstable_getFirstCallbackNode; + exports.unstable_next = unstable_next; + exports.unstable_pauseExecution = unstable_pauseExecution; + exports.unstable_requestPaint = unstable_requestPaint; + exports.unstable_runWithPriority = unstable_runWithPriority; + exports.unstable_scheduleCallback = unstable_scheduleCallback; + exports.unstable_shouldYield = shouldYieldToHost; + exports.unstable_wrapCallback = unstable_wrapCallback; + if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined" && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop === "function") { + __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error()); + } + })(); + } + } +}); + +// node_modules/scheduler/index.js +var require_scheduler = __commonJS({ + "node_modules/scheduler/index.js"(exports, module) { + "use strict"; + if (false) { + module.exports = null; + } else { + module.exports = require_scheduler_development(); + } + } +}); + +// node_modules/react-dom/cjs/react-dom.development.js +var require_react_dom_development = __commonJS({ + "node_modules/react-dom/cjs/react-dom.development.js"(exports) { + "use strict"; + if (true) { + (function() { + "use strict"; + if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined" && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart === "function") { + __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error()); + } + var React = require_react(); + var Scheduler = require_scheduler(); + var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + var suppressWarning = false; + function setSuppressWarning(newSuppressWarning) { + { + suppressWarning = newSuppressWarning; + } + } + function warn(format) { + { + if (!suppressWarning) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + printWarning("warn", format, args); + } + } + } + function error(format) { + { + if (!suppressWarning) { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + printWarning("error", format, args); + } + } + } + function printWarning(level, format, args) { + { + var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame; + var stack = ReactDebugCurrentFrame2.getStackAddendum(); + if (stack !== "") { + format += "%s"; + args = args.concat([stack]); + } + var argsWithFormat = args.map(function(item) { + return String(item); + }); + argsWithFormat.unshift("Warning: " + format); + Function.prototype.apply.call(console[level], console, argsWithFormat); + } + } + var FunctionComponent = 0; + var ClassComponent = 1; + var IndeterminateComponent = 2; + var HostRoot = 3; + var HostPortal = 4; + var HostComponent = 5; + var HostText = 6; + var Fragment = 7; + var Mode = 8; + var ContextConsumer = 9; + var ContextProvider = 10; + var ForwardRef = 11; + var Profiler = 12; + var SuspenseComponent = 13; + var MemoComponent = 14; + var SimpleMemoComponent = 15; + var LazyComponent = 16; + var IncompleteClassComponent = 17; + var DehydratedFragment = 18; + var SuspenseListComponent = 19; + var ScopeComponent = 21; + var OffscreenComponent = 22; + var LegacyHiddenComponent = 23; + var CacheComponent = 24; + var TracingMarkerComponent = 25; + var enableClientRenderFallbackOnTextMismatch = true; + var enableNewReconciler = false; + var enableLazyContextPropagation = false; + var enableLegacyHidden = false; + var enableSuspenseAvoidThisFallback = false; + var disableCommentsAsDOMContainers = true; + var enableCustomElementPropertySupport = false; + var warnAboutStringRefs = true; + var enableSchedulingProfiler = true; + var enableProfilerTimer = true; + var enableProfilerCommitHooks = true; + var allNativeEvents = /* @__PURE__ */ new Set(); + var registrationNameDependencies = {}; + var possibleRegistrationNames = {}; + function registerTwoPhaseEvent(registrationName, dependencies) { + registerDirectEvent(registrationName, dependencies); + registerDirectEvent(registrationName + "Capture", dependencies); + } + function registerDirectEvent(registrationName, dependencies) { + { + if (registrationNameDependencies[registrationName]) { + error("EventRegistry: More than one plugin attempted to publish the same registration name, `%s`.", registrationName); + } + } + registrationNameDependencies[registrationName] = dependencies; + { + var lowerCasedName = registrationName.toLowerCase(); + possibleRegistrationNames[lowerCasedName] = registrationName; + if (registrationName === "onDoubleClick") { + possibleRegistrationNames.ondblclick = registrationName; + } + } + for (var i = 0; i < dependencies.length; i++) { + allNativeEvents.add(dependencies[i]); + } + } + var canUseDOM = !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined"); + var hasOwnProperty = Object.prototype.hasOwnProperty; + function typeName(value) { + { + var hasToStringTag = typeof Symbol === "function" && Symbol.toStringTag; + var type = hasToStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object"; + return type; + } + } + function willCoercionThrow(value) { + { + try { + testStringCoercion(value); + return false; + } catch (e) { + return true; + } + } + } + function testStringCoercion(value) { + return "" + value; + } + function checkAttributeStringCoercion(value, attributeName) { + { + if (willCoercionThrow(value)) { + error("The provided `%s` attribute is an unsupported type %s. This value must be coerced to a string before before using it here.", attributeName, typeName(value)); + return testStringCoercion(value); + } + } + } + function checkKeyStringCoercion(value) { + { + if (willCoercionThrow(value)) { + error("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.", typeName(value)); + return testStringCoercion(value); + } + } + } + function checkPropStringCoercion(value, propName) { + { + if (willCoercionThrow(value)) { + error("The provided `%s` prop is an unsupported type %s. This value must be coerced to a string before before using it here.", propName, typeName(value)); + return testStringCoercion(value); + } + } + } + function checkCSSPropertyStringCoercion(value, propName) { + { + if (willCoercionThrow(value)) { + error("The provided `%s` CSS property is an unsupported type %s. This value must be coerced to a string before before using it here.", propName, typeName(value)); + return testStringCoercion(value); + } + } + } + function checkHtmlStringCoercion(value) { + { + if (willCoercionThrow(value)) { + error("The provided HTML markup uses a value of unsupported type %s. This value must be coerced to a string before before using it here.", typeName(value)); + return testStringCoercion(value); + } + } + } + function checkFormFieldValueStringCoercion(value) { + { + if (willCoercionThrow(value)) { + error("Form field values (value, checked, defaultValue, or defaultChecked props) must be strings, not %s. This value must be coerced to a string before before using it here.", typeName(value)); + return testStringCoercion(value); + } + } + } + var RESERVED = 0; + var STRING = 1; + var BOOLEANISH_STRING = 2; + var BOOLEAN = 3; + var OVERLOADED_BOOLEAN = 4; + var NUMERIC = 5; + var POSITIVE_NUMERIC = 6; + var ATTRIBUTE_NAME_START_CHAR = ":A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; + var ATTRIBUTE_NAME_CHAR = ATTRIBUTE_NAME_START_CHAR + "\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; + var VALID_ATTRIBUTE_NAME_REGEX = new RegExp("^[" + ATTRIBUTE_NAME_START_CHAR + "][" + ATTRIBUTE_NAME_CHAR + "]*$"); + var illegalAttributeNameCache = {}; + var validatedAttributeNameCache = {}; + function isAttributeNameSafe(attributeName) { + if (hasOwnProperty.call(validatedAttributeNameCache, attributeName)) { + return true; + } + if (hasOwnProperty.call(illegalAttributeNameCache, attributeName)) { + return false; + } + if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) { + validatedAttributeNameCache[attributeName] = true; + return true; + } + illegalAttributeNameCache[attributeName] = true; + { + error("Invalid attribute name: `%s`", attributeName); + } + return false; + } + function shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag) { + if (propertyInfo !== null) { + return propertyInfo.type === RESERVED; + } + if (isCustomComponentTag) { + return false; + } + if (name.length > 2 && (name[0] === "o" || name[0] === "O") && (name[1] === "n" || name[1] === "N")) { + return true; + } + return false; + } + function shouldRemoveAttributeWithWarning(name, value, propertyInfo, isCustomComponentTag) { + if (propertyInfo !== null && propertyInfo.type === RESERVED) { + return false; + } + switch (typeof value) { + case "function": + case "symbol": + return true; + case "boolean": { + if (isCustomComponentTag) { + return false; + } + if (propertyInfo !== null) { + return !propertyInfo.acceptsBooleans; + } else { + var prefix2 = name.toLowerCase().slice(0, 5); + return prefix2 !== "data-" && prefix2 !== "aria-"; + } + } + default: + return false; + } + } + function shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag) { + if (value === null || typeof value === "undefined") { + return true; + } + if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, isCustomComponentTag)) { + return true; + } + if (isCustomComponentTag) { + return false; + } + if (propertyInfo !== null) { + switch (propertyInfo.type) { + case BOOLEAN: + return !value; + case OVERLOADED_BOOLEAN: + return value === false; + case NUMERIC: + return isNaN(value); + case POSITIVE_NUMERIC: + return isNaN(value) || value < 1; + } + } + return false; + } + function getPropertyInfo(name) { + return properties.hasOwnProperty(name) ? properties[name] : null; + } + function PropertyInfoRecord(name, type, mustUseProperty, attributeName, attributeNamespace, sanitizeURL2, removeEmptyString) { + this.acceptsBooleans = type === BOOLEANISH_STRING || type === BOOLEAN || type === OVERLOADED_BOOLEAN; + this.attributeName = attributeName; + this.attributeNamespace = attributeNamespace; + this.mustUseProperty = mustUseProperty; + this.propertyName = name; + this.type = type; + this.sanitizeURL = sanitizeURL2; + this.removeEmptyString = removeEmptyString; + } + var properties = {}; + var reservedProps = [ + "children", + "dangerouslySetInnerHTML", + // TODO: This prevents the assignment of defaultValue to regular + // elements (not just inputs). Now that ReactDOMInput assigns to the + // defaultValue property -- do we need this? + "defaultValue", + "defaultChecked", + "innerHTML", + "suppressContentEditableWarning", + "suppressHydrationWarning", + "style" + ]; + reservedProps.forEach(function(name) { + properties[name] = new PropertyInfoRecord( + name, + RESERVED, + false, + // mustUseProperty + name, + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + [["acceptCharset", "accept-charset"], ["className", "class"], ["htmlFor", "for"], ["httpEquiv", "http-equiv"]].forEach(function(_ref) { + var name = _ref[0], attributeName = _ref[1]; + properties[name] = new PropertyInfoRecord( + name, + STRING, + false, + // mustUseProperty + attributeName, + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + ["contentEditable", "draggable", "spellCheck", "value"].forEach(function(name) { + properties[name] = new PropertyInfoRecord( + name, + BOOLEANISH_STRING, + false, + // mustUseProperty + name.toLowerCase(), + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + ["autoReverse", "externalResourcesRequired", "focusable", "preserveAlpha"].forEach(function(name) { + properties[name] = new PropertyInfoRecord( + name, + BOOLEANISH_STRING, + false, + // mustUseProperty + name, + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + [ + "allowFullScreen", + "async", + // Note: there is a special case that prevents it from being written to the DOM + // on the client side because the browsers are inconsistent. Instead we call focus(). + "autoFocus", + "autoPlay", + "controls", + "default", + "defer", + "disabled", + "disablePictureInPicture", + "disableRemotePlayback", + "formNoValidate", + "hidden", + "loop", + "noModule", + "noValidate", + "open", + "playsInline", + "readOnly", + "required", + "reversed", + "scoped", + "seamless", + // Microdata + "itemScope" + ].forEach(function(name) { + properties[name] = new PropertyInfoRecord( + name, + BOOLEAN, + false, + // mustUseProperty + name.toLowerCase(), + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + [ + "checked", + // Note: `option.selected` is not updated if `select.multiple` is + // disabled with `removeAttribute`. We have special logic for handling this. + "multiple", + "muted", + "selected" + // NOTE: if you add a camelCased prop to this list, + // you'll need to set attributeName to name.toLowerCase() + // instead in the assignment below. + ].forEach(function(name) { + properties[name] = new PropertyInfoRecord( + name, + BOOLEAN, + true, + // mustUseProperty + name, + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + [ + "capture", + "download" + // NOTE: if you add a camelCased prop to this list, + // you'll need to set attributeName to name.toLowerCase() + // instead in the assignment below. + ].forEach(function(name) { + properties[name] = new PropertyInfoRecord( + name, + OVERLOADED_BOOLEAN, + false, + // mustUseProperty + name, + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + [ + "cols", + "rows", + "size", + "span" + // NOTE: if you add a camelCased prop to this list, + // you'll need to set attributeName to name.toLowerCase() + // instead in the assignment below. + ].forEach(function(name) { + properties[name] = new PropertyInfoRecord( + name, + POSITIVE_NUMERIC, + false, + // mustUseProperty + name, + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + ["rowSpan", "start"].forEach(function(name) { + properties[name] = new PropertyInfoRecord( + name, + NUMERIC, + false, + // mustUseProperty + name.toLowerCase(), + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + var CAMELIZE = /[\-\:]([a-z])/g; + var capitalize = function(token) { + return token[1].toUpperCase(); + }; + [ + "accent-height", + "alignment-baseline", + "arabic-form", + "baseline-shift", + "cap-height", + "clip-path", + "clip-rule", + "color-interpolation", + "color-interpolation-filters", + "color-profile", + "color-rendering", + "dominant-baseline", + "enable-background", + "fill-opacity", + "fill-rule", + "flood-color", + "flood-opacity", + "font-family", + "font-size", + "font-size-adjust", + "font-stretch", + "font-style", + "font-variant", + "font-weight", + "glyph-name", + "glyph-orientation-horizontal", + "glyph-orientation-vertical", + "horiz-adv-x", + "horiz-origin-x", + "image-rendering", + "letter-spacing", + "lighting-color", + "marker-end", + "marker-mid", + "marker-start", + "overline-position", + "overline-thickness", + "paint-order", + "panose-1", + "pointer-events", + "rendering-intent", + "shape-rendering", + "stop-color", + "stop-opacity", + "strikethrough-position", + "strikethrough-thickness", + "stroke-dasharray", + "stroke-dashoffset", + "stroke-linecap", + "stroke-linejoin", + "stroke-miterlimit", + "stroke-opacity", + "stroke-width", + "text-anchor", + "text-decoration", + "text-rendering", + "underline-position", + "underline-thickness", + "unicode-bidi", + "unicode-range", + "units-per-em", + "v-alphabetic", + "v-hanging", + "v-ideographic", + "v-mathematical", + "vector-effect", + "vert-adv-y", + "vert-origin-x", + "vert-origin-y", + "word-spacing", + "writing-mode", + "xmlns:xlink", + "x-height" + // NOTE: if you add a camelCased prop to this list, + // you'll need to set attributeName to name.toLowerCase() + // instead in the assignment below. + ].forEach(function(attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord( + name, + STRING, + false, + // mustUseProperty + attributeName, + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + [ + "xlink:actuate", + "xlink:arcrole", + "xlink:role", + "xlink:show", + "xlink:title", + "xlink:type" + // NOTE: if you add a camelCased prop to this list, + // you'll need to set attributeName to name.toLowerCase() + // instead in the assignment below. + ].forEach(function(attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord( + name, + STRING, + false, + // mustUseProperty + attributeName, + "http://www.w3.org/1999/xlink", + false, + // sanitizeURL + false + ); + }); + [ + "xml:base", + "xml:lang", + "xml:space" + // NOTE: if you add a camelCased prop to this list, + // you'll need to set attributeName to name.toLowerCase() + // instead in the assignment below. + ].forEach(function(attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord( + name, + STRING, + false, + // mustUseProperty + attributeName, + "http://www.w3.org/XML/1998/namespace", + false, + // sanitizeURL + false + ); + }); + ["tabIndex", "crossOrigin"].forEach(function(attributeName) { + properties[attributeName] = new PropertyInfoRecord( + attributeName, + STRING, + false, + // mustUseProperty + attributeName.toLowerCase(), + // attributeName + null, + // attributeNamespace + false, + // sanitizeURL + false + ); + }); + var xlinkHref = "xlinkHref"; + properties[xlinkHref] = new PropertyInfoRecord( + "xlinkHref", + STRING, + false, + // mustUseProperty + "xlink:href", + "http://www.w3.org/1999/xlink", + true, + // sanitizeURL + false + ); + ["src", "href", "action", "formAction"].forEach(function(attributeName) { + properties[attributeName] = new PropertyInfoRecord( + attributeName, + STRING, + false, + // mustUseProperty + attributeName.toLowerCase(), + // attributeName + null, + // attributeNamespace + true, + // sanitizeURL + true + ); + }); + var isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + var didWarn = false; + function sanitizeURL(url) { + { + if (!didWarn && isJavaScriptProtocol.test(url)) { + didWarn = true; + error("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed %s.", JSON.stringify(url)); + } + } + } + function getValueForProperty(node, name, expected, propertyInfo) { + { + if (propertyInfo.mustUseProperty) { + var propertyName = propertyInfo.propertyName; + return node[propertyName]; + } else { + { + checkAttributeStringCoercion(expected, name); + } + if (propertyInfo.sanitizeURL) { + sanitizeURL("" + expected); + } + var attributeName = propertyInfo.attributeName; + var stringValue = null; + if (propertyInfo.type === OVERLOADED_BOOLEAN) { + if (node.hasAttribute(attributeName)) { + var value = node.getAttribute(attributeName); + if (value === "") { + return true; + } + if (shouldRemoveAttribute(name, expected, propertyInfo, false)) { + return value; + } + if (value === "" + expected) { + return expected; + } + return value; + } + } else if (node.hasAttribute(attributeName)) { + if (shouldRemoveAttribute(name, expected, propertyInfo, false)) { + return node.getAttribute(attributeName); + } + if (propertyInfo.type === BOOLEAN) { + return expected; + } + stringValue = node.getAttribute(attributeName); + } + if (shouldRemoveAttribute(name, expected, propertyInfo, false)) { + return stringValue === null ? expected : stringValue; + } else if (stringValue === "" + expected) { + return expected; + } else { + return stringValue; + } + } + } + } + function getValueForAttribute(node, name, expected, isCustomComponentTag) { + { + if (!isAttributeNameSafe(name)) { + return; + } + if (!node.hasAttribute(name)) { + return expected === void 0 ? void 0 : null; + } + var value = node.getAttribute(name); + { + checkAttributeStringCoercion(expected, name); + } + if (value === "" + expected) { + return expected; + } + return value; + } + } + function setValueForProperty(node, name, value, isCustomComponentTag) { + var propertyInfo = getPropertyInfo(name); + if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) { + return; + } + if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) { + value = null; + } + if (isCustomComponentTag || propertyInfo === null) { + if (isAttributeNameSafe(name)) { + var _attributeName = name; + if (value === null) { + node.removeAttribute(_attributeName); + } else { + { + checkAttributeStringCoercion(value, name); + } + node.setAttribute(_attributeName, "" + value); + } + } + return; + } + var mustUseProperty = propertyInfo.mustUseProperty; + if (mustUseProperty) { + var propertyName = propertyInfo.propertyName; + if (value === null) { + var type = propertyInfo.type; + node[propertyName] = type === BOOLEAN ? false : ""; + } else { + node[propertyName] = value; + } + return; + } + var attributeName = propertyInfo.attributeName, attributeNamespace = propertyInfo.attributeNamespace; + if (value === null) { + node.removeAttribute(attributeName); + } else { + var _type = propertyInfo.type; + var attributeValue; + if (_type === BOOLEAN || _type === OVERLOADED_BOOLEAN && value === true) { + attributeValue = ""; + } else { + { + { + checkAttributeStringCoercion(value, attributeName); + } + attributeValue = "" + value; + } + if (propertyInfo.sanitizeURL) { + sanitizeURL(attributeValue.toString()); + } + } + if (attributeNamespace) { + node.setAttributeNS(attributeNamespace, attributeName, attributeValue); + } else { + node.setAttribute(attributeName, attributeValue); + } + } + } + var REACT_ELEMENT_TYPE = Symbol.for("react.element"); + var REACT_PORTAL_TYPE = Symbol.for("react.portal"); + var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); + var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"); + var REACT_PROFILER_TYPE = Symbol.for("react.profiler"); + var REACT_PROVIDER_TYPE = Symbol.for("react.provider"); + var REACT_CONTEXT_TYPE = Symbol.for("react.context"); + var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"); + var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"); + var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"); + var REACT_MEMO_TYPE = Symbol.for("react.memo"); + var REACT_LAZY_TYPE = Symbol.for("react.lazy"); + var REACT_SCOPE_TYPE = Symbol.for("react.scope"); + var REACT_DEBUG_TRACING_MODE_TYPE = Symbol.for("react.debug_trace_mode"); + var REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen"); + var REACT_LEGACY_HIDDEN_TYPE = Symbol.for("react.legacy_hidden"); + var REACT_CACHE_TYPE = Symbol.for("react.cache"); + var REACT_TRACING_MARKER_TYPE = Symbol.for("react.tracing_marker"); + var MAYBE_ITERATOR_SYMBOL = Symbol.iterator; + var FAUX_ITERATOR_SYMBOL = "@@iterator"; + function getIteratorFn(maybeIterable) { + if (maybeIterable === null || typeof maybeIterable !== "object") { + return null; + } + var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]; + if (typeof maybeIterator === "function") { + return maybeIterator; + } + return null; + } + var assign = Object.assign; + var disabledDepth = 0; + var prevLog; + var prevInfo; + var prevWarn; + var prevError; + var prevGroup; + var prevGroupCollapsed; + var prevGroupEnd; + function disabledLog() { + } + disabledLog.__reactDisabledLog = true; + function disableLogs() { + { + if (disabledDepth === 0) { + prevLog = console.log; + prevInfo = console.info; + prevWarn = console.warn; + prevError = console.error; + prevGroup = console.group; + prevGroupCollapsed = console.groupCollapsed; + prevGroupEnd = console.groupEnd; + var props = { + configurable: true, + enumerable: true, + value: disabledLog, + writable: true + }; + Object.defineProperties(console, { + info: props, + log: props, + warn: props, + error: props, + group: props, + groupCollapsed: props, + groupEnd: props + }); + } + disabledDepth++; + } + } + function reenableLogs() { + { + disabledDepth--; + if (disabledDepth === 0) { + var props = { + configurable: true, + enumerable: true, + writable: true + }; + Object.defineProperties(console, { + log: assign({}, props, { + value: prevLog + }), + info: assign({}, props, { + value: prevInfo + }), + warn: assign({}, props, { + value: prevWarn + }), + error: assign({}, props, { + value: prevError + }), + group: assign({}, props, { + value: prevGroup + }), + groupCollapsed: assign({}, props, { + value: prevGroupCollapsed + }), + groupEnd: assign({}, props, { + value: prevGroupEnd + }) + }); + } + if (disabledDepth < 0) { + error("disabledDepth fell below zero. This is a bug in React. Please file an issue."); + } + } + } + var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; + var prefix; + function describeBuiltInComponentFrame(name, source, ownerFn) { + { + if (prefix === void 0) { + try { + throw Error(); + } catch (x) { + var match = x.stack.trim().match(/\n( *(at )?)/); + prefix = match && match[1] || ""; + } + } + return "\n" + prefix + name; + } + } + var reentry = false; + var componentFrameCache; + { + var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map; + componentFrameCache = new PossiblyWeakMap(); + } + function describeNativeComponentFrame(fn, construct) { + if (!fn || reentry) { + return ""; + } + { + var frame = componentFrameCache.get(fn); + if (frame !== void 0) { + return frame; + } + } + var control; + reentry = true; + var previousPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = void 0; + var previousDispatcher; + { + previousDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = null; + disableLogs(); + } + try { + if (construct) { + var Fake = function() { + throw Error(); + }; + Object.defineProperty(Fake.prototype, "props", { + set: function() { + throw Error(); + } + }); + if (typeof Reflect === "object" && Reflect.construct) { + try { + Reflect.construct(Fake, []); + } catch (x) { + control = x; + } + Reflect.construct(fn, [], Fake); + } else { + try { + Fake.call(); + } catch (x) { + control = x; + } + fn.call(Fake.prototype); + } + } else { + try { + throw Error(); + } catch (x) { + control = x; + } + fn(); + } + } catch (sample) { + if (sample && control && typeof sample.stack === "string") { + var sampleLines = sample.stack.split("\n"); + var controlLines = control.stack.split("\n"); + var s = sampleLines.length - 1; + var c = controlLines.length - 1; + while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) { + c--; + } + for (; s >= 1 && c >= 0; s--, c--) { + if (sampleLines[s] !== controlLines[c]) { + if (s !== 1 || c !== 1) { + do { + s--; + c--; + if (c < 0 || sampleLines[s] !== controlLines[c]) { + var _frame = "\n" + sampleLines[s].replace(" at new ", " at "); + if (fn.displayName && _frame.includes("")) { + _frame = _frame.replace("", fn.displayName); + } + { + if (typeof fn === "function") { + componentFrameCache.set(fn, _frame); + } + } + return _frame; + } + } while (s >= 1 && c >= 0); + } + break; + } + } + } + } finally { + reentry = false; + { + ReactCurrentDispatcher.current = previousDispatcher; + reenableLogs(); + } + Error.prepareStackTrace = previousPrepareStackTrace; + } + var name = fn ? fn.displayName || fn.name : ""; + var syntheticFrame = name ? describeBuiltInComponentFrame(name) : ""; + { + if (typeof fn === "function") { + componentFrameCache.set(fn, syntheticFrame); + } + } + return syntheticFrame; + } + function describeClassComponentFrame(ctor, source, ownerFn) { + { + return describeNativeComponentFrame(ctor, true); + } + } + function describeFunctionComponentFrame(fn, source, ownerFn) { + { + return describeNativeComponentFrame(fn, false); + } + } + function shouldConstruct(Component) { + var prototype = Component.prototype; + return !!(prototype && prototype.isReactComponent); + } + function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) { + if (type == null) { + return ""; + } + if (typeof type === "function") { + { + return describeNativeComponentFrame(type, shouldConstruct(type)); + } + } + if (typeof type === "string") { + return describeBuiltInComponentFrame(type); + } + switch (type) { + case REACT_SUSPENSE_TYPE: + return describeBuiltInComponentFrame("Suspense"); + case REACT_SUSPENSE_LIST_TYPE: + return describeBuiltInComponentFrame("SuspenseList"); + } + if (typeof type === "object") { + switch (type.$$typeof) { + case REACT_FORWARD_REF_TYPE: + return describeFunctionComponentFrame(type.render); + case REACT_MEMO_TYPE: + return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn); + case REACT_LAZY_TYPE: { + var lazyComponent = type; + var payload = lazyComponent._payload; + var init = lazyComponent._init; + try { + return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn); + } catch (x) { + } + } + } + } + return ""; + } + function describeFiber(fiber) { + var owner = fiber._debugOwner ? fiber._debugOwner.type : null; + var source = fiber._debugSource; + switch (fiber.tag) { + case HostComponent: + return describeBuiltInComponentFrame(fiber.type); + case LazyComponent: + return describeBuiltInComponentFrame("Lazy"); + case SuspenseComponent: + return describeBuiltInComponentFrame("Suspense"); + case SuspenseListComponent: + return describeBuiltInComponentFrame("SuspenseList"); + case FunctionComponent: + case IndeterminateComponent: + case SimpleMemoComponent: + return describeFunctionComponentFrame(fiber.type); + case ForwardRef: + return describeFunctionComponentFrame(fiber.type.render); + case ClassComponent: + return describeClassComponentFrame(fiber.type); + default: + return ""; + } + } + function getStackByFiberInDevAndProd(workInProgress2) { + try { + var info = ""; + var node = workInProgress2; + do { + info += describeFiber(node); + node = node.return; + } while (node); + return info; + } catch (x) { + return "\nError generating stack: " + x.message + "\n" + x.stack; + } + } + function getWrappedName(outerType, innerType, wrapperName) { + var displayName = outerType.displayName; + if (displayName) { + return displayName; + } + var functionName = innerType.displayName || innerType.name || ""; + return functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName; + } + function getContextName(type) { + return type.displayName || "Context"; + } + function getComponentNameFromType(type) { + if (type == null) { + return null; + } + { + if (typeof type.tag === "number") { + error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."); + } + } + if (typeof type === "function") { + return type.displayName || type.name || null; + } + if (typeof type === "string") { + return type; + } + switch (type) { + case REACT_FRAGMENT_TYPE: + return "Fragment"; + case REACT_PORTAL_TYPE: + return "Portal"; + case REACT_PROFILER_TYPE: + return "Profiler"; + case REACT_STRICT_MODE_TYPE: + return "StrictMode"; + case REACT_SUSPENSE_TYPE: + return "Suspense"; + case REACT_SUSPENSE_LIST_TYPE: + return "SuspenseList"; + } + if (typeof type === "object") { + switch (type.$$typeof) { + case REACT_CONTEXT_TYPE: + var context = type; + return getContextName(context) + ".Consumer"; + case REACT_PROVIDER_TYPE: + var provider = type; + return getContextName(provider._context) + ".Provider"; + case REACT_FORWARD_REF_TYPE: + return getWrappedName(type, type.render, "ForwardRef"); + case REACT_MEMO_TYPE: + var outerName = type.displayName || null; + if (outerName !== null) { + return outerName; + } + return getComponentNameFromType(type.type) || "Memo"; + case REACT_LAZY_TYPE: { + var lazyComponent = type; + var payload = lazyComponent._payload; + var init = lazyComponent._init; + try { + return getComponentNameFromType(init(payload)); + } catch (x) { + return null; + } + } + } + } + return null; + } + function getWrappedName$1(outerType, innerType, wrapperName) { + var functionName = innerType.displayName || innerType.name || ""; + return outerType.displayName || (functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName); + } + function getContextName$1(type) { + return type.displayName || "Context"; + } + function getComponentNameFromFiber(fiber) { + var tag = fiber.tag, type = fiber.type; + switch (tag) { + case CacheComponent: + return "Cache"; + case ContextConsumer: + var context = type; + return getContextName$1(context) + ".Consumer"; + case ContextProvider: + var provider = type; + return getContextName$1(provider._context) + ".Provider"; + case DehydratedFragment: + return "DehydratedFragment"; + case ForwardRef: + return getWrappedName$1(type, type.render, "ForwardRef"); + case Fragment: + return "Fragment"; + case HostComponent: + return type; + case HostPortal: + return "Portal"; + case HostRoot: + return "Root"; + case HostText: + return "Text"; + case LazyComponent: + return getComponentNameFromType(type); + case Mode: + if (type === REACT_STRICT_MODE_TYPE) { + return "StrictMode"; + } + return "Mode"; + case OffscreenComponent: + return "Offscreen"; + case Profiler: + return "Profiler"; + case ScopeComponent: + return "Scope"; + case SuspenseComponent: + return "Suspense"; + case SuspenseListComponent: + return "SuspenseList"; + case TracingMarkerComponent: + return "TracingMarker"; + case ClassComponent: + case FunctionComponent: + case IncompleteClassComponent: + case IndeterminateComponent: + case MemoComponent: + case SimpleMemoComponent: + if (typeof type === "function") { + return type.displayName || type.name || null; + } + if (typeof type === "string") { + return type; + } + break; + } + return null; + } + var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; + var current = null; + var isRendering = false; + function getCurrentFiberOwnerNameInDevOrNull() { + { + if (current === null) { + return null; + } + var owner = current._debugOwner; + if (owner !== null && typeof owner !== "undefined") { + return getComponentNameFromFiber(owner); + } + } + return null; + } + function getCurrentFiberStackInDev() { + { + if (current === null) { + return ""; + } + return getStackByFiberInDevAndProd(current); + } + } + function resetCurrentFiber() { + { + ReactDebugCurrentFrame.getCurrentStack = null; + current = null; + isRendering = false; + } + } + function setCurrentFiber(fiber) { + { + ReactDebugCurrentFrame.getCurrentStack = fiber === null ? null : getCurrentFiberStackInDev; + current = fiber; + isRendering = false; + } + } + function getCurrentFiber() { + { + return current; + } + } + function setIsRendering(rendering) { + { + isRendering = rendering; + } + } + function toString(value) { + return "" + value; + } + function getToStringValue(value) { + switch (typeof value) { + case "boolean": + case "number": + case "string": + case "undefined": + return value; + case "object": + { + checkFormFieldValueStringCoercion(value); + } + return value; + default: + return ""; + } + } + var hasReadOnlyValue = { + button: true, + checkbox: true, + image: true, + hidden: true, + radio: true, + reset: true, + submit: true + }; + function checkControlledValueProps(tagName, props) { + { + if (!(hasReadOnlyValue[props.type] || props.onChange || props.onInput || props.readOnly || props.disabled || props.value == null)) { + error("You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."); + } + if (!(props.onChange || props.readOnly || props.disabled || props.checked == null)) { + error("You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`."); + } + } + } + function isCheckable(elem) { + var type = elem.type; + var nodeName = elem.nodeName; + return nodeName && nodeName.toLowerCase() === "input" && (type === "checkbox" || type === "radio"); + } + function getTracker(node) { + return node._valueTracker; + } + function detachTracker(node) { + node._valueTracker = null; + } + function getValueFromNode(node) { + var value = ""; + if (!node) { + return value; + } + if (isCheckable(node)) { + value = node.checked ? "true" : "false"; + } else { + value = node.value; + } + return value; + } + function trackValueOnNode(node) { + var valueField = isCheckable(node) ? "checked" : "value"; + var descriptor = Object.getOwnPropertyDescriptor(node.constructor.prototype, valueField); + { + checkFormFieldValueStringCoercion(node[valueField]); + } + var currentValue = "" + node[valueField]; + if (node.hasOwnProperty(valueField) || typeof descriptor === "undefined" || typeof descriptor.get !== "function" || typeof descriptor.set !== "function") { + return; + } + var get2 = descriptor.get, set2 = descriptor.set; + Object.defineProperty(node, valueField, { + configurable: true, + get: function() { + return get2.call(this); + }, + set: function(value) { + { + checkFormFieldValueStringCoercion(value); + } + currentValue = "" + value; + set2.call(this, value); + } + }); + Object.defineProperty(node, valueField, { + enumerable: descriptor.enumerable + }); + var tracker = { + getValue: function() { + return currentValue; + }, + setValue: function(value) { + { + checkFormFieldValueStringCoercion(value); + } + currentValue = "" + value; + }, + stopTracking: function() { + detachTracker(node); + delete node[valueField]; + } + }; + return tracker; + } + function track(node) { + if (getTracker(node)) { + return; + } + node._valueTracker = trackValueOnNode(node); + } + function updateValueIfChanged(node) { + if (!node) { + return false; + } + var tracker = getTracker(node); + if (!tracker) { + return true; + } + var lastValue = tracker.getValue(); + var nextValue = getValueFromNode(node); + if (nextValue !== lastValue) { + tracker.setValue(nextValue); + return true; + } + return false; + } + function getActiveElement(doc) { + doc = doc || (typeof document !== "undefined" ? document : void 0); + if (typeof doc === "undefined") { + return null; + } + try { + return doc.activeElement || doc.body; + } catch (e) { + return doc.body; + } + } + var didWarnValueDefaultValue = false; + var didWarnCheckedDefaultChecked = false; + var didWarnControlledToUncontrolled = false; + var didWarnUncontrolledToControlled = false; + function isControlled(props) { + var usesChecked = props.type === "checkbox" || props.type === "radio"; + return usesChecked ? props.checked != null : props.value != null; + } + function getHostProps(element, props) { + var node = element; + var checked = props.checked; + var hostProps = assign({}, props, { + defaultChecked: void 0, + defaultValue: void 0, + value: void 0, + checked: checked != null ? checked : node._wrapperState.initialChecked + }); + return hostProps; + } + function initWrapperState(element, props) { + { + checkControlledValueProps("input", props); + if (props.checked !== void 0 && props.defaultChecked !== void 0 && !didWarnCheckedDefaultChecked) { + error("%s contains an input of type %s with both checked and defaultChecked props. Input elements must be either controlled or uncontrolled (specify either the checked prop, or the defaultChecked prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components", getCurrentFiberOwnerNameInDevOrNull() || "A component", props.type); + didWarnCheckedDefaultChecked = true; + } + if (props.value !== void 0 && props.defaultValue !== void 0 && !didWarnValueDefaultValue) { + error("%s contains an input of type %s with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components", getCurrentFiberOwnerNameInDevOrNull() || "A component", props.type); + didWarnValueDefaultValue = true; + } + } + var node = element; + var defaultValue = props.defaultValue == null ? "" : props.defaultValue; + node._wrapperState = { + initialChecked: props.checked != null ? props.checked : props.defaultChecked, + initialValue: getToStringValue(props.value != null ? props.value : defaultValue), + controlled: isControlled(props) + }; + } + function updateChecked(element, props) { + var node = element; + var checked = props.checked; + if (checked != null) { + setValueForProperty(node, "checked", checked, false); + } + } + function updateWrapper(element, props) { + var node = element; + { + var controlled = isControlled(props); + if (!node._wrapperState.controlled && controlled && !didWarnUncontrolledToControlled) { + error("A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"); + didWarnUncontrolledToControlled = true; + } + if (node._wrapperState.controlled && !controlled && !didWarnControlledToUncontrolled) { + error("A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"); + didWarnControlledToUncontrolled = true; + } + } + updateChecked(element, props); + var value = getToStringValue(props.value); + var type = props.type; + if (value != null) { + if (type === "number") { + if (value === 0 && node.value === "" || // We explicitly want to coerce to number here if possible. + // eslint-disable-next-line + node.value != value) { + node.value = toString(value); + } + } else if (node.value !== toString(value)) { + node.value = toString(value); + } + } else if (type === "submit" || type === "reset") { + node.removeAttribute("value"); + return; + } + { + if (props.hasOwnProperty("value")) { + setDefaultValue(node, props.type, value); + } else if (props.hasOwnProperty("defaultValue")) { + setDefaultValue(node, props.type, getToStringValue(props.defaultValue)); + } + } + { + if (props.checked == null && props.defaultChecked != null) { + node.defaultChecked = !!props.defaultChecked; + } + } + } + function postMountWrapper(element, props, isHydrating2) { + var node = element; + if (props.hasOwnProperty("value") || props.hasOwnProperty("defaultValue")) { + var type = props.type; + var isButton = type === "submit" || type === "reset"; + if (isButton && (props.value === void 0 || props.value === null)) { + return; + } + var initialValue = toString(node._wrapperState.initialValue); + if (!isHydrating2) { + { + if (initialValue !== node.value) { + node.value = initialValue; + } + } + } + { + node.defaultValue = initialValue; + } + } + var name = node.name; + if (name !== "") { + node.name = ""; + } + { + node.defaultChecked = !node.defaultChecked; + node.defaultChecked = !!node._wrapperState.initialChecked; + } + if (name !== "") { + node.name = name; + } + } + function restoreControlledState(element, props) { + var node = element; + updateWrapper(node, props); + updateNamedCousins(node, props); + } + function updateNamedCousins(rootNode, props) { + var name = props.name; + if (props.type === "radio" && name != null) { + var queryRoot = rootNode; + while (queryRoot.parentNode) { + queryRoot = queryRoot.parentNode; + } + { + checkAttributeStringCoercion(name, "name"); + } + var group = queryRoot.querySelectorAll("input[name=" + JSON.stringify("" + name) + '][type="radio"]'); + for (var i = 0; i < group.length; i++) { + var otherNode = group[i]; + if (otherNode === rootNode || otherNode.form !== rootNode.form) { + continue; + } + var otherProps = getFiberCurrentPropsFromNode(otherNode); + if (!otherProps) { + throw new Error("ReactDOMInput: Mixing React and non-React radio inputs with the same `name` is not supported."); + } + updateValueIfChanged(otherNode); + updateWrapper(otherNode, otherProps); + } + } + } + function setDefaultValue(node, type, value) { + if ( + // Focused number inputs synchronize on blur. See ChangeEventPlugin.js + type !== "number" || getActiveElement(node.ownerDocument) !== node + ) { + if (value == null) { + node.defaultValue = toString(node._wrapperState.initialValue); + } else if (node.defaultValue !== toString(value)) { + node.defaultValue = toString(value); + } + } + } + var didWarnSelectedSetOnOption = false; + var didWarnInvalidChild = false; + var didWarnInvalidInnerHTML = false; + function validateProps(element, props) { + { + if (props.value == null) { + if (typeof props.children === "object" && props.children !== null) { + React.Children.forEach(props.children, function(child) { + if (child == null) { + return; + } + if (typeof child === "string" || typeof child === "number") { + return; + } + if (!didWarnInvalidChild) { + didWarnInvalidChild = true; + error("Cannot infer the option value of complex children. Pass a `value` prop or use a plain string as children to