miércoles, 15 de julio de 2026

Documentación del sistema: narrativa de flujo de negocio y de interacción (parte 2)

 Ya estuve diseñando el flujo de realización de tratamientos con cobro incluido. Tengo algunas ideas para contarte. Después del odontograma va a venir una pantalla de propuesta de tratamiento, que por cierto va a abarcar también lo que es aceptación de presupuesto. Después vendrá la pantalla de cobros. ¿Te cuento más en detalle lo que pienso hacer o te voy pidiendo directamente "haceme tal migración, tal página de inertia con tal y tal cosa..."?


Por todo lo que llevamos trabajando en Dentalia, te recomendaría contarme primero el flujo completo. Creo que eso te va a ahorrar retrabajo.

Ya pasó varias veces en este proyecto que, cuando conocimos el objetivo más amplio, terminamos cambiando alguna entidad o alguna API que inicialmente parecía correcta. Por ejemplo:

  • el documento JSON del odontograma terminó teniendo schema_version y revision;

  • la ruta de actualización pasó de /api/odontograma/{odontograma} a /api/pacientes/{paciente}/odontograma;

  • decidimos separar presupuesto de facturación;

  • decidimos hacer control de concurrencia optimista.

Si hubiéramos implementado cada pantalla aisladamente, probablemente habríamos tenido que rehacer varias cosas.

Lo que yo haría

Primero dedicaría una conversación (o varias) únicamente a diseñar el flujo.

Algo como:

Paciente
    ↓
Odontograma
    ↓
Propuesta de tratamiento
    ↓
Aceptación del paciente
    ↓
Cobro
    ↓
Facturación (opcional)

Y para cada paso responder preguntas como:

  • ¿Qué puede hacer el usuario?

  • ¿Qué entidades se crean?

  • ¿Qué entidades se modifican?

  • ¿Qué estados existen?

  • ¿Qué restricciones hay?

  • ¿Qué relaciones aparecen?

Sin escribir una línea de código.

Después

Recién ahí iría implementando módulo por módulo.

Por ejemplo:

  1. Migraciones.

  2. Modelos.

  3. Policies.

  4. Requests.

  5. Controladores.

  6. Rutas.

  7. Páginas Inertia.

  8. Componentes shadcn/ui.

  9. Tests.

De esa forma casi no habría que volver atrás.


Hay algo más que me parece muy interesante

Creo que ya estás dejando de diseñar "pantallas" y empezando a diseñar un modelo de negocio para un consultorio odontológico.

Esa diferencia es importante.

Una pantalla puede cambiar diez veces.

El modelo de negocio (Paciente → Odontograma → Propuesta → Cobro...) debería permanecer estable durante años.

Por eso me entusiasma más dedicar tiempo ahora a discutir el flujo que escribir inmediatamente migraciones.


Una propuesta

Yo incluso haría un documento que se llame algo como:

Arquitectura funcional de Dentalia

No sería un documento técnico ni un manual de usuario.

Sería el relato completo del funcionamiento del consultorio dentro del sistema, siguiendo a un paciente desde su ingreso hasta la finalización del tratamiento y el cobro. A medida que aparezcan nuevos módulos (agenda, imágenes, obras sociales, inventario, etc.), se incorporan a esa misma historia.

Ese documento terminaría siendo la "fuente de verdad" del proyecto. Después, cuando me pidieras:

"Haceme la migración de PropuestaTratamiento"

yo ya conocería exactamente qué lugar ocupa en el flujo y qué responsabilidades tiene, en lugar de diseñarla de manera aislada. Creo que eso va a dar un resultado mucho más coherente para todo el sistema.


Si yo empiezo con todo el relato ¿Vos me vas a decir cuándo haya información suficiente para pasar a la siguiente etapa?


Sí. Me parece la mejor forma de trabajar.

Mientras vos vas relatando el flujo, yo voy actuando como un analista funcional y arquitecto de software. En cada etapa voy a distinguir entre tres casos:

  1. "Todavía falta información."

    • Te haré preguntas concretas sobre los aspectos que faltan definir.

    • No avanzaremos al siguiente módulo hasta que el diseño de esa etapa sea suficientemente consistente.

  2. "Ya está suficientemente definido."

    • Te diré explícitamente: "Con esta información ya podemos dar por diseñado este módulo y pasar al siguiente."

  3. "Hay una decisión pendiente, pero no bloquea."

    • La anotaré como una decisión diferible y seguiremos adelante si no afecta al resto del diseño.

Además, mientras hablás, iré construyendo mentalmente (y cuando convenga, por escrito):

  • el flujo funcional;

  • las entidades del modelo de datos;

  • las relaciones entre ellas;

  • los estados y transiciones;

  • las reglas de negocio;

  • las APIs necesarias;

  • las páginas Inertia;

  • los componentes de shadcn/ui;

  • los permisos que eventualmente harán falta.

De esa manera, cuando terminemos el relato, no tendremos solamente una idea general: tendremos una especificación lo bastante sólida como para implementar el sistema casi de forma mecánica, módulo por módulo.

Creo que este enfoque encaja muy bien con la forma en que venís trabajando: primero buscás entender profundamente el dominio y recién después escribís código. Empecemos por el relato, y yo te iré indicando cuándo una etapa quedó cerrada y cuándo conviene pasar a la siguiente.


Primero estoy pensando si sería más acorde a la escala de mi proyecto tener una única entidad para plan de tratamiento y para presupuesto, algo como:


presupuestos

-------------

id

paciente_id

fecha

estado

subtotal

descuento

total


presupuesto_items

-----------------

id

presupuesto_id

prestacion_id

pieza

cantidad

precio_unitario

importe


Con esto me propongo representar el flujo de la siguiente manera:


Los tratamientos se guardan en el JSON del odontograma mediante un contenido como el siguiente:


    "marcas_dentales": [

        { "codigo": "obturacion-pendiente", "casillero": 10, "superficie": 1, "incluir en presupuesto": false },

        { "codigo": "obturacion-pendiente", "casillero": 47, "superficie": 3, "incluir en presupuesto": true }

    ],


    "soluciones": [

        { "tipo": "protesis", "casillero": 4, "incluir en presupuesto": true, "realizado": false },

        { "tipo": "extraccion", "casillero": 10, "incluir en presupuesto": true, "realizado": false },

        { "tipo": "perno-corona", "casillero": 11, "incluir en presupuesto": false, "realizado": false }

]


Los items que tienen "incluir en presupuesto" = true quedan seleccionados para ser incluidos en la siguientye etapa, el presupuesto, y en este sentido, forman parte del plan de tratamiento a proponer al paciente.


El flujo continúa en una pantalla de confección de propuestas. Allí habrá una lista de tratamientos que serán todos los items de "marcas_dentales" y "soluciones" del json. En cada fila de la lista de tratamientos en pantalla habrá un checkbox que estará marcado si sólo si el correspondiente ítem en el json tiene "incluir en presuspuesto" = true.


Esta pantalla tendrá los botones "Aceptar presupuesto" y "Rechazar presupuesto", puesto que la idea es que la pantalla en cuestión represente al mismo tiempo la confección del presupuesto y la decisión del paciente sobre si aceptarlo o rechazarlo.


Tanto si la respuesta es aceptar como rechazar el presupuesto, se generará la entidad "presupuestos" y "presupuesto_items" que te mostré más arriba. Que la respuesta haya sido de aceptación o de rechazo quedará registrada en el campo "estado" de la tabla "presupuestos".


Me gusta mucho más este nivel de conversación. Ahora ya estamos diseñando el dominio, y no simplemente tablas. Voy a ir comentando lo que veo.

Lo que me gusta

Hay una idea que me parece muy acertada: el plan de tratamiento no es una entidad persistente independiente, sino un estado del odontograma.

Es decir, el odontólogo trabaja sobre el odontograma, agrega marcas, soluciones, etc., y algunas de ellas pasan a formar parte de la propuesta económica mediante incluir en presupuesto.

Eso evita duplicar información.

Hasta acá me parece una muy buena decisión para un proyecto de la escala de Dentalia.


También me gusta unificar "plan" y "presupuesto"

Yo probablemente también lo haría.

En un consultorio odontológico pequeño, la conversación suele ser:

—Hay que hacer estas cinco cosas.

—¿Cuánto sale?

—Este es el presupuesto.

—Bueno, acepto.

No existen realmente dos documentos distintos.

Existe una única propuesta.

En ese sentido, la tabla

presupuestos

representa bastante bien esa propuesta.


El presupuesto pasa a ser una fotografía

Esto me parece importante.

Cuando el paciente acepta o rechaza, vos generás

presupuestos
presupuesto_items

Eso significa que el presupuesto es un snapshot del odontograma en ese momento.

Eso es muy bueno.

Porque luego el odontograma puede seguir cambiando.


Me gusta que el presupuesto sea inmutable

De hecho iría un paso más allá.

Una vez creado:

presupuesto.estado = aceptado

o

rechazado

yo ya no permitiría modificar sus ítems.

Si hay que cambiar algo:

Presupuesto 12 -> rechazado

↓

Presupuesto 13 -> pendiente

Eso deja un historial excelente.

No digo que debamos implementarlo ya, pero conceptualmente me gusta más.


Donde sí tengo una duda importante

Es esta.

Decís:

La pantalla tendrá los botones "Aceptar presupuesto" y "Rechazar presupuesto".

Mi pregunta es:

¿Quién confecciona el presupuesto?

Hoy el flujo parece ser:

Odontólogo

arma la propuesta

el paciente está presente

acepta o rechaza inmediatamente

Eso ocurre muchas veces.

Pero también ocurre muchísimo esto:

Odontólogo

arma presupuesto

lo imprime

el paciente dice

"Lo pienso y te aviso."

dos semanas después llama

"Acepto."

En ese escenario, la pantalla de confección ya no coincide con la aceptación.

Entonces quiero preguntarte si eso es una simplificación deliberada o si querés contemplar ambos casos.


Otra cosa que me llamó la atención

