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_ides NULL a propósito para quien rota entre sedes, pero el catálogo se filtraba conlocation_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 (
weekStartsOn0 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
CHECKdecrm_tags.entity_typecon'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', yConversationTagsServiceya filtraba porconversation/both.
- conversación, así las etiquetas que ya existen no aparecen de golpe en la agenda, ni las de
recurso en la bandeja.
- 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. getAppointmentspide 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_appointmentssigue llamando auser_has_permissionsin envolverla en(SELECT …). Optimizarla toca RLS de citas en producción y necesita su propio análisis de impacto.