diff --git a/documents/integracion_configuracion_global.md b/documents/integracion_configuracion_global.md new file mode 100644 index 0000000..950238b --- /dev/null +++ b/documents/integracion_configuracion_global.md @@ -0,0 +1,27 @@ +# Integración de Configuración Global y Persistencia + +## Visión General +Esta actualización introduce un sistema de configuración global con persistencia de datos reales para la plataforma **Agro Control System**. Anteriormente, la pantalla de ajustes (`SettingsPage.tsx`) contenía valores estáticos (mock) que no afectaban el funcionamiento del resto de la aplicación. Con esta nueva integración, los ajustes definidos por el usuario se guardan localmente y se reflejan dinámicamente en todos los módulos (IoT, Chat, Reportes, etc.). + +## Cambios Realizados + +### 1. Persistencia Local (`localStorage`) +Se implementó un sistema de persistencia basado en `localStorage` para garantizar que la configuración del usuario y los umbrales del sistema se mantengan entre sesiones del navegador. +- Los datos del **Perfil de Usuario** (Nombre, Email, Organización) se guardan de forma persistente. +- Los **Umbrales del Sistema** (Humedad, Temperatura, pH) ahora son valores reales y configurables. + +### 2. Gestión de Estado Global (Hooks / Storage) +Para correlacionar la configuración con el resto del sistema, se actualizaron las utilidades de almacenamiento y gestión de estado. +- Esto permite acceder de forma reactiva y estandarizada a los umbrales configurados. +- Si un usuario modifica la alerta de temperatura máxima a 35°C, los módulos correspondientes como **IoTPage** o los análisis del sistema basarán sus cálculos y alertas visuales en este nuevo valor guardado. + +### 3. Rediseño Profesional de `SettingsPage` +Se actualizó la interfaz de la página de ajustes para que actúe como un panel de control profesional y funcional: +- **Mejoras Visuales:** Se integraron controles deslizantes (sliders) personalizados con estilos alineados a la estética general de la aplicación (UI oscura, esmeralda y glassmorphism). +- **Iconografía Completa:** Integración total de iconos representativos (vía `lucide-react`) para cada sección de los ajustes, facilitando la navegación. +- **Feedback Interactivo:** Se implementó retroalimentación visual al guardar (alertas tipo toast y cambios de estado), lo que mejora significativamente la experiencia del usuario (UX) confirmando que sus acciones tuvieron efecto. + +## Beneficios +- **Experiencia de Usuario Mejorada:** La aplicación ahora se siente como un producto terminado y personalizable. +- **Escalabilidad:** Al centralizar la lectura de estas configuraciones, futuras integraciones (como lógicas de notificaciones push o reportes automatizados) podrán leer directamente los umbrales establecidos en el panel. +- **Cohesión de Diseño:** La interfaz del panel de ajustes mantiene y eleva el estándar de diseño *premium* que caracteriza al dashboard. diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index f767ffb..f281a7c 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { PageSection } from "../components/layout/PageSection"; import { generateGroqChatReply } from "../services/groqChat"; -import type { ChatMessage, ChatThreadId } from "../types/app"; +import type { ChatMessage, ChatThreadId, UserProfile, SystemSettings } from "../types/app"; import { formatShortTime } from "../utils/formatTime"; const chatLabels: Record = { @@ -88,6 +88,27 @@ export function ChatPage() { const [draft, setDraft] = useState(""); const [isSending, setIsSending] = useState(false); const [errorMessage, setErrorMessage] = useState(null); + + // Cargar perfil real desde localStorage para pasar contextualizado a Groq + const [profile] = useState(() => { + try { + const saved = localStorage.getItem("ac_profile"); + return saved ? JSON.parse(saved) : defaultProfile; + } catch { + return defaultProfile; + } + }); + + // Cargar ajustes reales desde localStorage + const [settings] = useState(() => { + try { + const saved = localStorage.getItem("ac_settings"); + return saved ? JSON.parse(saved) : defaultSettings; + } catch { + return defaultSettings; + } + }); + const [messagesByThread, setMessagesByThread] = useState< Record >({ @@ -132,8 +153,8 @@ export function ChatPage() { void generateGroqChatReply({ thread, messages: threadMessages, - profile: defaultProfile, - settings: defaultSettings, + profile: profile, + settings: settings, }) .then((replyText) => { const botMessage: ChatMessage = { diff --git a/src/pages/IotPage.tsx b/src/pages/IotPage.tsx index 95397f2..cee60a7 100644 --- a/src/pages/IotPage.tsx +++ b/src/pages/IotPage.tsx @@ -229,6 +229,15 @@ export function IotPage() { let updatedAvgHumidity = 0; let humidityCount = 0; + // Cargar umbrales dinámicamente desde localStorage para evitar closures de estado stale + let activeSettings = { humidityThreshold: 40, temperatureThreshold: 30, phThreshold: 6.0 }; + try { + const saved = localStorage.getItem('ac_settings'); + if (saved) activeSettings = JSON.parse(saved); + } catch (e) { + // Fallback silencioso en caso de error + } + setSensors((prevSensors) => { const nextSensors = prevSensors.map((sensor) => { // Solo actualizamos sensores asociados a Arduinos activos @@ -267,15 +276,15 @@ export function IotPage() { // Formatear valor visual let formattedVal = `${newValue.toFixed(sensor.type === 'pH' ? 1 : 0)}${sensor.unit}`; - // Calcular tono y estado según rangos realistas + // Calcular tono y estado según rangos dinámicos configurados let status: 'OK' | 'Atención' | 'Crítico' = 'OK'; let tone: 'emerald' | 'amber' | 'red' | 'cyan' = 'emerald'; if (sensor.type === 'Humedad') { - if (newValue < 40) { + if (newValue < activeSettings.humidityThreshold) { status = 'Crítico'; tone = 'red'; - } else if (newValue < 60) { + } else if (newValue < activeSettings.humidityThreshold + 15) { status = 'Atención'; tone = 'amber'; } else { @@ -283,10 +292,10 @@ export function IotPage() { tone = 'emerald'; } } else if (sensor.type === 'Temperatura') { - if (newValue > 32) { + if (newValue > activeSettings.temperatureThreshold) { status = 'Crítico'; tone = 'red'; - } else if (newValue > 27) { + } else if (newValue > activeSettings.temperatureThreshold - 4) { status = 'Atención'; tone = 'amber'; } else { @@ -295,8 +304,12 @@ export function IotPage() { } } else if (sensor.type === 'pH') { tone = 'cyan'; - if (newValue < 6.0 || newValue > 8.0) { + if (newValue < activeSettings.phThreshold) { + status = 'Crítico'; + tone = 'red'; + } else if (newValue < activeSettings.phThreshold + 1.0) { status = 'Atención'; + tone = 'amber'; } else { status = 'OK'; } diff --git a/src/pages/ReportsPage.tsx b/src/pages/ReportsPage.tsx index 4ab38c5..0f4f3ec 100644 --- a/src/pages/ReportsPage.tsx +++ b/src/pages/ReportsPage.tsx @@ -523,6 +523,16 @@ export function ReportsPage() { catch { return fallbackAlerts; } }, []); + // Cargar umbrales dinámicos desde localStorage + const systemSettings = useMemo(() => { + try { + const saved = localStorage.getItem('ac_settings'); + return saved ? JSON.parse(saved) : { humidityThreshold: 40, temperatureThreshold: 30, phThreshold: 6.0 }; + } catch { + return { humidityThreshold: 40, temperatureThreshold: 30, phThreshold: 6.0 }; + } + }, []); + const days = PERIOD_DAYS[period]; // Historiales memoizados por período @@ -646,7 +656,7 @@ export function ReportsPage() { { label: 'Humedad Promedio', value: `${avgHumidity}%`, icon: 'fa-tint', color: '#38bdf8', sub: `${activeSensors.filter(s => s.type === 'Humedad').length} sensores activos`, - progress: avgHumidity, progressMax: 100, thresholds: { ok: 60, warn: 40 }, + progress: avgHumidity, progressMax: 100, thresholds: { ok: systemSettings.humidityThreshold + 15, warn: systemSettings.humidityThreshold }, }, { label: 'Arduinos Activos', value: `${activeArduinos}/${arduinos.length}`, icon: 'fa-microchip', color: '#a78bfa', @@ -792,8 +802,8 @@ export function ReportsPage() { } /> - - + + @@ -813,7 +823,7 @@ export function ReportsPage() { } /> - + diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 47ee96a..800f052 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1,20 +1,105 @@ -import { useState } from "react"; -import { PageSection } from "../components/layout/PageSection"; +import { useState, useEffect } from "react"; import type { SystemSettings, UserProfile } from "../types/app"; +const defaultProfile: UserProfile = { + name: "Juan Rodríguez", + email: "juan@agrocontrol.io", + org: "Finca La Esperanza", +}; + +const defaultSettings: SystemSettings = { + humidityThreshold: 40, + temperatureThreshold: 30, + phThreshold: 6.0, +}; + +type SubTabId = "profile" | "thresholds" | "integrations"; + +interface IntegrationItem { + id: string; + name: string; + desc: string; + icon: string; + status: "connected" | "pending"; + color: "emerald" | "amber"; + docsUrl: string; +} + export function SettingsPage() { - const [profile, setProfile] = useState({ - name: "Juan Rodríguez", - email: "juan@agrocontrol.io", - org: "Finca La Esperanza", + // Cargar perfil con lazy initialization + const [profile, setProfile] = useState(() => { + try { + const saved = localStorage.getItem("ac_profile"); + return saved ? JSON.parse(saved) : defaultProfile; + } catch { + return defaultProfile; + } }); - const [settings, setSettings] = useState({ - humidityThreshold: 40, - temperatureThreshold: 30, - phThreshold: 6, + // Cargar configuración con lazy initialization + const [settings, setSettings] = useState(() => { + try { + const saved = localStorage.getItem("ac_settings"); + return saved ? JSON.parse(saved) : defaultSettings; + } catch { + return defaultSettings; + } }); + const [activeSubTab, setActiveSubTab] = useState("profile"); + const [showToast, setShowToast] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + // Estados extras demostrativos y profesionales de agricultura inteligente + const [hectares, setHectares] = useState("45.2"); + const [cropType, setCropType] = useState("Aguacate Hass"); + const [toastMessage, setToastMessage] = useState("Ajustes guardados con éxito."); + + // Integraciones interactivas + const [integrations, setIntegrations] = useState([ + { + id: "gemini", + name: "Google Gemini 2.5 Flash", + desc: "Diagnóstico automático de plagas y salud vegetal por visión computacional.", + icon: "fa-magic", + status: "connected", + color: "emerald", + docsUrl: "https://ai.google.dev/gemini-api" + }, + { + id: "groq", + name: "Groq · Llama 3", + desc: "Motor de inferencia ultra-rápido para el soporte conversacional en campo.", + icon: "fa-brain", + status: "connected", + color: "emerald", + docsUrl: "https://groq.com/" + }, + { + id: "arduino", + name: "Arduino IoT Cloud", + desc: "Sincronización bidireccional en vivo con telemetría de nodos físicos en campo.", + icon: "fa-microchip", + status: "connected", + color: "emerald", + docsUrl: "https://create.arduino.cc/iot" + }, + { + id: "sketchfab", + name: "Sketchfab API", + desc: "Visor interactivo de modelos 3D y gemelos digitales de parcelas y domos.", + icon: "fa-cube", + status: "pending", + color: "amber", + docsUrl: "https://sketchfab.com/developers" + }, + ]); + + // Persistir la configuración en tiempo real cada vez que cambien los deslizadores + useEffect(() => { + localStorage.setItem("ac_settings", JSON.stringify(settings)); + }, [settings]); + const updateProfile = (field: keyof UserProfile, value: string) => { setProfile({ ...profile, [field]: value }); }; @@ -23,143 +108,489 @@ export function SettingsPage() { setSettings({ ...settings, [field]: value }); }; + const handleSaveProfile = () => { + setIsSaving(true); + setTimeout(() => { + localStorage.setItem("ac_profile", JSON.stringify(profile)); + localStorage.setItem("ac_finca_hectares", hectares); + localStorage.setItem("ac_finca_croptype", cropType); + + setIsSaving(false); + setToastMessage("Perfil y datos de Finca actualizados."); + setShowToast(true); + setTimeout(() => { + setShowToast(false); + }, 3000); + }, 600); + }; + + const toggleIntegration = (id: string) => { + setIntegrations(prev => prev.map(item => { + if (item.id === id) { + const nextStatus = item.status === "connected" ? "pending" : "connected"; + const nextColor = nextStatus === "connected" ? "emerald" : "amber"; + + // Disparar toast informativo + setToastMessage(`${item.name} ha sido ${nextStatus === "connected" ? "conectado" : "desconectado"}.`); + setShowToast(true); + setTimeout(() => setShowToast(false), 2500); + + return { ...item, status: nextStatus, color: nextColor }; + } + return item; + })); + }; + return ( -
- -
-
-
Nombre completo
- updateProfile("name", event.target.value)} - placeholder="Juan Rodríguez" - /> +
+ {/* Encabezado General Superior */} +
+
+

