-- ===================================================== -- SCHEMA: projects -- PROPÓSITO: Gestión de proyectos, tareas, milestones -- MÓDULOS: MGN-011 (Proyectos Genéricos) -- FECHA: 2025-11-24 -- ===================================================== -- Crear schema CREATE SCHEMA IF NOT EXISTS projects; -- ===================================================== -- TYPES (ENUMs) -- ===================================================== CREATE TYPE projects.project_status AS ENUM ( 'draft', 'active', 'completed', 'cancelled', 'on_hold' ); CREATE TYPE projects.privacy_type AS ENUM ( 'public', 'private', 'followers' ); CREATE TYPE projects.task_status AS ENUM ( 'todo', 'in_progress', 'review', 'done', 'cancelled' ); CREATE TYPE projects.task_priority AS ENUM ( 'low', 'normal', 'high', 'urgent' ); CREATE TYPE projects.dependency_type AS ENUM ( 'finish_to_start', 'start_to_start', 'finish_to_finish', 'start_to_finish' ); CREATE TYPE projects.milestone_status AS ENUM ( 'pending', 'completed' ); -- ===================================================== -- TABLES -- ===================================================== -- Tabla: projects (Proyectos) CREATE TABLE projects.projects ( 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 auth.companies(id) ON DELETE CASCADE, -- Identificación name VARCHAR(255) NOT NULL, code VARCHAR(50), description TEXT, -- Responsables manager_id UUID REFERENCES auth.users(id), partner_id UUID REFERENCES core.partners(id), -- Cliente -- Analítica (1-1) analytic_account_id UUID REFERENCES analytics.analytic_accounts(id), -- Fechas date_start DATE, date_end DATE, -- Estado status projects.project_status NOT NULL DEFAULT 'draft', privacy projects.privacy_type NOT NULL DEFAULT 'public', -- Configuración allow_timesheets BOOLEAN DEFAULT TRUE, color VARCHAR(20), -- Color para UI -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_projects_code_company UNIQUE (company_id, code), CONSTRAINT chk_projects_dates CHECK (date_end IS NULL OR date_end >= date_start) ); -- Tabla: project_stages (Etapas de tareas) CREATE TABLE projects.project_stages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, project_id UUID REFERENCES projects.projects(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, sequence INTEGER NOT NULL DEFAULT 1, is_closed BOOLEAN DEFAULT FALSE, -- Etapa final fold BOOLEAN DEFAULT FALSE, -- Plegada en kanban -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT chk_project_stages_sequence CHECK (sequence > 0) ); -- Tabla: tasks (Tareas) CREATE TABLE projects.tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE, stage_id UUID REFERENCES projects.project_stages(id), -- Identificación name VARCHAR(255) NOT NULL, description TEXT, -- Asignación assigned_to UUID REFERENCES auth.users(id), partner_id UUID REFERENCES core.partners(id), -- Jerarquía parent_id UUID REFERENCES projects.tasks(id), -- Fechas date_start DATE, date_deadline DATE, -- Esfuerzo planned_hours DECIMAL(8, 2) DEFAULT 0, actual_hours DECIMAL(8, 2) DEFAULT 0, progress INTEGER DEFAULT 0, -- 0-100 -- Prioridad y estado priority projects.task_priority NOT NULL DEFAULT 'normal', status projects.task_status NOT NULL DEFAULT 'todo', -- Milestone milestone_id UUID REFERENCES projects.milestones(id), -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT chk_tasks_no_self_parent CHECK (id != parent_id), CONSTRAINT chk_tasks_dates CHECK (date_deadline IS NULL OR date_deadline >= date_start), CONSTRAINT chk_tasks_planned_hours CHECK (planned_hours >= 0), CONSTRAINT chk_tasks_actual_hours CHECK (actual_hours >= 0), CONSTRAINT chk_tasks_progress CHECK (progress >= 0 AND progress <= 100) ); -- Tabla: milestones (Hitos) CREATE TABLE projects.milestones ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, description TEXT, target_date DATE NOT NULL, status projects.milestone_status NOT NULL DEFAULT 'pending', -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), completed_at TIMESTAMP, completed_by UUID REFERENCES auth.users(id) ); -- Tabla: task_dependencies (Dependencias entre tareas) CREATE TABLE projects.task_dependencies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), task_id UUID NOT NULL REFERENCES projects.tasks(id) ON DELETE CASCADE, depends_on_id UUID NOT NULL REFERENCES projects.tasks(id) ON DELETE CASCADE, dependency_type projects.dependency_type NOT NULL DEFAULT 'finish_to_start', -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_task_dependencies UNIQUE (task_id, depends_on_id), CONSTRAINT chk_task_dependencies_no_self CHECK (task_id != depends_on_id) ); -- Tabla: task_tags (Etiquetas de tareas) CREATE TABLE projects.task_tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, color VARCHAR(20), -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_task_tags_name_tenant UNIQUE (tenant_id, name) ); -- Tabla: task_tag_assignments (Many-to-many) CREATE TABLE projects.task_tag_assignments ( task_id UUID NOT NULL REFERENCES projects.tasks(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES projects.task_tags(id) ON DELETE CASCADE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (task_id, tag_id) ); -- Tabla: timesheets (Registro de horas) CREATE TABLE projects.timesheets ( 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 auth.companies(id) ON DELETE CASCADE, task_id UUID REFERENCES projects.tasks(id) ON DELETE SET NULL, project_id UUID NOT NULL REFERENCES projects.projects(id), employee_id UUID, -- FK a hr.employees (se crea después) user_id UUID REFERENCES auth.users(id), -- Fecha y horas date DATE NOT NULL, hours DECIMAL(8, 2) NOT NULL, -- Descripción description TEXT, -- Analítica analytic_account_id UUID REFERENCES analytics.analytic_accounts(id), -- Distribución analítica analytic_line_id UUID REFERENCES analytics.analytic_lines(id), -- Línea analítica generada -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT chk_timesheets_hours CHECK (hours > 0) ); -- Tabla: task_checklists (Checklists dentro de tareas) CREATE TABLE projects.task_checklists ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), task_id UUID NOT NULL REFERENCES projects.tasks(id) ON DELETE CASCADE, item_name VARCHAR(255) NOT NULL, is_completed BOOLEAN DEFAULT FALSE, sequence INTEGER DEFAULT 1, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMP, completed_by UUID REFERENCES auth.users(id) ); -- Tabla: project_templates (Plantillas de proyectos) CREATE TABLE projects.project_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, description TEXT, -- Template data (JSON con estructura de proyecto, tareas, etc.) template_data JSONB DEFAULT '{}', -- Control active BOOLEAN DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_project_templates_name_tenant UNIQUE (tenant_id, name) ); -- ===================================================== -- INDICES -- ===================================================== -- Projects CREATE INDEX idx_projects_tenant_id ON projects.projects(tenant_id); CREATE INDEX idx_projects_company_id ON projects.projects(company_id); CREATE INDEX idx_projects_manager_id ON projects.projects(manager_id); CREATE INDEX idx_projects_partner_id ON projects.projects(partner_id); CREATE INDEX idx_projects_analytic_account_id ON projects.projects(analytic_account_id); CREATE INDEX idx_projects_status ON projects.projects(status); -- Project Stages CREATE INDEX idx_project_stages_tenant_id ON projects.project_stages(tenant_id); CREATE INDEX idx_project_stages_project_id ON projects.project_stages(project_id); CREATE INDEX idx_project_stages_sequence ON projects.project_stages(sequence); -- Tasks CREATE INDEX idx_tasks_tenant_id ON projects.tasks(tenant_id); CREATE INDEX idx_tasks_project_id ON projects.tasks(project_id); CREATE INDEX idx_tasks_stage_id ON projects.tasks(stage_id); CREATE INDEX idx_tasks_assigned_to ON projects.tasks(assigned_to); CREATE INDEX idx_tasks_parent_id ON projects.tasks(parent_id); CREATE INDEX idx_tasks_milestone_id ON projects.tasks(milestone_id); CREATE INDEX idx_tasks_status ON projects.tasks(status); CREATE INDEX idx_tasks_priority ON projects.tasks(priority); CREATE INDEX idx_tasks_date_deadline ON projects.tasks(date_deadline); -- Milestones CREATE INDEX idx_milestones_tenant_id ON projects.milestones(tenant_id); CREATE INDEX idx_milestones_project_id ON projects.milestones(project_id); CREATE INDEX idx_milestones_status ON projects.milestones(status); CREATE INDEX idx_milestones_target_date ON projects.milestones(target_date); -- Task Dependencies CREATE INDEX idx_task_dependencies_task_id ON projects.task_dependencies(task_id); CREATE INDEX idx_task_dependencies_depends_on_id ON projects.task_dependencies(depends_on_id); -- Timesheets CREATE INDEX idx_timesheets_tenant_id ON projects.timesheets(tenant_id); CREATE INDEX idx_timesheets_company_id ON projects.timesheets(company_id); CREATE INDEX idx_timesheets_task_id ON projects.timesheets(task_id); CREATE INDEX idx_timesheets_project_id ON projects.timesheets(project_id); CREATE INDEX idx_timesheets_employee_id ON projects.timesheets(employee_id); CREATE INDEX idx_timesheets_date ON projects.timesheets(date); CREATE INDEX idx_timesheets_analytic_account_id ON projects.timesheets(analytic_account_id) WHERE analytic_account_id IS NOT NULL; -- Task Checklists CREATE INDEX idx_task_checklists_task_id ON projects.task_checklists(task_id); -- Project Templates CREATE INDEX idx_project_templates_tenant_id ON projects.project_templates(tenant_id); CREATE INDEX idx_project_templates_active ON projects.project_templates(active) WHERE active = TRUE; -- ===================================================== -- FUNCTIONS -- ===================================================== -- Función: update_task_actual_hours CREATE OR REPLACE FUNCTION projects.update_task_actual_hours() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'DELETE' THEN UPDATE projects.tasks SET actual_hours = ( SELECT COALESCE(SUM(hours), 0) FROM projects.timesheets WHERE task_id = OLD.task_id ) WHERE id = OLD.task_id; ELSE UPDATE projects.tasks SET actual_hours = ( SELECT COALESCE(SUM(hours), 0) FROM projects.timesheets WHERE task_id = NEW.task_id ) WHERE id = NEW.task_id; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION projects.update_task_actual_hours IS 'Actualiza las horas reales de una tarea al cambiar timesheets'; -- Función: check_task_dependencies CREATE OR REPLACE FUNCTION projects.check_task_dependencies(p_task_id UUID) RETURNS BOOLEAN AS $$ DECLARE v_unfinished_count INTEGER; BEGIN SELECT COUNT(*) INTO v_unfinished_count FROM projects.task_dependencies td JOIN projects.tasks t ON td.depends_on_id = t.id WHERE td.task_id = p_task_id AND t.status != 'done'; RETURN v_unfinished_count = 0; END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION projects.check_task_dependencies IS 'Verifica si todas las dependencias de una tarea están completadas'; -- Función: prevent_circular_dependencies CREATE OR REPLACE FUNCTION projects.prevent_circular_dependencies() RETURNS TRIGGER AS $$ BEGIN -- Verificar si crear esta dependencia crea un ciclo IF EXISTS ( WITH RECURSIVE dep_chain AS ( SELECT task_id, depends_on_id FROM projects.task_dependencies WHERE task_id = NEW.depends_on_id UNION ALL SELECT td.task_id, td.depends_on_id FROM projects.task_dependencies td JOIN dep_chain dc ON td.task_id = dc.depends_on_id ) SELECT 1 FROM dep_chain WHERE depends_on_id = NEW.task_id ) THEN RAISE EXCEPTION 'Cannot create circular dependency'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION projects.prevent_circular_dependencies IS 'Previene la creación de dependencias circulares entre tareas'; -- ===================================================== -- TRIGGERS -- ===================================================== CREATE TRIGGER trg_projects_updated_at BEFORE UPDATE ON projects.projects FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_tasks_updated_at BEFORE UPDATE ON projects.tasks FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_milestones_updated_at BEFORE UPDATE ON projects.milestones FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_timesheets_updated_at BEFORE UPDATE ON projects.timesheets FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_project_templates_updated_at BEFORE UPDATE ON projects.project_templates FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger: Actualizar horas reales de tarea al cambiar timesheet CREATE TRIGGER trg_timesheets_update_task_hours AFTER INSERT OR UPDATE OR DELETE ON projects.timesheets FOR EACH ROW EXECUTE FUNCTION projects.update_task_actual_hours(); -- Trigger: Prevenir dependencias circulares CREATE TRIGGER trg_task_dependencies_prevent_circular BEFORE INSERT ON projects.task_dependencies FOR EACH ROW EXECUTE FUNCTION projects.prevent_circular_dependencies(); -- ===================================================== -- TRACKING AUTOMÁTICO (mail.thread pattern) -- ===================================================== -- Trigger: Tracking automático para proyectos CREATE TRIGGER track_project_changes AFTER INSERT OR UPDATE OR DELETE ON projects.projects FOR EACH ROW EXECUTE FUNCTION system.track_field_changes(); COMMENT ON TRIGGER track_project_changes ON projects.projects IS 'Registra automáticamente cambios en proyectos (estado, nombre, responsable, fechas)'; -- ===================================================== -- ROW LEVEL SECURITY (RLS) -- ===================================================== ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY; ALTER TABLE projects.project_stages ENABLE ROW LEVEL SECURITY; ALTER TABLE projects.tasks ENABLE ROW LEVEL SECURITY; ALTER TABLE projects.milestones ENABLE ROW LEVEL SECURITY; ALTER TABLE projects.task_tags ENABLE ROW LEVEL SECURITY; ALTER TABLE projects.timesheets ENABLE ROW LEVEL SECURITY; ALTER TABLE projects.project_templates ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_projects ON projects.projects USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_project_stages ON projects.project_stages USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_tasks ON projects.tasks USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_milestones ON projects.milestones USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_task_tags ON projects.task_tags USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_timesheets ON projects.timesheets USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_project_templates ON projects.project_templates USING (tenant_id = get_current_tenant_id()); -- ===================================================== -- COMENTARIOS -- ===================================================== COMMENT ON SCHEMA projects IS 'Schema de gestión de proyectos, tareas y timesheets'; COMMENT ON TABLE projects.projects IS 'Proyectos genéricos con tracking de tareas'; COMMENT ON TABLE projects.project_stages IS 'Etapas/columnas para tablero Kanban de tareas'; COMMENT ON TABLE projects.tasks IS 'Tareas dentro de proyectos con jerarquía y dependencias'; COMMENT ON TABLE projects.milestones IS 'Hitos importantes en proyectos'; COMMENT ON TABLE projects.task_dependencies IS 'Dependencias entre tareas (precedencia)'; COMMENT ON TABLE projects.task_tags IS 'Etiquetas para categorizar tareas'; COMMENT ON TABLE projects.timesheets IS 'Registro de horas trabajadas en tareas'; COMMENT ON TABLE projects.task_checklists IS 'Checklists dentro de tareas'; COMMENT ON TABLE projects.project_templates IS 'Plantillas de proyectos para reutilización'; -- ===================================================== -- FIN DEL SCHEMA PROJECTS -- =====================================================