puertos

Puerto de Sevilla · Gestión de turnos de voluntarios

Conjunto de tres páginas HTML independientes (sin build ni dependencias que instalar) para organizar los turnos de los voluntarios del Puerto de Sevilla. Cada archivo se abre directamente en el navegador o se publica como página estática.

Archivos

Archivo Uso Público Backend
disponibilidad.html Formulario para que cada voluntario registre su disponibilidad semanal (5 semanas × 7 días, hora de entrada/salida). Voluntarios Google Apps Script (APP_URL) que escribe en una hoja de cálculo.
disponibilidad-bloques.html Formulario de disponibilidad por bloques (Mañana/Mediodía/Tarde/Noche × L-D), fija hasta que se actualice, con migración automática desde la disponibilidad ya registrada. Voluntarios Supabase REST (SU / SK).
planificador.html Panel de administración: cuadrante mensual con motor de planificación automática, balance de carga, alertas de portador de llave, gestión de voluntarios, bajas y sustituciones, archivo de meses, reglas del motor y notificaciones (navegador + Telegram opcional). Coordinación Supabase REST (SUPA_URL / SUPA_KEY).
sustituciones.html Portal para que un voluntario de baja libere sus turnos y otro se apunte a cubrirlos. Voluntarios Supabase REST (SU / SK).

planificador.html, sustituciones.html y disponibilidad-bloques.html comparten la misma base de datos Supabase (escriben en la misma tabla disponibilidad que ya usan las estadísticas y el motor de planificación — nada más que actualizar para que la lean). La configuración del portal de sustituciones se sincroniza vía la tabla config_portal y se cachea en localStorage.

Disponibilidad por bloques (disponibilidad-bloques.html)

Pensado para reducir la dispersión de horarios: en vez de que cada uno escriba su propio rango de horas (que casi nunca coincide exactamente con el de otro), todos eligen entre los mismos 4 bloques fijos — Mañana (8–12), Mediodía (12–15), Tarde (15–19) y Noche (19–22) — para cada día de la semana. Marcar un bloque entero no cambia la duración de los turnos (siguen siendo de 2 horas): significa “puedo cubrir cualquier turno de 2h dentro de este bloque”, lo que hace mucho más fácil que varios voluntarios coincidan a la misma hora.

Requiere ejecutar antes supabase/08_disponibilidad_bloques.sql en el SQL Editor de Supabase: la tabla disponibilidad solo permitía una fila por voluntario, semana y día (venía del formulario antiguo, con un único rango por día), y en cuanto alguien marca dos bloques no contiguos el mismo día (p. ej. Mañana y Noche) hace falta poder guardar más de una fila. Sin ese SQL, al guardar sale un error 23505 que el propio formulario reconoce y explica. El guardado hace primero una copia de la disponibilidad anterior del voluntario y la restaura automáticamente si el guardado falla a mitad, así que no se pierde nada aunque falte ejecutar el SQL — pero conviene ejecutarlo cuanto antes.

Antes de empezar a probarlo, conviene tener además supabase/09_backup_disponibilidad.sql: crea disponibilidad_respaldo, una copia de solo lectura de toda la tabla en ese momento (con instrucciones de cómo restaurar desde ella dentro del propio archivo). Se puede volver a ejecutar para renovar la copia; cada ejecución sustituye la anterior, no las acumula.

Panel de migración: planificador.html → 🧱 Nueva disponibilidad, junto a la pestaña de Disponibilidad de siempre. Lee la misma tabla disponibilidad, pero con la lógica de bloques: por cada voluntario y día, qué bloques quedan cubiertos (un punto verde por bloque, ≥ la mitad de sus horas), y si sus filas ya son bloques exactos (🧱 Sistema nuevo) o siguen siendo el horario libre de siempre (🕰️ Sistema antiguo) — con filtros para ver solo unos u otros. Es la forma de comprobar, según se va pidiendo a cada voluntario que rellene el formulario nuevo, quién falta, sin tener que abrir la ficha de cada uno. Cuando todo el mundo esté en 🧱, se puede retirar disponibilidad.html (el formulario antiguo, que además escribe en Google Sheets en vez de Supabase — ver más abajo) y esta misma pestaña.

Preparar el mes siguiente sin que se vea todavía

En ⚙️ Calendario cada día tiene dos interruptores independientes:

