316 lines
8.3 KiB
Ruby
316 lines
8.3 KiB
Ruby
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
# tools/orquestador/lib/hitos/manager.rb
|
|
# ======================================
|
|
# Clase para manejo centralizado de hitos del dashboard P2601
|
|
# Filosofía: "Menos es más" - código reutilizable y modular
|
|
|
|
require_relative '../dashboard_core'
|
|
|
|
module DashboardCore
|
|
class HitosManager < Base
|
|
attr_reader :hitos_cache
|
|
|
|
def initialize(dashboard_path = nil)
|
|
super(dashboard_path)
|
|
@hitos_cache = nil
|
|
end
|
|
|
|
# Métodos principales para gestión de hitos
|
|
|
|
def cargar_hitos(force_reload = false)
|
|
return @hitos_cache if @hitos_cache && !force_reload
|
|
|
|
contenido = leer_dashboard
|
|
@hitos_cache = extraer_hitos(contenido)
|
|
@hitos_cache
|
|
end
|
|
|
|
def buscar_hito_por_id(id_hito)
|
|
hitos = cargar_hitos
|
|
hitos.find { |h| h[:id] == id_hito }
|
|
end
|
|
|
|
def buscar_hitos_por_fase(numero_fase)
|
|
hitos = cargar_hitos
|
|
hitos.select do |hito|
|
|
info = HitoIdGenerator.parsear_id(hito[:id])
|
|
info && info[:fase] == numero_fase
|
|
end
|
|
end
|
|
|
|
def buscar_hitos_por_estado(estado)
|
|
hitos = cargar_hitos
|
|
hitos.select { |h| h[:estado] == estado }
|
|
end
|
|
|
|
def buscar_hitos_por_tag(tag)
|
|
hitos = cargar_hitos
|
|
hitos.select { |h| h[:tags].include?(tag) }
|
|
end
|
|
|
|
def agregar_hito(hito_data)
|
|
# Validar datos del hito
|
|
unless hito_data[:id] && hito_data[:titulo]
|
|
raise "ID y título son requeridos para agregar un hito"
|
|
end
|
|
|
|
# Validar formato del ID
|
|
unless HitoIdGenerator.es_valido?(hito_data[:id])
|
|
raise "ID de hito inválido. Formato esperado: fNNNNN (ej: f05001)"
|
|
end
|
|
|
|
# Verificar que no exista ya
|
|
if buscar_hito_por_id(hito_data[:id])
|
|
raise "El hito con ID #{hito_data[:id]} ya existe"
|
|
end
|
|
|
|
# Crear backup
|
|
crear_backup('agregar_hito')
|
|
|
|
# Leer contenido actual
|
|
contenido = leer_dashboard
|
|
|
|
# Generar HTML del hito
|
|
hito_html = generar_html_hito(hito_data)
|
|
|
|
# Insertar en la timeline
|
|
nuevo_contenido = insertar_hitos_en_timeline(contenido, hito_html, 'append')
|
|
|
|
# Guardar cambios
|
|
guardar_dashboard(nuevo_contenido)
|
|
|
|
# Actualizar cache
|
|
@hitos_cache = nil
|
|
cargar_hitos(true)
|
|
|
|
puts "✅ Hito #{hito_data[:id]} agregado exitosamente"
|
|
true
|
|
end
|
|
|
|
def actualizar_hito(id_hito, cambios)
|
|
# Verificar que el hito exista
|
|
hito_existente = buscar_hito_por_id(id_hito)
|
|
unless hito_existente
|
|
raise "Hito #{id_hito} no encontrado"
|
|
end
|
|
|
|
# Validar cambios permitidos
|
|
cambios_permitidos = [:estado, :fecha, :titulo, :descripcion, :tags]
|
|
cambios.each_key do |campo|
|
|
unless cambios_permitidos.include?(campo)
|
|
raise "Campo '#{campo}' no es modificable"
|
|
end
|
|
end
|
|
|
|
# Validar estado si se está actualizando
|
|
if cambios[:estado] && !Validator.validar_estado(cambios[:estado])
|
|
raise "Estado '#{cambios[:estado]}' no válido"
|
|
end
|
|
|
|
# Crear backup
|
|
crear_backup('actualizar_hito')
|
|
|
|
# Leer contenido actual
|
|
contenido = leer_dashboard
|
|
|
|
# Actualizar hito en el contenido
|
|
nuevo_estado = cambios[:estado] || hito_existente[:estado]
|
|
nueva_fecha = cambios[:fecha] || Time.now.strftime('%Y-%m-%d')
|
|
|
|
nuevo_contenido, encontrado = actualizar_hito_en_contenido(
|
|
contenido, id_hito, nuevo_estado, nueva_fecha
|
|
)
|
|
|
|
unless encontrado
|
|
raise "No se pudo encontrar el hito #{id_hito} para actualizar"
|
|
end
|
|
|
|
# Guardar cambios
|
|
guardar_dashboard(nuevo_contenido)
|
|
|
|
# Actualizar cache
|
|
@hitos_cache = nil
|
|
cargar_hitos(true)
|
|
|
|
puts "✅ Hito #{id_hito} actualizado exitosamente"
|
|
true
|
|
end
|
|
|
|
def eliminar_hito(id_hito)
|
|
# Verificar que el hito exista
|
|
unless buscar_hito_por_id(id_hito)
|
|
raise "Hito #{id_hito} no encontrado"
|
|
end
|
|
|
|
# Crear backup
|
|
crear_backup('eliminar_hito')
|
|
|
|
# Leer contenido actual
|
|
contenido = leer_dashboard
|
|
|
|
# Patrón para encontrar y eliminar el hito específico
|
|
patron_hito = /<div class="tl-item">[\s\S]*?<span class="tl-id">#{Regexp.escape(id_hito)}<\/span>[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/
|
|
|
|
nuevo_contenido = contenido.gsub(patron_hito, '')
|
|
|
|
# Verificar que se eliminó algo
|
|
if nuevo_contenido == contenido
|
|
raise "No se pudo eliminar el hito #{id_hito}"
|
|
end
|
|
|
|
# Guardar cambios
|
|
guardar_dashboard(nuevo_contenido)
|
|
|
|
# Actualizar cache
|
|
@hitos_cache = nil
|
|
cargar_hitos(true)
|
|
|
|
puts "✅ Hito #{id_hito} eliminado exitosamente"
|
|
true
|
|
end
|
|
|
|
def agregar_hitos_fase(fase_nombre, modo = 'append')
|
|
# Validar fase
|
|
unless Validator.validar_fase(fase_nombre)
|
|
raise "Fase '#{fase_nombre}' no válida. Fases disponibles: #{FaseManager.fases_disponibles.join(', ')}"
|
|
end
|
|
|
|
# Validar modo
|
|
unless Validator.validar_modo_insercion(modo)
|
|
raise "Modo '#{modo}' no válido. Use 'append' o 'replace'."
|
|
end
|
|
|
|
# Crear backup
|
|
crear_backup("agregar_#{fase_nombre}")
|
|
|
|
# Leer contenido actual
|
|
contenido = leer_dashboard
|
|
|
|
# Generar HTML de hitos de la fase
|
|
hitos_html = FaseManager.generar_hitos_html(fase_nombre)
|
|
|
|
# Insertar en la timeline
|
|
nuevo_contenido = insertar_hitos_en_timeline(contenido, hitos_html, modo)
|
|
|
|
# Guardar cambios
|
|
guardar_dashboard(nuevo_contenido)
|
|
|
|
# Actualizar cache
|
|
@hitos_cache = nil
|
|
hitos_agregados = cargar_hitos(true)
|
|
|
|
# Filtrar hitos de esta fase
|
|
numero_fase = FaseManager.extraer_numero_fase(fase_nombre)
|
|
hitos_fase = buscar_hitos_por_fase(numero_fase)
|
|
|
|
puts "✅ #{hitos_fase.size} hitos de #{fase_nombre} agregados exitosamente"
|
|
hitos_fase
|
|
end
|
|
|
|
# Métodos de consulta y reporte
|
|
|
|
def estadisticas
|
|
hitos = cargar_hitos
|
|
|
|
{
|
|
total: hitos.size,
|
|
por_estado: contar_por_estado(hitos),
|
|
por_fase: contar_por_fase(hitos),
|
|
por_tag: contar_por_tag(hitos),
|
|
ultima_actualizacion: Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
|
}
|
|
end
|
|
|
|
def reporte_detallado
|
|
hitos = cargar_hitos
|
|
Reporter.generar_reporte_hitos(hitos)
|
|
end
|
|
|
|
def verificar_duplicados
|
|
contenido = leer_dashboard
|
|
duplicados = buscar_hitos_duplicados(contenido)
|
|
|
|
if duplicados.empty?
|
|
"✅ No se encontraron hitos duplicados"
|
|
else
|
|
reporte = "⚠️ HITOS DUPLICADOS ENCONTRADOS:\n"
|
|
reporte += "=" * 40 + "\n"
|
|
duplicados.each do |id|
|
|
reporte += "• #{id}\n"
|
|
end
|
|
reporte += "\nTotal: #{duplicados.size} duplicados"
|
|
reporte
|
|
end
|
|
end
|
|
|
|
def limpiar_duplicados
|
|
contenido = leer_dashboard
|
|
duplicados = buscar_hitos_duplicados(contenido)
|
|
|
|
if duplicados.empty?
|
|
puts "✅ No hay hitos duplicados para limpiar"
|
|
return true
|
|
end
|
|
|
|
puts "🧹 Limpiando #{duplicados.size} hitos duplicados..."
|
|
|
|
# Crear backup
|
|
crear_backup('limpiar_duplicados')
|
|
|
|
# Extraer todos los hitos únicos
|
|
hitos_unicos = extraer_hitos(contenido)
|
|
|
|
# Reconstruir timeline con hitos únicos
|
|
timeline_html = hitos_unicos.map { |h| generar_html_hito(h) }.join("\n\n")
|
|
|
|
# Reemplazar la timeline completa
|
|
timeline_pattern = /<div class="timeline">[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/m
|
|
nuevo_contenido = contenido.gsub(timeline_pattern) do |match|
|
|
<<~HTML
|
|
<div class="timeline">
|
|
#{timeline_html}
|
|
</div>
|
|
HTML
|
|
end
|
|
|
|
# Guardar cambios
|
|
guardar_dashboard(nuevo_contenido)
|
|
|
|
# Actualizar cache
|
|
@hitos_cache = nil
|
|
cargar_hitos(true)
|
|
|
|
puts "✅ #{duplicados.size} hitos duplicados eliminados"
|
|
true
|
|
end
|
|
|
|
private
|
|
|
|
def contar_por_estado(hitos)
|
|
conteo = Hash.new(0)
|
|
hitos.each { |h| conteo[h[:estado]] += 1 }
|
|
conteo
|
|
end
|
|
|
|
def contar_por_fase(hitos)
|
|
conteo = Hash.new(0)
|
|
hitos.each do |hito|
|
|
if info = HitoIdGenerator.parsear_id(hito[:id])
|
|
conteo[info[:fase]] += 1
|
|
end
|
|
end
|
|
conteo
|
|
end
|
|
|
|
def contar_por_tag(hitos)
|
|
conteo = Hash.new(0)
|
|
hitos.each do |hito|
|
|
hito[:tags].each { |tag| conteo[tag] += 1 }
|
|
end
|
|
conteo
|
|
end
|
|
end
|
|
end
|