erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/requerimientos/RF-BI-002-dashboards-interactivos.md

48 KiB

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

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

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

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

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

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

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

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