Calendario: si vedono tutti gli orari validi, e il PDF ne fa una pagina ciascuno

Il popup mostrava solo l'orario del momento: chi guarda ad agosto vuole sapere
com'e' settembre per intero, e una griglia sola gli fa credere che valga per
sempre. Ora ci sono tutti quelli non ancora scaduti, ognuno con la sua etichetta
- In vigore, oppure la data da cui parte - e quelli passati spariscono da soli.

Il bottone scarica /calendario/tutti.pdf: un orario per pagina, con le giornate
fuori griglia ripetute su ognuna, perche' un foglio stampato da solo deve bastare
a se stesso. Gli id singoli restano validi per mandarne uno solo.

calendarioDaMostrare (uno) diventa calendariDaMostrare (tutti): una funzione
sola, non due che dicono cose simili - e' lo stesso difetto dei due link della
pagina servizi, che erano due espressioni per lo stesso indirizzo e sono
divergute.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 17:22:22 +02:00
parent d03c62641e
commit 69bd17a296
7 changed files with 207 additions and 147 deletions
+93 -74
View File
@@ -1,42 +1,40 @@
---
// Popup a pieno schermo con la griglia delle class in Sala Piccola.
// Popup a pieno schermo con gli orari delle class in Sala Piccola.
//
// Il calendario è ricostruito dal dato (`data/calendari.ts`) e non è l'immagine ricevuta dal
// cliente: si legge da telefono, si stampa, e per correggere una cella non serve rifare uno
// screenshot. Quale griglia compare lo decide la data — quella in vigore, o la prossima
// dichiarando da quando entra.
// I calendari sono ricostruiti dal dato (`data/calendari.ts`) e non sono le immagini ricevute
// dal cliente: si leggono da telefono, si scaricano in PDF, e per correggere una cella non
// serve rifare uno screenshot.
//
// Si vedono tutti gli orari non ancora scaduti, uno sotto l'altro: chi guarda ad agosto vuole
// sapere com'è settembre per intero. Quelli passati spariscono da soli.
//
// Si apre da qualunque elemento con `data-apri-calendario` nella stessa pagina.
import { calendarioDaMostrare, avvisiAttivi, GIORNI, DISCIPLINE, type Giorno } from '../data/calendari';
import { calendariDaMostrare, avvisiAttivi, GIORNI, DISCIPLINE, type Calendario, type Giorno } from '../data/calendari';
import { t } from '../lib/content';
const adesso = new Date();
const scelta = calendarioDaMostrare(adesso);
const mostrati = calendariDaMostrare(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', {
const dataDi = (iso: string) => new Date(iso).toLocaleDateString('it-IT', {
day: 'numeric', month: 'long', year: 'numeric', timeZone: 'Europe/Rome',
}) : '';
// Discipline davvero presenti in questa griglia: una legenda che elenca anche ciò che non c'è
// fa cercare lezioni che non esistono.
const presenti = cal
? DISCIPLINE.filter((d) => cal.righe.some((r) => Object.values(r.celle).includes(d)))
: [];
});
// Legenda per griglia: elencare discipline che lì non compaiono farebbe cercare lezioni
// che quel periodo non ha.
const disciplinePresenti = (cal: Calendario) =>
DISCIPLINE.filter((d) => cal.righe.some((r) => Object.values(r.celle).includes(d)));
---
{cal && (
<dialog class="cal-dlg" aria-label={`${cal.sala} — ${cal.titolo}`}>
{mostrati.length > 0 && (
<dialog class="cal-dlg" aria-label="Calendario delle class">
<div class="cal-dlg__box">
<header class="cal-dlg__top">
<div>
<p class="cal-dlg__sala">{cal.sala}</p>
<h2 class="cal-dlg__titolo">{cal.titolo}</h2>
{scelta!.futuro && (
<p class="cal-dlg__futuro">{t('calendario.futuro-label')} {dal}</p>
)}
<p class="cal-dlg__sala">{mostrati[0].cal.sala}</p>
<h2 class="cal-dlg__titolo">Calendario delle class</h2>
</div>
<div class="cal-dlg__azioni">
<a class="cal-dlg__btn" href={`/calendario/${cal.id}.pdf`} download>{t('calendario.pdf-label')}</a>
<a class="cal-dlg__btn" href="/calendario/tutti.pdf" download>{t('calendario.pdf-label')}</a>
<button type="button" class="cal-dlg__chiudi" data-chiudi-calendario aria-label={t('calendario.chiudi-label')}>×</button>
</div>
</header>
@@ -50,43 +48,55 @@ const presenti = cal
)}
<div class="cal-dlg__scroll">
<table class="cal-tab">
<thead>
<tr>
<th scope="col" class="cal-tab__ora-h">{cal.sala}</th>
{GIORNI.map((g) => (
<th scope="col" class:list={['cal-tab__gg', cal.chiusi.includes(g) && 'is-chiuso']}>
{g}
{cal.noteGiorno?.[g] && <span class="cal-tab__nota">{cal.noteGiorno[g]}</span>}
</th>
{mostrati.map(({ cal, stato }) => (
<section class="cal-blocco">
<div class="cal-blocco__top">
<h3 class="cal-blocco__titolo">{cal.titolo}</h3>
<span class:list={['cal-tag', stato === 'in-corso' && 'is-ora']}>
{stato === 'in-corso' ? 'In vigore' : `${t('calendario.futuro-label')} ${dataDi(cal.from)}`}
</span>
</div>
<table class="cal-tab">
<thead>
<tr>
<th scope="col" class="cal-tab__ora-h">Ora</th>
{GIORNI.map((g) => (
<th scope="col" class:list={['cal-tab__gg', cal.chiusi.includes(g) && 'is-chiuso']}>
{g}
{cal.noteGiorno?.[g] && <span class="cal-tab__nota">{cal.noteGiorno[g]}</span>}
</th>
))}
</tr>
</thead>
<tbody>
{cal.righe.map((r) => (
<tr class:list={[r.clou && 'is-clou']}>
<th scope="row" class="cal-tab__ora">{r.ora}</th>
{GIORNI.map((g: Giorno) => {
const chiuso = cal.chiusi.includes(g);
const lezione = r.celle[g];
return (
<td class:list={['cal-tab__cella', chiuso && 'is-chiuso', lezione && `is-${lezione.toLowerCase()}`]}>
{chiuso ? <span class="cal-tab__chiuso-txt">{t('calendario.chiuso-label')}</span> : lezione ?? ''}
</td>
);
})}
</tr>
))}
</tbody>
</table>
<ul class="cal-legenda">
{disciplinePresenti(cal).map((d) => (
<li class={`is-${d.toLowerCase()}`}>{d}</li>
))}
</tr>
</thead>
<tbody>
{cal.righe.map((r) => (
<tr class:list={[r.clou && 'is-clou']}>
<th scope="row" class="cal-tab__ora">{r.ora}</th>
{GIORNI.map((g: Giorno) => {
const chiuso = cal.chiusi.includes(g);
const lezione = r.celle[g];
return (
<td class:list={['cal-tab__cella', chiuso && 'is-chiuso', lezione && `is-${lezione.toLowerCase()}`]}>
{chiuso ? <span class="cal-tab__chiuso-txt">{t('calendario.chiuso-label')}</span> : lezione ?? ''}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</ul>
</section>
))}
</div>
<footer class="cal-dlg__giu">
<ul class="cal-legenda">
{presenti.map((d) => (
<li class={`is-${d.toLowerCase()}`}>{d}</li>
))}
</ul>
<p class="cal-dlg__nota">{t('calendario.nota')}</p>
</footer>
</div>
@@ -104,9 +114,9 @@ const presenti = cal
dlg.querySelector('[data-chiudi-calendario]')?.addEventListener('click', () => dlg.close());
// Click sul fondale: il target è il dialog stesso solo quando si preme fuori dal riquadro.
dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); });
// "Scarica in PDF" è un link a `/calendario/<id>.pdf`, cioè un file vero generato dal
// server: si salva sul telefono e si gira su WhatsApp. Il CSS di stampa più sotto resta
// per chi preme Ctrl+P col popup aperto.
// "Scarica in PDF" è un link a /calendario/tutti.pdf, cioè un file vero generato dal
// server, un orario per pagina: si salva sul telefono e si gira su WhatsApp. Il CSS di
// stampa più sotto resta per chi preme Ctrl+P col popup aperto.
}
</script>
@@ -126,10 +136,6 @@ const presenti = cal
letter-spacing: .25em; text-transform: uppercase; color: var(--c-accent-dark);
}
.cal-dlg__titolo { margin: 6px 0 0; font-size: 1.5rem; }
.cal-dlg__futuro {
margin: 6px 0 0; font-family: var(--font-heading); font-size: .72rem; letter-spacing: .12em;
text-transform: uppercase; color: var(--c-text-light);
}
.cal-dlg__azioni { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
.cal-dlg__btn {
display: inline-block; font-family: var(--font-heading); font-size: .68rem; font-weight: 600;
@@ -144,8 +150,8 @@ 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. */
/* Giornate fuori griglia: stanno sopra le tabelle perché sono l'eccezione a ciò che le
tabelle dicono, 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;
@@ -153,13 +159,24 @@ const presenti = cal
.cal-avvisi li { font-size: .84rem; color: var(--c-text); }
.cal-avvisi strong { color: var(--c-heading); }
.cal-dlg__scroll { flex: 1; overflow: auto; margin: 18px 0 0; }
.cal-blocco + .cal-blocco { margin-top: 34px; padding-top: 26px; border-top: 1px solid #e4e0d9; }
.cal-blocco__top { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; }
.cal-blocco__titolo { margin: 0; font-size: 1.05rem; }
.cal-tag {
font-family: var(--font-heading); font-size: .62rem; font-weight: 600; letter-spacing: .14em;
text-transform: uppercase; padding: 4px 9px; background: var(--c-bg-alt); color: var(--c-text-light);
}
/* L'orario valido adesso si distingue a colpo d'occhio da quello che comincerà dopo: senza,
due griglie affiancate si leggono come alternative fra cui scegliere. */
.cal-tag.is-ora { background: var(--c-accent); color: #fff; }
/* 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; }
.cal-tab { border-collapse: collapse; width: 100%; min-width: 720px; }
.cal-tab th, .cal-tab td { border: 1px solid #e4e0d9; padding: 9px 10px; text-align: left; }
.cal-tab thead th {
position: sticky; top: 0; z-index: 2; background: var(--c-bg-alt);
background: var(--c-bg-alt);
font-family: var(--font-heading); font-size: .7rem; font-weight: 600; letter-spacing: .16em;
text-transform: uppercase; color: var(--c-heading);
}
@@ -169,7 +186,7 @@ const presenti = cal
}
.cal-tab__ora-h { color: var(--c-accent-dark) !important; }
.cal-tab__ora {
position: sticky; left: 0; z-index: 1; background: var(--c-bg);
background: var(--c-bg);
font-family: var(--font-heading); font-size: .78rem; font-weight: 600; color: var(--c-heading);
white-space: nowrap;
}
@@ -185,8 +202,7 @@ const presenti = cal
.cal-tab__chiuso-txt { font-size: .62rem; letter-spacing: .12em; color: var(--c-text-light); }
.cal-tab thead th.is-chiuso { color: var(--c-text-light); }
.cal-dlg__giu { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 24px; padding-top: 16px; }
.cal-legenda { display: flex; flex-wrap: wrap; gap: 8px 16px; list-style: none; margin: 0; padding: 0; }
.cal-legenda { display: flex; flex-wrap: wrap; gap: 8px 16px; list-style: none; margin: 12px 0 0; padding: 0; }
.cal-legenda li {
font-family: var(--font-heading); font-size: .66rem; letter-spacing: .12em; padding: 5px 10px;
color: var(--c-heading);
@@ -195,11 +211,13 @@ const presenti = cal
.cal-legenda li.is-pilates { background: #f6efe0; box-shadow: inset 3px 0 0 #cbb98f; }
.cal-legenda li.is-yoga { background: #e3eadf; box-shadow: inset 3px 0 0 #8ba57f; }
.cal-legenda li.is-mobility { background: #f0e3e3; box-shadow: inset 3px 0 0 #b78e8e; }
.cal-dlg__giu { padding-top: 16px; }
.cal-dlg__nota { margin: 0; font-size: .8rem; color: var(--c-text-light); }
/* Stampa (ossia "Salva come PDF"): resta solo la griglia, in orizzontale e a piena pagina.
`visibility` invece di `display` perché il dialog è annidato nel corpo della pagina e
nascondere gli antenati nasconderebbe anche lui. */
/* Stampa dalla pagina (Ctrl+P col popup aperto): resta solo il calendario, un orario per
foglio. Il bottone scarica invece il PDF generato dal server, che è il documento buono da
girare. `visibility` invece di `display` perché il dialog è annidato nel corpo della
pagina e nascondere gli antenati nasconderebbe anche lui. */
@media print {
@page { size: A4 landscape; margin: 10mm; }
body { visibility: hidden; }
@@ -209,8 +227,9 @@ const presenti = cal
.cal-dlg__box { padding: 0; height: auto; }
.cal-dlg__azioni { display: none; }
.cal-dlg__scroll { overflow: visible; }
.cal-blocco { break-inside: avoid; }
.cal-blocco + .cal-blocco { break-before: page; border-top: 0; padding-top: 0; }
.cal-tab { min-width: 0; }
.cal-tab thead th, .cal-tab__ora { position: static; }
.cal-tab th, .cal-tab td { padding: 5px 6px; }
.cal-tab__cella { font-size: .62rem; }
}