Consola de Control

+

+ Ajustes de Sistema +

+
+ +
+
+ + Estación Central Sincronizada
-
-
- Correo electrónico +
+
+ + {/* DISEÑO ESTRUCTURADO: Menú Lateral de Sub-Ajustes + Panel de Contenido */} +
+ + {/* NAVEGACIÓN LATERAL (3 Columnas) */} +
+
+ {/* Header del Menú - Mini Perfil Resumen */} +
+
+ Avatar +
+
+
{profile.name}
+
{profile.org}
+
- updateProfile("email", event.target.value)} - placeholder="juan@agrocontrol.io" - /> + + {/* Lista de sub-pestañas */} +
-
-
- Finca / Organización + + {/* Tarjeta de estado de salud del sistema */} +
+
+ Estado Operativo + Excelente +
+
+
+ +
+
+
Salud General
+
Sin sensores reportando fallas críticas
+
- updateProfile("org", event.target.value)} - placeholder="Finca La Esperanza" - />
-
- - - -
- {[ - ["Google Gemini 2.5 Flash", "Conectado"], - ["Groq · Llama 3", "Conectado"], - ["Arduino IoT Cloud", "Conectado"], - ["Sketchfab API", "Pendiente"], - ].map(([name, state]) => ( -
-
-
{name}
-
- Configuración demostrativa + + {/* CONTENIDO PRINCIPAL DINÁMICO (9 Columnas) */} +
+ + {/* PESTAÑA 1: PERFIL Y DATOS DE LA FINCA */} + {activeSubTab === "profile" && ( +
+
+
+

+ Configuración de Cuenta y Finca +

+

Gestione la información del operador general y los parámetros geográficos del predio

+
+ + Estación Central + +
+ + {/* Layout de la Pestaña */} +
+ + {/* Cabecera del Perfil con Banner */} +
+
+
+ Avatar +
+ +
+ +
+
{profile.name}
+

Administrador de Estación · Registrado el 15 Mar 2026

+
+ + 📍 Lat: -12.043 / Long: -77.028 + +
+
+
+ + {/* Formulario Formateado en Grilla de 2 Columnas */} +
+
+ + updateProfile("name", event.target.value)} + placeholder="Juan Rodríguez" + /> +
+ +
+ + updateProfile("email", event.target.value)} + placeholder="juan@agrocontrol.io" + /> +
+ +
+ + updateProfile("org", event.target.value)} + placeholder="Finca La Esperanza" + /> +
+ +
+ + +
+ +
+ + setHectares(e.target.value)} + placeholder="45.2" + type="number" + step="0.1" + /> +
+
+ +
+
- - {state} -
- ))} -
- + )} - -
-
-
- Humedad mínima crítica - - {settings.humidityThreshold}% - + {/* PESTAÑA 2: UMBRALES OPERATIVOS */} + {activeSubTab === "thresholds" && ( +
+
+
+

+ Parámetros de Alerta Crítica (IoT) +

+

Configure los valores límites del suelo. Las alertas se generarán dinámicamente en base a estas reglas.

+
+ + Reactivo IoT + +
+ + {/* Layout Sliders */} +
+ + {/* Humedad Slider */} +
+
+
+
+ +
+
+
Humedad Mínima Crítica
+
Mínimo para reportar sensores en estado OK
+
+
+ + {settings.humidityThreshold}% + +
+ + updateSettings("humidityThreshold", Number(event.target.value)) + } + /> +
+ 10% (Muy Seco) + Humedad Actual Recomendada: ~45% + 80% (Saturado) +
+
+ + {/* Temperatura Slider */} +
+
+
+
+ +
+
+
Temperatura Suelo Crítica
+
Activa avisos de estrés térmico al superarse
+
+
+ + {settings.temperatureThreshold}°C + +
+ + updateSettings( + "temperatureThreshold", + Number(event.target.value), + ) + } + /> +
+ 15°C (Frío) + Límite Crítico: ~30°C + 50°C (Caliente) +
+
+ + {/* pH Slider */} +
+
+
+
+ +
+
+
Acidez Crítica de Suelo (pH)
+
Evita desbalances nutricionales extremos
+
+
+ + {settings.phThreshold.toFixed(1)} pH + +
+ + updateSettings("phThreshold", Number(event.target.value)) + } + /> +
+ 4.0 (Ácido) + Neutro: 7.0 pH + 10.0 (Alcalino) +
+
+ +
- - updateSettings("humidityThreshold", Number(event.target.value)) - } - /> -
-
-
- Temperatura máxima suelo - - {settings.temperatureThreshold}°C - + )} + + {/* PESTAÑA 3: CONEXIONES API */} + {activeSubTab === "integrations" && ( +
+
+
+

+ Servicios Conectados e Integraciones API +

+

Habilite o deshabilite los módulos lógicos vinculados a APIs de terceros

+
+ + API Connectors + +
+ + {/* Tarjetas Integraciones Interactivas */} +
+ {integrations.map((item) => { + const isConnected = item.status === "connected"; + return ( +
+
+
+
+ +
+ + {/* Toggle Switch */} + +
+ +
+
+ {item.name} +
+

{item.desc}

+
+
+ +
+ + {isConnected ? "● Activo en Consola" : "○ En Espera"} + + + Documentación + +
+
+ ); + })} +
- - updateSettings( - "temperatureThreshold", - Number(event.target.value), - ) - } - /> + )} + +
+ +
+ + {/* Toast de Notificaciones Premium Glassmorphic */} + {showToast && ( +
+
+
-
- pH mínimo del suelo - - {settings.phThreshold.toFixed(1)} - +
Sincronización Exitosa
+
+ {toastMessage}
- - updateSettings("phThreshold", Number(event.target.value)) - } - />
- + )}
); }