441 lines
13 KiB
Ruby
441 lines
13 KiB
Ruby
#!/usr/bin/env ruby
|
||
# frozen_string_literal: true
|
||
|
||
# tools/orquestador/lib/estados/manager.rb
|
||
# ========================================
|
||
# Clase para manejo centralizado de estados de hitos del dashboard P2601
|
||
# Filosofía: "Menos es más" - código reutilizable y modular
|
||
|
||
require_relative '../dashboard_core'
|
||
|
||
module DashboardCore
|
||
class EstadosManager < Base
|
||
attr_reader :estados_cache
|
||
|
||
def initialize(dashboard_path = nil)
|
||
super(dashboard_path)
|
||
@estados_cache = nil
|
||
end
|
||
|
||
# Métodos principales para gestión de estados
|
||
|
||
def cargar_estados(force_reload = false)
|
||
return @estados_cache if @estados_cache && !force_reload
|
||
|
||
hitos = cargar_hitos
|
||
@estados_cache = analizar_estados(hitos)
|
||
@estados_cache
|
||
end
|
||
|
||
def listar_estados_disponibles
|
||
ESTADOS_VALIDOS.map do |codigo, info|
|
||
{
|
||
codigo: codigo,
|
||
nombre: info[:nombre],
|
||
clase: info[:clase],
|
||
color: info[:color],
|
||
descripcion: descripcion_estado(codigo)
|
||
}
|
||
end
|
||
end
|
||
|
||
def actualizar_estado_hito(id_hito, nuevo_estado, fecha = nil)
|
||
# Validar estado
|
||
unless Validator.validar_estado(nuevo_estado)
|
||
raise "Estado '#{nuevo_estado}' no válido. Estados disponibles: #{ESTADOS_VALIDOS.keys.join(', ')}"
|
||
end
|
||
|
||
# Verificar que el hito exista
|
||
hito = buscar_hito_por_id(id_hito)
|
||
unless hito
|
||
raise "Hito #{id_hito} no encontrado"
|
||
end
|
||
|
||
# No hacer nada si el estado ya es el mismo
|
||
if hito[:estado] == nuevo_estado
|
||
puts "ℹ️ El hito #{id_hito} ya tiene estado '#{nuevo_estado}'"
|
||
return true
|
||
end
|
||
|
||
# Crear backup
|
||
crear_backup('actualizar_estado')
|
||
|
||
# Leer contenido actual
|
||
contenido = leer_dashboard
|
||
|
||
# Actualizar estado en el contenido
|
||
fecha_actual = fecha || Time.now.strftime('%Y-%m-%d')
|
||
estado_info = ESTADOS_VALIDOS[nuevo_estado]
|
||
|
||
nuevo_contenido, encontrado = actualizar_hito_en_contenido(
|
||
contenido, id_hito, nuevo_estado, fecha_actual
|
||
)
|
||
|
||
unless encontrado
|
||
raise "No se pudo encontrar el hito #{id_hito} para actualizar estado"
|
||
end
|
||
|
||
# Guardar cambios
|
||
guardar_dashboard(nuevo_contenido)
|
||
|
||
# Actualizar cache
|
||
@estados_cache = nil
|
||
cargar_estados(true)
|
||
|
||
# Registrar cambio
|
||
registrar_cambio_estado(id_hito, hito[:estado], nuevo_estado, fecha_actual)
|
||
|
||
puts "✅ Estado del hito #{id_hito} actualizado: #{hito[:estado]} → #{nuevo_estado}"
|
||
true
|
||
end
|
||
|
||
def actualizar_estados_multiples(cambios)
|
||
# Validar todos los cambios primero
|
||
cambios.each do |id_hito, nuevo_estado|
|
||
unless Validator.validar_estado(nuevo_estado)
|
||
raise "Estado '#{nuevo_estado}' no válido para hito #{id_hito}"
|
||
end
|
||
end
|
||
|
||
# Crear backup
|
||
crear_backup('actualizar_estados_multiples')
|
||
|
||
# Leer contenido actual
|
||
contenido = leer_dashboard
|
||
nuevo_contenido = contenido.dup
|
||
|
||
# Aplicar cada cambio
|
||
resultados = {}
|
||
cambios.each do |id_hito, nuevo_estado|
|
||
puts "🔍 Actualizando hito: #{id_hito} → #{nuevo_estado}"
|
||
|
||
# Verificar que el hito exista
|
||
hito = buscar_hito_por_id(id_hito)
|
||
if hito
|
||
# Actualizar en el contenido
|
||
fecha_actual = Time.now.strftime('%Y-%m-%d')
|
||
nuevo_contenido, encontrado = actualizar_hito_en_contenido(
|
||
nuevo_contenido, id_hito, nuevo_estado, fecha_actual
|
||
)
|
||
|
||
if encontrado && hito[:estado] != nuevo_estado
|
||
resultados[id_hito] = { exito: true, anterior: hito[:estado], nuevo: nuevo_estado }
|
||
registrar_cambio_estado(id_hito, hito[:estado], nuevo_estado, fecha_actual)
|
||
else
|
||
resultados[id_hito] = { exito: false, error: 'No encontrado o estado igual' }
|
||
end
|
||
else
|
||
resultados[id_hito] = { exito: false, error: 'Hito no encontrado' }
|
||
end
|
||
end
|
||
|
||
# Guardar cambios si hubo actualizaciones exitosas
|
||
exitos = resultados.values.count { |r| r[:exito] }
|
||
if exitos > 0
|
||
guardar_dashboard(nuevo_contenido)
|
||
@estados_cache = nil
|
||
cargar_estados(true)
|
||
end
|
||
|
||
# Mostrar resultados
|
||
mostrar_resultados_actualizacion(resultados)
|
||
|
||
exitos > 0
|
||
end
|
||
|
||
def avanzar_estado_hito(id_hito)
|
||
# Obtener hito actual
|
||
hito = buscar_hito_por_id(id_hito)
|
||
unless hito
|
||
raise "Hito #{id_hito} no encontrado"
|
||
end
|
||
|
||
# Determinar siguiente estado según flujo de trabajo
|
||
siguiente_estado = siguiente_estado_en_flujo(hito[:estado])
|
||
|
||
if siguiente_estado
|
||
actualizar_estado_hito(id_hito, siguiente_estado)
|
||
else
|
||
puts "ℹ️ El hito #{id_hito} ya está en el estado final del flujo"
|
||
false
|
||
end
|
||
end
|
||
|
||
def retroceder_estado_hito(id_hito)
|
||
# Obtener hito actual
|
||
hito = buscar_hito_por_id(id_hito)
|
||
unless hito
|
||
raise "Hito #{id_hito} no encontrado"
|
||
end
|
||
|
||
# Determinar estado anterior según flujo de trabajo
|
||
estado_anterior = estado_anterior_en_flujo(hito[:estado])
|
||
|
||
if estado_anterior
|
||
actualizar_estado_hito(id_hito, estado_anterior)
|
||
else
|
||
puts "ℹ️ El hito #{id_hito} ya está en el estado inicial del flujo"
|
||
false
|
||
end
|
||
end
|
||
|
||
def reiniciar_estado_hito(id_hito)
|
||
actualizar_estado_hito(id_hito, 'pending')
|
||
end
|
||
|
||
def completar_hito(id_hito, fecha = nil)
|
||
actualizar_estado_hito(id_hito, 'completed', fecha)
|
||
end
|
||
|
||
def bloquear_hito(id_hito, fecha = nil)
|
||
actualizar_estado_hito(id_hito, 'blocked', fecha)
|
||
end
|
||
|
||
def poner_en_espera_hito(id_hito, fecha = nil)
|
||
actualizar_estado_hito(id_hito, 'on-hold', fecha)
|
||
end
|
||
|
||
def iniciar_hito(id_hito, fecha = nil)
|
||
actualizar_estado_hito(id_hito, 'in-progress', fecha)
|
||
end
|
||
|
||
# Métodos de consulta y análisis
|
||
|
||
def estadisticas_estados
|
||
estados = cargar_estados
|
||
|
||
{
|
||
total_hitos: estados[:total_hitos],
|
||
distribucion: estados[:distribucion],
|
||
porcentajes: estados[:porcentajes],
|
||
tendencia: calcular_tendencia(estados),
|
||
ultima_actualizacion: Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
end
|
||
|
||
def reporte_estados
|
||
estados = cargar_estados
|
||
|
||
reporte = "📊 REPORTE DE ESTADOS DE HITOS\n"
|
||
reporte += "=" * 60 + "\n"
|
||
reporte += "Total de hitos: #{estados[:total_hitos]}\n\n"
|
||
|
||
reporte += "📈 DISTRIBUCIÓN POR ESTADO:\n"
|
||
estados[:distribucion].each do |estado, cantidad|
|
||
porcentaje = estados[:porcentajes][estado]
|
||
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
|
||
reporte += " • #{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)\n"
|
||
end
|
||
|
||
# Análisis de progreso
|
||
completados = estados[:distribucion]['completed'] || 0
|
||
en_progreso = estados[:distribucion]['in-progress'] || 0
|
||
total = estados[:total_hitos]
|
||
|
||
if total > 0
|
||
progreso_total = ((completados + (en_progreso * 0.5)) / total * 100).round(1)
|
||
reporte += "\n🎯 PROGRESO GENERAL DEL PROYECTO:\n"
|
||
reporte += " • Progreso estimado: #{progreso_total}%\n"
|
||
reporte += " • Hitos completados: #{completados}/#{total}\n"
|
||
reporte += " • Hitos en progreso: #{en_progreso}/#{total}\n"
|
||
end
|
||
|
||
# Recomendaciones
|
||
reporte += "\n💡 RECOMENDACIONES:\n"
|
||
if (estados[:distribucion]['pending'] || 0) > total * 0.7
|
||
reporte += " • Muchos hitos pendientes (#{estados[:distribucion]['pending']}/#{total})\n"
|
||
reporte += " • Considerar iniciar más hitos\n"
|
||
end
|
||
|
||
if (estados[:distribucion]['blocked'] || 0) > 0
|
||
reporte += " • Hay hitos bloqueados (#{estados[:distribucion]['blocked']})\n"
|
||
reporte += " • Revisar dependencias y obstáculos\n"
|
||
end
|
||
|
||
if (estados[:distribucion]['on-hold'] || 0) > 0
|
||
reporte += " • Hay hitos en espera (#{estados[:distribucion]['on-hold']})\n"
|
||
reporte += " • Evaluar prioridades y recursos\n"
|
||
end
|
||
|
||
reporte
|
||
end
|
||
|
||
def hitos_por_estado(estado)
|
||
hitos = cargar_hitos
|
||
hitos.select { |h| h[:estado] == estado }
|
||
end
|
||
|
||
def hitos_pendientes
|
||
hitos_por_estado('pending')
|
||
end
|
||
|
||
def hitos_en_progreso
|
||
hitos_por_estado('in-progress')
|
||
end
|
||
|
||
def hitos_completados
|
||
hitos_por_estado('completed')
|
||
end
|
||
|
||
def hitos_bloqueados
|
||
hitos_por_estado('blocked')
|
||
end
|
||
|
||
def hitos_en_espera
|
||
hitos_por_estado('on-hold')
|
||
end
|
||
|
||
def progreso_por_fase
|
||
hitos = cargar_hitos
|
||
progreso_por_fase = {}
|
||
|
||
hitos.group_by do |hito|
|
||
info = HitoIdGenerator.parsear_id(hito[:id])
|
||
info ? info[:fase] : 'sin_fase'
|
||
end.each do |fase, hitos_fase|
|
||
total = hitos_fase.size
|
||
completados = hitos_fase.count { |h| h[:estado] == 'completed' }
|
||
en_progreso = hitos_fase.count { |h| h[:estado] == 'in-progress' }
|
||
|
||
progreso = total > 0 ? ((completados + (en_progreso * 0.5)) / total * 100).round(1) : 0
|
||
|
||
progreso_por_fase[fase] = {
|
||
total: total,
|
||
completados: completados,
|
||
en_progreso: en_progreso,
|
||
progreso: progreso
|
||
}
|
||
end
|
||
|
||
progreso_por_fase
|
||
end
|
||
|
||
private
|
||
|
||
def cargar_hitos
|
||
contenido = leer_dashboard
|
||
extraer_hitos(contenido)
|
||
end
|
||
|
||
def buscar_hito_por_id(id_hito)
|
||
hitos = cargar_hitos
|
||
hitos.find { |h| h[:id] == id_hito }
|
||
end
|
||
|
||
def analizar_estados(hitos)
|
||
total_hitos = hitos.size
|
||
distribucion = Hash.new(0)
|
||
hitos.each { |h| distribucion[h[:estado]] += 1 }
|
||
|
||
porcentajes = {}
|
||
distribucion.each do |estado, cantidad|
|
||
porcentajes[estado] = total_hitos > 0 ? (cantidad * 100.0 / total_hitos).round(1) : 0
|
||
end
|
||
|
||
{
|
||
total_hitos: total_hitos,
|
||
distribucion: distribucion,
|
||
porcentajes: porcentajes,
|
||
hitos: hitos
|
||
}
|
||
end
|
||
|
||
def descripcion_estado(codigo_estado)
|
||
case codigo_estado
|
||
when 'pending'
|
||
"Hito pendiente de iniciar. Color amarillo."
|
||
when 'in-progress'
|
||
"Hito en ejecución activa. Color azul."
|
||
when 'completed'
|
||
"Hito finalizado exitosamente. Color verde."
|
||
when 'blocked'
|
||
"Hito bloqueado por dependencias u obstáculos. Color rojo."
|
||
when 'on-hold'
|
||
"Hito en espera por priorización o recursos. Color púrpura."
|
||
else
|
||
"Estado desconocido."
|
||
end
|
||
end
|
||
|
||
def siguiente_estado_en_flujo(estado_actual)
|
||
flujo = {
|
||
'pending' => 'in-progress',
|
||
'in-progress' => 'completed',
|
||
'completed' => nil, # Estado final
|
||
'blocked' => 'in-progress', # Desbloqueado
|
||
'on-hold' => 'in-progress' # Reanudado
|
||
}
|
||
|
||
flujo[estado_actual]
|
||
end
|
||
|
||
def estado_anterior_en_flujo(estado_actual)
|
||
flujo_inverso = {
|
||
'in-progress' => 'pending',
|
||
'completed' => 'in-progress',
|
||
'blocked' => 'in-progress',
|
||
'on-hold' => 'in-progress'
|
||
}
|
||
|
||
flujo_inverso[estado_actual]
|
||
end
|
||
|
||
def registrar_cambio_estado(id_hito, estado_anterior, estado_nuevo, fecha)
|
||
# Podría guardarse en un log o base de datos
|
||
log_entry = {
|
||
id_hito: id_hito,
|
||
estado_anterior: estado_anterior,
|
||
estado_nuevo: estado_nuevo,
|
||
fecha: fecha,
|
||
timestamp: Time.now.to_i
|
||
}
|
||
|
||
# Por ahora solo imprimimos para debug
|
||
debug("Cambio de estado registrado: #{log_entry}")
|
||
end
|
||
|
||
def mostrar_resultados_actualizacion(resultados)
|
||
puts "\n📊 RESULTADOS DE ACTUALIZACIÓN DE ESTADOS"
|
||
puts "=" * 60
|
||
|
||
exitos = resultados.values.count { |r| r[:exito] }
|
||
fallos = resultados.values.count { |r| !r[:exito] }
|
||
|
||
resultados.each do |id_hito, resultado|
|
||
if resultado[:exito]
|
||
puts "✅ #{id_hito}: #{resultado[:anterior]} → #{resultado[:nuevo]}"
|
||
else
|
||
puts "❌ #{id_hito}: #{resultado[:error]}"
|
||
end
|
||
end
|
||
|
||
puts "\n📈 RESUMEN:"
|
||
puts " • Hitós actualizados exitosamente: #{exitos}"
|
||
puts " • Hitós con errores: #{fallos}"
|
||
end
|
||
|
||
def calcular_tendencia(estados)
|
||
# Simple análisis de tendencia (podría mejorarse con histórico)
|
||
completados = estados[:distribucion]['completed'] || 0
|
||
en_progreso = estados[:distribucion]['in-progress'] || 0
|
||
total = estados[:total_hitos]
|
||
|
||
if total == 0
|
||
return 'sin_datos'
|
||
end
|
||
|
||
porcentaje_completados = (completados * 100.0 / total).round(1)
|
||
|
||
if porcentaje_completados > 70
|
||
'avanzado'
|
||
elsif porcentaje_completados > 30
|
||
'en_progreso'
|
||
elsif en_progreso > 0
|
||
'iniciando'
|
||
else
|
||
'pendiente'
|
||
end
|
||
end
|
||
end
|
||
end
|