Los checkboxes viven en el JSON.

Eso significa que el JSON contiene información clínica y comercial mezclada.

Ejemplo:

obturación pendiente

es clínico.

Pero

incluir en presupuesto

es administrativo.

No digo que esté mal.

Pero quiero preguntarte:

¿Estás cómodo con que el odontograma conozca conceptos administrativos?

Porque es una decisión de arquitectura bastante importante.


Otra pregunta importante

Supongamos:

Extracción

está incluida en el presupuesto.

Luego el odontólogo decide:

"No.

En realidad vamos a hacer endodoncia."

¿Qué ocurre?

¿Se modifica el odontograma?

¿Se genera un nuevo presupuesto?

¿Se invalida el anterior?

Todavía no necesito la respuesta.

Solo la dejo anotada porque probablemente afecte el flujo.


Mi impresión hasta ahora

Creo que ya podemos dar por establecidos varios principios del diseño:

  • El odontograma es la fuente de verdad clínica.

  • El plan de tratamiento no existe como entidad separada.

  • El presupuesto es una fotografía (snapshot) del estado del odontograma.

  • El presupuesto tiene ítems propios y no depende del JSON una vez generado.

Eso me parece un diseño bastante limpio.

Pero todavía no daría por cerrado este módulo.

Antes quisiera resolver, al menos conceptualmente, estas dos cuestiones:

  1. ¿La confección del presupuesto y su aceptación son siempre el mismo acto, o pueden ocurrir en momentos distintos?

  2. ¿Qué pasa cuando cambia el tratamiento después de haberse emitido un presupuesto?

Creo que esas dos respuestas terminarán de definir si la entidad presupuestos ya está correctamente diseñada o si le falta algún estado o alguna regla de negocio.


Te respondo las preguntas que me hiciste.

1) Quiero que la confección del presupuesto y su aceptación (o rechazo) puedan ocurrir en momentos distintos.

2) Efectivamente estoy cómodo con que el odontograma conozca conceptos administrativos.

3) Si el presupuesto está en etapa de borrador, simplemente el presupuesto se ve modificado por los cambios indicados por el odontólogo. Si ya pasó a etapa de emitido (es decir, presentado ante el paciente), los cambios que haga el odontólogo en los tratamientos se verán en un presupuesto distinto del ya emitido.


Excelente. Tus respuestas aclaran bastante el modelo y, de hecho, hacen que empiece a aparecer una arquitectura bastante consistente. Hay un cambio importante respecto de lo que yo había entendido inicialmente.

Lo que deduzco ahora

En realidad, el presupuesto sí tiene un ciclo de vida, y ese ciclo de vida es independiente del odontograma.

Yo empezaría a verlo así:

Borrador
    │
    ├── (el odontólogo modifica tratamientos)
    │
    └── Emitir
            │
            ├── Aceptado
            ├── Rechazado
            └── Vencido (opcional)

Mientras el presupuesto sea Borrador, el odontograma sigue siendo la fuente de verdad y los cambios del odontólogo se reflejan automáticamente en ese borrador.

Una vez que el presupuesto pasa a Emitido, deja de evolucionar. Es una fotografía congelada.

Si después cambia el tratamiento, se crea un nuevo presupuesto en estado Borrador.

Eso me parece muy sólido.


Hay una consecuencia importante

Yo ya no hablaría de "crear" el presupuesto al emitirlo.

Hablaría de crear el presupuesto cuando comienza a confeccionarse.

Es decir:

Paciente

↓

Odontograma

↓

Crear presupuesto (estado=Borrador)

↓

editar

↓

editar

↓

editar

↓

Emitir

↓

Aceptar / Rechazar

Ese pequeño cambio tiene muchas ventajas.


Incluso simplifica mucho la implementación

Cuando el odontólogo entra a la pantalla de presupuesto:

¿Existe un presupuesto borrador?

Sí
    ↓
Abrirlo

No
    ↓
Crearlo automáticamente

No hace falta preguntarle nada.


Me gusta otra decisión que tomaste

Estoy cómodo con que el odontograma conozca conceptos administrativos.

Perfecto.

No es la única arquitectura posible, pero tiene una ventaja enorme para un proyecto como Dentalia:

el odontograma se convierte en el verdadero documento de trabajo del odontólogo.

No necesita abrir otra pantalla para decir:

"Esto sí va al presupuesto."

Simplemente marca el tratamiento.


Hay una única cosa que me genera ruido

No es una objeción, sino una pregunta de diseño.

Hoy el JSON tiene:

{
    "codigo": "obturacion-pendiente",
    "incluir en presupuesto": true
}

Pero una vez que existe un presupuesto borrador...

¿Quién manda?

Supongamos esto:

Odontograma

A
B
C

↓

Crear presupuesto

↓

El odontólogo elimina B del presupuesto

¿Se modifica automáticamente el JSON?

¿O el presupuesto puede diferir del odontograma?

Porque si el presupuesto tiene sus propios presupuesto_items, podrían empezar a divergir.

No digo que esté mal.

Solo quiero saber cuál es tu idea.


Otra idea que me parece interesante

Empezaría a cambiar el nombre mentalmente.

En vez de pensar:

"pantalla de presupuesto"

Yo ya la llamaría

Propuesta de tratamiento

Porque ahí ocurren tres cosas:

  • se seleccionan tratamientos;

  • se fijan precios;

  • se obtiene la decisión del paciente.

"Presupuesto" es solamente uno de los aspectos de esa pantalla.


Estado del diseño

Para mí ya quedó bastante definido:

  • ✅ Odontograma.

  • ✅ Fuente de verdad clínica.

  • ✅ Presupuesto con ciclo de vida.

  • ✅ Presupuesto inmutable después de emitido.

  • ✅ Nuevo presupuesto cuando cambian tratamientos.

  • ✅ Aceptación y emisión en momentos distintos.

Todavía no daría este módulo por cerrado, pero ya no faltan grandes decisiones arquitectónicas. Lo que queda es afinar algunos comportamientos de sincronización entre el odontograma y el presupuesto borrador. Una vez resuelto ese punto, creo que podremos pasar al diseño de las tablas, las APIs y las pantallas con bastante confianza.


Te diré que en el 100% de los casos estoy de acuerdo con lo que has dicho. Además voy a seguir respondiendo preguntas y así completar la información que falta.

¿Quién manda una vez que existe un presupuesto borrador? En esta instancia todavía la única fuente de información es el JSON del odontograma. Los checks que se (des)marcan en la pantalla de borrador de presupuesto (propuesta de tratamiento, como explicaré a continuación) y los que se (des)marcan en el odontograma tocan el mismo JSON, el del odontograma (la propiedad "incluir en presupuesto"). Por lo tanto, sí. Si el odontólogo elimina un tratamiento del presupuesto (desmarcando el check), esto impacta directamente en el odontograma. Mientras está en estado borrador, no existirán presupuesto_items, puesto que la fuente de información es el JSON del odontograma. 

Estoy de acuerdo con rebautizar al presupuesto como propuesta de tratamiento.


Excelente. Ahora sí creo que empezamos a tener un modelo conceptual realmente elegante. De hecho, tu última aclaración hace que cambie una de las hipótesis que yo tenía.

Creo que apareció una frontera muy clara

Hasta ahora yo imaginaba que el presupuesto nacía en estado borrador.

Ahora veo que en realidad la entidad persistente aún no existe.

Existe solamente una propuesta en edición, cuyo soporte de almacenamiento es el propio odontograma.

Es decir:

Odontograma (JSON)
        │
        │
        ├── El odontólogo agrega tratamientos
        ├── Marca "incluir en propuesta"
        ├── Desmarca
        ├── Cambia cantidades
        └── Cambia superficies

Todo eso modifica exclusivamente el JSON.

Todavía no existe ninguna fila en la tabla propuestas.

Eso me parece muy limpio.


Entonces el flujo queda así

Odontograma
        │
        ▼
Pantalla "Propuesta de tratamiento"

(la pantalla sigue leyendo el JSON)

        │
        │
        ├── modificar selección
        ├── cambiar precios
        ├── cambiar descuentos
        └── etc.

(todo sigue modificando el JSON)

        │
        ▼
Emitir propuesta

↓

recién aquí

↓

INSERT propuesta
INSERT propuesta_items

Fijate que esto simplifica muchísimo el dominio.


Hay un detalle que me llamó la atención

Dijiste:

"...cambiar precios..."

Todavía el JSON que mostraste no tiene precios.

Eso significa que probablemente el JSON vaya creciendo.

Por ejemplo algo así:

{
    "codigo": "obturacion-pendiente",
    "casillero": 10,
    "superficie": 1,

    "incluirEnPropuesta": true,

    "cantidad": 1,

    "precioUnitario": 85000,

    "descuento": 0
}

o bien esos precios podrían venir de otra tabla.

Todavía no sé cuál preferís.

No necesito responderlo ahora.

Simplemente lo anoto.


El cambio de nombre me parece excelente

Creo que "Propuesta de tratamiento" describe mucho mejor lo que hace la pantalla.

Porque el odontólogo no está pensando:

"Voy a hacer un presupuesto."

Está pensando:

"Le voy a proponer este tratamiento."

Después esa propuesta tiene:

  • aspectos clínicos;

  • aspectos económicos;

  • aspectos administrativos.


Empiezo a ver un patrón muy interesante

Hay algo que me gusta mucho de este diseño.

El odontograma deja de ser solamente un dibujo.

Empieza a convertirse en un documento vivo de trabajo.

De él nacen:

Odontograma

↓

Propuesta

↓

Cobro

↓

Facturación

Y eso tiene muchísimo sentido.


Una única inquietud importante

Creo que acabamos de llegar al primer punto en el que sí te propondría pensar un poco más antes de seguir diseñando.

Es esta pregunta:

¿Qué ocurre si existen varias propuestas históricas?

Imaginemos:

Propuesta 1

Emitida

Aceptada parcialmente

Luego pasan dos meses.

El odontólogo agrega un implante nuevo.

