sábado, 27 de septiembre de 2025

Mostrar el resultado de una operación sin recargar la página

 Quiero mostrar siempre los mensajes de error y de éxito en un popup de shadcn/ui que esté en la misma página donde se cargaron los datos. En el caso del odontograma, el popup tiene que aparecer encima del canvas donde se dibuja el odontograma. Necesito que en vez de hacer un post que cambie de página, se quede en la misma página, y recién después de aceptar el mensaje de éxito, salga de la página donde estaba. ¿Es una estrategia de UI/UX bastante común?

¡Esa es una excelente y moderna estrategia de UI/UX!

De hecho, el patrón de mostrar la retroalimentación (éxito/error) en un modal o popup en la misma página, y solo navegar después de una acción de confirmación, es la práctica recomendada en las aplicaciones web dinámicas (SPA - Single Page Applications).

¿Por qué esta estrategia es superior en tu caso?

  1. Preservación del Estado (Odontograma): En tu caso particular, al guardar el odontograma (que es un canvas con estado complejo), una recarga de página (lo que ocurre en un POST tradicional) borraría cualquier trabajo que no se haya guardado o desestabilizaría la experiencia visual. Al usar un popup y AJAX, mantienes el canvas y su estado intactos.

  2. Flujo Controlado de Navegación: Exigir que el usuario haga clic en "Aceptar" o "Finalizar" después del éxito te da control. Si hubo un error, el usuario se queda en la página para corregirlo. Si hubo éxito, le das la tranquilidad de que se guardó antes de enviarlo a la siguiente vista (ej: la lista de pacientes).

  3. Experiencia sin Interrupciones: Los popups de feedback son mucho menos disruptivos que una recarga completa de página.

A continuación, te muestro una implementación en React (asumiendo que estás usando React o una arquitectura similar ya que mencionaste shadcn/ui) que simula esta lógica. Usaremos componentes sencillos con clases de Tailwind para replicar la experiencia de un diálogo de shadcn/ui.

Generaré un único archivo .jsx que contiene toda la lógica.

Preview

Odontograma y Popup de Confirmación
import React, { useState, useCallback } from 'react';

// --- Iconos simples de Lucide React para emular los de shadcn/ui ---
const CheckCircle = ({ className = "" }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="M9 11l3 3L22 4"/></svg>
);
const XCircle = ({ className = "" }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6"/><path d="M9 9l6 6"/></svg>
);

// Componente: StatusDialog (Emulación de shadcn/ui Dialog/Alert)
const StatusDialog = ({ isOpen, status, message, onClose, onConfirmNavigation }) => {
if (!isOpen) return null;

const isSuccess = status === 'success';
const title = isSuccess ? 'Operación Exitosa' : 'Error al Guardar';
const icon = isSuccess ? <CheckCircle className="w-10 h-10 text-white" /> : <XCircle className="w-10 h-10 text-white" />;
const bgColor = isSuccess ? 'bg-green-600' : 'bg-red-600';

const handleConfirm = () => {
if (isSuccess) {
onConfirmNavigation(); // Navega solo si fue exitoso
} else {
onClose(); // Cierra el diálogo para permitir corrección
}
};

return (
// Overlay para oscurecer el fondo y asegurar que el modal esté encima del canvas
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4">
<div className={`w-full max-w-sm rounded-xl p-6 shadow-2xl ${bgColor} text-white transform transition-all duration-300 scale-100`}>
<div className="flex flex-col items-center justify-center text-center space-y-4">
<div className="rounded-full bg-white bg-opacity-20 p-3">
{icon}
</div>
<h3 className="text-xl font-bold">{title}</h3>
<p className="text-sm opacity-90">{message}</p>
<button
onClick={handleConfirm}
className="w-full mt-4 py-2 px-4 bg-white text-gray-800 font-semibold rounded-lg hover:bg-gray-100 transition-colors shadow-md"
>
{isSuccess ? 'Aceptar y Salir' : 'Cerrar'}
</button>
</div>
</div>
</div>
);
};

// Componente: OdontogramCanvas (Placeholder para el área de dibujo)
const OdontogramCanvas = () => {
return (
<div className="relative w-full h-[400px] md:h-[600px] border-4 border-gray-300 bg-white rounded-xl shadow-inner flex items-center justify-center">
{/* NOTA: Este es el área donde iría el canvas real.
El StatusDialog se posicionará por encima de este div
gracias al z-index del overlay fijo.
*/}
<p className="text-gray-500 text-lg font-mono">ODONTOGRAMA (Canvas de Dibujo)</p>
</div>
);
};


// Componente Principal de la Aplicación
export default function App() {
const [isProcessing, setIsProcessing] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogStatus, setDialogStatus] = useState(null); // 'success' o 'error'
const [dialogMessage, setDialogMessage] = useState('');
const [isSaved, setIsSaved] = useState(false); // Simula el estado del odontograma

