viernes, 12 de septiembre de 2025

Ventajas y desventajas de usar el helper global request() contra inyectar un parámetro Request.

La elección entre el helper global request() y la inyección de parámetros es un debate común en el desarrollo con Laravel, y cada enfoque tiene sus ventajas y desventajas.


Inyección de Parámetros (Request $request)

Este es el método recomendado por la documentación de Laravel y la mayoría de los desarrolladores.

Ventajas

  • Legibilidad y Claridad: Al ver $request en la firma del método, sabes de inmediato que el método necesita y usará la información de la solicitud. Esto hace que el código sea más fácil de leer y entender, tanto para ti como para otros desarrolladores.

  • Facilita las Pruebas Unitarias: La inyección de dependencias es un principio clave del diseño de software. Al pasar la instancia de Request como un argumento, puedes fácilmente pasar un objeto Request simulado (un mock) cuando escribes pruebas. Esto te permite testear la lógica de tu controlador de manera aislada, sin tener que hacer una solicitud HTTP real.

  • Mejor Autocompletado: Los IDEs como PhpStorm reconocen el tipo Request y te ofrecen un autocompletado más preciso para sus métodos y propiedades, mejorando la productividad.

  • Cumple con los Principios de Programación Orientada a Objetos: Seguir la inyección de dependencias es una buena práctica de diseño de software.

Desventajas

  • Más Verboso: Tienes que escribir (Request $request) en la firma de cada método del controlador que necesite acceder a la solicitud. Aunque es una pequeña cantidad de código, para algunos puede parecer innecesario.


Helper Global request()

Este enfoque es rápido y fácil, pero generalmente se considera una mala práctica para la lógica principal de la aplicación.

Ventajas

  • Conveniencia: Es rápido de escribir y usar en cualquier lugar del código, sin necesidad de inyectar la dependencia en la firma de los métodos.

Desventajas

  • Dificulta las Pruebas Unitarias: El helper global request() crea una dependencia oculta. Cuando intentas testear tu controlador, el método siempre intentará obtener la instancia de la solicitud actual, lo que hace muy difícil aislar la lógica del controlador para las pruebas. No puedes simplemente "simular" el objeto Request de forma sencilla.

  • Falta de Claridad: Al leer el código, no es inmediatamente obvio que un método depende de la solicitud HTTP. Si ves una función que usa request()->all() o request()->method(), tienes que buscar dónde se usa el helper para entender su dependencia.

  • No Es Estándar: No sigue las buenas prácticas de la programación orientada a objetos ni los principios de diseño recomendados por Laravel.

Conclusión

Aunque el helper global request() puede ser tentador por su conveniencia, la inyección de parámetros (Request $request) es la opción preferida y recomendada.

Usar la inyección de dependencias hace que tu código sea más legible, más fácil de mantener y, lo más importante, mucho más fácil de probar. Para la lógica principal de tu controlador, siempre opta por la inyección de parámetros. Reserva el helper global solo para situaciones muy específicas y generalmente fuera del flujo de tu lógica de negocio, como en el middleware o en scripts de seeder donde la capacidad de prueba no es una preocupación.

miércoles, 10 de septiembre de 2025

Información de auditoría y Eventos de Modelo en Laravel (Model Events)

 Necesito algo para dejar asentado quién fue el usuario que editó un registro. Como información de auditoría.

Una excelente idea. La auditoría es una práctica fundamental en cualquier sistema para mantener la integridad de los datos y la rendición de cuentas. Implementar un registro de quién y cuándo se hizo un cambio es crucial.

La estrategia más común y robusta para lograrlo es agregar columnas a tus tablas para rastrear la información de auditoría.


Estrategia de Backend y Base de Datos

La forma más sencilla de implementar esto es añadir dos columnas a cada tabla que quieras auditar.

  • updated_by_user_id: Un entero que almacenará el ID del usuario que realizó la última modificación. Puede ser una clave foránea si tienes una tabla de users.

  • updated_at: Un timestamp que registrará la fecha y hora exacta de la última modificación.

Tu tabla offices, por ejemplo, quedaría de la siguiente manera:

SQL
CREATE TABLE offices (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    address VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    updated_by_user_id BIGINT UNSIGNED NULL,
    FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
);

En Laravel con Eloquent

En Laravel, puedes automatizar esto usando eventos de modelos (Model Events) o observers. Los eventos updating y saving son perfectos para esta tarea, ya que se activan antes de que el registro se guarde en la base de datos.

Con este código, cada vez que un registro Office se actualice, Laravel verificará si hay un usuario autenticado y, de ser así, llenará automáticamente la columna updated_by_user_id antes de guardar el cambio.


Estrategia de Frontend con React e Inertia

En el frontend, no necesitas hacer ningún cambio explícito para que esto funcione. La información de auditoría se manejará completamente en el backend.

Sin embargo, para mostrar esta información al usuario final, puedes modificar tu componente de React para renderizar estas columnas en una tabla. Esto puede ser útil para que el personal sepa quién hizo el último cambio.

Mostrar Información de Auditoría
import { Head, useForm } from '@inertiajs/react';
import React from 'react';

import AppLayout from '@/layouts/app-layout';

// Mock data to make the component runnable
// In a real application, this data would come from Laravel props
const mockOffices = [
{ id: 1, name: 'Consultorio Central', address: 'Calle Falsa 123', created_at: '2023-10-26T10:00:00Z', updated_at: '2023-10-26T10:30:00Z', updated_by_user_id: 1 },
{ id: 2, name: 'Consultorio Zona Norte', address: 'Av. Siempre Viva 742', created_at: '2023-10-27T14:00:00Z', updated_at: '2023-10-27T14:00:00Z', updated_by_user_id: 2 },
];
const mockUsers = [
{ id: 1, name: 'Juan Perez' },
{ id: 2, name: 'Ana Gomez' },
];

type User = {
id: number;
name: string;
};

type Office = {
id: number;
name: string;
address: string;
created_at: string;
updated_at: string;
updated_by_user_id: number | null;
};

type OfficesPageProps = {
offices: Office[];
users: User[];
};

export default function Offices({ offices = mockOffices, users = mockUsers }: OfficesPageProps) {
const { data, setData, post, processing, errors } = useForm({
name: '',
address: '',
});

// Helper function to find user name by ID
const getUserName = (userId: number | null): string => {
const user = users.find(u => u.id === userId);
return user ? user.name : 'N/A';
};

// Helper function to format date
const formatDateTime = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};

return (
<AppLayout>
<Head title="Consultorios" />

<div className="container mx-auto p-6 bg-white shadow-lg rounded-lg">
<h1 className="text-3xl font-bold mb-6 text-gray-800">Consultorios</h1>
{/* Formulario de creación (ejemplo) */}
<form className="mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
<h2 className="text-xl font-semibold mb-4 text-gray-700">Añadir Nuevo Consultorio</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-gray-700 font-semibold mb-2">Nombre</label>
<input
type="text"
className="w-full px-4 py-2 border rounded-md"
value={data.name}
onChange={e => setData('name', e.target.value)}
/>
</div>
<div>
<label className="block text-gray-700 font-semibold mb-2">Dirección</label>
<input
type="text"
className="w-full px-4 py-2 border rounded-md"
value={data.address}
onChange={e => setData('address', e.target.value)}
/>
</div>
</div>
<button
type="submit"
className="mt-4 px-6 py-2 bg-blue-600 text-white font-semibold rounded-full shadow-lg hover:bg-blue-700 transition-colors"
disabled={processing}
>
Guardar
</button>
</form>

{/* Tabla de consultorios */}
<div className="overflow-x-auto">
<table className="min-w-full bg-white border border-gray-200 rounded-lg">
<thead className="bg-gray-100 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nombre</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dirección</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Última Actualización</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actualizado Por</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{offices.map((office) => (
<tr key={office.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{office.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">{office.address}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">{formatDateTime(office.updated_at)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">{getUserName(office.updated_by_user_id)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</AppLayout>
);
}

Con esta estrategia, cada vez que un registro se actualice, el backend registrará el usuario que lo hizo, y el frontend puede mostrar esta información de forma transparente.

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.