diff --git a/__pycache__/config_manager.cpython-310.pyc b/__pycache__/config_manager.cpython-310.pyc index a7dc8c1..ed725ab 100644 Binary files a/__pycache__/config_manager.cpython-310.pyc and b/__pycache__/config_manager.cpython-310.pyc differ diff --git a/app.py b/app.py index f4ea53b..755dca7 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,7 @@ from flask import Flask, render_template, request, jsonify, url_for from flask_sock import Sock from config_manager import ConfigurationManager +from datetime import datetime import os import json # Added import @@ -31,14 +32,32 @@ def handle_websocket(ws): def broadcast_message(message): """Envía un mensaje a todas las conexiones WebSocket activas y guarda en log.""" dead_connections = set() - for ws in websocket_connections: - try: - ws.send(message) - except Exception: - dead_connections.add(ws) + timestamp = datetime.now().strftime("[%H:%M:%S] ") - # Guardar en archivo de log - config_manager.append_log(message) + # Si es una lista de mensajes, procesar cada uno + if isinstance(message, list): + messages = message + else: + # Si es un solo mensaje, dividirlo en líneas + messages = [line.strip() for line in message.splitlines() if line.strip()] + + # Procesar cada mensaje + for msg in messages: + # Verificar si el mensaje ya tiene timestamp + has_timestamp = msg.startswith("[") and "]" in msg.split(" ")[0] + formatted_msg = msg if has_timestamp else f"{timestamp}{msg}" + + # Escribir en el archivo de log + with open(config_manager.log_file, "a", encoding="utf-8") as f: + f.write(f"{formatted_msg}\n") + + # Enviar a todos los clientes WebSocket + for ws in list(websocket_connections): + try: + if ws.connected: + ws.send(f"{formatted_msg}\n") + except Exception: + dead_connections.add(ws) # Limpiar conexiones muertas websocket_connections.difference_update(dead_connections) diff --git a/backend/script_groups/EmailCrono/config.json b/backend/script_groups/EmailCrono/config.json deleted file mode 100644 index f9b1ad9..0000000 --- a/backend/script_groups/EmailCrono/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "input_dir": "C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\Emails", - "output_dir": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-InLavoro\\HENKEL\\93040 - HENKEL - BowlingGreen\\Description\\HENKEL - ALPLA - AUTEFA - Batch Data", - "cronologia_file": "cronologia.md", - "attachments_dir": "adjuntos" -} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/data.json b/backend/script_groups/EmailCrono/data.json index 7cac4f7..f9c99fb 100644 --- a/backend/script_groups/EmailCrono/data.json +++ b/backend/script_groups/EmailCrono/data.json @@ -1,5 +1,4 @@ { "attachments_dir": "adjuntos", - "cronologia_file": "cronologia.md", - "output_dir": "C:\\\\Users\\\\migue\\\\OneDrive\\\\Miguel\\\\Obsidean\\\\Trabajo\\\\VM\\\\04-InLavoro\\\\HENKEL\\\\93040 - HENKEL - BowlingGreen\\\\Description\\\\HENKEL - ALPLA - AUTEFA - Batch Data" + "cronologia_file": "cronologia.md" } \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/esquema_work.json b/backend/script_groups/EmailCrono/esquema_work.json index 76dd02a..1b887a1 100644 --- a/backend/script_groups/EmailCrono/esquema_work.json +++ b/backend/script_groups/EmailCrono/esquema_work.json @@ -1,10 +1,11 @@ { "type": "object", "properties": { - "output_dir": { + "output_directory": { "type": "string", - "title": "Directorio de destino de la cronologia", - "description": "" + "format": "directory", + "title": "Directorio donde escribir el archivo de cronologia", + "description": "Lugar para el archivo cronologia.md" } } } \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/x1.py b/backend/script_groups/EmailCrono/x1.py index 73e2900..0fe3c86 100644 --- a/backend/script_groups/EmailCrono/x1.py +++ b/backend/script_groups/EmailCrono/x1.py @@ -1,70 +1,92 @@ """ -Script para dessasemblar los emails y generar un archivo de texto con la cronología de los mensajes. +Script para desensamblar los emails y generar un archivo de texto con la cronología de los mensajes. """ -# main.py import os +import sys from pathlib import Path from utils.email_parser import procesar_eml from utils.markdown_handler import cargar_cronologia_existente from utils.beautify import BeautifyProcessor -from config.config import Config -import hashlib +import json + +# Forzar UTF-8 en la salida estándar +sys.stdout.reconfigure(encoding="utf-8") + def generar_indice(mensajes): """ Genera una lista de mensajes usando el formato de Obsidian """ indice = "# Índice de Mensajes\n\n" - + for mensaje in mensajes: indice += mensaje.get_index_entry() + "\n" - + indice += "\n---\n\n" return indice + def main(): - config = Config() - + # Cargar configuraciones del entorno + configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}")) + + # Obtener working directory + working_directory = configs.get("working_directory", ".") + + # Obtener configuraciones de nivel 2 (grupo) + group_config = configs.get("level2", {}) + cronologia_file = group_config.get("cronologia_file", "cronologia.md") + attachments_dir = group_config.get("attachments_dir", "adjuntos") + + work_config = configs.get("level3", {}) + outpu_directory = work_config.get("output_directory", ".") + + # Construir rutas absolutas + input_dir = ( + working_directory # El directorio de trabajo es el directorio de entrada + ) + output_file = os.path.join(outpu_directory, cronologia_file) + attachments_path = os.path.join(working_directory, attachments_dir) + # Debug prints - print(f"Input directory: {config.get_input_dir()}") - print(f"Output directory: {config.get_output_dir()}") - print(f"Cronologia file: {config.get_cronologia_file()}") - print(f"Attachments directory: {config.get_attachments_dir()}") - + print(f"Working directory: {working_directory}") + print(f"Input directory: {input_dir}") + print(f"Output directory: {outpu_directory}") + print(f"Cronologia file: {output_file}") + print(f"Attachments directory: {attachments_path}") + # Obtener el directorio donde está el script actual script_dir = os.path.dirname(os.path.abspath(__file__)) - # Construir la ruta al archivo de reglas en el subdirectorio config beautify_rules = os.path.join(script_dir, "config", "beautify_rules.json") beautifier = BeautifyProcessor(beautify_rules) print(f"Beautify rules file: {beautify_rules}") - + # Ensure directories exist - os.makedirs(config.get_output_dir(), exist_ok=True) - os.makedirs(config.get_attachments_dir(), exist_ok=True) + os.makedirs(attachments_path, exist_ok=True) # Check if input directory exists and has files - input_path = Path(config.get_input_dir()) + input_path = Path(input_dir) if not input_path.exists(): print(f"Error: Input directory {input_path} does not exist") return - - eml_files = list(input_path.glob('*.eml')) + + eml_files = list(input_path.glob("*.eml")) print(f"Found {len(eml_files)} .eml files") - + mensajes = [] print(f"Loaded {len(mensajes)} existing messages") mensajes_hash = {msg.hash for msg in mensajes} - + total_procesados = 0 total_nuevos = 0 mensajes_duplicados = 0 for archivo in eml_files: print(f"\nProcessing {archivo}") - nuevos_mensajes = procesar_eml(archivo, config.get_attachments_dir()) + nuevos_mensajes = procesar_eml(archivo, attachments_path) total_procesados += len(nuevos_mensajes) - + # Verificar duplicados y aplicar beautify solo a los mensajes nuevos for msg in nuevos_mensajes: if msg.hash not in mensajes_hash: @@ -76,10 +98,10 @@ def main(): else: mensajes_duplicados += 1 - print(f"\nEstadísticas de procesamiento:") - print(f"- Total mensajes encontrados: {total_procesados}") - print(f"- Mensajes únicos añadidos: {total_nuevos}") - print(f"- Mensajes duplicados ignorados: {mensajes_duplicados}") + print("\nEstadísticas de procesamiento:") + print("- Total mensajes encontrados:", total_procesados) + print("- Mensajes únicos añadidos:", total_nuevos) + print("- Mensajes duplicados ignorados:", mensajes_duplicados) # Ordenar mensajes de más reciente a más antiguo mensajes.sort(key=lambda x: x.fecha, reverse=True) @@ -88,14 +110,14 @@ def main(): indice = generar_indice(mensajes) # Escribir el archivo con el índice y los mensajes - output_file = config.get_cronologia_file() print(f"\nWriting {len(mensajes)} messages to {output_file}") - with open(output_file, 'w', encoding='utf-8') as f: + with open(output_file, "w", encoding="utf-8") as f: # Primero escribir el índice f.write(indice) # Luego escribir todos los mensajes for msg in mensajes: f.write(msg.to_markdown()) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/backend/script_groups/example_group/esquema_group.json b/backend/script_groups/example_group/esquema_group.json new file mode 100644 index 0000000..1c9e43a --- /dev/null +++ b/backend/script_groups/example_group/esquema_group.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} \ No newline at end of file diff --git a/backend/script_groups/example_group/esquema_work.json b/backend/script_groups/example_group/esquema_work.json new file mode 100644 index 0000000..1c9e43a --- /dev/null +++ b/backend/script_groups/example_group/esquema_work.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} \ No newline at end of file diff --git a/config_manager.py b/config_manager.py index ab5fcc0..f776713 100644 --- a/config_manager.py +++ b/config_manager.py @@ -3,6 +3,8 @@ import json import subprocess import re from typing import Dict, Any, List +import time # Add this import +from datetime import datetime # Add this import class ConfigurationManager: @@ -15,6 +17,8 @@ class ConfigurationManager: self.working_directory = None self.log_file = os.path.join(self.data_path, "log.txt") self._init_log_file() + self.last_execution_time = 0 # Add this attribute + self.min_execution_interval = 1 # Minimum seconds between executions def _init_log_file(self): """Initialize log file if it doesn't exist""" @@ -25,13 +29,39 @@ class ConfigurationManager: f.write("") def append_log(self, message: str) -> None: - """Append a message to the log file""" + """Append a message to the log file with timestamp.""" try: - with open(self.log_file, "a", encoding="utf-8") as f: - f.write(message) + timestamp = datetime.now().strftime("[%H:%M:%S] ") + # Filtrar líneas vacías y agregar timestamp solo a líneas con contenido + lines = message.split("\n") + lines_with_timestamp = [] + for line in lines: + if line.strip(): + # Solo agregar timestamp si la línea no tiene ya uno + if not line.strip().startswith("["): + line = f"{timestamp}{line}" + lines_with_timestamp.append(f"{line}\n") + + if lines_with_timestamp: + with open(self.log_file, "a", encoding="utf-8") as f: + f.writelines(lines_with_timestamp) except Exception as e: print(f"Error writing to log file: {e}") + def read_last_log_line(self) -> str: + """Read the last line from the log file.""" + try: + with open(self.log_file, "r", encoding="utf-8") as f: + # Leer las últimas líneas y encontrar la última no vacía + lines = f.readlines() + for line in reversed(lines): + if line.strip(): + return line + return "" + except Exception as e: + print(f"Error reading last log line: {e}") + return "" + def read_log(self) -> str: """Read the entire log file""" try: @@ -124,7 +154,9 @@ class ConfigurationManager: if level == "1": path = os.path.join(self.data_path, "esquema_general.json") elif level == "2": - path = os.path.join(self.script_groups_path, group, "esquema_group.json") + path = os.path.join( + self.script_groups_path, group, "esquema_group.json" + ) elif level == "3": if not group: return {"type": "object", "properties": {}} @@ -331,64 +363,89 @@ class ConfigurationManager: self, group: str, script_name: str, broadcast_fn=None ) -> Dict[str, Any]: """Execute script with real-time logging via WebSocket broadcast function.""" - script_path = os.path.join(self.script_groups_path, group, script_name) - if not os.path.exists(script_path): - return {"error": "Script not found"} + # Check execution throttling + current_time = time.time() + time_since_last = current_time - self.last_execution_time - # Obtener el directorio de trabajo del grupo + if time_since_last < self.min_execution_interval: + if broadcast_fn: + broadcast_fn( + f"Por favor espere {self.min_execution_interval} segundo(s) entre ejecuciones" + ) + return { + "status": "throttled", + "error": f"Por favor espere {self.min_execution_interval} segundo(s) entre ejecuciones", + } + + self.last_execution_time = current_time + script_path = os.path.join(self.script_groups_path, group, script_name) + + if not os.path.exists(script_path): + if broadcast_fn: + broadcast_fn("Error: Script no encontrado") + return {"status": "error", "error": "Script not found"} + + # Get working directory working_dir = self.get_work_dir(group) if not working_dir: - return {"error": "Working directory not set for this script group"} + if broadcast_fn: + broadcast_fn("Error: Directorio de trabajo no configurado") + return {"status": "error", "error": "Working directory not set"} + # Prepare environment configurations configs = { "level1": self.get_config("1"), "level2": self.get_config("2", group), - "level3": self.get_config("3") if self.working_directory else {}, + "level3": self.get_config("3", group) if working_dir else {}, + "working_directory": working_dir, } try: if broadcast_fn: - broadcast_fn(f"\nIniciando ejecución de {script_name}...\n") + broadcast_fn(f"Iniciando ejecución de {script_name}") + # Execute script with configured environment process = subprocess.Popen( ["python", script_path], - cwd=working_dir or self.base_path, + cwd=working_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, - env=dict(os.environ, **{"SCRIPT_CONFIGS": json.dumps(configs)}), + env=dict( + os.environ, + SCRIPT_CONFIGS=json.dumps(configs), + PYTHONIOENCODING="utf-8", + ), ) - output = [] + # Stream output in real-time while True: line = process.stdout.readline() if not line and process.poll() is not None: break - if line: - output.append(line) - if broadcast_fn: - broadcast_fn(line) + if line and broadcast_fn: + broadcast_fn(line.rstrip()) + # Handle any errors stderr = process.stderr.read() - if stderr: - if broadcast_fn: - broadcast_fn(f"\nERROR: {stderr}\n") - output.append(f"ERROR: {stderr}") + if stderr and broadcast_fn: + broadcast_fn(f"ERROR: {stderr.strip()}") + # Signal completion if broadcast_fn: - broadcast_fn("\nEjecución completada.\n") + broadcast_fn("Ejecución completada") return { - "output": "".join(output), - "error": stderr if stderr else None, "status": "success" if process.returncode == 0 else "error", + "error": stderr if stderr else None, } + except Exception as e: error_msg = str(e) if broadcast_fn: - broadcast_fn(f"\nError inesperado: {error_msg}\n") - return {"error": error_msg} + broadcast_fn(f"Error inesperado: {error_msg}") + return {"status": "error", "error": error_msg} def get_work_dir(self, group: str) -> str: """Get working directory path for a script group.""" diff --git a/data/log.txt b/data/log.txt index 2fdfa8f..dbff567 100644 --- a/data/log.txt +++ b/data/log.txt @@ -1,62 +1,26 @@ - -Iniciando ejecución de x1.py... -=== Ejecutando Script de Prueba 1 === - -Configuraciones cargadas: -Nivel 1: { - "api_key": "your-api-key-here", - "model": "gpt-3.5-turbo" -} -Nivel 2: { - "input_dir": "D:/Datos/Entrada", - "output_dir": "D:/Datos/Salida", - "batch_size": 50 -} -Nivel 3: { - "campo_1739099176331": "", - "debug_mode": false, - "process_type": "basic", - "project_name": "Test2" -} - -Simulando procesamiento... -Progreso: 20% -Progreso: 40% -Progreso: 60% -Progreso: 80% -Progreso: 100% - -¡Proceso completado! - -Ejecución completada. - -Iniciando ejecución de x1.py... -=== Ejecutando Script de Prueba 1 === - -Configuraciones cargadas: -Nivel 1: { - "api_key": "your-api-key-here", - "model": "gpt-3.5-turbo" -} -Nivel 2: { - "input_dir": "D:/Datos/Entrada", - "output_dir": "D:/Datos/Salida", - "batch_size": 50 -} -Nivel 3: { - "campo_1739099176331": "", - "debug_mode": true, - "process_type": "basic", - "project_name": "Test2" -} - -Simulando procesamiento... -Progreso: 20% -Progreso: 40% -Progreso: 60% -Progreso: 80% -Progreso: 100% - -¡Proceso completado! - -Ejecución completada. +[19:32:18] Iniciando ejecución de x1.py +[19:32:18] === Ejecutando Script de Prueba 1 === +[19:32:18] Configuraciones cargadas: +[19:32:18] Nivel 1: { +[19:32:18] "api_key": "your-api-key-here", +[19:32:18] "model": "gpt-3.5-turbo" +[19:32:18] } +[19:32:18] Nivel 2: { +[19:32:18] "input_dir": "D:/Datos/Entrada", +[19:32:18] "output_dir": "D:/Datos/Salida", +[19:32:18] "batch_size": 50 +[19:32:18] } +[19:32:18] Nivel 3: { +[19:32:18] "campo_1739099176331": "", +[19:32:18] "debug_mode": true, +[19:32:18] "process_type": "basic", +[19:32:18] "project_name": "Test2" +[19:32:18] } +[19:32:18] Simulando procesamiento... +[19:32:19] Progreso: 20% +[19:32:20] Progreso: 40% +[19:32:21] Progreso: 60% +[19:32:22] Progreso: 80% +[19:32:23] Progreso: 100% +[19:32:23] ¡Proceso completado! +[19:32:23] Ejecución completada diff --git a/static/js/scripts.js b/static/js/scripts.js index b73a144..3520199 100644 --- a/static/js/scripts.js +++ b/static/js/scripts.js @@ -18,32 +18,44 @@ function initWebSocket() { // Load configurations for all levels async function loadConfigs() { - const group = document.getElementById('script-group').value; - currentGroup = group; - console.log('Loading configs for group:', group); // Debug line + const group = currentGroup; + console.log('Loading configs for group:', group); + if (!group) { + console.error('No group selected'); + return; + } + try { // Cargar niveles 1 y 2 for (let level of [1, 2]) { - console.log(`Loading level ${level} config...`); // Debug line + console.log(`Loading level ${level} config...`); const response = await fetch(`/api/config/${level}?group=${group}`); + if (!response.ok) throw new Error(`Error loading level ${level} config`); const data = await response.json(); - console.log(`Level ${level} data:`, data); // Debug line + console.log(`Level ${level} data:`, data); await renderForm(`level${level}-form`, data); } // Cargar nivel 3 solo si hay directorio de trabajo const workingDirResponse = await fetch(`/api/working-directory/${group}`); const workingDirResult = await workingDirResponse.json(); + if (workingDirResult.status === 'success' && workingDirResult.path) { - console.log('Loading level 3 config...'); // Debug line + console.log('Loading level 3 config...'); const response = await fetch(`/api/config/3?group=${group}`); + if (!response.ok) throw new Error('Error loading level 3 config'); const data = await response.json(); - console.log('Level 3 data:', data); // Debug line + console.log('Level 3 data:', data); await renderForm('level3-form', data); + + // Actualizar input del directorio de trabajo + document.getElementById('working-directory').value = workingDirResult.path; } + // Cargar scripts disponibles await loadScripts(group); + } catch (error) { console.error('Error loading configs:', error); } @@ -98,17 +110,34 @@ async function renderForm(containerId, data) { return; } + // Guardar el estado del botón si existe + const existingButton = document.getElementById(`save-config-${level}`); + const buttonState = existingButton ? { + text: existingButton.innerText, + className: existingButton.className, + disabled: existingButton.disabled + } : null; + container.innerHTML = `
${generateFormFields(schema, data || {}, '', level)}
-
`; + + // Restaurar el estado del botón si existía + if (buttonState) { + const newButton = document.getElementById(`save-config-${level}`); + newButton.innerText = buttonState.text; + newButton.className = buttonState.className; + newButton.disabled = buttonState.disabled; + } + } catch (error) { console.error(`Error rendering form ${containerId}:`, error); container.innerHTML = '

Error cargando el esquema.

'; @@ -163,6 +192,18 @@ function generateInputField(def, key, value, level) { switch (def.type) { case 'string': + if (def.format === 'directory') { + return `
+ + +
`; + } if (def.enum) { return ` - + + @@ -337,7 +400,7 @@ function updateFieldType(select) { div.innerHTML = ` + onchange="updateVisualSchema()"> `; fieldContainer.appendChild(div); } @@ -387,21 +450,29 @@ function updateVisualSchema() { const select = field.getElementsByTagName('select')[0]; const key = inputs[0].value; - schema.properties[key] = { - type: select.value === 'enum' ? 'string' : select.value, - title: inputs[1].value, - description: inputs[2].value - }; - - if (select.value === 'enum') { - const textarea = field.getElementsByTagName('textarea')[0]; - if (textarea) { - schema.properties[key].enum = textarea.value.split('\n').filter(v => v.trim()); - } + if (select.value === 'directory') { + schema.properties[key] = { + type: 'string', + format: 'directory', + title: inputs[1].value, + description: inputs[2].value + }; + } else if (select.value === 'enum') { + schema.properties[key] = { + type: 'string', + title: inputs[1].value, + description: inputs[2].value, + enum: field.querySelector('textarea').value.split('\n').filter(v => v.trim()) + }; + } else { + schema.properties[key] = { + type: select.value, + title: inputs[1].value, + description: inputs[2].value + }; } }); - // Actualizar el JSON editor directamente const jsonEditor = document.getElementById('json-editor'); if (jsonEditor) { jsonEditor.value = JSON.stringify(schema, null, 2); @@ -562,50 +633,59 @@ async function loadStoredLogs() { // Initialize on page load async function initializeApp() { try { + // Inicializar WebSocket initWebSocket(); - await loadStoredLogs(); // Cargar logs almacenados + await loadStoredLogs(); - // Primero establecer el grupo actual - const group = localStorage.getItem('selectedGroup'); + // Configurar grupo actual const selectElement = document.getElementById('script-group'); - if (group) { - selectElement.value = group; + currentGroup = localStorage.getItem('selectedGroup') || selectElement.value; + + // Actualizar el select con el valor guardado + if (currentGroup) { + selectElement.value = currentGroup; } - currentGroup = selectElement.value; // Siempre establecer currentGroup con el valor actual del select - console.log('Current group initialized as:', currentGroup); // Debug line - updateGroupDescription(); // Actualizar descripción inicial + + // Limpiar evento anterior si existe + selectElement.removeEventListener('change', handleGroupChange); - // Configurar el evento de cambio de grupo - selectElement.addEventListener('change', async (e) => { - currentGroup = e.target.value; - localStorage.setItem('selectedGroup', e.target.value); - console.log('Group changed to:', currentGroup); // Debug line - updateGroupDescription(); // Actualizar descripción al cambiar - await initWorkingDirectory(); - await loadConfigs(); - }); + // Agregar el nuevo manejador de eventos + selectElement.addEventListener('change', handleGroupChange); - // Luego cargar el directorio de trabajo + // Cargar datos iniciales + updateGroupDescription(); await initWorkingDirectory(); - - // Finalmente cargar las configuraciones await loadConfigs(); + + } catch (error) { + console.error('Error during initialization:', error); + } +} + +// Separar la lógica del cambio de grupo en una función +async function handleGroupChange(e) { + try { + currentGroup = e.target.value; + localStorage.setItem('selectedGroup', currentGroup); + console.log('Group changed to:', currentGroup); - // Configurar el evento de cambio de grupo - selectElement.addEventListener('change', async (e) => { - currentGroup = e.value; - localStorage.setItem('selectedGroup', e.value); - console.log('Group changed to:', currentGroup); // Debug line - await initWorkingDirectory(); - await loadConfigs(); + // Limpiar formularios existentes + ['level1-form', 'level2-form', 'level3-form'].forEach(id => { + const element = document.getElementById(id); + if (element) element.innerHTML = ''; }); - // Close sidebar on small screens when changing groups + // Actualizar la interfaz + updateGroupDescription(); + await initWorkingDirectory(); + await loadConfigs(); + + // Cerrar sidebar en móviles if (window.innerWidth < 768) { toggleSidebar(); } } catch (error) { - console.error('Error during initialization:', error); + console.error('Error in handleGroupChange:', error); } } @@ -791,35 +871,19 @@ function collectFormData(level) { // Agregar función para guardar configuración async function saveConfig(level) { - try { - const form = document.getElementById(`config-form-${level}`); - const formData = {}; - - // Recolectar datos de todos los inputs en el formulario - form.querySelectorAll('input, select').forEach(input => { - const key = input.getAttribute('data-key'); - if (!key) return; - - let value; - if (input.type === 'checkbox') { - value = input.checked; - } else if (input.type === 'number') { - value = Number(input.value); - } else { - value = input.value; - } - - // Manejar claves anidadas (por ejemplo: "parent.child") - const keys = key.split('.'); - let current = formData; - for (let i = 0; i < keys.length - 1; i++) { - current[keys[i]] = current[keys[i]] || {}; - current = current[keys[i]]; - } - current[keys[keys.length - 1]] = value; - }); + const saveButton = document.getElementById(`save-config-${level}`); + if (!saveButton || saveButton.disabled) return; // Evitar múltiples envíos + + const originalText = saveButton.innerText; + const originalClasses = 'bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded transition-colors duration-300'; + + try { + saveButton.disabled = true; + saveButton.className = 'bg-yellow-500 text-white px-4 py-2 rounded cursor-wait transition-colors duration-300'; + saveButton.innerText = 'Guardando...'; + + const formData = collectFormData(level); - // Enviar datos al servidor const response = await fetch(`/api/config/${level}?group=${currentGroup}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -832,16 +896,30 @@ async function saveConfig(level) { const result = await response.json(); if (result.status === 'success') { - alert('Configuración guardada correctamente'); - // Recargar el formulario para mostrar los datos actualizados - const configResponse = await fetch(`/api/config/${level}?group=${currentGroup}`); - const updatedData = await configResponse.json(); - await renderForm(`level${level}-form`, updatedData); + saveButton.className = 'bg-green-500 text-white px-4 py-2 rounded transition-colors duration-300'; + saveButton.innerText = '¡Guardado con Éxito!'; + + setTimeout(() => { + if (saveButton) { + saveButton.className = originalClasses; + saveButton.innerText = originalText; + saveButton.disabled = false; + } + }, 2000); } else { throw new Error(result.message || 'Error desconocido'); } } catch (error) { console.error('Error saving config:', error); - alert('Error guardando la configuración: ' + error.message); + saveButton.className = 'bg-red-500 text-white px-4 py-2 rounded transition-colors duration-300'; + saveButton.innerText = 'Error al Guardar'; + + setTimeout(() => { + if (saveButton) { + saveButton.className = originalClasses; + saveButton.innerText = originalText; + saveButton.disabled = false; + } + }, 2000); } } \ No newline at end of file