Ir al contenido principal

Desarrollo de sistema gerencial (parte 3)

Quiero hacer notar que en la app está resultando incompleto el asentamiento de realización de tratamientos. Resulta que hay una marca de realización en un lugar del JSON del odontograma (en el nodo marcas como obturacion-realizada dentro de los elementos del array y en el nodo soluciones como el atributo realizado dentro de los elementos del array). Pero en ningún lugar se guarda la fecha de realización, lo que origina un faltante de información para el cálculo de los tratamientos realizados en el dashboard, puesto que ello requiere manejar la noción de mes actual, mes anterior, etc. ¿Soy claro? ¿Qué opinás?

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

MarcaUbicación¿Guarda fecha?
obturacion-realizadamarcas_dentales[].codigo❌ No
realizado: truesoluciones[].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

KPIProblema sin fecha
Tratamientos realizados este mesNo se puede calcular
Tratamientos realizados mes anteriorNo se puede calcular
Variación vs mes anteriorNo se puede calcular
Evolución mensual de tratamientosNo se puede calcular
Productividad del odontólogo por mesNo 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)

json
"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 null en los antiguos)


Opción 2: Crear una tabla tratamientos_realizados

sql
tratamientos_realizados:
    - id
    - paciente_id
    - odontologo_id
    - consultorio_id
    - prestacion_id
    - pieza
    - superficie
    - fecha_realizacion
    - propuesta_id (nullable)
    - created_at
    - updated_at

Ventajas:

  • ✅ 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_realizacion en 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:

  1. El dashboard es prioritario y necesita datos por mes

  2. El odontograma ya tiene la marca de realizado

  3. No querés sobre-complejizar el sistema

Te recomiendo la Opción 1 (agregar fecha_realizacion al JSON), con un complemento:

  • Agregar fecha_realizacion al JSON del odontograma

  • Al momento de marcar un tratamiento como realizado, capturar la fecha (por defecto la actual, pero editable)

  • Para los tratamientos ya realizados sin fecha, dejar null o 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

  1. ¿Querés que implementemos la Opción 1 (fecha en JSON) o preferís la Opción 2 (tabla nueva)?

  2. ¿Tenés forma de capturar la fecha desde el frontend del odontograma? (¿el odontólogo marca "realizado" desde una pantalla con fecha editable?)

  3. ¿Qué hacés con los tratamientos ya realizados sin fecha? ¿Los dejamos con null o les asignamos una fecha estimada?

  4. ¿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?

Elijo la opción 2: crear una tabla tratamientos_realizados. ¿Me vas a hacer preguntas sobre los requerimientos que tengo para esta función?

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_id como 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: true o 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_realizados o se marca como anulado?

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:

text
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:

text
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é?

  1. El JSON ya es la fuente de verdad para todo el odontograma (dentadura, marcas, problemas, soluciones, enlaces). No tiene sentido crear una segunda fuente.

  2. El frontend ya tiene toda la información para saber qué está realizado y qué no.

  3. La reconciliación es simple: al guardar el odontograma, leemos el JSON y sincronizamos la tabla.

  4. 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)

php
// 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

php
// 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

bash
php artisan make:migration create_tratamientos_realizados_table
php
// 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

  1. ¿Cómo se guarda hoy el odontograma? (¿AJAX inmediato o botón "Guardar"?)

  2. ¿Dónde querés que aparezca el selector de fecha? (¿al marcar "realizado" aparece un popup? ¿o se elige después?)

  3. ¿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.


Elijo la opción 1: el JSON del odontograma es la fuente de la verdad. La implementación propuesta se alínea a lo que yo pensaba. Ahora voy a responder a tus preguntas de la sección "Puntos Clave: La fecha de realización": El grabado se maneja de modo que con cada check de realización marcado, se envía todo el JSON al backend. No tengo implementada la captura / el ingreso de la fecha de realización, necesito hacerlo. Quiero que sea un diálogo con las opciones rápidas mencionadas. De ahí, la fecha se adosaría al elemento del JSON correspondiente (dentro de soluciones y marcas), pero una vez concluida la sincronización se borrarán las fechas del JSON y este JSON será enviado nuevamente al frontend del odontograma, y entonces el frontend ya no sabrá qué fecha tenía en cada elemento del JSON. Ahora voy a responder a tus preguntas de la sección "Preguntas Finales": 1. Botón guardar. 2. Al marcar realizado aparecerá un popup. 3. Temporalmente va en el JSON para la transferencia del frontend al backend. Pero el backend, luego de enviar las fechas a la tabla de tratamientos realizados, las quita del JSON y esto significa que las fechas sólo se persisten como parte de tratamiento realizado, no se persisten como parte del JSON.

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

text
┌─────────────────────────────────────────────────────────────────────┐
│  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

tsx
// 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

php
// 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

php
// 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:

php
if (!isset($solucion['fecha_realizacion'])) {
    Log::warning("Tratamiento realizado sin fecha en JSON", [
        'paciente_id' => $paciente->id,
        'solucion' => $solucion,
    ]);
}

📋 Resumen del Flujo

PasoActorAcción
1FrontendOdontólogo marca "realizado"
2FrontendPopup para elegir fecha
3FrontendGuarda fecha en JSON (temporal)
4FrontendPresiona "Guardar odontograma"
5BackendRecibe JSON con fechas
6BackendSincroniza tabla tratamientos_realizados
7BackendLimpia fechas del JSON
8BackendGuarda odontograma (sin fechas)
9BackendDevuelve JSON limpio al frontend
10FrontendMuestra odontograma sin fechas

❓ Preguntas finales

  1. ¿El popup debe aparecer cada vez que se marca "realizado"? ¿O solo cuando se marca por primera vez?

  2. ¿Qué pasa si el odontólogo marca varios tratamientos como realizados a la vez? ¿Se abre un popup por cada uno?

  3. ¿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?

Comentarios