-- Inicialización de base de datos PostgreSQL para DTIC Bitácoras -- Ejecutado automáticamente al crear el contenedor -- Crear esquema principal CREATE SCHEMA IF NOT EXISTS dtic; -- Usar el esquema dtic por defecto SET search_path TO dtic; -- Tabla de técnicos CREATE TABLE IF NOT EXISTS tecnicos ( id SERIAL PRIMARY KEY, dtic_id VARCHAR(20) UNIQUE NOT NULL, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, phone VARCHAR(20), department VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'technician', 'viewer')), password_hash VARCHAR(255), is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Índices para búsquedas comunes CREATE INDEX IF NOT EXISTS idx_tecnicos_email ON tecnicos (email); CREATE INDEX IF NOT EXISTS idx_tecnicos_department ON tecnicos (department); CREATE INDEX IF NOT EXISTS idx_tecnicos_role ON tecnicos (role); CREATE INDEX IF NOT EXISTS idx_tecnicos_active ON tecnicos (is_active); CREATE INDEX IF NOT EXISTS idx_tecnicos_dtic_id ON tecnicos (dtic_id); -- Tabla de tareas CREATE TABLE IF NOT EXISTS tareas ( id SERIAL PRIMARY KEY, dtic_id VARCHAR(20) UNIQUE NOT NULL, title VARCHAR(200) NOT NULL, description TEXT, technician_id INTEGER REFERENCES tecnicos(id) ON DELETE SET NULL, status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed', 'cancelled')), priority VARCHAR(10) DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high', 'urgent')), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, due_date TIMESTAMP WITH TIME ZONE, completed_at TIMESTAMP WITH TIME ZONE ); -- Índices para tareas CREATE INDEX IF NOT EXISTS idx_tareas_technician ON tareas (technician_id); CREATE INDEX IF NOT EXISTS idx_tareas_status ON tareas (status); CREATE INDEX IF NOT EXISTS idx_tareas_priority ON tareas (priority); CREATE INDEX IF NOT EXISTS idx_tareas_due_date ON tareas (due_date); -- Tabla de auditoría CREATE TABLE IF NOT EXISTS audit_log ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES tecnicos(id) ON DELETE SET NULL, action VARCHAR(50) NOT NULL, entity_type VARCHAR(50) NOT NULL, entity_id INTEGER, old_values JSONB, new_values JSONB, ip_address INET, user_agent TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Índices para auditoría CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log (user_id); CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log (entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log (created_at); -- Función para generar DTIC ID CREATE OR REPLACE FUNCTION dtic.generate_dtic_id(prefix TEXT) RETURNS TEXT AS $$ DECLARE counter INTEGER; new_id TEXT; BEGIN -- Obtener el último contador para este prefijo desde todas las tablas SELECT COALESCE(MAX(CAST(SUBSTRING(dtic_id FROM LENGTH(prefix) + 2) AS INTEGER)), 0) + 1 INTO counter FROM ( SELECT dtic_id FROM dtic.tecnicos WHERE dtic_id LIKE prefix || '-%' UNION ALL SELECT dtic_id FROM dtic.tareas WHERE dtic_id LIKE prefix || '-%' UNION ALL SELECT dtic_id FROM dtic.recursos WHERE dtic_id LIKE prefix || '-%' UNION ALL SELECT dtic_id FROM dtic.usuarios_relacionados WHERE dtic_id LIKE prefix || '-%' UNION ALL SELECT dtic_id FROM dtic.tarea_recursos WHERE dtic_id LIKE prefix || '-%' ) AS all_ids; -- Generar nuevo ID new_id := prefix || '-' || LPAD(counter::TEXT, 4, '0'); RETURN new_id; END; $$ LANGUAGE plpgsql; -- Función para actualizar updated_at automáticamente CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Triggers para actualizar updated_at CREATE TRIGGER update_tecnicos_updated_at BEFORE UPDATE ON tecnicos FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_tareas_updated_at BEFORE UPDATE ON tareas FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Insertar datos de ejemplo INSERT INTO tecnicos (dtic_id, first_name, last_name, email, password_hash, department, role, is_active) VALUES ('TEC-0001', 'Juan', 'PÉREZ', 'juan.perez@dtic.gob.ar', '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPjYQmHqU9yO', 'dtic', 'admin', true), ('TEC-0002', 'María', 'GONZÁLEZ', 'maria.gonzalez@dtic.gob.ar', '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPjYQmHqU9yO', 'sistemas', 'technician', true), ('TEC-0003', 'Carlos', 'RODRÍGUEZ', 'carlos.rodriguez@dtic.gob.ar', '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPjYQmHqU9yO', 'redes', 'technician', true), ('TEC-0004', 'Ana', 'MARTÍNEZ', 'ana.martinez@dtic.gob.ar', '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPjYQmHqU9yO', 'seguridad', 'technician', true), ('TEC-0005', 'Luis', 'LÓPEZ', 'luis.lopez@dtic.gob.ar', '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPjYQmHqU9yO', 'dtic', 'viewer', true), ('TEC-0006', 'Laura', 'SÁNCHEZ', 'laura.sanchez@dtic.gob.ar', '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPjYQmHqU9yO', 'sistemas', 'technician', false), ('TEC-0007', 'Rodrigo', 'MONLA', 'rmonla@frlr.utn.edu.ar', '$2a$12$T4OKj7qWBnjtYhXGmDibP.WszYyFIF5iKEZW3TDpVE2GI9Pfmu5zS', 'dtic', 'admin', true) ON CONFLICT (dtic_id) DO NOTHING; -- Tabla de recursos CREATE TABLE IF NOT EXISTS recursos ( id SERIAL PRIMARY KEY, dtic_id VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, description TEXT, category VARCHAR(50) NOT NULL CHECK (category IN ('hardware', 'software', 'network', 'security', 'tools', 'facilities')), status VARCHAR(20) DEFAULT 'available' CHECK (status IN ('available', 'assigned', 'maintenance', 'retired')), location VARCHAR(200), technical_specs JSONB, serial_number VARCHAR(100), model VARCHAR(100), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Índices para recursos CREATE INDEX IF NOT EXISTS idx_recursos_dtic_id ON recursos (dtic_id); CREATE INDEX IF NOT EXISTS idx_recursos_category ON recursos (category); CREATE INDEX IF NOT EXISTS idx_recursos_status ON recursos (status); CREATE INDEX IF NOT EXISTS idx_recursos_location ON recursos (location); -- Tabla de usuarios relacionados (diferentes de técnicos del sistema) CREATE TABLE IF NOT EXISTS usuarios_relacionados ( id SERIAL PRIMARY KEY, dtic_id VARCHAR(20) UNIQUE NOT NULL, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, email VARCHAR(100) UNIQUE, phone VARCHAR(20), department VARCHAR(100), position VARCHAR(100), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Índices para usuarios relacionados CREATE INDEX IF NOT EXISTS idx_usuarios_relacionados_dtic_id ON usuarios_relacionados (dtic_id); CREATE INDEX IF NOT EXISTS idx_usuarios_relacionados_email ON usuarios_relacionados (email); CREATE INDEX IF NOT EXISTS idx_usuarios_relacionados_department ON usuarios_relacionados (department); -- Tabla de asignaciones recurso-usuario (relación muchos a muchos) CREATE TABLE IF NOT EXISTS recurso_asignaciones ( id SERIAL PRIMARY KEY, recurso_id INTEGER NOT NULL REFERENCES recursos(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES usuarios_relacionados(id) ON DELETE CASCADE, assigned_by INTEGER REFERENCES tecnicos(id), -- técnico que realizó la asignación assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, unassigned_by INTEGER REFERENCES tecnicos(id), -- técnico que realizó la desasignación unassigned_at TIMESTAMP WITH TIME ZONE, activo BOOLEAN DEFAULT true, -- Constraint único para asignaciones activas UNIQUE (recurso_id, user_id) ); -- Índices para asignaciones de recursos CREATE INDEX IF NOT EXISTS idx_recurso_asignaciones_recurso ON recurso_asignaciones (recurso_id); CREATE INDEX IF NOT EXISTS idx_recurso_asignaciones_user ON recurso_asignaciones (user_id); CREATE INDEX IF NOT EXISTS idx_recurso_asignaciones_activo ON recurso_asignaciones (activo); CREATE INDEX IF NOT EXISTS idx_recurso_asignaciones_assigned_at ON recurso_asignaciones (assigned_at); -- Tabla de asignaciones tarea-recurso (relación muchos a muchos) CREATE TABLE IF NOT EXISTS tarea_recursos ( id SERIAL PRIMARY KEY, tarea_id INTEGER NOT NULL REFERENCES tareas(id) ON DELETE CASCADE, recurso_id INTEGER NOT NULL REFERENCES recursos(id) ON DELETE CASCADE, assigned_by INTEGER REFERENCES tecnicos(id), -- técnico que realizó la asignación assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, unassigned_by INTEGER REFERENCES tecnicos(id), -- técnico que realizó la desasignación unassigned_at TIMESTAMP WITH TIME ZONE, activo BOOLEAN DEFAULT true, -- Campos adicionales específicos de la asignación estimated_hours DECIMAL(5,2), -- horas estimadas para usar el recurso en esta tarea actual_hours DECIMAL(5,2), -- horas reales utilizadas notes TEXT, -- notas específicas sobre el uso del recurso en esta tarea -- Constraint único para asignaciones activas de la misma tarea-recurso UNIQUE (tarea_id, recurso_id, activo) ); -- Tabla de historial de asignaciones tarea-recurso CREATE TABLE IF NOT EXISTS tarea_recurso_historial ( id SERIAL PRIMARY KEY, tarea_recurso_id INTEGER NOT NULL REFERENCES tarea_recursos(id) ON DELETE CASCADE, action VARCHAR(50) NOT NULL, -- 'assigned', 'unassigned', 'updated', 'completed' tecnico_id INTEGER REFERENCES tecnicos(id), -- técnico que realizó la acción old_values JSONB, -- valores anteriores new_values JSONB, -- valores nuevos details TEXT, -- descripción detallada de la acción created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Tabla de historial de recursos CREATE TABLE IF NOT EXISTS recurso_historial ( id SERIAL PRIMARY KEY, recurso_id INTEGER NOT NULL REFERENCES recursos(id) ON DELETE CASCADE, action VARCHAR(50) NOT NULL, -- 'assigned', 'unassigned', 'maintenance', 'created', 'updated' usuario_id INTEGER REFERENCES usuarios_relacionados(id), -- usuario asignado (si aplica) tecnico_id INTEGER REFERENCES tecnicos(id), -- técnico que realizó la acción details TEXT, -- descripción detallada de la acción created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Índices para asignaciones tarea-recurso CREATE INDEX IF NOT EXISTS idx_tarea_recursos_tarea ON tarea_recursos (tarea_id); CREATE INDEX IF NOT EXISTS idx_tarea_recursos_recurso ON tarea_recursos (recurso_id); CREATE INDEX IF NOT EXISTS idx_tarea_recursos_activo ON tarea_recursos (activo); CREATE INDEX IF NOT EXISTS idx_tarea_recursos_assigned_at ON tarea_recursos (assigned_at); -- Índices para historial de asignaciones tarea-recurso CREATE INDEX IF NOT EXISTS idx_tarea_recurso_historial_tarea_recurso ON tarea_recurso_historial (tarea_recurso_id); CREATE INDEX IF NOT EXISTS idx_tarea_recurso_historial_action ON tarea_recurso_historial (action); CREATE INDEX IF NOT EXISTS idx_tarea_recurso_historial_tecnico ON tarea_recurso_historial (tecnico_id); CREATE INDEX IF NOT EXISTS idx_tarea_recurso_historial_created ON tarea_recurso_historial (created_at); -- Índices para historial de recursos CREATE INDEX IF NOT EXISTS idx_recurso_historial_recurso ON recurso_historial (recurso_id); CREATE INDEX IF NOT EXISTS idx_recurso_historial_action ON recurso_historial (action); CREATE INDEX IF NOT EXISTS idx_recurso_historial_tecnico ON recurso_historial (tecnico_id); CREATE INDEX IF NOT EXISTS idx_recurso_historial_created ON recurso_historial (created_at); -- Triggers para actualizar updated_at CREATE TRIGGER update_recursos_updated_at BEFORE UPDATE ON recursos FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_usuarios_relacionados_updated_at BEFORE UPDATE ON usuarios_relacionados FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Función para validar asignaciones de recursos a tareas CREATE OR REPLACE FUNCTION validate_tarea_recurso_assignment() RETURNS TRIGGER AS $$ BEGIN -- Si es una nueva asignación activa IF NEW.activo = true THEN -- Verificar que el recurso esté disponible (no asignado a otras tareas activas) IF EXISTS ( SELECT 1 FROM tarea_recursos tr WHERE tr.recurso_id = NEW.recurso_id AND tr.activo = true AND tr.id != COALESCE(NEW.id, 0) ) THEN RAISE EXCEPTION 'El recurso ya está asignado a otra tarea activa'; END IF; -- Verificar que el recurso esté en estado 'available' IF NOT EXISTS ( SELECT 1 FROM recursos r WHERE r.id = NEW.recurso_id AND r.status = 'available' ) THEN RAISE EXCEPTION 'El recurso debe estar en estado disponible para ser asignado'; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger para validar asignaciones CREATE TRIGGER validate_tarea_recurso_assignment_trigger BEFORE INSERT OR UPDATE ON tarea_recursos FOR EACH ROW EXECUTE FUNCTION validate_tarea_recurso_assignment(); -- Función para actualizar estado de recursos basado en asignaciones CREATE OR REPLACE FUNCTION update_recurso_status_from_assignments() RETURNS TRIGGER AS $$ BEGIN -- Si se asigna un recurso a una tarea IF (TG_OP = 'INSERT' AND NEW.activo = true) OR (TG_OP = 'UPDATE' AND NEW.activo = true AND OLD.activo = false) THEN -- Cambiar estado del recurso a 'assigned' UPDATE recursos SET status = 'assigned', updated_at = CURRENT_TIMESTAMP WHERE id = NEW.recurso_id AND status = 'available'; -- Si se desasigna un recurso de una tarea ELSIF (TG_OP = 'UPDATE' AND NEW.activo = false AND OLD.activo = true) OR TG_OP = 'DELETE' THEN -- Verificar si el recurso ya no está asignado a ninguna tarea activa IF NOT EXISTS ( SELECT 1 FROM tarea_recursos tr WHERE tr.recurso_id = COALESCE(NEW.recurso_id, OLD.recurso_id) AND tr.activo = true ) THEN -- Cambiar estado del recurso a 'available' UPDATE recursos SET status = 'available', updated_at = CURRENT_TIMESTAMP WHERE id = COALESCE(NEW.recurso_id, OLD.recurso_id) AND status = 'assigned'; END IF; END IF; RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql; -- Trigger para actualizar estado de recursos CREATE TRIGGER update_recurso_status_from_assignments_trigger AFTER INSERT OR UPDATE OR DELETE ON tarea_recursos FOR EACH ROW EXECUTE FUNCTION update_recurso_status_from_assignments(); -- Insertar datos de ejemplo para recursos INSERT INTO recursos (dtic_id, name, description, category, status, location, technical_specs, serial_number, model) VALUES ('REC-0001', 'Laptop Dell Latitude 5420', 'Laptop para trabajo administrativo', 'hardware', 'available', 'Oficina Principal', '{"cpu": "Intel i5", "ram": "16GB", "storage": "512GB SSD"}', 'DL5420-2025-001', 'Dell Latitude 5420'), ('REC-0002', 'Proyector Epson EB-S41', 'Proyector para presentaciones', 'hardware', 'assigned', 'Sala de Reuniones', '{"resolution": "SVGA", "brightness": "3200 lumens"}', 'EP-S41-2025-002', 'Epson EB-S41'), ('REC-0003', 'Antivirus Bitdefender Total Security', 'Licencia corporativa de antivirus', 'software', 'available', NULL, '{"licenses": 25, "valid_until": "2025-12-15"}', 'BD-TS-2025-003', 'Bitdefender Total Security'), ('REC-0004', 'Router Cisco RV340', 'Router para red corporativa', 'network', 'maintenance', 'Sala de Servidores', '{"ports": "16 Gigabit", "wifi": "Dual Band"}', 'CS-RV340-2025-004', 'Cisco RV340'), ('REC-0005', 'Kit de Herramientas de Red', 'Herramientas para mantenimiento de red', 'tools', 'available', 'Depósito Técnico', '{"items": ["Tester", "Cables", "Conectores"]}', 'KT-RD-2025-005', 'Kit Herramientas Red'), ('REC-0006', 'Licencia Windows Server 2022', 'Licencia para servidor principal', 'software', 'assigned', 'Sala de Servidores', '{"edition": "Standard", "cores": 16}', 'WS2022-STD-006', 'Windows Server 2022') ON CONFLICT (dtic_id) DO NOTHING; -- Insertar datos de ejemplo para usuarios relacionados INSERT INTO usuarios_relacionados (dtic_id, first_name, last_name, email, phone, department, position) VALUES ('USR-0001', 'Juan', 'Pérez', 'juan.perez@empresa.com', '+54 11 1234-5678', 'Administración', 'Gerente'), ('USR-0002', 'María', 'González', 'maria.gonzalez@empresa.com', '+54 11 2345-6789', 'Recursos Humanos', 'Coordinadora'), ('USR-0003', 'Carlos', 'Rodríguez', 'carlos.rodriguez@empresa.com', '+54 11 3456-7890', 'IT', 'Analista'), ('USR-0004', 'Ana', 'Martínez', 'ana.martinez@empresa.com', '+54 11 4567-8901', 'Contabilidad', 'Contadora'), ('USR-0005', 'Luis', 'López', 'luis.lopez@empresa.com', '+54 11 5678-9012', 'Ventas', 'Vendedor') ON CONFLICT (dtic_id) DO NOTHING; -- Insertar asignaciones de ejemplo INSERT INTO recurso_asignaciones (recurso_id, user_id, assigned_by) VALUES (2, 1, 1), -- Proyector asignado a Juan Pérez (6, 3, 2) -- Windows Server asignado a Carlos Rodríguez ON CONFLICT DO NOTHING; -- Crear índices adicionales para búsquedas de texto completo CREATE INDEX IF NOT EXISTS idx_tecnicos_full_name ON tecnicos USING gin (to_tsvector('spanish', first_name || ' ' || last_name)); CREATE INDEX IF NOT EXISTS idx_tecnicos_search ON tecnicos USING gin (to_tsvector('spanish', first_name || ' ' || last_name || ' ' || email || ' ' || dtic_id)); CREATE INDEX IF NOT EXISTS idx_recursos_name ON recursos USING gin (to_tsvector('spanish', name || ' ' || description || ' ' || dtic_id)); CREATE INDEX IF NOT EXISTS idx_usuarios_relacionados_name ON usuarios_relacionados USING gin (to_tsvector('spanish', first_name || ' ' || last_name || ' ' || email || ' ' || dtic_id)); -- Comentarios en las tablas COMMENT ON TABLE tecnicos IS 'Tabla principal de técnicos del sistema DTIC Bitácoras'; COMMENT ON TABLE tareas IS 'Tabla de tareas asignadas a técnicos'; COMMENT ON TABLE tarea_recursos IS 'Tabla de relación many-to-many entre tareas y recursos asignados'; COMMENT ON TABLE tarea_recurso_historial IS 'Historial de asignaciones y desasignaciones de recursos a tareas'; COMMENT ON TABLE audit_log IS 'Registro de auditoría de todas las operaciones del sistema'; -- Permisos básicos (ajustar según necesidades de seguridad) GRANT USAGE ON SCHEMA dtic TO dtic_user; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA dtic TO dtic_user; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA dtic TO dtic_user;