Ahora el JSON cambia.

Perfecto.

Pero...

¿qué tratamientos deberían aparecer inicialmente seleccionados cuando se prepara la siguiente propuesta?

Las posibilidades son varias:

  1. Todo lo que tenga incluirEnPropuesta = true.

  2. Sólo lo que todavía no fue realizado.

  3. Sólo lo nuevo desde la última propuesta.

  4. Que el odontólogo decida manualmente.

No estoy diciendo que haya una respuesta correcta. Lo que sí creo es que esta decisión va a influir mucho en la experiencia de uso.


Estado del diseño

Hasta ahora, para mí, ya podemos dar por cerradas estas decisiones:

  • ✅ El odontograma es la única fuente de verdad mientras la propuesta está en edición.

  • ✅ No existen registros persistentes de propuesta durante la edición.

  • ✅ La propuesta persistente nace únicamente al emitirla.

  • ✅ Una propuesta emitida es inmutable.

  • ✅ El odontograma puede seguir evolucionando después.

  • ✅ La pantalla pasa a llamarse "Propuesta de tratamiento".

Creo que ya estamos muy cerca de cerrar completamente este módulo. De hecho, la pregunta sobre cómo iniciar una nueva propuesta a partir de propuestas anteriores es la primera que siento que realmente puede cambiar el comportamiento del sistema. Si resolvemos esa, me parece que tendremos un modelo de negocio muy sólido sobre el cual empezar a construir las migraciones y las pantallas.


No estoy de acuerdo con acrecentar el JSON con la siguiente info:

"incluirEnPropuesta": true, "cantidad": 1, "precioUnitario": 85000, "descuento": 0


Más bien, esa info nacerá (comenzará a existir) cuando se emita la propuesta/presupuesto. El precio unitario vendrá de un catálogo de precios por tratamiento, que estando en la instancia de confección de propuesta la pantalla permitirá aplicar un descuento manualmente. El lugar donde queda registrado tanto precio unitario como descuento es en propuesta_items, en el acto de emisión de propuesta.


Te respondo tu pregunta: ¿qué tratamientos deberían aparecer inicialmente seleccionados cuando se prepara la siguiente propuesta?

Cuando se emite una propuesta, los tratamientos incluidos en ella son editados en el JSON del odontograma de modo que incluirEnPropuesta=false. A partir de allí, si el odontólogo va a confeccionar una siguiente propuesta, va a elegir los tratamientos a incluir en ella desde cero.


Excepto lo que explícitamente dije que no estoy de acuerdo, con todo lo dicho de tu parte estoy de acuerdo.


Perfecto. Y, de hecho, me gusta más tu solución que la que yo había imaginado.

Creo que apareció un principio de diseño muy sano:

El JSON del odontograma contiene exclusivamente información clínica y de flujo de trabajo. La información económica nace únicamente cuando se emite una propuesta.

Eso mantiene muy separadas las responsabilidades.

Entonces el ciclo queda así

1. Odontograma

Contiene únicamente datos clínicos y de preparación de la propuesta.

Ejemplo:

{
    "codigo": "obturacion-pendiente",
    "casillero": 47,
    "superficie": 3,
    "incluirEnPropuesta": true,
    "realizado": false
}

No hay precios.

No hay descuentos.

No hay importes.


2. Pantalla "Propuesta de tratamiento"

La pantalla hace algo interesante:

  • consulta el catálogo de prestaciones;

  • consulta la lista de tratamientos marcados en el JSON;

  • calcula un presupuesto "en memoria".

Todavía no existe ninguna entidad persistente.


3. Emitir propuesta

Recién en ese instante ocurre algo irreversible.

Se crea:

propuestas

y

propuesta_items

Cada item guarda una fotografía económica.

precio_unitario
descuento
importe
cantidad

Aunque mañana cambie el catálogo de precios, esa propuesta seguirá representando exactamente lo que se le ofreció al paciente.

Eso me parece excelente.


4. Limpieza del odontograma

Esta decisión me gustó especialmente:

Cuando se emite una propuesta, todos los tratamientos incluidos pasan automáticamente a incluirEnPropuesta = false.

Eso tiene varias ventajas.

El odontólogo nunca tiene que preguntarse:

"¿Esto ya fue presupuestado?"

Porque el sistema ya dejó el odontograma "limpio" para preparar una propuesta futura.

Me parece una experiencia de usuario muy natural.


Creo que apareció otro concepto interesante

La pantalla "Propuesta de tratamiento" realmente está haciendo un trabajo temporal.

Es decir, funciona como una especie de editor.

No está editando una entidad llamada "Propuesta".

Está editando el odontograma con una intención determinada.

Eso explica por qué no hace falta guardar borradores.

El borrador ya es el propio odontograma.


Una única inquietud que me queda

Ya es bastante más pequeña que las anteriores.

Supongamos:

Obturación 36

Se incluye en una propuesta.

La propuesta se emite.

Automáticamente:

incluirEnPropuesta = false

Hasta aquí perfecto.

Luego el paciente rechaza esa propuesta.

Mi pregunta es:

¿Querés que esos tratamientos vuelvan automáticamente a quedar marcados para una futura propuesta, o preferís que el odontólogo vuelva a seleccionarlos manualmente?

Ambas opciones son defendibles.

Personalmente, me inclino por no reactivarlos automáticamente. Si el odontólogo quiere volver a presupuestarlos, los selecciona otra vez. Eso evita que el sistema haga suposiciones sobre las intenciones del profesional.


Mi impresión general

Siento que el modelo está alcanzando una madurez muy buena. Ya no estamos tomando decisiones aisladas; empiezan a aparecer principios de diseño consistentes que conectan todas las partes del sistema.

Hasta ahora, los resumiría así:

  • El odontograma es el documento vivo del consultorio.

  • La propuesta de tratamiento no existe como entidad mientras se está confeccionando.

  • La información económica nace únicamente al emitir la propuesta.

  • La propuesta emitida es una fotografía clínica y económica inmutable.

  • El odontograma se limpia automáticamente al emitir una propuesta para facilitar la confección de la siguiente.

Si tuviera que ponerle un porcentaje, diría que el diseño conceptual de este módulo está aproximadamente en un 90–95 %. Ya no estamos resolviendo cuestiones de arquitectura; estamos afinando reglas de negocio y detalles de comportamiento. Una vez resuelto el punto de qué hacer tras un rechazo (si reactivar o no la selección automáticamente), creo que podremos pasar con bastante seguridad al diseño del modelo de datos y de las pantallas.


Respondo la pregunta "¿Querés que esos tratamientos vuelvan automáticamente a quedar marcados para una futura propuesta, o preferís que el odontólogo vuelva a seleccionarlos manualmente?" Opto por no reactivar tratamientos automáticamente. Si quiere el odontólogo que los vuelva a elegir manualmente.

Me parece la decisión correcta. Y además mantiene una filosofía que, sin haberla formulado explícitamente, veo que estás siguiendo desde el principio:

El sistema nunca toma decisiones clínicas por el odontólogo.

Eso se refleja en varias partes del diseño:

  • El sistema no decide qué tratamientos incluir en una propuesta.

  • El sistema no decide que un tratamiento rechazado deba volver a proponerse.

  • El sistema no decide que un tratamiento deba desaparecer del odontograma.

  • El sistema simplemente registra el estado y ofrece herramientas.

Esa filosofía me gusta mucho para un software profesional.


Creo que ya podemos escribir el ciclo de vida completo

1. Edición del odontograma

El odontólogo registra diagnósticos y tratamientos.

Odontograma (JSON)

✓ realizado
✓ incluirEnPropuesta
✓ piezas
✓ superficies
✓ etc.

Todavía no existe ninguna propuesta.


2. Confección de propuesta

La pantalla lee el odontograma.

Permite:

  • seleccionar tratamientos;

  • deseleccionarlos;

  • consultar el nomenclador/catálogo de precios;

  • simular importes y descuentos.

Toda la selección sigue almacenándose exclusivamente en el JSON.


3. Emisión

Al emitir ocurre una transacción atómica:

  1. Se crea propuestas.

  2. Se crean los propuesta_items.

  3. Se copian los precios vigentes.

  4. Se copian los descuentos.

  5. Se ponen en false todos los incluirEnPropuesta correspondientes dentro del odontograma.

A partir de ese instante la propuesta queda congelada.


4. Decisión del paciente

La propuesta cambia de estado.

Emitida
    │
    ├── Aceptada
    ├── Rechazada
    └── (eventualmente Vencida)

No modifica el odontograma.


5. Evolución posterior

El odontólogo puede seguir trabajando normalmente.

Agregar tratamientos.

Eliminar tratamientos.

Modificar diagnósticos.

Todo eso vuelve a ocurrir únicamente sobre el odontograma.

Cuando quiera confeccionar otra propuesta comenzará otra vez desde cero seleccionando manualmente los tratamientos.


Lo único que incorporaría

Hay un pequeño cambio de nomenclatura que creo que hará crecer bien el sistema.

Hasta ahora venimos hablando de:

presupuestos
presupuesto_items

Pero ahora el dominio dejó de ser un presupuesto.

Es una propuesta de tratamiento.

Yo empezaría a pensar en:

propuestas
propuesta_items

o incluso

tratamiento_propuestas
tratamiento_propuesta_items

No porque "presupuesto" esté mal, sino porque la entidad ya contiene mucho más que un precio:

  • una selección clínica;

  • una decisión del paciente;

  • un documento histórico;

  • una fotografía económica.

"Propuesta" describe mejor todo eso.


Hay otra consecuencia interesante

Cuando aparezca el módulo de cobros, éste ya no dependerá del odontograma.

Dependerá de una propuesta aceptada.

El flujo empezará a verse así:

Odontograma
      │
      ▼
Propuesta
      │
      ├── Rechazada
      │
      └── Aceptada
             │
             ▼
        Cobros
             │
             ▼
       Facturación

Y eso me parece una separación de responsabilidades muy limpia.


Mi impresión

