Saltar al contenido principal

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 CHECK de scheduling_appointments.status;
  • appointment-transitions.ts en 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.substatuses del módulo scheduling: el mismo sitio y el mismo patrón que statusColors.
    • Forma: [{ key, name, color, base_statuses[], active }].
    • Lo escribe el backend (PUT /scheduling/config/substatuses, con scheduling.settings.manage).
    • normalizeSubstatuses exige 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.
  • En la cita: la columna scheduling_appointments.substatus (migración 20261113000002). Es columna y no metadata para 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 a invalid_substatus.
    • Al cambiar solo el estado, si el sub-estado ya no aplica, se pone a NULL en silencio. Nunca bloquea un cambio de estado.
    • Desactivar un sub-estado no lo borra de las citas que ya lo tienen.
  • Escritura: POST /scheduling/appointments/substatus, con el mismo AppointmentWriteGuard que el estado. No toca recordatorios ni emite eventos.
  • Interfaz:
    • SubstatusChip va junto a StatusLabel y 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 de normalizeSubstatuses y setSubstatus, y vitest de la configuración y del selector.