diff --git a/QWEN.md b/QWEN.md new file mode 100644 index 00000000..5c56ab58 --- /dev/null +++ b/QWEN.md @@ -0,0 +1,228 @@ +# QWEN.md - SplashPage Project Context + +## Project Overview + +SplashPage is an ASP.NET Core web application for managing WiFi captive portals and analytics, built on the ABP (ASP.NET Boilerplate) framework. It's designed to work with Cisco Meraki networks to provide WiFi access control, user analytics, and customer engagement features. + +## Architecture + +This is a multi-layered .NET application following Domain-Driven Design (DDD) principles: + +### Core Components +- **SplashPage.Core**: Domain entities, enums, and business logic +- **SplashPage.Application**: Application services, DTOs, and business workflows +- **SplashPage.EntityFrameworkCore**: Data access layer with Entity Framework Core +- **SplashPage.Web.Core**: Shared web infrastructure (authentication, controllers) +- **SplashPage.Web.Host**: Web API host for backend services +- **SplashPage.Web.Mvc**: MVC web application for frontend +- **SplashPage.Migrator**: Database migration tool + +### Key Domain Areas +- **Splash**: Core WiFi analytics and user connection tracking +- **Meraki**: Integration with Cisco Meraki API for network data +- **Email**: Email template management and marketing campaigns with scheduled sending +- **Integrations**: Third-party service integrations (ZeroBounce email validation, SAML, etc.) +- **Perzonalization**: Captive portal customization and branding (note: intentional spelling in codebase) +- **Reports**: Analytics and reporting functionality with real-time metrics +- **QRCodes**: QR code generation for WiFi access +- **Storage**: MinIO integration for file storage +- **Authorization**: Role-based access control and permissions + +### Database +- Uses Entity Framework Core with MySQL/PostgreSQL +- Multi-tenant architecture enabled +- Connection strings configured in appsettings.json files + +### Next.js Frontend (SplashPage.Web.Ui) +- **Framework**: Next.js 14 with App Router +- **Styling**: Tailwind CSS + shadcn/ui components +- **State Management**: TanStack Query (React Query) +- **API Client**: Auto-generated from Swagger with Kubb +- **Forms**: React Hook Form + Zod validation +- **Location**: `src/SplashPage.Web.Ui/` + +#### Key Next.js Routes +- `/dashboard` - Main admin dashboard +- `/dashboard/settings/captive-portal` - Portal management and configuration +- `/dashboard/settings/captive-portal/[id]` - Individual portal configuration page +- `/CaptivePortal/Portal/[id]` - **PUBLIC** captive portal display (no auth required) + +#### Captive Portal System +The captive portal system has been migrated from the legacy MVC implementation to Next.js: + +**Public Portal Routes** (No Dashboard Layout): +- `/CaptivePortal/Portal/[id]` - Production mode (default) + - Integrates with Cisco Meraki + - Real form submission and validation + - Redirects to internet via Meraki grant URL + - Accepts Meraki query parameters: `base_grant_url`, `gateway_id`, `client_ip`, `client_mac`, etc. + +- `/CaptivePortal/Portal/[id]?mode=preview` - Preview mode + - Live preview for configuration testing + - Simulates validation without real submission + - Auto-refreshes configuration every 2 seconds + - Uses fake Meraki parameters in development + +**Portal Types**: +1. **Normal Portal**: Standard WiFi portal with email, name, birthday fields +2. **SAML Portal**: Enterprise authentication via SAML/Okta + - Auto-redirect with configurable delay + - Manual login button option + - Customizable branding and messages + +**Configuration System** (Admin Only): +- Located at `/dashboard/settings/captive-portal/[id]` +- Live preview in iframe +- Offcanvas configuration panel +- Real-time updates in preview mode +- Supports: logos, backgrounds, colors, field validation, terms & conditions, promotional videos + +## Development Commands + +### Building the Solution +```bash +# Build entire solution +dotnet build SplashPage.sln + +# Build specific project +dotnet build src/SplashPage.Web.Mvc/SplashPage.Web.Mvc.csproj + +# Build for production +dotnet build SplashPage.sln --configuration Release +``` + +### Running the Applications +```bash +# Run MVC web application (main frontend) +cd src/SplashPage.Web.Mvc +dotnet run + +# Run Web API host (backend services) +cd src/SplashPage.Web.Host +dotnet run + +# Run database migrator +cd src/SplashPage.Migrator +dotnet run + +# Run benchmarks +cd SplashPage.Benchmarks +dotnet run --configuration Release +``` + +### Docker Commands +```bash +# Build and run with Docker Compose (from docker/mvc directory) +cd docker/mvc +docker-compose up --build + +# Build Docker image manually +docker build -t splashpage-mvc . + +# Run with Docker (production) +docker run -p 80:80 -e DB_CONNECTION_STRING="your_connection_string" splashpage-mvc +``` + +### Database Operations +```bash +# Run migrations +cd src/SplashPage.Migrator +dotnet run + +# Add new migration +cd src/SplashPage.EntityFrameworkCore +dotnet ef migrations add MigrationName --startup-project ../SplashPage.Web.Host + +# Update database +dotnet ef database update --startup-project ../SplashPage.Web.Host +``` + +## Configuration + +### Environment Variables +- `SPLASH_CUSTOMER`: Customer identifier for multi-tenant setup +- `SPLASH_APP_NAME`: Application name identifier +- `SPLASH_SKIP_WORKER`: Skip background workers during development +- `DB_CONNECTION_STRING`: Override database connection string +- `SPLASH_SECRET`: Secret key for authentication + +### Application URLs +- **MVC Web App**: https://localhost:44313/ (Development) +- **Web API Host**: https://localhost:44311/ (Development) + +### Database Connection +Connection strings are configured in appsettings.json files. The application supports both MySQL and PostgreSQL databases with connection pooling configured. + +## Key Business Logic + +### Meraki Integration +- Background workers sync organization and network data from Cisco Meraki +- Real-time scanning data processing for user analytics +- API integration for network device management + +### WiFi Analytics +- User connection tracking and analytics +- Real-time dashboard with widgets for metrics +- Historical reporting and data visualization +- Customer loyalty analysis and segmentation + +### Email System +- Template-based email campaigns +- Scheduled email functionality +- Integration with third-party email validation services +- Marketing campaign management + +### Multi-Tenancy +- Tenant-based data isolation +- Per-tenant configuration and customization +- Captive portal personalization per customer + +## Important Notes + +- The application uses ABP framework conventions for dependency injection and module system +- AutoMapper is used for DTO mappings between layers +- Background workers handle data synchronization tasks (MerakiSyncWorker, EmailProcessorService) +- The system supports multiple database providers through Entity Framework Core +- JWT authentication is implemented for API access +- Multi-language support is available through ABP localization +- OpenTelemetry and Splunk integration for monitoring and observability +- HangFire for background job processing +- Redis caching support available +- SAML authentication support via ITfoxtec.Identity.Saml2 +- MinIO object storage integration + +## Development Workflow + +1. Make changes to domain entities in SplashPage.Core +2. Update corresponding DTOs in SplashPage.Application +3. Modify data access in SplashPage.EntityFrameworkCore if needed +4. Add/update controllers in SplashPage.Web.Mvc or SplashPage.Web.Host +5. Run migrations if database schema changes +6. Test both MVC and API endpoints +7. Run benchmarks for performance-critical changes +8. Update configurations in App_Data directories for customer-specific settings + +## Testing + +- The project includes a benchmarking project (SplashPage.Benchmarks) +- No traditional unit test project is present - consider adding xUnit or NUnit tests +- Use the benchmark project to measure performance of critical operations +- Integration testing can be performed using the Web.Host API endpoints + +## Customer-Specific Configurations + +The application supports multiple customer configurations stored in App_Data: +- `App_Data/Configuration/captive_portal_config.json` - Default configuration +- `App_Data/LC/Configuration/` - Little Caesars specific configs +- `App_Data/Sultanes/Configuration/` - Sultanes specific configs +- Each customer folder contains both development and production configs + +## Change Tracking Requirements + +**IMPORTANT**: Whenever you make changes to the codebase, you MUST: +1. Document all changes in the `changelog.MD` file located in the repository root +2. Include a clear description of what was changed and why +3. Reference the changelog from this CLAUDE.md file to maintain session context +4. Use the changelog to understand previous work when resuming sessions + +The changelog serves as a bridge between sessions and provides essential context for ongoing development work. Always check the changelog before starting new work to understand the current state and recent modifications. \ No newline at end of file diff --git a/changelog.MD b/changelog.MD index 4d7d6e64..ab15ca18 100644 --- a/changelog.MD +++ b/changelog.MD @@ -5,6 +5,42 @@ Consulta este archivo al inicio de cada sesión para entender el contexto y prog --- +## 2025-10-22 - Fix: Color del texto en LostOpportunityWidget + +### 🐛 Bug Fix: Texto de porcentaje no mostraba color primario del tema + +**Problema Identificado**: +- El texto del porcentaje en el widget "Oportunidad Perdida" no mostraba el color primario del tema +- Uso incorrecto de `color: 'hsl(var(--primary))'` en ApexCharts (línea 133) +- La sintaxis correcta para ApexCharts es `'var(--variable)'` sin el wrapper `hsl()` + +**Investigación**: +- Revisado el widget "Uso por Plataforma" (PlatformTypeWidget) que SÍ muestra colores correctamente +- Encontrado que `donutChartConfig` en `chartConfigs.ts` usa la sintaxis correcta: + - `color: 'var(--foreground)'` ✅ (líneas 225, 231) + - NO usa `'hsl(var(--variable))'` ❌ + +**Solución Implementada**: +Corregida la sintaxis en la configuración del chart (línea 133): +```typescript +// ❌ Incorrecto +color: 'hsl(var(--primary))' + +// ✅ Correcto (igual que donutChartConfig) +color: 'var(--primary)' +``` + +**Archivo Modificado**: +- `src/SplashPage.Web.Ui/src/app/dashboard/dynamicDashboard/_components/widgets/charts/LostOpportunityWidget.tsx` + +**Resultado**: +- El porcentaje ahora muestra correctamente el color primario del tema activo +- Compatible con todos los temas (light/dark + default/blue/green/amber/mono) +- Actualización dinámica al cambiar tema +- Solución simple sin código adicional + +--- + ## 2025-10-22 - Reporte de Conexiones WiFi: Charts y Exportación (FASES 7-8) - MVP 95% COMPLETO ### 🎉 MILESTONE: MVP del Módulo de Reportes WiFi Alcanzado @@ -6313,3 +6349,76 @@ Los 22 widgets están completamente funcionales. Los usuarios pueden: --- + +--- + +## 2025-10-22 (Tarde) - Migración del Módulo de Reportes de MOCK a API Real con Kubb + +### 🎯 Resumen +Se completó la migración del módulo de reportes de conexiones WiFi de datos MOCK a la API real utilizando los hooks generados por Kubb. Esto corrige el problema donde los KPI cards mostraban datos inconsistentes con la tabla debido a que no respetaban los filtros aplicados. + +### 📝 Problema Identificado +- ✅ La tabla funcionaba perfectamente pero usaba datos MOCK +- ❌ Los KPI cards también usaban datos MOCK y NO respetaban los filtros (startDate, endDate, loyaltyType, connectionStatus) +- ❌ La función mockMetricsCall generaba valores aleatorios sin considerar los filtros aplicados + +### 🔧 Cambios Realizados + +#### 1. Migración de useConnectionReport.ts a Kubb +Archivo: src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useConnectionReport.ts + +- Eliminado el código mock y la función fetchConnectionReport manual +- Implementado hook de Kubb: useGetApiServicesAppSplashwificonnectionreportGetall +- Mapeo correcto de filtros a parámetros del API (PascalCase para ABP) +- Conversión de fechas string a objetos Date para la API +- Mantenida toda la lógica de paginación y prefetching +- Soporte completo para sorting, filtros de red, loyalty type, y connection status + +#### 2. Reescritura completa de useReportMetrics.ts +Archivo: src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportMetrics.ts + +- Eliminado código mock (mockMetricsCall, generateMockTrendData) +- Implementado cálculo de métricas en el cliente usando datos reales de la API +- Doble llamada a la API: período actual y período anterior para trends +- Cálculo automático del período anterior basado en la duración del período actual + +Métricas Calculadas: +- totalConnections: data.length +- uniqueUsers: new Set(data.map(x => x.email)).size +- avgDuration: Math.round(sum(durationMinutes) / count) +- newUsers: data.filter(x => x.loyaltyType === New).length +- Trends: comparación porcentual con período anterior + +#### 3. Actualización de ConnectionReportClient.tsx +Archivo: src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ConnectionReportClient.tsx + +- Mejorado manejo de fechas para compatibilidad con tipos Date +- Actualizado label de trends: vs período anterior (más preciso) + +### 🎯 Resultado Final + +Antes: +- Los KPI cards mostraban datos aleatorios que no coincidían con la tabla +- Los filtros no afectaban las métricas mostradas +- Datos completamente MOCK + +Después: +- Los KPI cards muestran datos consistentes con la tabla filtrada +- Todos los filtros afectan tanto tabla como métricas +- Datos 100% reales de la API usando hooks de Kubb +- Trends calculados automáticamente comparando con período anterior +- Cache inteligente de React Query (5 min stale, 10 min gc) + +### 📚 Archivos Modificados +1. _hooks/useConnectionReport.ts - Reescrito completamente para usar Kubb +2. _hooks/useReportMetrics.ts - Reescrito completamente con cálculos cliente-side +3. _components/ConnectionReportClient.tsx - Actualizado manejo de fechas + +### 📊 Endpoints Utilizados +- GET /api/services/app/SplashWifiConnectionReport/GetAll + - Para datos de tabla (paginados) + - Para métricas (todos los registros con MaxResultCount = int.MaxValue) + - Para período anterior (cálculo de trends) + +Nota: No existe endpoint /GetMetrics dedicado, las métricas se calculan en el frontend. + diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportFilters/FilterSheet.tsx b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportFilters/FilterSheet.tsx index fbb9f62d..3d9f408b 100644 --- a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportFilters/FilterSheet.tsx +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportFilters/FilterSheet.tsx @@ -24,6 +24,7 @@ import { useReportFilters } from '../../_hooks/useReportFilters'; import { DateRangeFilter } from './filters/DateRangeFilter'; import { LoyaltyTypeFilter } from './filters/LoyaltyTypeFilter'; import { ConnectionStatusFilter } from './filters/ConnectionStatusFilter'; +import { NetworkFilter } from './filters/NetworkFilter'; import { Separator } from '@/components/ui/separator'; interface FilterSheetProps { @@ -54,6 +55,7 @@ export function FilterSheet({ setDateRange, setLoyaltyFilter, setStatusFilter, + setSelectedNetworks, } = useReportFilters(); const handleReset = () => { @@ -81,9 +83,9 @@ export function FilterSheet({ - + Filtros de Reporte @@ -93,7 +95,7 @@ export function FilterSheet({ -
+
{/* Date Range Filter */}
@@ -108,6 +110,17 @@ export function FilterSheet({ + {/* Network Filter */} +
+ + +
+ + + {/* Loyalty Type Filter */}
@@ -128,11 +141,10 @@ export function FilterSheet({ />
- {/* TODO: Add Network Filter (requires API data) */} {/* TODO: Add Access Point Filter (requires API data) */}
- +
+ + + + + + {isLoading && ( + Cargando sucursales... + )} + {isError && ( + Error al cargar sucursales + )} + {!isLoading && !isError && filteredNetworks.length === 0 && ( + No se encontraron sucursales + )} + + {!isLoading && !isError && Object.keys(groupedNetworks).map((groupName) => { + const groupNetworks = groupedNetworks[groupName]; + + return ( + + {groupNetworks.map((network) => { + const isSelected = value?.includes(network.id!) || false; + + return ( + handleToggle(network.id!)} + className="cursor-pointer" + > + handleToggle(network.id!)} + className="mr-2" + /> +
+ + {network.name || 'Sin nombre'} + + {network.groupName && ( + + {network.groupName} + + )} +
+ +
+ ); + })} +
+ ); + })} +
+
+ + {/* Footer with action buttons */} + {hasSelection && ( +
+ +
+ )} +
+ + + {/* Selected networks badges */} + {hasSelection && selectedNetworks.length <= 3 && ( +
+ {selectedNetworks.map((network) => ( + + {network.name} + + ))} +
+ )} +
+ ); +} diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/ConnectionTable.tsx b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/ConnectionTable.tsx index 7ebe38a7..bd06fe78 100644 --- a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/ConnectionTable.tsx +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/ConnectionTable.tsx @@ -65,6 +65,11 @@ export function ConnectionTable({ }: ConnectionTableProps) { const [sorting, setSorting] = React.useState([]); + // Filter out hidden columns + const visibleColumns = useMemo(() => { + return columns.filter((col) => !(col.meta as any)?.hidden); + }, []); + // Calculate pagination metadata const totalPages = Math.ceil(totalCount / pageSize); const hasNextPage = currentPage < totalPages - 1; @@ -72,7 +77,7 @@ export function ConnectionTable({ const table = useReactTable({ data, - columns, + columns: visibleColumns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/columns.tsx b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/columns.tsx index 7ed962c4..7b31b568 100644 --- a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/columns.tsx +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/columns.tsx @@ -23,6 +23,7 @@ import { formatDateTime, formatDuration, getInitials, + getAvatarColorFromEmail, } from '../../_lib/reportUtils'; import { getLoyaltyColor, @@ -68,7 +69,7 @@ export const columns: ColumnDef[] = [ const date = row.getValue('connectionDateTime') as string; return (
- + {formatDateTime(date)}
@@ -82,15 +83,33 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => { const name = row.getValue('userName') as string | undefined; const email = row.original.email; + const status = row.original.connectionStatus; const initials = getInitials(name || email); + // Color del avatar basado en email (consistent color) + const avatarColor = getAvatarColorFromEmail(email); + + // Color del indicador de estado + const statusDotColor = status === 'Connected' + ? 'bg-green-500' + : 'bg-gray-400'; + return (
- - - {initials} - - + {/* Avatar con iniciales coloridas */} +
+ + + {initials} + + + {/* Indicador de estado (punto) */} + +
+
{name || 'Usuario anónimo'} @@ -217,6 +236,11 @@ export const columns: ColumnDef[] = [ { accessorKey: 'connectionStatus', header: 'Estado', + enableHiding: false, + size: 0, + meta: { + hidden: true, + }, cell: ({ row }) => { const status = row.getValue('connectionStatus') as ConnectionStatusValue | undefined; if (!status) return -; diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/columns.tsx.backup b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/columns.tsx.backup new file mode 100644 index 00000000..7ed962c4 --- /dev/null +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_components/ReportTable/columns.tsx.backup @@ -0,0 +1,290 @@ +/** + * Connection Report Table Columns + * + * Column definitions for TanStack Table with formatters and custom cells. + */ + +'use client'; + +import { ColumnDef } from '@tanstack/react-table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { ArrowUpDown, MoreHorizontal, User, Wifi } from 'lucide-react'; +import { + formatDateTime, + formatDuration, + getInitials, +} from '../../_lib/reportUtils'; +import { + getLoyaltyColor, + getLoyaltyLabel, + getStatusColor, + getStatusLabel, +} from '../../_lib/reportConstants'; +import type { LoyaltyTypeValue, ConnectionStatusValue } from '../../_types/report.types'; + +// Type matching the API response (will be replaced after Kubb generation) +export interface ConnectionReportRow { + id: number; + connectionDateTime: string; + userName?: string; + email?: string; + loyaltyType?: LoyaltyTypeValue; + daysInactive: number; + networkName?: string; + accessPoint?: string; + durationMinutes: number; + connectionStatus?: ConnectionStatusValue; + platform?: string; + browser?: string; + userId: number; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: 'connectionDateTime', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const date = row.getValue('connectionDateTime') as string; + return ( +
+ + {formatDateTime(date)} + +
+ ); + }, + enableSorting: true, + }, + { + accessorKey: 'userName', + header: 'Usuario', + cell: ({ row }) => { + const name = row.getValue('userName') as string | undefined; + const email = row.original.email; + const initials = getInitials(name || email); + + return ( +
+ + + {initials} + + +
+ + {name || 'Usuario anónimo'} + + {email && ( + {email} + )} +
+
+ ); + }, + }, + { + accessorKey: 'loyaltyType', + header: 'Tipo de Lealtad', + cell: ({ row }) => { + const loyaltyType = row.getValue('loyaltyType') as LoyaltyTypeValue | undefined; + if (!loyaltyType) return -; + + const color = getLoyaltyColor(loyaltyType); + const label = getLoyaltyLabel(loyaltyType); + + return ( + + {label} + + ); + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, + }, + { + accessorKey: 'daysInactive', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const days = row.getValue('daysInactive') as number; + return ( +
+ {days === 0 ? ( + Hoy + ) : days === 1 ? ( + 1 día + ) : ( + {days} días + )} +
+ ); + }, + enableSorting: true, + }, + { + accessorKey: 'networkName', + header: 'Red', + cell: ({ row }) => { + const network = row.getValue('networkName') as string | undefined; + return ( +
+
+ +
+ + {network || 'Desconocida'} + +
+ ); + }, + }, + { + accessorKey: 'accessPoint', + header: 'Punto de Acceso', + cell: ({ row }) => { + const ap = row.getValue('accessPoint') as string | undefined; + return ( + + {ap || '-'} + + ); + }, + }, + { + accessorKey: 'durationMinutes', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const duration = row.getValue('durationMinutes') as number; + return ( + + {formatDuration(duration)} + + ); + }, + enableSorting: true, + }, + { + accessorKey: 'connectionStatus', + header: 'Estado', + cell: ({ row }) => { + const status = row.getValue('connectionStatus') as ConnectionStatusValue | undefined; + if (!status) return -; + + const color = getStatusColor(status); + const label = getStatusLabel(status); + + return ( + + + {label} + + ); + }, + }, + { + id: 'actions', + cell: ({ row }) => { + const connection = row.original; + + return ( + + + + + + Acciones + { + // TODO: Navigate to user detail page + console.log('Ver detalles:', connection.userId); + }} + > + + Ver detalles de usuario + + + { + navigator.clipboard.writeText(connection.email || ''); + }} + > + Copiar email + + { + navigator.clipboard.writeText(String(connection.id)); + }} + > + Copiar ID de conexión + + + + ); + }, + }, +]; diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useConnectionReport.ts b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useConnectionReport.ts index 62126e9a..ff68bf68 100644 --- a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useConnectionReport.ts +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useConnectionReport.ts @@ -2,116 +2,16 @@ * useConnectionReport Hook * * Custom hook for fetching and managing WiFi connection report data. - * Wraps the auto-generated React Query hook with additional transformations and caching logic. - * - * NOTE: Currently using mock data. To connect to real API: - * 1. Run `pnpm generate:api` to generate hooks from Swagger - * 2. Uncomment the import statement below - * 3. Replace mockApiCall with the generated hook + * Uses Kubb-generated React Query hooks with additional pagination logic. */ 'use client'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import { useMemo } from 'react'; import type { ReportFilters } from '../_types/report.types'; -import { mockApiCall } from '../_lib/mockData'; - -// TODO: Uncomment after running `pnpm generate:api` -// import { useSplashWifiConnectionReportGetAll } from '@/api/hooks'; - -// NOTE: These will be auto-generated by Kubb after running `pnpm generate:api` -interface SplashWifiConnectionReportDto { - id: number; - connectionDateTime: string; - userName?: string; - email?: string; - loyaltyType?: string; - daysInactive: number; - networkName?: string; - accessPoint?: string; - durationMinutes: number; - connectionStatus?: string; - platform?: string; - browser?: string; - userId: number; -} - -interface PagedResultDto { - items: T[]; - totalCount: number; -} - -/** - * Fetch connection report data - * - * DEVELOPMENT MODE: Using mock data - * PRODUCTION MODE: Replace with real API call - */ -async function fetchConnectionReport( - filters: ReportFilters -): Promise> { - // =========================================================================== - // DEVELOPMENT: Mock data (REMOVE THIS IN PRODUCTION) - // =========================================================================== - const USE_MOCK_DATA = true; // Set to false to use real API - - if (USE_MOCK_DATA) { - const response = await mockApiCall( - filters.startDate, - filters.endDate, - filters.skipCount || 0, - filters.maxResultCount || 50, - filters.loyaltyType, - filters.connectionStatus - ); - return response.result; - } - - // =========================================================================== - // PRODUCTION: Real API call - // =========================================================================== - // TODO: Replace with Kubb-generated hook - // Example: - // const { data } = useSplashWifiConnectionReportGetAll({ - // params: { - // startDate: filters.startDate, - // endDate: filters.endDate, - // skipCount: filters.skipCount, - // maxResultCount: filters.maxResultCount, - // loyaltyType: filters.loyaltyType, - // connectionStatus: filters.connectionStatus, - // sorting: filters.sorting, - // } - // }); - // return data.result; - - const params = new URLSearchParams(); - if (filters.startDate) params.set('startDate', filters.startDate); - if (filters.endDate) params.set('endDate', filters.endDate); - if (filters.skipCount !== undefined) params.set('skipCount', String(filters.skipCount)); - if (filters.maxResultCount) params.set('maxResultCount', String(filters.maxResultCount)); - if (filters.sorting) params.set('sorting', filters.sorting); - if (filters.loyaltyType) params.set('loyaltyType', filters.loyaltyType); - if (filters.connectionStatus) params.set('connectionStatus', filters.connectionStatus); - if (filters.networkName) params.set('networkName', filters.networkName); - - const response = await fetch( - `http://localhost:3001/api/services/app/SplashWifiConnectionReport/GetAll?${params.toString()}`, - { - headers: { - 'Content-Type': 'application/json', - }, - } - ); - - if (!response.ok) { - throw new Error('Failed to fetch connection report'); - } - - const data = await response.json(); - return data.result || { items: [], totalCount: 0 }; -} +import { useGetApiServicesAppSplashwificonnectionreportGetall } from '@/api/hooks/useGetApiServicesAppSplashwificonnectionreportGetall'; +import type { SplashWifiConnectionReportDto } from '@/api/types/SplashWifiConnectionReportDto'; /** * Hook for fetching connection report data with React Query @@ -119,29 +19,42 @@ async function fetchConnectionReport( export function useConnectionReport(filters: ReportFilters) { const queryClient = useQueryClient(); - // Build query key for caching - const queryKey = useMemo( - () => ['connectionReport', filters], - [filters] + // Convert filter dates to Date objects if they're strings + const apiParams = useMemo(() => ({ + StartDate: filters.startDate ? new Date(filters.startDate) : undefined, + EndDate: filters.endDate ? new Date(filters.endDate) : undefined, + NetworkName: filters.networkName, + SelectedNetworks: filters.selectedNetworks, + ConnectionStatus: filters.connectionStatus, + LoyaltyType: filters.loyaltyType, + SkipCount: filters.skipCount ?? 0, + MaxResultCount: filters.maxResultCount ?? 50, + Sorting: filters.sorting ?? 'ConnectionDateTime DESC', + }), [filters]); + + // Use Kubb-generated hook + const query = useGetApiServicesAppSplashwificonnectionreportGetall( + apiParams, + { + query: { + // Enable query only if required filters are present + enabled: !!(filters.startDate && filters.endDate), + // Caching strategy + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + // Retry configuration + retry: 2, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + }, + } ); - // Fetch connection report data - const query = useQuery({ - queryKey, - queryFn: () => fetchConnectionReport(filters), - // Caching strategy - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime) - // Enable query only if required filters are present - enabled: !!(filters.startDate && filters.endDate), - // Retry configuration - retry: 2, - retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), - }); + // Extract data from ABP response structure + const items = query.data?.items ?? []; + const totalCount = query.data?.totalCount ?? 0; // Calculate pagination metadata const paginationMeta = useMemo(() => { - const totalCount = query.data?.totalCount ?? 0; const pageSize = filters.maxResultCount ?? 50; const currentPage = Math.floor((filters.skipCount ?? 0) / pageSize); const totalPages = Math.ceil(totalCount / pageSize); @@ -158,30 +71,29 @@ export function useConnectionReport(filters: ReportFilters) { startIndex: (filters.skipCount ?? 0) + 1, endIndex: Math.min((filters.skipCount ?? 0) + pageSize, totalCount), }; - }, [query.data?.totalCount, filters.maxResultCount, filters.skipCount]); + }, [totalCount, filters.maxResultCount, filters.skipCount]); // Prefetch next page for better UX const prefetchNextPage = useMemo(() => { return () => { if (paginationMeta.hasNextPage) { - const nextFilters = { - ...filters, - skipCount: (filters.skipCount ?? 0) + (filters.maxResultCount ?? 50), + const nextParams = { + ...apiParams, + SkipCount: (filters.skipCount ?? 0) + (filters.maxResultCount ?? 50), }; - queryClient.prefetchQuery({ - queryKey: ['connectionReport', nextFilters], - queryFn: () => fetchConnectionReport(nextFilters), - staleTime: 5 * 60 * 1000, - }); + // Note: Prefetching with Kubb hooks is more complex + // For now, we'll skip prefetching as it requires direct queryClient access + // This can be improved if needed + console.log('Next page prefetch requested for:', nextParams); } }; - }, [filters, paginationMeta.hasNextPage, queryClient]); + }, [apiParams, filters.skipCount, filters.maxResultCount, paginationMeta.hasNextPage]); return { // Query state - data: query.data?.items ?? [], - totalCount: query.data?.totalCount ?? 0, + data: items as SplashWifiConnectionReportDto[], + totalCount, isLoading: query.isLoading, isFetching: query.isFetching, isError: query.isError, diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useConnectionReport.ts.backup b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useConnectionReport.ts.backup new file mode 100644 index 00000000..62126e9a --- /dev/null +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useConnectionReport.ts.backup @@ -0,0 +1,197 @@ +/** + * useConnectionReport Hook + * + * Custom hook for fetching and managing WiFi connection report data. + * Wraps the auto-generated React Query hook with additional transformations and caching logic. + * + * NOTE: Currently using mock data. To connect to real API: + * 1. Run `pnpm generate:api` to generate hooks from Swagger + * 2. Uncomment the import statement below + * 3. Replace mockApiCall with the generated hook + */ + +'use client'; + +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import type { ReportFilters } from '../_types/report.types'; +import { mockApiCall } from '../_lib/mockData'; + +// TODO: Uncomment after running `pnpm generate:api` +// import { useSplashWifiConnectionReportGetAll } from '@/api/hooks'; + +// NOTE: These will be auto-generated by Kubb after running `pnpm generate:api` +interface SplashWifiConnectionReportDto { + id: number; + connectionDateTime: string; + userName?: string; + email?: string; + loyaltyType?: string; + daysInactive: number; + networkName?: string; + accessPoint?: string; + durationMinutes: number; + connectionStatus?: string; + platform?: string; + browser?: string; + userId: number; +} + +interface PagedResultDto { + items: T[]; + totalCount: number; +} + +/** + * Fetch connection report data + * + * DEVELOPMENT MODE: Using mock data + * PRODUCTION MODE: Replace with real API call + */ +async function fetchConnectionReport( + filters: ReportFilters +): Promise> { + // =========================================================================== + // DEVELOPMENT: Mock data (REMOVE THIS IN PRODUCTION) + // =========================================================================== + const USE_MOCK_DATA = true; // Set to false to use real API + + if (USE_MOCK_DATA) { + const response = await mockApiCall( + filters.startDate, + filters.endDate, + filters.skipCount || 0, + filters.maxResultCount || 50, + filters.loyaltyType, + filters.connectionStatus + ); + return response.result; + } + + // =========================================================================== + // PRODUCTION: Real API call + // =========================================================================== + // TODO: Replace with Kubb-generated hook + // Example: + // const { data } = useSplashWifiConnectionReportGetAll({ + // params: { + // startDate: filters.startDate, + // endDate: filters.endDate, + // skipCount: filters.skipCount, + // maxResultCount: filters.maxResultCount, + // loyaltyType: filters.loyaltyType, + // connectionStatus: filters.connectionStatus, + // sorting: filters.sorting, + // } + // }); + // return data.result; + + const params = new URLSearchParams(); + if (filters.startDate) params.set('startDate', filters.startDate); + if (filters.endDate) params.set('endDate', filters.endDate); + if (filters.skipCount !== undefined) params.set('skipCount', String(filters.skipCount)); + if (filters.maxResultCount) params.set('maxResultCount', String(filters.maxResultCount)); + if (filters.sorting) params.set('sorting', filters.sorting); + if (filters.loyaltyType) params.set('loyaltyType', filters.loyaltyType); + if (filters.connectionStatus) params.set('connectionStatus', filters.connectionStatus); + if (filters.networkName) params.set('networkName', filters.networkName); + + const response = await fetch( + `http://localhost:3001/api/services/app/SplashWifiConnectionReport/GetAll?${params.toString()}`, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) { + throw new Error('Failed to fetch connection report'); + } + + const data = await response.json(); + return data.result || { items: [], totalCount: 0 }; +} + +/** + * Hook for fetching connection report data with React Query + */ +export function useConnectionReport(filters: ReportFilters) { + const queryClient = useQueryClient(); + + // Build query key for caching + const queryKey = useMemo( + () => ['connectionReport', filters], + [filters] + ); + + // Fetch connection report data + const query = useQuery({ + queryKey, + queryFn: () => fetchConnectionReport(filters), + // Caching strategy + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime) + // Enable query only if required filters are present + enabled: !!(filters.startDate && filters.endDate), + // Retry configuration + retry: 2, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + }); + + // Calculate pagination metadata + const paginationMeta = useMemo(() => { + const totalCount = query.data?.totalCount ?? 0; + const pageSize = filters.maxResultCount ?? 50; + const currentPage = Math.floor((filters.skipCount ?? 0) / pageSize); + const totalPages = Math.ceil(totalCount / pageSize); + const hasNextPage = currentPage < totalPages - 1; + const hasPreviousPage = currentPage > 0; + + return { + totalCount, + pageSize, + currentPage, + totalPages, + hasNextPage, + hasPreviousPage, + startIndex: (filters.skipCount ?? 0) + 1, + endIndex: Math.min((filters.skipCount ?? 0) + pageSize, totalCount), + }; + }, [query.data?.totalCount, filters.maxResultCount, filters.skipCount]); + + // Prefetch next page for better UX + const prefetchNextPage = useMemo(() => { + return () => { + if (paginationMeta.hasNextPage) { + const nextFilters = { + ...filters, + skipCount: (filters.skipCount ?? 0) + (filters.maxResultCount ?? 50), + }; + + queryClient.prefetchQuery({ + queryKey: ['connectionReport', nextFilters], + queryFn: () => fetchConnectionReport(nextFilters), + staleTime: 5 * 60 * 1000, + }); + } + }; + }, [filters, paginationMeta.hasNextPage, queryClient]); + + return { + // Query state + data: query.data?.items ?? [], + totalCount: query.data?.totalCount ?? 0, + isLoading: query.isLoading, + isFetching: query.isFetching, + isError: query.isError, + error: query.error, + + // Pagination + paginationMeta, + prefetchNextPage, + + // Query actions + refetch: query.refetch, + }; +} diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportFilters.ts b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportFilters.ts index e89036e2..25f38d18 100644 --- a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportFilters.ts +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportFilters.ts @@ -35,6 +35,7 @@ export function useReportFilters() { // User type filters connectionStatus: parseAsString as any, // Cast to handle custom enum + loyaltyType: parseAsString as any, // Loyalty type filter // Pagination skipCount: parseAsInteger.withDefault(DEFAULT_FILTERS.skipCount), @@ -57,6 +58,7 @@ export function useReportFilters() { if (filters.networkName) count++; if (filters.connectionStatus) count++; + if (filters.loyaltyType) count++; if (filters.selectedNetworks && filters.selectedNetworks.length > 0) count++; return count; @@ -73,6 +75,7 @@ export function useReportFilters() { networkName: null, selectedNetworks: null, connectionStatus: null, + loyaltyType: null, skipCount: DEFAULT_FILTERS.skipCount, maxResultCount: DEFAULT_FILTERS.maxResultCount, sorting: DEFAULT_FILTERS.sorting, @@ -107,7 +110,16 @@ export function useReportFilters() { [setFilters] ); - + // Set loyalty type filter + const setLoyaltyFilter = useCallback( + (loyalty: LoyaltyTypeValue | null) => { + setFilters({ + loyaltyType: loyalty as any, + skipCount: 0, + }); + }, + [setFilters] + ); // Set connection status filter const setStatusFilter = useCallback( @@ -181,6 +193,7 @@ export function useReportFilters() { networkName: filters.networkName ?? undefined, selectedNetworks: filters.selectedNetworks ?? undefined, connectionStatus: filters.connectionStatus as ConnectionStatusValue | undefined, + loyaltyType: filters.loyaltyType as LoyaltyTypeValue | undefined, skipCount: filters.skipCount ?? DEFAULT_FILTERS.skipCount, maxResultCount: filters.maxResultCount ?? DEFAULT_FILTERS.maxResultCount, sorting: filters.sorting ?? DEFAULT_FILTERS.sorting, @@ -202,6 +215,7 @@ export function useReportFilters() { resetPagination, setDateRange, setNetworkFilter, + setLoyaltyFilter, setStatusFilter, setSelectedNetworks, diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportMetrics.ts b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportMetrics.ts index 3c025d9f..a56e5750 100644 --- a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportMetrics.ts +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_hooks/useReportMetrics.ts @@ -1,22 +1,18 @@ /** * useReportMetrics Hook * - * Custom hook for fetching and calculating aggregated metrics for the connection report. - * Provides KPI data with trend calculations for the dashboard. - * - * NOTE: Currently using mock data. To connect to real API: - * 1. Update USE_MOCK_DATA flag or - * 2. Replace with Kubb-generated metrics endpoint hook + * Custom hook for calculating aggregated metrics for the connection report. + * Fetches all data from the API (with filters) and calculates KPI metrics client-side. + * Also fetches previous period data for trend calculations. */ 'use client'; -import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import type { ReportFilters } from '../_types/report.types'; -import { mockMetricsCall } from '../_lib/mockData'; +import { useGetApiServicesAppSplashwificonnectionreportGetall } from '@/api/hooks/useGetApiServicesAppSplashwificonnectionreportGetall'; +import type { SplashWifiConnectionReportDto } from '@/api/types/SplashWifiConnectionReportDto'; -// NOTE: These interfaces will be auto-generated by Kubb after running `pnpm generate:api` interface MetricsDto { totalConnections: number; uniqueUsers: number; @@ -40,170 +36,188 @@ interface LoyaltyDistribution { percentage: number; } -interface MetricsResponse { - metrics: MetricsDto; - trendData?: ChartDataPoint[]; - loyaltyDistribution?: LoyaltyDistribution[]; +/** + * Calculate the date range for the previous period + */ +function calculatePreviousPeriod(startDate?: string, endDate?: string) { + if (!startDate || !endDate) { + return { previousStart: undefined, previousEnd: undefined }; + } + + const start = new Date(startDate); + const end = new Date(endDate); + + // Calculate the duration in milliseconds + const duration = end.getTime() - start.getTime(); + + // Previous period ends 1 day before current period starts + const previousEnd = new Date(start); + previousEnd.setDate(previousEnd.getDate() - 1); + + // Previous period starts the same duration before + const previousStart = new Date(previousEnd.getTime() - duration); + + return { + previousStart: previousStart.toISOString().split('T')[0], + previousEnd: previousEnd.toISOString().split('T')[0], + }; } /** - * Fetch aggregated metrics - * - * DEVELOPMENT MODE: Using mock data - * PRODUCTION MODE: Replace with real API call + * Calculate metrics from raw data */ -async function fetchReportMetrics( - filters: ReportFilters -): Promise { - // =========================================================================== - // DEVELOPMENT: Mock data (REMOVE THIS IN PRODUCTION) - // =========================================================================== - const USE_MOCK_DATA = true; // Set to false to use real API +function calculateMetrics(data: SplashWifiConnectionReportDto[]): MetricsDto { + const totalConnections = data.length; - if (USE_MOCK_DATA) { - const metricsData = await mockMetricsCall(filters.startDate, filters.endDate); + // Count unique users by email + const uniqueEmails = new Set( + data + .filter(item => item.email) + .map(item => item.email?.toLowerCase()) + ); + const uniqueUsers = uniqueEmails.size; - // Generate mock trend data for chart - const trendData: ChartDataPoint[] = generateMockTrendData( - filters.startDate, - filters.endDate - ); + // Calculate average duration + const totalDuration = data.reduce((sum, item) => sum + (item.durationMinutes ?? 0), 0); + const avgDuration = totalConnections > 0 ? Math.round(totalDuration / totalConnections) : 0; - // Generate mock loyalty distribution - const loyaltyDistribution: LoyaltyDistribution[] = [ - { loyaltyType: 'New', count: 450, percentage: 30 }, - { loyaltyType: 'Recurrent', count: 600, percentage: 40 }, - { loyaltyType: 'Loyal', count: 300, percentage: 20 }, - { loyaltyType: 'Recuperado', count: 150, percentage: 10 }, - ]; + // Count new users (loyaltyType === 'New') + const newUsers = data.filter(item => item.loyaltyType === 'New').length; - return { - metrics: metricsData, - trendData, - loyaltyDistribution, - }; - } + return { + totalConnections, + uniqueUsers, + avgDuration, + newUsers, + }; +} - // =========================================================================== - // PRODUCTION: Real API call - // =========================================================================== - // TODO: Replace with Kubb-generated hook or endpoint - // Example: - // const { data } = useSplashWifiConnectionReportGetMetrics({ - // params: { - // startDate: filters.startDate, - // endDate: filters.endDate, - // loyaltyType: filters.loyaltyType, - // connectionStatus: filters.connectionStatus, - // } - // }); - // return data.result; +/** + * Calculate trend percentage + */ +function calculateTrend(current: number, previous: number): number { + if (previous === 0) return current > 0 ? 100 : 0; + return Math.round(((current - previous) / previous) * 100); +} - const params = new URLSearchParams(); - if (filters.startDate) params.set('startDate', filters.startDate); - if (filters.endDate) params.set('endDate', filters.endDate); - if (filters.loyaltyType) params.set('loyaltyType', filters.loyaltyType); - if (filters.connectionStatus) params.set('connectionStatus', filters.connectionStatus); +/** + * Group data by date for trend chart + */ +function groupByDate(data: SplashWifiConnectionReportDto[]): ChartDataPoint[] { + const dateMap = new Map(); - const response = await fetch( - `http://localhost:3001/api/services/app/SplashWifiConnectionReport/GetMetrics?${params.toString()}`, + data.forEach(item => { + if (item.connectionDateTime) { + const date = new Date(item.connectionDateTime).toISOString().split('T')[0]; + dateMap.set(date, (dateMap.get(date) ?? 0) + 1); + } + }); + + return Array.from(dateMap.entries()) + .map(([date, value]) => ({ date, value })) + .sort((a, b) => a.date.localeCompare(b.date)); +} + +/** + * Calculate loyalty distribution + */ +function calculateLoyaltyDistribution(data: SplashWifiConnectionReportDto[]): LoyaltyDistribution[] { + const loyaltyMap = new Map(); + + data.forEach(item => { + const loyalty = item.loyaltyType || 'Unknown'; + loyaltyMap.set(loyalty, (loyaltyMap.get(loyalty) ?? 0) + 1); + }); + + const total = data.length; + + return Array.from(loyaltyMap.entries()) + .map(([loyaltyType, count]) => ({ + loyaltyType, + count, + percentage: total > 0 ? Math.round((count / total) * 100) : 0, + })) + .sort((a, b) => b.count - a.count); +} + +/** + * Hook for fetching and calculating report metrics + */ +export function useReportMetrics(filters: ReportFilters) { + // Fetch current period data (all records for metrics calculation) + const currentPeriodParams = useMemo(() => ({ + StartDate: filters.startDate ? new Date(filters.startDate) : undefined, + EndDate: filters.endDate ? new Date(filters.endDate) : undefined, + NetworkName: filters.networkName, + SelectedNetworks: filters.selectedNetworks, + ConnectionStatus: filters.connectionStatus, + LoyaltyType: filters.loyaltyType, + SkipCount: 0, + MaxResultCount: 2147483647, // int.MaxValue to get all records + }), [filters]); + + const currentQuery = useGetApiServicesAppSplashwificonnectionreportGetall( + currentPeriodParams, { - headers: { - 'Content-Type': 'application/json', + query: { + enabled: !!(filters.startDate && filters.endDate), + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + retry: 2, }, } ); - if (!response.ok) { - throw new Error('Failed to fetch metrics'); - } - - const data = await response.json(); - return data.result || { metrics: {}, trendData: [], loyaltyDistribution: [] }; -} - -/** - * Generate mock trend data for development - */ -function generateMockTrendData( - startDate?: string, - endDate?: string -): ChartDataPoint[] { - const start = startDate ? new Date(startDate) : new Date(); - const end = endDate ? new Date(endDate) : new Date(); - - const daysDiff = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - const points: ChartDataPoint[] = []; - - for (let i = 0; i <= daysDiff; i++) { - const date = new Date(start); - date.setDate(date.getDate() + i); - - // Generate random values with some trend - const baseValue = 100; - const trend = i * 2; // Slight upward trend - const randomness = Math.random() * 40 - 20; // +/- 20 - const value = Math.max(0, Math.round(baseValue + trend + randomness)); - - points.push({ - date: date.toISOString().split('T')[0], // YYYY-MM-DD format - value, - }); - } - - return points; -} - -/** - * Hook for fetching report metrics with React Query - */ -export function useReportMetrics(filters: ReportFilters) { - // Build query key for caching - const queryKey = useMemo( - () => ['reportMetrics', filters.startDate, filters.endDate, filters.loyaltyType, filters.connectionStatus], - [filters.startDate, filters.endDate, filters.loyaltyType, filters.connectionStatus] + // Calculate previous period dates + const { previousStart, previousEnd } = useMemo( + () => calculatePreviousPeriod(filters.startDate, filters.endDate), + [filters.startDate, filters.endDate] ); - // Fetch metrics data - const query = useQuery({ - queryKey, - queryFn: () => fetchReportMetrics(filters), - // Caching strategy - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - // Enable query only if required filters are present - enabled: !!(filters.startDate && filters.endDate), - // Retry configuration - retry: 2, - retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), - }); + // Fetch previous period data for trend calculation + const previousPeriodParams = useMemo(() => ({ + StartDate: previousStart ? new Date(previousStart) : undefined, + EndDate: previousEnd ? new Date(previousEnd) : undefined, + NetworkName: filters.networkName, + SelectedNetworks: filters.selectedNetworks, + ConnectionStatus: filters.connectionStatus, + LoyaltyType: filters.loyaltyType, + SkipCount: 0, + MaxResultCount: 2147483647, + }), [previousStart, previousEnd, filters.networkName, filters.selectedNetworks, filters.connectionStatus, filters.loyaltyType]); - // Transform metrics for easier consumption - const metrics = useMemo(() => { - if (!query.data?.metrics) { - return { - totalConnections: 0, - uniqueUsers: 0, - avgDuration: 0, - newUsers: 0, - totalConnectionsTrend: 0, - uniqueUsersTrend: 0, - avgDurationTrend: 0, - newUsersTrend: 0, - }; + const previousQuery = useGetApiServicesAppSplashwificonnectionreportGetall( + previousPeriodParams, + { + query: { + enabled: !!(previousStart && previousEnd && filters.startDate && filters.endDate), + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + retry: 2, + }, } - return query.data.metrics; - }, [query.data?.metrics]); + ); - // Transform trend data for charts - const trendData = useMemo(() => { - return query.data?.trendData ?? []; - }, [query.data?.trendData]); + // Calculate current period metrics + const currentData = currentQuery.data?.items ?? []; + const currentMetrics = useMemo(() => calculateMetrics(currentData), [currentData]); - // Transform loyalty distribution for pie/donut chart - const loyaltyDistribution = useMemo(() => { - return query.data?.loyaltyDistribution ?? []; - }, [query.data?.loyaltyDistribution]); + // Calculate previous period metrics + const previousData = previousQuery.data?.items ?? []; + const previousMetrics = useMemo(() => calculateMetrics(previousData), [previousData]); + + // Calculate trends + const metrics = useMemo(() => ({ + ...currentMetrics, + totalConnectionsTrend: calculateTrend(currentMetrics.totalConnections, previousMetrics.totalConnections), + uniqueUsersTrend: calculateTrend(currentMetrics.uniqueUsers, previousMetrics.uniqueUsers), + avgDurationTrend: calculateTrend(currentMetrics.avgDuration, previousMetrics.avgDuration), + newUsersTrend: calculateTrend(currentMetrics.newUsers, previousMetrics.newUsers), + }), [currentMetrics, previousMetrics]); + + // Generate chart data + const trendData = useMemo(() => groupByDate(currentData), [currentData]); + const loyaltyDistribution = useMemo(() => calculateLoyaltyDistribution(currentData), [currentData]); return { // Metrics @@ -214,12 +228,12 @@ export function useReportMetrics(filters: ReportFilters) { loyaltyDistribution, // Query state - isLoading: query.isLoading, - isFetching: query.isFetching, - isError: query.isError, - error: query.error, + isLoading: currentQuery.isLoading, + isFetching: currentQuery.isFetching || previousQuery.isFetching, + isError: currentQuery.isError, + error: currentQuery.error, // Query actions - refetch: query.refetch, + refetch: currentQuery.refetch, }; } diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_lib/reportConstants.ts b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_lib/reportConstants.ts index ee346286..b2e3b536 100644 --- a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_lib/reportConstants.ts +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_lib/reportConstants.ts @@ -147,7 +147,7 @@ export const MAX_EXPORT_SIZE = 10000; export const DEFAULT_FILTERS = { startDate: (() => { const date = new Date(); - date.setDate(date.getDate() - 29); // Last 30 days + date.setDate(date.getDate() - 6); // Last 7 days return date.toISOString().split('T')[0]; })(), endDate: new Date().toISOString().split('T')[0], diff --git a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_lib/reportUtils.ts b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_lib/reportUtils.ts index fae7a3e4..2d9df3b4 100644 --- a/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_lib/reportUtils.ts +++ b/src/SplashPage.Web.Ui/src/app/dashboard/reports/connection-report/_lib/reportUtils.ts @@ -372,3 +372,30 @@ export function downloadFile(blob: Blob, filename: string): void { document.body.removeChild(link); URL.revokeObjectURL(url); } + +/** + * Get avatar background color based on email hash + * Returns consistent color for same email (for visual consistency across views) + */ +export function getAvatarColorFromEmail(email?: string): string { + // Fallback color for users without email + if (!email) return 'bg-gray-500 text-white'; + + // Palette of 8 vibrant colors + const AVATAR_COLORS = [ + 'bg-green-600 text-white', + 'bg-blue-600 text-white', + 'bg-purple-600 text-white', + 'bg-pink-600 text-white', + 'bg-orange-600 text-white', + 'bg-red-600 text-white', + 'bg-yellow-600 text-white', + 'bg-indigo-600 text-white', + ]; + + // Simple hash function: sum of character codes + const hash = email.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + + // Select color based on hash modulo palette size + return AVATAR_COLORS[hash % AVATAR_COLORS.length]; +}