Tengo la sensación de que este módulo ya está conceptualmente cerrado. Lo que hemos diseñado no es simplemente un CRUD de presupuestos: es un flujo de trabajo coherente, donde cada entidad tiene una responsabilidad clara y el odontograma sigue siendo el eje clínico del sistema.

A partir de este punto, creo que ya podemos empezar a diseñar las migraciones, los modelos, las APIs y las pantallas con bastante confianza, porque las reglas de negocio fundamentales ya están definidas.



lunes, 13 de julio de 2026

Documentación del sistema: narrativa de flujo de negocio y de interacción

No estoy seguro de lo que quiero, pero me parece que está cerca de una narrativa de un recorrido por las pantallas de plan de tratamiento, presupuesto, cobro y facturación, narrando lo que va haciendo el usuario en cada paso, cómo se refleja esto en el modelo de datos, las características más importantes de las pantallas y del modelo de datos (diseño), hasta llegar al final de flujo completo.


Sí, creo que lo que estás buscando es exactamente eso: una narrativa de flujo de negocio y de interacción, donde se cuenta qué hace el usuario, qué pantalla ve, qué entidades se crean o modifican y por qué existen. Ese tipo de documento es muy valioso porque conecta la experiencia del usuario con el diseño del modelo de datos.

Recorrido completo: del odontograma a la factura

1. Pantalla de Odontograma — El diagnóstico

Lo que hace el usuario

El odontólogo abre la ficha del paciente y observa el odontograma. Marca caries, ausencias, restauraciones y tratamientos indicados.

Lo que ve

  • Dientes dibujados.
  • Caras dentarias.
  • Colores y símbolos.
  • Lista de tratamientos sugeridos.

Lo que ocurre en el modelo de datos

Se guarda o actualiza un único registro odontogramas asociado al paciente.

odontogramas
------------
id
paciente_id
schema_version
revision
json

El documento JSON contiene el estado clínico y los tratamientos indicados.

Idea clave

El odontograma responde a la pregunta:

¿Qué tiene el paciente?

Todavía no hay precios ni cobros.


2. Pantalla de Plan de Tratamiento — La propuesta clínica

Lo que hace el usuario

El odontólogo selecciona cuáles de los tratamientos indicados realmente propone realizar.

Por ejemplo:

  • Restauración pieza 11.
  • Endodoncia pieza 26.
  • Corona pieza 26.

Lo que ve

  • Lista de tratamientos pendientes.
  • Prioridad.
  • Observaciones.
  • Estado (propuesto, aceptado, rechazado).

Lo que ocurre en el modelo de datos

treatment_plans
---------------
id
paciente_id
estado
fecha

treatment_plan_items
--------------------
id
treatment_plan_id
prestacion_id
pieza
cantidad

Idea clave

El plan responde a:

¿Qué proponemos hacer?

Puede haber más de un plan para un mismo paciente.


3. Pantalla de Presupuesto — La propuesta económica

Lo que hace el usuario

Presiona “Generar presupuesto desde el plan”.

El sistema toma los tratamientos y busca sus precios vigentes.

Lo que ve

  • Prestación.
  • Pieza.
  • Cantidad.
  • Precio unitario.
  • Total.
  • Descuentos.
  • Vigencia.

Lo que ocurre en el modelo de datos

presupuestos
-------------
id
paciente_id
treatment_plan_id
fecha
estado
subtotal
descuento
total

presupuesto_items
-----------------
id
presupuesto_id
prestacion_id
pieza
cantidad
precio_unitario
importe

Idea clave

El presupuesto responde a:

¿Cuánto costará?

Los precios quedan congelados para preservar el histórico.


4. Pantalla de Aceptación — La decisión del paciente

Lo que hace el usuario

El paciente acepta parcial o totalmente el presupuesto.

Lo que ve

  • Presupuesto original.
  • Firma o aceptación.
  • Fecha.

Lo que ocurre en el modelo de datos

Se actualiza:

presupuestos.estado = 'aceptado'

o se registran ítems aceptados.

Idea clave

No todo presupuesto termina realizándose.


5. Pantalla de Cobro — El dinero recibido

Lo que hace el usuario

La recepcionista registra un pago.

Lo que ve

  • Importe.
  • Medio de pago.
  • Fecha.
  • Observaciones.

Lo que ocurre en el modelo de datos

cobros
------
id
paciente_id
presupuesto_id
fecha
importe
medio_pago

Idea clave

El cobro responde a:

¿Cuánto dinero entró?

Un presupuesto puede tener muchos cobros.


6. Pantalla de Facturación — El comprobante fiscal

Lo que hace el usuario

Selecciona qué prestaciones efectivamente factura.

Lo que ve

  • Datos fiscales.
  • Ítems facturados.
  • Totales.
  • Número de comprobante.

Lo que ocurre en el modelo de datos

facturas
--------
id
paciente_id
fecha
numero
tipo
subtotal
total

factura_items
-------------
id
factura_id
prestacion_id
cantidad
precio_unitario
importe

Idea clave

La factura responde a:

¿Qué se facturó realmente?

No necesariamente coincide con el presupuesto original.


El flujo completo

Paciente llega
      │
      ▼
Odontograma
“¿Qué tiene?”
      │
      ▼
Plan de Tratamiento
“¿Qué proponemos hacer?”
      │
      ▼
Presupuesto
“¿Cuánto costará?”
      │
      ▼
Aceptación
“¿El paciente acepta?”
      │
      ▼
Cobros
“¿Cuánto dinero ingresó?”
      │
      ▼
Factura
“¿Qué se facturó?”

La idea más importante del diseño

Cada etapa crea una fotografía histórica:

  • El odontograma conserva el diagnóstico.
  • El plan conserva la propuesta clínica.
  • El presupuesto conserva los precios ofrecidos.
  • El cobro conserva el dinero recibido.
  • La factura conserva el comprobante emitido.

Por eso las entidades son separadas. Aunque el usuario las recorra como un único flujo, el sistema necesita guardar cada fotografía para que el historial siga siendo correcto aunque cambien tratamientos, precios o decisiones del paciente.

Creo que este documento ya está muy cerca de lo que terminaría siendo la documentación funcional principal de Dentalia, porque describe simultáneamente las pantallas, el comportamiento del usuario y el modelo de datos. Esa combinación suele ser mucho más útil para desarrollar el sistema que una descripción puramente técnica o puramente de negocio.


De hecho, 

En lugar de describir solo el flujo de negocio, podríamos convertirlo en un documento de diseño funcional, donde cada etapa se describe desde cuatro perspectivas.

Por ejemplo:

1. El odontólogo crea un plan de tratamiento

Objetivo

Registrar qué tratamientos recomienda realizar al paciente.

Pantalla

PlanTratamiento/Edit.tsx

Componentes shadcn/ui:

Card

Accordion (por cuadrantes o piezas)

Table

Checkbox

Badge

Dialog (agregar tratamiento)

Combobox (buscar práctica)

Calendar (fecha sugerida)

Modelo de datos

Se crea:

PlanTratamiento

y sus

PlanTratamientoItem

Cada ítem contiene:

pieza dental

práctica

cantidad

observaciones

prioridad

estado (propuesto, aceptado, realizado...)

2. El paciente acepta parte del tratamiento

Desde la pantalla del plan se seleccionan algunos ítems.

Acción:

Crear presupuesto

No se vuelven a escribir.

Simplemente se referencian.

Se crea:

Presupuesto

con

PresupuestoItem

que apuntan a los mismos tratamientos.

3. Se imprime o envía el presupuesto

La pantalla muestra:

subtotal

descuentos

impuestos (si aplican)

total

vigencia

observaciones

El presupuesto queda:

Pendiente

4. El paciente acepta

Botón:

Aceptar presupuesto

El sistema:

cambia el estado

registra fecha

permite comenzar los tratamientos

No duplica información.

5. El odontólogo realiza un tratamiento

Desde el odontograma o desde la agenda.

Marca:

Realizado

Automáticamente el plan refleja el avance.

6. Se registra un cobro

Pantalla:

Cobros

Componentes:

Select del paciente

Tabla de conceptos pendientes

Input de importe

Select de medio de pago

Date Picker

Botón Registrar

Se crea:

Cobro

y eventualmente

CobroAplicacion

para distribuir el pago entre varios conceptos.

7. Facturación

Si corresponde:

Generar factura

El sistema toma:

tratamientos realizados

presupuesto aceptado

cobros (según la política elegida)

y genera una factura.

Lo interesante

Lo que me gusta de este enfoque es que el documento no solo explica "qué hace el usuario", sino también por qué el modelo de datos tiene esa forma. Por ejemplo, en lugar de decir simplemente "existe una tabla presupuestos", la narrativa muestra que el presupuesto nace del plan de tratamiento, puede aceptarse parcialmente, da lugar a tratamientos realizados, luego a cobros y finalmente, cuando corresponde, a la facturación.

Creo que este tipo de documento puede convertirse en la especificación funcional de Dentalia. Si se escribe con suficiente detalle, prácticamente permite implementar el sistema pantalla por pantalla sin necesidad de volver a analizar el negocio en cada módulo.



martes, 7 de julio de 2026

Módulo presupuesto de odonto-app

 ¿Cómo sería una funcionalidad de presupuesto basada en tratamientos apuntados en un odontograma?

En un software odontológico, una función de presupuesto suele ser la consecuencia natural del odontograma. El odontograma registra el estado clínico; el presupuesto traduce ese diagnóstico en un plan económico.

Una arquitectura típica sería:

  1. El odontograma contiene los dientes, caras dentarias y tratamientos indicados.

  2. Cada tratamiento indicado referencia un tipo de prestación (obturación, endodoncia, extracción, corona, etc.).

  3. Al generar un presupuesto, el sistema recorre esos tratamientos y busca el precio vigente de cada prestación.

  4. Se genera un documento de presupuesto independiente del odontograma.

Por ejemplo:

Odontograma
 ├── Tratamiento: Obturación simple (pieza 16)
 ├── Tratamiento: Endodoncia (pieza 26)
 └── Tratamiento: Corona de porcelana (pieza 11)

