1373 lines
48 KiB
Markdown
1373 lines
48 KiB
Markdown
# RF-BI-002: Dashboards Interactivos de BI
|
|
|
|
**Epica:** MAI-006 - Reportes y Business Intelligence
|
|
**Modulo:** Dashboards Interactivos
|
|
**Responsable:** Product Owner
|
|
**Fecha:** 2025-11-17
|
|
**Version:** 1.0
|
|
|
|
---
|
|
|
|
## 1. Objetivo
|
|
|
|
Proporcionar dashboards interactivos configurables que permitan a usuarios de diferentes niveles crear, personalizar y compartir visualizaciones de datos con capacidades de drill-down, filtrado dinamico y segmentacion en tiempo real para analisis profundo de metricas de proyectos.
|
|
|
|
---
|
|
|
|
## 2. Casos de Uso
|
|
|
|
### CU-BI-006: Dashboard Personalizable con Widgets
|
|
|
|
**Actor:** Gerente de Proyecto, Director de Operaciones, CFO
|
|
**Precondiciones:**
|
|
- Usuario autenticado con permisos de visualizacion
|
|
- Existen datos de proyectos en el sistema
|
|
|
|
**Flujo Principal:**
|
|
|
|
1. Usuario accede a modulo de dashboards personalizados
|
|
2. Usuario selecciona "Crear Nuevo Dashboard"
|
|
3. Sistema muestra galeria de widgets disponibles:
|
|
- Graficas de barras (avance fisico/financiero)
|
|
- Graficas de lineas (tendencias temporales)
|
|
- Graficas circulares (distribucion de costos)
|
|
- Tablas dinamicas (detalles de partidas)
|
|
- KPI Cards (metricas clave)
|
|
- Mapas de calor (riesgos)
|
|
- Gauges (SPI, CPI)
|
|
- Timeline (cronograma)
|
|
4. Usuario arrastra y suelta widgets en grid responsive:
|
|
```
|
|
┌─────────────────────────────────────────────────┐
|
|
│ Mi Dashboard - Proyecto Fraccionamiento Valle │
|
|
├─────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─ Avance General ──┐ ┌─ SPI/CPI ────────────┐ │
|
|
│ │ │ │ ┌───┐ │ │
|
|
│ │ Fisico: 68% │ │ SPI │1.1│ On │ │
|
|
│ │ ████████░░ │ │ └───┘ Track │ │
|
|
│ │ │ │ ┌───┐ │ │
|
|
│ │ Financiero: 65% │ │ CPI │1.0│ On │ │
|
|
│ │ ███████░░░ │ │ └───┘Budget │ │
|
|
│ └───────────────────┘ └──────────────────────┘ │
|
|
│ │
|
|
│ ┌─ Costos por Partida ──────────────────────┐ │
|
|
│ │ │ │
|
|
│ │ Obra Civil $45M ███████████████ │ │
|
|
│ │ Instalaciones $18M ██████ │ │
|
|
│ │ Acabados $12M ████ │ │
|
|
│ │ Urbanizacion $25M ████████ │ │
|
|
│ └────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌─ Tendencia de Avance ──────────────────────┐ │
|
|
│ │ │ │
|
|
│ │ 100%│ ╱─ Planif. │ │
|
|
│ │ 80%│ ●───● │ │
|
|
│ │ 60%│ ●───● Real │ │
|
|
│ │ 40%│ ●───● │ │
|
|
│ │ 20%│ ●───● │ │
|
|
│ │ 0%└──────────────────────────→ │ │
|
|
│ │ Ene Feb Mar Abr May Jun │ │
|
|
│ └─────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ [+ Agregar Widget] [Guardar] │
|
|
└─────────────────────────────────────────────────┘
|
|
```
|
|
5. Usuario configura cada widget:
|
|
- Selecciona fuente de datos (proyecto, partida, periodo)
|
|
- Configura metricas a mostrar
|
|
- Define colores y estilos
|
|
- Establece umbrales y alertas
|
|
6. Usuario guarda dashboard con nombre descriptivo
|
|
7. Usuario define si dashboard es privado o compartido
|
|
|
|
**Postcondiciones:**
|
|
- Dashboard personalizado creado y guardado
|
|
- Widgets se actualizan automaticamente con datos en tiempo real
|
|
|
|
**Wireframe - Configuracion de Widget:**
|
|
|
|
```
|
|
┌───────────────────────────────────────────────┐
|
|
│ Configurar Widget: Grafica de Barras │
|
|
├───────────────────────────────────────────────┤
|
|
│ │
|
|
│ Nombre del Widget: │
|
|
│ [Costos por Partida ] │
|
|
│ │
|
|
│ Fuente de Datos: │
|
|
│ ○ Proyecto Actual │
|
|
│ ● Multiples Proyectos │
|
|
│ ☑ Fracc. Del Valle │
|
|
│ ☑ Torres del Sol │
|
|
│ ☐ Privada Roble │
|
|
│ │
|
|
│ Metrica: │
|
|
│ [▼ Costo Acumulado ] │
|
|
│ │
|
|
│ Agrupar por: │
|
|
│ [▼ Partida Presupuestal ] │
|
|
│ │
|
|
│ Periodo: │
|
|
│ ○ Mes Actual │
|
|
│ ○ Trimestre Actual │
|
|
│ ● Acumulado a la Fecha │
|
|
│ │
|
|
│ Visualizacion: │
|
|
│ ☑ Mostrar valores │
|
|
│ ☑ Mostrar porcentajes │
|
|
│ ☐ Ordenar descendente │
|
|
│ │
|
|
│ Colores: │
|
|
│ Esquema: [▼ Azules ] │
|
|
│ │
|
|
│ [Cancelar] [Vista Previa] [Aplicar] │
|
|
└───────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
### CU-BI-007: Drill-Down y Navegacion Jerarquica
|
|
|
|
**Actor:** Gerente de Proyecto, Analista de Costos
|
|
**Precondiciones:**
|
|
- Dashboard configurado con widgets
|
|
- Datos disponibles en multiples niveles de detalle
|
|
|
|
**Flujo Principal:**
|
|
|
|
1. Usuario visualiza dashboard con grafica de costos consolidados
|
|
2. Usuario hace clic en barra de "Obra Civil - $45M"
|
|
3. Sistema ejecuta drill-down, mostrando desglose nivel 2:
|
|
```
|
|
Obra Civil: $45M
|
|
├─ Cimentacion: $12M (27%)
|
|
├─ Estructura: $18M (40%)
|
|
├─ Albanileria: $10M (22%)
|
|
└─ Impermeabilizacion: $5M (11%)
|
|
```
|
|
4. Usuario hace clic en "Estructura - $18M"
|
|
5. Sistema muestra nivel 3 de detalle:
|
|
```
|
|
Estructura: $18M
|
|
├─ Columnas: $7M (39%)
|
|
├─ Trabes: $6M (33%)
|
|
├─ Losas: $4M (22%)
|
|
└─ Escaleras: $1M (6%)
|
|
```
|
|
6. Usuario hace clic en "Columnas - $7M"
|
|
7. Sistema muestra tabla detallada con ordenes de compra:
|
|
```
|
|
┌──────────────────────────────────────────────────────────┐
|
|
│ Detalle: Columnas │
|
|
├──────────────────────────────────────────────────────────┤
|
|
│ OC │Proveedor │Concepto │Monto │Fecha │
|
|
│──────────┼─────────────┼────────────┼───────┼───────────┤
|
|
│ OC-1234 │Concreto ABC │Concreto 3k │$4.2M │2025-01-15 │
|
|
│ OC-1235 │Acero XYZ │Varillas #6 │$2.1M │2025-01-20 │
|
|
│ OC-1236 │Concreto ABC │Bombeo │$0.5M │2025-02-05 │
|
|
│ OC-1237 │Rental Inc │Cimbra │$0.2M │2025-01-10 │
|
|
└──────────────────────────────────────────────────────────┘
|
|
```
|
|
8. Usuario puede navegar hacia atras usando breadcrumbs:
|
|
```
|
|
Dashboard > Obra Civil > Estructura > Columnas
|
|
```
|
|
9. Usuario puede cambiar nivel de agregacion dinamicamente
|
|
|
|
**Postcondiciones:**
|
|
- Usuario obtiene visibilidad completa desde macro a micro
|
|
- Navegacion jerarquica mantiene contexto
|
|
|
|
**Wireframe - Drill-Down:**
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Dashboard > Costos > Obra Civil > Estructura │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─ Estructura: $18M ─────────────────────────────────────┐ │
|
|
│ │ │ │
|
|
│ │ ┌────────────────────────┐ │ │
|
|
│ │ │ Columnas $7M 39% │ ← Clic aqui para profundizar│ │
|
|
│ │ ├────────────────────────┤ │ │
|
|
│ │ │ Trabes $6M 33% │ │ │
|
|
│ │ ├────────────────────────┤ │ │
|
|
│ │ │ Losas $4M 22% │ │ │
|
|
│ │ ├────────────────────────┤ │ │
|
|
│ │ │ Escaleras $1M 6% │ │ │
|
|
│ │ └────────────────────────┘ │ │
|
|
│ │ │ │
|
|
│ │ Analisis: │ │
|
|
│ │ • Mayor costo en elementos verticales (72%) │ │
|
|
│ │ • Columnas representan 39% del total │ │
|
|
│ │ • Avance: 65% (en linea con programacion) │ │
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ [← Regresar] [Exportar Detalle] [Agregar Filtro] │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
### CU-BI-008: Filtros Dinamicos y Segmentacion
|
|
|
|
**Actor:** Director de Operaciones, Gerente de Proyecto
|
|
**Precondiciones:**
|
|
- Dashboard con multiples widgets configurados
|
|
- Datos con dimensiones filtrables
|
|
|
|
**Flujo Principal:**
|
|
|
|
1. Usuario accede a dashboard corporativo multi-proyecto
|
|
2. Usuario activa panel de filtros globales:
|
|
```
|
|
┌─ Filtros Globales ──────────────────┐
|
|
│ │
|
|
│ Proyectos: │
|
|
│ ☑ Fracc. Del Valle │
|
|
│ ☑ Torres del Sol │
|
|
│ ☐ Privada Roble │
|
|
│ ☑ Fracc. Los Pinos │
|
|
│ │
|
|
│ Periodo: │
|
|
│ Desde: [01/01/2025] Hasta: [30/06/25]│
|
|
│ │
|
|
│ Tipo de Proyecto: │
|
|
│ ☑ Fraccionamiento │
|
|
│ ☑ Vertical │
|
|
│ ☐ Mixto │
|
|
│ │
|
|
│ Estado: │
|
|
│ ☐ Planeacion │
|
|
│ ☑ En Construccion │
|
|
│ ☐ En Cierre │
|
|
│ │
|
|
│ Rango de Presupuesto: │
|
|
│ Min: [$50M] Max: [$200M] │
|
|
│ │
|
|
│ [Limpiar] [Aplicar Filtros] │
|
|
└──────────────────────────────────────┘
|
|
```
|
|
3. Usuario selecciona 3 proyectos y aplica filtro
|
|
4. Sistema actualiza todos los widgets del dashboard:
|
|
- KPIs se recalculan para proyectos seleccionados
|
|
- Graficas muestran solo datos filtrados
|
|
- Totales y promedios se ajustan
|
|
5. Usuario aplica filtro de periodo (Q1-2025)
|
|
6. Sistema muestra comparativo temporal:
|
|
```
|
|
┌─ Avance Trimestral Q1-2025 ──────────────┐
|
|
│ │
|
|
│ Proyecto │Ene │Feb │Mar │Total │
|
|
│─────────────────┼─────┼─────┼─────┼──────┤
|
|
│ Fracc. Valle │ 8% │ 12% │ 15% │ 35% │
|
|
│ Torres Sol │ 10% │ 14% │ 11% │ 35% │
|
|
│ Fracc. Pinos │ 6% │ 9% │ 10% │ 25% │
|
|
└───────────────────────────────────────────┘
|
|
```
|
|
7. Usuario guarda combinacion de filtros como "Vista Rapida"
|
|
8. Usuario puede alternar entre vistas guardadas con un clic
|
|
|
|
**Postcondiciones:**
|
|
- Dashboard muestra datos filtrados consistentemente
|
|
- Filtros guardados disponibles para uso futuro
|
|
|
|
**Wireframe - Segmentacion Avanzada:**
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────┐
|
|
│ Segmentacion Avanzada │
|
|
├──────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Crear Segmento: │
|
|
│ │
|
|
│ ┌─ Regla 1 ──────────────────────────────────────────┐ │
|
|
│ │ Campo: [▼ SPI ] │ │
|
|
│ │ Operador: [▼ Menor que ] │ │
|
|
│ │ Valor: [0.90 ] │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
│ [+ Y] [+ O] │
|
|
│ │
|
|
│ ┌─ Regla 2 ──────────────────────────────────────────┐ │
|
|
│ │ Campo: [▼ Margen Neto ] │ │
|
|
│ │ Operador: [▼ Menor que ] │ │
|
|
│ │ Valor: [15% ] │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Resultado: 3 proyectos coinciden │
|
|
│ • Residencial Lago │
|
|
│ • Fracc. Los Pinos │
|
|
│ • Privada Encinos │
|
|
│ │
|
|
│ Guardar Segmento como: [Proyectos en Riesgo ] │
|
|
│ │
|
|
│ [Cancelar] [Aplicar] [Guardar Segmento] │
|
|
└──────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
### CU-BI-009: Exportacion de Visualizaciones
|
|
|
|
**Actor:** Director General, Gerente de Proyecto, CFO
|
|
**Precondiciones:**
|
|
- Dashboard configurado con widgets
|
|
- Usuario tiene permisos de exportacion
|
|
|
|
**Flujo Principal:**
|
|
|
|
1. Usuario visualiza dashboard completo
|
|
2. Usuario hace clic en boton "Exportar"
|
|
3. Sistema muestra opciones de exportacion:
|
|
```
|
|
┌─ Exportar Dashboard ─────────────────┐
|
|
│ │
|
|
│ Formato: │
|
|
│ ○ PDF (Documento) │
|
|
│ ● PowerPoint (Presentacion) │
|
|
│ ○ Excel (Datos + Graficas) │
|
|
│ ○ Imagen PNG (Alta resolucion) │
|
|
│ │
|
|
│ Contenido: │
|
|
│ ● Dashboard Completo │
|
|
│ ○ Widgets Seleccionados │
|
|
│ ☐ Avance General │
|
|
│ ☐ SPI/CPI │
|
|
│ ☐ Costos por Partida │
|
|
│ │
|
|
│ Opciones: │
|
|
│ ☑ Incluir filtros aplicados │
|
|
│ ☑ Incluir fecha de generacion │
|
|
│ ☑ Incluir marca de agua │
|
|
│ ☐ Incluir datos detallados │
|
|
│ │
|
|
│ Orientacion: │
|
|
│ ● Horizontal (Landscape) │
|
|
│ ○ Vertical (Portrait) │
|
|
│ │
|
|
│ [Cancelar] [Vista Previa] │
|
|
│ [Exportar] │
|
|
└───────────────────────────────────────┘
|
|
```
|
|
4. Usuario selecciona formato PowerPoint
|
|
5. Sistema genera presentacion con:
|
|
- Portada con logo y titulo
|
|
- Una diapositiva por widget
|
|
- Graficas como imagenes vectoriales
|
|
- Tablas de datos en formato editable
|
|
- Pie de pagina con fecha y filtros aplicados
|
|
6. Usuario descarga archivo "Dashboard_Proyecto_Valle_2025-11-17.pptx"
|
|
7. Usuario abre presentacion y verifica calidad
|
|
8. Usuario puede editar presentacion en PowerPoint
|
|
|
|
**Flujo Alternativo - Exportacion Programada:**
|
|
|
|
1. Usuario configura exportacion automatica:
|
|
```
|
|
┌─ Programar Exportacion ──────────────┐
|
|
│ │
|
|
│ Dashboard: [Mi Dashboard Semanal ] │
|
|
│ │
|
|
│ Frecuencia: │
|
|
│ ○ Diaria │
|
|
│ ● Semanal (Lunes) │
|
|
│ ○ Mensual (Dia 1) │
|
|
│ ○ Personalizada │
|
|
│ │
|
|
│ Formato: [▼ PDF ] │
|
|
│ │
|
|
│ Enviar a: │
|
|
│ [gerencia@constructora.com ] │
|
|
│ [+ Agregar destinatario] │
|
|
│ │
|
|
│ Hora de envio: [08:00 AM] │
|
|
│ │
|
|
│ ☑ Activo │
|
|
│ │
|
|
│ [Cancelar] [Guardar] │
|
|
└───────────────────────────────────────┘
|
|
```
|
|
2. Sistema programa job recurrente
|
|
3. Cada lunes a las 8:00 AM, sistema:
|
|
- Genera dashboard con datos actualizados
|
|
- Exporta a PDF
|
|
- Envia por correo a destinatarios
|
|
|
|
**Postcondiciones:**
|
|
- Archivo exportado disponible para descarga
|
|
- Visualizaciones mantienen formato y calidad
|
|
- Exportaciones programadas configuradas
|
|
|
|
---
|
|
|
|
## 3. Requerimientos Funcionales
|
|
|
|
### RF-BI-002.1: Creacion y Personalizacion de Dashboards
|
|
- El sistema DEBE permitir crear dashboards personalizados con interfaz drag-and-drop
|
|
- El sistema DEBE ofrecer galeria de widgets pre-configurados (minimo 8 tipos)
|
|
- El sistema DEBE permitir redimensionar y reposicionar widgets en grid responsive
|
|
- El sistema DEBE permitir guardar dashboards con nombre y descripcion
|
|
- El sistema DEBE permitir definir dashboards como privados o compartidos
|
|
- El sistema DEBE permitir duplicar dashboards existentes
|
|
|
|
### RF-BI-002.2: Configuracion de Widgets
|
|
- El sistema DEBE permitir configurar fuente de datos por widget
|
|
- El sistema DEBE permitir seleccionar metricas y dimensiones a visualizar
|
|
- El sistema DEBE ofrecer multiples tipos de graficas (barras, lineas, circular, gauge, tabla)
|
|
- El sistema DEBE permitir configurar colores, estilos y umbrales
|
|
- El sistema DEBE validar configuracion antes de aplicar
|
|
- El sistema DEBE mostrar vista previa de widget antes de guardar
|
|
|
|
### RF-BI-002.3: Drill-Down y Navegacion Jerarquica
|
|
- El sistema DEBE permitir drill-down en graficas haciendo clic en elementos
|
|
- El sistema DEBE soportar al menos 4 niveles de profundidad
|
|
- El sistema DEBE mostrar breadcrumbs de navegacion jerarquica
|
|
- El sistema DEBE permitir regresar a nivel anterior
|
|
- El sistema DEBE mantener contexto de filtros durante drill-down
|
|
- El sistema DEBE mostrar detalle de transacciones en nivel mas bajo
|
|
|
|
### RF-BI-002.4: Filtros Dinamicos
|
|
- El sistema DEBE ofrecer panel de filtros globales aplicables a todo el dashboard
|
|
- El sistema DEBE permitir filtrar por proyecto, periodo, tipo, estado, presupuesto
|
|
- El sistema DEBE actualizar todos los widgets al aplicar filtros
|
|
- El sistema DEBE permitir guardar combinaciones de filtros como "Vistas Rapidas"
|
|
- El sistema DEBE permitir limpiar todos los filtros con un boton
|
|
- El sistema DEBE mantener filtros activos al cambiar entre dashboards
|
|
|
|
### RF-BI-002.5: Segmentacion Avanzada
|
|
- El sistema DEBE permitir crear segmentos con reglas AND/OR
|
|
- El sistema DEBE soportar operadores: igual, diferente, mayor, menor, contiene, entre
|
|
- El sistema DEBE mostrar preview de registros que coinciden con segmento
|
|
- El sistema DEBE permitir guardar segmentos para reutilizacion
|
|
- El sistema DEBE permitir aplicar multiples segmentos simultaneamente
|
|
|
|
### RF-BI-002.6: Exportacion de Visualizaciones
|
|
- El sistema DEBE permitir exportar dashboards a PDF, PowerPoint, Excel, PNG
|
|
- El sistema DEBE mantener calidad de graficas en exportacion (vectorial cuando posible)
|
|
- El sistema DEBE permitir seleccionar widgets especificos para exportar
|
|
- El sistema DEBE incluir metadata (fecha, filtros, usuario) en exportaciones
|
|
- El sistema DEBE generar archivos con nomenclatura estandar
|
|
- El sistema DEBE permitir programar exportaciones automaticas con recurrencia
|
|
|
|
### RF-BI-002.7: Actualizacion en Tiempo Real
|
|
- El sistema DEBE actualizar widgets automaticamente cada 5 minutos
|
|
- El sistema DEBE mostrar indicador de ultima actualizacion
|
|
- El sistema DEBE permitir forzar actualizacion manual
|
|
- El sistema DEBE usar websockets para actualizaciones en tiempo real
|
|
- El sistema DEBE notificar cuando datos estan desactualizados
|
|
|
|
---
|
|
|
|
## 4. Modelo de Datos
|
|
|
|
```typescript
|
|
// Dashboard Configuration
|
|
interface Dashboard {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
ownerId: string;
|
|
visibility: 'private' | 'shared' | 'public';
|
|
sharedWith: string[]; // user IDs
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
|
|
layout: {
|
|
columns: number; // grid columns (typically 12)
|
|
rowHeight: number; // px
|
|
};
|
|
|
|
widgets: Widget[];
|
|
|
|
globalFilters?: {
|
|
projectIds?: string[];
|
|
dateRange?: { from: Date; to: Date };
|
|
projectTypes?: string[];
|
|
status?: string[];
|
|
budgetRange?: { min: number; max: number };
|
|
};
|
|
|
|
autoRefresh: boolean;
|
|
refreshInterval: number; // seconds
|
|
}
|
|
|
|
// Widget Configuration
|
|
interface Widget {
|
|
id: string;
|
|
dashboardId: string;
|
|
type: WidgetType;
|
|
title: string;
|
|
|
|
// Layout position in grid
|
|
position: {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
// Data configuration
|
|
dataSource: {
|
|
type: 'project' | 'multi-project' | 'consolidated';
|
|
projectIds?: string[];
|
|
metric: string; // 'cost', 'progress', 'spi', 'cpi', etc.
|
|
aggregation?: 'sum' | 'avg' | 'min' | 'max' | 'count';
|
|
groupBy?: string; // dimension to group by
|
|
dateRange?: { from: Date; to: Date };
|
|
};
|
|
|
|
// Visualization settings
|
|
visualization: {
|
|
chartType: ChartType;
|
|
colors: string[];
|
|
showValues: boolean;
|
|
showPercentages: boolean;
|
|
showLegend: boolean;
|
|
thresholds?: {
|
|
value: number;
|
|
color: string;
|
|
operator: '>' | '<' | '>=' | '<=' | '=' | '!=';
|
|
}[];
|
|
};
|
|
|
|
// Drill-down configuration
|
|
drillDown?: {
|
|
enabled: boolean;
|
|
levels: {
|
|
dimension: string;
|
|
label: string;
|
|
}[];
|
|
};
|
|
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
type WidgetType =
|
|
| 'kpi-card'
|
|
| 'bar-chart'
|
|
| 'line-chart'
|
|
| 'pie-chart'
|
|
| 'gauge'
|
|
| 'table'
|
|
| 'heat-map'
|
|
| 'timeline';
|
|
|
|
type ChartType =
|
|
| 'vertical-bar'
|
|
| 'horizontal-bar'
|
|
| 'stacked-bar'
|
|
| 'line'
|
|
| 'area'
|
|
| 'pie'
|
|
| 'donut'
|
|
| 'gauge'
|
|
| 'table';
|
|
|
|
// Saved Views (Filter Combinations)
|
|
interface SavedView {
|
|
id: string;
|
|
dashboardId: string;
|
|
name: string;
|
|
filters: Dashboard['globalFilters'];
|
|
createdBy: string;
|
|
createdAt: Date;
|
|
}
|
|
|
|
// Segmentation
|
|
interface Segment {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
rules: SegmentRule[];
|
|
logicalOperator: 'AND' | 'OR';
|
|
createdBy: string;
|
|
createdAt: Date;
|
|
|
|
// Cache for performance
|
|
matchingRecords?: string[]; // IDs
|
|
lastEvaluated?: Date;
|
|
}
|
|
|
|
interface SegmentRule {
|
|
field: string; // 'spi', 'cpi', 'margin', 'budget', etc.
|
|
operator: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'between';
|
|
value: any;
|
|
value2?: any; // for 'between' operator
|
|
}
|
|
|
|
// Export Configuration
|
|
interface ExportConfig {
|
|
dashboardId: string;
|
|
format: 'pdf' | 'pptx' | 'xlsx' | 'png';
|
|
|
|
content: {
|
|
type: 'full' | 'selected';
|
|
widgetIds?: string[]; // if type is 'selected'
|
|
};
|
|
|
|
options: {
|
|
includeFilters: boolean;
|
|
includeTimestamp: boolean;
|
|
includeWatermark: boolean;
|
|
includeRawData: boolean;
|
|
orientation: 'landscape' | 'portrait';
|
|
};
|
|
|
|
// For scheduled exports
|
|
schedule?: {
|
|
enabled: boolean;
|
|
frequency: 'daily' | 'weekly' | 'monthly' | 'custom';
|
|
dayOfWeek?: number; // 0-6 for weekly
|
|
dayOfMonth?: number; // 1-31 for monthly
|
|
time: string; // "HH:mm"
|
|
recipients: string[]; // email addresses
|
|
subject?: string;
|
|
body?: string;
|
|
};
|
|
}
|
|
|
|
// Export Job (for tracking)
|
|
interface ExportJob {
|
|
id: string;
|
|
dashboardId: string;
|
|
configId?: string; // if from scheduled export
|
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
format: ExportConfig['format'];
|
|
filePath?: string;
|
|
fileSize?: number;
|
|
createdBy: string;
|
|
createdAt: Date;
|
|
completedAt?: Date;
|
|
error?: string;
|
|
}
|
|
|
|
// Drill-Down Navigation State
|
|
interface DrillDownState {
|
|
widgetId: string;
|
|
sessionId: string;
|
|
breadcrumbs: {
|
|
level: number;
|
|
dimension: string;
|
|
value: any;
|
|
label: string;
|
|
}[];
|
|
currentLevel: number;
|
|
filters: Record<string, any>;
|
|
}
|
|
```
|
|
|
|
### SQL Schema
|
|
|
|
```sql
|
|
-- Dashboards
|
|
CREATE TABLE dashboards (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
owner_id UUID NOT NULL REFERENCES users(id),
|
|
visibility VARCHAR(20) NOT NULL CHECK (visibility IN ('private', 'shared', 'public')),
|
|
layout JSONB NOT NULL DEFAULT '{"columns": 12, "rowHeight": 100}',
|
|
global_filters JSONB,
|
|
auto_refresh BOOLEAN DEFAULT true,
|
|
refresh_interval INTEGER DEFAULT 300,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
INDEX idx_dashboard_owner (owner_id),
|
|
INDEX idx_dashboard_visibility (visibility)
|
|
);
|
|
|
|
-- Dashboard Sharing
|
|
CREATE TABLE dashboard_shares (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
dashboard_id UUID NOT NULL REFERENCES dashboards(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
permission VARCHAR(20) DEFAULT 'view' CHECK (permission IN ('view', 'edit')),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
UNIQUE(dashboard_id, user_id),
|
|
INDEX idx_share_user (user_id)
|
|
);
|
|
|
|
-- Widgets
|
|
CREATE TABLE widgets (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
dashboard_id UUID NOT NULL REFERENCES dashboards(id) ON DELETE CASCADE,
|
|
type VARCHAR(50) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
position JSONB NOT NULL,
|
|
data_source JSONB NOT NULL,
|
|
visualization JSONB NOT NULL,
|
|
drill_down JSONB,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
INDEX idx_widget_dashboard (dashboard_id)
|
|
);
|
|
|
|
-- Saved Views
|
|
CREATE TABLE saved_views (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
dashboard_id UUID NOT NULL REFERENCES dashboards(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL,
|
|
filters JSONB NOT NULL,
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
INDEX idx_view_dashboard (dashboard_id)
|
|
);
|
|
|
|
-- Segments
|
|
CREATE TABLE segments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
rules JSONB NOT NULL,
|
|
logical_operator VARCHAR(10) DEFAULT 'AND',
|
|
matching_records JSONB,
|
|
last_evaluated TIMESTAMP,
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
INDEX idx_segment_creator (created_by)
|
|
);
|
|
|
|
-- Export Configurations
|
|
CREATE TABLE export_configs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
dashboard_id UUID NOT NULL REFERENCES dashboards(id) ON DELETE CASCADE,
|
|
format VARCHAR(10) NOT NULL,
|
|
content JSONB NOT NULL,
|
|
options JSONB NOT NULL,
|
|
schedule JSONB,
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
INDEX idx_export_config_dashboard (dashboard_id),
|
|
INDEX idx_export_config_scheduled (created_at) WHERE schedule->>'enabled' = 'true'
|
|
);
|
|
|
|
-- Export Jobs
|
|
CREATE TABLE export_jobs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
dashboard_id UUID NOT NULL REFERENCES dashboards(id),
|
|
config_id UUID REFERENCES export_configs(id),
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
format VARCHAR(10) NOT NULL,
|
|
file_path VARCHAR(500),
|
|
file_size BIGINT,
|
|
error TEXT,
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
completed_at TIMESTAMP,
|
|
|
|
INDEX idx_export_job_status (status),
|
|
INDEX idx_export_job_created (created_at)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Criterios de Aceptacion
|
|
|
|
### Creacion y Personalizacion de Dashboards
|
|
- [ ] Interfaz drag-and-drop funciona en navegadores Chrome, Firefox, Safari
|
|
- [ ] Galeria muestra al menos 8 tipos de widgets diferentes
|
|
- [ ] Grid responsive se ajusta correctamente a pantallas 1920x1080, 1366x768, tablets
|
|
- [ ] Dashboards se guardan correctamente con toda su configuracion
|
|
- [ ] Dashboards compartidos son visibles para usuarios autorizados
|
|
- [ ] Duplicar dashboard crea copia exacta con nuevo ID
|
|
|
|
### Configuracion de Widgets
|
|
- [ ] Configuracion de widget se valida antes de aplicar
|
|
- [ ] Vista previa muestra widget exactamente como aparecera en dashboard
|
|
- [ ] Cambio de tipo de grafica actualiza opciones de visualizacion disponibles
|
|
- [ ] Umbrales de colores se aplican correctamente en visualizaciones
|
|
- [ ] Widgets muestran mensaje de error si configuracion es invalida
|
|
|
|
### Drill-Down y Navegacion
|
|
- [ ] Clic en elemento de grafica ejecuta drill-down correctamente
|
|
- [ ] Breadcrumbs muestran ruta completa de navegacion
|
|
- [ ] Boton "Regresar" funciona en todos los niveles
|
|
- [ ] Filtros se mantienen durante navegacion jerarquica
|
|
- [ ] Nivel mas bajo muestra tabla con transacciones individuales
|
|
- [ ] Drill-down funciona en graficas de barras, lineas y circulares
|
|
|
|
### Filtros Dinamicos
|
|
- [ ] Aplicar filtros actualiza todos los widgets consistentemente
|
|
- [ ] Filtros se aplican en <2 segundos para dashboards con hasta 10 widgets
|
|
- [ ] Vistas rapidas guardadas restauran filtros correctamente
|
|
- [ ] Limpiar filtros restaura vista original del dashboard
|
|
- [ ] Filtros de rango de fecha validan que fecha inicio < fecha fin
|
|
|
|
### Segmentacion
|
|
- [ ] Reglas AND/OR funcionan correctamente en combinacion
|
|
- [ ] Preview muestra numero correcto de registros que coinciden
|
|
- [ ] Segmentos guardados se pueden aplicar en cualquier dashboard compatible
|
|
- [ ] Operador "between" valida que valor1 < valor2
|
|
- [ ] Segmentos complejos (>5 reglas) se evaluan en <3 segundos
|
|
|
|
### Exportacion
|
|
- [ ] Exportacion a PDF mantiene calidad de graficas (minimo 300 DPI)
|
|
- [ ] Exportacion a PowerPoint genera diapositivas editables
|
|
- [ ] Exportacion a Excel incluye datos y graficas
|
|
- [ ] Exportacion a PNG genera imagen de alta resolucion (minimo 1920x1080)
|
|
- [ ] Archivos exportados tienen nomenclatura: Dashboard_Nombre_YYYY-MM-DD.ext
|
|
- [ ] Exportaciones programadas se ejecutan en horario configurado (±5 min)
|
|
- [ ] Correos de exportaciones automaticas se envian a todos los destinatarios
|
|
|
|
### Actualizacion en Tiempo Real
|
|
- [ ] Widgets se actualizan automaticamente cada 5 minutos
|
|
- [ ] Indicador "Actualizado hace X minutos" es preciso
|
|
- [ ] Forzar actualizacion manual funciona inmediatamente
|
|
- [ ] Actualizaciones no interrumpen interaccion del usuario
|
|
- [ ] Notificacion aparece si datos tienen >15 minutos de antiguedad
|
|
|
|
---
|
|
|
|
## 6. Notas Tecnicas
|
|
|
|
### Implementacion de Drag-and-Drop
|
|
|
|
```typescript
|
|
import { useCallback } from 'react';
|
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
import GridLayout from 'react-grid-layout';
|
|
import 'react-grid-layout/css/styles.css';
|
|
|
|
const DashboardEditor = ({ dashboard, onLayoutChange }) => {
|
|
const layout = dashboard.widgets.map(widget => ({
|
|
i: widget.id,
|
|
x: widget.position.x,
|
|
y: widget.position.y,
|
|
w: widget.position.width,
|
|
h: widget.position.height,
|
|
minW: 2,
|
|
minH: 2
|
|
}));
|
|
|
|
const handleLayoutChange = useCallback((newLayout) => {
|
|
const updatedWidgets = dashboard.widgets.map(widget => {
|
|
const layoutItem = newLayout.find(item => item.i === widget.id);
|
|
return {
|
|
...widget,
|
|
position: {
|
|
x: layoutItem.x,
|
|
y: layoutItem.y,
|
|
width: layoutItem.w,
|
|
height: layoutItem.h
|
|
}
|
|
};
|
|
});
|
|
|
|
onLayoutChange(updatedWidgets);
|
|
}, [dashboard.widgets, onLayoutChange]);
|
|
|
|
return (
|
|
<GridLayout
|
|
className="dashboard-grid"
|
|
layout={layout}
|
|
cols={12}
|
|
rowHeight={100}
|
|
width={1200}
|
|
onLayoutChange={handleLayoutChange}
|
|
draggableHandle=".widget-drag-handle"
|
|
resizeHandles={['se']}
|
|
>
|
|
{dashboard.widgets.map(widget => (
|
|
<div key={widget.id} className="widget-container">
|
|
<WidgetRenderer widget={widget} />
|
|
</div>
|
|
))}
|
|
</GridLayout>
|
|
);
|
|
};
|
|
```
|
|
|
|
### Implementacion de Drill-Down
|
|
|
|
```typescript
|
|
interface DrillDownContext {
|
|
widgetId: string;
|
|
breadcrumbs: Breadcrumb[];
|
|
filters: Record<string, any>;
|
|
}
|
|
|
|
const useDrillDown = (widget: Widget) => {
|
|
const [context, setContext] = useState<DrillDownContext>({
|
|
widgetId: widget.id,
|
|
breadcrumbs: [],
|
|
filters: {}
|
|
});
|
|
|
|
const drillDown = useCallback((dimension: string, value: any, label: string) => {
|
|
const newBreadcrumb = {
|
|
level: context.breadcrumbs.length,
|
|
dimension,
|
|
value,
|
|
label
|
|
};
|
|
|
|
setContext(prev => ({
|
|
...prev,
|
|
breadcrumbs: [...prev.breadcrumbs, newBreadcrumb],
|
|
filters: {
|
|
...prev.filters,
|
|
[dimension]: value
|
|
}
|
|
}));
|
|
|
|
// Fetch data for next level
|
|
fetchDrillDownData(widget, newBreadcrumb.level + 1, {
|
|
...context.filters,
|
|
[dimension]: value
|
|
});
|
|
}, [context, widget]);
|
|
|
|
const drillUp = useCallback(() => {
|
|
if (context.breadcrumbs.length === 0) return;
|
|
|
|
const newBreadcrumbs = context.breadcrumbs.slice(0, -1);
|
|
const removedCrumb = context.breadcrumbs[context.breadcrumbs.length - 1];
|
|
|
|
const newFilters = { ...context.filters };
|
|
delete newFilters[removedCrumb.dimension];
|
|
|
|
setContext({
|
|
widgetId: widget.id,
|
|
breadcrumbs: newBreadcrumbs,
|
|
filters: newFilters
|
|
});
|
|
}, [context, widget]);
|
|
|
|
return { context, drillDown, drillUp };
|
|
};
|
|
|
|
// Example usage in chart component
|
|
const BarChartWidget = ({ widget, data }) => {
|
|
const { context, drillDown } = useDrillDown(widget);
|
|
|
|
const handleBarClick = (bar) => {
|
|
if (!widget.drillDown?.enabled) return;
|
|
|
|
const nextLevel = context.breadcrumbs.length;
|
|
const levelConfig = widget.drillDown.levels[nextLevel];
|
|
|
|
if (levelConfig) {
|
|
drillDown(levelConfig.dimension, bar.key, bar.label);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ResponsiveBar
|
|
data={data}
|
|
onClick={handleBarClick}
|
|
// ... other props
|
|
/>
|
|
);
|
|
};
|
|
```
|
|
|
|
### Implementacion de Filtros Dinamicos
|
|
|
|
```typescript
|
|
const DashboardFilters = ({ dashboard, onFilterChange }) => {
|
|
const [filters, setFilters] = useState(dashboard.globalFilters || {});
|
|
|
|
const applyFilters = useCallback(() => {
|
|
// Trigger re-fetch of all widget data with new filters
|
|
dashboard.widgets.forEach(widget => {
|
|
fetchWidgetData(widget, filters);
|
|
});
|
|
|
|
onFilterChange(filters);
|
|
}, [filters, dashboard.widgets, onFilterChange]);
|
|
|
|
const handleProjectFilter = (projectIds: string[]) => {
|
|
setFilters(prev => ({ ...prev, projectIds }));
|
|
};
|
|
|
|
const handleDateRangeFilter = (from: Date, to: Date) => {
|
|
setFilters(prev => ({
|
|
...prev,
|
|
dateRange: { from, to }
|
|
}));
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
setFilters({});
|
|
onFilterChange({});
|
|
};
|
|
|
|
return (
|
|
<FilterPanel>
|
|
<MultiSelect
|
|
label="Proyectos"
|
|
options={allProjects}
|
|
value={filters.projectIds}
|
|
onChange={handleProjectFilter}
|
|
/>
|
|
|
|
<DateRangePicker
|
|
from={filters.dateRange?.from}
|
|
to={filters.dateRange?.to}
|
|
onChange={handleDateRangeFilter}
|
|
/>
|
|
|
|
<ButtonGroup>
|
|
<Button onClick={clearFilters}>Limpiar</Button>
|
|
<Button variant="primary" onClick={applyFilters}>
|
|
Aplicar Filtros
|
|
</Button>
|
|
</ButtonGroup>
|
|
</FilterPanel>
|
|
);
|
|
};
|
|
```
|
|
|
|
### Generacion de Exportaciones
|
|
|
|
```typescript
|
|
import { jsPDF } from 'jspdf';
|
|
import pptxgen from 'pptxgenjs';
|
|
import * as XLSX from 'xlsx';
|
|
import html2canvas from 'html2canvas';
|
|
|
|
class DashboardExporter {
|
|
async exportToPDF(dashboard: Dashboard, config: ExportConfig): Promise<Blob> {
|
|
const pdf = new jsPDF({
|
|
orientation: config.options.orientation,
|
|
unit: 'px',
|
|
format: 'a4'
|
|
});
|
|
|
|
// Add title page
|
|
pdf.setFontSize(24);
|
|
pdf.text(dashboard.name, 40, 60);
|
|
|
|
if (config.options.includeTimestamp) {
|
|
pdf.setFontSize(12);
|
|
pdf.text(`Generado: ${new Date().toLocaleString()}`, 40, 80);
|
|
}
|
|
|
|
// Add each widget as a new page
|
|
const widgets = config.content.type === 'full'
|
|
? dashboard.widgets
|
|
: dashboard.widgets.filter(w => config.content.widgetIds?.includes(w.id));
|
|
|
|
for (const [index, widget] of widgets.entries()) {
|
|
if (index > 0) pdf.addPage();
|
|
|
|
const element = document.getElementById(`widget-${widget.id}`);
|
|
const canvas = await html2canvas(element, { scale: 2 });
|
|
const imgData = canvas.toDataURL('image/png');
|
|
|
|
pdf.addImage(imgData, 'PNG', 40, 40, 500, 350);
|
|
|
|
// Add widget title
|
|
pdf.setFontSize(16);
|
|
pdf.text(widget.title, 40, 30);
|
|
}
|
|
|
|
return pdf.output('blob');
|
|
}
|
|
|
|
async exportToPowerPoint(dashboard: Dashboard, config: ExportConfig): Promise<Blob> {
|
|
const pptx = new pptxgen();
|
|
|
|
// Title slide
|
|
const titleSlide = pptx.addSlide();
|
|
titleSlide.addText(dashboard.name, {
|
|
x: 1, y: 2.5, w: 8, h: 1,
|
|
fontSize: 36, bold: true, align: 'center'
|
|
});
|
|
|
|
if (config.options.includeTimestamp) {
|
|
titleSlide.addText(`Generado: ${new Date().toLocaleString()}`, {
|
|
x: 1, y: 4, w: 8, h: 0.5,
|
|
fontSize: 14, align: 'center'
|
|
});
|
|
}
|
|
|
|
// Widget slides
|
|
const widgets = config.content.type === 'full'
|
|
? dashboard.widgets
|
|
: dashboard.widgets.filter(w => config.content.widgetIds?.includes(w.id));
|
|
|
|
for (const widget of widgets) {
|
|
const slide = pptx.addSlide();
|
|
slide.addText(widget.title, {
|
|
x: 0.5, y: 0.5, w: 9, h: 0.75,
|
|
fontSize: 24, bold: true
|
|
});
|
|
|
|
const element = document.getElementById(`widget-${widget.id}`);
|
|
const canvas = await html2canvas(element, { scale: 2 });
|
|
const imgData = canvas.toDataURL('image/png');
|
|
|
|
slide.addImage({
|
|
data: imgData,
|
|
x: 0.5, y: 1.5, w: 9, h: 5
|
|
});
|
|
}
|
|
|
|
const blob = await pptx.write({ outputType: 'blob' });
|
|
return blob as Blob;
|
|
}
|
|
|
|
async exportToExcel(dashboard: Dashboard, config: ExportConfig): Promise<Blob> {
|
|
const workbook = XLSX.utils.book_new();
|
|
|
|
const widgets = config.content.type === 'full'
|
|
? dashboard.widgets
|
|
: dashboard.widgets.filter(w => config.content.widgetIds?.includes(w.id));
|
|
|
|
for (const widget of widgets) {
|
|
const data = await fetchWidgetData(widget, dashboard.globalFilters);
|
|
const worksheet = XLSX.utils.json_to_sheet(data);
|
|
|
|
// Sanitize sheet name (Excel max 31 chars, no special chars)
|
|
const sheetName = widget.title.substring(0, 31).replace(/[:\\\/?*\[\]]/g, '');
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
}
|
|
|
|
const excelBuffer = XLSX.write(workbook, {
|
|
bookType: 'xlsx',
|
|
type: 'array'
|
|
});
|
|
|
|
return new Blob([excelBuffer], {
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
});
|
|
}
|
|
|
|
async exportToPNG(dashboard: Dashboard): Promise<Blob> {
|
|
const element = document.getElementById(`dashboard-${dashboard.id}`);
|
|
const canvas = await html2canvas(element, {
|
|
scale: 2,
|
|
backgroundColor: '#ffffff',
|
|
width: 1920,
|
|
height: 1080
|
|
});
|
|
|
|
return new Promise((resolve) => {
|
|
canvas.toBlob((blob) => resolve(blob!), 'image/png', 1.0);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Scheduled export job processor
|
|
class ScheduledExportProcessor {
|
|
async processScheduledExports() {
|
|
const now = new Date();
|
|
const configs = await db.exportConfigs.findMany({
|
|
where: {
|
|
'schedule.enabled': true
|
|
}
|
|
});
|
|
|
|
for (const config of configs) {
|
|
if (this.shouldExecuteNow(config.schedule, now)) {
|
|
await this.executeExport(config);
|
|
}
|
|
}
|
|
}
|
|
|
|
private shouldExecuteNow(schedule: any, now: Date): boolean {
|
|
const [hour, minute] = schedule.time.split(':').map(Number);
|
|
|
|
if (now.getHours() !== hour || now.getMinutes() !== minute) {
|
|
return false;
|
|
}
|
|
|
|
switch (schedule.frequency) {
|
|
case 'daily':
|
|
return true;
|
|
case 'weekly':
|
|
return now.getDay() === schedule.dayOfWeek;
|
|
case 'monthly':
|
|
return now.getDate() === schedule.dayOfMonth;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async executeExport(config: ExportConfig) {
|
|
const job = await db.exportJobs.create({
|
|
data: {
|
|
dashboardId: config.dashboardId,
|
|
configId: config.id,
|
|
status: 'pending',
|
|
format: config.format,
|
|
createdBy: 'system'
|
|
}
|
|
});
|
|
|
|
try {
|
|
await db.exportJobs.update({
|
|
where: { id: job.id },
|
|
data: { status: 'processing' }
|
|
});
|
|
|
|
const dashboard = await db.dashboards.findUnique({
|
|
where: { id: config.dashboardId },
|
|
include: { widgets: true }
|
|
});
|
|
|
|
const exporter = new DashboardExporter();
|
|
let blob: Blob;
|
|
|
|
switch (config.format) {
|
|
case 'pdf':
|
|
blob = await exporter.exportToPDF(dashboard, config);
|
|
break;
|
|
case 'pptx':
|
|
blob = await exporter.exportToPowerPoint(dashboard, config);
|
|
break;
|
|
case 'xlsx':
|
|
blob = await exporter.exportToExcel(dashboard, config);
|
|
break;
|
|
case 'png':
|
|
blob = await exporter.exportToPNG(dashboard);
|
|
break;
|
|
}
|
|
|
|
const fileName = `${dashboard.name}_${format(new Date(), 'yyyy-MM-dd')}.${config.format}`;
|
|
const filePath = await uploadToStorage(blob, fileName);
|
|
|
|
await db.exportJobs.update({
|
|
where: { id: job.id },
|
|
data: {
|
|
status: 'completed',
|
|
filePath,
|
|
fileSize: blob.size,
|
|
completedAt: new Date()
|
|
}
|
|
});
|
|
|
|
// Send email to recipients
|
|
if (config.schedule.recipients.length > 0) {
|
|
await sendExportEmail(config.schedule.recipients, {
|
|
subject: config.schedule.subject || `Dashboard: ${dashboard.name}`,
|
|
body: config.schedule.body || 'Adjunto encontrará el reporte solicitado.',
|
|
attachment: { fileName, filePath }
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
await db.exportJobs.update({
|
|
where: { id: job.id },
|
|
data: {
|
|
status: 'failed',
|
|
error: error.message
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run scheduled exports every minute
|
|
setInterval(() => {
|
|
const processor = new ScheduledExportProcessor();
|
|
processor.processScheduledExports();
|
|
}, 60 * 1000);
|
|
```
|
|
|
|
### WebSocket para Actualizacion en Tiempo Real
|
|
|
|
```typescript
|
|
import { Server } from 'socket.io';
|
|
import { Server as HTTPServer } from 'http';
|
|
|
|
class DashboardRealtimeService {
|
|
private io: Server;
|
|
|
|
constructor(httpServer: HTTPServer) {
|
|
this.io = new Server(httpServer, {
|
|
cors: { origin: '*' }
|
|
});
|
|
|
|
this.setupEventHandlers();
|
|
}
|
|
|
|
private setupEventHandlers() {
|
|
this.io.on('connection', (socket) => {
|
|
console.log(`Client connected: ${socket.id}`);
|
|
|
|
socket.on('subscribe-dashboard', (dashboardId: string) => {
|
|
socket.join(`dashboard:${dashboardId}`);
|
|
console.log(`Socket ${socket.id} subscribed to dashboard ${dashboardId}`);
|
|
});
|
|
|
|
socket.on('unsubscribe-dashboard', (dashboardId: string) => {
|
|
socket.leave(`dashboard:${dashboardId}`);
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
console.log(`Client disconnected: ${socket.id}`);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Called when widget data changes
|
|
emitWidgetUpdate(dashboardId: string, widgetId: string, data: any) {
|
|
this.io.to(`dashboard:${dashboardId}`).emit('widget-updated', {
|
|
widgetId,
|
|
data,
|
|
timestamp: new Date()
|
|
});
|
|
}
|
|
|
|
// Called when dashboard configuration changes
|
|
emitDashboardUpdate(dashboardId: string, dashboard: Dashboard) {
|
|
this.io.to(`dashboard:${dashboardId}`).emit('dashboard-updated', {
|
|
dashboard,
|
|
timestamp: new Date()
|
|
});
|
|
}
|
|
}
|
|
|
|
// Client-side hook
|
|
const useDashboardRealtime = (dashboardId: string) => {
|
|
const [socket, setSocket] = useState<Socket | null>(null);
|
|
|
|
useEffect(() => {
|
|
const newSocket = io('http://localhost:3000');
|
|
setSocket(newSocket);
|
|
|
|
newSocket.emit('subscribe-dashboard', dashboardId);
|
|
|
|
newSocket.on('widget-updated', ({ widgetId, data, timestamp }) => {
|
|
// Update widget data in state
|
|
updateWidgetData(widgetId, data);
|
|
|
|
// Show notification
|
|
toast.info(`Widget actualizado: ${timestamp.toLocaleTimeString()}`);
|
|
});
|
|
|
|
newSocket.on('dashboard-updated', ({ dashboard, timestamp }) => {
|
|
// Reload dashboard configuration
|
|
loadDashboard(dashboard);
|
|
});
|
|
|
|
return () => {
|
|
newSocket.emit('unsubscribe-dashboard', dashboardId);
|
|
newSocket.close();
|
|
};
|
|
}, [dashboardId]);
|
|
|
|
return socket;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
**Fecha:** 2025-11-17
|
|
**Preparado por:** Equipo de Producto
|
|
**Version:** 1.0
|
|
**Estado:** Listo para Revision
|