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.
| 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-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.
08:00 a 12:00, en las 5
semanas), el motor de planificación y toda la sección 🧭 Horarios y disponibilidad de
Estadísticas lo leen sin ningún cambio.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.
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.
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:
DESC_OK; baja 1 a 1 hasta DESC_FLOOR solo si el día
quedaría sin ningún turno. Nunca por debajo de DESC_FLOOR.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).
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”.
_buildPrintHTML, con sus emoji e iconos habituales): se
renderiza fuera de pantalla, se captura con html2canvas y se monta como
imagen en el PDF con jsPDF (ambas se cargan desde cdnjs/jsdelivr la
primera vez; sin conexión cae al diálogo de impresión del navegador). Encima
de la imagen se superponen anotaciones /Link reales, calculadas a partir
de la posición de cada enlace en el HTML — funcionan en Firefox, Chrome y
Adobe igual, porque no dependen del motor de impresión del navegador.
Bloque por día (barco, detalle horario, muelle) y por turno (hora, equipo con
el portador de llave resaltado, enlace “Añadir a Google Calendar” y al
cuestionario). En los turnos sin cubrir enlaza al portal de sustituciones. Al
pie, la nota de la llave y el contacto de bajas. La paginación evita partir
un bloque de día entre dos páginas.localStorage → pdf_cfg). El detalle horario y el muelle
de cada día se editan en la fila de ese día en Calendario.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.
MIN_EQ) · ✓ completo (según IDEAL). También avisa si el
turno se quedó sin portador de llave tras una baja o si una franja
habilitada vacía se solapa con un turno ya formado.actividad, que rellenan unos disparadores de la base de datos
(supabase/01_actividad.sql) — queda registrado
siempre. Si aún no la has creado, la vista lo detecta comparando con la última
visita y lo guarda en el navegador (envivo_snap / envivo_log): solo se ve si
esa app estaba abierta, pero nunca lo inventa.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 (▲▼).
MIN_EQ de Reglas del motor). Con menos —incluso con 1 o 2 voluntarios que sí
acudieron— o con ninguna, cuenta como no realizado. Así, un turno con 3
confirmados del que se dan de baja 2 (queda 1) no se cuenta como hecho. Las
voluntarios que sí acudieron siguen sumando en su ficha y en la tabla de voluntarios.supabase/01_actividad.sql (la vista lo indica).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:
…Mayo26.pdf) y se
comprueba que los días de la semana cuadren. Solo sirve para PDF con texto: el que
genera esta app es una imagen, pero esos meses ya están en la app.12/06/2026 10:00 a 12:00 ANA GARCIA,
LUIS PEREZ; acepta tabuladores, ;, franjas 19-21, fechas d/m/aa…).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.
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.
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”.
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.
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.
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:
MIN_EQ
voluntarios libres, es decir, ahí faltan voluntarios.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”.
Las credenciales van embebidas en cada HTML (constantes al inicio del <script>):
APP_URL en disponibilidad.html (y la lógica de servidor
vive en el propio proyecto de Apps Script, fuera de este repo).localStorage, no en el repo. Esos
campos solo sirven para probar el bot — el envío real en segundo plano usa su propia
copia de las credenciales como secrets de la Edge Function (ver más abajo).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:
supabase/01_actividad.sql).supabase/functions/avisos-turnos + supabase/02_avisos.sql).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
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.