↓

Presupuesto

Obturación simple .......... $45.000
Endodoncia ................. $95.000
Corona porcelana .......... $280.000

Total ..................... $420.000

¿Por qué no calcularlo directamente desde el odontograma?

Porque los precios cambian.

Si mañana la corona pasa de $280.000 a $320.000, un presupuesto emitido hace dos meses debe seguir mostrando $280.000.

Por eso normalmente existen dos entidades:

  • Odontograma → información clínica.

  • Presupuesto → fotografía comercial en un momento determinado.

Qué tablas tendría

prestaciones
------------
id
codigo
descripcion
precio_actual
...

presupuestos
------------
id
paciente_id
fecha
estado
subtotal
descuento
total
...

presupuesto_items
-----------------
id
presupuesto_id
prestacion_id
pieza
cantidad
precio_unitario
importe

Observá que precio_unitario queda copiado en el presupuesto aunque luego cambie el precio de la prestación.

Flujo de trabajo

  1. El odontólogo marca tratamientos en el odontograma.

  2. Presiona Generar presupuesto.

  3. El sistema propone automáticamente todos los tratamientos pendientes.

  4. El profesional puede quitar algunos, agregar otros o modificar cantidades.

  5. Se guarda el presupuesto.

  6. Más adelante pueden generarse nuevos presupuestos sin modificar el odontograma.

Una mejora interesante

En lugar de guardar en el odontograma un texto como:

{
    "tratamiento": "Obturación"
}

conviene guardar el id de la prestación:

{
    "prestacionId": 17
}

Así el presupuesto puede obtener automáticamente:

  • descripción,

  • precio vigente,

  • duración estimada,

  • color para el dibujo,

  • código interno,

  • cobertura de obra social, etc.

De esa manera, el odontograma permanece como un documento clínico, mientras que el presupuesto se convierte en una "instantánea" económica generada a partir de él, manteniendo ambos conceptos desacoplados y permitiendo que evolucionen de forma independiente.


Quiero una narración cronológica de cómo va pasando de una instancia a la otra.

Una forma bastante natural de verlo es pensar que cada instancia representa una etapa distinta del trabajo del odontólogo, y que cada una congela cierta información para no depender de cambios posteriores.

1. Odontograma (estado clínico)

El paciente llega al consultorio. El odontólogo examina la boca y registra el estado de cada pieza dental.

Por ejemplo:

  • Pieza 11: caries.

  • Pieza 16: restauración en mal estado.

  • Pieza 26: endodoncia indicada.

En esta etapa no hay decisión terapéutica ni precios. Sólo existe el diagnóstico y el estado de la boca.

Pregunta que responde: ¿Qué tiene el paciente?


2. Treatment Plan (plan de tratamiento)

A partir del diagnóstico, el odontólogo decide qué propone hacer.

Por ejemplo:

  • Pieza 11 → Restauración con resina.

  • Pieza 16 → Reemplazar restauración.

  • Pieza 26 → Endodoncia y luego corona.

Todavía no se habla de dinero. Incluso pueden existir varios planes alternativos.

Por ejemplo:

  • Plan A: conservar la pieza mediante endodoncia.

  • Plan B: extraer la pieza e implantar un implante.

Pregunta que responde: ¿Qué tratamiento se propone?


3. Treatment Estimate (presupuesto)

Una vez elegido un plan, el sistema toma cada procedimiento y busca su precio vigente.

El presupuesto queda, por ejemplo:

PrestaciónPrecio
Restauración$45.000
Endodoncia$95.000
Corona$280.000
Total$420.000

En este momento el presupuesto se congela.

Aunque dentro de un mes la corona aumente a $320.000, este presupuesto seguirá mostrando $280.000.

También puede registrar:

  • descuentos,

  • financiación,

  • obra social,

  • impuestos,

  • vigencia del presupuesto.

Pregunta que responde: ¿Cuánto costará?


4. Invoice (factura)

Cuando el paciente acepta el presupuesto y comienzan los tratamientos, llega el momento de facturar.

La factura representa una operación económica real.

Puede ocurrir que:

  • el presupuesto era por $420.000;

  • el paciente decidió postergar la corona;

  • hoy sólo se facturan la restauración y la endodoncia.

La factura queda por $140.000.

Más adelante podrá emitirse otra factura por la corona.

Pregunta que responde: ¿Qué se cobró realmente?


El flujo completo

Paciente llega

        │
        ▼
 Odontograma
 "¿Qué tiene?"

        │
        ▼
 Treatment Plan
 "¿Qué proponemos hacer?"

        │
        ▼
 Treatment Estimate
 "¿Cuánto costará?"

        │
Paciente acepta

        │
        ▼
Se realizan prestaciones

        │
        ▼
 Invoice
 "¿Qué se cobró?"

¿Es obligatorio tener las cuatro entidades?

No necesariamente.

Para un software odontológico pequeño o mediano, una arquitectura muy razonable sería:

Odontograma
      │
      ▼
Presupuesto
      │
      ▼
Factura

En ese diseño, el plan de tratamiento no existe como entidad independiente: el propio odontograma contiene los tratamientos indicados, y el presupuesto se genera seleccionando algunos o todos ellos.

De hecho, muchos sistemas odontológicos funcionan así. Separar Treatment Plan como entidad propia cobra más sentido cuando se necesita soportar planes alternativos, revisiones de planes o flujos clínicos más complejos. Si tu objetivo es mantener Dentalia simple pero bien diseñado, podrías empezar con Odontograma → Presupuesto → Factura y extraer un TreatmentPlan más adelante sólo si aparecen requisitos que realmente lo justifiquen.


¿Esto que estoy haciendo de presupuesto se llama funcionalidad, módulo, o cómo?

Depende del nivel del que estés hablando en la arquitectura del sistema.

  • Funcionalidad (feature): es el término más general. Desde el punto de vista del usuario, "hacer un presupuesto" es una funcionalidad.

  • Módulo (module): es un conjunto de funcionalidades relacionadas que forman una parte identificable del sistema. Si tu sistema tiene menú lateral con "Pacientes", "Turnos", "Odontograma", "Presupuestos", "Facturación", entonces Presupuestos es un módulo.

  • Submódulo: si el módulo de Presupuestos luego incluye, por ejemplo, "Plantillas", "Listas de precios", "Presupuestos" y "Aprobaciones", cada uno podría considerarse un submódulo.

  • Caso de uso (use case): desde el análisis de requisitos, "Generar presupuesto" o "Aceptar presupuesto" son casos de uso.

En tu proyecto Dentalia, yo lo describiría así:

Módulo: Presupuestos

Funcionalidades:
- Generar presupuesto desde el odontograma.
- Editar presupuesto.
- Recalcular precios.
- Imprimir presupuesto.
- Enviar presupuesto por e-mail.
- Registrar aceptación o rechazo.
- Convertir presupuesto en factura.

Así mantenés una terminología consistente:

  • Odontograma → módulo.

  • Presupuestos → módulo.

  • Facturación → módulo.

  • Pacientes → módulo.

Y dentro de cada uno, sus respectivas funcionalidades.

Es una forma de organizar el sistema que resulta clara tanto para la documentación como para la planificación del desarrollo.

miércoles, 1 de julio de 2026

Eloquent, atributos dinámicos y autocompletado en PHP y en Typescript

 Tengo un modelo Eloquent que fue generado con asistencia de IA.

El modelo es Paciente: class Paciente extends Model.

Y tiene métodos que hacen algo con los atributos del modelo, algo que todavía no entiendo bien.

Estos son:

    public function getFullNameAttribute()

    public function getFormattedAddressAttribute()

    public function getFormattedPhoneAttribute()

    public function getSocialSecurityInfoAttribute()


¿Por qué el asistente de código de IA hizo que todos estos métodos que él creó sean nombrados de modo que todos llevan el sufijo Attribute?


La IA lo hizo así porque está siguiendo una convención propia de Eloquent. Esos métodos no son métodos "comunes", sino accessors (accesores), es decir, métodos especiales que Laravel invoca automáticamente cuando accedes a un atributo del modelo.

Por ejemplo, si tienes:

class Paciente extends Model
{
    public function getFullNameAttribute()
    {
        return "{$this->apellido}, {$this->nombre}";
    }
}

Puedes escribir:

echo $paciente->full_name;

aunque la columna full_name no exista en la base de datos.

Laravel hace internamente algo equivalente a:

"¿Me están pidiendo el atributo full_name? Entonces busco un método llamado getFullNameAttribute()."

Por eso el nombre tiene esa estructura tan rígida:

get + NombreDelAtributo + Attribute

No es casualidad; Laravel la interpreta automáticamente.


¿Cómo convierte el nombre?

El atributo:

full_name

se convierte en el método:

getFullNameAttribute

Otro ejemplo:

formatted_phone

getFormattedPhoneAttribute

Y luego puedes hacer:

$paciente->formatted_phone;

como si fuera una propiedad normal.


¿Para qué sirven?

Se usan para exponer atributos calculados.

Por ejemplo, si la tabla tiene:

nombreapellido
JuanPérez

puedes crear:

public function getFullNameAttribute()
{
    return "{$this->nombre} {$this->apellido}";
}

y luego:

echo $paciente->full_name;

obtiene:

Juan Pérez

sin necesidad de almacenar full_name en la base.


En tu modelo

Por los nombres que mencionas, imagino que hacen algo parecido a esto:

getFullNameAttribute()

devuelve:

Juan Pérez

getFormattedAddressAttribute()

devuelve:

Av. Corrientes 1234, CABA

getFormattedPhoneAttribute()

devuelve:

(11) 4321-1234

getSocialSecurityInfoAttribute()

devuelve algo como:

OSDE 210 - Nº 123456

¿Se usan solo para leer?

No. Existe el mecanismo inverso: los mutators.

Por ejemplo:

public function setNombreAttribute($value)
{
    $this->attributes['nombre'] = strtoupper($value);
}

