0014 — Sub-estados de cita
Estado: aceptada · Fecha: 2026-09-28
Contexto
Los clientes quieren marcar citas con cosas propias: «Pagado», «Esperando exámenes»,
«Pre-quirúrgico». La primera idea fue «añadir estados», pero los 7 estados
(scheduled … no_show) no son etiquetas: deciden transiciones, recordatorios, eventos de
dominio, el bot, el turnero y los huecos libres.
Esos estados están escritos a mano en unos 15 sitios:
- el
CHECKdescheduling_appointments.status; appointment-transitions.tsen backend y frontend;- las listas de estados sin aviso o de cierre de alertas;
- las herramientas del bot, el turnero, los reportes y
status-visuals.ts.
Un estado por tenant obligaría a que todo eso lo leyera de la base.
Decisión
Un sub-estado es una etiqueta con color encima del estado. No es un estado.
- Catálogo en
tenant_modules.config.substatusesdel móduloscheduling: el mismo sitio y el mismo patrón questatusColors.- Forma:
[{ key, name, color, base_statuses[], active }]. - Lo escribe el backend (
PUT /scheduling/config/substatuses, conscheduling.settings.manage). normalizeSubstatusesexige nombre y al menos un estado base, quita duplicados, deja un máximo de 30 y conserva la clave al renombrar, porque la clave es lo que guarda la cita.
- Forma:
- En la cita: la columna
scheduling_appointments.substatus(migración20261113000002). Es columna y nometadatapara poder filtrar e indexar. No lleva FK porque el catálogo es JSON. - La regla vive en la base, en el trigger
scheduling_appointments_substatus_guard. El estado lo cambian muchas puertas (panel, bot, turnero, cancelación, reapertura) y todas tienen que cumplirla:- Al asignar, si el sub-estado no existe, está desactivado o no aplica al estado actual,
devuelve
23514. El backend lo traduce ainvalid_substatus. - Al cambiar solo el estado, si el sub-estado ya no aplica, se pone a
NULLen silencio. Nunca bloquea un cambio de estado. - Desactivar un sub-estado no lo borra de las citas que ya lo tienen.
- Al asignar, si el sub-estado no existe, está desactivado o no aplica al estado actual,
devuelve
- Escritura:
POST /scheduling/appointments/substatus, con el mismoAppointmentWriteGuardque el estado. No toca recordatorios ni emite eventos. - Interfaz:
SubstatusChipva junto aStatusLabely nunca reemplaza el color del estado.- La configuración lleva un aviso fijo de alcance y límites, un tooltip por campo, vista previa,
contador y confirmación con el número de citas afectadas antes de guardar un cambio
destructivo (
GET /scheduling/config/substatuses/usage).
Consecuencias
- Un tenant puede marcar citas sin que nadie toque la máquina de estados.
- Pedir un paso nuevo del flujo sigue siendo un cambio de producto (un estado nuevo). La interfaz lo dice explícitamente para que no se intente con sub-estados.
- Si se borra un sub-estado con citas que lo usan, esas citas lo muestran en gris hasta su próximo cambio de estado, cuando el trigger lo quita.
- Pruebas:
supabase/tests/appointment_substatus/(trigger y accesos), jest denormalizeSubstatusesysetSubstatus, y vitest de la configuración y del selector.