0007 — Un solo cierre para la historia clínica, con candado configurable
Estado: aceptada · Fecha: 2026-09-21
Contexto
20260907000002 unificó el ciclo de vida de todo lo que se escribe en una historia: nace en
draft, se corrige libremente, y confirmar lo cierra. La intención era buena. El resultado, no.
Quedaron siete botones de cierre: un «Confirmar» por receta, por pedido, por documento, por adjunto y por toma de signos vitales, más un «Firmar» por evolución y otro por escala. Cada uno con su diálogo de dos pasos.
Y además, un borrador no se podía imprimir: el corte estaba en la pantalla
(prescription-section.tsx, botón deshabilitado) y otra vez en el servidor
(app/print/prescription/[id]/page.tsx, corte antes de auditar).
Eso obligaba al médico a confirmar receta por receta antes de poder entregar nada, en mitad de la consulta. El efecto práctico fue el contrario del buscado: se confirmaba sin leer. Un diálogo de dos pasos que se pulsa por inercia no protege nada; solo añade dos clics.
Decisión
1. Un solo botón, en la cabecera del paciente
health_close_record(contact_id) avanza de golpe todo lo que el paciente tenga en borrador, en
una transacción y con una fila de auditoría (health.record.close), porque es un solo acto
clínico. health_open_drafts(contact_id) da el mismo recuento sin tocar nada, para poder decir qué
se va a cerrar antes de que nadie pulse.
Se hace con UPDATE de conjunto y no llamando a las health_confirm_* una a una: esas re-comprueban
el permiso y auditan por fila, y cerrar una consulta con veinte cosas abiertas generaría veinte
apuntes para un solo acto. El folio de la receta se asigna en el INSERT (20260914000002), no al
confirmar, así que el UPDATE de conjunto no se salta nada.
2. Cerrar no mata la historia
No hay closed_at en clinical_records, y es deliberado. El cierre afecta a lo que está abierto
en ese momento; si el paciente vuelve el mes que viene, lo que se escriba entonces nace en borrador
y se cierra con su propio clic.
«Abierta» se deriva de «tiene borradores». Es la única definición que no envejece: una columna
closed_at habría que reabrirla, y reabrir una historia clínica es exactamente lo que no queremos
que exista.
3. Un borrador se imprime, y sale marcado
Se quitaron los cortes de las cuatro páginas de impresión. El papel de un borrador lleva el sello
BORRADOR en diagonal, reutilizando el mismo mecanismo que ya pintaba el de ANULADO en DocSheet.
Esto no relaja nada: antes el borrador no salía en papel pero la RPC lo devolvía igual —el bloqueo eran dos capas de interfaz, no la base—. Ahora sale, y se distingue.
4. Cerrar y bloquear dejan de ser la misma cosa
El cierre emite siempre: fecha, folio, firma electrónica disponible, sin marca de borrador. Que
además quede cerrado a la corrección se decide por sección y por tenant, y solo lo toca un
superadmin (health_save_record_lock), desde Configuración de Salud → Bloqueo.
Actualización (20261107000001). La marca de borrador salió de esa pestaña: es papelería, no política médico-legal, y se cambia con
health_save_draft_watermark, desde Configuración de Salud → Marca. Pide un permiso propio,health.draft_mark.manage(20261109000001), sembrado solo a los roles admin/administrador/owner; nocore.modules.manage, que abre también activar y desactivar módulos. El candado sigue siendo solo del superadmin.
Por defecto todo bloqueado, que es el comportamiento anterior. Una configuración ausente, mal
tecleada, o de un tenant que ni instaló Salud, se comporta exactamente como antes: ausente = true.
ficha no entra en el mapa: los antecedentes no tienen borrador que cerrar.
5. Lo que se escribe se guarda solo
Todo nace en borrador, así que guardar a mitad no tiene coste clínico. La evolución y la receta se autoguardan a los ~2 s de la última tecla, y salir y volver deja el texto donde estaba. El aviso al salir cubre el hueco: entre la última tecla y el rebote, y los borradores que todavía no son guardables (una evolución sin motivo).
Consecuencias
- Desaparecen siete botones y tres bloqueos de impresión. Menos superficie, menos clics de inercia, y el momento del cierre es uno solo y consciente.
- El cierre firma notas ajenas.
signed_by = auth.uid()se pone en todas las evoluciones en borrador del paciente, incluida la que escribió otro profesional. El confirmar por ítem tenía el mismo agujero, pero no de forma sistemática. Mitigación: la meta dehealth.record.closeguarda los ids afectados y sucreated_by. Queda pendiente decidir si debe además excluirlas. - Las escalas piden otro permiso.
health_sign_form_instanceexigehealth.forms.fill, no el de historias. Quien no lo tenga cierra todo lo demás y sus escalas quedan en borrador; se devuelveescalas_omitidasy la pantalla lo dice. No se firma nada con un permiso que no se tiene. - Se relajaron diez guardias de inmutabilidad, en ocho funciones, para que el candado pueda
abrirse. Es lo más delicado de la migración. Se hizo con el patrón de la casa (
replace()sobrepg_get_functiondef, conRAISE EXCEPTIONsi el predicado no aparece), y una fila anulada no vuelve a ser editable jamás, diga lo que diga la configuración. - Riesgo a futuro: una migración que haga
CREATE OR REPLACEde cualquiera de esas ocho funciones se lleva el candado por delante en silencio. La migración termina con un bloque que lo comprueba y revienta, pero solo salta si se reaplica. Volver a aplicar hoy20260915000002dejaríahealth_save_prescriptionsin candado.
Alternativas descartadas
- Cerrar la historia del paciente para siempre. Un paciente que vuelve necesitaría otra historia. No existe tal cosa.
- Que la sección sin candado simplemente no se cierre (se quede en borrador). Más simple y sin tocar ningún guardia, pero esa receta nunca llegaría a ser documento emitido: sin fecha de emisión, con marca de borrador y sin firma electrónica, que solo se ofrece sobre lo emitido.
- Poder reabrir una historia cerrada. El mecanismo de corrección ya existe y deja rastro: la adenda. Reabrir no lo deja.