# frozen_string_literal: true # adn/tools/cli/jornada.rb — Subcomando unificado para gestión de jornada laboral # ================================================================================== # Uso: # ./adn/tools/run jornada iniciar presencial [HH:MM] # ./adn/tools/run jornada iniciar remoto [HH:MM] # ./adn/tools/run jornada cerrar [presencial|remoto] [HH:MM] # ./adn/tools/run jornada estado require 'date' require 'time' require 'optparse' require_relative '../db/core/bitacora_db' require_relative '../core/colores' require_relative '../core/help_formatter' module ADN class SubcomandoJornada NODO_DEFAULT = 'dtic-DIIAA' def initialize(args, logger) @args = args @logger = logger @fecha = Date.today @opciones = { nodo: NODO_DEFAULT, fecha: nil } end def ejecutar if @args.empty? mostrar_ayuda return end # Parsear opciones globales (--nodo, --fecha) antes del subcomando parser_global = OptionParser.new do |opts| opts.on('--nodo NOMBRE', "Nodo a registrar (default: #{NODO_DEFAULT})") { |n| @opciones[:nodo] = n } opts.on('--fecha YYYY-MM-DD', 'Fecha de la bitácora (default: hoy)') { |f| @opciones[:fecha] = f } end parser_global.order!(@args) accion = @args.shift case accion when 'iniciar', 'inicio', 'abrir' ejecutar_inicio when 'cerrar', 'cierre', 'finalizar' ejecutar_cierre when 'estado', 'status' ejecutar_estado else puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}" mostrar_ayuda exit 1 end end private def mostrar_ayuda ayuda = ADN::HelpFormatter.generar_help( titulo: "Gestión de Jornada Laboral", descripcion: "Registro y control de jornada laboral", uso: "./adn/tools/run jornada [modo] [hora] [opciones]", opciones: [ { names: ['iniciar [presencial|remoto] [HH:MM]'], desc: "Registrar inicio de jornada" }, { names: ['cerrar [presencial|remoto] [HH:MM]'], desc: "Registrar cierre de jornada" }, { names: ['estado'], desc: "Ver estado de la jornada actual" }, { names: ['--nodo NOMBRE'], desc: "Nodo a registrar (default: #{NODO_DEFAULT})" }, { names: ['--fecha YYYY-MM-DD'], desc: "Fecha de la bitácora (default: hoy)" } ], ejemplos: [ { cmd: "./adn/tools/run jornada iniciar presencial 08:00", desc: "Iniciar jornada presencial a las 08:00" }, { cmd: "./adn/tools/run jornada cerrar remoto 23:00", desc: "Cerrar jornada remota a las 23:00" }, { cmd: "./adn/tools/run jornada estado", desc: "Ver estado actual de la jornada" } ] ) puts ayuda end def parsear_modo_y_hora modo = 'P' # default presencial hora = Time.now.strftime('%H:%M') @args.each do |arg| case arg.downcase when 'presencial', 'p' modo = 'P' when 'remoto', 'remota', 'r' modo = 'R' else # Si parece una hora (HH:MM), usarla if arg.match?(/^\d{1,2}:\d{2}$/) hora = arg end end end [modo, hora] end def obtener_fecha if @opciones[:fecha] Date.parse(@opciones[:fecha]) else @fecha end end def ejecutar_inicio modo, hora = parsear_modo_y_hora tipo_str = modo == 'P' ? 'Presencial' : 'Remoto' fecha = obtener_fecha @logger.info("Iniciando jornada #{tipo_str} a las #{hora}") BitacorasDB::BitacoraDB.with_connection do |db| # 1. Asegurar que existe la bitácora bitacora = db.find_bitacora_by_fecha(fecha) unless bitacora @logger.info("Creando bitácora en DB para #{fecha}...") bitacora = db.create_bitacora(fecha: fecha) end # 2. Buscar nodo nodo = db.find_nodo_by_nombre(@opciones[:nodo]) unless nodo @logger.error("Nodo '#{@opciones[:nodo]}' no encontrado en la BD.") puts "#{Color::RED}✗ Nodo '#{@opciones[:nodo]}' no existe. Usa 'db nodo:listar' para ver disponibles.#{Color::RESET}" return end # 3. Crear entrada de inicio entrada = db.create_entrada( inicio: hora, descripcion: "Inicio de jornada #{tipo_str.downcase}. Sincronización de contexto y revisión de tareas.", estado: '⏳', modo: modo, bitacora_id: bitacora['id'], nodo_id: nodo['id'] ) puts "#{Color::GREEN}✅ Jornada #{tipo_str.downcase} iniciada#{Color::RESET}" puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}" puts " #{Color::BOLD}Hora:#{Color::RESET} #{hora}" puts " #{Color::BOLD}Nodo:#{Color::RESET} #{@opciones[:nodo]}" puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}" @logger.exito("Jornada #{tipo_str} iniciada en Base de Datos.") end # 4. Verificar y mostrar URL de bitácora web verificar_y_mostrar_bitacora_web end def ejecutar_cierre modo, hora = parsear_modo_y_hora tipo_str = modo == 'P' ? 'Presencial' : 'Remoto' fecha = obtener_fecha @logger.info("Cerrando jornada #{tipo_str} a las #{hora}") BitacorasDB::BitacoraDB.with_connection do |db| # 1. Buscar la bitácora bitacora = db.find_bitacora_by_fecha(fecha) unless bitacora @logger.error("No existe bitácora en DB para #{fecha}. ¿Iniciaste la jornada?") puts "#{Color::RED}✗ No hay bitácora para #{fecha}#{Color::RESET}" return end # 2. Buscar nodo nodo = db.find_nodo_by_nombre(@opciones[:nodo]) unless nodo @logger.error("Nodo '#{@opciones[:nodo]}' no encontrado.") puts "#{Color::RED}✗ Nodo '#{@opciones[:nodo]}' no existe.#{Color::RESET}" return end # 3. Crear entrada de cierre entrada = db.create_entrada( inicio: hora, fin: hora, descripcion: "Cierre de jornada #{tipo_str.downcase}. Sincronización final y validación de backups.", estado: '✅', modo: modo, bitacora_id: bitacora['id'], nodo_id: nodo['id'] ) puts "#{Color::GREEN}✅ Jornada #{tipo_str.downcase} cerrada#{Color::RESET}" puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}" puts " #{Color::BOLD}Hora:#{Color::RESET} #{hora}" puts " #{Color::BOLD}Nodo:#{Color::RESET} #{@opciones[:nodo]}" puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}" @logger.exito("Jornada #{tipo_str} cerrada en Base de Datos.") end end def ejecutar_estado fecha = obtener_fecha puts "#{Color::CYAN}📅 Estado de jornada para #{fecha}#{Color::RESET}" puts "#{Color::DIM}#{'-' * 50}#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| bitacora = db.find_bitacora_by_fecha(fecha) unless bitacora puts "#{Color::YELLOW}⚠ No hay bitácora para #{fecha}#{Color::RESET}" return end entradas = db.list_entradas(bitacora_id: bitacora['id'], limit: 50) if entradas.empty? puts "#{Color::YELLOW}⚠ Sin entradas para hoy#{Color::RESET}" return end abiertas = entradas.select { |e| e['estado'] == '⏳' } cerradas = entradas.select { |e| e['estado'] == '✅' } puts " #{Color::GREEN}Total entradas:#{Color::RESET} #{entradas.length}" puts " #{Color::GREEN}Completadas:#{Color::RESET} #{cerradas.length}" puts " #{Color::YELLOW}Abiertas:#{Color::RESET} #{abiertas.length}" if abiertas.any? puts "\n #{Color::YELLOW}⏳ Entradas abiertas:#{Color::RESET}" abiertas.each do |e| nodo = e['nodo_nombre'] || '(sin nodo)' desc = e['descripcion'] desc = desc[0..50] + '...' if desc.length > 53 puts " ID #{e['id']} | #{e['inicio'][0..4]} | #{nodo} | #{desc}" end end puts "#{Color::DIM}#{'-' * 50}#{Color::RESET}" end end def verificar_y_mostrar_bitacora_web @logger.info("Verificando estado de bitácora web...") # Verificar si los servicios Docker están activos begin output = `docker compose -f servicios/nginx/dtic-bitacoras/docker-compose.yml ps 2>&1` # Docker muestra "Up" cuando los servicios están activos servicios_activos = output.include?('Up') if servicios_activos puts "" puts "#{Color::CYAN}🌐 Bitácora Web disponible:#{Color::RESET}" puts " #{Color::GREEN}✓ Dashboard local:#{Color::RESET} http://localhost:5174" puts " #{Color::GREEN}✓ Acceso remoto:#{Color::RESET} http://srv-ns8/bitacoras" @logger.exito("Bitácora web verificada y disponible") else puts "" puts "#{Color::YELLOW}⚠ Bitácora Web:#{Color::RESET}" puts " Servicios Docker no están activos. Ejecutar:" puts " #{Color::DIM}docker compose -f servicios/nginx/dtic-bitacoras/docker-compose.yml up -d#{Color::RESET}" @logger.warn("Bitácora web: servicios no activos") end rescue StandardError => e @logger.error("Error verificando bitácora web: #{e.message}") puts "#{Color::RED}✗ Error verificando servicios: #{e.message}#{Color::RESET}" end end end end