From d03c62641e1a43a9760fb6421518f55859f1cb9d Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 17 Aug 2026 17:07:24 +0200 Subject: [PATCH] Calendario: il 19 e il 20 settembre non sono un buco, sono un evento e una chiusura Quei due giorni non stanno in nessuna delle due griglie perche' non hanno class: il 19 c'e' la presentazione della nuova apertura, il 20 si sta chiusi. Scritto come avviso con la sua finestra, il vuoto smette di sembrare una trascrizione incompleta e diventa un'informazione. Gli avvisi compaiono in cima al popup e in fondo al PDF - e' il file che gira su WhatsApp, e chi lo riceve non ha la pagina davanti. Spariscono da soli quando sono passati: un avviso scaduto dice che il centro e' chiuso in un giorno gia' trascorso, e chi legge non sa piu' se valga per quest'anno o per il prossimo. Lo spazio riservato in fondo alla pagina cresce con gli avvisi, altrimenti la griglia si allunga fino a spingerli su una seconda pagina. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/CalendarioModal.astro | 24 ++++++++++++++-- src/data/calendari.ts | 43 ++++++++++++++++++++++++++++ src/lib/calendario-pdf.ts | 22 ++++++++++---- src/pages/calendario/[id].pdf.ts | 10 ++++--- tests/calendari.test.ts | 36 ++++++++++++++++++++--- tests/calendario-pdf.test.ts | 6 ++-- 6 files changed, 123 insertions(+), 18 deletions(-) diff --git a/src/components/CalendarioModal.astro b/src/components/CalendarioModal.astro index e3ad114..92d495a 100644 --- a/src/components/CalendarioModal.astro +++ b/src/components/CalendarioModal.astro @@ -7,10 +7,13 @@ // dichiarando da quando entra. // // Si apre da qualunque elemento con `data-apri-calendario` nella stessa pagina. -import { calendarioDaMostrare, GIORNI, DISCIPLINE, type Giorno } from '../data/calendari'; +import { calendarioDaMostrare, avvisiAttivi, GIORNI, DISCIPLINE, type Giorno } from '../data/calendari'; import { t } from '../lib/content'; -const scelta = calendarioDaMostrare(new Date()); +const adesso = new Date(); +const scelta = calendarioDaMostrare(adesso); +// Giornate fuori griglia (eventi, chiusure): spariscono da sole quando sono passate. +const avvisi = avvisiAttivi(adesso); const cal = scelta?.cal ?? null; const dal = cal ? new Date(cal.from).toLocaleDateString('it-IT', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'Europe/Rome', @@ -38,6 +41,14 @@ const presenti = cal + {avvisi.length > 0 && ( + + )} +
@@ -133,6 +144,15 @@ const presenti = cal } .cal-dlg__chiudi:hover { border-color: var(--c-accent); color: var(--c-accent-dark); } + /* Giornate fuori griglia: stanno sopra la tabella perché sono l'eccezione a ciò che la + tabella dice, e sotto passerebbero per una postilla. */ + .cal-avvisi { + list-style: none; margin: 18px 0 0; padding: 12px 16px; background: var(--c-bg-alt); + border-left: 3px solid var(--c-accent); display: flex; flex-direction: column; gap: 4px; + } + .cal-avvisi li { font-size: .84rem; color: var(--c-text); } + .cal-avvisi strong { color: var(--c-heading); } + /* La griglia ha otto colonne: su telefono scorre in orizzontale invece di comprimersi fino a diventare illeggibile, e la colonna delle ore resta ferma. */ .cal-dlg__scroll { flex: 1; overflow: auto; margin: 22px 0 0; } diff --git a/src/data/calendari.ts b/src/data/calendari.ts index cf2a6d9..28bca28 100644 --- a/src/data/calendari.ts +++ b/src/data/calendari.ts @@ -103,6 +103,49 @@ export const calendari: Calendario[] = [ }, ]; +/** + * Giornate che non stanno in nessuna griglia: eventi e chiusure. + * + * Il 19 e il 20 settembre non compaiono in nessuno dei due fogli, e la ragione non è una + * dimenticanza — il sabato c'è la presentazione della nuova apertura e la domenica si sta + * chiusi (detto da Adriano il 17/08/2026). Scritto qui, quel vuoto smette di sembrare un + * errore di trascrizione e diventa un'informazione per chi legge gli orari. + */ +export interface AvvisoCalendario { + id: string; + /** `null` = visibile da subito. */ + from: string | null; + /** Ultimo istante in cui ha senso mostrarlo: dopo, sparisce da solo. */ + to: string; + quando: string; + testo: string; +} + +export const avvisi: AvvisoCalendario[] = [ + { + id: 'presentazione-19-settembre', + from: null, + to: '2026-09-19T23:59:59+02:00', + quando: 'Sabato 19 settembre', + testo: 'Presentazione della nuova apertura: nessuna class in programma.', + }, + { + id: 'chiusura-20-settembre', + from: null, + to: '2026-09-20T23:59:59+02:00', + quando: 'Domenica 20 settembre', + testo: 'Chiuso.', + }, +]; + +/** Gli avvisi ancora validi, in ordine di scadenza: i più imminenti per primi. */ +export function avvisiAttivi(now: Date, list: AvvisoCalendario[] = avvisi): AvvisoCalendario[] { + const t = now.getTime(); + return list + .filter((a) => (a.from === null || t >= Date.parse(a.from)) && t <= Date.parse(a.to)) + .sort((x, y) => Date.parse(x.to) - Date.parse(y.to)); +} + /** Servizi dalla cui pagina si apre il calendario della sala. */ export const SERVIZI_CON_CALENDARIO = ['performance-class', 'holistic-class']; diff --git a/src/lib/calendario-pdf.ts b/src/lib/calendario-pdf.ts index 78b8dd3..8be8fc6 100644 --- a/src/lib/calendario-pdf.ts +++ b/src/lib/calendario-pdf.ts @@ -1,5 +1,5 @@ import PDFDocument from 'pdfkit'; -import { GIORNI, type Calendario, type Disciplina, type Giorno } from '../data/calendari'; +import { GIORNI, type AvvisoCalendario, type Calendario, type Disciplina, type Giorno } from '../data/calendari'; // PDF del calendario, disegnato dal dato con PDFKit. // @@ -32,7 +32,7 @@ export const nomeFilePdf = (cal: Calendario): string => * in A4 orizzontale, e spezzarla su due pagine renderebbe illeggibile proprio ciò che si va a * cercare — l'incrocio fra un giorno e un'ora. */ -export function generaCalendarioPdf(cal: Calendario): Promise { +export function generaCalendarioPdf(cal: Calendario, avvisi: AvvisoCalendario[] = []): Promise { const doc = new PDFDocument({ size: 'A4', layout: 'landscape', @@ -88,7 +88,9 @@ export function generaCalendarioPdf(cal: Calendario): Promise { // Righe: l'altezza si ricava dallo spazio che resta, così la griglia riempie la pagina // qualunque sia il numero di fasce e non serve ritoccarla a mano quando ne cambiano. - const spazioSotto = 52; // legenda + nota + piè di pagina + // legenda + nota + piè di pagina, più una riga per ogni giornata fuori griglia: senza + // tenerne conto la griglia si allungherebbe fino a spingere gli avvisi in una seconda pagina. + const spazioSotto = 52 + avvisi.length * 11; const altezzaRiga = Math.min( 22, (doc.page.height - doc.page.margins.bottom - spazioSotto - y) / cal.righe.length, @@ -147,12 +149,22 @@ export function generaCalendarioPdf(cal: Calendario): Promise { xLeg += 13 + doc.widthOfString(d) + 18; } + // --- Giornate fuori griglia (eventi, chiusure) --- + let yAvvisi = yLeg + 16; + for (const a of avvisi) { + doc.rect(x0, yAvvisi - 1, 2, 9).fill(C_ACCENTO); + doc.font('Helvetica-Bold').fontSize(7).fillColor(C_TESTO) + .text(a.quando, x0 + 7, yAvvisi, { continued: true }) + .font('Helvetica').fillColor(C_TENUE).text(` — ${a.testo}`); + yAvvisi += 11; + } + doc.font('Helvetica').fontSize(7).fillColor(C_TENUE).text( "Gli orari possono subire variazioni. Le lezioni si prenotano dall'app Bsport.", - x0, yLeg + 16, { width: larghezzaUtile }, + x0, yAvvisi + 3, { width: larghezzaUtile }, ); doc.font('Helvetica').fontSize(7).fillColor(C_ACCENTO) - .text('insanitylab.it', x0, yLeg + 27, { width: larghezzaUtile, align: 'right' }); + .text('insanitylab.it', x0, yAvvisi + 14, { width: larghezzaUtile, align: 'right' }); doc.end(); return finito; diff --git a/src/pages/calendario/[id].pdf.ts b/src/pages/calendario/[id].pdf.ts index 51d0370..20dd16b 100644 --- a/src/pages/calendario/[id].pdf.ts +++ b/src/pages/calendario/[id].pdf.ts @@ -1,5 +1,5 @@ import type { APIRoute } from 'astro'; -import { calendari } from '../../data/calendari'; +import { avvisiAttivi, calendari } from '../../data/calendari'; import { generaCalendarioPdf, nomeFilePdf } from '../../lib/calendario-pdf'; export const prerender = false; @@ -15,7 +15,9 @@ export const GET: APIRoute = async ({ params }) => { // credere che il calendario sia vuoto. if (!cal) return new Response('Calendario inesistente.', { status: 404 }); - const pdf = await generaCalendarioPdf(cal); + // Le giornate fuori griglia stanno anche nel PDF: è il file che gira su WhatsApp, e chi lo + // riceve non ha davanti la pagina dove sono scritte. + const pdf = await generaCalendarioPdf(cal, avvisiAttivi(new Date())); return new Response(new Uint8Array(pdf), { headers: { 'Content-Type': 'application/pdf', @@ -23,8 +25,8 @@ export const GET: APIRoute = async ({ params }) => { // scheda e su telefono resterebbe lì, senza finire fra i file. 'Content-Disposition': `attachment; filename="${nomeFilePdf(cal)}"`, 'Content-Length': String(pdf.length), - // Il calendario cambia solo quando cambia il dato, cioè con un deploy: un'ora di cache - // basta a non rigenerarlo a ogni click, e non abbastanza da servire un orario vecchio. + // Un'ora: basta a non rigenerarlo a ogni click, e non abbastanza perché un avviso ormai + // scaduto (le giornate fuori griglia spariscono da sole) resti in giro un giorno intero. 'Cache-Control': 'public, max-age=3600', }, }); diff --git a/tests/calendari.test.ts b/tests/calendari.test.ts index 0a28416..5ae6232 100644 --- a/tests/calendari.test.ts +++ b/tests/calendari.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { calendari, calendarioDaMostrare, DISCIPLINE, GIORNI, type Giorno } from '../src/data/calendari'; +import { avvisi, avvisiAttivi, calendari, calendarioDaMostrare, DISCIPLINE, GIORNI, type Giorno } from '../src/data/calendari'; const d = (iso: string) => new Date(iso); @@ -25,9 +25,11 @@ describe('calendarioDaMostrare', () => { }); }); - // 19 e 20 settembre non sono coperti da nessuno dei due fogli del cliente. Il buco è vero e - // resta dichiarato qui: se un giorno verrà colmato, questo test lo dirà. - it('nei due giorni scoperti (19-20 settembre) mostra il prossimo', () => { + // 19 e 20 settembre non stanno in nessuna griglia perché quei due giorni non hanno class: + // il 19 c'è la presentazione della nuova apertura, il 20 si sta chiusi (Adriano, 17/08). + // La griglia mostrata è quindi quella del 21, marcata futura, e l'informazione sui due + // giorni arriva dagli avvisi qui sotto. + it('nei due giorni fuori griglia (19-20 settembre) mostra il prossimo', () => { expect(calendarioDaMostrare(d('2026-09-19T10:00:00+02:00'))).toMatchObject({ cal: { id: 'settembre-21-in-poi' }, futuro: true, }); @@ -44,6 +46,32 @@ describe('calendarioDaMostrare', () => { }); }); +describe('giornate fuori griglia', () => { + it('la presentazione e la chiusura si vedono prima di arrivarci', () => { + const oggi = avvisiAttivi(d('2026-08-17T12:00:00+02:00')).map((a) => a.id); + expect(oggi).toEqual(['presentazione-19-settembre', 'chiusura-20-settembre']); + }); + + // Un avviso passato è peggio di nessun avviso: dice che il centro è chiuso un giorno che è + // già trascorso, e chi legge non sa più se valga per quest'anno o per il prossimo. + it('ogni avviso sparisce da solo quando è passato', () => { + expect(avvisiAttivi(d('2026-09-19T23:59:59+02:00')).map((a) => a.id)) + .toEqual(['presentazione-19-settembre', 'chiusura-20-settembre']); + expect(avvisiAttivi(d('2026-09-20T00:00:00+02:00')).map((a) => a.id)) + .toEqual(['chiusura-20-settembre']); + expect(avvisiAttivi(d('2026-09-21T00:00:00+02:00'))).toEqual([]); + }); + + it('ogni avviso ha una scadenza leggibile e un testo', () => { + for (const a of avvisi) { + expect(Number.isNaN(Date.parse(a.to)), `${a.id}: scadenza illeggibile`).toBe(false); + if (a.from !== null) expect(Date.parse(a.to)).toBeGreaterThanOrEqual(Date.parse(a.from)); + expect(a.quando.length).toBeGreaterThan(0); + expect(a.testo.length).toBeGreaterThan(0); + } + }); +}); + describe('coerenza dei calendari trascritti', () => { for (const c of calendari) { describe(c.id, () => { diff --git a/tests/calendario-pdf.test.ts b/tests/calendario-pdf.test.ts index e39bad6..676fd82 100644 --- a/tests/calendario-pdf.test.ts +++ b/tests/calendario-pdf.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { calendari } from '../src/data/calendari'; +import { avvisi, calendari } from '../src/data/calendari'; import { generaCalendarioPdf, nomeFilePdf } from '../src/lib/calendario-pdf'; describe('PDF del calendario', () => { @@ -18,9 +18,9 @@ describe('PDF del calendario', () => { // La griglia deve stare in una pagina sola: l'incrocio giorno/ora è proprio ciò che si va a // cercare, e spezzarlo a metà rende il documento inutile. Il conteggio si legge dall'oggetto // /Type /Pages, che dichiara quante pagine contiene. - it('sta in una pagina sola', async () => { + it('sta in una pagina sola, anche con le giornate fuori griglia in fondo', async () => { for (const cal of calendari) { - const pdf = await generaCalendarioPdf(cal); + const pdf = await generaCalendarioPdf(cal, avvisi); const conteggio = /\/Type\s*\/Pages[^>]*?\/Count\s+(\d+)/s.exec(pdf.toString('latin1')); expect(conteggio, `${cal.id}: numero di pagine non dichiarato`).not.toBeNull(); expect(Number(conteggio![1]), `${cal.id}: il calendario occupa più di una pagina`).toBe(1);