Para preparar un mes con calma sin que los voluntarios lo vean todavía: añade los días Activos (así puedes trabajar en el Cuadrante, asignar equipos, etc.) pero Ocultos. El día que quieras lanzarlo, los pasas a Visibles y a partir de ese momento aparecen en Sustituciones. Cada bloque de mes (se agrupan por separado, para no ver de golpe septiembre y octubre mezclados) tiene sus propios botones Activar/Desactivar todos y Ocultar/Mostrar todos para hacerlo de una vez en vez de día a día — recuerda pulsar “Guardar cambios” después.

Motor de planificación (planificador.html)

Genera el cuadrante a partir de la disponibilidad, el historial de turnos y el calendario de días activos. Los días ya confirmados no se recalculan nunca.

Hay dos motores, seleccionables con el toggle Motor con visión global de la pestaña Reglas del motor:

En ambos, tras formar los equipos se aplica la regla del portador de llave (primer turno, último turno y turnos junto a un hueco horario necesitan un portador presente).

Editar equipos a mano (excepción a la regla)

Cada turno del cuadrante muestra 4 plazas. En las vacías, + Añadir voluntario abre un selector con los disponibles de ese día y, aparte, toda la lista de voluntarios (estos últimos se añaden como excepción, ignorando la regla de descanso). Cada plaza ocupada tiene ⇄ (cambiar) y ✕ (quitar). Un turno tocado a mano se marca ✏️ Editado y ↩︎ Revertir lo devuelve al cálculo automático. ➕ Añadir turno manual / otro turno crea un turno vacío en una franja libre. Los cambios viven en memoria hasta que pulsas ✓ Confirmar — y solo bloquea el turno que confirmas: un día con algún turno ya confirmado sigue mostrando el resto de franjas habilitadas como huecos editables (nunca desaparecen).

El descanso/disponibilidad que ves en el selector y en “Disponibles adicionales” se recalcula en vivo cada vez que añades, cambias o quitas a alguien — quitar a un voluntario de un turno lo libera al instante en el resto del cuadrante (no hace falta un ↻ Recalc.). La sección “Todos los voluntarios” avisa si ese voluntario ya está asignado cerca de esa fecha antes de añadirla como excepción.

Parámetros en Reglas del motor (se guardan en localStorage → reglas_cuadrante):

Regla Def. Descripción
DESC_OK 5 Días de descanso objetivo entre turnos
DESC_FLOOR 3 Suelo absoluto de descanso (motor global)
DESC_MIN 2 Descanso mínimo del motor por-día (legacy)
IDEAL 4 Voluntarios ideales por turno
MIN_EQ 3 Mínimo aceptable por turno
DUR 2 Duración del turno en horas
VENTANA_CARGA 60 Días para equilibrar la carga por voluntario
MAX_TURNOS_DIA 3 Tope de turnos por día
motorGlobal true Motor global (true) o por-día (false)

Se pueden definir excepciones por día concreto (clave excepcion_dia en localStorage): descanso, tamaño de equipo o “ignorar descanso”.

Exportar el cuadrante

Vista “🛰️ En vivo” (planificador.html)

Un único tablero con todo lo que pasa en los turnos, para no tener que saltar entre Bajas, Apuntes y Turnos confirmados. Cruza historial (equipo confirmado) + bajas + refuerzos (apuntes de Sustituciones) + calendario (franjas habilitadas) y se refresca sola cada 30 s mientras la pestaña está a la vista.

Estadísticas (planificador.html → 📈 Estadísticas)

Panel de actividad de todo el histórico (historial activo + archivado, bajas, apuntes y voluntarios), por periodo: este mes, mes anterior, 3 o 6 meses, este año, año anterior, todo el histórico o un rango de meses a medida. Cada cifra se compara con el periodo anterior de la misma duración (▲▼).

Meses gestionados fuera de la app

Las estadísticas solo conocen lo que se registró en la app. Para no dar conclusiones falsas, cada mes se clasifica solo:

Además, nadie se da por “dejó de participar” salvo que lleve más del plazo elegido (6 meses por defecto) sin turnos y haya al menos 2 meses con datos posteriores a su último turno (si hay un hueco de meses sin registrar de por medio, se muestra aparte como “sin datos suficientes”). Y las tasas solo se comentan a partir de 15 plazas con registro.

📥 Importar turnos anteriores recupera esos meses. Dos formas:

Antes de guardar hay una previsualización: líneas que no se entienden, duplicados de lo que ya estaba, y los nombres dudosos, que eliges tú (sugiere “¿quizá es…?” para erratas como Daneil→Daniel, con ✨ Aceptar sugerencias claras; quien ya no está en tu lista se puede añadir como voluntario fuera del grupo para que su historia cuente, o se omite). Al terminar se puede ↩︎ deshacer toda la importación. Solo admite meses anteriores al actual.

