"""Pestaña Preclasificación de la aplicación GUI.""" import customtkinter as ctk from gui.styles import AppStyles class PreclassificationTab: """Encapsula la UI de la pestaña Preclasificación.""" def __init__(self, parent_tab): """Inicializa la pestaña de Preclasificación. 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() # Variables internas self._preclass_pattern_rows = [] self._setup_layout() self._create_root_section() self._create_options_section() self._create_target_section() self._create_patterns_section() self._create_action_section() self._create_output_section() 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.preclass_container = ctk.CTkScrollableFrame(self.tab, fg_color="transparent") self.preclass_container.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) self.preclass_container.grid_columnconfigure(0, weight=1) self.preclass_container.grid_rowconfigure(3, weight=1) self.preclass_container.grid_rowconfigure(5, weight=1) def _create_root_section(self): """Crea la sección de carpeta raíz.""" self.preclass_frame_root = ctk.CTkFrame(self.preclass_container) self.preclass_frame_root.grid(row=0, column=0, sticky="ew", padx=5, pady=(5, 10)) self.preclass_frame_root.grid_columnconfigure(1, weight=1) lbl_pre_root_title = ctk.CTkLabel( self.preclass_frame_root, text="Carpeta Raíz (Preclasificación)", font=self.fonts["section"] ) lbl_pre_root_title.grid(row=0, column=0, columnspan=3, sticky="w", padx=15, pady=(10, 5)) self.preclass_root_label = ctk.CTkLabel( self.preclass_frame_root, text="Carpeta Raíz:", font=self.fonts["ui"] ) self.preclass_root_label.grid(row=1, column=0, padx=15, pady=(0, 10), sticky="w") self.preclass_root_entry = ctk.CTkEntry( self.preclass_frame_root, placeholder_text="Selecciona la carpeta raíz para preclasificar...", font=self.fonts["ui"] ) self.preclass_root_entry.grid(row=1, column=1, padx=5, pady=(0, 10), sticky="ew") self.preclass_root_button = ctk.CTkButton( self.preclass_frame_root, text="Explorar...", font=self.fonts["ui"], width=100 ) self.preclass_root_button.grid(row=1, column=2, padx=15, pady=(0, 10)) # Selector de Modo self.preclass_mode_var = ctk.StringVar(value="radicados") lbl_mode = ctk.CTkLabel(self.preclass_frame_root, text="Estructura:", font=self.fonts["ui"]) lbl_mode.grid(row=2, column=0, padx=15, pady=(0, 10), sticky="w") self.frame_radios = ctk.CTkFrame(self.preclass_frame_root, fg_color="transparent") self.frame_radios.grid(row=2, column=1, columnspan=2, sticky="w", padx=5, pady=(0, 10)) self.radio_radicados = ctk.CTkRadioButton( self.frame_radios, text="Por Radicados (Subcarpetas)", variable=self.preclass_mode_var, value="radicados", font=self.fonts["ui"] ) self.radio_radicados.pack(side="left", padx=(0, 20)) self.radio_general = ctk.CTkRadioButton( self.frame_radios, text="Carpeta General (Archivos Planos)", variable=self.preclass_mode_var, value="general", font=self.fonts["ui"] ) self.radio_general.pack(side="left") def _create_options_section(self): """Crea la sección de opciones de rango.""" self.preclass_frame_options = ctk.CTkFrame(self.preclass_container) self.preclass_frame_options.grid(row=1, column=0, sticky="ew", padx=5, pady=(0, 10)) self.preclass_frame_options.grid_columnconfigure(1, weight=1) self.preclass_range_var = ctk.BooleanVar(value=False) self.preclass_range_cb = ctk.CTkCheckBox( self.preclass_frame_options, text="Habilitar rangos de página", variable=self.preclass_range_var, font=self.fonts["ui"], command=self._on_toggle_preclass_range ) self.preclass_range_cb.grid(row=0, column=0, padx=15, pady=10, sticky="w") self.preclass_range_inputs_frame = ctk.CTkFrame(self.preclass_frame_options, fg_color="transparent") lbl_desde = ctk.CTkLabel(self.preclass_range_inputs_frame, text="Desde:", font=self.fonts["ui"]) lbl_desde.grid(row=0, column=0, padx=(10, 5), pady=5) self.preclass_range_start_entry = ctk.CTkEntry(self.preclass_range_inputs_frame, width=60, font=self.fonts["ui"]) self.preclass_range_start_entry.insert(0, "1") self.preclass_range_start_entry.grid(row=0, column=1, padx=5, pady=5) lbl_hasta = ctk.CTkLabel(self.preclass_range_inputs_frame, text="Hasta:", font=self.fonts["ui"]) lbl_hasta.grid(row=0, column=2, padx=(10, 5), pady=5) self.preclass_range_end_entry = ctk.CTkEntry(self.preclass_range_inputs_frame, width=60, font=self.fonts["ui"]) self.preclass_range_end_entry.insert(0, "1") self.preclass_range_end_entry.grid(row=0, column=3, padx=5, pady=5) def _create_target_section(self): """Crea la sección de archivo objetivo.""" self.preclass_frame_target = ctk.CTkFrame(self.preclass_container) self.preclass_frame_target.grid(row=2, column=0, sticky="ew", padx=5, pady=(0, 10)) self.preclass_frame_target.grid_columnconfigure(0, weight=1) self.preclass_target_var = ctk.BooleanVar(value=False) self.preclass_target_cb = ctk.CTkCheckBox( self.preclass_frame_target, text="Habilitar archivo objetivo", variable=self.preclass_target_var, font=self.fonts["ui"], command=self._on_toggle_preclass_target ) self.preclass_target_cb.grid(row=0, column=0, padx=15, pady=10, sticky="w") self.preclass_target_inputs_frame = ctk.CTkFrame(self.preclass_frame_target, fg_color="transparent") lbl_target_def = ctk.CTkLabel(self.preclass_target_inputs_frame, text="Def. Tipo Doc:", font=self.fonts["ui"]) lbl_target_def.grid(row=0, column=0, padx=(15, 5), pady=5, sticky="w") self.preclass_target_def_entry = ctk.CTkEntry( self.preclass_target_inputs_frame, width=250, font=self.fonts["ui"], placeholder_text="Ej: Factura de Venta" ) self.preclass_target_def_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w") lbl_target_pattern = ctk.CTkLabel(self.preclass_target_inputs_frame, text="Patrón:", font=self.fonts["ui"]) lbl_target_pattern.grid(row=0, column=2, padx=(20, 5), pady=5, sticky="w") self.preclass_target_pattern_entry = ctk.CTkEntry( self.preclass_target_inputs_frame, width=200, font=self.fonts["ui"], placeholder_text="Ej: *_FAC_*.pdf" ) self.preclass_target_pattern_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w") def _create_patterns_section(self): """Crea la sección de patrones.""" self.preclass_frame_patterns = ctk.CTkFrame(self.preclass_container) self.preclass_frame_patterns.grid(row=3, column=0, sticky="nsew", padx=5, pady=5) self.preclass_frame_patterns.grid_columnconfigure(0, weight=1) self.preclass_patterns_header = ctk.CTkFrame(self.preclass_frame_patterns, fg_color="transparent") self.preclass_patterns_header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 2)) self.preclass_patterns_header.grid_columnconfigure(0, weight=1) self.preclass_patterns_label = ctk.CTkLabel( self.preclass_patterns_header, text="Patrones", font=self.fonts["section"] ) self.preclass_patterns_label.grid(row=0, column=0, sticky="w") self.add_preclass_pattern_btn = ctk.CTkButton( self.preclass_patterns_header, text="+ Añadir Patrón", font=self.fonts["ui"], width=140, height=28 ) self.add_preclass_pattern_btn.grid(row=0, column=1, sticky="e") self.add_preclass_pattern_btn.configure(command=self._add_preclass_pattern_row) self.preclass_patterns_hint = ctk.CTkLabel( self.preclass_frame_patterns, text="Agrega patrones de texto. Cada patrón puede ser solo 'key' o 'key + value'.", font=self.fonts["italic_small"], text_color=self.colors["gray_light"], justify="left" ) self.preclass_patterns_hint.grid(row=1, column=0, sticky="w", padx=15, pady=(0, 5)) self.preclass_patterns_list = ctk.CTkFrame(self.preclass_frame_patterns, fg_color="transparent") self.preclass_patterns_list.grid(row=2, column=0, sticky="ew", padx=10, pady=(2, 10)) self.preclass_patterns_list.grid_columnconfigure(0, weight=1) # Fila por defecto (vacía) self._add_preclass_pattern_row() def _create_action_section(self): """Crea la sección de acciones.""" self.preclass_action_frame = ctk.CTkFrame(self.preclass_container, fg_color="transparent") self.preclass_action_frame.grid(row=4, column=0, sticky="ew", padx=5, pady=10) self.preclass_action_frame.grid_columnconfigure(0, weight=1) self.preclass_run_btn = ctk.CTkButton( self.preclass_action_frame, text="EJECUTAR PRECLASIFICACIÓN", font=self.fonts["bold_large"], height=40, fg_color=self.colors["secondary_button"], hover_color=self.colors["secondary_hover"] ) self.preclass_run_btn.pack(side="left", fill="x", expand=True, padx=10) self.preclass_export_btn = ctk.CTkButton( self.preclass_action_frame, text="Exportar Excel", font=self.fonts["ui"], height=40, state="disabled", fg_color=self.colors["success"], hover_color=self.colors["success_hover"] ) self.preclass_export_btn.pack(side="left", padx=(0, 10)) self.preclass_organize_btn = ctk.CTkButton( self.preclass_action_frame, text="Organizar Carpetas", font=self.fonts["ui"], height=40, state="disabled", fg_color=self.colors["warning"], hover_color=self.colors["warning_hover"] ) self.preclass_organize_btn.pack(side="left", padx=(0, 10)) def _create_output_section(self): """Crea la sección de salida/logs.""" self.preclass_log_frame = ctk.CTkFrame(self.preclass_container) self.preclass_log_frame.grid(row=5, column=0, sticky="nsew", padx=5, pady=(0, 10)) self.preclass_log_frame.grid_columnconfigure(0, weight=1) self.preclass_log_frame.grid_rowconfigure(1, weight=1) lbl_pre_logs = ctk.CTkLabel( self.preclass_log_frame, text="Resultados:", font=self.fonts["ui"], text_color=self.colors["text_muted"] ) lbl_pre_logs.grid(row=0, column=0, sticky="w", padx=10, pady=(5, 0)) self.preclass_output = ctk.CTkTextbox( self.preclass_log_frame, font=self.fonts["mono"], activate_scrollbars=True ) self.preclass_output.grid(row=1, column=0, padx=5, pady=5, sticky="nsew") def _add_preclass_pattern_row(self, key: str = "", mode: str = "key", value: str = "") -> None: """Añade una fila editable para un patrón de preclasificación.""" row = ctk.CTkFrame(self.preclass_patterns_list, fg_color="transparent") row.pack(fill="x", pady=2) grid_inner = ctk.CTkFrame(row, fg_color="transparent") grid_inner.pack(fill="x", padx=5, pady=2) grid_inner.grid_columnconfigure(0, weight=1) grid_inner.grid_columnconfigure(2, weight=1) key_entry = ctk.CTkEntry(grid_inner, placeholder_text="Patrón / Key", height=28, font=self.fonts["ui"]) if key: key_entry.insert(0, key) key_entry.grid(row=0, column=0, padx=(0, 5), sticky="ew") mode_combo = ctk.CTkComboBox( grid_inner, values=["key", "key+value"], width=110, font=self.fonts["ui"] ) mode_combo.grid(row=0, column=1, padx=(0, 5), sticky="w") value_entry = ctk.CTkEntry( grid_inner, placeholder_text="Value (si aplica)", height=28, font=self.fonts["ui"] ) if value: value_entry.insert(0, value) value_entry.grid(row=0, column=2, padx=(0, 5), sticky="ew") def _apply_mode(m: str): m_norm = (m or "").strip().lower() if m_norm in ("key+value", "key + value", "key_value", "key-value"): value_entry.configure(state="normal", placeholder_text="Value (requerido)") else: value_entry.delete(0, "end") value_entry.configure(state="disabled", placeholder_text="Value (no aplica)") def _on_mode_change(choice: str): _apply_mode(choice) try: mode_combo.set(mode if mode in ("key", "key+value") else "key") except Exception: mode_combo.set("key") mode_combo.configure(command=_on_mode_change) _apply_mode(mode_combo.get()) def _remove(): try: for i, r_data in enumerate(list(self._preclass_pattern_rows)): if r_data.get("frame") == row: self._preclass_pattern_rows.pop(i) break except Exception: pass row.destroy() remove_btn = ctk.CTkButton( grid_inner, text="×", width=30, height=28, fg_color=self.colors["danger"], hover_color=self.colors["danger_hover"], command=_remove ) remove_btn.grid(row=0, column=3, sticky="e") self._preclass_pattern_rows.append({ "frame": row, "key_entry": key_entry, "mode_combo": mode_combo, "value_entry": value_entry, "remove_btn": remove_btn, }) def _on_toggle_preclass_range(self): """Muestra u oculta los inputs de rango según el checkbox.""" if self.preclass_range_var.get(): self.preclass_range_inputs_frame.grid(row=0, column=1, padx=10, pady=0, sticky="w") else: self.preclass_range_inputs_frame.grid_forget() def _on_toggle_preclass_target(self): """Muestra u oculta los inputs de archivo objetivo según el checkbox.""" if self.preclass_target_var.get(): self.preclass_target_inputs_frame.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="w") else: self.preclass_target_inputs_frame.grid_forget() # --- API Pública --- def set_on_run(self, callback): """Vincula callback al botón de ejecutar.""" self.preclass_run_btn.configure(command=callback) def set_on_export_excel(self, callback): """Vincula callback al botón de exportar Excel.""" self.preclass_export_btn.configure(command=callback) def set_on_organize_files(self, callback): """Vincula callback al botón de organizar carpetas.""" self.preclass_organize_btn.configure(command=callback) def set_on_select_root(self, callback): """Vincula callback al botón de seleccionar carpeta.""" self.preclass_root_button.configure(command=callback) def get_root_path(self) -> str: """Devuelve la ruta raíz de preclasificación.""" return self.preclass_root_entry.get().strip() def set_root_path(self, path: str) -> None: """Establece la ruta raíz de preclasificación.""" self.preclass_root_entry.configure(state="normal") self.preclass_root_entry.delete(0, "end") self.preclass_root_entry.insert(0, path) def get_mode(self) -> str: """Devuelve el modo de preclasificación: 'radicados' o 'general'.""" try: v = (self.preclass_mode_var.get() or "").strip().lower() return v if v in ("radicados", "general") else "radicados" except Exception: return "radicados" def get_range(self) -> tuple[bool, int, int]: """Devuelve (habilitado, desde, hasta) para preclasificación.""" enabled = self.preclass_range_var.get() try: start = int(self.preclass_range_start_entry.get().strip() or "1") end = int(self.preclass_range_end_entry.get().strip() or "1") except Exception: start, end = 1, 1 return enabled, start, end def get_target_info(self) -> dict: """Devuelve la configuración del archivo objetivo.""" return { "enabled": self.preclass_target_var.get(), "definition": self.preclass_target_def_entry.get().strip(), "pattern": self.preclass_target_pattern_entry.get().strip() } def get_patterns(self) -> list[dict]: """Devuelve la lista de patrones definida.""" out = [] for it in getattr(self, "_preclass_pattern_rows", []): try: key = it["key_entry"].get().strip() mode = (it["mode_combo"].get() or "").strip() value = it["value_entry"].get().strip() if not key: continue if mode == "key+value": out.append({"key": key, "mode": "key+value", "value": value}) else: out.append({"key": key, "mode": "key"}) except Exception: pass return out def append_output(self, text: str) -> None: """Añade texto al área de salida.""" self.preclass_output.insert("end", text + "\n") self.preclass_output.see("end") def append_output_safe(self, text: str) -> None: """Añade texto al área de salida de forma thread-safe.""" self.append_output(text) def enable_actions(self, enabled: bool): """Habilita o deshabilita los botones de acción.""" state = "normal" if enabled else "disabled" self.preclass_export_btn.configure(state=state) self.preclass_organize_btn.configure(state=state) def enable_controls(self, enabled: bool): """Habilita o deshabilita los controles principales.""" state = "normal" if enabled else "disabled" self.preclass_root_button.configure(state=state) self.preclass_root_entry.configure(state=state) self.preclass_range_cb.configure(state=state) self.preclass_range_start_entry.configure(state=state) self.preclass_range_end_entry.configure(state=state) self.preclass_target_cb.configure(state=state) self.preclass_target_def_entry.configure(state=state) self.preclass_target_pattern_entry.configure(state=state) self.radio_radicados.configure(state=state) self.radio_general.configure(state=state) self.add_preclass_pattern_btn.configure(state=state) self.preclass_run_btn.configure(state=state) for row in self._preclass_pattern_rows: row["key_entry"].configure(state=state) row["mode_combo"].configure(state=state) row["value_entry"].configure(state=state) row["remove_btn"].configure(state=state)