-- ============================================================= -- ARCHIVO: 31-reports.sql -- DESCRIPCION: Sistema de Reportes y Analytics -- VERSION: 1.0.0 -- PROYECTO: ERP-Core -- FECHA: 2026-01-26 -- MODULO: MGN-009 (Reports) -- EPIC: EPIC-MGN-009-reports -- ============================================================= -- ===================== -- SCHEMA: reports -- ===================== CREATE SCHEMA IF NOT EXISTS reports; -- ===================== -- ENUMS -- ===================== -- Tipo de reporte DO $$ BEGIN CREATE TYPE reports.report_type AS ENUM ( 'financial', 'accounting', 'tax', 'management', 'operational', 'custom' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- Estado de ejecucion DO $$ BEGIN CREATE TYPE reports.execution_status AS ENUM ( 'pending', 'running', 'completed', 'failed', 'cancelled' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- Formato de exportacion DO $$ BEGIN CREATE TYPE reports.export_format AS ENUM ( 'pdf', 'excel', 'csv', 'json', 'html' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- Metodo de entrega DO $$ BEGIN CREATE TYPE reports.delivery_method AS ENUM ( 'none', 'email', 'storage', 'webhook' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- Tipo de widget DO $$ BEGIN CREATE TYPE reports.widget_type AS ENUM ( 'kpi', 'bar_chart', 'line_chart', 'pie_chart', 'donut_chart', 'gauge', 'table', 'map', 'text' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- Tipo de parametro DO $$ BEGIN CREATE TYPE reports.param_type AS ENUM ( 'string', 'number', 'date', 'daterange', 'boolean', 'select', 'multiselect', 'entity' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- Operador de filtro DO $$ BEGIN CREATE TYPE reports.filter_operator AS ENUM ( 'eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'like', 'ilike', 'in', 'not_in', 'between', 'is_null', 'is_not_null' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ===================== -- TABLA: reports.report_definitions -- Definiciones de reportes -- ===================== CREATE TABLE IF NOT EXISTS reports.report_definitions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Identificacion code VARCHAR(50) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, category VARCHAR(100), report_type reports.report_type NOT NULL DEFAULT 'custom', -- Query base_query TEXT, query_function VARCHAR(255), is_sql_based BOOLEAN NOT NULL DEFAULT true, -- Parametros parameters_schema JSONB DEFAULT '[]', default_parameters JSONB DEFAULT '{}', -- Columnas columns_config JSONB NOT NULL DEFAULT '[]', totals_config JSONB DEFAULT '[]', -- Permisos required_permissions TEXT[] DEFAULT '{}', is_public BOOLEAN NOT NULL DEFAULT false, is_active BOOLEAN NOT NULL DEFAULT true, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), UNIQUE(tenant_id, code) ); -- Indices CREATE INDEX IF NOT EXISTS idx_report_definitions_tenant ON reports.report_definitions(tenant_id); CREATE INDEX IF NOT EXISTS idx_report_definitions_category ON reports.report_definitions(category); CREATE INDEX IF NOT EXISTS idx_report_definitions_type ON reports.report_definitions(report_type); CREATE INDEX IF NOT EXISTS idx_report_definitions_active ON reports.report_definitions(is_active) WHERE is_active = true; -- ===================== -- TABLA: reports.report_executions -- Historial de ejecuciones -- ===================== CREATE TABLE IF NOT EXISTS reports.report_executions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Relaciones report_definition_id UUID NOT NULL REFERENCES reports.report_definitions(id) ON DELETE CASCADE, executed_by UUID NOT NULL REFERENCES auth.users(id), -- Ejecucion status reports.execution_status NOT NULL DEFAULT 'pending', parameters JSONB DEFAULT '{}', -- Resultado result_data JSONB, result_summary JSONB, row_count INTEGER, export_format reports.export_format, file_path TEXT, file_size_bytes BIGINT, -- Timing started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, execution_time_ms INTEGER, -- Error error_message TEXT, error_details JSONB, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Indices CREATE INDEX IF NOT EXISTS idx_report_executions_tenant ON reports.report_executions(tenant_id); CREATE INDEX IF NOT EXISTS idx_report_executions_definition ON reports.report_executions(report_definition_id); CREATE INDEX IF NOT EXISTS idx_report_executions_user ON reports.report_executions(executed_by); CREATE INDEX IF NOT EXISTS idx_report_executions_status ON reports.report_executions(status); CREATE INDEX IF NOT EXISTS idx_report_executions_date ON reports.report_executions(created_at DESC); -- ===================== -- TABLA: reports.report_schedules -- Programacion de reportes -- ===================== CREATE TABLE IF NOT EXISTS reports.report_schedules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Relaciones report_definition_id UUID NOT NULL REFERENCES reports.report_definitions(id) ON DELETE CASCADE, -- Programacion name VARCHAR(255) NOT NULL, cron_expression VARCHAR(100) NOT NULL, timezone VARCHAR(100) NOT NULL DEFAULT 'America/Mexico_City', parameters JSONB DEFAULT '{}', -- Entrega delivery_method reports.delivery_method NOT NULL DEFAULT 'email', delivery_config JSONB DEFAULT '{}', export_format reports.export_format NOT NULL DEFAULT 'pdf', -- Estado is_active BOOLEAN NOT NULL DEFAULT true, last_run_at TIMESTAMPTZ, last_run_status reports.execution_status, next_run_at TIMESTAMPTZ, run_count INTEGER NOT NULL DEFAULT 0, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id) ); -- Indices CREATE INDEX IF NOT EXISTS idx_report_schedules_tenant ON reports.report_schedules(tenant_id); CREATE INDEX IF NOT EXISTS idx_report_schedules_definition ON reports.report_schedules(report_definition_id); CREATE INDEX IF NOT EXISTS idx_report_schedules_active ON reports.report_schedules(is_active) WHERE is_active = true; CREATE INDEX IF NOT EXISTS idx_report_schedules_next_run ON reports.report_schedules(next_run_at) WHERE is_active = true; -- ===================== -- TABLA: reports.report_recipients -- Destinatarios de reportes programados -- ===================== CREATE TABLE IF NOT EXISTS reports.report_recipients ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Relaciones schedule_id UUID NOT NULL REFERENCES reports.report_schedules(id) ON DELETE CASCADE, user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, -- Destinatario externo email VARCHAR(255), name VARCHAR(255), -- Estado is_active BOOLEAN NOT NULL DEFAULT true, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Indices CREATE INDEX IF NOT EXISTS idx_report_recipients_schedule ON reports.report_recipients(schedule_id); CREATE INDEX IF NOT EXISTS idx_report_recipients_user ON reports.report_recipients(user_id); -- ===================== -- TABLA: reports.schedule_executions -- Historial de ejecuciones programadas -- ===================== CREATE TABLE IF NOT EXISTS reports.schedule_executions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Relaciones schedule_id UUID NOT NULL REFERENCES reports.report_schedules(id) ON DELETE CASCADE, execution_id UUID REFERENCES reports.report_executions(id) ON DELETE SET NULL, -- Resultado status reports.execution_status NOT NULL, recipients_notified INTEGER DEFAULT 0, delivery_status JSONB DEFAULT '{}', -- Error error_message TEXT, -- Audit executed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Indices CREATE INDEX IF NOT EXISTS idx_schedule_executions_schedule ON reports.schedule_executions(schedule_id); CREATE INDEX IF NOT EXISTS idx_schedule_executions_date ON reports.schedule_executions(executed_at DESC); -- ===================== -- TABLA: reports.dashboards -- Dashboards personalizados -- ===================== CREATE TABLE IF NOT EXISTS reports.dashboards ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Identificacion name VARCHAR(255) NOT NULL, description TEXT, slug VARCHAR(100), icon VARCHAR(50), -- Layout layout_config JSONB DEFAULT '{"columns": 12, "rowHeight": 80}', -- Estado is_default BOOLEAN NOT NULL DEFAULT false, is_public BOOLEAN NOT NULL DEFAULT false, is_active BOOLEAN NOT NULL DEFAULT true, -- Permisos owner_id UUID NOT NULL REFERENCES auth.users(id), allowed_roles TEXT[] DEFAULT '{}', -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Indices CREATE INDEX IF NOT EXISTS idx_dashboards_tenant ON reports.dashboards(tenant_id); CREATE INDEX IF NOT EXISTS idx_dashboards_owner ON reports.dashboards(owner_id); CREATE INDEX IF NOT EXISTS idx_dashboards_default ON reports.dashboards(tenant_id, is_default) WHERE is_default = true; -- ===================== -- TABLA: reports.dashboard_widgets -- Widgets de dashboards -- ===================== CREATE TABLE IF NOT EXISTS reports.dashboard_widgets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Relaciones dashboard_id UUID NOT NULL REFERENCES reports.dashboards(id) ON DELETE CASCADE, -- Identificacion title VARCHAR(255) NOT NULL, widget_type reports.widget_type NOT NULL, -- Posicion position_x INTEGER NOT NULL DEFAULT 0, position_y INTEGER NOT NULL DEFAULT 0, width INTEGER NOT NULL DEFAULT 3, height INTEGER NOT NULL DEFAULT 2, -- Configuracion config JSONB NOT NULL DEFAULT '{}', refresh_interval_seconds INTEGER DEFAULT 300, -- Estado is_active BOOLEAN NOT NULL DEFAULT true, sort_order INTEGER NOT NULL DEFAULT 0, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Indices CREATE INDEX IF NOT EXISTS idx_dashboard_widgets_dashboard ON reports.dashboard_widgets(dashboard_id); CREATE INDEX IF NOT EXISTS idx_dashboard_widgets_type ON reports.dashboard_widgets(widget_type); -- ===================== -- TABLA: reports.widget_queries -- Queries de widgets -- ===================== CREATE TABLE IF NOT EXISTS reports.widget_queries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Relaciones widget_id UUID NOT NULL REFERENCES reports.dashboard_widgets(id) ON DELETE CASCADE, -- Query name VARCHAR(100) NOT NULL, query_text TEXT, query_function VARCHAR(255), parameters JSONB DEFAULT '{}', -- Resultado result_mapping JSONB DEFAULT '{}', -- Cache cache_ttl_seconds INTEGER DEFAULT 300, last_cached_at TIMESTAMPTZ, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Indices CREATE INDEX IF NOT EXISTS idx_widget_queries_widget ON reports.widget_queries(widget_id); -- ===================== -- TABLA: reports.custom_reports -- Reportes personalizados por usuario -- ===================== CREATE TABLE IF NOT EXISTS reports.custom_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Propietario owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, -- Base base_definition_id UUID REFERENCES reports.report_definitions(id) ON DELETE SET NULL, -- Identificacion name VARCHAR(255) NOT NULL, description TEXT, -- Configuracion personalizada custom_columns JSONB DEFAULT '[]', custom_filters JSONB DEFAULT '[]', custom_grouping JSONB DEFAULT '[]', custom_sorting JSONB DEFAULT '[]', -- Estado is_favorite BOOLEAN NOT NULL DEFAULT false, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Indices CREATE INDEX IF NOT EXISTS idx_custom_reports_tenant ON reports.custom_reports(tenant_id); CREATE INDEX IF NOT EXISTS idx_custom_reports_owner ON reports.custom_reports(owner_id); CREATE INDEX IF NOT EXISTS idx_custom_reports_favorite ON reports.custom_reports(owner_id, is_favorite) WHERE is_favorite = true; -- ===================== -- TABLA: reports.data_model_entities -- Modelo de datos para report builder -- ===================== CREATE TABLE IF NOT EXISTS reports.data_model_entities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificacion name VARCHAR(100) NOT NULL UNIQUE, display_name VARCHAR(255) NOT NULL, description TEXT, -- Schema schema_name VARCHAR(100) NOT NULL, table_name VARCHAR(100) NOT NULL, -- Configuracion primary_key_column VARCHAR(100) NOT NULL DEFAULT 'id', tenant_column VARCHAR(100) DEFAULT 'tenant_id', is_multi_tenant BOOLEAN NOT NULL DEFAULT true, -- Estado is_active BOOLEAN NOT NULL DEFAULT true, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Indices CREATE INDEX IF NOT EXISTS idx_data_model_entities_name ON reports.data_model_entities(name); CREATE INDEX IF NOT EXISTS idx_data_model_entities_schema ON reports.data_model_entities(schema_name, table_name); -- ===================== -- TABLA: reports.data_model_fields -- Campos del modelo de datos -- ===================== CREATE TABLE IF NOT EXISTS reports.data_model_fields ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Relaciones entity_id UUID NOT NULL REFERENCES reports.data_model_entities(id) ON DELETE CASCADE, -- Identificacion name VARCHAR(100) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT, -- Tipo data_type VARCHAR(50) NOT NULL, is_nullable BOOLEAN NOT NULL DEFAULT true, -- Configuracion UI is_filterable BOOLEAN NOT NULL DEFAULT true, is_sortable BOOLEAN NOT NULL DEFAULT true, is_groupable BOOLEAN NOT NULL DEFAULT false, is_aggregatable BOOLEAN NOT NULL DEFAULT false, aggregation_functions TEXT[] DEFAULT '{}', -- Formato format_pattern VARCHAR(100), display_format VARCHAR(50), -- Estado is_active BOOLEAN NOT NULL DEFAULT true, sort_order INTEGER NOT NULL DEFAULT 0, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(entity_id, name) ); -- Indices CREATE INDEX IF NOT EXISTS idx_data_model_fields_entity ON reports.data_model_fields(entity_id); -- ===================== -- TABLA: reports.data_model_relationships -- Relaciones entre entidades del modelo -- ===================== CREATE TABLE IF NOT EXISTS reports.data_model_relationships ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Entidades source_entity_id UUID NOT NULL REFERENCES reports.data_model_entities(id) ON DELETE CASCADE, target_entity_id UUID NOT NULL REFERENCES reports.data_model_entities(id) ON DELETE CASCADE, -- Relacion name VARCHAR(100) NOT NULL, relationship_type VARCHAR(20) NOT NULL CHECK (relationship_type IN ('one_to_one', 'one_to_many', 'many_to_one', 'many_to_many')), -- Join source_column VARCHAR(100) NOT NULL, target_column VARCHAR(100) NOT NULL, join_condition TEXT, -- Estado is_active BOOLEAN NOT NULL DEFAULT true, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(source_entity_id, target_entity_id, name) ); -- Indices CREATE INDEX IF NOT EXISTS idx_data_model_relationships_source ON reports.data_model_relationships(source_entity_id); CREATE INDEX IF NOT EXISTS idx_data_model_relationships_target ON reports.data_model_relationships(target_entity_id); -- ===================== -- RLS POLICIES -- ===================== -- Report Definitions ALTER TABLE reports.report_definitions ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation_definitions ON reports.report_definitions; CREATE POLICY tenant_isolation_definitions ON reports.report_definitions FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Report Executions ALTER TABLE reports.report_executions ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation_executions ON reports.report_executions; CREATE POLICY tenant_isolation_executions ON reports.report_executions FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Report Schedules ALTER TABLE reports.report_schedules ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation_schedules ON reports.report_schedules; CREATE POLICY tenant_isolation_schedules ON reports.report_schedules FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Dashboards ALTER TABLE reports.dashboards ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation_dashboards ON reports.dashboards; CREATE POLICY tenant_isolation_dashboards ON reports.dashboards FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Custom Reports ALTER TABLE reports.custom_reports ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation_custom_reports ON reports.custom_reports; CREATE POLICY tenant_isolation_custom_reports ON reports.custom_reports FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- ===================== -- TRIGGERS -- ===================== CREATE OR REPLACE FUNCTION reports.update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_report_definitions_updated_at BEFORE UPDATE ON reports.report_definitions FOR EACH ROW EXECUTE FUNCTION reports.update_timestamp(); CREATE TRIGGER trg_report_schedules_updated_at BEFORE UPDATE ON reports.report_schedules FOR EACH ROW EXECUTE FUNCTION reports.update_timestamp(); CREATE TRIGGER trg_dashboards_updated_at BEFORE UPDATE ON reports.dashboards FOR EACH ROW EXECUTE FUNCTION reports.update_timestamp(); CREATE TRIGGER trg_dashboard_widgets_updated_at BEFORE UPDATE ON reports.dashboard_widgets FOR EACH ROW EXECUTE FUNCTION reports.update_timestamp(); CREATE TRIGGER trg_custom_reports_updated_at BEFORE UPDATE ON reports.custom_reports FOR EACH ROW EXECUTE FUNCTION reports.update_timestamp(); -- ===================== -- FUNCIONES DE UTILIDAD -- ===================== -- Funcion para obtener reportes activos de un tenant CREATE OR REPLACE FUNCTION reports.get_active_reports(p_tenant_id UUID) RETURNS TABLE ( id UUID, code VARCHAR(50), name VARCHAR(255), category VARCHAR(100), report_type reports.report_type ) AS $$ BEGIN RETURN QUERY SELECT rd.id, rd.code, rd.name, rd.category, rd.report_type FROM reports.report_definitions rd WHERE rd.tenant_id = p_tenant_id AND rd.is_active = true ORDER BY rd.category, rd.name; END; $$ LANGUAGE plpgsql STABLE; -- Funcion para obtener historial de ejecuciones recientes CREATE OR REPLACE FUNCTION reports.get_recent_executions( p_tenant_id UUID, p_limit INTEGER DEFAULT 10 ) RETURNS TABLE ( id UUID, report_name VARCHAR(255), status reports.execution_status, executed_at TIMESTAMPTZ, execution_time_ms INTEGER ) AS $$ BEGIN RETURN QUERY SELECT re.id, rd.name, re.status, re.created_at, re.execution_time_ms FROM reports.report_executions re JOIN reports.report_definitions rd ON rd.id = re.report_definition_id WHERE re.tenant_id = p_tenant_id ORDER BY re.created_at DESC LIMIT p_limit; END; $$ LANGUAGE plpgsql STABLE; -- ===================== -- COMENTARIOS -- ===================== COMMENT ON SCHEMA reports IS 'Schema para sistema de reportes y analytics'; COMMENT ON TABLE reports.report_definitions IS 'Definiciones de reportes'; COMMENT ON TABLE reports.report_executions IS 'Historial de ejecuciones de reportes'; COMMENT ON TABLE reports.report_schedules IS 'Programacion de reportes automaticos'; COMMENT ON TABLE reports.report_recipients IS 'Destinatarios de reportes programados'; COMMENT ON TABLE reports.dashboards IS 'Dashboards personalizados'; COMMENT ON TABLE reports.dashboard_widgets IS 'Widgets de dashboards'; COMMENT ON TABLE reports.custom_reports IS 'Reportes personalizados por usuario'; COMMENT ON TABLE reports.data_model_entities IS 'Modelo de datos para report builder'; COMMENT ON TABLE reports.data_model_fields IS 'Campos del modelo de datos'; COMMENT ON TABLE reports.data_model_relationships IS 'Relaciones entre entidades';