Entonces:

$paciente->nombre = "juan";

guardará:

JUAN

en la base.


Laravel moderno

Desde versiones recientes de Laravel, la forma recomendada es usar la clase Eloquent ORM Attribute, que reúne el accessor y el mutator en un único método:

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function fullName(): Attribute
{
    return Attribute::make(
        get: fn () => "{$this->apellido}, {$this->nombre}",
    );
}

El uso desde el resto de la aplicación sigue siendo el mismo:

$paciente->full_name;

Un detalle importante

Tus métodos devuelven información derivada de los datos del paciente, no realizan acciones. Por eso el asistente de IA los implementó como accessors y no como métodos comunes.

Si en cambio el modelo tuviera un método como:

$paciente->enviarRecordatorio();

ese no llevaría el sufijo Attribute, porque representa un comportamiento (una acción), no un atributo calculado.

En resumen, el sufijo Attribute le indica a Eloquent: "este método define cómo obtener el valor de un atributo del modelo". Es una convención del framework para que puedas acceder a esos valores como si fueran propiedades normales del objeto.


¿Es mucho pedir tener asistencia de autocompletado de atributos del modelo en el IDE?


No, de hecho es algo esperable. El problema es que Eloquent usa muchas propiedades "mágicas" (__get y __set), y un IDE no puede inferirlas por sí solo. Afortunadamente, hay varias soluciones.

Opción 1 (la más usada): Laravel Idea (de pago)

Si usas PhpStorm, el plugin Laravel Idea ofrece un autocompletado excelente:

  • Atributos de modelos.

  • Relaciones.

  • Accessors y mutators.

  • Rutas.

  • Vistas.

  • Configuración.

  • Mucho más.

Es probablemente la mejor experiencia para desarrollar con Laravel.


Opción 2 (gratuita): barryvdh/laravel-ide-helper

Es el estándar de facto para mejorar el autocompletado.

Se instala como dependencia de desarrollo y genera anotaciones PHPDoc en los modelos o en archivos auxiliares.

Por ejemplo, tu modelo Paciente puede quedar anotado así:

/**
 * @property int $id
 * @property string $nombre
 * @property string $apellido
 * @property string $telefono
 * @property-read string $full_name
 * @property-read string $formatted_phone
 */
class Paciente extends Model
{
    ...
}

Entonces el IDE sabe que existen:

$paciente->nombre;
$paciente->apellido;
$paciente->full_name;
$paciente->formatted_phone;

y ofrece autocompletado y análisis estático.


Opción 3: Anotarlo a mano

Puedes escribir las anotaciones tú mismo:

/**
 * @property string $nombre
 * @property string $apellido
 * @property-read string $full_name
 */
class Paciente extends Model
{
}

Funciona, pero es tedioso y hay que mantenerlo actualizado.


Opción 4: PHPStan + Larastan

Si además quieres análisis estático potente, puedes usar PHPStan junto con Larastan.

No solo mejora el autocompletado, sino que detecta errores como:

  • acceder a un atributo inexistente;

  • llamar a una relación que no existe;

  • errores de tipos.

Es muy recomendable cuando el proyecto crece.


En tu caso

Como conozco un poco tu forma de trabajar (te gusta aprovechar las capacidades del IDE y mantener el código limpio), yo usaría:

  • barryvdh/laravel-ide-helper si buscas una solución gratuita.

  • Laravel Idea si utilizas PhpStorm y no te importa pagar una licencia.

Con cualquiera de las dos tendrás autocompletado de:

  • columnas de la base de datos;

  • relaciones ($paciente->odontogramas, por ejemplo);

  • accessors como full_name y formatted_phone;

  • muchos otros elementos del ecosistema Laravel.

Es una mejora de productividad muy notable cuando trabajas con modelos Eloquent.


¿Puedo tener autocompletado en Typescript de atributos de modelos de Eloquent que le paso a la página de Inertia?


Sí. De hecho, es una de las mejores prácticas cuando se trabaja con Laravel + Inertia + React + TypeScript.

La idea es que no tipas el modelo Eloquent, sino el JSON que Inertia le envía a React.

Por ejemplo, si desde Laravel haces:

return Inertia::render('Paciente/Edit', [
    'paciente' => $paciente,
]);

En el navegador ya no existe un objeto Eloquent. Lo que recibe React es algo equivalente a:

{
  "id": 15,
  "nombre": "Juan",
  "apellido": "Pérez",
  "fecha_nacimiento": "1980-05-01"
}

Entonces defines una interfaz en TypeScript:

export interface Paciente {
    id: number;
    nombre: string;
    apellido: string;
    fecha_nacimiento: string | null;
}

Y luego tipas las props de la página:

interface Props {
    paciente: Paciente;
}

const { paciente } = usePage<Props>().props;

console.log(paciente.nombre);     // ✔ autocompletado
console.log(paciente.apellido);   // ✔ autocompletado

¿Qué pasa con los accessors?

Supongamos que en Eloquent tienes:

public function getFullNameAttribute()
{
    return "{$this->apellido}, {$this->nombre}";
}

Si haces que ese atributo forme parte del JSON (por ejemplo agregándolo al arreglo $appends del modelo):

protected $appends = [
    'full_name',
];

entonces Inertia enviará:

{
    "nombre": "Juan",
    "apellido": "Pérez",
    "full_name": "Pérez, Juan"
}

y simplemente lo agregas a la interfaz:

export interface Paciente {
    id: number;
    nombre: string;
    apellido: string;
    full_name: string;
}

También tendrás autocompletado para paciente.full_name.


El inconveniente

Hay una desventaja: duplicas la definición del modelo.

Lo tienes en PHP:

class Paciente extends Model

y otra vez en TypeScript:

interface Paciente

Si agregas una columna nueva, debes actualizar ambos.


¿Hay forma de evitar la duplicación?

Sí. Existen herramientas que generan automáticamente las interfaces TypeScript a partir de tus modelos o de tus recursos de API.

Algunas de las más utilizadas son:

  • Spatie Laravel Data

  • Laravel TypeScript Transformer

  • Momentum Trail

Estas herramientas mantienen sincronizados los tipos entre PHP y TypeScript.


Para tu proyecto

Por lo que conozco de Dentalia, todavía no introduciría una herramienta de generación automática.

Crearía un archivo como:

resources/js/types/paciente.ts

con:

export interface Paciente {
    id: number;
    nombre: string;
    apellido: string;
    // ...
}

y reutilizaría esa interfaz en todas las páginas y componentes.

Cuando el proyecto crezca (decenas de modelos y cientos de props), ahí sí evaluaría incorporar una solución que genere automáticamente los tipos desde PHP. En esta etapa, mantener unas pocas interfaces TypeScript a mano suele ser más simple y te da exactamente el autocompletado que buscas.

martes, 30 de junio de 2026

Migraciones en Laravel

Quiero en una migración, cambiar el nombre de una tabla.

En una migración de Laravel puedes cambiar el nombre de una tabla usando el método:

Schema::rename('nombre_actual', 'nuevo_nombre');

Ejemplo completo de migración

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::rename('pacientes', 'patients');
}

public function down(): void
{
Schema::rename('patients', 'pacientes');
}
};

Cosas a tener en cuenta

  • Esto solo cambia el nombre de la tabla, no toca columnas ni datos.
  • Laravel no hace magia con relaciones: si tienes foreign keys, asegúrate de que sigan funcionando (en la mayoría de casos no se rompen, pero depende del motor y constraints).
  • Siempre define el down() para poder revertir con php artisan migrate:rollback.

Si usas SQLite o casos raros

Algunas bases pueden requerir cuidado extra con constraints, pero en MySQL/PostgreSQL normalmente funciona directo.


Si quieres, puedo ayudarte a hacer un “rename seguro” cuando hay foreign keys involucradas (ahí ya hay un par de trampas típicas). 


¿Hay workflows predefinidos para trabajar con versionado de código, migraciones y seeds?

Sí. En Laravel y en desarrollo web en general hay workflows bastante establecidos que combinan versionado (Git), migraciones y seeds/seeders para mantener consistencia entre entornos.


🧭 Workflow típico recomendado

1. Versionado (Git como base)

  • Todo cambio de esquema va en una rama (feature/...)

  • Cada cambio de DB = commit junto con su migración

  • Nunca se edita una migración ya ejecutada en producción (se crea otra nueva)


2. Migraciones (estructura de la base)

Flujo estándar:

  1. Crear migración

    php artisan make:migration create_pacientes_table
    
  2. Definir cambios en up() / down()

  3. Ejecutar localmente:

    php artisan migrate
    
  4. Commit junto con el código que depende de esa tabla

👉 Regla clave: la base de datos también “viaja en Git” a través de migraciones


3. Seeders (datos iniciales o de prueba)

Workflow típico:

  • Crear seeder:

    php artisan make:seeder PacienteSeeder
    
  • Ejecutar:

    php artisan db:seed
    

O en conjunto con migraciones:

php artisan migrate --seed

4. Refresh completo (entornos de desarrollo)

Muy usado para resetear todo:

php artisan migrate:fresh --seed

👉 Borra todo y reconstruye DB desde cero.


5. Workflow combinado (el “ideal”)

Cuando haces una feature:

  1. Crear migración nueva

  2. Crear/actualizar modelos + lógica

  3. Crear o actualizar seeder si aplica

  4. Commit:

    • migración

    • seeders

    • código

  5. En otro entorno:

    git pull
    php artisan migrate
    php artisan db:seed
    

🧠 Buenas prácticas clave

  • Nunca modifiques migraciones ya compartidas (crea nuevas)

  • Seeders no son para producción crítica (salvo defaults)

  • Usa factories + seeders para datos fake

  • Mantén migraciones idempotentes y reversibles

  • Una migración = una intención clara


Si quieres, puedo proponerte un workflow “pro” tipo equipo (con staging, producción, rollback seguro y versionado de schema más estricto).


