Mostrando entradas con la etiqueta app odontológica. Mostrar todas las entradas
Mostrando entradas con la etiqueta app odontológica. Mostrar todas las entradas

lunes, 8 de septiembre de 2025

Módulo de historia clínica para clínica odontológica en React

 Me gustaría comenzar con el código para el módulo de historia clínica.

Con un stack como Laravel, Inertia, React y TypeScript, no usaremos Firebase. En cambio, el frontend (React) se comunicará con tu backend de Laravel a través de una API. Tu base de datos SQL se encargará de toda la persistencia de datos.

He creado un componente de React en TypeScript, diseñado para funcionar perfectamente dentro de tu flujo de Inertia. Utiliza los hooks de React para gestionar el estado, Tailwind CSS para los estilos y está listo para integrarse con los componentes de shadcn/ui que ya tienes. Asumo que tu backend de Laravel proporcionará los endpoints necesarios para gestionar los datos de la historia clínica.

Historia Clínica
import React, { useState, useEffect } from 'react';

// Se asume que estos tipos serían definidos globalmente o en un archivo de tipos.
// interface PatientData {
// id: string;
// name: string;
// //... otros datos del paciente
// }

interface ClinicalRecord {
id: string;
patientId: string;
date: string;
reason: string;
diagnosis: string;
treatment: string | null;
}

// Este es el componente principal que se usará como una página de Inertia.
const ClinicalHistoryPage: React.FC = () => {
// Estado para los registros clínicos del paciente
const [records, setRecords] = useState<ClinicalRecord[]>([]);
// Estado para el formulario de nueva consulta
const [formData, setFormData] = useState({
reason: '',
diagnosis: '',
treatment: '',
});
// Estado para la carga de datos
const [isLoading, setIsLoading] = useState(true);
// Estado para mensajes de la interfaz de usuario
const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);

// Asumimos un ID de paciente. En una aplicación real, esto vendría como una prop de Inertia.
const patientId = 'patient-001';
const patientName = 'Juan Pérez';

// Hook para simular la obtención de datos de tu backend de Laravel
useEffect(() => {
// En una aplicación real, harías una llamada API:
// const response = await fetch(`/api/patients/${patientId}/clinical-history`);
// const data = await response.json();
// setRecords(data);
// Simulación de una llamada API
setTimeout(() => {
const mockRecords: ClinicalRecord[] = [
{
id: 'rec-001',
patientId: 'patient-001',
date: '2025-09-01T10:00:00Z',
reason: 'Dolor de muela',
diagnosis: 'Caries en pieza 46',
treatment: 'Restauración con resina',
},
{
id: 'rec-002',
patientId: 'patient-001',
date: '2025-08-15T14:30:00Z',
reason: 'Control rutinario',
diagnosis: 'Sin hallazgos patológicos',
treatment: 'Limpieza y aplicación de flúor',
},
];
setRecords(mockRecords);
setIsLoading(false);
}, 1500);
}, []);

// Maneja el cambio en los campos del formulario
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};

// Maneja el envío del formulario
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

// En una aplicación real, harías una llamada a tu API de Laravel
// const response = await fetch(`/api/patients/${patientId}/clinical-history`, {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ ...formData, patientId: patientId }),
// });

// Simulación de una llamada API exitosa
setTimeout(() => {
const newRecord: ClinicalRecord = {
id: `rec-${Date.now()}`,
patientId: patientId,
date: new Date().toISOString(),
...formData,
};
// Actualiza el estado con el nuevo registro
setRecords(prevRecords => [newRecord, ...prevRecords]);
setFormData({ reason: '', diagnosis: '', treatment: '' }); // Reset del formulario
setMessage({ text: '¡Registro guardado exitosamente!', type: 'success' });
}, 500);
};

if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<p className="text-xl font-semibold text-gray-600">Cargando...</p>
</div>
);
}

