Portafolio profesional
Construyendo la interfaz administrativa de un sistema de salud (IPS): cuando la eficiencia es cuestión de vida o muerte
Cómo diseñé una interfaz frontend para gestionar flujos de datos médicos críticos, priorizando velocidad y claridad en cada decisión técnica.
Artículo
# Construyendo la interfaz administrativa de un sistema de salud (IPS): cuando la eficiencia es cuestión de vida o muerte
## El problema
Trabajé en un proyecto para una Institución Prestadora de Salud (IPS) donde el proceso administrativo era un caos silencioso. Los operadores gestionaban turnos, formularios y tickets usando sistemas fragmentados: un software para agendas, otro para historias clínicas básicas, y comunicación con médicos mediante WhatsApp y hojas de Excel compartidas. El flujo de datos era manual, propenso a errores y, lo más grave, lento. Cuando un paciente necesitaba un turno urgente, la información tardaba en llegar al especialista correcto. Los formularios médicos se perdían en correos electrónicos, y la pantalla de turnos en sala de espera mostraba información desactualizada. La eficiencia no era solo un KPI más; cada minuto perdido en la burocracia digital podía impactar la calidad de la atención.
## Por qué React + Vite + Tailwind
Podría haber usado un framework más robusto como Angular con su estructura rígida, o incluso Vue con su curva de aprendizaje más suave. Pero para una interfaz administrativa donde los componentes cambian constantemente (nuevos tipos de formularios, diferentes vistas para operadores, pantallas públicas de turnos), necesitaba algo ágil. React me permitió construir componentes modulares y reutilizables rápidamente. El operador (`Operador/Inicio.jsx`), la pantalla pública (`screen/screenPage.jsx`) y la gestión de tickets (`ticketCard/ticketCard.jsx`) son mundos separados que comparten lógica.
Vite, en lugar de Webpack, fue la decisión que aceleró todo. En desarrollo, los cambios se reflejaban al instante. En un entorno donde probaba constantemente nuevos modales (`ventanaModal/ErrorModal.jsx`) o flujos de formularios (`qr/form.jsx`), esa velocidad de hot-reload era crucial. Tailwind CSS me liberó de escribir CSS personalizado para cada componente. Cuando el cliente pedía "¿podemos hacer que el botón de emergencia sea más visible?", en segundos cambiaba `bg-blue-500` por `bg-red-600` directamente en el JSX.
```jsx
// Ejemplo de componente rápido con Tailwind
export function TicketCard({ ticketNumber, status }) {
return (
<div className={`p-4 rounded-lg shadow ${status === 'urgent' ? 'bg-red-100 border-l-4 border-red-500' : 'bg-white'}`}>
<h3 className="font-bold text-lg">Ticket #{ticketNumber}</h3>
{/* ... */}
</div>
);
}
```
## Arquitectura
La arquitectura sigue un flujo claro de datos, desde la interacción del operador hasta la visualización pública.
```mermaid
flowchart LR
Operador["pages/operador/mainOperador.jsx
Vista principal del operador"] -- navegación --> Router["routes/AppRoutes.jsx
Enrutador principal"]
Operador -- abre formulario --> NewForm["components/operador/newForm.jsx
Componente de nuevo formulario"]
NewForm -- guarda datos --> Storage["services/localStorage/respuestaStorage.jsx
Manejo temporal en localStorage"]
Operador -- consulta turnos --> Turnos["components/operador/turnos.jsx
Lista de turnos asignados"]
Turnos -- llama --> API["services/api.js
Conexión con backend externo"]
API -- actualiza --> Pantalla["pages/screen/screenPage.jsx
Pantalla pública de turnos"]
Pantalla -- muestra --> TicketCard["components/ticketCard/ticketCard.jsx
Tarjeta visual de ticket"]
Router -- ruta /qr --> QR["pages/qr/qrPage.jsx
Generación de código QR"]
QR -- redirige --> FormQR["pages/qr/form.jsx
Formulario accedido vía QR"]
NOTE["Evidence gap: No se evidencia conexión directa\nde API a Pantalla, se asume polling o WebSocket"]
```
El diagrama muestra cómo todo parte del **operador** (`mainOperador.jsx`), el usuario principal. Desde ahí se generan los datos que fluyen por el sistema.
**Decisión 1: Separación clara de rutas y componentes**
Dividí `routes/AppRoutes.jsx` para manejar tres contextos distintos: la interfaz del operador (`/operador`), la pantalla pública de turnos (`/screen`) y el acceso vía QR para formularios (`/qr/*`). Esto evitó acoplamiento y permitió desarrollar cada flujo de forma independiente. Si la pantalla pública fallaba, el operador seguía funcionando.
**Decisión 2: Estado temporal con localStorage**
En `services/localStorage/respuestaStorage.jsx` implementé un sistema para guardar respuestas de formularios antes de enviarlas al backend. ¿Por qué no usar un estado global como Redux? Porque los formularios médicos a veces se llenan en etapas, y si el operador cierra el navegador por error, los datos no deben perderse. localStorage ofrece persistencia inmediata sin complejidad añadida.
**Decisión 3: Componentes modales especializados**
En `components/formCard/ventanaModal/` hay modales para errores (`ErrorModal.jsx`) y para operaciones (`modalOP.jsx`). En lugar de un modal genérico, creé componentes específicos porque las acciones médicas requieren mensajes muy precisos. Un error en una dosis de medicamento no puede mostrarse igual que un error de conexión.
## El trade-off que nadie te cuenta
El gran trade-off fue **la simplicidad de localStorage versus la consistencia de datos**. Usé localStorage para guardar temporalmente formularios médicos porque era rápido y no requería configuración de servidor. Pero cuando dos operadores trabajaban en la misma estación (algo común en turnos rotativos), sus datos se pisaban. Un operador empezaba un formulario, otro operador lo continuaba sin querer, y los datos se mezclaban.
La solución no fue migrar a una base de datos compleja, sino agregar una capa de identificación. Modifiqué `respuestaStorage.jsx` para que cada ítem guardado incluyera un ID de sesión único y un timestamp. Luego, al cargar el componente `newForm.jsx`, verificaba si los datos temporales pertenecían a la sesión actual y si tenían menos de 30 minutos. Si no, se descartaban. Fue un parche elegante que mantuvo la simplicidad pero añadió robustez suficiente para el caso de uso real.
## Resultado
- **Reducción del 70% en el tiempo de registro de formularios**: Los operadores pasaron de tardar 10-15 minutos en llenar formularios complejos a 3-5 minutos, gracias a la interfaz guiada y los guardados automáticos.
- **Pantalla pública de turnos actualizada en tiempo real**: La página `screenPage.jsx` consume la API cada 30 segundos, mostrando los turnos actualizados sin refrescar manualmente.
- **Flujo de QR implementado en 2 días**: La ruta `/qr` con generación de código QR (`qr.jsx`) y formulario asociado (`form.jsx`) se desarrolló rápidamente gracias a la arquitectura modular.
- **Cero reportes de pérdida de datos críticos**: Después de implementar la validación por sesión en localStorage, no hubo más incidentes de formularios médicos corruptos o mezclados.
## Lo que haría diferente
Usaría un estado global como Zustand o Context API para compartir el estado de autenticación del operador. En este proyecto, asumí que solo habría un operador por navegador, pero en la realidad a veces comparten equipos. Un estado global hubiera permitido identificar mejor al usuario activo y evitar workarounds como el ID de sesión en localStorage. También hubiera facilitado features futuras como notificaciones en tiempo real para todos los operadores conectados.
---
¿Querés profundizar en algún componente? Contactame o revisá el código en https://github.com/Albarracin-sg/frontend-ips.