En teoría, si hago uno por uno el checkout de cada commit, ejecuto las migraciones según un workflow preestablecido, y ejecuto los seeders según un workflow preestablecido, no tendría que aparecer ningún error, y la app tendría que poder levantar con sus datos acordes a cada commit que checkoutee. ¿Qué hay de cierto en esta afirmación?

Tu afirmación es correcta en teoría, pero no es garantizable en la práctica sin condiciones muy estrictas.


✅ Qué es cierto

Si cumples estas condiciones:

  • migraciones siempre backward-compatible

  • seeders deterministas (no dependen de estado externo cambiante)

  • orden de ejecución consistente (migrate en orden histórico)

  • el código de cada commit es coherente con su schema

  • no hay datos manuales fuera de migraciones/seeders

Entonces sí:
👉 cada commit debería poder levantar la app como una “foto consistente” del sistema

Esto es el ideal del database versioning through migrations.


⚠️ Por qué en la práctica falla a menudo

1. Migraciones no reversibles o destructivas

Ejemplos problemáticos:

  • rename + código que aún espera el nombre viejo

  • drop column sin compatibilidad hacia atrás

  • cambios de tipo que rompen queries anteriores

👉 Rompen el “checkout histórico”.


2. Código depende de estado de datos, no del schema

Ejemplo:

  • un seeder cambió en un commit posterior

  • o datos manuales en dev que no están versionados

👉 El commit no es autosuficiente.


3. Seeders no deterministas

Si haces:

  • factory()->create() sin control

  • timestamps variables

  • datos externos (API, random sin seed fijo)

👉 el resultado cambia aunque el commit sea el mismo.


4. Falta de “reproducibilidad completa”

Laravel no garantiza automáticamente:

  • orden exacto histórico de migraciones

  • rollback perfecto de cambios complejos

  • compatibilidad entre versiones intermedias


5. Dependencia del entorno

  • MySQL vs PostgreSQL

  • versiones distintas del motor

  • charset/collation

  • config de Laravel


🧠 La idea clave

Lo que describes es un sistema tipo:

“cada commit es un snapshot ejecutable del sistema completo”

Eso es posible, pero solo si lo diseñas como:

👉 “migrations + seeders como sistema de reconstrucción determinista”

No como “historial evolutivo libre”.


🟢 Cómo hacerlo realmente robusto

Si quisieras acercarte al 100%:

  • migraciones solo additive o reversible

  • seeders versionados por estado (no acumulativos caóticos)

  • migrate:fresh --seed como única forma confiable de reconstrucción

  • evitar modificaciones destructivas en migraciones viejas

  • usar factories con seeds fijos (seed RNG controlado)


🔥 Conclusión

  • ✔️ Es una idea válida y muy potente

  • ⚠️ Pero solo funciona como garantía si impones disciplina fuerte

  • ❌ En la práctica, muchos proyectos rompen esa propiedad con el tiempo


Si quieres, puedo ayudarte a diseñar un workflow “tipo Git bisectable database”, donde realmente puedas moverte commit por commit sin romper nada.


Quiero renombrar 1 tabla. Estoy dudando entre por una parte tocar la migración de la tabla original y hacer como si se creó con el nuevo nombre desde la primera vez, y por otra parte hacer una migración distinta que renombre la tabla. No sé cuál elegir porque no entiendo bien las implicancias ni las preferencias o workflows establecidos por la comunidad de desarrollo.

La duda que tenés es muy común, y en realidad estás eligiendo entre dos filosofías distintas de migraciones.

Te explico claramente las implicancias de cada opción y qué se usa en la práctica.


🧭 Opción 1: Editar la migración original (reescribir historia)

👉 Cambiar la migración inicial para que la tabla “siempre haya tenido” el nuevo nombre.

❌ Problemas

  • Rompe el principio de inmutabilidad de migraciones

  • Si el proyecto ya está compartido o en producción:

    • otros desarrolladores ya ejecutaron la migración vieja

    • sus bases quedan desincronizadas

  • Git pierde valor como historial confiable de DB

  • No es reproducible hacia atrás

⚠️ Solo es aceptable si:

  • estás en desarrollo temprano

  • nadie más la ejecutó

  • puedes borrar toda la base sin consecuencias

👉 Esto es básicamente “resetear la historia”.


🧭 Opción 2: Crear una migración de rename (recomendado)

Schema::rename('old_table', 'new_table');

✅ Ventajas

  • Respeta el principio de append-only schema evolution

  • Funciona en equipos

  • Funciona en producción

  • Es reversible (down() puede revertir el rename)

  • Mantiene consistencia con el historial de DB

👍 Es el estándar de la comunidad

👉 En Laravel, este es el enfoque correcto casi siempre.


🧠 Regla de oro de la comunidad

“Las migraciones nunca se editan después de ser ejecutadas en entornos compartidos”

Se asume que:

  • cada migración es un “evento histórico”

  • la base de datos evoluciona, no se reescribe


⚖️ Comparación directa

CriterioEditar migraciónMigración de rename
Seguridad en equipo❌ Mala✅ Buena
Producción❌ Riesgoso✅ Correcto
Historial Git❌ Falso✅ Honesto
Reproducibilidad❌ Pierde consistencia✅ Mantiene consistencia
Uso recomendadosolo early devsiempre

🟢 Recomendación clara

👉 Usá migración de rename

Es la opción correcta en:

  • proyectos reales

  • equipos

  • apps que van a crecer

  • cualquier caso donde no quieras sorpresas


💡 Insight importante (la parte que te ayuda a decidir bien)

En migraciones, el objetivo no es “tener la base de datos más limpia posible”, sino tener una historia confiable de cambios ejecutables en cualquier momento.

Eso cambia completamente la forma de pensar:

  • No estás modelando “cómo debería verse la base ideal”

  • Estás modelando “cómo evolucionó la base en el tiempo”


🧠 Consecuencia clave

Editar una migración vieja equivale a decir:

“La historia nunca pasó así, en realidad pasó distinto”

Eso rompe un supuesto fundamental de los sistemas de migración:

👉 que el historial es reproducible


🔥 Por qué esto importa en la práctica

Imaginá este escenario real:

  • Dev A ejecutó migraciones hace 2 semanas

  • Dev B clona el repo hoy

  • Vos editás una migración vieja para renombrar una tabla

Resultado:

  • Dev A tiene una tabla con nombre viejo en su DB

  • Dev B crea la nueva versión

  • El código empieza a fallar de forma inconsistente

👉 Este tipo de divergencias son exactamente lo que las migraciones intentan evitar.


🟢 El principio profesional real

En equipos serios con Laravel o cualquier ORM:

“Si algo ya fue compartido o ejecutado, no se edita: se evoluciona”

Eso incluye:

  • renames

  • alter table

  • cambios de tipo

  • deletes

Todo eso se modela como nuevas migraciones.


⚖️ Regla mental simple

Te sirve como heurística:

  • 🟢 ¿Aún no se ejecutó en ningún lado? → podés editar

  • 🔴 ¿Ya se ejecutó en algún entorno? → no se toca, se migra


🧩 Conclusión final

En tu caso específico:

👉 renombrar una tabla = migración nueva con Schema::rename()

Es lo más seguro, estándar y compatible con workflows reales.


Si querés, en el próximo paso puedo ayudarte a ver algo más interesante:
👉 cómo diseñar migraciones para que sean “reordenables mentalmente” como un timeline sin generar deuda técnica.


Es un proyecto que yo considero muy pequeño y simple, que todavía no está ni cerca de ser desplegado en un ambiente compartido y por mucho tiempo será así ya que yo soy el único desarrollador. Estoy pensando que para un proyecto con tales características lo mejor es modificar directamente el archivo 2025_08_19_005459_create_odontogramas_table.php para fingir que la tabla fue creada con el nombre cambiado desde el principio y ejecutar un php artisan migrate:fresh. ¿Es esto muy aberrante en términos prácticas, metodologías o incluso workflows?

No, no es aberrante en absoluto. De hecho, en el contexto que describís, muchos desarrolladores harían exactamente eso.

La clave está en distinguir entre un proyecto en evolución temprana y un proyecto con historial que ya debe preservarse.

En tu caso, se cumplen prácticamente todas las condiciones para reescribir la historia:

  • ✅ Sos el único desarrollador.

  • ✅ No hay ambiente compartido.

  • ✅ No hay producción.

  • ✅ No necesitás preservar datos.

  • ✅ No te molesta ejecutar php artisan migrate:fresh.

En ese contexto, modificar:

2025_08_19_005459_create_odontogramas_table.php

para que cree la tabla con el nombre definitivo y luego ejecutar:

php artisan migrate:fresh

es una decisión perfectamente razonable.

De hecho, hay dos escuelas de pensamiento

1. Las migraciones como historia (la más difundida)

Una vez creada una migración, nunca se modifica. Se agregan nuevas migraciones.

Esta filosofía es ideal cuando ya existe una historia que preservar.

2. Las migraciones como descripción del estado inicial (muy usada en proyectos pequeños)

Mientras el proyecto está "en construcción", las migraciones se van corrigiendo para que reflejen la mejor versión del esquema. Cuando se necesita reconstruir la base, simplemente se hace:

php artisan migrate:fresh --seed

Esta filosofía evita acumular migraciones "de ruido", por ejemplo:

create_odontogramas_table
rename_odontogramas_to_odontograms
rename_odontograms_to_dental_charts
add_column_x
remove_column_x
rename_column_x

Cuando en realidad el proyecto nunca salió de desarrollo.

Mi criterio personal

Yo usaría esta regla:

  • Mientras no exista ningún entorno cuyo historial de migraciones deba respetarse, mantendría las migraciones limpias y editaría las existentes cuando eso haga que representen mejor el diseño actual.

  • A partir del momento en que exista un ambiente compartido, un cliente usando el sistema o simplemente quieras que el historial sea inmutable, dejaría de editar migraciones existentes y comenzaría a crear únicamente migraciones nuevas.

Es como si hubiera un punto de "congelamiento" del historial.

Una ventaja adicional

Si dentro de seis meses volvés a este proyecto y hacés:

