Sos muy claro, y tu observación es 100% acertada. Es un problema de diseño de datos que va a impactar directamente en la calidad de los KPIs del dashboard.
🔍 El Problema
| Marca | Ubicación | ¿Guarda fecha? |
|---|---|---|
obturacion-realizada | marcas_dentales[].codigo | ❌ No |
realizado: true | soluciones[].realizado | ❌ No |
Consecuencia: No podés saber cuándo se realizó el tratamiento. Solo sabés que está realizado o no.
📊 Impacto en el Dashboard
| KPI | Problema sin fecha |
|---|---|
| Tratamientos realizados este mes | No se puede calcular |
| Tratamientos realizados mes anterior | No se puede calcular |
| Variación vs mes anterior | No se puede calcular |
| Evolución mensual de tratamientos | No se puede calcular |
| Productividad del odontólogo por mes | No se puede calcular |
Actualmente, en countTratamientosRealizados() estoy contando todos los tratamientos realizados, sin importar cuándo. Eso no refleja la realidad de un mes específico.
🎯 Soluciones Posibles
Opción 1: Agregar fecha_realizacion al JSON (Recomendada)
"soluciones": [ { "tipo": "protesis", "casillero": 4, "incluirEnPropuesta": true, "realizado": true, "fecha_realizacion": "2026-08-15" } ], "marcas_dentales": [ { "codigo": "obturacion-realizada", "casillero": 12, "superficie": 2, "fecha_realizacion": "2026-08-10" } ]
Ventajas:
✅ Es parte del odontograma (donde ya está la marca)
✅ No requiere nueva tabla
✅ Coherente con el diseño actual
Desventajas:
⚠️ Requiere modificar el frontend del odontograma para capturar la fecha
⚠️ El JSON crece un poco más
⚠️ Requiere migración de datos existentes (o dejar
nullen los antiguos)
Opción 2: Crear una tabla tratamientos_realizados
tratamientos_realizados:
- id
- paciente_id
- odontologo_id
- consultorio_id
- prestacion_id
- pieza
- superficie
- fecha_realizacion
- propuesta_id (nullable)
- created_at
- updated_atVentajas:
✅ Mucho más consultable y filtrable
✅ Permite reportes más ricos
✅ Aprovecha índices de BD
✅ Auditoría más clara
Desventajas:
⚠️ Duplica información con el JSON
⚠️ Hay que mantener sincronización con el odontograma
⚠️ Requiere más desarrollo (migración, modelo, controlador)
Opción 3: Híbrida (Recomendada si el tiempo lo permite)
Guardar
fecha_realizacionen el JSON (para visualización inmediata)Además registrar el evento en una tabla
tratamientos_realizados(para reportes)
Esta es la más robusta, pero también la más costosa en desarrollo.
🧠 Mi Recomendación
Dado que:
El dashboard es prioritario y necesita datos por mes
El odontograma ya tiene la marca de realizado
No querés sobre-complejizar el sistema
Te recomiendo la Opción 1 (agregar fecha_realizacion al JSON), con un complemento:
Agregar
fecha_realizacional JSON del odontogramaAl momento de marcar un tratamiento como realizado, capturar la fecha (por defecto la actual, pero editable)
Para los tratamientos ya realizados sin fecha, dejar
nullo usar una fecha por defecto (ej: fecha del odontograma)
Más adelante, si necesitás reportes más avanzados, migrás a la Opción 3.
❓ Preguntas para decidir
¿Querés que implementemos la Opción 1 (fecha en JSON) o preferís la Opción 2 (tabla nueva)?
¿Tenés forma de capturar la fecha desde el frontend del odontograma? (¿el odontólogo marca "realizado" desde una pantalla con fecha editable?)
¿Qué hacés con los tratamientos ya realizados sin fecha? ¿Los dejamos con
nullo les asignamos una fecha estimada?¿La fecha de realización puede ser diferente a la fecha de la sesión? (ej: el odontólogo marca como realizado hoy algo que hizo la semana pasada)
¿Cómo querés proceder?
Sí, exactamente. Antes de escribir código, necesito entender bien el requerimiento. La tabla tratamientos_realizados es una entidad nueva y quiero asegurarme de que el diseño se alinee con tu flujo real de trabajo.
📋 Preguntas sobre tratamientos_realizados
1. Momento de creación del registro
¿El registro se crea automáticamente cuando el odontólogo marca un tratamiento como realizado en el odontograma?
¿O el odontólogo debe confirmar explícitamente "registrar como realizado" desde otra pantalla?
¿Qué pasa si el odontólogo desmarca la marca de realizado? ¿Se elimina el registro o se marca como anulado?
2. Relación con la propuesta
¿Un tratamiento realizado siempre viene de una propuesta aceptada, o puede realizarse sin propuesta previa?
Ejemplo: una urgencia (extracción) que no pasó por presupuesto
Si viene de una propuesta, ¿guardamos el
tratamiento_propuesta_idcomo referencia?¿Qué pasa si se realiza un tratamiento que estaba en una propuesta pero con precio diferente al presupuestado?
3. Datos a guardar
Además de los campos que propuse, ¿necesitás alguno de estos?
| Campo | ¿Necesario? |
|---|---|
paciente_id | ✅ (ya propuesto) |
odontologo_id | ✅ (ya propuesto) |
consultorio_id | ✅ (ya propuesto) |
prestacion_id | ✅ (ya propuesto) |
pieza | ✅ (ya propuesto) |
superficie | ✅ (ya propuesto) |
fecha_realizacion | ✅ (ya propuesto) |
tratamiento_propuesta_id | ⚠️ ¿Sí? |
observaciones | ⚠️ ¿Sí? |
cantidad | ⚠️ ¿Podría ser más de 1? |
precio_al_momento | ⚠️ ¿Guardamos el precio del momento? |
4. Odontograma: ¿sigue siendo la fuente de verdad?
Después de crear la tabla, ¿el JSON del odontograma sigue marcando
realizado: trueo la tabla reemplaza esa marca?¿Los dos conviven? (JSON para visualización inmediata + tabla para reportes)
¿Qué pasa si hay discrepancia entre ambos?
5. Tipos de tratamiento a registrar
soluciones[].realizado = true→ ¿registramos cada solución?marcas_dentales[].codigo = 'obturacion-realizada'→ ¿registramos cada obturación?¿Ambos tipos van a la misma tabla o los diferenciamos?
6. Fecha de realización
¿La fecha la elige el odontólogo manualmente o se toma automáticamente del sistema?
¿Se puede editar la fecha después de creado el registro?
¿Hay validación de que la fecha no sea futura?
7. Anulación / eliminación
¿Se puede anular un tratamiento realizado?
Si se anula, ¿se elimina el registro o se marca como
anulado = true?¿Se necesita auditoría (quién anuló, cuándo)?
8. Cantidad
¿Un mismo tratamiento puede realizarse en varias sesiones?
Ejemplo: una endodoncia en 3 sesiones → ¿3 registros o 1 registro con cantidad?
¿Necesitás reflejar sesiones individuales?
9. Relación con la cuenta del paciente
¿El tratamiento realizado genera movimiento en la cuenta del paciente?
O ¿la cuenta solo se afecta por la aceptación de la propuesta (no por la realización)?
10. Vista de tratamientos realizados
¿Necesitás una pantalla para ver los tratamientos realizados de un paciente?
¿O solo se usa para el dashboard (reportes)?
11. Migración de datos existentes
Los tratamientos que ya están marcados como realizados en el JSON (ej:
obturacion-realizada), ¿los migramos a la nueva tabla?Si sí, ¿con qué fecha?
12. Desmarcar realizado
Si el odontólogo marca como realizado y luego se arrepiente, ¿qué pasa?
¿Se elimina el registro de
tratamientos_realizadoso se marca comoanulado?
13. Reportes
¿Necesitás reportes por:
Odontólogo
Tipo de tratamiento
Período (mes/semana)
Paciente
Consultorio
📝 Resumen de lo que necesito saber
Respondé las que puedas. Las que no tengas claras, las trabajamos juntos.
Mi prioridad: entender si la tabla tratamientos_realizados reemplaza la marca del JSON o coexiste con ella. Esa decisión define toda la arquitectura.
1. El registro se crea en el momento en que el odontólogo marca un tratamiento como realizado en el odontograma. No debe haber ninguna otra pantalla para operar sobre la marca de realización. Si el odontólogo desmarca la marca de realizado desde el odontorgama se elimina el registro de esta tabla nueva. 2. Puede realizarse sin propuesta previa, el ejemplo de la extracción de urgencia aplica bien al caso. No guardamos la relación con la propuesta, es decir dejamos la realización del tratamiento sin referencia a su propuesta. Si se realiza un tratamiento con precio diferente al presupuestado no afecta a la funcionalidad de tratamientos realizados porque no está en alcance de la misma, más allá de que el odontólogo pueda registrar un pago extra cosa que ya está estipulado en el sistema, pero no viene para nada al tema en cuestión. 3. No necesito ningún campo más de los de tu primera propuesta, es decir: no necesito ni tratamiento_propuesta_id, ni observaciones, ni cantidad, ni precio_al_momento. 4. El JSON del odontograma sigue manteniendo la representación del estado de realización. Efectivamente conviven y cada uno es útil a su propósito: el JSON para visualización inmediata y la tabla para los reportes. Si hay discrepancia entre ambos no pasa nada. 5. Registramos cada solución y obturación en un registro separado. Ambos tipos van a la misma tabla. 6. La fecha la elige el odontólogo/recepcionista, pero a priori aparecerá marcada la opción "hoy", al momento de ingresar este dato también habrá opciones "ayer", "anteayer", "la semana pasada", "la quincena pasada", "el mes pasado", y "fecha". Si la fecha es futura hay una advertencia pero ante el consentimiento del usuario será permitida tal cosa. 7. Efectivamente, se puede anular un tratamiento realizado. En tal caso, se elimina el registro. No se necesita auditoría. 8. Para el sistema no importa si un tratamiento se realiza en varias sesiones, de cualquier modo se registra una sola vez. Ni siquiera se utiliza la propiedad cantidad, sino que por ejemplo, las 3 sesiones cuentan para el sistema como un único evento/hito/tratamiento. 9. El tratamiento realizado no genera movimiento en la cuenta del paciente. La cuenta se afecta sólo por la aceptación de la propuesta, no por la realización del tratamiento. 10. No necesito una pantalla para ver los tratamientos realizados del paciente. Sólo se usa para el dashboard (reportes). 11. No vamos a migrar los tratamientos que ya estén marcados como realizados. La función comienza a operar a partir de su implementación sin retroactividad. 12. Si el odontólogo marca como realizado y luego se arrepiente, siempre puede desmarcarlo en el odontograma. Esto provocará que se elimine el registro de la tabla de tratamientos realizados. 13. Me mantengo con las métricas de tratamientos realizados que ya están en curso de desarrollo actualmente. Seguramente vamos a tener que modificar las queries en base a este nuevo modelo de tratamientos realizados. Pero no requiero ningún indicador ni ninguna métrica nueva. Pregunta: ¿Pensaste cómo vamos a mantener sincronizados el JSON del odontograma con la nueva tabla de tratamientos realizados?
Excelente. Ahora tengo el panorama completo. Y tu pregunta final es la pregunta clave. Sí, pensé en cómo mantenerlos sincronizados, y hay varios enfoques posibles. Te los presento con sus pros y contras.
🔄 Estrategias de Sincronización JSON ↔ Tabla
Estrategia 1: El JSON es la fuente de verdad (Recomendada)
Concepto: Cada vez que el odontograma se guarda, el backend reconcilia la tabla tratamientos_realizados con el estado actual del JSON.
Flujo:
1. Odontólogo marca/desmarca "realizado" en el frontend 2. Se guarda el odontograma (JSON actualizado) 3. El backend, al detectar el cambio, ejecuta una reconciliación: - Lee todos los "realizados" del JSON nuevo - Compara con los registros actuales de la tabla - Crea los que faltan - Elimina los que ya no están en el JSON
Ventajas:
✅ Una sola fuente de verdad (el JSON)
✅ No hay riesgo de desincronización
✅ Simple de implementar (un método
sincronizarTratamientosRealizados())✅ Si el JSON se corrompe, se puede reconstruir la tabla
Desventajas:
⚠️ Si se modifica el JSON manualmente (ej: Tinker), la tabla no se entera hasta el próximo guardado
⚠️ Requiere que el JSON siempre esté disponible
Estrategia 2: La tabla es la fuente de verdad
Concepto: Al marcar como realizado, se crea el registro en la tabla. Al desmarcar, se elimina. El JSON solo se usa para visualización (marcas verdes en el odontograma).
Flujo:
1. Odontólogo marca "realizado" en el frontend 2. Se crea el registro en `tratamientos_realizados` 3. Se actualiza el JSON (solo para visualización) 4. Si desmarca → se elimina el registro y se actualiza el JSON
Ventajas:
✅ La tabla es la fuente de verdad para reportes
✅ Más explícito el flujo
Desventajas:
⚠️ Riesgo de desincronización si el JSON se modifica sin pasar por el flujo
⚠️ Más código en el controlador (doble escritura)
Estrategia 3: Sincronización bidireccional con eventos
Concepto: Usar eventos de Eloquent (created, deleted, updated) en el modelo Odontograma para disparar la sincronización.
Ventajas:
✅ Desacopla la lógica
✅ Se dispara automáticamente
Desventajas:
⚠️ Más complejo de debuggear
⚠️ Riesgo de loops infinitos si no se maneja bien
🎯 Mi Recomendación: Estrategia 1 (JSON como fuente de verdad)
¿Por qué?
El JSON ya es la fuente de verdad para todo el odontograma (dentadura, marcas, problemas, soluciones, enlaces). No tiene sentido crear una segunda fuente.
El frontend ya tiene toda la información para saber qué está realizado y qué no.
La reconciliación es simple: al guardar el odontograma, leemos el JSON y sincronizamos la tabla.
Es resiliente: si la tabla se corrompe, se reconstruye desde el JSON.
🛠️ Implementación Propuesta
1. En el método actualizarSeleccion (o donde se guarde el odontograma)
// app/Http/Controllers/PropuestaTratamientoController.php // (o donde se guarde el odontograma) public function actualizarOdontograma(Request $request, Paciente $paciente) { // ... validaciones y control de concurrencia ... DB::transaction(function () use ($odontograma, $documento) { // Guardar el odontograma $odontograma->json = $documento; $odontograma->revision += 1; $odontograma->save(); // Sincronizar tratamientos realizados $this->sincronizarTratamientosRealizados($paciente, $documento); }); // ... respuesta ... } private function sincronizarTratamientosRealizados(Paciente $paciente, array $documento): void { $consultorioId = session()->get(config('constant.CONSULTORIO_SESSION_KEY')); $odontologoId = auth()->id(); // 1. Extraer los tratamientos realizados del JSON $realizadosEnJson = []; // Soluciones realizadas foreach ($documento['soluciones'] ?? [] as $solucion) { if (($solucion['realizado'] ?? false) === true) { $realizadosEnJson[] = [ 'tipo' => 'solucion', 'prestacion_id' => $solucion['tipo'], 'pieza' => (string) $solucion['casillero'], 'superficie' => null, 'fecha_realizacion' => $solucion['fecha_realizacion'] ?? now()->toDateString(), ]; } } // Marcas dentales realizadas foreach ($documento['marcas_dentales'] ?? [] as $marca) { if (($marca['codigo'] ?? null) === 'obturacion-realizada') { $realizadosEnJson[] = [ 'tipo' => 'marca', 'prestacion_id' => 'obturacion-realizada', 'pieza' => (string) $marca['casillero'], 'superficie' => $marca['superficie'] ?? null, 'fecha_realizacion' => $marca['fecha_realizacion'] ?? now()->toDateString(), ]; } } // 2. Obtener los registros actuales en la tabla $registrosActuales = TratamientoRealizado::where('paciente_id', $paciente->id) ->get() ->keyBy(function ($item) { return $item->prestacion_id . '|' . $item->pieza . '|' . ($item->superficie ?? 'null'); }); // 3. Crear los que faltan y eliminar los que sobran $clavesEnJson = collect($realizadosEnJson)->keyBy(function ($item) { return $item['prestacion_id'] . '|' . $item['pieza'] . '|' . ($item['superficie'] ?? 'null'); }); // Crear los que faltan foreach ($clavesEnJson as $clave => $item) { if (!$registrosActuales->has($clave)) { TratamientoRealizado::create([ 'paciente_id' => $paciente->id, 'odontologo_id' => $odontologoId, 'consultorio_id' => $consultorioId, 'prestacion_id' => $item['prestacion_id'], 'pieza' => $item['pieza'], 'superficie' => $item['superficie'], 'fecha_realizacion' => $item['fecha_realizacion'], ]); } } // Eliminar los que ya no están en el JSON foreach ($registrosActuales as $clave => $registro) { if (!$clavesEnJson->has($clave)) { $registro->delete(); } } }
📋 Modelo TratamientoRealizado
// app/Models/TratamientoRealizado.php <?php namespace App\Models; use App\Enums\PrestacionId; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class TratamientoRealizado extends Model { protected $table = 'tratamientos_realizados'; protected $fillable = [ 'paciente_id', 'odontologo_id', 'consultorio_id', 'prestacion_id', 'pieza', 'superficie', 'fecha_realizacion', ]; protected $casts = [ 'prestacion_id' => PrestacionId::class, 'superficie' => 'integer', 'fecha_realizacion' => 'date', ]; public function paciente(): BelongsTo { return $this->belongsTo(Paciente::class); } public function odontologo(): BelongsTo { return $this->belongsTo(User::class, 'odontologo_id'); } public function consultorio(): BelongsTo { return $this->belongsTo(Consultorio::class); } public function prestacion(): BelongsTo { return $this->belongsTo(Prestacion::class); } }
📊 Migración
php artisan make:migration create_tratamientos_realizados_table
// database/migrations/xxxx_xx_xx_create_tratamientos_realizados_table.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('tratamientos_realizados', function (Blueprint $table) { $table->id(); $table->foreignId('paciente_id') ->constrained() ->cascadeOnDelete(); $table->foreignId('odontologo_id') ->constrained('users') ->cascadeOnDelete(); $table->foreignId('consultorio_id') ->constrained('consultorios') ->cascadeOnDelete(); $table->string('prestacion_id'); $table->foreign('prestacion_id') ->references('id') ->on('prestaciones') ->cascadeOnDelete(); $table->string('pieza', 4); $table->unsignedTinyInteger('superficie')->nullable(); $table->date('fecha_realizacion'); $table->timestamps(); $table->index('paciente_id'); $table->index('odontologo_id'); $table->index('consultorio_id'); $table->index('fecha_realizacion'); $table->index(['paciente_id', 'fecha_realizacion']); }); } public function down(): void { Schema::dropIfExists('tratamientos_realizados'); } };
⚠️ Punto Clave: La fecha de realización
En tu respuesta mencionaste que el odontólogo elige la fecha con opciones rápidas ("hoy", "ayer", "anteayer", "la semana pasada", etc.). Esto implica que el frontend debe enviar la fecha al backend cuando se marca como realizado.
Pregunta: ¿Cómo manejás hoy el marcado de realizado en el odontograma? Es decir:
¿El odontólogo marca un checkbox y se guarda inmediatamente?
¿O hace clic en "Guardar odontograma" y se guarda todo junto?
¿Hay un diálogo emergente para elegir la fecha?
Esto define cómo implementamos la captura de la fecha en el JSON.
❓ Preguntas Finales
¿Cómo se guarda hoy el odontograma? (¿AJAX inmediato o botón "Guardar"?)
¿Dónde querés que aparezca el selector de fecha? (¿al marcar "realizado" aparece un popup? ¿o se elige después?)
¿La fecha se guarda en el JSON y en la tabla, o solo en la tabla?
Espero tus respuestas para definir el diseño final.
Perfecto. Ahora entiendo el flujo completo. Tu diseño es elegante: el JSON actúa como "transporte temporal" de la fecha, y una vez que el backend la persiste en la tabla, la quita del JSON. Esto mantiene el JSON limpio y la tabla como fuente de verdad para fechas.
🎯 Diseño Final del Flujo
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Odontograma) │
│ │
│ 1. Odontólogo marca checkbox "realizado" │
│ 2. Aparece popup con opciones de fecha: │
│ [Hoy] [Ayer] [Anteayer] [La semana pasada] [La quincena pasada]│
│ [El mes pasado] [Fecha personalizada] │
│ 3. Se guarda fecha en el JSON: │
│ { "realizado": true, "fecha_realizacion": "2026-09-11" } │
│ 4. Al presionar "Guardar odontograma": │
│ - Se envía JSON completo al backend │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ BACKEND │
│ │
│ 1. Recibe JSON con fechas │
│ 2. Guarda odontograma (revision++) │
│ 3. Sincroniza tabla `tratamientos_realizados`: │
│ - Crea los que faltan (con fecha del JSON) │
│ - Elimina los que ya no están en JSON │
│ 4. LIMPIA las fechas del JSON: │
│ - `unset($documento['soluciones'][i]['fecha_realizacion'])` │
│ - `unset($documento['marcas_dentales'][i]['fecha_realizacion'])`│
│ 5. Guarda el JSON limpio (sin fechas) │
│ 6. Devuelve el JSON limpio al frontend │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Odontograma) │
│ │
│ Recibe JSON limpio (sin fechas) │
│ Las marcas de "realizado" siguen visibles, pero sin fecha │
└─────────────────────────────────────────────────────────────────────┘🛠️ Implementación
1. Frontend: Popup de selección de fecha
// resources/js/Components/Odontograma/ModalFechaRealizacion.tsx import React, { useState } from 'react'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Calendar, Clock } from 'lucide-react'; interface Props { open: boolean; onClose: () => void; onConfirm: (fecha: string) => void; } export default function ModalFechaRealizacion({ open, onClose, onConfirm }: Props) { const [fechaPersonalizada, setFechaPersonalizada] = useState(''); const [mostrarFechaPersonalizada, setMostrarFechaPersonalizada] = useState(false); const opciones = [ { label: 'Hoy', dias: 0 }, { label: 'Ayer', dias: 1 }, { label: 'Anteayer', dias: 2 }, { label: 'La semana pasada', dias: 7 }, { label: 'La quincena pasada', dias: 15 }, { label: 'El mes pasado', dias: 30 }, ]; const calcularFecha = (diasAtras: number): string => { const fecha = new Date(); fecha.setDate(fecha.getDate() - diasAtras); return fecha.toISOString().split('T')[0]; }; const handleOpcionRapida = (dias: number) => { onConfirm(calcularFecha(dias)); }; const handleFechaPersonalizada = () => { if (fechaPersonalizada) { onConfirm(fechaPersonalizada); } }; return ( <Dialog open={open} onOpenChange={onClose}> <DialogContent className="sm:max-w-md"> <DialogHeader> <DialogTitle>Fecha de realización del tratamiento</DialogTitle> </DialogHeader> <div className="space-y-3"> <p className="text-sm text-muted-foreground"> Seleccioná cuándo se realizó el tratamiento: </p> <div className="grid grid-cols-2 gap-2"> {opciones.map((opcion) => ( <Button key={opcion.label} variant="outline" onClick={() => handleOpcionRapida(opcion.dias)} className="justify-start" > <Clock className="mr-2 h-4 w-4" /> {opcion.label} </Button> ))} </div> <div className="border-t pt-3"> {!mostrarFechaPersonalizada ? ( <Button variant="ghost" onClick={() => setMostrarFechaPersonalizada(true)} className="w-full justify-start" > <Calendar className="mr-2 h-4 w-4" /> Fecha personalizada... </Button> ) : ( <div className="space-y-2"> <Label htmlFor="fecha_personalizada">Fecha</Label> <div className="flex gap-2"> <Input id="fecha_personalizada" type="date" value={fechaPersonalizada} onChange={(e) => setFechaPersonalizada(e.target.value)} max={new Date().toISOString().split('T')[0]} /> <Button onClick={handleFechaPersonalizada}> Aceptar </Button> </div> </div> )} </div> </div> <DialogFooter> <Button variant="outline" onClick={onClose}> Cancelar </Button> </DialogFooter> </DialogContent> </Dialog> ); }
2. Backend: Sincronización y limpieza del JSON
// app/Http/Controllers/OdontogramaController.php (o donde se guarde el odontograma) <?php namespace App\Http\Controllers; use App\Models\Paciente; use App\Models\Odontograma; use App\Models\TratamientoRealizado; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Inertia\Inertia; class OdontogramaController extends Controller { public function update(Request $request, Paciente $paciente) { $validated = $request->validate([ 'json' => 'required|array', 'revision' => 'required|integer|min:1', ]); $odontograma = Odontograma::where('paciente_id', $paciente->id)->first(); if (!$odontograma) { return response()->json(['error' => 'Odontograma no encontrado.'], 404); } // Control de concurrencia if ($odontograma->revision !== (int) $validated['revision']) { return response()->json([ 'error' => 'El odontograma ha sido modificado por otro usuario.', 'nueva_revision' => $odontograma->revision, ], 409); } try { DB::transaction(function () use ($odontograma, $validated, $paciente) { $documento = $validated['json']; // 1. Sincronizar tratamientos realizados (con fechas del JSON) $this->sincronizarTratamientosRealizados($paciente, $documento); // 2. Limpiar fechas del JSON $this->limpiarFechasDelJson($documento); // 3. Guardar el odontograma actualizado (sin fechas) $odontograma->json = $documento; $odontograma->revision += 1; $odontograma->save(); }); // 4. Devolver el JSON limpio al frontend return response()->json([ 'success' => true, 'nueva_revision' => $odontograma->revision, 'json' => $odontograma->json, // JSON sin fechas ]); } catch (\Exception $e) { Log::error('Error al guardar odontograma: ' . $e->getMessage()); return response()->json([ 'error' => 'Error al guardar el odontograma.', ], 500); } } /** * Sincronizar tratamientos realizados con la tabla. */ private function sincronizarTratamientosRealizados(Paciente $paciente, array $documento): void { $consultorioId = session()->get(config('constant.CONSULTORIO_SESSION_KEY')); $odontologoId = auth()->id(); // 1. Extraer los tratamientos realizados del JSON $realizadosEnJson = []; // Soluciones realizadas foreach ($documento['soluciones'] ?? [] as $solucion) { if (($solucion['realizado'] ?? false) === true) { $realizadosEnJson[] = [ 'prestacion_id' => $solucion['tipo'], 'pieza' => (string) $solucion['casillero'], 'superficie' => null, 'fecha_realizacion' => $solucion['fecha_realizacion'] ?? now()->toDateString(), ]; } } // Marcas dentales realizadas foreach ($documento['marcas_dentales'] ?? [] as $marca) { if (($marca['codigo'] ?? null) === 'obturacion-realizada') { $realizadosEnJson[] = [ 'prestacion_id' => 'obturacion-realizada', 'pieza' => (string) $marca['casillero'], 'superficie' => $marca['superficie'] ?? null, 'fecha_realizacion' => $marca['fecha_realizacion'] ?? now()->toDateString(), ]; } } // 2. Clave única para identificar cada tratamiento $clave = fn($item) => $item['prestacion_id'] . '|' . $item['pieza'] . '|' . ($item['superficie'] ?? 'null'); $clavesEnJson = collect($realizadosEnJson)->keyBy($clave); // 3. Obtener los registros actuales en la tabla $registrosActuales = TratamientoRealizado::where('paciente_id', $paciente->id) ->get() ->keyBy($clave); // 4. Crear los que faltan foreach ($clavesEnJson as $claveItem => $item) { if (!$registrosActuales->has($claveItem)) { TratamientoRealizado::create([ 'paciente_id' => $paciente->id, 'odontologo_id' => $odontologoId, 'consultorio_id' => $consultorioId, 'prestacion_id' => $item['prestacion_id'], 'pieza' => $item['pieza'], 'superficie' => $item['superficie'], 'fecha_realizacion' => $item['fecha_realizacion'], ]); } } // 5. Eliminar los que ya no están en el JSON foreach ($registrosActuales as $claveItem => $registro) { if (!$clavesEnJson->has($claveItem)) { $registro->delete(); } } } /** * Limpiar las fechas de realización del JSON. */ private function limpiarFechasDelJson(array &$documento): void { // Limpiar soluciones if (isset($documento['soluciones'])) { foreach ($documento['soluciones'] as &$solucion) { unset($solucion['fecha_realizacion']); } unset($solucion); } // Limpiar marcas dentales if (isset($documento['marcas_dentales'])) { foreach ($documento['marcas_dentales'] as &$marca) { unset($marca['fecha_realizacion']); } unset($marca); } } }
3. Modelo TratamientoRealizado
// app/Models/TratamientoRealizado.php <?php namespace App\Models; use App\Enums\PrestacionId; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class TratamientoRealizado extends Model { protected $table = 'tratamientos_realizados'; protected $fillable = [ 'paciente_id', 'odontologo_id', 'consultorio_id', 'prestacion_id', 'pieza', 'superficie', 'fecha_realizacion', ]; protected $casts = [ 'prestacion_id' => PrestacionId::class, 'superficie' => 'integer', 'fecha_realizacion' => 'date', ]; public function paciente(): BelongsTo { return $this->belongsTo(Paciente::class); } public function odontologo(): BelongsTo { return $this->belongsTo(User::class, 'odontologo_id'); } public function consultorio(): BelongsTo { return $this->belongsTo(Consultorio::class); } public function prestacion(): BelongsTo { return $this->belongsTo(Prestacion::class); } }
⚠️ Consideración importante sobre la fecha por defecto
En la sincronización, si el JSON no trae fecha (ej: por un tratamiento viejo o por algún bug), usamos now()->toDateString() como fallback. Esto no es ideal, porque el reporte podría reflejar una fecha incorrecta.
Alternativa: Si no hay fecha, no crear el registro hasta que el usuario la ingrese. Pero esto complica el flujo.
Mi recomendación: Usar now() como fallback, pero loguear una advertencia para detectar casos anómalos:
if (!isset($solucion['fecha_realizacion'])) { Log::warning("Tratamiento realizado sin fecha en JSON", [ 'paciente_id' => $paciente->id, 'solucion' => $solucion, ]); }
📋 Resumen del Flujo
| Paso | Actor | Acción |
|---|---|---|
| 1 | Frontend | Odontólogo marca "realizado" |
| 2 | Frontend | Popup para elegir fecha |
| 3 | Frontend | Guarda fecha en JSON (temporal) |
| 4 | Frontend | Presiona "Guardar odontograma" |
| 5 | Backend | Recibe JSON con fechas |
| 6 | Backend | Sincroniza tabla tratamientos_realizados |
| 7 | Backend | Limpia fechas del JSON |
| 8 | Backend | Guarda odontograma (sin fechas) |
| 9 | Backend | Devuelve JSON limpio al frontend |
| 10 | Frontend | Muestra odontograma sin fechas |
❓ Preguntas finales
¿El popup debe aparecer cada vez que se marca "realizado"? ¿O solo cuando se marca por primera vez?
¿Qué pasa si el odontólogo marca varios tratamientos como realizados a la vez? ¿Se abre un popup por cada uno?
¿La fecha se puede cambiar después? Si el odontólogo ya marcó como realizado, ¿puede cambiar la fecha o solo desmarcar y volver a marcar?
¿Te parece bien este diseño? ¿Ajustamos algo?
- Obtener enlace
- X
- Correo electrónico
- Otras aplicaciones
- Obtener enlace
- X
- Correo electrónico
- Otras aplicaciones
Comentarios