Files
IPSExtractor/gui/tabs/results_tab.py
T

305 lines
12 KiB
Python

"""Pestaña de Resultados de la aplicación GUI."""
import os
import tkinter as tk
import customtkinter as ctk
import tkinter.ttk as ttk
from gui.styles import AppStyles
class ResultsTab:
"""Encapsula la UI de la pestaña Resultados."""
def __init__(self, parent_tab):
"""Inicializa la pestaña de Resultados.
Args:
parent_tab: El CTkFrame que representa la pestaña.
"""
self.tab = parent_tab
self.fonts = AppStyles.get_fonts()
self.colors = AppStyles.get_colors()
self.spacing = AppStyles.get_spacing()
self._setup_layout()
self._create_subtabs()
self._setup_treeview_style()
self._create_extractor_tab()
self._create_preclass_results_tab()
def _setup_layout(self):
"""Configura el layout base de la pestaña."""
self.tab.grid_columnconfigure(0, weight=1)
self.tab.grid_rowconfigure(0, weight=1)
self.tabs_res = ctk.CTkTabview(self.tab)
self.tabs_res.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
def _create_subtabs(self):
"""Crea las sub-pestañas de Resultados."""
self.tab_res_extractor = self.tabs_res.add("Extractor (Próximamente)")
self.tab_res_preclass = self.tabs_res.add("Preclasificación")
def _setup_treeview_style(self):
"""Configura el estilo de los treeviews."""
style = ttk.Style()
style.theme_use("clam")
style.configure("Treeview", rowheight=25)
def _create_extractor_tab(self):
"""Crea la sub-pestaña del Extractor (en construcción)."""
lbl_const = ctk.CTkLabel(
self.tab_res_extractor,
text="En construcción...",
font=ctk.CTkFont(size=20)
)
lbl_const.place(relx=0.5, rely=0.5, anchor="center")
def _create_preclass_results_tab(self):
"""Crea la sub-pestaña de resultados de preclasificación."""
self.tab_res_preclass.grid_columnconfigure(0, weight=1)
self.tab_res_preclass.grid_rowconfigure(1, weight=1)
self._create_filters_section()
self._create_tables_section()
self._create_actions_section()
def _create_filters_section(self):
"""Crea la sección de filtros."""
self.frame_res_filters = ctk.CTkFrame(self.tab_res_preclass)
self.frame_res_filters.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
ctk.CTkLabel(self.frame_res_filters, text="Run ID:").grid(row=0, column=0, padx=5, pady=5)
self.combo_res_run = ctk.CTkComboBox(self.frame_res_filters, values=["Todos"], width=150)
self.combo_res_run.grid(row=0, column=1, padx=5, pady=5)
ctk.CTkLabel(self.frame_res_filters, text="Estado:").grid(row=0, column=2, padx=5, pady=5)
self.combo_res_status = ctk.CTkComboBox(
self.frame_res_filters,
values=["Todos", "MATCH", "NO_MATCH", "ERROR", "NO_FILES"],
width=120
)
self.combo_res_status.grid(row=0, column=3, padx=5, pady=5)
self.entry_res_search = ctk.CTkEntry(
self.frame_res_filters,
placeholder_text="Buscar radicado/archivo...",
width=200
)
self.entry_res_search.grid(row=0, column=4, padx=5, pady=5)
self.btn_res_search = ctk.CTkButton(self.frame_res_filters, text="Buscar", width=100)
self.btn_res_search.grid(row=0, column=5, padx=10, pady=5)
self.lbl_res_priority = ctk.CTkLabel(
self.frame_res_filters,
text=(
"Filtros estrictos: se combinan (AND) con importancia: "
"Run > estado (MATCH, etc.) > criterio de búsqueda. "
"Puede ampliar la búsqueda (sin tocar el run) abajo."
),
text_color=("#5C6370", "#9CA3AF"),
font=ctk.CTkFont(size=12),
anchor="w",
wraplength=920,
)
self.lbl_res_priority.grid(
row=1, column=0, columnspan=6, padx=5, pady=(4, 0), sticky="w"
)
self._res_relax_var = tk.BooleanVar(value=False)
self.chk_res_relax = ctk.CTkCheckBox(
self.frame_res_filters,
text="Si 0 resultados, ampliar: quitar criterio de búsqueda, luego estado (el run fijado no se descarta).",
font=ctk.CTkFont(size=12),
variable=self._res_relax_var,
)
self.chk_res_relax.grid(
row=2, column=0, columnspan=6, padx=5, pady=4, sticky="w"
)
self.lbl_res_filter_note = ctk.CTkLabel(
self.frame_res_filters,
text="",
text_color=("#1D4ED8", "#60A5FA"),
font=ctk.CTkFont(size=12),
anchor="w",
wraplength=920,
)
self.lbl_res_filter_note.grid(
row=3, column=0, columnspan=6, padx=5, pady=(0, 4), sticky="w"
)
def _create_tables_section(self):
"""Crea la sección con las tablas (principal y de detalle)."""
self.frame_res_table = ctk.CTkFrame(self.tab_res_preclass, fg_color="transparent")
self.frame_res_table.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
self.frame_res_table.grid_columnconfigure(0, weight=1)
self.frame_res_table.grid_rowconfigure(0, weight=3)
self.frame_res_table.grid_rowconfigure(2, weight=2)
self._create_main_results_table()
self._create_detail_label()
self._create_matches_table()
def _create_main_results_table(self):
"""Crea la tabla principal de resultados."""
cols = ("ID", "Run", "Fecha", "Radicado", "Estado", "Archivo")
self.tree_results = ttk.Treeview(
self.frame_res_table,
columns=cols,
show="headings",
selectmode="browse"
)
for c in cols:
self.tree_results.heading(c, text=c)
self.tree_results.column(c, width=100)
self.tree_results.column("Archivo", width=300)
self.tree_results.column("ID", width=50)
self.tree_results.column("Run", width=50)
# Scrollbars
sb_y = ttk.Scrollbar(self.frame_res_table, orient="vertical", command=self.tree_results.yview)
sb_x = ttk.Scrollbar(self.frame_res_table, orient="horizontal", command=self.tree_results.xview)
self.tree_results.configure(yscrollcommand=sb_y.set, xscrollcommand=sb_x.set)
self.tree_results.grid(row=0, column=0, sticky="nsew")
sb_y.grid(row=0, column=1, sticky="ns")
sb_x.grid(row=1, column=0, sticky="ew")
def _create_detail_label(self):
"""Crea la etiqueta de la sección de detalle."""
lbl_detail = ctk.CTkLabel(
self.frame_res_table,
text="Detalle de Matches (Seleccionar fila arriba)",
font=ctk.CTkFont(size=12, weight="bold")
)
lbl_detail.grid(row=1, column=0, sticky="w", pady=(10, 5))
def _create_matches_table(self):
"""Crea la tabla de detalle de matches."""
cols_detail = ("Key", "Valor Encontrado", "Existe", "Valor Esperado")
self.tree_matches = ttk.Treeview(
self.frame_res_table,
columns=cols_detail,
show="headings",
selectmode="browse",
height=5
)
for c in cols_detail:
self.tree_matches.heading(c, text=c)
self.tree_matches.column(c, width=150)
sb_detail_y = ttk.Scrollbar(self.frame_res_table, orient="vertical", command=self.tree_matches.yview)
self.tree_matches.configure(yscrollcommand=sb_detail_y.set)
self.tree_matches.grid(row=2, column=0, sticky="nsew")
sb_detail_y.grid(row=2, column=1, sticky="ns")
def _create_actions_section(self):
"""Crea la sección de acciones."""
self.frame_res_actions = ctk.CTkFrame(self.tab_res_preclass)
self.frame_res_actions.grid(row=2, column=0, sticky="ew", padx=10, pady=10)
self.btn_res_export = ctk.CTkButton(
self.frame_res_actions,
text="Exportar Vista a Excel",
fg_color=self.colors["success"]
)
self.btn_res_export.pack(side="right", padx=10, pady=5)
self.btn_res_organize = ctk.CTkButton(
self.frame_res_actions,
text="Organizar Carpetas (Run)",
fg_color=self.colors["warning"],
hover_color=self.colors["warning_hover"]
)
self.btn_res_organize.pack(side="right", padx=10, pady=5)
self.btn_res_refresh = ctk.CTkButton(self.frame_res_actions, text="Refrescar Runs")
self.btn_res_refresh.pack(side="left", padx=10, pady=5)
# --- API Pública ---
def set_on_search(self, callback):
"""Vincula el callback al botón de búsqueda."""
self.btn_res_search.configure(command=callback)
def set_on_export(self, callback):
"""Vincula el callback al botón de exportación."""
self.btn_res_export.configure(command=callback)
def set_on_refresh(self, callback):
"""Vincula el callback al botón de refrescar."""
self.btn_res_refresh.configure(command=callback)
def set_on_organize(self, callback):
"""Vincula el callback al botón de organizar."""
self.btn_res_organize.configure(command=callback)
def set_on_select_result(self, callback):
"""Vincula el callback al evento de selección de fila."""
self.tree_results.bind("<<TreeviewSelect>>", callback)
def get_selected_result_values(self) -> tuple | None:
"""Devuelve los valores de la fila seleccionada o None si no hay selección."""
selection = self.tree_results.selection()
if not selection:
return None
return self.tree_results.item(selection[0], "values") or None
def update_matches_table(self, rows):
"""Actualiza la tabla de matches con los datos proporcionados."""
for item in self.tree_matches.get_children():
self.tree_matches.delete(item)
for r in rows:
exists_str = "✅" if r.get("exists_bool") else "❌"
vals = (r.get("key"), r.get("found_value"), exists_str, r.get("expected_value") or "-")
self.tree_matches.insert("", "end", values=vals)
def update_runs_combo(self, runs: list):
"""Actualiza el combo box con los IDs de ejecuciones disponibles."""
vals = ["Todos"] + [str(r) for r in runs]
self.combo_res_run.configure(values=vals)
self.combo_res_run.set("Todos")
def set_run_id(self, run_id: int | str):
"""Establece el ID de ejecución seleccionado."""
try:
self.combo_res_run.set(str(run_id))
except Exception:
pass
def get_filters(self) -> dict:
"""Devuelve los filtros actuales."""
return {
"run_id": self.combo_res_run.get(),
"status": self.combo_res_status.get(),
"text": self.entry_res_search.get(),
"relax_on_empty": bool(self._res_relax_var.get()),
}
def set_filter_note(self, text: str) -> None:
"""Mensaje bajo ampliar (p. ej. qué criterio se relajó). Texto vacío la limpia."""
self.lbl_res_filter_note.configure(text=(text or "").strip())
def clear_filter_note(self) -> None:
self.set_filter_note("")
def update_results_table(self, rows):
"""Actualiza la tabla de resultados con los datos proporcionados."""
for item in self.tree_results.get_children():
self.tree_results.delete(item)
for r in rows:
vals = (
r.get("id"),
r.get("run_id"),
r.get("timestamp"),
r.get("radicado"),
r.get("status"),
os.path.basename(r.get("file_path") or "")
)
self.tree_results.insert("", "end", values=vals)