feat(ddl): Add DDL for Settings, Reports and HR modules
- 30-settings.sql: system_settings, plan_settings, tenant_settings, user_preferences (4 tables) - 31-reports.sql: report_definitions, executions, schedules, dashboards, widgets (12 tables, 7 enums) - 45-hr.sql: employees, departments, job_positions, contracts, leave_types, leaves (7 tables, 6 enums) Includes RLS policies, triggers, and utility functions for each module. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
32ac439c76
commit
e964ff4812
319
ddl/30-settings.sql
Normal file
319
ddl/30-settings.sql
Normal file
@ -0,0 +1,319 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 30-settings.sql
|
||||
-- DESCRIPCION: Sistema de Settings para configuraciones multi-nivel
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core
|
||||
-- FECHA: 2026-01-26
|
||||
-- MODULO: MGN-006 (Settings)
|
||||
-- RF: RF-SETTINGS-001, RF-SETTINGS-002, RF-SETTINGS-003
|
||||
-- NOTA: Feature Flags ya existen en 11-feature-flags.sql (schema flags)
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- SCHEMA: core_settings
|
||||
-- =====================
|
||||
CREATE SCHEMA IF NOT EXISTS core_settings;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: core_settings.system_settings
|
||||
-- Configuraciones globales del sistema (RF-SETTINGS-001)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS core_settings.system_settings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Identificacion
|
||||
key VARCHAR(100) UNIQUE NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
|
||||
-- Metadata
|
||||
data_type VARCHAR(20) NOT NULL DEFAULT 'string'
|
||||
CHECK (data_type IN ('string', 'number', 'boolean', 'json', 'array', 'secret')),
|
||||
category VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- Control
|
||||
is_public BOOLEAN NOT NULL DEFAULT false,
|
||||
is_editable BOOLEAN NOT NULL DEFAULT true,
|
||||
default_value JSONB,
|
||||
validation_rules JSONB DEFAULT '{}',
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- Indices para system_settings
|
||||
CREATE INDEX IF NOT EXISTS idx_system_settings_category
|
||||
ON core_settings.system_settings(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_system_settings_public
|
||||
ON core_settings.system_settings(is_public) WHERE is_public = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_system_settings_editable
|
||||
ON core_settings.system_settings(is_editable) WHERE is_editable = true;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: core_settings.plan_settings
|
||||
-- Configuraciones por defecto por plan de suscripcion (RF-SETTINGS-002)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS core_settings.plan_settings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Relaciones
|
||||
plan_id UUID NOT NULL REFERENCES billing.subscription_plans(id) ON DELETE CASCADE,
|
||||
key VARCHAR(100) NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(plan_id, key)
|
||||
);
|
||||
|
||||
-- Indices para plan_settings
|
||||
CREATE INDEX IF NOT EXISTS idx_plan_settings_plan
|
||||
ON core_settings.plan_settings(plan_id);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: core_settings.tenant_settings
|
||||
-- Configuraciones personalizadas por tenant (RF-SETTINGS-002)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS core_settings.tenant_settings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Relaciones
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
key VARCHAR(100) NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
|
||||
-- Herencia
|
||||
inherited_from VARCHAR(20) NOT NULL DEFAULT 'custom'
|
||||
CHECK (inherited_from IN ('system', 'plan', 'custom')),
|
||||
is_overridden BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(tenant_id, key)
|
||||
);
|
||||
|
||||
-- Indices para tenant_settings
|
||||
CREATE INDEX IF NOT EXISTS idx_tenant_settings_tenant
|
||||
ON core_settings.tenant_settings(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tenant_settings_key
|
||||
ON core_settings.tenant_settings(key);
|
||||
|
||||
-- RLS para tenant_settings
|
||||
ALTER TABLE core_settings.tenant_settings ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_settings ON core_settings.tenant_settings;
|
||||
CREATE POLICY tenant_isolation_settings ON core_settings.tenant_settings
|
||||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: core_settings.user_preferences
|
||||
-- Preferencias personales de usuario (RF-SETTINGS-003)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS core_settings.user_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Relaciones
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
key VARCHAR(100) NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
|
||||
-- Sync
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(user_id, key)
|
||||
);
|
||||
|
||||
-- Indices para user_preferences
|
||||
CREATE INDEX IF NOT EXISTS idx_user_preferences_user
|
||||
ON core_settings.user_preferences(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_preferences_key
|
||||
ON core_settings.user_preferences(key);
|
||||
|
||||
-- =====================
|
||||
-- FUNCIONES
|
||||
-- =====================
|
||||
|
||||
-- Funcion para obtener setting efectivo del tenant (herencia system -> plan -> tenant)
|
||||
CREATE OR REPLACE FUNCTION core_settings.get_tenant_setting(
|
||||
p_tenant_id UUID,
|
||||
p_key VARCHAR
|
||||
) RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
v_value JSONB;
|
||||
v_plan_id UUID;
|
||||
BEGIN
|
||||
-- 1. Buscar en tenant_settings (override)
|
||||
SELECT value INTO v_value
|
||||
FROM core_settings.tenant_settings
|
||||
WHERE tenant_id = p_tenant_id
|
||||
AND key = p_key
|
||||
AND is_overridden = true;
|
||||
|
||||
IF v_value IS NOT NULL THEN
|
||||
RETURN v_value;
|
||||
END IF;
|
||||
|
||||
-- 2. Buscar en plan_settings
|
||||
SELECT t.plan_id INTO v_plan_id
|
||||
FROM auth.tenants t WHERE t.id = p_tenant_id;
|
||||
|
||||
IF v_plan_id IS NOT NULL THEN
|
||||
SELECT value INTO v_value
|
||||
FROM core_settings.plan_settings
|
||||
WHERE plan_id = v_plan_id AND key = p_key;
|
||||
|
||||
IF v_value IS NOT NULL THEN
|
||||
RETURN v_value;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- 3. Retornar system default
|
||||
SELECT COALESCE(value, default_value) INTO v_value
|
||||
FROM core_settings.system_settings
|
||||
WHERE key = p_key;
|
||||
|
||||
RETURN v_value;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Funcion para obtener todas las preferencias de un usuario
|
||||
CREATE OR REPLACE FUNCTION core_settings.get_user_preferences(p_user_id UUID)
|
||||
RETURNS TABLE (
|
||||
key VARCHAR(100),
|
||||
value JSONB,
|
||||
synced_at TIMESTAMPTZ
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT up.key, up.value, up.synced_at
|
||||
FROM core_settings.user_preferences up
|
||||
WHERE up.user_id = p_user_id
|
||||
ORDER BY up.key;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Funcion para obtener todos los settings de un tenant (merged)
|
||||
CREATE OR REPLACE FUNCTION core_settings.get_all_tenant_settings(p_tenant_id UUID)
|
||||
RETURNS TABLE (
|
||||
key VARCHAR(100),
|
||||
value JSONB,
|
||||
source VARCHAR(20)
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
-- System settings publicos
|
||||
SELECT ss.key, COALESCE(ss.value, ss.default_value), 'system'::VARCHAR(20)
|
||||
FROM core_settings.system_settings ss
|
||||
WHERE ss.is_public = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM core_settings.tenant_settings ts
|
||||
WHERE ts.tenant_id = p_tenant_id AND ts.key = ss.key AND ts.is_overridden = true
|
||||
)
|
||||
UNION ALL
|
||||
-- Tenant overrides
|
||||
SELECT ts.key, ts.value, ts.inherited_from
|
||||
FROM core_settings.tenant_settings ts
|
||||
WHERE ts.tenant_id = p_tenant_id AND ts.is_overridden = true
|
||||
ORDER BY key;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- =====================
|
||||
-- TRIGGERS
|
||||
-- =====================
|
||||
|
||||
-- Trigger para updated_at
|
||||
CREATE OR REPLACE FUNCTION core_settings.update_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_system_settings_updated_at
|
||||
BEFORE UPDATE ON core_settings.system_settings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION core_settings.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_plan_settings_updated_at
|
||||
BEFORE UPDATE ON core_settings.plan_settings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION core_settings.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_tenant_settings_updated_at
|
||||
BEFORE UPDATE ON core_settings.tenant_settings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION core_settings.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_user_preferences_updated_at
|
||||
BEFORE UPDATE ON core_settings.user_preferences
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION core_settings.update_timestamp();
|
||||
|
||||
-- =====================
|
||||
-- SEED DATA: System Settings Base
|
||||
-- =====================
|
||||
INSERT INTO core_settings.system_settings (key, value, data_type, category, description, is_public, is_editable, default_value) VALUES
|
||||
-- Security
|
||||
('security.max_login_attempts', '5', 'number', 'security', 'Intentos maximos de login antes de bloqueo', true, true, '5'),
|
||||
('security.lockout_duration_minutes', '15', 'number', 'security', 'Duracion de bloqueo en minutos', true, true, '15'),
|
||||
('security.session_timeout_hours', '24', 'number', 'security', 'Timeout de sesion en horas', true, true, '24'),
|
||||
('security.password_min_length', '8', 'number', 'security', 'Longitud minima de password', true, true, '8'),
|
||||
('security.require_mfa', 'false', 'boolean', 'security', 'Requerir MFA para todos los usuarios', true, true, 'false'),
|
||||
('security.allowed_ip_ranges', '[]', 'array', 'security', 'Rangos IP permitidos (vacio = todos)', false, true, '[]'),
|
||||
|
||||
-- Email
|
||||
('email.smtp_host', '""', 'string', 'email', 'Host SMTP para envio de emails', false, true, '""'),
|
||||
('email.smtp_port', '587', 'number', 'email', 'Puerto SMTP', false, true, '587'),
|
||||
('email.smtp_secure', 'true', 'boolean', 'email', 'Usar TLS para conexion SMTP', false, true, 'true'),
|
||||
('email.from_address', '"noreply@erp-core.local"', 'string', 'email', 'Email de origen por defecto', true, true, '"noreply@erp-core.local"'),
|
||||
('email.from_name', '"ERP Core"', 'string', 'email', 'Nombre de remitente por defecto', true, true, '"ERP Core"'),
|
||||
|
||||
-- Storage
|
||||
('storage.max_file_size_mb', '10', 'number', 'storage', 'Tamano maximo de archivo en MB', true, true, '10'),
|
||||
('storage.allowed_extensions', '["pdf","jpg","jpeg","png","xlsx","docx","csv"]', 'array', 'storage', 'Extensiones de archivo permitidas', true, true, '["pdf","jpg","jpeg","png","xlsx","docx","csv"]'),
|
||||
('storage.provider', '"local"', 'string', 'storage', 'Proveedor de almacenamiento (local, s3, r2)', false, true, '"local"'),
|
||||
|
||||
-- Performance
|
||||
('performance.cache_ttl_seconds', '300', 'number', 'performance', 'TTL de cache en segundos', false, true, '300'),
|
||||
('performance.pagination_default_limit', '20', 'number', 'performance', 'Items por pagina por defecto', true, true, '20'),
|
||||
('performance.pagination_max_limit', '100', 'number', 'performance', 'Maximo items por pagina', true, false, '100'),
|
||||
|
||||
-- Localization
|
||||
('localization.default_timezone', '"America/Mexico_City"', 'string', 'localization', 'Timezone por defecto', true, true, '"America/Mexico_City"'),
|
||||
('localization.default_locale', '"es-MX"', 'string', 'localization', 'Locale por defecto', true, true, '"es-MX"'),
|
||||
('localization.default_currency', '"MXN"', 'string', 'localization', 'Moneda por defecto', true, true, '"MXN"'),
|
||||
('localization.date_format', '"DD/MM/YYYY"', 'string', 'localization', 'Formato de fecha', true, true, '"DD/MM/YYYY"'),
|
||||
|
||||
-- Business
|
||||
('business.fiscal_year_start_month', '1', 'number', 'business', 'Mes de inicio del ano fiscal', true, true, '1'),
|
||||
('business.default_tax_rate', '16', 'number', 'business', 'Tasa de IVA por defecto', true, true, '16'),
|
||||
('business.invoice_prefix', '"INV-"', 'string', 'business', 'Prefijo de facturas', true, true, '"INV-"'),
|
||||
('business.require_approval_amount', '10000', 'number', 'business', 'Monto que requiere aprobacion', true, true, '10000')
|
||||
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
-- =====================
|
||||
-- COMENTARIOS
|
||||
-- =====================
|
||||
COMMENT ON SCHEMA core_settings IS 'Schema para configuraciones del sistema multi-nivel';
|
||||
COMMENT ON TABLE core_settings.system_settings IS 'Configuraciones globales del sistema';
|
||||
COMMENT ON TABLE core_settings.plan_settings IS 'Configuraciones por defecto por plan de suscripcion';
|
||||
COMMENT ON TABLE core_settings.tenant_settings IS 'Configuraciones personalizadas por tenant';
|
||||
COMMENT ON TABLE core_settings.user_preferences IS 'Preferencias personales de usuario';
|
||||
COMMENT ON FUNCTION core_settings.get_tenant_setting IS 'Obtiene setting efectivo con herencia system->plan->tenant';
|
||||
COMMENT ON FUNCTION core_settings.get_user_preferences IS 'Obtiene todas las preferencias de un usuario';
|
||||
COMMENT ON FUNCTION core_settings.get_all_tenant_settings IS 'Obtiene todos los settings de un tenant merged con sistema';
|
||||
679
ddl/31-reports.sql
Normal file
679
ddl/31-reports.sql
Normal file
@ -0,0 +1,679 @@
|
||||
-- =============================================================
|
||||
-- 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';
|
||||
666
ddl/45-hr.sql
Normal file
666
ddl/45-hr.sql
Normal file
@ -0,0 +1,666 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 45-hr.sql
|
||||
-- DESCRIPCION: Sistema de Recursos Humanos (RRHH)
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core
|
||||
-- FECHA: 2026-01-26
|
||||
-- MODULO: MGN-010 (RRHH Basico)
|
||||
-- REFERENCIA: odoo-hr-analysis.md, hr-domain.md
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- SCHEMA: hr
|
||||
-- =====================
|
||||
CREATE SCHEMA IF NOT EXISTS hr;
|
||||
|
||||
-- =====================
|
||||
-- ENUMS
|
||||
-- =====================
|
||||
|
||||
-- Estado del empleado
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE hr.employee_status AS ENUM (
|
||||
'active',
|
||||
'inactive',
|
||||
'on_leave',
|
||||
'terminated'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- Tipo de contrato
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE hr.contract_type AS ENUM (
|
||||
'permanent',
|
||||
'temporary',
|
||||
'contractor',
|
||||
'internship',
|
||||
'part_time'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- Estado del contrato
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE hr.contract_status AS ENUM (
|
||||
'draft',
|
||||
'active',
|
||||
'expired',
|
||||
'terminated',
|
||||
'cancelled'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- Tipo de ausencia
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE hr.leave_type_category AS ENUM (
|
||||
'vacation',
|
||||
'sick',
|
||||
'personal',
|
||||
'maternity',
|
||||
'paternity',
|
||||
'bereavement',
|
||||
'unpaid',
|
||||
'other'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- Estado de ausencia
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE hr.leave_status AS ENUM (
|
||||
'draft',
|
||||
'submitted',
|
||||
'approved',
|
||||
'rejected',
|
||||
'cancelled'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- Tipo de asignacion
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE hr.allocation_type AS ENUM (
|
||||
'fixed',
|
||||
'accrual',
|
||||
'unlimited'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: hr.departments
|
||||
-- Departamentos organizacionales
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS hr.departments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(50),
|
||||
|
||||
-- Jerarquia
|
||||
parent_id UUID REFERENCES hr.departments(id) ON DELETE SET NULL,
|
||||
manager_id UUID, -- FK a employees (se agrega despues)
|
||||
|
||||
-- Estado
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
-- Metadata
|
||||
description TEXT,
|
||||
color VARCHAR(7),
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(tenant_id, company_id, code)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_departments_tenant ON hr.departments(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_departments_company ON hr.departments(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_departments_parent ON hr.departments(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_departments_active ON hr.departments(is_active) WHERE is_active = true;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: hr.job_positions
|
||||
-- Puestos de trabajo
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS hr.job_positions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(50),
|
||||
description TEXT,
|
||||
|
||||
-- Relaciones
|
||||
department_id UUID REFERENCES hr.departments(id) ON DELETE SET NULL,
|
||||
|
||||
-- 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,
|
||||
|
||||
UNIQUE(tenant_id, company_id, code)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_job_positions_tenant ON hr.job_positions(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_job_positions_department ON hr.job_positions(department_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_job_positions_active ON hr.job_positions(is_active) WHERE is_active = true;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: hr.employees
|
||||
-- Empleados
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS hr.employees (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion
|
||||
employee_number VARCHAR(50) NOT NULL,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
full_name VARCHAR(255) GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED,
|
||||
|
||||
-- Relaciones con usuario y partner
|
||||
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
partner_id UUID, -- REFERENCES partners.partners(id)
|
||||
|
||||
-- Organizacion
|
||||
department_id UUID REFERENCES hr.departments(id) ON DELETE SET NULL,
|
||||
job_position_id UUID REFERENCES hr.job_positions(id) ON DELETE SET NULL,
|
||||
manager_id UUID REFERENCES hr.employees(id) ON DELETE SET NULL,
|
||||
|
||||
-- Contacto laboral
|
||||
work_email VARCHAR(255),
|
||||
work_phone VARCHAR(50),
|
||||
work_mobile VARCHAR(50),
|
||||
work_location VARCHAR(255),
|
||||
|
||||
-- Datos personales
|
||||
personal_email VARCHAR(255),
|
||||
personal_phone VARCHAR(50),
|
||||
birth_date DATE,
|
||||
gender VARCHAR(20),
|
||||
marital_status VARCHAR(20),
|
||||
|
||||
-- Identificacion oficial
|
||||
identification_type VARCHAR(50),
|
||||
identification_number VARCHAR(100),
|
||||
tax_id VARCHAR(50), -- RFC en Mexico
|
||||
social_security_number VARCHAR(50), -- NSS/IMSS
|
||||
|
||||
-- Direccion
|
||||
street VARCHAR(255),
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
postal_code VARCHAR(20),
|
||||
country VARCHAR(100),
|
||||
|
||||
-- Empleo
|
||||
hire_date DATE NOT NULL,
|
||||
termination_date DATE,
|
||||
status hr.employee_status NOT NULL DEFAULT 'active',
|
||||
|
||||
-- Emergencia
|
||||
emergency_contact_name VARCHAR(255),
|
||||
emergency_contact_phone VARCHAR(50),
|
||||
emergency_contact_relationship VARCHAR(50),
|
||||
|
||||
-- Metadata
|
||||
notes TEXT,
|
||||
avatar_url TEXT,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(tenant_id, employee_number)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_tenant ON hr.employees(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_company ON hr.employees(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_department ON hr.employees(department_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_job ON hr.employees(job_position_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_manager ON hr.employees(manager_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_user ON hr.employees(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_status ON hr.employees(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_active ON hr.employees(status) WHERE status = 'active';
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_name ON hr.employees(full_name);
|
||||
|
||||
-- Agregar FK de manager_id en departments
|
||||
ALTER TABLE hr.departments
|
||||
ADD CONSTRAINT fk_departments_manager
|
||||
FOREIGN KEY (manager_id) REFERENCES hr.employees(id) ON DELETE SET NULL;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: hr.contracts
|
||||
-- Contratos laborales
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS hr.contracts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
||||
|
||||
-- Relaciones
|
||||
employee_id UUID NOT NULL REFERENCES hr.employees(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion
|
||||
name VARCHAR(255) NOT NULL,
|
||||
reference VARCHAR(50),
|
||||
|
||||
-- Tipo y estado
|
||||
contract_type hr.contract_type NOT NULL DEFAULT 'permanent',
|
||||
status hr.contract_status NOT NULL DEFAULT 'draft',
|
||||
|
||||
-- Vigencia
|
||||
date_start DATE NOT NULL,
|
||||
date_end DATE,
|
||||
|
||||
-- Puesto y departamento al momento del contrato
|
||||
job_position_id UUID REFERENCES hr.job_positions(id) ON DELETE SET NULL,
|
||||
department_id UUID REFERENCES hr.departments(id) ON DELETE SET NULL,
|
||||
|
||||
-- Compensacion
|
||||
wage DECIMAL(15,2) NOT NULL,
|
||||
wage_type VARCHAR(20) NOT NULL DEFAULT 'monthly' CHECK (wage_type IN ('hourly', 'daily', 'weekly', 'biweekly', 'monthly', 'annual')),
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'MXN',
|
||||
|
||||
-- Jornada
|
||||
hours_per_week DECIMAL(5,2) DEFAULT 48,
|
||||
schedule_type VARCHAR(50),
|
||||
|
||||
-- Probatorio
|
||||
trial_period_months INTEGER DEFAULT 0,
|
||||
trial_date_end DATE,
|
||||
|
||||
-- Metadata
|
||||
notes TEXT,
|
||||
document_url TEXT,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
activated_at TIMESTAMPTZ,
|
||||
terminated_at TIMESTAMPTZ,
|
||||
terminated_by UUID REFERENCES auth.users(id),
|
||||
termination_reason TEXT
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_contracts_tenant ON hr.contracts(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_contracts_employee ON hr.contracts(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_contracts_status ON hr.contracts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_contracts_active ON hr.contracts(employee_id, status) WHERE status = 'active';
|
||||
CREATE INDEX IF NOT EXISTS idx_contracts_dates ON hr.contracts(date_start, date_end);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: hr.leave_types
|
||||
-- Tipos de ausencia
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS hr.leave_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(7) DEFAULT '#3B82F6',
|
||||
|
||||
-- Categoria
|
||||
leave_category hr.leave_type_category NOT NULL DEFAULT 'other',
|
||||
|
||||
-- Configuracion
|
||||
allocation_type hr.allocation_type NOT NULL DEFAULT 'fixed',
|
||||
requires_approval BOOLEAN NOT NULL DEFAULT true,
|
||||
requires_document BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- Limites
|
||||
max_days_per_request INTEGER,
|
||||
max_days_per_year INTEGER,
|
||||
min_days_notice INTEGER DEFAULT 0,
|
||||
|
||||
-- Pago
|
||||
is_paid BOOLEAN NOT NULL DEFAULT true,
|
||||
pay_percentage DECIMAL(5,2) DEFAULT 100,
|
||||
|
||||
-- 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,
|
||||
|
||||
UNIQUE(tenant_id, company_id, code)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_leave_types_tenant ON hr.leave_types(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leave_types_company ON hr.leave_types(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leave_types_category ON hr.leave_types(leave_category);
|
||||
CREATE INDEX IF NOT EXISTS idx_leave_types_active ON hr.leave_types(is_active) WHERE is_active = true;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: hr.leave_allocations
|
||||
-- Asignacion de dias por empleado
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS hr.leave_allocations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Relaciones
|
||||
employee_id UUID NOT NULL REFERENCES hr.employees(id) ON DELETE CASCADE,
|
||||
leave_type_id UUID NOT NULL REFERENCES hr.leave_types(id) ON DELETE CASCADE,
|
||||
|
||||
-- Asignacion
|
||||
days_allocated DECIMAL(5,2) NOT NULL,
|
||||
days_used DECIMAL(5,2) NOT NULL DEFAULT 0,
|
||||
days_remaining DECIMAL(5,2) GENERATED ALWAYS AS (days_allocated - days_used) STORED,
|
||||
|
||||
-- Periodo
|
||||
date_from DATE NOT NULL,
|
||||
date_to DATE NOT NULL,
|
||||
|
||||
-- Metadata
|
||||
notes TEXT,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CHECK (date_to > date_from),
|
||||
CHECK (days_allocated >= 0),
|
||||
CHECK (days_used >= 0)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_leave_allocations_tenant ON hr.leave_allocations(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leave_allocations_employee ON hr.leave_allocations(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leave_allocations_type ON hr.leave_allocations(leave_type_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leave_allocations_period ON hr.leave_allocations(date_from, date_to);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: hr.leaves
|
||||
-- Solicitudes de ausencia
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS hr.leaves (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
||||
|
||||
-- Relaciones
|
||||
employee_id UUID NOT NULL REFERENCES hr.employees(id) ON DELETE CASCADE,
|
||||
leave_type_id UUID NOT NULL REFERENCES hr.leave_types(id) ON DELETE RESTRICT,
|
||||
allocation_id UUID REFERENCES hr.leave_allocations(id) ON DELETE SET NULL,
|
||||
|
||||
-- Periodo solicitado
|
||||
date_from DATE NOT NULL,
|
||||
date_to DATE NOT NULL,
|
||||
days_requested DECIMAL(5,2) NOT NULL,
|
||||
|
||||
-- Si es parcial
|
||||
is_half_day BOOLEAN NOT NULL DEFAULT false,
|
||||
half_day_type VARCHAR(20) CHECK (half_day_type IN ('morning', 'afternoon')),
|
||||
|
||||
-- Estado
|
||||
status hr.leave_status NOT NULL DEFAULT 'draft',
|
||||
|
||||
-- Aprobacion
|
||||
approver_id UUID REFERENCES auth.users(id),
|
||||
approved_at TIMESTAMPTZ,
|
||||
rejection_reason TEXT,
|
||||
|
||||
-- Metadata
|
||||
request_reason TEXT,
|
||||
document_url TEXT,
|
||||
notes TEXT,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
submitted_at TIMESTAMPTZ,
|
||||
cancelled_at TIMESTAMPTZ,
|
||||
cancelled_by UUID REFERENCES auth.users(id),
|
||||
|
||||
CHECK (date_to >= date_from),
|
||||
CHECK (days_requested > 0)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_leaves_tenant ON hr.leaves(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaves_company ON hr.leaves(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaves_employee ON hr.leaves(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaves_type ON hr.leaves(leave_type_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaves_status ON hr.leaves(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaves_dates ON hr.leaves(date_from, date_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaves_pending ON hr.leaves(status) WHERE status IN ('submitted');
|
||||
CREATE INDEX IF NOT EXISTS idx_leaves_approver ON hr.leaves(approver_id) WHERE status = 'submitted';
|
||||
|
||||
-- =====================
|
||||
-- RLS POLICIES
|
||||
-- =====================
|
||||
|
||||
-- Departments
|
||||
ALTER TABLE hr.departments ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_departments ON hr.departments;
|
||||
CREATE POLICY tenant_isolation_departments ON hr.departments
|
||||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- Job Positions
|
||||
ALTER TABLE hr.job_positions ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_job_positions ON hr.job_positions;
|
||||
CREATE POLICY tenant_isolation_job_positions ON hr.job_positions
|
||||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- Employees
|
||||
ALTER TABLE hr.employees ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_employees ON hr.employees;
|
||||
CREATE POLICY tenant_isolation_employees ON hr.employees
|
||||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- Contracts
|
||||
ALTER TABLE hr.contracts ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_contracts ON hr.contracts;
|
||||
CREATE POLICY tenant_isolation_contracts ON hr.contracts
|
||||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- Leave Types
|
||||
ALTER TABLE hr.leave_types ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_leave_types ON hr.leave_types;
|
||||
CREATE POLICY tenant_isolation_leave_types ON hr.leave_types
|
||||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- Leave Allocations
|
||||
ALTER TABLE hr.leave_allocations ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_leave_allocations ON hr.leave_allocations;
|
||||
CREATE POLICY tenant_isolation_leave_allocations ON hr.leave_allocations
|
||||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- Leaves
|
||||
ALTER TABLE hr.leaves ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_leaves ON hr.leaves;
|
||||
CREATE POLICY tenant_isolation_leaves ON hr.leaves
|
||||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- =====================
|
||||
-- TRIGGERS
|
||||
-- =====================
|
||||
|
||||
CREATE OR REPLACE FUNCTION hr.update_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_departments_updated_at
|
||||
BEFORE UPDATE ON hr.departments
|
||||
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_job_positions_updated_at
|
||||
BEFORE UPDATE ON hr.job_positions
|
||||
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_employees_updated_at
|
||||
BEFORE UPDATE ON hr.employees
|
||||
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_contracts_updated_at
|
||||
BEFORE UPDATE ON hr.contracts
|
||||
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_leave_types_updated_at
|
||||
BEFORE UPDATE ON hr.leave_types
|
||||
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_leave_allocations_updated_at
|
||||
BEFORE UPDATE ON hr.leave_allocations
|
||||
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
||||
|
||||
CREATE TRIGGER trg_leaves_updated_at
|
||||
BEFORE UPDATE ON hr.leaves
|
||||
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
||||
|
||||
-- =====================
|
||||
-- FUNCIONES DE UTILIDAD
|
||||
-- =====================
|
||||
|
||||
-- Obtener subordinados directos de un manager
|
||||
CREATE OR REPLACE FUNCTION hr.get_direct_subordinates(p_manager_id UUID)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
employee_number VARCHAR(50),
|
||||
full_name VARCHAR(255),
|
||||
department_name VARCHAR(255),
|
||||
job_title VARCHAR(255)
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT e.id, e.employee_number, e.full_name, d.name, j.name
|
||||
FROM hr.employees e
|
||||
LEFT JOIN hr.departments d ON d.id = e.department_id
|
||||
LEFT JOIN hr.job_positions j ON j.id = e.job_position_id
|
||||
WHERE e.manager_id = p_manager_id
|
||||
AND e.status = 'active'
|
||||
ORDER BY e.full_name;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Obtener saldo de dias de un tipo de ausencia
|
||||
CREATE OR REPLACE FUNCTION hr.get_leave_balance(
|
||||
p_employee_id UUID,
|
||||
p_leave_type_id UUID,
|
||||
p_date DATE DEFAULT CURRENT_DATE
|
||||
)
|
||||
RETURNS TABLE (
|
||||
days_allocated DECIMAL(5,2),
|
||||
days_used DECIMAL(5,2),
|
||||
days_remaining DECIMAL(5,2)
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
COALESCE(SUM(la.days_allocated), 0),
|
||||
COALESCE(SUM(la.days_used), 0),
|
||||
COALESCE(SUM(la.days_remaining), 0)
|
||||
FROM hr.leave_allocations la
|
||||
WHERE la.employee_id = p_employee_id
|
||||
AND la.leave_type_id = p_leave_type_id
|
||||
AND p_date BETWEEN la.date_from AND la.date_to;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Obtener contrato activo de un empleado
|
||||
CREATE OR REPLACE FUNCTION hr.get_active_contract(p_employee_id UUID)
|
||||
RETURNS hr.contracts AS $$
|
||||
DECLARE
|
||||
v_contract hr.contracts;
|
||||
BEGIN
|
||||
SELECT * INTO v_contract
|
||||
FROM hr.contracts
|
||||
WHERE employee_id = p_employee_id
|
||||
AND status = 'active'
|
||||
LIMIT 1;
|
||||
|
||||
RETURN v_contract;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Verificar si hay solapamiento de ausencias
|
||||
CREATE OR REPLACE FUNCTION hr.check_leave_overlap(
|
||||
p_employee_id UUID,
|
||||
p_date_from DATE,
|
||||
p_date_to DATE,
|
||||
p_exclude_leave_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_overlap_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_overlap_count
|
||||
FROM hr.leaves l
|
||||
WHERE l.employee_id = p_employee_id
|
||||
AND l.status IN ('submitted', 'approved')
|
||||
AND l.date_from <= p_date_to
|
||||
AND l.date_to >= p_date_from
|
||||
AND (p_exclude_leave_id IS NULL OR l.id != p_exclude_leave_id);
|
||||
|
||||
RETURN v_overlap_count > 0;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- =====================
|
||||
-- SEED DATA: Tipos de Ausencia Base
|
||||
-- =====================
|
||||
|
||||
-- Nota: Este seed se ejecuta por tenant cuando se crea la empresa
|
||||
-- Aqui solo documentamos los tipos base
|
||||
|
||||
/*
|
||||
INSERT INTO hr.leave_types (tenant_id, company_id, name, code, leave_category, allocation_type, requires_approval, is_paid, max_days_per_year) VALUES
|
||||
-- Vacaciones (Mexico: 12 dias primer ano, incrementa)
|
||||
(tenant_id, company_id, 'Vacaciones', 'VAC', 'vacation', 'fixed', true, true, 30),
|
||||
-- Enfermedad
|
||||
(tenant_id, company_id, 'Incapacidad por Enfermedad', 'SICK', 'sick', 'unlimited', false, true, NULL),
|
||||
-- Maternidad (Mexico: 84 dias)
|
||||
(tenant_id, company_id, 'Licencia de Maternidad', 'MAT', 'maternity', 'fixed', true, true, 84),
|
||||
-- Paternidad (Mexico: 5 dias)
|
||||
(tenant_id, company_id, 'Licencia de Paternidad', 'PAT', 'paternity', 'fixed', true, true, 5),
|
||||
-- Luto
|
||||
(tenant_id, company_id, 'Permiso por Luto', 'BER', 'bereavement', 'fixed', true, true, 5),
|
||||
-- Personal sin goce
|
||||
(tenant_id, company_id, 'Permiso Personal (Sin Goce)', 'UNPAID', 'unpaid', 'unlimited', true, false, NULL),
|
||||
-- Personal con goce
|
||||
(tenant_id, company_id, 'Permiso Personal (Con Goce)', 'PERS', 'personal', 'fixed', true, true, 3);
|
||||
*/
|
||||
|
||||
-- =====================
|
||||
-- COMENTARIOS
|
||||
-- =====================
|
||||
COMMENT ON SCHEMA hr IS 'Schema para gestion de Recursos Humanos';
|
||||
COMMENT ON TABLE hr.departments IS 'Departamentos organizacionales con jerarquia';
|
||||
COMMENT ON TABLE hr.job_positions IS 'Puestos de trabajo';
|
||||
COMMENT ON TABLE hr.employees IS 'Empleados de la empresa';
|
||||
COMMENT ON TABLE hr.contracts IS 'Contratos laborales';
|
||||
COMMENT ON TABLE hr.leave_types IS 'Tipos de ausencia configurables';
|
||||
COMMENT ON TABLE hr.leave_allocations IS 'Asignacion de dias por empleado y tipo';
|
||||
COMMENT ON TABLE hr.leaves IS 'Solicitudes de ausencia';
|
||||
COMMENT ON FUNCTION hr.get_direct_subordinates IS 'Obtiene subordinados directos de un manager';
|
||||
COMMENT ON FUNCTION hr.get_leave_balance IS 'Obtiene saldo de dias de ausencia';
|
||||
COMMENT ON FUNCTION hr.get_active_contract IS 'Obtiene contrato activo de un empleado';
|
||||
COMMENT ON FUNCTION hr.check_leave_overlap IS 'Verifica solapamiento de ausencias';
|
||||
Loading…
Reference in New Issue
Block a user