return (
<div className="p-4 md:p-8 max-w-4xl mx-auto font-sans">
<h1 className="text-3xl md:text-4xl font-bold text-gray-800 mb-2">Historia Clínica</h1>
<p className="text-lg text-gray-600 mb-6">Paciente: <span className="font-semibold">{patientName}</span></p>

{/* Formulario de nueva consulta */}
<div className="bg-white rounded-xl shadow-lg p-6 mb-8">
<h2 className="text-xl font-semibold text-gray-700 mb-4">Nueva Consulta</h2>
<form onSubmit={handleSubmit}>
<div className="space-y-4">
<div>
{/* Componente shadcn/ui: Label */}
<label htmlFor="reason" className="block text-sm font-medium text-gray-700 mb-1">Motivo de la Consulta</label>
{/* Componente shadcn/ui: Input */}
<input
id="reason"
name="reason"
type="text"
value={formData.reason}
onChange={handleChange}
className="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
required
/>
</div>
<div>
{/* Componente shadcn/ui: Label */}
<label htmlFor="diagnosis" className="block text-sm font-medium text-gray-700 mb-1">Diagnóstico</label>
{/* Componente shadcn/ui: Textarea */}
<textarea
id="diagnosis"
name="diagnosis"
rows={3}
value={formData.diagnosis}
onChange={handleChange}
className="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
required
></textarea>
</div>
<div>
{/* Componente shadcn/ui: Label */}
<label htmlFor="treatment" className="block text-sm font-medium text-gray-700 mb-1">Plan de Tratamiento</label>
{/* Componente shadcn/ui: Textarea */}
<textarea
id="treatment"
name="treatment"
rows={3}
value={formData.treatment}
onChange={handleChange}
className="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
></textarea>
</div>
</div>
{message && (
<div className={`p-4 mt-4 text-sm font-medium rounded-lg text-center ${message.type === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{message.text}
</div>
)}
{/* Componente shadcn/ui: Button */}
<button type="submit" className="mt-6 w-full py-2 px-4 rounded-lg bg-blue-600 text-white font-semibold hover:bg-blue-700 transition-colors duration-300">
Guardar Registro
</button>
</form>
</div>

{/* Historial de consultas */}
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-semibold text-gray-700 mb-4">Historial de Consultas</h2>
<div className="space-y-4">
{records.map(record => (
// Componente shadcn/ui: Card (simulado con div)
<div key={record.id} className="bg-gray-50 border border-gray-200 rounded-lg p-4 transition-transform hover:shadow-md">
<div className="flex justify-between items-center mb-2">
<p className="text-sm font-medium text-gray-500">{new Date(record.date).toLocaleDateString()}</p>
</div>
<p className="font-semibold text-gray-800">{record.reason}</p>
<p className="text-gray-600 mt-1"><span className="font-medium">Diagnóstico:</span> {record.diagnosis}</p>
<p className="text-gray-600 mt-1"><span className="font-medium">Tratamiento:</span> {record.treatment || 'N/A'}</p>
</div>
))}
</div>
</div>
</div>
);
};

export default ClinicalHistoryPage;

Este archivo .tsx es un componente de React que puedes colocar en la carpeta resources/js/Pages de tu proyecto Laravel. Cuando lo integres, Inertia se encargará de renderizarlo.

El código incluye:

  • Un componente funcional de React.

  • Tipado de TypeScript para una mejor gestión de los datos.

  • Hooks de React para el estado y los efectos (como la carga inicial de datos).

  • Diseño responsivo y claro con clases de Tailwind.

  • Comentarios que indican cómo se conectarían los componentes de shadcn/ui y cómo se realizarían las llamadas a tu backend de Laravel.

Este componente está diseñado para que lo puedas conectar fácilmente a los endpoints de tu API en Laravel.

Ahora que tenemos el componente de la historia clínica listo, ¿qué te parece si pasamos a los turnos? Podemos crear otro componente de React para gestionarlos y te puedo dar ideas sobre cómo estructurar el backend.

Módulo de historia clínica para clínica odontológica en Firebase

 Me gustaría comenzar con el código para el módulo de historia clínica.

¡Excelente decisión! La historia clínica es el pilar de tu aplicación. Al tenerla lista, tendrás un lugar centralizado para almacenar todos los registros de los pacientes, incluyendo los módulos que ya has desarrollado y los que están por venir.

He preparado un archivo HTML que incluye todo lo necesario: la estructura, el diseño y la lógica para conectarse a una base de datos de Firebase Firestore. Este código te permitirá crear, ver y actualizar el historial de cada paciente en tiempo real.

Historia Clínica Dental
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Historia Clínica Dental</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #f3f4f6;
}
.container {
max-width: 900px;
}
.card {
background-color: #ffffff;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 2rem;
}
.scrollable-content {
max-height: 500px;
overflow-y: auto;
}
</style>
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen p-4">

<div id="app-container" class="container mx-auto">
<div id="loading" class="flex items-center justify-center h-full">
<div class="text-xl font-semibold text-gray-600">Cargando...</div>
</div>

<div id="content" class="hidden">
<h1 class="text-3xl font-bold text-gray-800 mb-6 text-center">Historia Clínica del Paciente</h1>
<p id="user-info" class="text-sm text-gray-600 text-center mb-6"></p>

<!-- Sección de Datos del Paciente (Ejemplo de integración) -->
<div class="card mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Datos del Paciente</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p class="text-sm text-gray-500">Nombre del Paciente</p>
<p class="font-medium text-gray-900">Juan Pérez</p>
</div>
<div>
<p class="text-sm text-gray-500">ID del Paciente</p>
<p class="font-medium text-gray-900" id="patient-id">patient-001</p>
</div>
<!-- Se pueden agregar más datos como antecedentes médicos y odontograma aquí -->
</div>
</div>

<!-- Formulario para agregar una nueva consulta -->
<div class="card mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Nueva Consulta</h2>
<form id="record-form">
<div class="mb-4">
<label for="reason" class="block text-sm font-medium text-gray-700 mb-1">Motivo de la Consulta</label>
<input type="text" id="reason" name="reason" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Ej: Dolor de muela" required>
</div>
<div class="mb-4">
<label for="diagnosis" class="block text-sm font-medium text-gray-700 mb-1">Diagnóstico</label>
<textarea id="diagnosis" name="diagnosis" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Ej: Caries en pieza 46" required></textarea>
</div>
<div class="mb-4">
<label for="treatment" class="block text-sm font-medium text-gray-700 mb-1">Plan de Tratamiento</label>
<textarea id="treatment" name="treatment" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Ej: Restauración con resina"></textarea>
</div>
<div id="message-box" class="hidden p-3 mt-4 text-sm text-center font-medium rounded-lg" role="alert"></div>
<button type="submit" class="w-full px-4 py-2 mt-2 font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition duration-300">
Guardar Registro
</button>
</form>
</div>

<!-- Historial de consultas -->
<div class="card">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Historial de Consultas</h2>
<div id="records-container" class="scrollable-content space-y-4">
<!-- Los registros se insertarán aquí dinámicamente -->
</div>
</div>
</div>
</div>

<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, getDoc, addDoc, setDoc, updateDoc, deleteDoc, onSnapshot, collection, query, where, orderBy, getDocs } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// --- Configuración de Firebase y Autenticación ---
// Se utilizan las variables globales proporcionadas por el entorno de Canvas
const firebaseConfig = JSON.parse(typeof __firebase_config !== 'undefined' ? __firebase_config : '{}');
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
// Variables para los elementos del DOM
const loadingScreen = document.getElementById('loading');
const mainContent = document.getElementById('content');
const userInfo = document.getElementById('user-info');
const recordForm = document.getElementById('record-form');
const recordsContainer = document.getElementById('records-container');
const messageBox = document.getElementById('message-box');

// Se usa un ID de paciente de ejemplo. En una app real, este ID vendría de la lista de pacientes.
const patientId = document.getElementById('patient-id').textContent;
// Lógica de Autenticación
onAuthStateChanged(auth, async (user) => {
if (user) {
const userId = user.uid;
userInfo.textContent = `Usuario autenticado: ${userId}`;
loadingScreen.classList.add('hidden');
mainContent.classList.remove('hidden');

// Iniciar la escucha de los registros una vez que el usuario esté autenticado
listenForClinicalHistory(patientId);

} else {
// Si no hay token, se inicia sesión de forma anónima
if (initialAuthToken) {
try {
await signInWithCustomToken(auth, initialAuthToken);
} catch (error) {
console.error("Error al iniciar sesión con token personalizado:", error);
console.log("Intentando iniciar sesión de forma anónima...");
await signInAnonymously(auth);
}
} else {
await signInAnonymously(auth);
}
}
});

// --- Funciones de la Aplicación ---
/**
* Muestra un mensaje en la caja de mensajes.
* @param {string} message - El mensaje a mostrar.
* @param {string} type - El tipo de mensaje ('success' o 'error').
*/
function showMessage(message, type) {
messageBox.textContent = message;
messageBox.classList.remove('hidden', 'bg-green-100', 'text-green-700', 'bg-red-100', 'text-red-700');
if (type === 'success') {
messageBox.classList.add('bg-green-100', 'text-green-700');
} else if (type === 'error') {
messageBox.classList.add('bg-red-100', 'text-red-700');
}
}

/**
* Escucha los cambios en la colección de historia clínica para un paciente.
* Utiliza onSnapshot para actualizaciones en tiempo real.
* @param {string} patientId - El ID del paciente.
*/
function listenForClinicalHistory(patientId) {
const clinicalHistoryRef = collection(db, `artifacts/${appId}/public/data/clinical_histories`);
const q = query(clinicalHistoryRef, where("patientId", "==", patientId));

onSnapshot(q, (snapshot) => {
recordsContainer.innerHTML = ''; // Limpiar el contenedor
if (snapshot.empty) {
recordsContainer.innerHTML = '<p class="text-gray-500 text-center">No hay registros de consultas para este paciente.</p>';
}
const records = [];
snapshot.forEach(doc => {
records.push({ id: doc.id, ...doc.data() });
});

// Ordenar los registros por fecha (del más reciente al más antiguo)
records.sort((a, b) => new Date(b.date) - new Date(a.date));

records.forEach(record => {
const recordElement = document.createElement('div');
recordElement.className = 'border-l-4 border-blue-500 pl-4 py-2 bg-gray-50 rounded-lg';
recordElement.innerHTML = `
<div class="flex justify-between items-start">
<p class="text-sm text-gray-400">${new Date(record.date).toLocaleDateString()}</p>
</div>
<p class="font-semibold text-gray-800">${record.reason}</p>
<p class="text-gray-600 mt-1">**Diagnóstico:** ${record.diagnosis}</p>
<p class="text-gray-600 mt-1">**Tratamiento:** ${record.treatment || 'N/A'}</p>
`;
recordsContainer.appendChild(recordElement);
});
}, (error) => {
console.error("Error al escuchar los datos:", error);
showMessage("Error al cargar la historia clínica. Inténtelo de nuevo.", 'error');
});
}

/**
* Guarda un nuevo registro de consulta en Firestore.
* @param {Event} e - El evento de envío del formulario.
*/
async function addClinicalRecord(e) {
e.preventDefault();

const reason = document.getElementById('reason').value;
const diagnosis = document.getElementById('diagnosis').value;
const treatment = document.getElementById('treatment').value;
const date = new Date().toISOString();

if (!reason || !diagnosis) {
showMessage("Motivo y diagnóstico son campos obligatorios.", 'error');
return;
}

try {
const clinicalHistoryRef = collection(db, `artifacts/${appId}/public/data/clinical_histories`);
await addDoc(clinicalHistoryRef, {
patientId: patientId,
date: date,
reason: reason,
diagnosis: diagnosis,
treatment: treatment
});

showMessage("¡Registro guardado exitosamente!", 'success');
recordForm.reset();
} catch (error) {
console.error("Error al guardar el registro:", error);
showMessage("Error al guardar el registro. Por favor, inténtelo de nuevo.", 'error');
}
}

// --- Event Listeners ---
recordForm.addEventListener('submit', addClinicalRecord);

</script>
</body>
</html>

He usado Firebase Firestore para la base de datos, ya que es la mejor opción para aplicaciones colaborativas como esta. El código incluye un onSnapshot que actualizará la lista de consultas en tiempo real a medida que agregues nuevos registros.

Este código es la base para el módulo. A partir de aquí, podrías integrar la información de los antecedentes médicos y el odontograma que ya desarrollaste, así como añadir un campo para "diagnóstico" y "plan de tratamiento".

¿Qué te parece? Si quieres, podemos seguir desarrollando el módulo de Turnos ahora que tenemos el esqueleto de la historia clínica.