module ADN module SubcomandoDBEvento # Comando: evento:crear [OPCIONES] - Crear nueva entrada cronológica def comando_evento_crear(args) opts = { inicio: nil, descripcion: nil, fin: nil, estado: nil, modo: nil, es_ia: false, nodo: nil, tema: nil, plantilla: nil, fecha: nil, ambito: nil, doc_path: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db evento:crear [OPCIONES]" opt.on("--inicio HH:MM[:SS]", "Hora de inicio (requerido)") do |i| opts[:inicio] = i end opt.on("--descripcion TEXTO", "Descripción de la actividad (requerido)") do |d| opts[:descripcion] = d end opt.on("--fin HH:MM[:SS]", "Hora de fin (opcional)") do |f| opts[:fin] = f end opt.on("--estado ESTADO", "Estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e| opts[:estado] = e end opt.on("--modo MODO", "Modo (P=Presencial, R=Remoto)") do |m| opts[:modo] = m end opt.on("--nodo NOMBRE", "Nombre del nodo (servidor/VM)") do |n| opts[:nodo] = n end opt.on("--fecha YYYY-MM-DD", "Fecha de la bitácora (default: hoy)") do |f| opts[:fecha] = f end opt.on("--ambito NOMBRE_O_ID", "Ámbito del evento") do |a| opts[:ambito] = a end opt.on("--doc RUTA", "Ruta al plan .md (ej: ambito/dtic-DASUTEN/P2601.09_DASUTEN-sin-DC.md)") do |d| opts[:doc_path] = d end opt.on("--ia", "Marcar como generado por IA") do opts[:es_ia] = true end opt.on("--plantilla TIPO", "Usar plantilla (implementacion, investigacion, resolucion, documentacion, test, debug, planificacion)") do |p| opts[:plantilla] = p end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end # Validaciones básicas unless opts[:descripcion] puts "#{Color::RED}✗ Error: --descripcion es requerido#{Color::RESET}" puts parser exit 1 end # Si no se especifica --inicio, usar hora actual opts[:inicio] ||= Time.now.strftime('%H:%M:%S') puts "#{Color::DIM}ℹ Hora de inicio: #{opts[:inicio]}#{Color::RESET}" # Si se specify plantilla, mostrar y prependear al texto if opts[:plantilla] require_relative '../../templates/plantillas_eventos' if ADN::PlantillasEventos.existe?(opts[:plantilla]) plantilla = ADN::PlantillasEventos.obtener(opts[:plantilla]) puts "#{Color::CYAN}📋 Plantilla: #{opts[:plantilla]}#{Color::RESET}" puts plantilla puts "" puts "#{Color::YELLOW}💡 Completa la plantilla y usa --descripcion con el resultado#{Color::RESET}" else puts "#{Color::RED}✗ Tipo de plantilla desconocido: #{opts[:plantilla]}#{Color::RESET}" puts "#{Color::YELLOW}Disponibles: #{ADN::PlantillasEventos.lista_tipos.join(', ')}#{Color::RESET}" exit 1 end end # Obtener modo desde jornada actual si no se especifica modo_resuelto = opts[:modo] if modo_resuelto.nil? puts "#{Color::DIM}ℹ Modo no especificado, buscando jornada actual...#{Color::RESET}" end puts "#{Color::CYAN}📝 Creando nueva entrada...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| # Buscar nodo por nombre si se especificó nodo_id = nil if opts[:nodo] nodo = db.find_nodo_by_nombre(opts[:nodo]) if nodo nodo_id = nodo['id'] puts " #{Color::DIM}Nodo encontrado: #{opts[:nodo]} (ID: #{nodo_id})#{Color::RESET}" else puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo]}' no encontrado#{Color::RESET}" puts " Creando entrada sin nodo asociado" end end # Resolver ámbito si se especificó ambito_id = nil if opts[:ambito] ambito = db.get_ambito(opts[:ambito]) if ambito ambito_id = ambito['id'] puts " #{Color::DIM}Ámbito encontrado: #{opts[:ambito]} (ID: #{ambito_id})#{Color::RESET}" else puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito]}' no encontrado#{Color::RESET}" end end # Determinar fecha: --fecha o hoy fecha_target = opts[:fecha] ? opts[:fecha] : Date.today.to_s if opts[:fecha] begin Date.parse(opts[:fecha]) puts " #{Color::DIM}ℹ Fecha especificada: #{fecha_target}#{Color::RESET}" rescue Date::Error puts "#{Color::RED}✗ Formato de fecha inválido: #{opts[:fecha]}. Usar YYYY-MM-DD#{Color::RESET}" exit 1 end end bitacora = db.find_bitacora_by_fecha(fecha_target) if bitacora.nil? puts " #{Color::DIM}Creando bitácora para #{fecha_target}...#{Color::RESET}" bitacora = db.create_bitacora(fecha: fecha_target) end bitacora_id = bitacora['id'] # Obtener modo desde jornada actual si no se especificó if modo_resuelto.nil? if bitacora && bitacora['modo'] modo_resuelto = bitacora['modo'] puts " #{Color::DIM}ℹ Modo resuelto desde jornada: #{modo_resuelto == 'P' ? 'Presencial' : (modo_resuelto == 'R' ? 'Remoto' : 'Servicio')}#{Color::RESET}" else modo_resuelto = 'P' # default puts " #{Color::DIM}ℹ Sin jornada activa, usando default: Presencial#{Color::RESET}" end end # Calcular estado automáticamente desde hora_fin estado_resuelto = opts[:estado] if estado_resuelto.nil? if opts[:fin] estado_resuelto = '✅' puts " #{Color::DIM}ℹ Estado calculado: ✅ (tiene hora de fin)#{Color::RESET}" else estado_resuelto = '⏳' puts " #{Color::DIM}ℹ Estado calculado: ⏳ (sin hora de fin)#{Color::RESET}" end end # Crear la entrada entrada = db.create_entrada( inicio: opts[:inicio], descripcion: opts[:descripcion], fin: opts[:fin], estado: estado_resuelto, modo: modo_resuelto, es_ia: opts[:es_ia], bitacora_id: bitacora_id, nodo_id: nodo_id, tema_id: nil # Por ahora sin tema ) # Asignar doc_path si se especificó if opts[:doc_path] && entrada db.update_entrada(entrada['id'], { doc_path: opts[:doc_path] }) puts " #{Color::DIM}ℹ Doc vinculado: #{opts[:doc_path]}#{Color::RESET}" end # Asignar ámbito si se especificó if ambito_id && entrada db.update_entrada(entrada['id'], { ambito_id: ambito_id }) puts " #{Color::DIM}ℹ Ámbito asignado (ID: #{ambito_id})#{Color::RESET}" end puts "#{Color::GREEN}✅ Entrada creada exitosamente#{Color::RESET}" puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha_target}" puts " #{Color::BOLD}Inicio:#{Color::RESET} #{entrada['inicio']}" puts " #{Color::BOLD}Fin:#{Color::RESET} #{entrada['fin'] || '(sin fin)'}" puts " 📝 #{Color::BOLD}Descripción:#{Color::RESET} #{entrada['descripcion']}" mostrar_pendientes(db) end end # Comando: evento:actualizar [OPCIONES] - Actualizar una entrada existente def comando_evento_actualizar(args) if args.length < 1 || args[0].start_with?('-') puts "#{Color::YELLOW}Uso: adn db evento:actualizar [OPCIONES]#{Color::RESET}" puts "Opciones soportadas: --fin \"HH:MM[:SS]\", --inicio \"HH:MM[:SS]\", --estado \"✅\", --descripcion \"Texto\", --nodo \"Nombre\"" return end id_evento = args.shift.to_i opts = {} parser = OptionParser.new do |opt| opt.banner = "Opciones:" opt.on("--inicio HH:MM[:SS]", "Nueva hora de inicio") { |i| opts[:inicio] = i } opt.on("--fin HH:MM[:SS]", "Nueva hora de fin") { |f| opts[:fin] = f } opt.on("--descripcion TEXTO", "Nueva descripción") { |d| opts[:descripcion] = d } opt.on("--estado ESTADO", "Nuevo estado (⏳, ✅, ❌, ⚠️, ⏸️)") { |e| opts[:estado] = e } opt.on("--modo MODO", "Modo (P=Presencial, R=Remoto)") { |m| opts[:modo] = m } opt.on("--nodo NOMBRE", "Nuevo nodo") { |n| opts[:nodo_raw] = n } opt.on("--ambito NOMBRE_O_ID", "Nuevo ámbito") { |a| opts[:ambito_raw] = a } opt.on("--doc RUTA", "Ruta al plan .md vinculado") { |d| opts[:doc_path] = d } opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" exit 1 end if opts.empty? puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --fin, --inicio, --descripcion, --estado, --modo o --nodo#{Color::RESET}" return end puts "#{Color::CYAN}🔄 Actualizando evento #{id_evento}...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| # Calcular estado automáticamente si se proporciona --fin (sin verificar entrada actual) if opts.key?(:fin) && !opts.key?(:estado) if opts[:fin] && !opts[:fin].empty? opts[:estado] = '✅' puts " #{Color::DIM}ℹ Estado calculado automáticamente: ✅ (hora de fin agregada)#{Color::RESET}" end end if opts.key?(:nodo_raw) if opts[:nodo_raw].nil? || opts[:nodo_raw].empty? opts[:nodo_id] = nil else nodo = db.find_nodo_by_nombre(opts[:nodo_raw]) if nodo opts[:nodo_id] = nodo['id'] else puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo_raw]}' no encontrado#{Color::RESET}" end end opts.delete(:nodo_raw) end if opts.key?(:ambito_raw) if opts[:ambito_raw].nil? || opts[:ambito_raw].empty? opts[:ambito_id] = nil else ambito = db.get_ambito(opts[:ambito_raw]) if ambito opts[:ambito_id] = ambito['id'] else puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito_raw]}' no encontrado#{Color::RESET}" end end opts.delete(:ambito_raw) end begin res = db.update_entrada(id_evento, opts) if res puts "#{Color::GREEN}✅ Evento #{id_evento} actualizado exitosamente.#{Color::RESET}" puts " #{Color::BOLD}Inicio:#{Color::RESET} #{res['inicio']}" puts " #{Color::BOLD}Fin:#{Color::RESET} #{res['fin'] || '(sin fin)'}" puts " 📝 #{Color::BOLD}Descripción:#{Color::RESET} #{res['descripcion']}" else puts "#{Color::YELLOW}⚠ No se pudo actualizar el evento. ¿Existe el ID #{id_evento}?#{Color::RESET}" end rescue => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" end mostrar_pendientes(db) end end # Comando: evento:listar [OPCIONES] - Listar entradas con filtros def comando_evento_listar(args) opts = { limit: 20, offset: 0, nodo: nil, estado: nil, buscar: nil, desde: nil, hasta: nil, detalle: false } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db evento:listar [OPCIONES]" opt.on("--limit N", Integer, "Número máximo de resultados (default: 20)") do |n| opts[:limit] = n end opt.on("--offset N", Integer, "Desplazamiento para paginación (default: 0)") do |n| opts[:offset] = n end opt.on("--nodo NOMBRE", "Filtrar por nombre de nodo") do |n| opts[:nodo] = n end opt.on("--ambito NOMBRE_O_ID", "Filtrar por ámbito") do |a| opts[:ambito] = a end opt.on("--estado ESTADO", "Filtrar por estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e| opts[:estado] = e end opt.on("--buscar TEXTO", "Buscar texto en descripción") do |b| opts[:buscar] = b end opt.on("--desde FECHA", "Filtrar desde fecha (YYYY-MM-DD)") do |d| opts[:desde] = d end opt.on("--hasta FECHA", "Filtrar hasta fecha (YYYY-MM-DD)") do |h| opts[:hasta] = h end opt.on("--detalle", "Mostrar descripciones completas (formato vertical)") do opts[:detalle] = true end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}📋 Listando entradas...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| # Obtener ID de nodo si se especificó nombre nodo_id = nil if opts[:nodo] nodo = db.find_nodo_by_nombre(opts[:nodo]) if nodo nodo_id = nodo['id'] puts " #{Color::DIM}Filtrando por nodo: #{opts[:nodo]} (ID: #{nodo_id})#{Color::RESET}" else puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo]}' no encontrado#{Color::RESET}" end end # Obtener ID de ámbito si se especificó nombre ambito_id = nil if opts[:ambito] ambito = db.get_ambito(opts[:ambito]) if ambito ambito_id = ambito['id'] puts " #{Color::DIM}Filtrando por ámbito: #{opts[:ambito]} (ID: #{ambito_id})#{Color::RESET}" else puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito]}' no encontrado#{Color::RESET}" end end entradas = db.list_entradas( ambito_id: ambito_id, nodo_id: nodo_id, estado: opts[:estado], buscar: opts[:buscar], desde: opts[:desde], hasta: opts[:hasta], limit: opts[:limit], offset: opts[:offset] ) if entradas.empty? puts "#{Color::YELLOW}⚠ No se encontraron entradas#{Color::RESET}" return end if opts[:detalle] # Formato vertical con descripciones completas entradas.each_with_index do |e, i| puts "#{Color::DIM}#{"─" * 60}#{Color::RESET}" if i > 0 id = e['id'].to_s inicio = e['inicio'] ? e['inicio'][0..7] : '--:--:--' fin = e['fin'] ? e['fin'][0..7] : '--:--:--' nodo = "#{e['ambito_nombre'] || '-'} / #{e['nodo_nombre'] || '-'}" estado = e['estado'] desc = e['descripcion'] puts "#{estado} #{Color::BOLD}##{id}#{Color::RESET} | #{inicio} → #{fin} | #{Color::CYAN}#{nodo}#{Color::RESET}" puts " #{desc}" end else # Formato tabla compacta (original ajustado) puts "" puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(9)} #{"Fin".ljust(9)} #{"Ámbito/Nodo".ljust(25)} #{"Est".ljust(3)} #{"Descripción".ljust(35)}#{Color::RESET}" puts "#{"─" * 5} #{"─" * 9} #{"─" * 9} #{"─" * 25} #{"─" * 3} #{"─" * 35}" entradas.each do |e| id = e['id'].to_s inicio = e['inicio'] ? e['inicio'][0..7] : '--:--' fin = e['fin'] ? e['fin'][0..7] : '--:--' nodo = "#{e['ambito_nombre'] || '-'} / #{e['nodo_nombre'] || '-'}" nodo = nodo[0..23] + '...' if nodo.length > 25 estado = e['estado'] desc = e['descripcion'] || '' desc = desc.split("\n").first || '' # Tomar solo la primera línea desc = desc[0..32] + '...' if desc.length > 35 puts "#{id.ljust(5)} #{inicio.ljust(9)} #{fin.ljust(9)} #{nodo.ljust(25)} #{estado.ljust(3)} #{desc.ljust(35)}" end end puts "" puts "Total: #{entradas.length} entradas" end end # Comando: evento:eliminar - Eliminar una entrada existente def comando_evento_eliminar(args) if args.empty? || args[0].start_with?('-') puts "#{Color::YELLOW}Uso: adn db evento:eliminar #{Color::RESET}" return end id_evento = args.shift.to_i puts "#{Color::CYAN}🗑️ Eliminando evento #{id_evento}...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| begin res = db.delete_entrada(id_evento) if res puts "#{Color::GREEN}✅ Evento #{id_evento} eliminado exitosamente.#{Color::RESET}" else puts "#{Color::YELLOW}⚠ No se pudo eliminar el evento. ¿Existe el ID #{id_evento}?#{Color::RESET}" end rescue => e puts "#{Color::RED}✗ Error al eliminar: #{e.message}#{Color::RESET}" end mostrar_pendientes(db) end end private def mostrar_pendientes(db) puts "" puts "#{Color::CYAN}⏳ Eventos pendientes de cierre:#{Color::RESET}" # Filtrar por bitácora de hoy y estado '⏳' o '⏸️' fecha_hoy = Date.today.to_s bitacora = db.find_bitacora_by_fecha(fecha_hoy) unless bitacora puts " #{Color::DIM}No hay bitácora para hoy.#{Color::RESET}" return end # Buscar todos los pendientes de hoy entradas_pendientes = db.list_entradas( bitacora_id: bitacora['id'], estado: '⏳', limit: 20 ) # Sumamos pausados si los hay entradas_pausadas = db.list_entradas( bitacora_id: bitacora['id'], estado: '⏸️', limit: 20 ) entradas = entradas_pendientes + entradas_pausadas if entradas.empty? puts " #{Color::GREEN}¡Excelente! No hay eventos pendientes de cierre.#{Color::RESET}" return end # Ordenar por hora de inicio (más antiguos primero) entradas = entradas.sort_by { |e| e['inicio'] } entradas.each do |e| id = e['id'].to_s inicio = e['inicio'] ? e['inicio'][0..7] : '--:--:--' # HH:MM:SS nodo = (e['nodo_nombre'] || '(sin nodo)')[0..14] desc = e['descripcion'] desc = desc[0..47] + '...' if desc.length > 50 estado_icon = e['estado'] puts " #{estado_icon} #{Color::YELLOW}ID #{id.ljust(5)}#{Color::RESET} #{inicio.ljust(11)} #{nodo.ljust(15)} #{desc}" end end end end