Mejora 3: Implementado el auto-guardado con debounce en el formulario de configuración.
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { Box, TextField, Button, Paper, CircularProgress, Typography } from '@mui/material';
|
import { Box, TextField, Paper, CircularProgress, Chip } from '@mui/material';
|
||||||
import type { Configuracion } from '../types';
|
import type { Configuracion } from '../types';
|
||||||
import * as api from '../services/apiService';
|
import * as api from '../services/apiService';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useDebounce } from '../hooks/useDebounce';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
config: Configuracion | null;
|
config: Configuracion | null;
|
||||||
@@ -9,61 +10,79 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FormularioConfiguracion = ({ config, setConfig }: Props) => {
|
const FormularioConfiguracion = ({ config, setConfig }: Props) => {
|
||||||
const [saving, setSaving] = useState(false);
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
|
||||||
const [success, setSuccess] = useState(false);
|
const debouncedConfig = useDebounce(config, 750);
|
||||||
|
|
||||||
|
const isInitialLoad = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Solo procedemos si tenemos un objeto de configuración "debounced"
|
||||||
|
if (debouncedConfig) {
|
||||||
|
// Si la 'ref' es true, significa que esta es la primera vez que recibimos
|
||||||
|
// un objeto de configuración válido. Lo ignoramos y marcamos la carga inicial como completada.
|
||||||
|
if (isInitialLoad.current) {
|
||||||
|
isInitialLoad.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la 'ref' ya es false, significa que cualquier cambio posterior
|
||||||
|
// es una modificación real del usuario, por lo que procedemos a guardar.
|
||||||
|
const saveConfig = async () => {
|
||||||
|
setSaveStatus('saving');
|
||||||
|
try {
|
||||||
|
await api.guardarConfiguracion(debouncedConfig);
|
||||||
|
setSaveStatus('saved');
|
||||||
|
setTimeout(() => {
|
||||||
|
setSaveStatus('idle');
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error en el auto-guardado:", err);
|
||||||
|
setSaveStatus('idle');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
}, [debouncedConfig]); // La dependencia sigue siendo la misma
|
||||||
|
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <CircularProgress />;
|
return <CircularProgress />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSaveStatus('idle');
|
||||||
const { name, value } = event.target;
|
const { name, value } = event.target;
|
||||||
const numericFields = ['intervaloMinutos', 'cantidadTitularesAScrapear'];
|
const numericFields = ['intervaloMinutos', 'cantidadTitularesAScrapear'];
|
||||||
|
|
||||||
setConfig(prevConfig => prevConfig ? {
|
setConfig(prevConfig => prevConfig ? {
|
||||||
...prevConfig,
|
...prevConfig,
|
||||||
[name]: numericFields.includes(name) ? Number(value) : value
|
[name]: numericFields.includes(name) ? Number(value) : value
|
||||||
} : null);
|
} : null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent) => {
|
const getSaveStatusIndicator = () => {
|
||||||
event.preventDefault();
|
if (saveStatus === 'saving') {
|
||||||
if (!config) return;
|
return <Chip size="small" label="Guardando..." icon={<CircularProgress size={16} />} />;
|
||||||
setSaving(true);
|
|
||||||
setSuccess(false);
|
|
||||||
try {
|
|
||||||
await api.guardarConfiguracion(config);
|
|
||||||
setSuccess(true);
|
|
||||||
setTimeout(() => setSuccess(false), 2000);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error al guardar configuración", err);
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
}
|
||||||
|
if (saveStatus === 'saved') {
|
||||||
|
return <Chip size="small" label="Guardado" color="success" />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper elevation={0} sx={{ padding: 2 }}>
|
<Paper elevation={0} sx={{ padding: 2 }}>
|
||||||
<Box component="form" onSubmit={handleSubmit}>
|
<Box component="form">
|
||||||
<TextField fullWidth name="rutaCsv" label="Ruta del archivo CSV" value={config.rutaCsv} onChange={handleChange} variant="outlined" sx={{ mb: 2 }} disabled={saving} />
|
<TextField fullWidth name="rutaCsv" label="Ruta del archivo CSV" value={config.rutaCsv} onChange={handleChange} sx={{ mb: 2 }} />
|
||||||
<TextField fullWidth name="intervaloMinutos" label="Intervalo de Actualización (minutos)" value={config.intervaloMinutos} onChange={handleChange} type="number" variant="outlined" sx={{ mb: 2 }} disabled={saving} />
|
<TextField fullWidth name="intervaloMinutos" label="Intervalo de Actualización (minutos)" value={config.intervaloMinutos} onChange={handleChange} type="number" sx={{ mb: 2 }} />
|
||||||
<TextField fullWidth name="cantidadTitularesAScrapear" label="Titulares a Capturar por Ciclo" value={config.cantidadTitularesAScrapear} onChange={handleChange} type="number" variant="outlined" sx={{ mb: 2 }} disabled={saving} />
|
<TextField fullWidth name="cantidadTitularesAScrapear" label="Titulares a Capturar por Ciclo" value={config.cantidadTitularesAScrapear} onChange={handleChange} type="number" sx={{ mb: 2 }} />
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth name="viñetaPorDefecto" label="Viñeta por Defecto" value={config.viñetaPorDefecto}
|
||||||
name="viñetaPorDefecto"
|
onChange={handleChange} sx={{ mb: 2 }}
|
||||||
label="Viñeta por Defecto"
|
|
||||||
value={config.viñetaPorDefecto}
|
|
||||||
onChange={handleChange}
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ mb: 2 }}
|
|
||||||
disabled={saving}
|
|
||||||
helperText="El símbolo a usar si un titular no tiene una viñeta específica."
|
helperText="El símbolo a usar si un titular no tiene una viñeta específica."
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', height: '36.5px' }}>
|
||||||
{success && <Typography color="success.main" sx={{ mr: 2 }}>¡Guardado!</Typography>}
|
{getSaveStatusIndicator()}
|
||||||
<Button type="submit" variant="contained" disabled={saving}>
|
|
||||||
{saving ? <CircularProgress size={24} /> : 'Guardar Cambios'}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
27
frontend/src/hooks/useDebounce.ts
Normal file
27
frontend/src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// frontend/src/hooks/useDebounce.ts
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
// Este hook toma un valor y un retardo (delay) en milisegundos.
|
||||||
|
// Devuelve una nueva versión del valor que solo se actualiza
|
||||||
|
// después de que el valor original no haya cambiado durante el 'delay' especificado.
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Configura un temporizador para actualizar el valor "debounced"
|
||||||
|
// después de que pase el tiempo de 'delay'.
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
// Función de limpieza: Si el 'value' cambia (porque el usuario sigue escribiendo),
|
||||||
|
// este return se ejecuta primero, limpiando el temporizador anterior.
|
||||||
|
// Esto previene que el valor se actualice mientras el usuario sigue interactuando.
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]); // El efecto se vuelve a ejecutar solo si el valor o el retardo cambian.
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user