Turnos que no salieron adelante. Las franjas que aparecen en blanco en el programa (nadie se apuntó) se guardan como sin cubrir, y en la previsualización puedes marcar un turno con gente como no salió adelante (por bajas, otro motivo). También en texto: 14/06/2026 12-14 sin voluntarios o … cancelado. Se guardan en la tabla turnos_no_realizados (hay que ejecutar antes supabase/03_turnos_no_realizados.sql; sin ella se importan solo las asignaciones). Con ellas las estadísticas calculan qué porcentaje de los turnos programados salen adelante, por qué no salen (sin voluntarios / bajas / otros) y qué días y franjas se quedan más veces sin cubrir (✖ en el mapa día × franja), con recomendaciones concretas.

Bajas que solo se recuerdan (“al menos N”)

Si de un mes gestionado a mano no se conservan las bajas una a una, pero se sabe que hubo al menos un número (p. ej. “en abril hubo al menos 2”), se indica en 🩺 Calidad de los datos → ✎ Indicar bajas. Se guarda en la tabla ajustes_estadisticas (supabase/04_ajustes_estadisticas.sql).

Los meses hechos solo con apuntes (sin cuadrante) se pueden reconstruir desde el chat de Telegram con 💬 Desde Telegram: guarda los apuntes y bajas en apuntes_archivo / bajas_archivo (supabase/05_archivo_apuntes_bajas.sql, pasos en NOTIFICACIONES.md, Ampliación E). En esos turnos asisten los apuntados si se llega a 3 voluntarios, y quien se apuntó y se dio de baja después no cuenta.

Un mes sin ningún dato se puede marcar como 🌴 sin actividad o 🗂️ sin datos que recuperar (🩺 Calidad de los datos; requiere supabase/06_marca_mes.sql): deja de contar como hueco.

Cómo salió cada turno se puede indicar a mano (🧾 Turnos: «salió gracias a los apuntes» y «cuántos asistieron»; requiere supabase/07_turnos_resultado.sql), y la sección 🙋 Turnos que salieron gracias a los apuntes resume el histórico completo por mes. Las estadísticas lo tratan como dato aproximado: solo suma lo que falte hasta ese mínimo (si el mes ya tiene más bajas registradas, no cambia nada), el mes pasa a contar para la tasa de baja y las cifras salen como “≥” (bajas, tasa, y el aviso de tasa habla de “al menos”). No inventa turnos, voluntarios ni antelación: solo un número.

Confirmar turnos desde Apuntes

Cada turno de Apuntes tiene ✓ Confirmar turno con estos apuntes: añade a los apuntados al equipo (historial), de modo que un turno formado solo por apuntes pasa a ser un turno confirmado real (cuadrante, PDF, descansos y estadísticas). Quienes ya están en el equipo se marcan como ✓ EN EL EQUIPO. En las estadísticas ese turno sigue contando como “salido adelante por apuntes”.

Sacar del grupo (Voluntarios)

Cada voluntario activo tiene 🚪 Sacar del grupo: deja de aparecer en el cuadrante, en la disponibilidad, en el balance y entre los sustitutos sugeridos, pero conserva todo su historial para las estadísticas y se puede volver a añadir (filtro 🚪 Fuera del grupo → ♻️ Volver al grupo). Si tiene turnos o apuntes por delante, se ofrece quitarlo también de ellos. 🗑️ Eliminar solo está disponible para alguien fuera del grupo que no consta en ningún turno, baja ni apunte (se comprueba en directo); si consta, se queda fuera del grupo para no falsear las estadísticas. El filtro 💤 Sin actividad reúne a quienes dejaron de participar o nunca empezaron.

Anular un turno

En Bajas, cada turno tiene 🚫 Anular turno (y en En vivo, los turnos con bajas o con atención): lo marca como turno no realizado y cuenta así en las estadísticas aunque hubiera voluntarios apuntados (sus plazas y su asistencia a ese turno no cuentan). No borra a nadie del turno y se puede ↩︎ Restaurar. Se guarda en turnos_no_realizados (origen anulado; hay que haber ejecutado supabase/03_turnos_no_realizados.sql). Un turno anulado deja de generar avisos de “turno en riesgo” (hay que volver a pegar avisos-turnos en Supabase). No cambia lo que ven los voluntarios en Sustituciones.

Horarios y disponibilidad

