Saltar al contenido principal

0013 — Etiquetas de recurso y filtros de la agenda

Estado: aceptada · Fecha: 2026-09-28

Contexto​

En tenants con muchos recursos (Manta Hospital Center: decenas de médicos y consultorios) la agenda tenía cuatro problemas:

  • Vista día ilegible. Con más de 12 recursos y los filtros en «Todos», el calendario dejaba de pintar columnas y metía todas las citas en una sola. Con menos, cada columna se aplastaba.
  • Sin forma de acotar. Solo había filtros de sede, piso y recurso. No se podía pedir «solo consultorios» ni «solo pediatría».
  • La sede escondía médicos. scheduling_resources.location_id es NULL a propósito para quien rota entre sedes, pero el catálogo se filtraba con location_id = X. Al elegir una sede, esos médicos perdían su columna y sus citas no se pintaban.
  • Carga sin aviso y con lecturas de más. No había indicador mientras llegaban las citas, los bloqueos se pedían dos o tres veces al abrir, una respuesta vieja podía pisar a la nueva, y en español el último domingo de la semana y del mes nunca se pedía (weekStartsOn 0 contra una rejilla que empieza en lunes).

Decisión​

Etiquetas: el mismo catálogo, con entity_type = 'resource'​

No se crea un catálogo nuevo. crm_tags ya es plataforma (ver 0004) y tiene su editor en Configuración → Etiquetas. La migración 20261113000001:

  • Amplía el CHECK de crm_tags.entity_type con 'resource'. 'both' sigue significando lead
    • conversación, así las etiquetas que ya existen no aparecen de golpe en la agenda, ni las de recurso en la bandeja. getTags({ entity_type: 'resource' }) filtra por igualdad, sin 'both', y ConversationTagsService ya filtraba por conversation/both.
  • Crea el pivote scheduling_resource_tags, con RLS y sin grants por defecto:
    • Leerlo puede cualquier miembro del tenant del recurso. El catálogo de recursos ya es legible por todo el tenant, y sin esto no se puede filtrar.
    • Poner o quitar exige scheduling.resources.manage, con etiqueta y recurso del mismo tenant.
    • No hay UPDATE: se quita y se pone.
  • Añade índices para los bloqueos por recurso y rango, y para las citas por sede.

La escritura va directa con la sesión (setResourceTags), no por el backend: RLS decide. Un DELETE bloqueado devuelve 0 filas sin error, así que la acción compara lo borrado con lo pedido y lo trata como forbidden. Las pruebas por rol están en supabase/tests/resource_tags/.

Filtros: una sola función pura para las tres pantallas​

sections/scheduling/agenda-resource-filters.ts:

  • matchesLocation: un recurso sin sede fija entra en cualquier sede.
  • filterAgendaResources: sede, tipo y etiqueta se combinan con Y; dentro de cada filtro, con O.
  • pickDayColumns: el interruptor «Solo con citas».

La usan el calendario, el listado y Hoy. El catálogo compartido (useSchedulingCatalog) pide siempre los recursos de todo el tenant (una sola entrada de caché) y aplica la sede en el cliente.

«Solo con citas» está encendido por defecto en la vista día y sustituye al umbral de 12 columnas. Se muestran todos igualmente con un recurso elegido, en modo bloqueo, o si nadie tiene citas. Con muchas columnas, la rejilla hace scroll horizontal con un ancho mínimo por columna.

Carga​

  • El calendario pide citas y bloqueos juntos (loadRange), cuando ya hay catálogo, y con un número de petición: la respuesta de una petición superada se descarta.
  • La dependencia es una clave de texto con los ids, no el array.
  • Con tipo, etiqueta o piso activos y hasta 150 recursos, las citas se acotan en la base (resource_id IN …). Por encima, la lista no cabe con holgura en la URL de PostgREST y se filtra en el cliente.
  • getAppointments pide por solape con el rango, no solo por inicio, y tiene un tope defensivo de 3000 filas.
  • Hoy sondea solo con la pestaña visible y no vuelve a mostrar la barra de carga en cada sondeo.

Consecuencias​

  • El editor de etiquetas ofrece «Recursos de agenda». Una etiqueta de recurso no se puede usar en conversaciones, y al revés.
  • La ficha del recurso tiene un campo de etiquetas, que solo ve quien gestiona todos los recursos.
  • Las preferencias de filtro (tipos, etiquetas, «Solo con citas») se guardan por tenant en localStorage. Si no hay almacenamiento, la agenda funciona con los valores por defecto.
  • Queda fuera: la política de lectura de scheduling_appointments sigue llamando a user_has_permission sin envolverla en (SELECT …). Optimizarla toca RLS de citas en producción y necesita su propio análisis de impacto.