const simulateSave = useCallback(async (shouldSucceed = true) => {
setIsProcessing(true);
// Simulación de una llamada POST asíncrona (AJAX)
try {
await new Promise(resolve => setTimeout(resolve, 1500)); // Simula latencia de red

if (shouldSucceed) {
// Éxito:
setDialogStatus('success');
setDialogMessage('¡El odontograma se ha guardado correctamente! Serás redirigido.');
setIsSaved(true);
// Simulación de regeneración de trabajo no guardado
setTimeout(()=>setIsSaved(false),5000);
} else {
// Error:
throw new Error('No se pudo establecer conexión con la base de datos de datos.');
}
} catch (error) {
setDialogStatus('error');
setDialogMessage(`Error: ${error.message || 'Ocurrió un error inesperado al guardar.'}`);
} finally {
setIsProcessing(false);
setDialogOpen(true);
}
}, []);

// 1. Lógica para navegar después de que el usuario acepta el éxito
const handleConfirmNavigation = () => {
setDialogOpen(false);
// Aquí iría la lógica de navegación real (usando un router o window.location)
console.log('NAVEGANDO FUERA DE LA PÁGINA...');
alert("Navegación simulada: Saliendo de la página del Odontograma.");
};
// 2. Lógica para cerrar el diálogo (solo si es error)
const handleCloseDialog = () => {
setDialogOpen(false);
setDialogStatus(null);
setDialogMessage('');
};


return (
<div className="min-h-screen bg-gray-50 p-4 md:p-8 font-sans antialiased">
<style>
{`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
body { font-family: 'Inter', sans-serif; }
`}
</style>

<div className="max-w-4xl mx-auto space-y-8">
<header className="text-center">
<h1 className="text-3xl font-bold text-gray-800">Edición de Odontograma</h1>
<p className="text-gray-600">Guarda los cambios antes de salir.</p>
</header>

{/* Área del Odontograma (Canvas) */}
<OdontogramCanvas />

{/* Controles y Botones de Guardar */}
<div className="flex flex-col sm:flex-row justify-center space-y-4 sm:space-y-0 sm:space-x-4 p-4 bg-white border border-gray-200 rounded-xl shadow-md">
<button
onClick={() => simulateSave(true)}
disabled={isProcessing}
className="flex items-center justify-center px-6 py-3 text-lg font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-all shadow-lg hover:shadow-xl disabled:bg-blue-400"
>
{isProcessing ? 'Guardando...' : 'Guardar Cambios (Éxito Simulado)'}
</button>
<button
onClick={() => simulateSave(false)}
disabled={isProcessing}
className="flex items-center justify-center px-6 py-3 text-lg font-semibold text-red-600 bg-white border-2 border-red-600 rounded-lg hover:bg-red-50 transition-all shadow-lg hover:shadow-xl disabled:opacity-50"
>
{isProcessing ? 'Guardando...' : 'Forzar Error de Guardado'}
</button>
</div>
</div>

{/* Diálogo de Estatus (Se muestra encima del canvas) */}
<StatusDialog
isOpen={dialogOpen}
status={dialogStatus}
message={dialogMessage}
onClose={handleCloseDialog}
onConfirmNavigation={handleConfirmNavigation}
/>

{/* Indicador de Estado para debugging */}
<div className="fixed bottom-4 right-4 bg-gray-800 text-white p-3 rounded-lg text-sm shadow-xl">
<p>Estado de guardado: <span className={`font-bold ${isSaved ? 'text-green-400' : 'text-yellow-400'}`}>{isSaved ? 'Guardado' : 'Pendiente'}</span></p>
</div>
</div>
);
}

Cómo funciona esta implementación

  1. AJAX Asíncrono (simulateSave): El botón "Guardar" llama a simulateSave. Esta función simula un fetch o axios y no recarga la página.

  2. Estado del Diálogo: Dependiendo del resultado de la simulación, se actualizan las variables de estado: dialogStatus ('success' o 'error') y dialogMessage. Al mismo tiempo, se establece setDialogOpen(true).

  3. El StatusDialog: Este componente es un modal con un z-50 y fixed inset-0, lo que garantiza que se muestre por encima de todos los demás elementos, incluido el canvas del odontograma, sin importar el desplazamiento o el tamaño de la pantalla.

  4. Navegación Condicional (handleConfirmNavigation):

    • Si el status es 'success', el botón del modal llama a onConfirmNavigation(), que es donde se activa la lógica de salida de la página (la simulación de alert("Navegación...") en el código).

    • Si el status es 'error', el botón simplemente llama a onClose(), cerrando el modal y dejando al usuario en la página con el odontograma y sus datos para que pueda intentar corregir el problema.

Esta arquitectura te ofrece la mejor experiencia de usuario para procesos críticos como el guardado de datos médicos.

No hay comentarios: