import React, { useState, useEffect, useCallback } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, doc, setDoc, collection, onSnapshot, query, where } from 'firebase/firestore';
// --- CONFIGURACIÓN E INICIALIZACIÓN DE FIREBASE ---
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
// Mapa de precios simulados para el presupuesto
const TREATMENT_PRICES = {
'Empaste / Obturación': 50,
'Tratamiento de Conducto': 300,
'Corona': 650,
'Implante': 1500,
'Prótesis Fija': 800,
'Prótesis Removible': 400,
'Extracción': 100,
};
// Mapeo de sugerencias de tratamiento basado en el diagnóstico
const SUGGESTIONS_MAP = {
'Caries': ['Empaste / Obturación', 'Tratamiento de Conducto', 'Corona'],
'Fractura': ['Empaste / Obturación', 'Corona', 'Extracción'],
'Ausencia': ['Implante', 'Prótesis Fija', 'Prótesis Removible'],
// ... puedes añadir más mapeos aquí
};
// Datos del diente para el POC
const INITIAL_TOOTH_DATA = {
problem: null,
treatment: null,
status: null, // 'Para Realizar' | 'Realizado'
inBudget: false,
};
// Función de utilidad para obtener la ruta del documento
const getPlanDocPath = (db, userId, toothId) =>
doc(db, `artifacts/${appId}/users/${userId}/dental_plans`, toothId);
// --- COMPONENTE PRINCIPAL ---
const App = () => {
const [db, setDb] = useState(null);
const [auth, setAuth] = useState(null);
const [userId, setUserId] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Estado local del diente 14 (solo para demostración)
const [toothPlan, setToothPlan] = useState(INITIAL_TOOTH_DATA);
const currentToothId = 'Tooth_14';
// Total del presupuesto
const [budgetTotal, setBudgetTotal] = useState(0);
// 1. Inicialización de Firebase y Autenticación
useEffect(() => {
try {
const app = initializeApp(firebaseConfig);
const authInstance = getAuth(app);
const dbInstance = getFirestore(app);
setAuth(authInstance);
setDb(dbInstance);
const unsubscribe = onAuthStateChanged(authInstance, async (user) => {
if (user) {
setUserId(user.uid);
setLoading(false);
} else {
try {
// Intentar iniciar sesión con token personalizado
if (initialAuthToken) {
await signInWithCustomToken(authInstance, initialAuthToken);
} else {
// Iniciar sesión anónimamente si no hay token
await signInAnonymously(authInstance);
}
} catch (e) {
console.error("Error signing in:", e);
setError("Error de autenticación. Consulta la consola.");
setLoading(false);
setUserId(crypto.randomUUID()); // Usar ID anónimo en caso de fallo
}
}
});
return () => unsubscribe();
} catch (e) {
console.error("Error initializing Firebase:", e);
setError("Error al inicializar Firebase. Ver consola.");
setLoading(false);
}
}, []);
// 2. Suscripción a Firestore (Plan del Diente y Presupuesto)
useEffect(() => {
if (!db || !userId) return;
// Suscripción al plan del diente actual
const planRef = getPlanDocPath(db, userId, currentToothId);
const unsubscribePlan = onSnapshot(planRef, (docSnap) => {
if (docSnap.exists()) {
setToothPlan(docSnap.data());
} else {
setToothPlan(INITIAL_TOOTH_DATA);
}
}, (e) => console.error("Error fetching tooth plan:", e));
// Suscripción al cálculo del presupuesto total
const budgetQuery = query(
collection(db, `artifacts/${appId}/users/${userId}/dental_plans`),
where("inBudget", "==", true)
);
const unsubscribeBudget = onSnapshot(budgetQuery, (snapshot) => {
let total = 0;
snapshot.forEach(doc => {
const data = doc.data();
if (data.treatment && TREATMENT_PRICES[data.treatment]) {
total += TREATMENT_PRICES[data.treatment];
}
});
setBudgetTotal(total);
}, (e) => console.error("Error fetching budget:", e));
return () => {
unsubscribePlan();
unsubscribeBudget();
};
}, [db, userId]);
// 3. Manejadores de Estado (persistencia)
const updateToothPlan = useCallback(async (updates) => {
if (!db || !userId) {
console.error("Database not initialized.");
return;
}
const newPlan = { ...toothPlan, ...updates };
const docRef = getPlanDocPath(db, userId, currentToothId);
try {
await setDoc(docRef, newPlan, { merge: true });
} catch (e) {
console.error("Error saving plan:", e);
setError("Error al guardar en la DB. Consulta la consola.");
}
}, [db, userId, toothPlan]);
// 4. Lógica de Interfaz
const handleProblemSelect = (problem) => {
// Al seleccionar un problema, se resetea el tratamiento y estado
updateToothPlan({
problem,
treatment: null,
status: null,
inBudget: false // Reseteamos el presupuesto
});
};
const handleTreatmentSelect = (treatment) => {
updateToothPlan({ treatment, status: 'Para Realizar' });
};
const handleStatusChange = (status) => {
updateToothPlan({ status });
};
const handleBudgetToggle = () => {
updateToothPlan({ inBudget: !toothPlan.inBudget });
};
if (loading) {
return (
<div className="flex justify-center items-center h-screen bg-gray-50">
<p className="text-xl text-indigo-600">Cargando aplicación...</p>
</div>
);
}
if (error) {
return (
<div className="p-8 text-center bg-red-100 text-red-700 border border-red-400 rounded-lg">
<p>{error}</p>
</div>
);
}
const suggestedTreatments = SUGGESTIONS_MAP[toothPlan.problem] || [];
const currentPrice = toothPlan.treatment ? TREATMENT_PRICES[toothPlan.treatment] || 0 : 0;
const isReadyForBudget = toothPlan.treatment && toothPlan.status;
const budgetButtonClasses = toothPlan.inBudget
? "bg-green-600 hover:bg-green-700"
: "bg-indigo-600 hover:bg-indigo-700";
return (
<div className="p-4 md:p-8 bg-gray-50 min-h-screen font-sans">
<h1 className="text-3xl font-extrabold text-gray-900 mb-6 border-b pb-2">
Planificación Odontológica (Diente {currentToothId})
</h1>
<p className="text-sm text-gray-500 mb-6">
**Usuario:** {userId}
</p>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* --- Columna 1: Odontograma (Simulado) --- */}
<div className="lg:col-span-1 bg-white p-6 rounded-xl shadow-lg border border-gray-200">
<h2 className="text-xl font-semibold mb-4 text-indigo-700">1. Diagnóstico Inicial</h2>
<div className="mb-4">
<p className="text-lg font-medium">Diente Seleccionado: <span className="text-indigo-600">#14</span></p>
<p className="text-sm text-gray-500">Haz clic en el problema para iniciar el plan.</p>
</div>
<div className="flex flex-wrap gap-2">
{Object.keys(SUGGESTIONS_MAP).map(problem => (
<button
key={problem}
onClick={() => handleProblemSelect(problem)}
className={`px-4 py-2 text-sm font-medium rounded-full transition duration-150 ${
toothPlan.problem === problem
? 'bg-red-600 text-white shadow-md'
: 'bg-gray-100 text-gray-700 hover:bg-red-50'
}`}
>
{problem}
</button>
))}
</div>
{toothPlan.problem && (
<p className="mt-4 p-3 bg-red-50 text-red-700 rounded-lg border border-red-200">
**Diagnóstico Activo:** {toothPlan.problem}
</p>
)}
</div>
{/* --- Columna 2: Plan de Tratamiento y Estado --- */}
<div className="lg:col-span-2 bg-white p-6 rounded-xl shadow-lg border border-gray-200">
<h2 className="text-xl font-semibold mb-4 text-indigo-700">2. Plan de Tratamiento Sugerido</h2>
{/* SUGERENCIAS BASADAS EN DIAGNÓSTICO */}
{toothPlan.problem && (
<div className="mb-6 border-b pb-4">
<p className="font-medium mb-2 text-gray-700">Sugerencias para **{toothPlan.problem}**:</p>
<div className="flex flex-wrap gap-2">
{suggestedTreatments.map(treatment => (
<button
key={treatment}
onClick={() => handleTreatmentSelect(treatment)}
className={`px-4 py-2 text-sm rounded-lg transition duration-150 ${
toothPlan.treatment === treatment
? 'bg-blue-600 text-white shadow-md'
: 'bg-indigo-50 text-indigo-700 hover:bg-indigo-100'
}`}
>
{treatment} (Precio: ${TREATMENT_PRICES[treatment] || 'N/A'})
</button>
))}
</div>
</div>
)}
{/* SELECCIÓN Y ESTADO DEL TRATAMIENTO */}
{toothPlan.treatment && (
<div>
<p className="text-lg font-bold mb-4">Tratamiento Seleccionado: <span className="text-blue-600">{toothPlan.treatment}</span></p>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">Estado del Correctivo:</label>
<div className="flex space-x-4">
{['Para Realizar', 'Realizado'].map(statusOption => (
<button
key={statusOption}
onClick={() => handleStatusChange(statusOption)}
className={`flex-1 px-4 py-2 rounded-lg border transition duration-150 ${
toothPlan.status === statusOption
? 'bg-green-100 border-green-600 text-green-800 font-semibold'
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
{statusOption}
</button>
))}
</div>
{toothPlan.status && (
<p className="mt-2 text-sm font-medium text-right text-gray-600">
Estado actual: **{toothPlan.status}**
</p>
)}
</div>
</div>
)}
</div>
</div>
{/* --- Fila 3: Presupuesto y Resumen --- */}
<div className="mt-8 p-6 bg-white rounded-xl shadow-lg border border-gray-200">
<h2 className="text-xl font-semibold mb-4 text-indigo-700">3. Conexión con Presupuesto</h2>
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
{/* Botón de Presupuesto */}
<div className="w-full md:w-auto">
{isReadyForBudget ? (
<button
onClick={handleBudgetToggle}
className={`w-full md:w-auto px-6 py-3 text-white font-bold rounded-xl transition duration-200 shadow-lg ${budgetButtonClasses}`}
>
{toothPlan.inBudget ? '✅ REMOVER del Presupuesto' : '➕ CARGAR al Presupuesto'}
</button>
) : (
<div className="px-6 py-3 bg-gray-200 text-gray-500 rounded-xl cursor-not-allowed">
Complete Tratamiento y Estado
</div>
)}
</div>
{/* Resumen del Presupuesto */}
<div className="text-2xl font-extrabold text-right">
<p className="text-sm font-medium text-gray-500">TOTAL ESTIMADO DEL PLAN:</p>
<p className="text-green-700">${budgetTotal.toFixed(2)}</p>
<p className="text-sm text-gray-400 font-normal">
(Total calculado de todos los tratamientos marcados con "Cargar al Presupuesto").
</p>
</div>
</div>
</div>
</div>
);
};
export default App;
No hay comentarios:
Publicar un comentario