Files

514 lines
19 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <ID> [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 <ID> [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 <ID> - Eliminar una entrada existente
def comando_evento_eliminar(args)
if args.empty? || args[0].start_with?('-')
puts "#{Color::YELLOW}Uso: adn db evento:eliminar <ID>#{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