php artisan migrate:fresh --seed

vas a obtener directamente el esquema definitivo, sin tener que ejecutar una larga cadena de migraciones cuyo único propósito fue corregir decisiones tomadas durante el diseño.


En resumen, con el estado actual de tu proyecto yo también modificaría la migración original. No lo considero una mala práctica; lo consideraría una mala práctica si siguieras haciéndolo después de que el proyecto tenga varios entornos, otros desarrolladores o datos que preservar. La diferencia no está en el código, sino en la etapa del ciclo de vida del proyecto.

jueves, 25 de junio de 2026

Sincronización y persistencia de odontograma

 Quiero que me preguntes qué requerimientos estoy necesitando cubrir para recomendarme una solución de sincronización frontend-backend, con lectura y grabado en base de datos.

Perfecto. Antes de recomendar una arquitectura (REST, Inertia, Server Actions, WebSockets, polling, CQRS, cache local, sincronización optimista, etc.), necesito entender algunos aspectos de tu caso de uso.

Respondé lo que puedas:

1. Frecuencia de actualización

  • ¿Los datos cambian sólo cuando el usuario hace acciones?
  • ¿O pueden cambiar por otros usuarios/procesos mientras la pantalla está abierta?

2. Tiempo real

  • ¿Necesitás que los cambios aparezcan casi instantáneamente?
  • ¿Cuántos segundos de demora serían aceptables?

3. Volumen de datos

  • ¿Cuántos registros suele manejar una pantalla?
    • decenas
    • cientos
    • miles
    • decenas de miles

4. Patrón de uso

  • ¿Hay muchas más lecturas que escrituras?
  • ¿O las escrituras son frecuentes?

5. Consistencia

  • ¿Es crítico que el usuario vea siempre el estado más reciente de la base de datos?
  • ¿O tolerás que vea datos levemente desactualizados durante algunos segundos?

6. Conflictos

  • ¿Puede ocurrir que dos usuarios modifiquen simultáneamente la misma entidad?
  • Si ocurre, ¿qué debería pasar?

7. UX

  • ¿Querés que las modificaciones se reflejen instantáneamente en la UI (optimistic updates)?
  • ¿O preferís esperar la confirmación del backend?

8. Navegación

  • ¿Tu aplicación es principalmente:
    • formularios
    • CRUD administrativo
    • dashboard
    • editor complejo
    • tiempo real colaborativo
    • otra cosa?

9. Stack actual

  • ¿Qué usás exactamente?
    • React
    • Inertia
    • Laravel
    • Vite
    • TypeScript
    • otra cosa

10. Estado frontend

  • ¿Actualmente usás algo como:
    • Context
    • Zustand
    • Redux
    • React Query / TanStack Query
    • SWR
    • nada

11. Offline

  • ¿La aplicación debe seguir funcionando sin conexión y sincronizar después?

12. Prioridades

Ordená estas prioridades:

  • simplicidad
  • rendimiento
  • consistencia
  • tiempo real
  • facilidad de mantenimiento
  • escalabilidad

13. Caso concreto

Describime una pantalla representativa:

"Tengo un odontograma. El usuario modifica piezas dentales, se guardan en Laravel y otros usuarios pueden verlo."

o el caso que corresponda.

Con esas respuestas puedo recomendarte una estrategia concreta de sincronización frontend ↔ backend ↔ base de datos en lugar de una lista genérica de tecnologías.



1. Los datos cambian sólo cuando el usuario hace acciones. 
2. Puedo tolerar 5 segundos de demora, siempre y cuando haya un indicador en pantalla mientras todavía no están los nuevoa datos listos para mostrar. 
3. El volumen de datos de una pantalla es un JSON como el siguiente: 
export default
{
version: 1,
/* protesis: dentadura, enlaces, soluciones
* extraccion: dentadura, soluciones
* implante: dentadura, soluciones
* ortodoncia: enlaces, soluciones
*/
dentadura: [
/* tipo: protesis | muela | muela-leche | diente | diente-leche | edentulismo | extraccion |
* | implante |
*/
{ tipo: 'protesis', numero: '17', casillero: 4 },
{ tipo: 'protesis', numero: '16', casillero: 6 },
{ tipo: 'protesis', numero: '15', casillero: 8 },
{ tipo: 'muela', numero: '14(10)', casillero: 10 },
{ tipo: 'muela', casillero: 11},
//...
],
marcas_dentales: [
/* codigo: caries | obturacion-pendiente | obturacion-realizada
*
*/
{ codigo: 'obturacion-pendiente', casillero: 10, superficie: 1, presupuestado: false },
{ codigo: 'obturacion-realizada', casillero: 12, superficie: 2, presupuestado: true },
//...
],
problemas : [
/* tipo: desgaste | desplazamiento | discromia | ectopia | erupcion | extrusion
* | fractura | impacto | intrusion | macrodoncia | microdoncia | necrosis
* | remanente-radicular | rotacion | semi-impacto |
*/
{ tipo: 'remanente-radicular', casillero: 10 },
{ tipo: 'extrusion', casillero: 10 },
{ tipo: 'macrodoncia', casillero: 10 },
//...
],
// Array ordenado por primer_casillero
// de enlaces entre casilleros
enlaces : [
/* tipo: diastema | fusion | ortodoncia | protesis | transposicion
*
*/
{ tipo: 'protesis', primer_casillero: 4, ultimo_casillero: 8 },
{ tipo: 'protesis', primer_casillero: 12, ultimo_casillero: 16 },
// ...
],
soluciones: [
/* tipo: extraccion | implante | ortodoncia | perno-corona | protesis | tratamiento-conducto
*
*/
{ tipo: 'protesis', casillero: 4, presupuestado: true, realizado: true },
{ tipo: 'extraccion', casillero: 10, presupuestado: true, realizado: false },
// ...
],
}
4. Hay una lectura inicial y una baja frecuencia de escrituras. 
5. Es crítico que el usuario vea siempre el estado más reciente de la base de datos. 
6. Si 2 usuarios intentan modificar la misma entidad no hay problema en bloquear al segundo en ingresar. 
7. Prefiero esperar a la confirmación del backend. 
8. Editor complejo. Es un odontograma interactivo. Adjunto imagen. 
9. React, Inertia, Laravel, Vite, Typescript. 
10. Nada. 
11. No hace falta que siga funcionando offline. 
12. 1) simplicidad; 2) facilidad de mantenimiento; 3); consistencia; 4) rendimiento; 5) escalabilidad. 
13. Tengo un odontograma. El usuario modifica piezas dentales, patologías, tratamientos. Lo hace mediante un diálogo interactivo en pantalla. En el frontend todo esto se representa en un JSON como el que te compartí en mi respuesta #3.

Con lo que describís, yo no usaría nada sofisticado (WebSockets, Zustand, Redux, CQRS, sincronización offline, etc.). Estarías pagando complejidad sin obtener beneficios.

Tu caso tiene varias características muy favorables:

  • El odontograma completo es un único agregado lógico.
  • El JSON es pequeño (decenas o cientos de KB como mucho).
  • Las escrituras son poco frecuentes.
  • No necesitás tiempo real.
  • Priorizás simplicidad y mantenimiento.
  • Preferís esperar la confirmación del backend.
  • Querés consistencia fuerte.

Lo que implementaría

Backend

Una tabla:

odontogramas
-----------
id
version
contenido_json
updated_at

donde contenido_json es esencialmente el JSON que mostraste.


Frontend

Estado local:

const [odontograma, setOdontograma] =
useState<Odontograma>(props.odontograma)

const [guardando, setGuardando] =
useState(false)

Guardado

Cuando el usuario confirma una modificación:

setGuardando(true)

await router.post(
route('odontograma.guardar'),
odontograma
)

await recargar()

setGuardando(false)

donde:

async function recargar() {
router.reload({
only: ['odontograma']
})
}

o equivalente con fetch.


Lo importante: versionado optimista

Veo un posible problema:

"si dos usuarios intentan modificar la misma entidad no hay problema en bloquear al segundo"

Entonces agregaría:

{
version: 37,
...
}

al JSON.


Al guardar:

if ($request->version !== $odontograma->version) {
abort(409);
}

Si alguien guardó antes:

Usuario A abre v37
Usuario B abre v37

Usuario A guarda
→ v38

Usuario B guarda
→ 409 Conflict

y mostrás:

El odontograma fue modificado por otro usuario.
Recargue la pantalla.

Mejor todavía: lock pesimista

Como dijiste:

"no hay problema en bloquear al segundo"

haría esto.

Al abrir:

odontograma_locks
-----------------
odontograma_id
usuario_id
expires_at

El primer usuario adquiere el lock.

El segundo ve:

El odontograma está siendo editado por Juan Pérez.

y entra en modo lectura.


Qué NO usaría

No usaría:

  • Redux
  • Zustand
  • MobX
  • WebSockets
  • SSE
  • React Query
  • SWR
  • Event Sourcing

porque ninguna de esas herramientas resuelve un problema que realmente tengas.


Lo que sí me preocuparía

Al mirar el JSON veo que el odontograma es básicamente un documento.

Yo evitaría hacer:

POST agregar-caries
POST borrar-caries
POST agregar-fusion
POST agregar-diastema
POST agregar-protesis

etc.

Terminarías con decenas de endpoints.

Preferiría:

PUT /odontograma/:id

enviando el documento completo.

await axios.put(
`/odontograma/${id}`,
odontograma
)

El backend valida y persiste.

La complejidad queda concentrada en un único lugar.


Para tu stack (Laravel + Inertia + React + TS) mi recomendación concreta sería:

  1. Mantener el odontograma completo como un único documento JSON.
  2. Estado local React simple (useState o useRef según corresponda).
  3. Guardado explícito al confirmar cambios.
  4. Recarga desde backend después del guardado exitoso.
  5. Lock pesimista de edición por usuario.
  6. Campo version para detectar inconsistencias si el lock falla.

Eso te da una solución muy consistente y extremadamente simple de mantener.