changes: - Fix widget "Tasa de recuperados" calculation

- Added wigdet "Usuarios recuperados"
This commit is contained in:
2025-10-22 08:05:42 -06:00
parent 5739befd0d
commit 88aa4b788a
11 changed files with 1315 additions and 12 deletions

View 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;

View 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;

View 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

View File

@@ -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

View File

@@ -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>();
}
}
}

View File

@@ -39,6 +39,7 @@ namespace SplashPage.Splash.Enum
AverageConnectionTimeByDay = 33,
TopRetrievedUsers = 34,
RecoveryRate = 35,
RecoveredUsers = 36,

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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}"
};
}
}
}

View File

@@ -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>