Sección 🧭 Horarios y disponibilidad de Estadísticas. Pensada para entenderse leyendo, no interpretando gráficos. A partir del horario que cada voluntario registró (disponibilidad) y de los turnos que ha habido, se organiza en tres partes:

  1. 📌 En resumen, en frases: cuándo se hacen turnos, cuándo hay más voluntarios libres, en qué franjas hay turnos pero faltan voluntarios, en cuáles sobran voluntarios donde nunca hay turnos (quizá se podrían habilitar), cuántos no tienen horario registrado y por qué no participan los que nunca lo han hecho.
  2. ¿Cuántos voluntarios están libres cada hora?: un mapa día × hora con el número de voluntarios libres (media del mes o una semana concreta). Borde oscuro = ahí se han hecho turnos; borde rojo = se han hecho turnos pero hay menos de MIN_EQ voluntarios libres, es decir, ahí faltan voluntarios.
  3. ¿Qué le dificulta participar a cada voluntario?: una tarjeta por voluntario con la causa principal en una frase y su explicación —🔴/⚪ sin horario registrado o tramos demasiado cortos, 🟠 su horario cae lejos de donde se hacen turnos o libre solo unas pocas semanas al mes, 🔑 coincide con gente pero nadie tiene llave cerca, 🟡 coincide poco con otros voluntarios, 🟢 su horario no es el problema—, su horario escrito con sus semanas (Mar 16–20 (solo la 3.ª sem.)) y dibujado hora a hora. Se filtra por grupo (🆕 nunca han participado, ⚠️ con alguna dificultad, 💤 dejaron de participar, 🔑 portadores de llave) y por causa. Un desplegable ❓ Cómo leer el dibujo explica los colores.

🔑 La disponibilidad de los portadores de llave prioriza. Sin uno presente no se puede fijar un turno, por mucha gente que coincida. Un interruptor («Tener en cuenta que hace falta un portador de llave») activado por defecto reclasifica a quien solo coincide con gente sin llave como 🔑 en vez de 🟢/🟡 «coincide poco», marca en el mapa (borde morado) las franjas con gente de sobra pero casi sin llave, y lo dice en «En resumen». Se puede apagar para volver a ver solo el número de gente, sin la llave.

🔍 Los dos mapas de calor (este y el de «🗺️ Mapa día × franja» más abajo) se amplían al tocarlos, en una ventana más grande y legible.

Se trabaja semana a semana (las 5 semanas del formulario: días 1-7, 8-14, 15-21, 22-28 y 29 en adelante): dos voluntarios solo coinciden si están libres el mismo día de la misma semana, y no es lo mismo estar libre todos los martes que solo el tercero. Las tres medidas que hay detrás: coincide con otros (qué parte de las franjas de un turno que puede cubrir tienen, esa semana, al menos MIN_EQ − 1 voluntarios más), constancia (cuántas de las 5 semanas está libre) y encaja con los turnos (qué parte de sus horas cae donde ha habido turnos). Una fila de disponibilidad sin semana se toma como “todas las semanas”.

Configuración

Las credenciales van embebidas en cada HTML (constantes al inicio del <script>):

App instalable (PWA) y notificaciones en segundo plano

planificador.html se puede instalar en el móvil (Android/Chrome → “Añadir a pantalla de inicio”): icono propio, pantalla completa. manifest.webmanifest y sw.js son archivos estáticos reales — necesario para que la instalación sea fiable (antes se generaban como blob URLs efímeras, que Chrome ni siquiera acepta para registrar un service worker).

Las notificaciones de bajas y apuntes nuevos, para que lleguen aunque la app esté cerrada, no pueden depender del navegador (ningún sondeo desde JS de página sobrevive a que Android mate la pestaña en segundo plano). Por eso el aviso a Telegram lo dispara Supabase directamente al insertarse la fila — supabase/functions/notificar-telegram/ + dos Database Webhooks. Se despliega entero desde el panel de Supabase, sin CLI. Guía paso a paso: NOTIFICACIONES.md, que incluye además:

Pruebas

El motor global tiene un banco de pruebas que carga el <script> de planificador.html en un DOM simulado y lo ejercita con escenarios sintéticos (reparto de cobertura, cascada de descanso, portador de llave, ventana de carga, franjas habilitadas, mes completo):

node test/motor-global.test.js

Incluye además el modelo de la vista En vivo y de la ficha de voluntario. La función avisos-turnos tiene su propia prueba, que carga el index.ts tal cual con Supabase, Telegram y el reloj simulados (autorización, horario, no repetir, “ya cubierto”, cambio horario):

node test/avisos-turnos.test.js

Despliegue

Son ficheros estáticos: sirve la carpeta con cualquier hosting estático (GitHub Pages, Netlify, etc.) o abre los .html directamente. planificador.html y sustituciones.html registran un service worker en línea para funcionar como PWA instalable.