Skip to content
27 changes: 27 additions & 0 deletions documents/integracion_configuracion_global.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 24 additions & 3 deletions src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
@@ -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<ChatThreadId, string> = {
Expand Down Expand Up @@ -88,6 +88,27 @@ export function ChatPage() {
const [draft, setDraft] = useState("");
const [isSending, setIsSending] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

// Cargar perfil real desde localStorage para pasar contextualizado a Groq
const [profile] = useState<UserProfile>(() => {
try {
const saved = localStorage.getItem("ac_profile");
return saved ? JSON.parse(saved) : defaultProfile;
} catch {
return defaultProfile;
}
});

// Cargar ajustes reales desde localStorage
const [settings] = useState<SystemSettings>(() => {
try {
const saved = localStorage.getItem("ac_settings");
return saved ? JSON.parse(saved) : defaultSettings;
} catch {
return defaultSettings;
}
});

const [messagesByThread, setMessagesByThread] = useState<
Record<ChatThreadId, ChatMessage[]>
>({
Expand Down Expand Up @@ -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 = {
Expand Down
25 changes: 19 additions & 6 deletions src/pages/IotPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -267,26 +276,26 @@ 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 {
status = 'OK';
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 {
Expand All @@ -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';
}
Expand Down
18 changes: 14 additions & 4 deletions src/pages/ReportsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -792,8 +802,8 @@ export function ReportsPage() {
<XAxis dataKey="day" stroke="#475569" fontSize={9} tickLine={false} axisLine={false} />
<YAxis stroke="#475569" fontSize={9} tickLine={false} axisLine={false} domain={[20, 100]} />
<Tooltip content={<CustomTooltip />} />
<ReferenceLine y={60} stroke="#38bdf8" strokeDasharray="4 2" strokeOpacity={0.3} label={{ value: 'Óptimo', position: 'right', fill: '#38bdf8', fontSize: 9 }} />
<ReferenceLine y={40} stroke="#ef4444" strokeDasharray="4 2" strokeOpacity={0.3} label={{ value: 'Crítico', position: 'right', fill: '#ef4444', fontSize: 9 }} />
<ReferenceLine y={systemSettings.humidityThreshold + 15} stroke="#38bdf8" strokeDasharray="4 2" strokeOpacity={0.3} label={{ value: 'Óptimo', position: 'right', fill: '#38bdf8', fontSize: 9 }} />
<ReferenceLine y={systemSettings.humidityThreshold} stroke="#ef4444" strokeDasharray="4 2" strokeOpacity={0.3} label={{ value: 'Crítico', position: 'right', fill: '#ef4444', fontSize: 9 }} />
<Area type="monotone" dataKey="Humedad" stroke="#38bdf8" strokeWidth={2.5} fillOpacity={1} fill="url(#ghum2)" name="Humedad" />
</AreaChart>
</ResponsiveContainer>
Expand All @@ -813,7 +823,7 @@ export function ReportsPage() {
<XAxis dataKey="day" stroke="#475569" fontSize={9} tickLine={false} axisLine={false} />
<YAxis stroke="#475569" fontSize={9} tickLine={false} axisLine={false} domain={[10, 45]} />
<Tooltip content={<CustomTooltip />} />
<ReferenceLine y={32} stroke="#ef4444" strokeDasharray="4 2" strokeOpacity={0.3} label={{ value: 'Estrés', position: 'right', fill: '#ef4444', fontSize: 9 }} />
<ReferenceLine y={systemSettings.temperatureThreshold} stroke="#ef4444" strokeDasharray="4 2" strokeOpacity={0.3} label={{ value: 'Estrés', position: 'right', fill: '#ef4444', fontSize: 9 }} />
<Area type="monotone" dataKey="Temperatura" stroke="#f59e0b" strokeWidth={2.5} fillOpacity={1} fill="url(#gtemp2)" name="Temperatura" />
</AreaChart>
</ResponsiveContainer>
Expand Down
Loading
Loading