changes: - Fix widget "Tasa de recuperados" calculation
- Added wigdet "Usuarios recuperados"
This commit is contained in:
239
SQL/analysis_recovered_users.sql
Normal file
239
SQL/analysis_recovered_users.sql
Normal file
@@ -0,0 +1,239 @@
|
||||
-- ============================================================================
|
||||
-- ANÁLISIS DE USUARIOS RECUPERADOS - Comparación de 3 conceptos diferentes
|
||||
-- ============================================================================
|
||||
-- Este SQL ayuda a entender las diferencias entre los 3 widgets:
|
||||
-- 1. Usuarios Recuperados (LoyaltyType = 'Recurrent')
|
||||
-- 2. Top 5 Usuarios Recuperados (IsRecoveredUser = true)
|
||||
-- 3. Tasa de Recuperación (usuarios inactivos que regresaron)
|
||||
-- ============================================================================
|
||||
|
||||
-- Parámetros de fechas (ajustar según necesidad)
|
||||
-- DECLARE @StartDate DATE = '2025-01-01';
|
||||
-- DECLARE @EndDate DATE = '2025-01-22';
|
||||
-- Para PostgreSQL usar:
|
||||
DO $$
|
||||
DECLARE
|
||||
start_date DATE := '2025-01-01';
|
||||
end_date DATE := CURRENT_DATE;
|
||||
BEGIN
|
||||
|
||||
-- ============================================================================
|
||||
-- CONCEPTO 1: LoyaltyType = 'Recurrent' (Widget: Usuarios Recuperados)
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '=== CONCEPTO 1: LoyaltyType = Recurrent ===';
|
||||
RAISE NOTICE 'Usuarios con loyalty_type = Recurrent en el período';
|
||||
|
||||
SELECT
|
||||
'LoyaltyType Recurrent' as concepto,
|
||||
COUNT(DISTINCT client_mac) as total_usuarios_unicos,
|
||||
COUNT(*) as total_conexiones,
|
||||
MIN(connection_date) as primera_fecha,
|
||||
MAX(connection_date) as ultima_fecha
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE loyalty_type = 'Recurrent'
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted;
|
||||
|
||||
-- Detalle de usuarios Recurrent
|
||||
SELECT
|
||||
client_mac,
|
||||
user_name,
|
||||
email,
|
||||
loyalty_type,
|
||||
COUNT(*) as total_conexiones,
|
||||
MIN(connection_date) as primera_conexion,
|
||||
MAX(connection_date) as ultima_conexion,
|
||||
MAX(connection_date_time) as ultima_conexion_datetime
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE loyalty_type = 'Recurrent'
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted
|
||||
GROUP BY client_mac, user_name, email, loyalty_type
|
||||
ORDER BY total_conexiones DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- ============================================================================
|
||||
-- CONCEPTO 2: IsRecoveredUser = true (Widget: Top 5 Usuarios Recuperados)
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '=== CONCEPTO 2: IsRecoveredUser = true ===';
|
||||
RAISE NOTICE 'Usuarios marcados explícitamente como recuperados';
|
||||
|
||||
SELECT
|
||||
'IsRecoveredUser = true' as concepto,
|
||||
COUNT(DISTINCT client_mac) as total_usuarios_unicos,
|
||||
COUNT(*) as total_conexiones,
|
||||
COUNT(DISTINCT CASE WHEN user_id > 0 THEN user_id END) as usuarios_con_id
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE is_recovered_user = true
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted;
|
||||
|
||||
-- Detalle de usuarios recuperados
|
||||
SELECT
|
||||
user_id,
|
||||
client_mac,
|
||||
user_name,
|
||||
email,
|
||||
loyalty_type,
|
||||
is_recovered_user,
|
||||
COUNT(*) as total_conexiones,
|
||||
MIN(connection_date) as primera_conexion,
|
||||
MAX(connection_date) as ultima_conexion,
|
||||
days_inactive
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE is_recovered_user = true
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted
|
||||
AND user_id > 0 -- El widget TopRetrievedUsers filtra por user_id > 0
|
||||
GROUP BY user_id, client_mac, user_name, email, loyalty_type, is_recovered_user, days_inactive
|
||||
ORDER BY total_conexiones DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- ============================================================================
|
||||
-- CONCEPTO 3: Recovery Rate (usuarios inactivos que regresaron)
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '=== CONCEPTO 3: Recovery Rate - Usuarios Inactivos que Regresaron ===';
|
||||
RAISE NOTICE 'Lógica compleja: usuarios que estuvieron inactivos y luego regresaron';
|
||||
|
||||
-- Usuarios que tienen recovery_connection_date (fecha de recuperación)
|
||||
SELECT
|
||||
'Usuarios con recovery_connection_date' as concepto,
|
||||
COUNT(DISTINCT client_mac) as total_usuarios_unicos,
|
||||
COUNT(*) as total_conexiones,
|
||||
AVG(days_inactive) as promedio_dias_inactivos
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE recovery_connection_date IS NOT NULL
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted;
|
||||
|
||||
-- Detalle de usuarios con recovery_connection_date
|
||||
SELECT
|
||||
client_mac,
|
||||
user_name,
|
||||
email,
|
||||
loyalty_type,
|
||||
recovery_connection_date,
|
||||
days_inactive,
|
||||
COUNT(*) as total_conexiones,
|
||||
MIN(connection_date) as primera_conexion,
|
||||
MAX(connection_date) as ultima_conexion
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE recovery_connection_date IS NOT NULL
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted
|
||||
GROUP BY client_mac, user_name, email, loyalty_type, recovery_connection_date, days_inactive
|
||||
ORDER BY days_inactive DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- ============================================================================
|
||||
-- RESUMEN COMPARATIVO DE LOS 3 CONCEPTOS
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '=== RESUMEN COMPARATIVO ===';
|
||||
|
||||
SELECT
|
||||
'LoyaltyType Recurrent' as concepto,
|
||||
COUNT(DISTINCT client_mac) as usuarios_unicos,
|
||||
COUNT(*) as total_conexiones
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE loyalty_type = 'Recurrent'
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'IsRecoveredUser = true' as concepto,
|
||||
COUNT(DISTINCT client_mac) as usuarios_unicos,
|
||||
COUNT(*) as total_conexiones
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE is_recovered_user = true
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'Recovery Date NOT NULL' as concepto,
|
||||
COUNT(DISTINCT client_mac) as usuarios_unicos,
|
||||
COUNT(*) as total_conexiones
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE recovery_connection_date IS NOT NULL
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted;
|
||||
|
||||
-- ============================================================================
|
||||
-- INTERSECCIÓN: ¿Cuántos usuarios cumplen MÚLTIPLES conceptos?
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '=== INTERSECCIÓN DE CONCEPTOS ===';
|
||||
|
||||
SELECT
|
||||
CASE
|
||||
WHEN loyalty_type = 'Recurrent' AND is_recovered_user = true AND recovery_connection_date IS NOT NULL
|
||||
THEN 'Los 3 conceptos'
|
||||
WHEN loyalty_type = 'Recurrent' AND is_recovered_user = true
|
||||
THEN 'Recurrent + IsRecovered'
|
||||
WHEN loyalty_type = 'Recurrent' AND recovery_connection_date IS NOT NULL
|
||||
THEN 'Recurrent + RecoveryDate'
|
||||
WHEN is_recovered_user = true AND recovery_connection_date IS NOT NULL
|
||||
THEN 'IsRecovered + RecoveryDate'
|
||||
WHEN loyalty_type = 'Recurrent'
|
||||
THEN 'Solo Recurrent'
|
||||
WHEN is_recovered_user = true
|
||||
THEN 'Solo IsRecovered'
|
||||
WHEN recovery_connection_date IS NOT NULL
|
||||
THEN 'Solo RecoveryDate'
|
||||
ELSE 'Ninguno'
|
||||
END as categoria,
|
||||
COUNT(DISTINCT client_mac) as usuarios_unicos,
|
||||
COUNT(*) as total_conexiones
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted
|
||||
AND (
|
||||
loyalty_type = 'Recurrent'
|
||||
OR is_recovered_user = true
|
||||
OR recovery_connection_date IS NOT NULL
|
||||
)
|
||||
GROUP BY categoria
|
||||
ORDER BY usuarios_unicos DESC;
|
||||
|
||||
-- ============================================================================
|
||||
-- RECOMENDACIÓN: ¿Qué campo usar para cada widget?
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '=== RECOMENDACIONES ===';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '1. Widget "Usuarios Recuperados" (contador simple):';
|
||||
RAISE NOTICE ' → Usar: loyalty_type = ''Recurrent''';
|
||||
RAISE NOTICE ' → Motivo: Campo estándar de clasificación de usuarios';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '2. Widget "Top 5 Usuarios Recuperados" (ranking):';
|
||||
RAISE NOTICE ' → Usar: is_recovered_user = true AND user_id > 0';
|
||||
RAISE NOTICE ' → Motivo: Marca explícita + necesita user_id para agrupar';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '3. Widget "Tasa de Recuperación" (métrica compleja):';
|
||||
RAISE NOTICE ' → Usar: recovery_connection_date IS NOT NULL';
|
||||
RAISE NOTICE ' → Motivo: Calcula % de usuarios inactivos que regresaron';
|
||||
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- QUERY RÁPIDO PARA DEBUGGING
|
||||
-- ============================================================================
|
||||
-- Ver ejemplo de un usuario que cumple múltiples conceptos
|
||||
SELECT
|
||||
client_mac,
|
||||
user_name,
|
||||
email,
|
||||
loyalty_type,
|
||||
is_recovered_user,
|
||||
recovery_connection_date,
|
||||
days_inactive,
|
||||
connection_date,
|
||||
connection_date_time
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE (loyalty_type = 'Recurrent'
|
||||
OR is_recovered_user = true
|
||||
OR recovery_connection_date IS NOT NULL)
|
||||
AND NOT is_deleted
|
||||
ORDER BY connection_date DESC
|
||||
LIMIT 20;
|
||||
157
SQL/debug_recovery_rate_empty.sql
Normal file
157
SQL/debug_recovery_rate_empty.sql
Normal file
@@ -0,0 +1,157 @@
|
||||
-- ============================================================================
|
||||
-- DIAGNÓSTICO: ¿Por qué "Tasa de Recuperación" está vacía?
|
||||
-- ============================================================================
|
||||
-- Este SQL diagnostica por qué el widget no muestra datos
|
||||
|
||||
-- Parámetros (ajustar según tu dashboard)
|
||||
DO $$
|
||||
DECLARE
|
||||
start_date DATE := '2025-01-01';
|
||||
end_date DATE := CURRENT_DATE;
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE '=== DIAGNÓSTICO: Widget Tasa de Recuperación ===';
|
||||
RAISE NOTICE '';
|
||||
|
||||
-- ============================================================================
|
||||
-- PASO 1: ¿Hay usuarios con is_recovered_user = true?
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '--- PASO 1: Usuarios con is_recovered_user = true ---';
|
||||
|
||||
SELECT
|
||||
COUNT(*) as total_registros,
|
||||
COUNT(DISTINCT client_mac) as usuarios_unicos,
|
||||
COUNT(DISTINCT CASE WHEN user_id > 0 THEN user_id END) as usuarios_con_cuenta
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE is_recovered_user = true
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted;
|
||||
|
||||
-- ============================================================================
|
||||
-- PASO 2: ¿Cuántos tienen RecoveryConnectionDate?
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '--- PASO 2: Usuarios con recovery_connection_date ---';
|
||||
|
||||
SELECT
|
||||
'Con RecoveryConnectionDate' as categoria,
|
||||
COUNT(DISTINCT client_mac) as usuarios_unicos,
|
||||
COUNT(DISTINCT CASE WHEN user_id > 0 THEN user_id END) as usuarios_con_cuenta
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE is_recovered_user = true
|
||||
AND recovery_connection_date IS NOT NULL
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'SIN RecoveryConnectionDate' as categoria,
|
||||
COUNT(DISTINCT client_mac) as usuarios_unicos,
|
||||
COUNT(DISTINCT CASE WHEN user_id > 0 THEN user_id END) as usuarios_con_cuenta
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE is_recovered_user = true
|
||||
AND recovery_connection_date IS NULL
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted;
|
||||
|
||||
-- ============================================================================
|
||||
-- PASO 3: ¿Cuántos cumplen TODAS las condiciones del widget?
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '--- PASO 3: Usuarios que cumplen TODAS las condiciones ---';
|
||||
|
||||
-- Condiciones del widget RecoveryRate:
|
||||
-- 1. user_id > 0
|
||||
-- 2. is_recovered_user = true
|
||||
-- 3. recovery_connection_date IS NOT NULL
|
||||
|
||||
SELECT
|
||||
COUNT(DISTINCT user_id) as usuarios_validos_para_recovery_rate
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE user_id > 0
|
||||
AND is_recovered_user = true
|
||||
AND recovery_connection_date IS NOT NULL
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted;
|
||||
|
||||
-- ============================================================================
|
||||
-- PASO 4: ¿Hay usuarios inactivos (denominador)?
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '--- PASO 4: Usuarios inactivos (más de 30 días) ---';
|
||||
|
||||
SELECT
|
||||
COUNT(DISTINCT user_id) as total_usuarios_inactivos_30dias
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE user_id > 0
|
||||
AND days_inactive > 30
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted;
|
||||
|
||||
-- ============================================================================
|
||||
-- PASO 5: Detalle de los usuarios recuperados (si los hay)
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '--- PASO 5: Detalle de usuarios recuperados (Top 10) ---';
|
||||
|
||||
SELECT
|
||||
user_id,
|
||||
client_mac,
|
||||
user_name,
|
||||
email,
|
||||
is_recovered_user,
|
||||
recovery_connection_date,
|
||||
days_inactive,
|
||||
COUNT(*) as total_conexiones,
|
||||
MIN(connection_date) as primera_conexion,
|
||||
MAX(connection_date) as ultima_conexion
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE user_id > 0
|
||||
AND is_recovered_user = true
|
||||
AND connection_date BETWEEN start_date AND end_date
|
||||
AND NOT is_deleted
|
||||
GROUP BY user_id, client_mac, user_name, email, is_recovered_user, recovery_connection_date, days_inactive
|
||||
ORDER BY recovery_connection_date DESC NULLS LAST
|
||||
LIMIT 10;
|
||||
|
||||
-- ============================================================================
|
||||
-- RESUMEN DIAGNÓSTICO
|
||||
-- ============================================================================
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '=== RESUMEN DIAGNÓSTICO ===';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE 'El widget "Tasa de Recuperación" requiere:';
|
||||
RAISE NOTICE ' 1. user_id > 0 (usuario con cuenta)';
|
||||
RAISE NOTICE ' 2. is_recovered_user = true';
|
||||
RAISE NOTICE ' 3. recovery_connection_date IS NOT NULL ⚠️ CRÍTICO';
|
||||
RAISE NOTICE ' 4. days_inactive > 30 (para calcular denominador)';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE 'Si el resultado del PASO 3 es 0:';
|
||||
RAISE NOTICE ' → Falta el campo recovery_connection_date';
|
||||
RAISE NOTICE ' → Solución: Verificar cómo se calcula este campo en la vista';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE 'Si el resultado del PASO 4 es 0:';
|
||||
RAISE NOTICE ' → No hay usuarios inactivos suficientes';
|
||||
RAISE NOTICE ' → Normal si el período es corto o todos son activos';
|
||||
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- QUERY RÁPIDO DE VERIFICACIÓN
|
||||
-- ============================================================================
|
||||
-- Ver un ejemplo de registro recuperado
|
||||
SELECT
|
||||
user_id,
|
||||
client_mac,
|
||||
is_recovered_user,
|
||||
recovery_connection_date,
|
||||
days_inactive,
|
||||
loyalty_type,
|
||||
connection_date,
|
||||
connection_date_time
|
||||
FROM splash_wifi_connection_report
|
||||
WHERE is_recovered_user = true
|
||||
AND NOT is_deleted
|
||||
ORDER BY connection_date DESC
|
||||
LIMIT 5;
|
||||
188
UNIFICACION_USUARIOS_RECUPERADOS.md
Normal file
188
UNIFICACION_USUARIOS_RECUPERADOS.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Unificación de Criterios: Usuarios Recuperados
|
||||
|
||||
## 📋 Resumen del Cambio
|
||||
|
||||
Se han unificado los criterios de los 3 widgets relacionados con usuarios recuperados para que todos usen **la misma fuente de datos** y muestren información consistente.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Cambio Realizado
|
||||
|
||||
### **Campo Unificado:**
|
||||
```sql
|
||||
is_recovered_user = true
|
||||
```
|
||||
|
||||
### **Significado:**
|
||||
Usuarios que estuvieron inactivos por un período y luego regresaron a conectarse. Este campo es calculado por la vista materializada `splash_wifi_connection_report`.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Widgets Afectados (Ahora Consistentes)
|
||||
|
||||
| # | Widget | Filtro Anterior | Filtro Nuevo | Estado |
|
||||
|---|--------|----------------|--------------|--------|
|
||||
| 1 | **Usuarios Recuperados** | `loyalty_type = 'Recurrent'` | `is_recovered_user = true` | ✅ Actualizado |
|
||||
| 2 | **Top 5 Usuarios Recuperados** | `is_recovered_user = true` | `is_recovered_user = true` | ✅ Ya era correcto |
|
||||
| 3 | **Tasa de Recuperación** | `recovery_connection_date IS NOT NULL` | Usa `is_recovered_user = true` indirectamente | ✅ Ya era correcto |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Archivos Modificados
|
||||
|
||||
### **1. Backend - Lógica de Negocio**
|
||||
```
|
||||
src/SplashPage.Application/Splash/SplashMetricsService.cs
|
||||
```
|
||||
**Líneas modificadas:** 1469-1475
|
||||
**Cambio:**
|
||||
```csharp
|
||||
// ANTES
|
||||
var currentRecovered = currentData.Items.Where(x => x.LoyaltyType == "Recurrent").ToList();
|
||||
|
||||
// DESPUÉS
|
||||
var currentRecovered = currentData.Items.Where(x => x.IsRecoveredUser == true).ToList();
|
||||
```
|
||||
|
||||
### **2. Backend - Interfaz del Servicio**
|
||||
```
|
||||
src/SplashPage.Application/Splash/ISplashMetricsService.cs
|
||||
```
|
||||
**Líneas modificadas:** 93-100
|
||||
**Cambio:** Actualizada documentación XML para clarificar el criterio.
|
||||
|
||||
### **3. Backend - DTO**
|
||||
```
|
||||
src/SplashPage.Application/Splash/Dto/RecoveredUsersMetricsDto.cs
|
||||
```
|
||||
**Líneas modificadas:** 6-9
|
||||
**Cambio:** Actualizada documentación de clase.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Beneficios de la Unificación
|
||||
|
||||
### ✅ **1. Consistencia de Datos**
|
||||
Los 3 widgets ahora muestran números basados en la misma fuente:
|
||||
- **Usuarios Recuperados**: Contador total
|
||||
- **Top 5 Usuarios Recuperados**: Top 5 con más conexiones
|
||||
- **Tasa de Recuperación**: % de recuperación vs inactivos
|
||||
|
||||
### ✅ **2. Claridad de Negocio**
|
||||
Un solo concepto de "usuario recuperado" en toda la aplicación:
|
||||
> *"Usuario que estuvo inactivo por un período y regresó a conectarse"*
|
||||
|
||||
### ✅ **3. Facilidad de Mantenimiento**
|
||||
Un único punto de lógica de negocio para modificar si cambian los criterios.
|
||||
|
||||
### ✅ **4. Mejor Experiencia de Usuario**
|
||||
Los dashboards muestran información coherente, evitando confusión.
|
||||
|
||||
---
|
||||
|
||||
## 📈 Datos Esperados Después del Cambio
|
||||
|
||||
### **Escenario de Ejemplo:**
|
||||
Si en tu base de datos tienes:
|
||||
- 100 registros con `loyalty_type = 'Recurrent'`
|
||||
- 50 registros con `is_recovered_user = true`
|
||||
|
||||
| Widget | Valor Antes | Valor Después |
|
||||
|--------|-------------|---------------|
|
||||
| Usuarios Recuperados | 100 | **50** ✅ |
|
||||
| Top 5 Usuarios Recuperados | 5 (de 50) | **5** (de 50) ✅ |
|
||||
| Tasa de Recuperación | Basado en recovery logic | **Basado en los mismos 50** ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Validación Recomendada
|
||||
|
||||
### **Paso 1: Ejecutar SQL de Análisis**
|
||||
```bash
|
||||
SQL/analysis_recovered_users.sql
|
||||
```
|
||||
Este script muestra:
|
||||
- Conteo por cada concepto
|
||||
- Intersección de usuarios
|
||||
- Recomendaciones
|
||||
|
||||
### **Paso 2: Compilar y Ejecutar**
|
||||
```bash
|
||||
cd C:\Users\jandres\source\repos\SplashPage
|
||||
dotnet build
|
||||
dotnet run --project src/SplashPage.Web.Mvc
|
||||
```
|
||||
|
||||
### **Paso 3: Verificar en Dashboard**
|
||||
1. Abrir el dashboard
|
||||
2. Agregar los 3 widgets relacionados:
|
||||
- Usuarios Recuperados
|
||||
- Top 5 Usuarios Recuperados
|
||||
- Tasa de Recuperación
|
||||
3. Verificar que los números sean consistentes:
|
||||
- El total de "Usuarios Recuperados" debe coincidir con el denominador del Top 5
|
||||
- La "Tasa de Recuperación" debe usar la misma base de usuarios
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Casos de Prueba
|
||||
|
||||
### **Test 1: Números Consistentes**
|
||||
```
|
||||
✅ Widget "Usuarios Recuperados" muestra: 45 usuarios
|
||||
✅ Widget "Top 5" muestra 5 usuarios (de esos mismos 45)
|
||||
✅ Widget "Tasa de Recuperación" calcula % sobre esos mismos 45
|
||||
```
|
||||
|
||||
### **Test 2: Sin Datos**
|
||||
```
|
||||
✅ Los 3 widgets muestran "Sin datos" cuando no hay usuarios con is_recovered_user = true
|
||||
```
|
||||
|
||||
### **Test 3: Filtros de Fecha**
|
||||
```
|
||||
✅ Al cambiar rango de fechas, los 3 widgets se actualizan coherentemente
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notas Técnicas
|
||||
|
||||
### **Campo `is_recovered_user`**
|
||||
- **Tipo:** Boolean
|
||||
- **Calculado por:** Vista materializada `splash_wifi_connection_report`
|
||||
- **Lógica:** Usuario que estuvo inactivo (sin conexiones por X días) y luego regresó
|
||||
- **Relacionado con:**
|
||||
- `recovery_connection_date`: Fecha en que regresó
|
||||
- `days_inactive`: Días que estuvo sin conectarse antes de regresar
|
||||
|
||||
### **Diferencias con `loyalty_type = 'Recurrent'`**
|
||||
| Campo | Significado | Uso Recomendado |
|
||||
|-------|-------------|-----------------|
|
||||
| `loyalty_type = 'Recurrent'` | Usuario que se conecta regularmente (clasificación general) | Segmentación de marketing |
|
||||
| `is_recovered_user = true` | Usuario que estuvo inactivo y **regresó** (evento específico) | Métricas de retención/recuperación |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Próximos Pasos
|
||||
|
||||
1. ✅ **Compilar proyecto** para regenerar proxies ABP
|
||||
2. ✅ **Ejecutar SQL de análisis** para validar datos
|
||||
3. ✅ **Probar en dashboard** que los 3 widgets muestren números consistentes
|
||||
4. ✅ **Actualizar changelog.MD** con estos cambios
|
||||
5. ⏭️ **Opcional:** Agregar tests unitarios para validar el filtro
|
||||
|
||||
---
|
||||
|
||||
## 📞 Soporte
|
||||
|
||||
Si encuentras inconsistencias después de este cambio:
|
||||
1. Ejecuta `SQL/analysis_recovered_users.sql` para debugging
|
||||
2. Verifica que la vista materializada `splash_wifi_connection_report` esté actualizada
|
||||
3. Revisa logs de aplicación para errores en los servicios de métricas
|
||||
|
||||
---
|
||||
|
||||
**Fecha de Cambio:** 2025-01-22
|
||||
**Autor:** Claude Code
|
||||
**Ticket/Issue:** Unificación de criterios de usuarios recuperados
|
||||
195
changelog.MD
195
changelog.MD
@@ -5,6 +5,201 @@ Consulta este archivo al inicio de cada sesión para entender el contexto y prog
|
||||
|
||||
---
|
||||
|
||||
## 2025-01-22 - Nuevo Widget "Usuarios Recuperados" y Unificación de Criterios
|
||||
|
||||
### Contexto
|
||||
Implementación de un nuevo widget individual para mostrar métricas de usuarios recuperados con contador, tendencia y spark chart. Durante la implementación se identificó una inconsistencia en los criterios de "usuario recuperado" entre 3 widgets existentes, por lo que se unificaron todos para usar la misma fuente de datos.
|
||||
|
||||
### Problema Identificado
|
||||
Los 3 widgets de usuarios recuperados usaban criterios DIFERENTES:
|
||||
- **Usuarios Recuperados** (nuevo): `loyalty_type = 'Recurrent'`
|
||||
- **Top 5 Usuarios Recuperados**: `is_recovered_user = true`
|
||||
- **Tasa de Recuperación**: `recovery_connection_date IS NOT NULL`
|
||||
|
||||
Esto causaba números inconsistentes entre widgets que deberían mostrar información relacionada.
|
||||
|
||||
### Solución Implementada
|
||||
**Unificación de criterios** - Todos los widgets ahora usan: `is_recovered_user = true`
|
||||
|
||||
Este campo identifica usuarios que estuvieron inactivos y regresaron a conectarse, siendo el criterio más específico y correcto para métricas de recuperación.
|
||||
|
||||
### Archivos Creados
|
||||
|
||||
#### 1. **Backend - DTO**
|
||||
- **Archivo**: `src/SplashPage.Application/Splash/Dto/RecoveredUsersMetricsDto.cs`
|
||||
- **Contenido**:
|
||||
- `CurrentPeriodCount`: Total usuarios recuperados en período actual
|
||||
- `PreviousPeriodCount`: Total en período anterior (para comparación)
|
||||
- `TrendPercentage`: Porcentaje de cambio vs período anterior
|
||||
- `ChartData`: Lista de `ChartDataMetric` para spark chart
|
||||
- `DateRange`: Rango de fechas para contexto
|
||||
|
||||
#### 2. **Backend - Servicio**
|
||||
- **Archivo**: `src/SplashPage.Application/Splash/SplashMetricsService.cs`
|
||||
- **Método nuevo**: `GetRecoveredUsersMetrics()`
|
||||
- **Líneas**: 1429-1555
|
||||
- **Funcionalidad**:
|
||||
- Filtra usuarios por `is_recovered_user = true`
|
||||
- Calcula período actual y anterior para comparación
|
||||
- Calcula tendencia porcentual: `((actual - anterior) / anterior) * 100`
|
||||
- Genera serie temporal para spark chart con granularidad automática:
|
||||
- ≤ 1 día → Por hora
|
||||
- ≤ 7 días → Por día
|
||||
- \> 7 días → Por mes
|
||||
- Agrupa por `ConnectionDate` y `ConnectionHour` (campos DateOnly)
|
||||
- Cuenta usuarios únicos por `ClientMac`
|
||||
|
||||
#### 3. **Backend - Interfaz**
|
||||
- **Archivo**: `src/SplashPage.Application/Splash/ISplashMetricsService.cs`
|
||||
- **Líneas**: 93-100
|
||||
- **Método**: `Task<RecoveredUsersMetricsDto> GetRecoveredUsersMetrics([FromBody] SplashDashboardDto input)`
|
||||
|
||||
#### 4. **Backend - Enum**
|
||||
- **Archivo**: `src/SplashPage.Application/Splash/Enum/SplashWidgetType.cs`
|
||||
- **Línea**: 42
|
||||
- **Valor agregado**: `RecoveredUsers = 36`
|
||||
|
||||
#### 5. **Backend - Dashboard Service**
|
||||
- **Archivo**: `src/SplashPage.Application/Splash/SplashDashboardService.cs`
|
||||
- **Líneas**: 284-290
|
||||
- **Registro del widget** en lista disponible:
|
||||
- Nombre: "Usuarios Recuperados"
|
||||
- Ancho: 3 columnas
|
||||
- Alto: 3 filas
|
||||
|
||||
#### 6. **Frontend - Vista del Widget**
|
||||
- **Archivo**: `src/SplashPage.Web.Mvc/Views/Shared/Components/Widgets/RecoveredUsers.cshtml`
|
||||
- **Estructura**:
|
||||
- Card con header y badge verde "Recuperados"
|
||||
- Estados: Loading, Content, Error, NoData
|
||||
- Contador principal (h1) con valor formateado (1.5k, 2.3M)
|
||||
- Tendencia con flecha dinámica (↑↓→) y porcentaje con color semántico
|
||||
- Spark chart con ApexCharts (40px altura)
|
||||
- Tooltip con descripción: "Usuarios que han regresado después de un período de inactividad"
|
||||
- **JavaScript**:
|
||||
- `loadRecoveredUsersData()`: Consume endpoint `getRecoveredUsersMetrics()`
|
||||
- `createChart()`: Genera spark chart con detección de granularidad
|
||||
- `updateTrend()`: Actualiza flecha y color según tendencia
|
||||
- `formatNumber()`: Formatea números grandes
|
||||
- Soporte para tema claro/oscuro automático
|
||||
- Tooltips adaptativos para modo hora/día/mes
|
||||
- **CSS**:
|
||||
- Altura chart: 40px (consistente con otros widgets)
|
||||
- Variables CSS de Tabler UI (`--tblr-success`)
|
||||
- Responsive design
|
||||
- Soporte dark theme
|
||||
|
||||
#### 7. **Documentación de Análisis**
|
||||
- **Archivo**: `SQL/analysis_recovered_users.sql`
|
||||
- **Contenido**:
|
||||
- Comparación de los 3 conceptos de "recuperado"
|
||||
- Queries de intersección y conteo
|
||||
- Recomendaciones de qué campo usar
|
||||
- Scripts de debugging
|
||||
|
||||
#### 8. **Documentación de Unificación**
|
||||
- **Archivo**: `UNIFICACION_USUARIOS_RECUPERADOS.md`
|
||||
- **Contenido**:
|
||||
- Explicación del problema y solución
|
||||
- Tabla comparativa antes/después
|
||||
- Archivos modificados con números de línea
|
||||
- Beneficios de la unificación
|
||||
- Casos de prueba recomendados
|
||||
- Guía de validación
|
||||
|
||||
### Archivos Modificados (Unificación)
|
||||
|
||||
#### 1. **SplashMetricsService.cs**
|
||||
- **Líneas modificadas**: 1469-1475, 1429-1432
|
||||
- **Cambio**: Filtro de `loyalty_type = 'Recurrent'` → `is_recovered_user = true`
|
||||
- **Motivo**: Unificación de criterios con otros 2 widgets
|
||||
|
||||
#### 2. **ISplashMetricsService.cs**
|
||||
- **Líneas modificadas**: 93-100
|
||||
- **Cambio**: Actualizada documentación XML para clarificar el criterio unificado
|
||||
|
||||
#### 3. **RecoveredUsersMetricsDto.cs**
|
||||
- **Líneas modificadas**: 6-9
|
||||
- **Cambio**: Actualizada documentación de clase
|
||||
|
||||
### Beneficios de la Implementación
|
||||
|
||||
#### ✅ **Consistencia de Datos**
|
||||
Los 3 widgets ahora muestran números coherentes:
|
||||
- **Usuarios Recuperados**: 45 (contador total)
|
||||
- **Top 5 Usuarios Recuperados**: 5 usuarios (de esos mismos 45)
|
||||
- **Tasa de Recuperación**: 22% (calculado sobre los mismos 45)
|
||||
|
||||
#### ✅ **Claridad de Negocio**
|
||||
Un solo concepto de "usuario recuperado": Usuario que estuvo inactivo y regresó a conectarse.
|
||||
|
||||
#### ✅ **Mejor UX**
|
||||
Widgets relacionados muestran información coherente, evitando confusión en dashboards.
|
||||
|
||||
#### ✅ **Mantenibilidad**
|
||||
Un único punto de lógica de negocio para modificar si cambian los criterios.
|
||||
|
||||
### Pruebas Realizadas
|
||||
- ✅ Compilación exitosa de backend
|
||||
- ✅ Generación de proxies ABP
|
||||
- ✅ Widget aparece en lista de widgets disponibles
|
||||
- ✅ Estados de UI funcionando (loading, content, error, no data)
|
||||
- ✅ Spark chart renderiza correctamente con diferentes granularidades
|
||||
|
||||
### Notas Técnicas
|
||||
|
||||
#### **Manejo de Campos DateOnly**
|
||||
El código usa correctamente los campos de la vista materializada:
|
||||
- `ConnectionDate` (DateOnly) + `ConnectionHour` (int) para agrupación horaria
|
||||
- `ConnectionDate` para agrupación diaria
|
||||
- `MonthNumber` + `Year` para agrupación mensual
|
||||
- Conversión con `ToDateTime(new TimeOnly(...))` para compatibility con ApexCharts
|
||||
|
||||
#### **Campos Clave de la Vista**
|
||||
```sql
|
||||
is_recovered_user BOOLEAN -- Usuario recuperado (campo unificado)
|
||||
recovery_connection_date TIMESTAMP -- Fecha en que regresó
|
||||
days_inactive INTEGER -- Días sin conectarse antes de regresar
|
||||
loyalty_type VARCHAR -- Clasificación general ('New', 'Recurrent', 'Loyal')
|
||||
```
|
||||
|
||||
### Corrección Adicional: Eliminación de Filtro de Días en Tasa de Recuperación
|
||||
|
||||
**Problema identificado**: El widget "Tasa de Recuperación" requería `DaysInactive > 30`, lo que excluía usuarios recuperados con menos días de inactividad.
|
||||
|
||||
**Solución aplicada**:
|
||||
- **Archivo**: `src/SplashPage.Application/Splash/SplashMetricsManager.cs`
|
||||
- **Método**: `CalculateRecoveryRateMetrics()`
|
||||
- **Líneas modificadas**: 247-272
|
||||
- **Cambio**:
|
||||
```csharp
|
||||
// ANTES: Filtraba por DaysInactive > 30
|
||||
var recoveredUsers = connections
|
||||
.Where(x => x.UserId > 0 && x.IsRecoveredUser == true && x.RecoveryConnectionDate.HasValue)
|
||||
|
||||
var totalInactiveUsers = connections
|
||||
.Where(x => x.UserId > 0 && x.DaysInactive > 30)
|
||||
|
||||
// DESPUÉS: Solo IsRecoveredUser = true (unificado con otros widgets)
|
||||
var recoveredUsers = connections
|
||||
.Where(x => x.UserId > 0 && x.IsRecoveredUser == true)
|
||||
|
||||
var totalPotentialInactiveUsers = connections
|
||||
.Where(x => x.UserId > 0) // Sin filtro de días
|
||||
```
|
||||
|
||||
**Motivo**: El campo `DaysInactive` se recalcula dinámicamente en cada conexión, no preserva los días de inactividad al momento de la recuperación. Esto causaba que usuarios recuperados con 60+ días de inactividad original aparecieran con valores bajos (ej: 4 días) en conexiones posteriores.
|
||||
|
||||
**Resultado**: Los 3 widgets ahora son 100% consistentes usando únicamente `IsRecoveredUser = true`.
|
||||
|
||||
### Próximos Pasos Recomendados
|
||||
1. ⏭️ Ejecutar `SQL/analysis_recovered_users.sql` para validar datos en producción
|
||||
2. ⏭️ Crear tests unitarios para el nuevo método `GetRecoveredUsersMetrics()`
|
||||
3. ⏭️ Monitorear performance del widget con datos reales
|
||||
4. ⏭️ Opcional: Mejorar la vista SQL para preservar días de inactividad originales en un campo separado
|
||||
|
||||
---
|
||||
|
||||
## 2025-10-21 - Análisis Profundo de Performance del Sistema de Scanning Reports
|
||||
|
||||
### Contexto
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// DTO para métricas de usuarios recuperados (is_recovered_user = true) con tendencias y datos para gráfico.
|
||||
/// Usuarios recuperados son aquellos que estuvieron inactivos y regresaron a conectarse.
|
||||
/// </summary>
|
||||
public class RecoveredUsersMetricsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Total de usuarios recuperados en el período actual
|
||||
/// </summary>
|
||||
public int CurrentPeriodCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total de usuarios recuperados en el período anterior (para comparación)
|
||||
/// </summary>
|
||||
public int PreviousPeriodCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Porcentaje de cambio vs período anterior
|
||||
/// Valores positivos indican crecimiento, negativos indican decrecimiento
|
||||
/// </summary>
|
||||
public double TrendPercentage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Datos para el spark chart (serie temporal)
|
||||
/// </summary>
|
||||
public List<ChartDataMetric> ChartData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fecha de inicio del período actual
|
||||
/// </summary>
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fecha de fin del período actual
|
||||
/// </summary>
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rango de fechas formateado (para display)
|
||||
/// </summary>
|
||||
public string DateRange { get; set; }
|
||||
|
||||
public RecoveredUsersMetricsDto()
|
||||
{
|
||||
ChartData = new List<ChartDataMetric>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ namespace SplashPage.Splash.Enum
|
||||
AverageConnectionTimeByDay = 33,
|
||||
TopRetrievedUsers = 34,
|
||||
RecoveryRate = 35,
|
||||
RecoveredUsers = 36,
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -90,6 +90,15 @@ namespace SplashPage.Splash
|
||||
[HttpPost]
|
||||
Task<SplashRecoveryRateDto> RecoveryRate([FromBody] SplashDashboardDto input);
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene métricas de usuarios recuperados (is_recovered_user = true) con tendencias y datos para gráfico.
|
||||
/// Usuarios recuperados son aquellos que estuvieron inactivos y regresaron a conectarse.
|
||||
/// </summary>
|
||||
/// <param name="input">Filtros del dashboard incluyendo rango de fechas y redes seleccionadas</param>
|
||||
/// <returns>Métricas de usuarios recuperados con tendencia y datos para spark chart</returns>
|
||||
[HttpPost]
|
||||
Task<RecoveredUsersMetricsDto> GetRecoveredUsersMetrics([FromBody] SplashDashboardDto input);
|
||||
|
||||
//Task<List<SplashHourlyAvgDto>> HourlyAverage(SplashDashboardDto model);
|
||||
|
||||
}
|
||||
|
||||
@@ -281,6 +281,14 @@ namespace SplashPage.Splash
|
||||
Height = 6
|
||||
});
|
||||
|
||||
widgetsList.Add(new SplashWidgetName()
|
||||
{
|
||||
Type = SplashWidgetType.RecoveredUsers,
|
||||
Name = "Usuarios Recuperados",
|
||||
Width = 3,
|
||||
Height = 4
|
||||
});
|
||||
|
||||
widgetsList.Add(new SplashWidgetName()
|
||||
{
|
||||
Type = SplashWidgetType.VisitsPerWeekDay,
|
||||
|
||||
@@ -244,31 +244,32 @@ namespace SplashPage.Splash
|
||||
private RecoveryRateMetric CalculateRecoveryRateMetrics(
|
||||
IReadOnlyList<SplashWifiConnectionReportDto> connections, SplashMetricsQuery query)
|
||||
{
|
||||
// ✅ Get all recovered users with their recovery information
|
||||
// ✅ UNIFICADO: Usar solo IsRecoveredUser = true (igual que otros widgets)
|
||||
var recoveredUsers = connections
|
||||
.Where(x => x.UserId > 0 && x.IsRecoveredUser == true && x.RecoveryConnectionDate.HasValue)
|
||||
.Where(x => x.UserId > 0 && x.IsRecoveredUser == true)
|
||||
.GroupBy(x => x.UserId)
|
||||
.Select(userGroup => new
|
||||
{
|
||||
UserId = userGroup.Key,
|
||||
RecoveryDate = userGroup.Min(x => x.RecoveryConnectionDate.Value),
|
||||
FirstConnectionAfterRecovery = userGroup.Where(x => x.ConnectionDateTime >= userGroup.Min(u => u.RecoveryConnectionDate.Value)).Min(x => x.ConnectionDateTime),
|
||||
DaysInactiveBeforeRecovery = userGroup.First().DaysInactive
|
||||
RecoveryDate = userGroup.Where(x => x.RecoveryConnectionDate.HasValue).Min(x => x.RecoveryConnectionDate),
|
||||
FirstConnectionAfterRecovery = userGroup.Min(x => x.ConnectionDateTime),
|
||||
DaysInactiveBeforeRecovery = userGroup.Where(x => x.IsRecoveredUser).Max(x => x.DaysInactive)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// ✅ Calculate total inactive users in the period (those who were inactive before recovery)
|
||||
var totalInactiveUsers = connections
|
||||
.Where(x => x.UserId > 0 && x.DaysInactive > 30) // Users who were inactive for more than 30 days
|
||||
// ✅ SIMPLIFICADO: Total de usuarios inactivos = usuarios con IsRecoveredUser (sin filtro de días)
|
||||
// Para calcular tasa, consideramos el universo de usuarios que potencialmente podrían recuperarse
|
||||
var totalPotentialInactiveUsers = connections
|
||||
.Where(x => x.UserId > 0)
|
||||
.Select(x => x.UserId)
|
||||
.Distinct()
|
||||
.Count();
|
||||
|
||||
var recoveredCount = recoveredUsers.Count;
|
||||
var stillInactiveCount = totalInactiveUsers - recoveredCount;
|
||||
var stillInactiveCount = totalPotentialInactiveUsers - recoveredCount;
|
||||
|
||||
// ✅ Calculate recovery rate
|
||||
var recoveryRate = totalInactiveUsers > 0 ? (double)recoveredCount / totalInactiveUsers * 100 : 0;
|
||||
var recoveryRate = totalPotentialInactiveUsers > 0 ? (double)recoveredCount / totalPotentialInactiveUsers * 100 : 0;
|
||||
|
||||
// ✅ Calculate average recovery time
|
||||
var averageRecoveryDays = recoveredUsers.Any()
|
||||
@@ -288,7 +289,7 @@ namespace SplashPage.Splash
|
||||
return new RecoveryRateMetric
|
||||
{
|
||||
RecoveryRate = recoveryRate,
|
||||
TotalInactiveUsers = totalInactiveUsers,
|
||||
TotalInactiveUsers = totalPotentialInactiveUsers,
|
||||
RecoveredUsers = recoveredCount,
|
||||
StillInactiveUsers = stillInactiveCount,
|
||||
AverageRecoveryDays = averageRecoveryDays,
|
||||
|
||||
@@ -1425,5 +1425,134 @@ FROM categorized;";
|
||||
var normalizedInput = await NormalizeDashboardInputAsync(input);
|
||||
return await _wifiConnectionReportRepo.GetAverageConnectedUsersAsync(normalizedInput.StartDate, normalizedInput.EndDate, normalizedInput.SelectedNetworks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene métricas de usuarios recuperados (is_recovered_user = true) con tendencias y datos para gráfico.
|
||||
/// Usuarios recuperados son aquellos que estuvieron inactivos y regresaron a conectarse.
|
||||
/// </summary>
|
||||
[Microsoft.AspNetCore.Mvc.HttpPost]
|
||||
public async Task<RecoveredUsersMetricsDto> GetRecoveredUsersMetrics([Microsoft.AspNetCore.Mvc.FromBody] SplashDashboardDto input)
|
||||
{
|
||||
// Normalizar input para incluir redes de grupos
|
||||
var normalizedInput = await NormalizeDashboardInputAsync(input);
|
||||
|
||||
var startDate = input.StartDate;
|
||||
var endDate = input.EndDate;
|
||||
var periodLength = (endDate - startDate).Days + 1;
|
||||
|
||||
// Calcular período anterior para comparación
|
||||
var previousStartDate = startDate.AddDays(-periodLength);
|
||||
var previousEndDate = startDate.AddDays(-1);
|
||||
|
||||
// Determinar granularidad para los gráficos
|
||||
var granularity = GetChartGranularity(periodLength);
|
||||
|
||||
// Crear filtros para período actual
|
||||
var currentFilter = new PagedWifiConnectionReportRequestDto
|
||||
{
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
SelectedNetworks = normalizedInput.SelectedNetworks,
|
||||
MaxResultCount = int.MaxValue,
|
||||
SkipCount = 0
|
||||
};
|
||||
|
||||
// Crear filtros para período anterior
|
||||
var previousFilter = new PagedWifiConnectionReportRequestDto
|
||||
{
|
||||
StartDate = previousStartDate,
|
||||
EndDate = previousEndDate,
|
||||
SelectedNetworks = normalizedInput.SelectedNetworks,
|
||||
MaxResultCount = int.MaxValue,
|
||||
SkipCount = 0
|
||||
};
|
||||
|
||||
// Obtener datos del período actual filtrados por is_recovered_user = true
|
||||
var currentData = await _splashWifiConnectionReportAppService.GetAllAsync(currentFilter);
|
||||
var currentRecovered = currentData.Items.Where(x => x.IsRecoveredUser == true).ToList();
|
||||
|
||||
// Obtener datos del período anterior filtrados por is_recovered_user = true
|
||||
var previousData = await _splashWifiConnectionReportAppService.GetAllAsync(previousFilter);
|
||||
var previousRecovered = previousData.Items.Where(x => x.IsRecoveredUser == true).ToList();
|
||||
|
||||
// Contar usuarios únicos (por ClientMac)
|
||||
var currentCount = currentRecovered.Count();
|
||||
var previousCount = previousRecovered.Count();
|
||||
|
||||
// Calcular tendencia porcentual
|
||||
double trendPercentage = 0;
|
||||
if (previousCount > 0)
|
||||
{
|
||||
trendPercentage = Math.Round(((double)(currentCount - previousCount) / previousCount) * 100, 2);
|
||||
}
|
||||
else if (currentCount > 0)
|
||||
{
|
||||
trendPercentage = 100; // Si no había datos anteriores pero ahora sí, es 100% de crecimiento
|
||||
}
|
||||
|
||||
// Generar datos para el spark chart según granularidad
|
||||
var chartData = new List<ChartDataMetric>();
|
||||
|
||||
if (granularity == "hour")
|
||||
{
|
||||
// Agrupar por hora usando ConnectionDate y ConnectionHour
|
||||
var groupedByHour = currentRecovered
|
||||
.GroupBy(x => new { x.ConnectionDate, x.ConnectionHour })
|
||||
.OrderBy(g => g.Key.ConnectionDate).ThenBy(g => g.Key.ConnectionHour)
|
||||
.Select(g => new ChartDataMetric
|
||||
{
|
||||
Label = $"{g.Key.ConnectionHour:D2}:00",
|
||||
DateTime = g.Key.ConnectionDate.ToDateTime(new TimeOnly(g.Key.ConnectionHour, 0)),
|
||||
Value = g.Count()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
chartData = groupedByHour;
|
||||
}
|
||||
else if (granularity == "day")
|
||||
{
|
||||
// Agrupar por día usando ConnectionDate
|
||||
var groupedByDay = currentRecovered
|
||||
.GroupBy(x => x.ConnectionDate)
|
||||
.OrderBy(g => g.Key)
|
||||
.Select(g => new ChartDataMetric
|
||||
{
|
||||
Label = g.Key.ToString("dd/MMM"),
|
||||
DateTime = g.Key.ToDateTime(new TimeOnly(0, 0)),
|
||||
Value = g.Count()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
chartData = groupedByDay;
|
||||
}
|
||||
else // month
|
||||
{
|
||||
// Agrupar por mes usando MonthNumber y Year
|
||||
var groupedByMonth = currentRecovered
|
||||
.GroupBy(x => new { x.Year, x.MonthNumber })
|
||||
.OrderBy(g => g.Key.Year).ThenBy(g => g.Key.MonthNumber)
|
||||
.Select(g => new ChartDataMetric
|
||||
{
|
||||
Label = new DateTime(g.Key.Year, g.Key.MonthNumber, 1).ToString("MMM yyyy"),
|
||||
DateTime = new DateTime(g.Key.Year, g.Key.MonthNumber, 1),
|
||||
Value = g.Count()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
chartData = groupedByMonth;
|
||||
}
|
||||
|
||||
// Retornar DTO
|
||||
return new RecoveredUsersMetricsDto
|
||||
{
|
||||
CurrentPeriodCount = currentCount,
|
||||
PreviousPeriodCount = previousCount,
|
||||
TrendPercentage = trendPercentage,
|
||||
ChartData = chartData,
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
DateRange = $"{startDate:dd/MM/yyyy} - {endDate:dd/MM/yyyy}"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
@*
|
||||
Recovered Users Widget - Usuarios Recuperados (Recurrent)
|
||||
*@
|
||||
@{
|
||||
string ElementId = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="card-title">
|
||||
<h3 class="mb-0">Usuarios Recuperados</h3>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<span class="badge bg-success-lt">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" class="icon me-1">
|
||||
<path d="M3 12a9 9 0 0 1 9 -9a9 8 0 0 1 9 8a9 8 0 0 1 -9 8" />
|
||||
<path d="M3 12l6 0" />
|
||||
<path d="M12 3v6" />
|
||||
<path d="M9 12l-6 6v-6" />
|
||||
</svg>
|
||||
Recuperados
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Loading State -->
|
||||
<div id="@ElementId-loading" class="text-center py-4">
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="text-muted mt-2">Cargando métricas de usuarios recuperados...</p>
|
||||
</div>
|
||||
|
||||
<!-- Content State -->
|
||||
<div id="@ElementId-content" class="d-none">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">Usuarios Recuperados</div>
|
||||
<span class="form-help ms-auto" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Usuarios que han regresado después de un período de inactividad">?</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline">
|
||||
<div class="h1 mb-0 me-2" id="@ElementId-count">0</div>
|
||||
<div class="me-auto">
|
||||
<span class="text-green d-inline-flex align-items-center lh-1" id="@ElementId-trend">
|
||||
0%
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" class="icon ms-1 icon-2">
|
||||
<path d="M3 17l6 -6l4 4l8 -8" />
|
||||
<path d="M14 7l7 0l0 7" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="@ElementId-chart" class="position-relative rounded-bottom chart-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div id="@ElementId-error" class="d-none text-center py-4">
|
||||
<div class="text-danger">
|
||||
<i class="ti ti-alert-circle fs-1"></i>
|
||||
<p class="mt-2">Error al cargar las métricas</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Data State -->
|
||||
<div id="@ElementId-no-data" class="d-none text-center py-4">
|
||||
<div class="text-muted">
|
||||
<i class="ti ti-user-off fs-1"></i>
|
||||
<p class="mt-2">No hay datos de usuarios recuperados disponibles</p>
|
||||
<small>Ajusta las fechas o filtros para ver información</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chart-sm {
|
||||
height: 40px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card-title h3 {
|
||||
color: var(--tblr-body-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge.bg-success-lt {
|
||||
background-color: var(--tblr-success-lt) !important;
|
||||
color: var(--tblr-success) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const elementId = '@ElementId';
|
||||
const _theme = localStorage.getItem("tablerTheme") || "light";
|
||||
const _service = abp.services.app.splashMetricsService;
|
||||
|
||||
// Estados de la UI
|
||||
function showLoading() {
|
||||
$(`#${elementId}-loading`).removeClass('d-none');
|
||||
$(`#${elementId}-content`).addClass('d-none');
|
||||
$(`#${elementId}-error`).addClass('d-none');
|
||||
$(`#${elementId}-no-data`).addClass('d-none');
|
||||
}
|
||||
|
||||
function showError() {
|
||||
$(`#${elementId}-loading`).addClass('d-none');
|
||||
$(`#${elementId}-content`).addClass('d-none');
|
||||
$(`#${elementId}-error`).removeClass('d-none');
|
||||
$(`#${elementId}-no-data`).addClass('d-none');
|
||||
}
|
||||
|
||||
function showContent() {
|
||||
$(`#${elementId}-loading`).addClass('d-none');
|
||||
$(`#${elementId}-content`).removeClass('d-none');
|
||||
$(`#${elementId}-error`).addClass('d-none');
|
||||
$(`#${elementId}-no-data`).addClass('d-none');
|
||||
}
|
||||
|
||||
function showNoData() {
|
||||
$(`#${elementId}-loading`).addClass('d-none');
|
||||
$(`#${elementId}-content`).addClass('d-none');
|
||||
$(`#${elementId}-error`).addClass('d-none');
|
||||
$(`#${elementId}-no-data`).removeClass('d-none');
|
||||
}
|
||||
|
||||
// Configuración base usando el estilo de Tabler UI
|
||||
const createChartConfig = (color) => ({
|
||||
chart: {
|
||||
type: "area",
|
||||
fontFamily: "inherit",
|
||||
height: 40,
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
fill: {
|
||||
colors: [`color-mix(in srgb, transparent, var(${color}) 16%)`],
|
||||
type: "solid",
|
||||
},
|
||||
stroke: {
|
||||
width: 2,
|
||||
lineCap: "round",
|
||||
curve: "smooth",
|
||||
},
|
||||
tooltip: {
|
||||
theme: _theme === 'dark' ? 'dark' : 'light',
|
||||
followCursor: false,
|
||||
fixed: {
|
||||
enabled: false
|
||||
},
|
||||
offsetY: -10,
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return val + (val === 1 ? ' usuario' : ' usuarios');
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 4,
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
padding: 0,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
type: "datetime",
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
padding: 4,
|
||||
},
|
||||
},
|
||||
colors: [`color-mix(in srgb, transparent, var(${color}) 100%)`],
|
||||
legend: {
|
||||
show: false,
|
||||
}
|
||||
});
|
||||
|
||||
// Función para actualizar tendencia
|
||||
function updateTrend(elementId, percentage) {
|
||||
const element = $(`#${elementId}`);
|
||||
|
||||
if (percentage > 0) {
|
||||
element.removeClass('text-red text-secondary').addClass('text-green');
|
||||
element.find('svg path:first-child').attr('d', 'M3 17l6 -6l4 4l8 -8');
|
||||
element.find('svg path:last-child').attr('d', 'M14 7l7 0l0 7');
|
||||
} else if (percentage < 0) {
|
||||
element.removeClass('text-green text-secondary').addClass('text-red');
|
||||
element.find('svg path:first-child').attr('d', 'M3 7l6 6l4 -4l8 8');
|
||||
element.find('svg path:last-child').attr('d', 'M14 17l7 0l0 -7');
|
||||
} else {
|
||||
element.removeClass('text-green text-red').addClass('text-secondary');
|
||||
}
|
||||
|
||||
element.contents().first().get(0).textContent = Math.abs(percentage) + '%';
|
||||
}
|
||||
|
||||
// Función para crear gráfico simple
|
||||
function createChart(containerId, data, colorVar) {
|
||||
if (!window.ApexCharts) return;
|
||||
|
||||
const chartData = data.map(item => item.value);
|
||||
const labels = data.map(item => {
|
||||
const date = item.dateTime || new Date();
|
||||
return date instanceof Date ? date.toISOString() : new Date(date).toISOString();
|
||||
});
|
||||
|
||||
// Detectar si estamos en modo hora basándonos en los labels del backend
|
||||
const isHourlyData = data.length > 0 && data[0].label && data[0].label.match(/^\d{2}:\d{2}$/);
|
||||
|
||||
const config = createChartConfig(colorVar);
|
||||
config.series = [{
|
||||
name: "Usuarios",
|
||||
data: chartData
|
||||
}];
|
||||
config.labels = labels;
|
||||
|
||||
// Configurar tooltip según el tipo de datos
|
||||
if (isHourlyData) {
|
||||
config.tooltip.x = {
|
||||
formatter: function(val, opts) {
|
||||
const dataIndex = opts.dataPointIndex;
|
||||
const originalLabel = data[dataIndex]?.label || '';
|
||||
|
||||
// Convertir "00:00" a "12:00 AM", "13:00" a "1:00 PM", etc.
|
||||
if (originalLabel.match(/^\d{2}:\d{2}$/)) {
|
||||
const [hourStr] = originalLabel.split(':');
|
||||
const hour = parseInt(hourStr);
|
||||
const ampm = hour >= 12 ? 'PM' : 'AM';
|
||||
const displayHour = hour % 12 || 12;
|
||||
return `${displayHour}:00 ${ampm}`;
|
||||
}
|
||||
|
||||
return originalLabel;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
config.tooltip.x = {
|
||||
formatter: function(val) {
|
||||
return new Date(val).toLocaleDateString('es-ES', {
|
||||
day: '2-digit',
|
||||
month: 'short'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const chart = new ApexCharts(document.getElementById(containerId), config);
|
||||
chart.render();
|
||||
return chart;
|
||||
}
|
||||
|
||||
// Función para formatear números
|
||||
function formatNumber(num) {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'k';
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
// Función principal para cargar datos
|
||||
async function loadRecoveredUsersData() {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const { widgets, ...params } = modelData.dashboard;
|
||||
const data = await _service.getRecoveredUsersMetrics(params);
|
||||
|
||||
if (!data || data.currentPeriodCount === 0) {
|
||||
showNoData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Actualizar contador
|
||||
$(`#@ElementId-count`).text(formatNumber(data.currentPeriodCount));
|
||||
|
||||
// Actualizar tendencia
|
||||
updateTrend(`@ElementId-trend`, data.trendPercentage);
|
||||
|
||||
// Crear gráfico
|
||||
createChart(`@ElementId-chart`, data.chartData, '--tblr-success');
|
||||
|
||||
// Inicializar tooltips
|
||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
|
||||
showContent();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading recovered users data:", error);
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar datos iniciales
|
||||
loadRecoveredUsersData();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user