feat: Added NetworkGroups
- Entity - Dtos - Manage Services - SQL Table creation - View & permissions
This commit is contained in:
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebFetch(domain:tabler.io)",
|
||||
"WebFetch(domain:docs.tabler.io)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
105
SQL/NetworkGroups.sql
Normal file
105
SQL/NetworkGroups.sql
Normal file
@@ -0,0 +1,105 @@
|
||||
-- Migration: AddNetworkGroups
|
||||
-- Description: Adds SplashNetworkGroup and SplashNetworkGroupMember tables
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Create SplashNetworkGroups table
|
||||
CREATE TABLE "SplashNetworkGroups" (
|
||||
"Id" serial PRIMARY KEY,
|
||||
"Name" character varying(256) NOT NULL,
|
||||
"Description" character varying(512),
|
||||
"IsActive" boolean NOT NULL DEFAULT true,
|
||||
"TenantId" integer NOT NULL,
|
||||
"CreationTime" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"CreatorUserId" bigint,
|
||||
"LastModificationTime" timestamp with time zone,
|
||||
"LastModifierUserId" bigint,
|
||||
"IsDeleted" boolean NOT NULL DEFAULT false,
|
||||
"DeleterUserId" bigint,
|
||||
"DeletionTime" timestamp with time zone
|
||||
);
|
||||
|
||||
-- Create SplashNetworkGroupMembers table
|
||||
CREATE TABLE "SplashNetworkGroupMembers" (
|
||||
"Id" serial PRIMARY KEY,
|
||||
"NetworkGroupId" integer NOT NULL,
|
||||
"NetworkId" integer NOT NULL,
|
||||
"TenantId" integer NOT NULL,
|
||||
"CreationTime" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"CreatorUserId" bigint
|
||||
);
|
||||
|
||||
-- Create indexes for SplashNetworkGroups
|
||||
CREATE UNIQUE INDEX "IX_SplashNetworkGroups_Name_TenantId"
|
||||
ON "SplashNetworkGroups" ("Name", "TenantId");
|
||||
|
||||
CREATE INDEX "IX_SplashNetworkGroups_TenantId"
|
||||
ON "SplashNetworkGroups" ("TenantId");
|
||||
|
||||
CREATE INDEX "IX_SplashNetworkGroups_IsActive"
|
||||
ON "SplashNetworkGroups" ("IsActive");
|
||||
|
||||
-- Create indexes for SplashNetworkGroupMembers
|
||||
CREATE UNIQUE INDEX "IX_SplashNetworkGroupMembers_NetworkGroupId_NetworkId"
|
||||
ON "SplashNetworkGroupMembers" ("NetworkGroupId", "NetworkId");
|
||||
|
||||
CREATE INDEX "IX_SplashNetworkGroupMembers_NetworkGroupId"
|
||||
ON "SplashNetworkGroupMembers" ("NetworkGroupId");
|
||||
|
||||
CREATE INDEX "IX_SplashNetworkGroupMembers_NetworkId"
|
||||
ON "SplashNetworkGroupMembers" ("NetworkId");
|
||||
|
||||
CREATE INDEX "IX_SplashNetworkGroupMembers_TenantId"
|
||||
ON "SplashNetworkGroupMembers" ("TenantId");
|
||||
|
||||
-- Add foreign key constraints
|
||||
ALTER TABLE "SplashNetworkGroupMembers"
|
||||
ADD CONSTRAINT "FK_SplashNetworkGroupMembers_SplashNetworkGroups_NetworkGroupId"
|
||||
FOREIGN KEY ("NetworkGroupId") REFERENCES "SplashNetworkGroups" ("Id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "SplashNetworkGroupMembers"
|
||||
ADD CONSTRAINT "FK_SplashNetworkGroupMembers_SplashMerakiNetworks_NetworkId"
|
||||
FOREIGN KEY ("NetworkId") REFERENCES "SplashMerakiNetworks" ("Id") ON DELETE CASCADE;
|
||||
|
||||
-- Add SelectedNetworkGroups column to SplashDashboards table
|
||||
ALTER TABLE "SplashDashboards"
|
||||
ADD COLUMN "SelectedNetworkGroups" integer[] DEFAULT '{}';
|
||||
|
||||
-- Create index for the new column
|
||||
CREATE INDEX "IX_SplashDashboards_SelectedNetworkGroups"
|
||||
ON "SplashDashboards" USING GIN ("SelectedNetworkGroups");
|
||||
|
||||
-- Insert migration history record (if using EF Core migrations table)
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250106000000_AddNetworkGroups', '8.0.0');
|
||||
|
||||
COMMIT;
|
||||
|
||||
Notas importantes sobre el script:
|
||||
|
||||
1. Tipos de datos PostgreSQL: Uso serial para auto-incremento y timestamp with time zone para fechas
|
||||
2. Arrays: PostgreSQL maneja arrays nativamente, por eso SelectedNetworkGroups es integer[]
|
||||
3. Índices GIN: Para el array SelectedNetworkGroups uso un índice GIN que es óptimo para arrays
|
||||
4. Cascada: Las foreign keys tienen ON DELETE CASCADE para mantener integridad
|
||||
5. Valores por defecto: Arrays vacíos se inicializan con '{}'
|
||||
|
||||
Para ejecutar este script:
|
||||
|
||||
# Si usas psql directamente:
|
||||
psql -h localhost -U your_user -d your_database -f migration_script.sql
|
||||
|
||||
# O copiarlo y pegarlo en tu cliente de PostgreSQL preferido
|
||||
|
||||
Verificación después de ejecutar:
|
||||
|
||||
-- Verificar que las tablas se crearon correctamente
|
||||
SELECT table_name FROM information_schema.tables
|
||||
WHERE table_name IN ('SplashNetworkGroups', 'SplashNetworkGroupMembers');
|
||||
|
||||
-- Verificar la nueva columna en SplashDashboards
|
||||
SELECT column_name, data_type FROM information_schema.columns
|
||||
WHERE table_name = 'SplashDashboards' AND column_name = 'SelectedNetworkGroups';
|
||||
|
||||
-- Verificar índices
|
||||
SELECT indexname FROM pg_indexes
|
||||
WHERE tablename IN ('SplashNetworkGroups', 'SplashNetworkGroupMembers');
|
||||
284
changelog.MD
284
changelog.MD
@@ -157,5 +157,289 @@ Consulta este archivo al inicio de cada sesión para entender el contexto y prog
|
||||
3. **Reinicio de progresión**: Recuperado → Recurrent (2da conexión) → Loyal (3ra+ conexión)
|
||||
4. **Zona horaria**: Todos los cálculos usan 'America/Mexico_City'
|
||||
|
||||
---
|
||||
|
||||
## 2025-01-09 - Implementación de Algoritmo Híbrido para Cálculo de Duración de Sesiones
|
||||
|
||||
### Cambios Realizados
|
||||
|
||||
#### 1. **Problema Identificado**
|
||||
- **Duración irreal**: `DurationMinutes` mostraba períodos de días/semanas (ej: 20,209 minutos = 14 días)
|
||||
- **Causa**: Campo representa "período de vida del registro" no sesiones reales de conectividad
|
||||
- **Impacto**: Métricas de tiempo de conexión no reflejaban uso real de la red
|
||||
|
||||
#### 2. **Investigación de Soluciones**
|
||||
- **Opción 2 (Implementada)**: Estimación basada en NetworkUsage con algoritmo híbrido
|
||||
- **Opción 3 (Evaluada)**: APIs granulares de Meraki para eventos de asociación/desasociación
|
||||
|
||||
#### 3. **Algoritmo Híbrido Implementado**
|
||||
|
||||
**a) Lógica Principal**:
|
||||
```sql
|
||||
LEAST(
|
||||
-- Método 1: Duración bruta del período
|
||||
EXTRACT(epoch FROM "LastSeen" - "FirstSeen") / 60,
|
||||
-- Método 2: Estimación basada en NetworkUsage
|
||||
CASE
|
||||
WHEN NetworkUsage > 1MB THEN (NetworkUsage/1024) * 0.1 min/MB
|
||||
WHEN NetworkUsage > 0 THEN 10% del período total
|
||||
ELSE 5% del período total
|
||||
END
|
||||
)
|
||||
```
|
||||
|
||||
**b) Beneficios del Enfoque**:
|
||||
- ✅ **Conservador**: Siempre toma el menor valor para evitar sobreestimación
|
||||
- ✅ **Flexible**: Maneja casos extremos de NetworkUsage o período temporal
|
||||
- ✅ **Auto-limitante**: Límites máximos (8h, 30min) protegen contra valores irreales
|
||||
- ✅ **Realista**: Sesiones cortas usan tiempo bruto, períodos largos usan estimación
|
||||
|
||||
#### 4. **Archivos Modificados**
|
||||
|
||||
**a) SQL View (`splash_wifi_connection_report.sql`)**:
|
||||
- **Campo actualizado**: `DurationMinutes` → algoritmo híbrido
|
||||
- **Nuevos campos**:
|
||||
- `UserAvgEstimatedMinutes`: Promedio por usuario
|
||||
- `SessionDurationCategory`: Quick (<30min), Medium (30min-2h), Extended (>2h)
|
||||
- **Validación de tipos**: `CAST(NetworkUsage AS bigint)` para compatibilidad
|
||||
|
||||
**b) Backend (C#)**:
|
||||
- **Entity** (`SplashWifiConnectionReport.cs`):
|
||||
- `DurationMinutes` cambiado a `decimal`
|
||||
- Agregados `UserAvgEstimatedMinutes`, `SessionDurationCategory`
|
||||
- **DTO** (`SplashWifiConnectionReportDto.cs`): Sincronizado con entity
|
||||
- **Service** (`SplashWifiConnectionReportAppService.cs`): Mapeo actualizado
|
||||
- **Entity Framework** (`SplashWifiConnectionReportConfiguration.cs`): Configuración de nuevos campos
|
||||
|
||||
**c) Frontend (JavaScript)**:
|
||||
- **Función habilitada**: `formatDuration()` descomentada
|
||||
- **Formato mejorado**: Manejo inteligente de minutos → horas → días
|
||||
- **Precisión decimal**: Soporte para valores decimales del nuevo cálculo
|
||||
|
||||
#### 5. **Resultados Obtenidos**
|
||||
|
||||
**Ejemplo Usuario "Omar Montoya" (SplashUserId = 22)**:
|
||||
- **Antes**: 20,209 min (14 días), 23,155 min (16 días)
|
||||
- **Después**: 20.42 min, 8.56 min, 1.85 min, 23.88 min, 875.89 min (14.6h)
|
||||
- **Mejora**: Duraciones realistas basadas en uso real de red
|
||||
|
||||
#### 6. **Configuración del Algoritmo**
|
||||
|
||||
**Factores Ajustables**:
|
||||
- **Factor NetworkUsage**: 0.1 minutos por MB
|
||||
- **Límite porcentual**: 30% del período total como máximo
|
||||
- **Límites absolutos**: 8 horas (uso moderado), 30 minutos (sin uso)
|
||||
|
||||
### Impacto en el Sistema
|
||||
- ✅ **Compatibilidad**: Mantiene estructura de campos existente
|
||||
- ✅ **Precisión mejorada**: Duraciones realistas para análisis
|
||||
- ✅ **Escalabilidad**: Algoritmo optimizado para datasets grandes
|
||||
- ✅ **Calibración**: Factores ajustables según patrones de uso real
|
||||
|
||||
### Archivos Modificados
|
||||
1. `splash_wifi_connection_report.sql` - Algoritmo híbrido implementado
|
||||
2. `SplashWifiConnectionReport.cs` - Entity actualizada
|
||||
3. `SplashWifiConnectionReportDto.cs` - DTO sincronizado
|
||||
4. `SplashWifiConnectionReportAppService.cs` - Mapeo actualizado
|
||||
5. `SplashWifiConnectionReportConfiguration.cs` - EF configurado
|
||||
6. `Index.js` - Frontend habilitado para nuevo formato
|
||||
7. `changelog.MD` - Documentación de cambios
|
||||
|
||||
### Estado Actual
|
||||
- ✅ Implementación completa del algoritmo híbrido
|
||||
- ✅ Backend y frontend sincronizados
|
||||
- ✅ Validación con datos reales completada
|
||||
- 📋 Listo para ajuste de factores según métricas reales
|
||||
|
||||
### Notas para Próxima Sesión
|
||||
- **Factores calibrables**: 0.1 min/MB, 30% límite, 8h/30min máximos
|
||||
- **Monitoreo**: Verificar distribución de `SessionDurationCategory`
|
||||
- **Optimización**: Posible implementación futura de Opción 3 (APIs granulares)
|
||||
- **Reinicio requerido**: Aplicación debe reiniciarse para tomar cambios de Entity Framework
|
||||
|
||||
### Análisis de Capacidades Meraki Realizado
|
||||
- ✅ **APIs disponibles**: `/networks/{id}/events`, `/networks/{id}/clients/{id}/connectionEvents`
|
||||
- ✅ **Event Log**: Eventos de asociación/desasociación con timestamps exactos
|
||||
- ❌ **Limitación actual**: Worker solo usa endpoints agregados, no eventos granulares
|
||||
- 📋 **Futuro**: Implementar worker adicional para captura de eventos en tiempo real
|
||||
|
||||
---
|
||||
|
||||
## 2025-09-06 - Corrección de Error HTTP 400 en Actualización de Grupos de Redes
|
||||
|
||||
### Problema Identificado
|
||||
- **Error**: HTTP 400 Bad Request al intentar actualizar grupos de redes
|
||||
- **Causa**: Inconsistencia entre el payload JSON del frontend y el model binding del controlador MVC
|
||||
- **JSON enviado**: `selectedNetworkIds: [92, 96, 101, ...]`
|
||||
- **DTO esperado**: `NetworkIds: [...]`
|
||||
|
||||
### Cambios Realizados
|
||||
|
||||
#### 1. **Corrección del Model Binding en NetworkGroupController.cs**
|
||||
- **Problema**: ViewModels no coincidían con el JSON enviado desde JavaScript
|
||||
- **Solución**: Creación de clases de request específicas para cada operación
|
||||
|
||||
**a) Método Update:**
|
||||
```csharp
|
||||
// Antes: [FromBody] EditNetworkGroupViewModel model
|
||||
// Después: [FromBody] UpdateGroupRequest request
|
||||
|
||||
public class UpdateGroupRequest
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public List<int> SelectedNetworkIds { get; set; } // Coincide con JSON
|
||||
}
|
||||
```
|
||||
|
||||
**b) Método Create (también corregido):**
|
||||
```csharp
|
||||
// Antes: [FromBody] CreateNetworkGroupViewModel model
|
||||
// Después: [FromBody] CreateGroupRequest request
|
||||
|
||||
public class CreateGroupRequest
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public List<int> SelectedNetworkIds { get; set; } // Coincide con JSON
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **Flujo de Datos Corregido**
|
||||
- **Frontend JS**: `selectedNetworkIds` → **Request Class**: `SelectedNetworkIds` → **DTO**: `NetworkIds`
|
||||
- **Mapeo consistente**: Los request objects ahora mapean correctamente al DTO interno
|
||||
|
||||
### Archivos Modificados
|
||||
1. `src/SplashPage.Web.Mvc/Controllers/NetworkGroupController.cs`
|
||||
- Método `Create`: Usa `CreateGroupRequest` en lugar de `CreateNetworkGroupViewModel`
|
||||
- Método `Update`: Usa `UpdateGroupRequest` en lugar de `EditNetworkGroupViewModel`
|
||||
- Agregadas clases de request internas para model binding correcto
|
||||
2. `changelog.MD` - Documentación de cambios
|
||||
|
||||
### Resultado Esperado
|
||||
- ✅ **HTTP 200**: Actualizaciones de grupos funcionando correctamente
|
||||
- ✅ **Model binding**: JSON parseado correctamente a objetos .NET
|
||||
- ✅ **Compatibilidad**: Frontend no requiere cambios, solo backend corregido
|
||||
- ✅ **Consistencia**: Ambos métodos Create/Update usan la misma estructura
|
||||
|
||||
### Estado Actual
|
||||
- ✅ Error HTTP 400 corregido
|
||||
- ✅ Model binding consistente implementado
|
||||
- ✅ Compatibilidad con UI moderna mantenida
|
||||
- 📋 Listo para testing de funcionalidad completa
|
||||
|
||||
### Notas para Próxima Sesión
|
||||
- **Testing requerido**: Verificar que tanto Create como Update funcionan con listas grandes de redes
|
||||
- **UI optimization**: La UI moderna con Tabler Components está implementada y optimizada
|
||||
- **Performance**: JavaScript optimizado con caching, request queueing, y virtual scrolling
|
||||
- **Payload ejemplo**: `{"id": 3, "name": "Region CO", "selectedNetworkIds": [92, 96, 101, ...]}`
|
||||
|
||||
---
|
||||
|
||||
## 2025-09-06 - Refactor a ABP Service Proxies (Misma Sesión)
|
||||
|
||||
### Problema Identificado
|
||||
- **Arquitectura inconsistente**: Se estaba usando controladores MVC personalizados en lugar de los service proxies automáticos de ABP
|
||||
- **Recomendación del usuario**: Usar el patrón establecido como en reportes (`abp.services.app.*`)
|
||||
- **Complejidad innecesaria**: Código duplicado entre controlador MVC y App Service
|
||||
|
||||
### Cambios Realizados
|
||||
|
||||
#### 1. **Refactor JavaScript a ABP Service Proxies**
|
||||
- **Antes**: Llamadas fetch manuales a `/NetworkGroup/*` endpoints
|
||||
- **Después**: Uso de `abp.services.app.splashNetworkGroup.*`
|
||||
|
||||
**Ejemplo de cambios:**
|
||||
```javascript
|
||||
// Antes (fetch manual)
|
||||
const response = await fetch('/NetworkGroup/Update', { method: 'POST', body: JSON.stringify(data) });
|
||||
|
||||
// Después (ABP service proxy)
|
||||
const result = await this._networkGroupService.update(groupData);
|
||||
```
|
||||
|
||||
#### 2. **Simplificación del Controlador MVC**
|
||||
- **Eliminados**: Todos los métodos API (Create, Update, Delete, Get, GetList, etc.)
|
||||
- **Conservado**: Solo el método `Index()` para mostrar la vista
|
||||
- **Resultado**: 90% reducción de código en el controlador
|
||||
|
||||
**Antes:** 160+ líneas con 8 métodos API
|
||||
**Después:** 16 líneas con 1 método de vista
|
||||
|
||||
#### 3. **Extensión del App Service**
|
||||
- **Agregado**: Método `GetStatisticsAsync()` para métricas del dashboard
|
||||
- **Nuevo DTO**: `NetworkGroupStatisticsDto` para estadísticas
|
||||
- **Mejorada**: Interfaz `ISplashNetworkGroupAppService` con nuevo método
|
||||
|
||||
#### 4. **Optimización de Llamadas JavaScript**
|
||||
```javascript
|
||||
// Inicialización de servicios ABP
|
||||
this._networkGroupService = abp.services.app.splashNetworkGroup;
|
||||
this._dashboardService = abp.services.app.splashDashboardService;
|
||||
|
||||
// Configuración de DataTable con ABP
|
||||
listAction: {
|
||||
ajaxFunction: this._networkGroupService.getAll,
|
||||
inputFilter: () => ({
|
||||
keyword: $('#globalSearch').val(),
|
||||
isActive: this.getCurrentFilterValue()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Beneficios del Refactor
|
||||
|
||||
#### **1. Consistencia Arquitectural**
|
||||
- ✅ **Patrón uniforme**: Ahora sigue el mismo patrón que reportes y otros módulos
|
||||
- ✅ **ABP Best Practices**: Usa las capacidades automáticas del framework
|
||||
- ✅ **Menos código**: Eliminación de proxy manual y wrapper controllers
|
||||
|
||||
#### **2. Mantenibilidad Mejorada**
|
||||
- ✅ **Single source of truth**: App Service es la única fuente de lógica de negocio
|
||||
- ✅ **Type safety**: ABP genera proxies tipados automáticamente
|
||||
- ✅ **Error handling**: Manejo de errores nativo de ABP
|
||||
|
||||
#### **3. Performance Optimizada**
|
||||
- ✅ **Menos roundtrips**: Eliminación de capa intermedia MVC
|
||||
- ✅ **Caching automático**: ABP gestiona caché de service proxies
|
||||
- ✅ **Serialización optimizada**: ABP usa serializers optimizados
|
||||
|
||||
### Archivos Modificados
|
||||
1. `src/SplashPage.Web.Mvc/wwwroot/js/views/networkGroup/index.js`
|
||||
- Convertido completamente a ABP service proxies
|
||||
- Eliminadas llamadas fetch() manuales
|
||||
- Agregada inicialización de servicios ABP
|
||||
2. `src/SplashPage.Web.Mvc/Controllers/NetworkGroupController.cs`
|
||||
- Simplificado a solo método `Index()`
|
||||
- Eliminados 7 métodos API innecesarios
|
||||
3. `src/SplashPage.Application/Splash/ISplashNetworkGroupAppService.cs`
|
||||
- Agregado método `GetStatisticsAsync()`
|
||||
4. `src/SplashPage.Application/Splash/SplashNetworkGroupAppService.cs`
|
||||
- Implementado método `GetStatisticsAsync()`
|
||||
5. `src/SplashPage.Application/Splash/Dto/NetworkGroupStatisticsDto.cs` (Nuevo)
|
||||
- DTO para estadísticas del dashboard
|
||||
6. `changelog.MD` - Documentación de cambios
|
||||
|
||||
### Estado Actual
|
||||
- ✅ **Refactor completo**: Migración a ABP service proxies completada
|
||||
- ✅ **Arquitectura consistente**: Patrón uniforme con otros módulos
|
||||
- ✅ **Código simplificado**: Eliminación de complejidad innecesaria
|
||||
- ✅ **Compatibilidad mantenida**: UI funciona igual para el usuario
|
||||
- 📋 **Listo para testing**: Verificar funcionalidad completa con ABP proxies
|
||||
|
||||
### Notas Técnicas
|
||||
- **Service Proxies**: Generados automáticamente en `/AbpServiceProxies/GetAll`
|
||||
- **Naming Convention**: JavaScript usa camelCase (`getAll`), C# usa PascalCase (`GetAllAsync`)
|
||||
- **Parameter Objects**: ABP requiere objetos para parámetros (`{ id: id }` no `id`)
|
||||
- **Error Handling**: ABP maneja automáticamente errores y excepciones
|
||||
|
||||
### Próximo Testing
|
||||
1. **CRUD Operations**: Verificar Create, Update, Delete funcionan
|
||||
2. **Statistics Loading**: Confirmar métricas del dashboard cargan
|
||||
3. **Network Management**: Probar asignación/desasignación de redes
|
||||
4. **Performance**: Monitorear tiempos de respuesta vs implementación anterior
|
||||
|
||||
---
|
||||
*Recuerda actualizar este changelog cada vez que realices cambios significativos*
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
public class AssignNetworksToGroupDto
|
||||
{
|
||||
[Required]
|
||||
public int GroupId { get; set; }
|
||||
|
||||
[Required]
|
||||
public List<int> NetworkIds { get; set; } = new List<int>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
public class CreateSplashNetworkGroupDto
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
public string Description { get; set; }
|
||||
|
||||
public List<int> NetworkIds { get; set; } = new List<int>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
public class NetworkGroupStatisticsDto
|
||||
{
|
||||
public int TotalGroups { get; set; }
|
||||
|
||||
public int ActiveGroups { get; set; }
|
||||
|
||||
public int TotalNetworks { get; set; }
|
||||
|
||||
public int UnassignedNetworks { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Abp.Application.Services.Dto;
|
||||
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
public class PagedSplashNetworkGroupRequestDto : PagedAndSortedResultRequestDto
|
||||
{
|
||||
public string Keyword { get; set; }
|
||||
|
||||
public bool? IsActive { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace SplashPage.Splash.Dto
|
||||
public SplashDashboardDto()
|
||||
{
|
||||
SelectedNetworks = [];
|
||||
SelectedNetworkGroups = [];
|
||||
StartDate = new DateTime(DateTime.UtcNow.Year, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
EndDate = DateTime.UtcNow;
|
||||
}
|
||||
@@ -17,6 +18,7 @@ namespace SplashPage.Splash.Dto
|
||||
public string Name { get; set; }
|
||||
public List<SplashWidgetDto> Widgets { get; set; }
|
||||
public List<int> SelectedNetworks { get; set; }
|
||||
public List<int> SelectedNetworkGroups { get; set; }
|
||||
|
||||
public int TopValue { get; set; } = 5;
|
||||
public SplashLoyaltyType loyaltyType { get; set; } = 0;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Abp.Application.Services.Dto;
|
||||
using Abp.AutoMapper;
|
||||
using SplashPage.Splash;
|
||||
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
[AutoMapFrom(typeof(SplashMerakiNetwork))]
|
||||
public class SplashMerakiNetworkDto : EntityDto<int>
|
||||
{
|
||||
public string MerakiId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public int OrganizationId { get; set; }
|
||||
|
||||
public int TenantId { get; set; }
|
||||
|
||||
public string GroupName { get; set; }
|
||||
|
||||
public int? GroupId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Abp.Application.Services.Dto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
public class SplashNetworkGroupDto : FullAuditedEntityDto<int>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public int TenantId { get; set; }
|
||||
|
||||
public int NetworkCount { get; set; }
|
||||
|
||||
public List<SplashMerakiNetworkDto> Networks { get; set; } = new List<SplashMerakiNetworkDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
public class SplashNetworkGroupMapProfile : Profile
|
||||
{
|
||||
public SplashNetworkGroupMapProfile()
|
||||
{
|
||||
CreateMap<SplashNetworkGroup, SplashNetworkGroupDto>()
|
||||
.ForMember(dest => dest.Networks, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.NetworkCount, opt => opt.Ignore());
|
||||
|
||||
CreateMap<CreateSplashNetworkGroupDto, SplashNetworkGroup>()
|
||||
.ForMember(dest => dest.Networks, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.TenantId, opt => opt.Ignore());
|
||||
|
||||
CreateMap<UpdateSplashNetworkGroupDto, SplashNetworkGroup>()
|
||||
.ForMember(dest => dest.Networks, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.TenantId, opt => opt.Ignore());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Abp.Application.Services.Dto;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SplashPage.Splash.Dto
|
||||
{
|
||||
public class UpdateSplashNetworkGroupDto : EntityDto<int>
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
public string Description { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public List<int> NetworkIds { get; set; } = new List<int>();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ namespace SplashPage.Splash
|
||||
[HttpPost]
|
||||
Task<bool> SetDashboardNetworks(SplashDashboardDto model);
|
||||
[HttpPost]
|
||||
Task<bool> SetDashboardNetworkGroups(SplashDashboardDto model);
|
||||
[HttpPost]
|
||||
Task<object> NetworkFirstAP(SplashDashboardDto model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Abp.Application.Services;
|
||||
using SplashPage.Splash.Dto;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SplashPage.Splash
|
||||
{
|
||||
public interface ISplashNetworkGroupAppService : IAsyncCrudAppService<SplashNetworkGroupDto, int, PagedSplashNetworkGroupRequestDto, CreateSplashNetworkGroupDto, UpdateSplashNetworkGroupDto>
|
||||
{
|
||||
Task AssignNetworksAsync(AssignNetworksToGroupDto input);
|
||||
|
||||
Task<List<SplashMerakiNetworkDto>> GetAvailableNetworksAsync(int? excludeGroupId = null);
|
||||
|
||||
Task<List<SplashMerakiNetworkDto>> GetNetworksInGroupAsync(int groupId);
|
||||
|
||||
Task<List<SplashNetworkGroupDto>> GetAllGroupsWithNetworkCountAsync();
|
||||
|
||||
Task<List<SplashMerakiNetworkDto>> GetAllNetworksWithGroupInfoAsync();
|
||||
|
||||
Task<NetworkGroupStatisticsDto> GetStatisticsAsync();
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,25 @@ namespace SplashPage.Splash
|
||||
private readonly IRepository<SplashDashboard> _splashDashboardRepository;
|
||||
private readonly IRepository<SplashMerakiNetwork> _splashMerakiNetworkRepository;
|
||||
private readonly IRepository<SplashAccessPoint> _splashAccessPointRepository;
|
||||
private readonly IRepository<SplashNetworkGroup> _networkGroupRepository;
|
||||
private readonly IRepository<SplashNetworkGroupMember> _networkGroupMemberRepository;
|
||||
private readonly SplashNetworkGroupManager _networkGroupManager;
|
||||
private readonly IUnitOfWorkManager _unitOfWorkManager;
|
||||
public SplashDashboardService(IRepository<SplashDashboard> splashDashboardRepository,
|
||||
IUnitOfWorkManager unitOfWorkManager, IRepository<SplashMerakiNetwork> splashMerakiNetworkRepository, IRepository<SplashAccessPoint> splashAccessPointRepository)
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
IRepository<SplashMerakiNetwork> splashMerakiNetworkRepository,
|
||||
IRepository<SplashAccessPoint> splashAccessPointRepository,
|
||||
IRepository<SplashNetworkGroup> networkGroupRepository,
|
||||
IRepository<SplashNetworkGroupMember> networkGroupMemberRepository,
|
||||
SplashNetworkGroupManager networkGroupManager)
|
||||
{
|
||||
_splashDashboardRepository = splashDashboardRepository;
|
||||
_unitOfWorkManager = unitOfWorkManager;
|
||||
_splashMerakiNetworkRepository = splashMerakiNetworkRepository;
|
||||
_splashAccessPointRepository = splashAccessPointRepository;
|
||||
_networkGroupRepository = networkGroupRepository;
|
||||
_networkGroupMemberRepository = networkGroupMemberRepository;
|
||||
_networkGroupManager = networkGroupManager;
|
||||
}
|
||||
|
||||
public async Task<SplashDashboard> CreateDashboard(CreateSplashDashboardDto model)
|
||||
@@ -34,7 +45,7 @@ namespace SplashPage.Splash
|
||||
SplashDashboard splashDashboard = new()
|
||||
{
|
||||
Name = model.Name,
|
||||
TenantId = _unitOfWorkManager.Current.GetTenantId() ?? 1,
|
||||
TenantId = 1, // Always use tenant 1 as specified
|
||||
};
|
||||
|
||||
try
|
||||
@@ -61,7 +72,8 @@ namespace SplashPage.Splash
|
||||
dashboardId = dashboardId,
|
||||
Name = "Dashboard not found",
|
||||
Widgets = new List<SplashWidgetDto>(),
|
||||
SelectedNetworks = new List<int>()
|
||||
SelectedNetworks = new List<int>(),
|
||||
SelectedNetworkGroups = new List<int>()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +91,7 @@ namespace SplashPage.Splash
|
||||
Name = _dashboard.Name ?? string.Empty,
|
||||
dashboardId = _dashboard.Id,
|
||||
SelectedNetworks = _dashboard.SelectedNetworks ?? new List<int>(),
|
||||
SelectedNetworkGroups = _dashboard.SelectedNetworkGroups ?? new List<int>(),
|
||||
//StartDate = _dashboard.StartDate,
|
||||
//EndDate = _dashboard.EndDate
|
||||
};
|
||||
@@ -90,7 +103,8 @@ namespace SplashPage.Splash
|
||||
dashboardId = dashboardId,
|
||||
Name = "Error retrieving dashboard",
|
||||
Widgets = new List<SplashWidgetDto>(),
|
||||
SelectedNetworks = new List<int>()
|
||||
SelectedNetworks = new List<int>(),
|
||||
SelectedNetworkGroups = new List<int>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -389,7 +403,7 @@ namespace SplashPage.Splash
|
||||
W = widget.W,
|
||||
X = widget.X,
|
||||
Y = widget.Y,
|
||||
TenantId = _unitOfWorkManager.Current.GetTenantId() ?? 1,
|
||||
TenantId = 1, // Always use tenant 1 as specified
|
||||
Content = widget.Content,
|
||||
});
|
||||
}
|
||||
@@ -444,9 +458,13 @@ namespace SplashPage.Splash
|
||||
{
|
||||
var _networksCount = await _splashMerakiNetworkRepository.GetAllReadonly().CountAsync();
|
||||
var _dashboard = await _splashDashboardRepository.GetAsync(model.dashboardId);
|
||||
|
||||
// Reset networks and groups
|
||||
_dashboard.SelectedNetworks = [];
|
||||
_dashboard.SelectedNetworkGroups = [];
|
||||
_splashDashboardRepository.Update(_dashboard);
|
||||
|
||||
// Handle networks selection
|
||||
if (model.SelectedNetworks == null || model.SelectedNetworks.Contains(0) || model.SelectedNetworks.Count(s => s != 0) == _networksCount)
|
||||
{
|
||||
_dashboard.SelectedNetworks.Add(0);
|
||||
@@ -459,8 +477,16 @@ namespace SplashPage.Splash
|
||||
}
|
||||
}
|
||||
|
||||
_splashDashboardRepository.Update(_dashboard);
|
||||
// Handle groups selection
|
||||
if (model.SelectedNetworkGroups != null)
|
||||
{
|
||||
foreach (var groupId in model.SelectedNetworkGroups)
|
||||
{
|
||||
_dashboard.SelectedNetworkGroups.Add(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
_splashDashboardRepository.Update(_dashboard);
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
@@ -468,9 +494,35 @@ namespace SplashPage.Splash
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SetDashboardNetworkGroups(SplashDashboardDto model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var _dashboard = await _splashDashboardRepository.GetAsync(model.dashboardId);
|
||||
|
||||
// Reset only groups, keep networks
|
||||
_dashboard.SelectedNetworkGroups = [];
|
||||
_splashDashboardRepository.Update(_dashboard);
|
||||
|
||||
// Handle groups selection
|
||||
if (model.SelectedNetworkGroups != null)
|
||||
{
|
||||
foreach (var groupId in model.SelectedNetworkGroups)
|
||||
{
|
||||
_dashboard.SelectedNetworkGroups.Add(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
_splashDashboardRepository.Update(_dashboard);
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[UnitOfWork]
|
||||
@@ -492,6 +544,93 @@ namespace SplashPage.Splash
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<SplashNetworkGroupDto>> GetNetworkGroupsAsync()
|
||||
{
|
||||
const int tenantId = 1; // Always use tenant 1 as specified
|
||||
var groups = await _networkGroupRepository.GetAll()
|
||||
.Where(g => g.TenantId == tenantId && g.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
return groups.Select(g => new SplashNetworkGroupDto
|
||||
{
|
||||
Id = g.Id,
|
||||
Name = g.Name,
|
||||
Description = g.Description,
|
||||
IsActive = g.IsActive
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<SplashMerakiNetworkDto>> GetNetworksForSelectorAsync()
|
||||
{
|
||||
const int tenantId = 1; // Always use tenant 1 as specified
|
||||
|
||||
var networks = await _splashMerakiNetworkRepository.GetAll()
|
||||
.Where(n => n.TenantId == tenantId && !n.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
// Get network group memberships
|
||||
var networkGroupMembers = await _networkGroupMemberRepository.GetAllIncluding(m => m.NetworkGroup)
|
||||
.Where(m => m.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
var networkDtos = new List<SplashMerakiNetworkDto>();
|
||||
|
||||
foreach (var network in networks)
|
||||
{
|
||||
var dto = new SplashMerakiNetworkDto
|
||||
{
|
||||
Id = network.Id,
|
||||
Name = network.Name,
|
||||
MerakiId = network.MerakiId,
|
||||
OrganizationId = network.OrganizationId,
|
||||
TenantId = network.TenantId
|
||||
};
|
||||
|
||||
var groupMember = networkGroupMembers.FirstOrDefault(m => m.NetworkId == network.Id);
|
||||
if (groupMember != null)
|
||||
{
|
||||
dto.GroupId = groupMember.NetworkGroupId;
|
||||
dto.GroupName = groupMember.NetworkGroup?.Name;
|
||||
}
|
||||
|
||||
networkDtos.Add(dto);
|
||||
}
|
||||
|
||||
return networkDtos;
|
||||
}
|
||||
|
||||
public async Task<List<int>> GetEffectiveNetworkIdsAsync(SplashDashboardDto model)
|
||||
{
|
||||
var effectiveNetworkIds = new List<int>();
|
||||
|
||||
// Add directly selected networks
|
||||
if (model.SelectedNetworks?.Any() == true && !model.SelectedNetworks.Contains(0))
|
||||
{
|
||||
effectiveNetworkIds.AddRange(model.SelectedNetworks);
|
||||
}
|
||||
|
||||
// Add networks from selected groups
|
||||
if (model.SelectedNetworkGroups?.Any() == true)
|
||||
{
|
||||
var groupNetworkIds = await _networkGroupManager.GetNetworkIdsByGroupIdsAsync(model.SelectedNetworkGroups);
|
||||
effectiveNetworkIds.AddRange(groupNetworkIds);
|
||||
}
|
||||
|
||||
// If no specific networks or groups are selected, or "All" is selected, return all networks
|
||||
if (!effectiveNetworkIds.Any() || (model.SelectedNetworks?.Contains(0) == true))
|
||||
{
|
||||
const int tenantId = 1; // Always use tenant 1 as specified
|
||||
var allNetworkIds = await _splashMerakiNetworkRepository.GetAll()
|
||||
.Where(n => n.TenantId == tenantId && !n.IsDeleted)
|
||||
.Select(n => n.Id)
|
||||
.ToListAsync();
|
||||
|
||||
return allNetworkIds;
|
||||
}
|
||||
|
||||
return effectiveNetworkIds.Distinct().ToList();
|
||||
}
|
||||
|
||||
//public async Task<SplashWidget> AddWidget(SplashAddWidgetDto model)
|
||||
//{
|
||||
// var _dashboard = await _splashDashboardRepository.GetAsync(model.DashboardId);
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
using Abp.Application.Services;
|
||||
using Abp.Application.Services.Dto;
|
||||
using Abp.Authorization;
|
||||
using Abp.Domain.Repositories;
|
||||
using Abp.Domain.Uow;
|
||||
using Abp.Linq.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SplashPage.Authorization;
|
||||
using SplashPage.Splash.Dto;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SplashPage.Splash
|
||||
{
|
||||
[AbpAuthorize(PermissionNames.Pages_Administration_NetworkGroups)]
|
||||
public class SplashNetworkGroupAppService : AsyncCrudAppService<SplashNetworkGroup, SplashNetworkGroupDto, int, PagedSplashNetworkGroupRequestDto, CreateSplashNetworkGroupDto, UpdateSplashNetworkGroupDto>, ISplashNetworkGroupAppService
|
||||
{
|
||||
private readonly SplashNetworkGroupManager _networkGroupManager;
|
||||
private readonly IRepository<SplashNetworkGroupMember> _networkGroupMemberRepository;
|
||||
private readonly IRepository<SplashMerakiNetwork> _networkRepository;
|
||||
|
||||
public SplashNetworkGroupAppService(
|
||||
IRepository<SplashNetworkGroup, int> repository,
|
||||
SplashNetworkGroupManager networkGroupManager,
|
||||
IRepository<SplashNetworkGroupMember> networkGroupMemberRepository,
|
||||
IRepository<SplashMerakiNetwork> networkRepository)
|
||||
: base(repository)
|
||||
{
|
||||
_networkGroupManager = networkGroupManager;
|
||||
_networkGroupMemberRepository = networkGroupMemberRepository;
|
||||
_networkRepository = networkRepository;
|
||||
}
|
||||
|
||||
protected override IQueryable<SplashNetworkGroup> CreateFilteredQuery(PagedSplashNetworkGroupRequestDto input)
|
||||
{
|
||||
return Repository.GetAll()
|
||||
.WhereIf(!string.IsNullOrWhiteSpace(input.Keyword),
|
||||
x => x.Name.Contains(input.Keyword) || x.Description.Contains(input.Keyword))
|
||||
.WhereIf(input.IsActive.HasValue, x => x.IsActive == input.IsActive.Value);
|
||||
}
|
||||
|
||||
public override async Task<SplashNetworkGroupDto> CreateAsync(CreateSplashNetworkGroupDto input)
|
||||
{
|
||||
CheckCreatePermission();
|
||||
|
||||
const int tenantId = 1; // Always use tenant 1 as specified
|
||||
|
||||
// Use domain service to create the group with validations
|
||||
var group = await _networkGroupManager.CreateGroupAsync(input.Name, input.Description, tenantId);
|
||||
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
|
||||
// Assign networks if provided
|
||||
if (input.NetworkIds?.Any() == true)
|
||||
{
|
||||
await _networkGroupManager.AssignNetworksToGroupAsync(group.Id, input.NetworkIds);
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Map to DTO and return
|
||||
var dto = ObjectMapper.Map<SplashNetworkGroupDto>(group);
|
||||
dto.NetworkCount = input.NetworkIds?.Count ?? 0;
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public override async Task<SplashNetworkGroupDto> UpdateAsync(UpdateSplashNetworkGroupDto input)
|
||||
{
|
||||
CheckUpdatePermission();
|
||||
|
||||
var group = await Repository.GetAsync(input.Id);
|
||||
|
||||
// Use domain service to update with validations
|
||||
await _networkGroupManager.UpdateGroupAsync(group, input.Name, input.Description);
|
||||
group.IsActive = input.IsActive;
|
||||
|
||||
await Repository.UpdateAsync(group);
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
|
||||
// Update network assignments if provided
|
||||
if (input.NetworkIds != null)
|
||||
{
|
||||
await _networkGroupManager.AssignNetworksToGroupAsync(group.Id, input.NetworkIds);
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Map to DTO and return
|
||||
var dto = ObjectMapper.Map<SplashNetworkGroupDto>(group);
|
||||
dto.NetworkCount = input.NetworkIds?.Count ?? 0;
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public override async Task DeleteAsync(EntityDto<int> input)
|
||||
{
|
||||
CheckDeletePermission();
|
||||
|
||||
await _networkGroupManager.DeleteGroupAsync(input.Id);
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public override async Task<SplashNetworkGroupDto> GetAsync(EntityDto<int> input)
|
||||
{
|
||||
var group = await Repository.GetAsync(input.Id);
|
||||
|
||||
var dto = new SplashNetworkGroupDto
|
||||
{
|
||||
Id = group.Id,
|
||||
Name = group.Name,
|
||||
Description = group.Description,
|
||||
IsActive = group.IsActive,
|
||||
TenantId = group.TenantId,
|
||||
CreationTime = group.CreationTime,
|
||||
CreatorUserId = group.CreatorUserId,
|
||||
LastModificationTime = group.LastModificationTime,
|
||||
LastModifierUserId = group.LastModifierUserId,
|
||||
DeletionTime = group.DeletionTime,
|
||||
DeleterUserId = group.DeleterUserId,
|
||||
IsDeleted = group.IsDeleted
|
||||
};
|
||||
|
||||
// Get network count and networks for this group
|
||||
var networkGroupMembers = await _networkGroupMemberRepository.GetAll()
|
||||
.Where(m => m.NetworkGroupId == group.Id)
|
||||
.ToListAsync();
|
||||
|
||||
dto.NetworkCount = networkGroupMembers.Count;
|
||||
|
||||
// Load associated networks if needed
|
||||
if (networkGroupMembers.Any())
|
||||
{
|
||||
var networkIds = networkGroupMembers.Select(m => m.NetworkId).ToList();
|
||||
var networks = await _networkRepository.GetAll()
|
||||
.Where(n => networkIds.Contains(n.Id))
|
||||
.ToListAsync();
|
||||
|
||||
dto.Networks = ObjectMapper.Map<List<SplashMerakiNetworkDto>>(networks);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
[AbpAuthorize(PermissionNames.Pages_Administration_NetworkGroups_Edit)]
|
||||
public async Task AssignNetworksAsync(AssignNetworksToGroupDto input)
|
||||
{
|
||||
await _networkGroupManager.AssignNetworksToGroupAsync(input.GroupId, input.NetworkIds);
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<SplashMerakiNetworkDto>> GetAvailableNetworksAsync(int? excludeGroupId = null)
|
||||
{
|
||||
const int tenantId = 1; // Always use tenant 1 as specified
|
||||
var availableNetworks = await _networkGroupManager.GetAvailableNetworksAsync(tenantId, excludeGroupId);
|
||||
|
||||
return ObjectMapper.Map<List<SplashMerakiNetworkDto>>(availableNetworks);
|
||||
}
|
||||
|
||||
public async Task<List<SplashMerakiNetworkDto>> GetNetworksInGroupAsync(int groupId)
|
||||
{
|
||||
var networks = await _networkRepository.GetAll()
|
||||
.Where(n => _networkGroupMemberRepository.GetAll()
|
||||
.Where(m => m.NetworkGroupId == groupId)
|
||||
.Select(m => m.NetworkId)
|
||||
.Contains(n.Id))
|
||||
.ToListAsync();
|
||||
|
||||
return ObjectMapper.Map<List<SplashMerakiNetworkDto>>(networks);
|
||||
}
|
||||
|
||||
public async Task<List<SplashNetworkGroupDto>> GetAllGroupsWithNetworkCountAsync()
|
||||
{
|
||||
const int tenantId = 1; // Always use tenant 1 as specified
|
||||
var groups = await _networkGroupManager.GetGroupsWithNetworkCountAsync(tenantId);
|
||||
|
||||
var dtos = ObjectMapper.Map<List<SplashNetworkGroupDto>>(groups);
|
||||
|
||||
// Set network count for each group
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
var group = groups.FirstOrDefault(g => g.Id == dto.Id);
|
||||
dto.NetworkCount = group?.Networks?.Count ?? 0;
|
||||
}
|
||||
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task<List<SplashMerakiNetworkDto>> GetAllNetworksWithGroupInfoAsync()
|
||||
{
|
||||
const int tenantId = 1; // Always use tenant 1 as specified
|
||||
|
||||
var networks = await _networkRepository.GetAll()
|
||||
.Where(n => n.TenantId == tenantId && !n.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
var networkGroupMembers = await _networkGroupMemberRepository.GetAllIncluding(m => m.NetworkGroup)
|
||||
.Where(m => m.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
var networkDtos = ObjectMapper.Map<List<SplashMerakiNetworkDto>>(networks);
|
||||
|
||||
// Add group information to each network
|
||||
foreach (var networkDto in networkDtos)
|
||||
{
|
||||
var groupMember = networkGroupMembers.FirstOrDefault(m => m.NetworkId == networkDto.Id);
|
||||
if (groupMember != null)
|
||||
{
|
||||
networkDto.GroupId = groupMember.NetworkGroupId;
|
||||
networkDto.GroupName = groupMember.NetworkGroup?.Name;
|
||||
}
|
||||
}
|
||||
|
||||
return networkDtos;
|
||||
}
|
||||
|
||||
public async Task<NetworkGroupStatisticsDto> GetStatisticsAsync()
|
||||
{
|
||||
const int tenantId = 1; // Always use tenant 1 as specified
|
||||
|
||||
// Count total groups
|
||||
var totalGroups = await Repository.CountAsync(g => g.TenantId == tenantId && !g.IsDeleted);
|
||||
|
||||
// Count active groups
|
||||
var activeGroups = await Repository.CountAsync(g => g.TenantId == tenantId && !g.IsDeleted && g.IsActive);
|
||||
|
||||
// Count total networks
|
||||
var totalNetworks = await _networkRepository.CountAsync(n => n.TenantId == tenantId && !n.IsDeleted);
|
||||
|
||||
// Count assigned networks
|
||||
var assignedNetworkIds = await _networkGroupMemberRepository.GetAll()
|
||||
.Where(m => m.TenantId == tenantId)
|
||||
.Select(m => m.NetworkId)
|
||||
.Distinct()
|
||||
.CountAsync();
|
||||
|
||||
var unassignedNetworks = totalNetworks - assignedNetworkIds;
|
||||
|
||||
return new NetworkGroupStatisticsDto
|
||||
{
|
||||
TotalGroups = totalGroups,
|
||||
ActiveGroups = activeGroups,
|
||||
TotalNetworks = totalNetworks,
|
||||
UnassignedNetworks = unassignedNetworks
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,9 @@ public static class PermissionNames
|
||||
public const string Pages_Captive_Portal = "Pages.CP";
|
||||
|
||||
public const string Pages_Integrations = "Pages.Integrations";
|
||||
|
||||
public const string Pages_Administration_NetworkGroups = "Pages.Administration.NetworkGroups";
|
||||
public const string Pages_Administration_NetworkGroups_Create = "Pages.Administration.NetworkGroups.Create";
|
||||
public const string Pages_Administration_NetworkGroups_Edit = "Pages.Administration.NetworkGroups.Edit";
|
||||
public const string Pages_Administration_NetworkGroups_Delete = "Pages.Administration.NetworkGroups.Delete";
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ public class SplashPageAuthorizationProvider : AuthorizationProvider
|
||||
|
||||
context.CreatePermission(PermissionNames.Pages_Captive_Portal, L("CaptivePortal"));
|
||||
context.CreatePermission(PermissionNames.Pages_Integrations, L("Integrations"));
|
||||
|
||||
// Network Groups permissions
|
||||
var networkGroupsPermission = context.CreatePermission(PermissionNames.Pages_Administration_NetworkGroups, L("NetworkGroups"));
|
||||
networkGroupsPermission.CreateChildPermission(PermissionNames.Pages_Administration_NetworkGroups_Create, L("CreatingNetworkGroup"));
|
||||
networkGroupsPermission.CreateChildPermission(PermissionNames.Pages_Administration_NetworkGroups_Edit, L("EditingNetworkGroup"));
|
||||
networkGroupsPermission.CreateChildPermission(PermissionNames.Pages_Administration_NetworkGroups_Delete, L("DeletingNetworkGroup"));
|
||||
}
|
||||
|
||||
private static ILocalizableString L(string name)
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace SplashPage.Splash
|
||||
public string ExtensionData { get; set; }
|
||||
public List<SplashWidget> Widgets { get; set; } = [];
|
||||
public List<int> SelectedNetworks { get; set; } = [];
|
||||
public List<int> SelectedNetworkGroups { get; set; } = [];
|
||||
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
39
src/SplashPage.Core/Splash/SplashNetworkGroup.cs
Normal file
39
src/SplashPage.Core/Splash/SplashNetworkGroup.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Abp.Domain.Entities;
|
||||
using Abp.Domain.Entities.Auditing;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SplashPage.Splash
|
||||
{
|
||||
public class SplashNetworkGroup : FullAuditedEntity<int>, IMustHaveTenant
|
||||
{
|
||||
public SplashNetworkGroup()
|
||||
{
|
||||
IsActive = true;
|
||||
Networks = new List<SplashNetworkGroupMember>();
|
||||
}
|
||||
|
||||
public SplashNetworkGroup(string name, string description, int tenantId)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
TenantId = tenantId;
|
||||
IsActive = true;
|
||||
Networks = new List<SplashNetworkGroupMember>();
|
||||
}
|
||||
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
public string Description { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public int TenantId { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public virtual ICollection<SplashNetworkGroupMember> Networks { get; set; }
|
||||
}
|
||||
}
|
||||
187
src/SplashPage.Core/Splash/SplashNetworkGroupManager.cs
Normal file
187
src/SplashPage.Core/Splash/SplashNetworkGroupManager.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using Abp.Domain.Repositories;
|
||||
using Abp.Domain.Services;
|
||||
using Abp.UI;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SplashPage.Splash
|
||||
{
|
||||
public class SplashNetworkGroupManager : DomainService
|
||||
{
|
||||
private readonly IRepository<SplashNetworkGroup> _networkGroupRepository;
|
||||
private readonly IRepository<SplashNetworkGroupMember> _networkGroupMemberRepository;
|
||||
private readonly IRepository<SplashMerakiNetwork> _networkRepository;
|
||||
|
||||
public SplashNetworkGroupManager(
|
||||
IRepository<SplashNetworkGroup> networkGroupRepository,
|
||||
IRepository<SplashNetworkGroupMember> networkGroupMemberRepository,
|
||||
IRepository<SplashMerakiNetwork> networkRepository)
|
||||
{
|
||||
_networkGroupRepository = networkGroupRepository;
|
||||
_networkGroupMemberRepository = networkGroupMemberRepository;
|
||||
_networkRepository = networkRepository;
|
||||
}
|
||||
|
||||
public async Task<SplashNetworkGroup> CreateGroupAsync(string name, string description, int tenantId)
|
||||
{
|
||||
await ValidateGroupNameAsync(name, tenantId);
|
||||
|
||||
var group = new SplashNetworkGroup(name, description, tenantId);
|
||||
|
||||
return await _networkGroupRepository.InsertAsync(group);
|
||||
}
|
||||
|
||||
public async Task<SplashNetworkGroup> UpdateGroupAsync(SplashNetworkGroup group, string name, string description)
|
||||
{
|
||||
await ValidateGroupNameAsync(name, group.TenantId, group.Id);
|
||||
|
||||
group.Name = name;
|
||||
group.Description = description;
|
||||
|
||||
return await _networkGroupRepository.UpdateAsync(group);
|
||||
}
|
||||
|
||||
public async Task ValidateGroupNameAsync(string name, int tenantId, int? excludeId = null)
|
||||
{
|
||||
var query = _networkGroupRepository.GetAll()
|
||||
.Where(g => g.TenantId == tenantId && g.Name == name);
|
||||
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(g => g.Id != excludeId.Value);
|
||||
}
|
||||
|
||||
var exists = await query.AnyAsync();
|
||||
|
||||
if (exists)
|
||||
{
|
||||
throw new UserFriendlyException($"Ya existe un grupo con el nombre '{name}'");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AssignNetworksToGroupAsync(int groupId, List<int> networkIds)
|
||||
{
|
||||
if (networkIds == null || !networkIds.Any())
|
||||
return;
|
||||
|
||||
var group = await _networkGroupRepository.GetAsync(groupId);
|
||||
|
||||
// Validar que todas las redes existen y pertenecen al tenant correcto
|
||||
var validNetworks = await _networkRepository.GetAll()
|
||||
.Where(n => networkIds.Contains(n.Id) && n.TenantId == group.TenantId && !n.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
if (validNetworks.Count != networkIds.Count)
|
||||
{
|
||||
var invalidIds = networkIds.Except(validNetworks.Select(n => n.Id)).ToList();
|
||||
throw new UserFriendlyException($"Las siguientes redes no son válidas: {string.Join(", ", invalidIds)}");
|
||||
}
|
||||
|
||||
// Remover asignaciones existentes para este grupo
|
||||
var existingMembers = await _networkGroupMemberRepository.GetAll()
|
||||
.Where(m => m.NetworkGroupId == groupId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var member in existingMembers)
|
||||
{
|
||||
await _networkGroupMemberRepository.DeleteAsync(member);
|
||||
}
|
||||
|
||||
// Crear nuevas asignaciones
|
||||
foreach (var networkId in networkIds)
|
||||
{
|
||||
var member = new SplashNetworkGroupMember(groupId, networkId, group.TenantId);
|
||||
await _networkGroupMemberRepository.InsertAsync(member);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveNetworkFromGroupAsync(int groupId, int networkId)
|
||||
{
|
||||
var member = await _networkGroupMemberRepository.FirstOrDefaultAsync(
|
||||
m => m.NetworkGroupId == groupId && m.NetworkId == networkId);
|
||||
|
||||
if (member != null)
|
||||
{
|
||||
await _networkGroupMemberRepository.DeleteAsync(member);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<int>> GetNetworkIdsByGroupIdsAsync(List<int> groupIds)
|
||||
{
|
||||
if (groupIds == null || !groupIds.Any())
|
||||
return new List<int>();
|
||||
|
||||
return await _networkGroupMemberRepository.GetAll()
|
||||
.Where(m => groupIds.Contains(m.NetworkGroupId))
|
||||
.Select(m => m.NetworkId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<SplashNetworkGroup>> GetGroupsWithNetworkCountAsync(int tenantId)
|
||||
{
|
||||
var groups = await _networkGroupRepository.GetAllIncluding(g => g.Networks)
|
||||
.Where(g => g.TenantId == tenantId && g.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
public async Task<bool> CanDeleteGroupAsync(int groupId)
|
||||
{
|
||||
// Aquí se pueden agregar validaciones adicionales
|
||||
// Por ejemplo, verificar si el grupo está siendo usado en dashboards
|
||||
|
||||
// Por ahora, siempre permitir eliminar
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task DeleteGroupAsync(int groupId)
|
||||
{
|
||||
if (!await CanDeleteGroupAsync(groupId))
|
||||
{
|
||||
throw new UserFriendlyException("No se puede eliminar este grupo porque está siendo usado.");
|
||||
}
|
||||
|
||||
var group = await _networkGroupRepository.GetAsync(groupId);
|
||||
|
||||
// Eliminar todos los miembros del grupo primero
|
||||
var members = await _networkGroupMemberRepository.GetAll()
|
||||
.Where(m => m.NetworkGroupId == groupId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
await _networkGroupMemberRepository.DeleteAsync(member);
|
||||
}
|
||||
|
||||
// Eliminar el grupo
|
||||
await _networkGroupRepository.DeleteAsync(group);
|
||||
}
|
||||
|
||||
public async Task<List<SplashMerakiNetwork>> GetAvailableNetworksAsync(int tenantId, int? excludeGroupId = null)
|
||||
{
|
||||
var assignedNetworkIds = await _networkGroupMemberRepository.GetAll()
|
||||
.Where(m => excludeGroupId == null || m.NetworkGroupId != excludeGroupId)
|
||||
.Select(m => m.NetworkId)
|
||||
.ToListAsync();
|
||||
|
||||
return await _networkRepository.GetAll()
|
||||
.Where(n => n.TenantId == tenantId && !n.IsDeleted && !assignedNetworkIds.Contains(n.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<SplashMerakiNetwork>> GetNetworksInGroupAsync(int groupId)
|
||||
{
|
||||
return await _networkRepository.GetAll()
|
||||
.Where(n => n.Id == _networkGroupMemberRepository.GetAll()
|
||||
.Where(m => m.NetworkGroupId == groupId)
|
||||
.Select(m => m.NetworkId)
|
||||
.FirstOrDefault())
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/SplashPage.Core/Splash/SplashNetworkGroupMember.cs
Normal file
30
src/SplashPage.Core/Splash/SplashNetworkGroupMember.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Abp.Domain.Entities;
|
||||
using Abp.Domain.Entities.Auditing;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SplashPage.Splash
|
||||
{
|
||||
public class SplashNetworkGroupMember : CreationAuditedEntity<int>, IMustHaveTenant
|
||||
{
|
||||
public SplashNetworkGroupMember()
|
||||
{
|
||||
}
|
||||
|
||||
public SplashNetworkGroupMember(int networkGroupId, int networkId, int tenantId)
|
||||
{
|
||||
NetworkGroupId = networkGroupId;
|
||||
NetworkId = networkId;
|
||||
TenantId = tenantId;
|
||||
}
|
||||
|
||||
public int NetworkGroupId { get; set; }
|
||||
|
||||
public int NetworkId { get; set; }
|
||||
|
||||
public int TenantId { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public virtual SplashNetworkGroup NetworkGroup { get; set; }
|
||||
public virtual SplashMerakiNetwork Network { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,9 @@ public class SplashPageDbContext : AbpZeroDbContext<Tenant, Role, User, SplashPa
|
||||
|
||||
public DbSet<SplashIntegration> SplashIntegrations { get; set; }
|
||||
public DbSet<SplashEmailValidation> SplashEmailValidations { get; set; }
|
||||
|
||||
public DbSet<SplashNetworkGroup> SplashNetworkGroups { get; set; }
|
||||
public DbSet<SplashNetworkGroupMember> SplashNetworkGroupMembers { get; set; }
|
||||
public SplashPageDbContext(DbContextOptions<SplashPageDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
@@ -69,6 +72,30 @@ public class SplashPageDbContext : AbpZeroDbContext<Tenant, Role, User, SplashPa
|
||||
entity.Property(e => e.Timestamp);
|
||||
});
|
||||
|
||||
// SplashNetworkGroup configuration
|
||||
modelBuilder.Entity<SplashNetworkGroup>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => new { e.Name, e.TenantId }).IsUnique();
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(256);
|
||||
entity.Property(e => e.Description).HasMaxLength(512);
|
||||
});
|
||||
|
||||
// SplashNetworkGroupMember configuration
|
||||
modelBuilder.Entity<SplashNetworkGroupMember>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => new { e.NetworkGroupId, e.NetworkId }).IsUnique();
|
||||
|
||||
entity.HasOne(e => e.NetworkGroup)
|
||||
.WithMany(g => g.Networks)
|
||||
.HasForeignKey(e => e.NetworkGroupId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(e => e.Network)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.NetworkId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
|
||||
// Automatically apply all configurations from current assembly
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(SplashPageDbContext).Assembly);
|
||||
|
||||
@@ -19,14 +19,17 @@ namespace SplashPage.Web.Controllers
|
||||
private readonly ISplashDashboardService _splashDashboardService;
|
||||
private readonly ISplashDataService _splashDataService;
|
||||
private readonly ISplashMetricsService _splashMetricsService;
|
||||
private readonly ISplashNetworkGroupAppService _networkGroupAppService;
|
||||
public DashboardController(ISplashDashboardService splashDashboardService,
|
||||
ISplashDataService splashDataService,
|
||||
ISplashMetricsService splashMetricsService
|
||||
ISplashMetricsService splashMetricsService,
|
||||
ISplashNetworkGroupAppService networkGroupAppService
|
||||
)
|
||||
{
|
||||
_splashDashboardService = splashDashboardService;
|
||||
_splashDataService = splashDataService;
|
||||
_splashMetricsService = splashMetricsService;
|
||||
_networkGroupAppService = networkGroupAppService;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(int? id, bool dev = false)
|
||||
@@ -41,6 +44,7 @@ namespace SplashPage.Web.Controllers
|
||||
};
|
||||
|
||||
var _networks = await _splashDashboardService.GetNetworksAsync();
|
||||
var _networkGroups = await _networkGroupAppService.GetAllGroupsWithNetworkCountAsync();
|
||||
|
||||
model.Dashboard = await _splashDashboardService.GetDashboard(dashboardId);
|
||||
|
||||
@@ -72,6 +76,30 @@ namespace SplashPage.Web.Controllers
|
||||
|
||||
model.AvailableNetworks.Add(new SelectListItem { Text = "Todas", Value = "0", Selected = model.Dashboard.SelectedNetworks.Where(d => d == 0).Any() });
|
||||
|
||||
// Populate Network Groups
|
||||
if (model.Dashboard.SelectedNetworkGroups is not null)
|
||||
{
|
||||
model.AvailableNetworkGroups = _networkGroups
|
||||
.Where(g => g.IsActive) // Solo grupos activos
|
||||
.Select(g => new SelectListItem
|
||||
{
|
||||
Text = $"{g.Name} ({g.NetworkCount} redes)",
|
||||
Value = g.Id.ToString(),
|
||||
Selected = model.Dashboard.SelectedNetworkGroups.Contains(g.Id)
|
||||
}).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
model.Dashboard.SelectedNetworkGroups = [];
|
||||
model.AvailableNetworkGroups = _networkGroups
|
||||
.Where(g => g.IsActive) // Solo grupos activos
|
||||
.Select(g => new SelectListItem
|
||||
{
|
||||
Text = $"{g.Name} ({g.NetworkCount} redes)",
|
||||
Value = g.Id.ToString(),
|
||||
Selected = false,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
//Fix dashbaord with no networks selected
|
||||
var firstNetwork = model.AvailableNetworks.FirstOrDefault();
|
||||
|
||||
16
src/SplashPage.Web.Mvc/Controllers/NetworkGroupController.cs
Normal file
16
src/SplashPage.Web.Mvc/Controllers/NetworkGroupController.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SplashPage.Authorization;
|
||||
using SplashPage.Controllers;
|
||||
using Abp.AspNetCore.Mvc.Authorization;
|
||||
|
||||
namespace SplashPage.Web.Mvc.Controllers
|
||||
{
|
||||
[AbpMvcAuthorize(PermissionNames.Pages_Administration_NetworkGroups)]
|
||||
public class NetworkGroupController : SplashPageControllerBase
|
||||
{
|
||||
public ActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,12 @@ namespace SplashPage.Web.Models.Dashboard
|
||||
public DashboardViewModel()
|
||||
{
|
||||
AvailableNetworks = [];
|
||||
AvailableNetworkGroups = [];
|
||||
AvailableWidgets = [];
|
||||
}
|
||||
public List<SplashWidgetName> AvailableWidgets { get; set; }
|
||||
public List<SelectListItem> AvailableNetworks { get; set; }
|
||||
public List<SelectListItem> AvailableNetworkGroups { get; set; }
|
||||
public SplashDashboardDto Dashboard { get; set; }
|
||||
public SplashAverageDto AverageData { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SplashPage.Web.Models.NetworkGroup
|
||||
{
|
||||
public class CreateNetworkGroupViewModel
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
public string Description { get; set; }
|
||||
|
||||
public List<int> SelectedNetworkIds { get; set; } = new List<int>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SplashPage.Web.Models.NetworkGroup
|
||||
{
|
||||
public class EditNetworkGroupViewModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
public string Description { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public List<int> SelectedNetworkIds { get; set; } = new List<int>();
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,5 @@ public class PageNames
|
||||
public const string WifiConnectionReport = "Reporte de Conexiones";
|
||||
public const string WifiScanningReport = "Reporte Transeuntes y Visitas";
|
||||
public const string Integrations = "Integrations";
|
||||
public const string NetworkGroups = "NetworkGroups";
|
||||
}
|
||||
|
||||
@@ -76,6 +76,13 @@ public class SplashPageNavigationProvider : NavigationProvider
|
||||
url: "Roles",
|
||||
icon: "ti ti-user-cog",
|
||||
permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Roles)
|
||||
))
|
||||
.AddItem(new MenuItemDefinition(
|
||||
PageNames.NetworkGroups,
|
||||
new FixedLocalizableString("Grupos de Redes"),
|
||||
url: "NetworkGroup",
|
||||
icon: "ti ti-network",
|
||||
permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Administration_NetworkGroups)
|
||||
));
|
||||
|
||||
// Crear un submenú para los dashboards
|
||||
|
||||
@@ -366,6 +366,14 @@
|
||||
|
||||
|
||||
|
||||
<div class="me-3">
|
||||
<select type="text" class="form-select" placeholder="Grupos de Redes" id="select-network-groups" value="" multiple>
|
||||
@foreach (var group in Model.AvailableNetworkGroups)
|
||||
{
|
||||
<option value="@group.Value" selected=@group.Selected>@group.Text</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="me-3">
|
||||
<select type="text" class="form-select" placeholder="Sucursales" id="select-tags" value="" multiple>
|
||||
@foreach (var item in Model.AvailableNetworks)
|
||||
|
||||
729
src/SplashPage.Web.Mvc/Views/NetworkGroup/Index.cshtml
Normal file
729
src/SplashPage.Web.Mvc/Views/NetworkGroup/Index.cshtml
Normal file
@@ -0,0 +1,729 @@
|
||||
@{
|
||||
ViewData["Title"] = "Grupos de Redes";
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
/* Modern Network Groups UI Styles */
|
||||
.network-group-header {
|
||||
background: linear-gradient(135deg, var(--tblr-primary) 0%, var(--tblr-primary-darker) 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.network-group-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -20%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background-color: var(--tblr-bg-surface);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.stats-card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.network-item {
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 8px;
|
||||
border: 2px dashed transparent;
|
||||
}
|
||||
|
||||
.network-item:hover {
|
||||
background: var(--tblr-bg-surface-secondary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.network-item.dragging {
|
||||
opacity: 0.6;
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
border: 2px dashed var(--tblr-primary);
|
||||
background: rgba(var(--tblr-primary-rgb), 0.05);
|
||||
border-radius: 8px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.drop-zone.active {
|
||||
border-color: var(--tblr-success);
|
||||
background: rgba(var(--tblr-success-rgb), 0.1);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--tblr-bg-surface-secondary);
|
||||
z-index: 100;
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.network-card {
|
||||
cursor: grab;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.network-card:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.network-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--tblr-primary);
|
||||
animation: pulse 1.4s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
.pulse-dot:nth-child(1) { animation-delay: -0.32s; }
|
||||
.pulse-dot:nth-child(2) { animation-delay: -0.16s; }
|
||||
|
||||
@@keyframes pulse {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Responsive improvements */
|
||||
@@media (max-width: 768px) {
|
||||
.network-group-header {
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<!-- Page wrapper -->
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<!-- Header Hero Section -->
|
||||
<div class="network-group-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle text-white-50">
|
||||
Administración
|
||||
</div>
|
||||
<h1 class="page-title text-white mb-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg me-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 9a6 6 0 1 0 12 0a6 6 0 0 0 -12 0"/>
|
||||
<path d="M12 3c1.333 .333 2 2.333 2 6s-.667 5.667 -2 6"/>
|
||||
<path d="M12 3c-1.333 .333 -2 2.333 -2 6s.667 5.667 2 6"/>
|
||||
<path d="M6 9h12"/>
|
||||
<path d="M3 20h7l-1 -1"/>
|
||||
<path d="M21 20h-7l1 -1"/>
|
||||
</svg>
|
||||
Grupos de Redes
|
||||
</h1>
|
||||
<div class="page-subtitle text-white-75 mt-2">
|
||||
Organiza y gestiona tus redes por regiones para un mejor control y análisis
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="btn-list">
|
||||
<button type="button" class="btn btn-white btn-icon" data-bs-toggle="tooltip" title="Exportar datos">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
<path d="M9 15l2 2l4 -4"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="btn btn-white" onclick="createGroup()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 5l0 14"/>
|
||||
<path d="M5 12l14 0"/>
|
||||
</svg>
|
||||
Crear Grupo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<!-- Stats Cards -->
|
||||
<div class="row row-deck row-cards mb-4" id="statsCards">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="stats-card-icon bg-primary-lt text-primary me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-md" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"/>
|
||||
<path d="M12 12l8 -4.5"/>
|
||||
<path d="M12 12l0 9"/>
|
||||
<path d="M12 12l-8 -4.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h3 mb-0" id="totalGroups">-</div>
|
||||
<div class="text-muted">Total Grupos</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="stats-card-icon bg-success-lt text-success me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-md" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/>
|
||||
<path d="M12 1l0 6"/>
|
||||
<path d="M12 17l0 6"/>
|
||||
<path d="M5.636 5.636l4.243 4.243"/>
|
||||
<path d="M14.121 14.121l4.243 4.243"/>
|
||||
<path d="M1 12l6 0"/>
|
||||
<path d="M17 12l6 0"/>
|
||||
<path d="M5.636 18.364l4.243 -4.243"/>
|
||||
<path d="M14.121 9.879l4.243 -4.243"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h3 mb-0" id="activeGroups">-</div>
|
||||
<div class="text-muted">Grupos Activos</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="stats-card-icon bg-info-lt text-info me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-md" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 9a6 6 0 1 0 12 0a6 6 0 0 0 -12 0"/>
|
||||
<path d="M12 3c1.333 .333 2 2.333 2 6s-.667 5.667 -2 6"/>
|
||||
<path d="M12 3c-1.333 .333 -2 2.333 -2 6s.667 5.667 2 6"/>
|
||||
<path d="M6 9h12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h3 mb-0" id="totalNetworks">-</div>
|
||||
<div class="text-muted">Redes Asignadas</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="stats-card-icon bg-warning-lt text-warning me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-md" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 9a6 6 0 1 0 12 0a6 6 0 0 0 -12 0"/>
|
||||
<path d="M12 3c1.333 .333 2 2.333 2 6s-.667 5.667 -2 6"/>
|
||||
<path d="M12 3c-1.333 .333 -2 2.333 -2 6s.667 5.667 2 6"/>
|
||||
<path d="M6 9h12"/>
|
||||
<path d="M12 21l3 -9l-6 0l3 9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h3 mb-0" id="unassignedNetworks">-</div>
|
||||
<div class="text-muted">Sin Asignar</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h3 class="card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 11l3 3l8 -8"/>
|
||||
<path d="M20 12v6a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h9"/>
|
||||
</svg>
|
||||
Gestión de Grupos
|
||||
</h3>
|
||||
<div class="card-subtitle">
|
||||
Administra tus grupos de redes de manera eficiente
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<!-- Search and Filters -->
|
||||
<div class="d-flex gap-2">
|
||||
<div class="input-icon" style="width: 250px;">
|
||||
<input type="text" id="globalSearch" class="form-control" placeholder="Buscar grupos...">
|
||||
<span class="input-icon-addon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/>
|
||||
<path d="M21 21l-6 -6"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-1" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 6l16 0"/>
|
||||
<path d="M4 12l7 0"/>
|
||||
<path d="M4 18l9 0"/>
|
||||
</svg>
|
||||
Filtros
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item filter-option" href="#" data-filter="all">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon dropdown-item-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/>
|
||||
</svg>
|
||||
Todos los grupos
|
||||
</a>
|
||||
<a class="dropdown-item filter-option" href="#" data-filter="active">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon dropdown-item-icon text-success" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M5 12l5 5l10 -10"/>
|
||||
</svg>
|
||||
Solo activos
|
||||
</a>
|
||||
<a class="dropdown-item filter-option" href="#" data-filter="inactive">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon dropdown-item-icon text-danger" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 6l-12 12"/>
|
||||
<path d="M6 6l12 12"/>
|
||||
</svg>
|
||||
Solo inactivos
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body position-relative">
|
||||
<!-- Loading overlay -->
|
||||
<div id="tableLoadingOverlay" class="loading-overlay" style="display: none;">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
|
||||
<span class="text-muted">Cargando datos...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="table-responsive">
|
||||
<table id="networkGroupsTable" class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-1">
|
||||
<input class="form-check-input" type="checkbox" id="selectAll">
|
||||
</th>
|
||||
<th>Grupo</th>
|
||||
<th>Estado</th>
|
||||
<th>Redes</th>
|
||||
<th>Creación</th>
|
||||
<th class="w-1">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
<div class="me-auto text-muted" id="tableInfo">
|
||||
<!-- DataTable info will be inserted here -->
|
||||
</div>
|
||||
<div id="tablePagination">
|
||||
<!-- DataTable pagination will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modern Create/Edit Modal with Stepper -->
|
||||
<div class="modal modal-blur fade" id="createEditModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-md me-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"/>
|
||||
<path d="M12 12l8 -4.5"/>
|
||||
<path d="M12 12l0 9"/>
|
||||
<path d="M12 12l-8 -4.5"/>
|
||||
</svg>
|
||||
Crear Grupo de Redes
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Stepper Navigation -->
|
||||
<div class="steps steps-counter steps-lime mb-4">
|
||||
<div class="step-item active" id="step1-nav">
|
||||
<div class="step-counter">1</div>
|
||||
<div class="step-display">
|
||||
<div class="step-title">Información Básica</div>
|
||||
<div class="step-subtitle">Nombre y descripción</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-item" id="step2-nav">
|
||||
<div class="step-counter">2</div>
|
||||
<div class="step-display">
|
||||
<div class="step-title">Asignar Redes</div>
|
||||
<div class="step-subtitle">Selecciona las redes</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-item" id="step3-nav">
|
||||
<div class="step-counter">3</div>
|
||||
<div class="step-display">
|
||||
<div class="step-title">Confirmación</div>
|
||||
<div class="step-subtitle">Revisar y guardar</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="groupForm" novalidate>
|
||||
<input type="hidden" id="groupId" />
|
||||
|
||||
<!-- Step 1: Basic Information -->
|
||||
<div class="step-content active" id="step1-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 6l0 12"/>
|
||||
<path d="M6 12l12 0"/>
|
||||
</svg>
|
||||
Información del Grupo
|
||||
</h3>
|
||||
<p class="text-muted">Ingresa los datos básicos para tu nuevo grupo de redes.</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label required">Nombre del Grupo</label>
|
||||
<input type="text" class="form-control" id="groupName" required maxlength="256" placeholder="Ej: Región México Central">
|
||||
<div class="invalid-feedback">El nombre es requerido y debe tener menos de 256 caracteres.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descripción</label>
|
||||
<textarea class="form-control" id="groupDescription" rows="4" maxlength="512" placeholder="Describe el propósito de este grupo de redes..."></textarea>
|
||||
<div class="form-hint">Opcional. Máximo 512 caracteres.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="groupIsActive" checked>
|
||||
<span class="form-check-label">
|
||||
<strong>Grupo Activo</strong>
|
||||
<span class="form-check-description">Los grupos inactivos no aparecerán en los filtros del dashboard</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Network Assignment -->
|
||||
<div class="step-content" id="step2-content" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 9a6 6 0 1 0 12 0a6 6 0 0 0 -12 0"/>
|
||||
<path d="M12 3c1.333 .333 2 2.333 2 6s-.667 5.667 -2 6"/>
|
||||
<path d="M12 3c-1.333 .333 -2 2.333 -2 6s.667 5.667 2 6"/>
|
||||
<path d="M6 9h12"/>
|
||||
</svg>
|
||||
Asignar Redes al Grupo
|
||||
</h3>
|
||||
<p class="text-muted">Selecciona las redes que pertenecerán a este grupo. Puedes usar drag & drop o hacer clic para seleccionar.</p>
|
||||
|
||||
<!-- Search Box -->
|
||||
<div class="search-box">
|
||||
<div class="input-icon mb-3">
|
||||
<input type="text" id="networkSearch" class="form-control" placeholder="Buscar redes...">
|
||||
<span class="input-icon-addon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/>
|
||||
<path d="M21 21l-6 -6"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Available Networks -->
|
||||
<div class="col-md-5">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0">Redes Disponibles</h4>
|
||||
<span class="badge bg-info" id="availableCount">0</span>
|
||||
</div>
|
||||
<div class="border rounded p-3" style="min-height: 300px; max-height: 400px; overflow-y: auto;" id="availableNetworksContainer">
|
||||
<div class="text-center text-muted py-4" id="availableEmpty">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg mb-2" width="48" height="48" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 9a6 6 0 1 0 12 0a6 6 0 0 0 -12 0"/>
|
||||
<path d="M12 3c1.333 .333 2 2.333 2 6s-.667 5.667 -2 6"/>
|
||||
<path d="M12 3c-1.333 .333 -2 2.333 -2 6s.667 5.667 2 6"/>
|
||||
<path d="M6 9h12"/>
|
||||
</svg>
|
||||
<div>Cargando redes...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transfer Controls -->
|
||||
<div class="col-md-2 d-flex flex-column align-items-center justify-content-center">
|
||||
<div class="btn-list-vertical">
|
||||
<button type="button" class="btn btn-primary btn-icon mb-2" onclick="moveSelectedToGroup()" data-bs-toggle="tooltip" title="Mover seleccionados al grupo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 6l6 6l-6 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-icon mb-2" onclick="moveAllToGroup()" data-bs-toggle="tooltip" title="Mover todas al grupo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 6l4 6l-4 6"/>
|
||||
<path d="M13 6l4 6l-4 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-icon mb-2" onclick="moveSelectedFromGroup()" data-bs-toggle="tooltip" title="Quitar seleccionados del grupo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 6l-6 6l6 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-icon" onclick="moveAllFromGroup()" data-bs-toggle="tooltip" title="Quitar todas del grupo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 6l-4 6l4 6"/>
|
||||
<path d="M11 6l-4 6l4 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Networks -->
|
||||
<div class="col-md-5">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0">Redes en el Grupo</h4>
|
||||
<span class="badge bg-success" id="selectedCount">0</span>
|
||||
</div>
|
||||
<div class="border rounded p-3 drop-zone" style="min-height: 300px; max-height: 400px; overflow-y: auto;" id="selectedNetworksContainer">
|
||||
<div class="text-center text-muted py-4" id="selectedEmpty">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg mb-2" width="48" height="48" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"/>
|
||||
<path d="M12 12l8 -4.5"/>
|
||||
<path d="M12 12l0 9"/>
|
||||
<path d="M12 12l-8 -4.5"/>
|
||||
</svg>
|
||||
<div>Arrastra redes aquí o usa los botones</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Confirmation -->
|
||||
<div class="step-content" id="step3-content" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-2 text-success" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M5 12l5 5l10 -10"/>
|
||||
</svg>
|
||||
Confirmar Creación del Grupo
|
||||
</h3>
|
||||
<p class="text-muted">Revisa la información antes de crear el grupo.</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Nombre:</label>
|
||||
<div class="h4" id="confirmName">-</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Estado:</label>
|
||||
<div id="confirmStatus">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Descripción:</label>
|
||||
<div id="confirmDescription">-</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Redes Asignadas:</label>
|
||||
<div class="h4 text-primary" id="confirmNetworkCount">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/>
|
||||
<path d="M12 9h.01"/>
|
||||
<path d="M11 12h1v4h1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
Una vez creado el grupo, podrás usarlo en los filtros del dashboard para analizar métricas específicas de estas redes.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="me-auto">
|
||||
<button type="button" class="btn btn-link" id="prevBtn" onclick="previousStep()" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-1" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 6l-6 6l6 6"/>
|
||||
</svg>
|
||||
Anterior
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="button" class="btn btn-primary" id="nextBtn" onclick="nextStep()">
|
||||
Siguiente
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon ms-1" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 6l6 6l-6 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="btn btn-success" id="saveBtn" onclick="saveGroup()" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-1" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"/>
|
||||
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/>
|
||||
<path d="M14 4l0 4l-6 0l0 -4"/>
|
||||
</svg>
|
||||
Crear Grupo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container for Notifications -->
|
||||
<div id="toast-container" class="position-fixed bottom-0 end-0 p-3" style="z-index: 1055;"></div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/views/networkGroup/index.js"></script>
|
||||
}
|
||||
@@ -34,8 +34,8 @@
|
||||
|
||||
<title>@pageTitle</title>
|
||||
|
||||
@await Html.PartialAsync("_Styles.cshtml")
|
||||
@RenderSection("styles", required: false)
|
||||
@await Html.PartialAsync("_Styles.cshtml")
|
||||
</head>
|
||||
<body data-bs-theme="dark">
|
||||
<div class="page">
|
||||
|
||||
1096
src/SplashPage.Web.Mvc/wwwroot/js/views/networkGroup/index.js
Normal file
1096
src/SplashPage.Web.Mvc/wwwroot/js/views/networkGroup/index.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let tomSelect;
|
||||
let tomSelectGroups;
|
||||
const TOM_SELECT_MAX_DISPLAY_NUMBER = 0;
|
||||
const _service = abp.services.app.splashDashboardService;
|
||||
let grid;
|
||||
@@ -37,6 +38,30 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNetworkGroupsFilter() {
|
||||
let selectedGroups = tomSelectGroups.getValue();
|
||||
|
||||
if (Array.isArray(selectedGroups)) {
|
||||
if (selectedGroups.length === 0) {
|
||||
return toastr.info('Debe seleccionar al menos un grupo!!');
|
||||
}
|
||||
} else if (typeof selectedGroups === 'string' && selectedGroups.trim() === '') {
|
||||
return toastr.info('Debe seleccionar al menos un grupo!!');
|
||||
}
|
||||
|
||||
let result = await _service.setDashboardNetworkGroups({
|
||||
dashboardId: modelData.dashboard.dashboardId,
|
||||
selectedNetworkGroups: selectedGroups
|
||||
});
|
||||
|
||||
if (result) {
|
||||
modelData.dashboard.selectedNetworkGroups = selectedGroups;
|
||||
LoadGridHtml();
|
||||
} else {
|
||||
toastr.error('Error al aplicar filtro de grupos');
|
||||
}
|
||||
}
|
||||
|
||||
tomSelect = new TomSelect(el = document.getElementById('select-tags'), {
|
||||
createFilter: function(input) {
|
||||
input = input.toLowerCase();
|
||||
@@ -91,6 +116,45 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
window.tomSelectss = tomSelect;
|
||||
|
||||
// TomSelect para Grupos de Redes
|
||||
tomSelectGroups = new TomSelect(el = document.getElementById('select-network-groups'), {
|
||||
createFilter: function(input) {
|
||||
input = input.toLowerCase();
|
||||
return function(item) {
|
||||
return item.text.toLowerCase().includes(input);
|
||||
};
|
||||
},
|
||||
copyClassesToDropdown: false,
|
||||
onBlur: handleNetworkGroupsFilter,
|
||||
controlInput: '<input>',
|
||||
plugins: {
|
||||
'checkbox_options': {
|
||||
'checkedClassNames': ['ts-checked'],
|
||||
'uncheckedClassNames': ['ts-unchecked'],
|
||||
},
|
||||
},
|
||||
render: {
|
||||
item: function (data, escape) {
|
||||
if (this.items.length == 0) {
|
||||
this.settings.placeholder = ``
|
||||
return '<span class="tag is-success mb-1 mr-1">' + escape(data.text) + '</span>';
|
||||
}
|
||||
else {
|
||||
return '<span class="tag is-success mb-1 mr-1" style="display:none">' + escape(data.text) + '</span>';
|
||||
}
|
||||
}
|
||||
},
|
||||
onChange: function () {
|
||||
console.log("groups - data: " + this.items + "this.items: " + this.items.length);
|
||||
if (this.items.length > 1) {
|
||||
this.settings.placeholder = `+${this.items.length - 1} Grupos`;
|
||||
this.inputState();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
window.tomSelectGroups = tomSelectGroups;
|
||||
|
||||
async function LoadDT() {
|
||||
// Get references to elements
|
||||
const startDateInput = document.getElementById('startDate');
|
||||
|
||||
Reference in New Issue
Block a user