Calendario delle class: popup a pieno schermo dalle pagine Performance e Holistic

Le due griglie fornite dal cliente (1-18 settembre, dal 21 in poi) sono
trascritte come dato e ridisegnate nello stile del sito: si leggono da telefono,
si stampano in PDF e per correggere una cella non serve rifare uno screenshot.

Quale griglia compare lo decide la data, come per le edizioni della promo: quella
in vigore, oppure - se nessuna lo e' ancora - la prossima, dichiarando da quando
entra. Serve perche' il link esiste da subito e ad agosto un "nessun calendario"
non direbbe niente a chi vuole gli orari di settembre.

Il popup e' un <dialog> nativo: fondale, Esc e trappola del focus senza
librerie. "Scarica in PDF" stampa la sola griglia, gia' impaginata in orizzontale
dal CSS di stampa. Le discipline hanno una barra colorata a sinistra oltre al
fondo, cosi' restano distinguibili anche quando il browser stampa senza sfondi.

Quattordici test sul dato: confini al secondo in ora italiana, fasce orarie
uniche e in ordine, nessuna lezione in un giorno di chiusura, e il buco vero fra
il 19 e il 20 settembre resta dichiarato invece che nascosto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:58:47 +02:00
parent a18a505254
commit 01f49ab6d2
5 changed files with 453 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
// Calendari delle class in Sala Piccola.
//
// Trascritti dai due fogli forniti dal cliente il 10/08/2026 (immagini su Wasabi, cartella
// del progetto). Sono dato e non immagini di proposito: così la griglia si legge da telefono,
// si stampa in PDF e si corregge una cella senza rifare uno screenshot.
//
// Ogni calendario porta la sua finestra di validità: la pagina mostra quello in vigore, e
// quando nessuno lo è ancora mostra il prossimo dichiarando da quando entra. Il passaggio da
// un orario all'altro avviene da solo, senza deploy — stesso principio delle edizioni della
// pagina promo (`data/promo-edizioni.ts`).
export const GIORNI = ['LUN', 'MAR', 'MER', 'GIO', 'VEN', 'SAB', 'DOM'] as const;
export type Giorno = (typeof GIORNI)[number];
/** Le discipline della sala: il colore di ognuna è definito nel componente del calendario. */
export const DISCIPLINE = ['REFORMER', 'PILATES', 'YOGA', 'MOBILITY'] as const;
export type Disciplina = (typeof DISCIPLINE)[number];
export interface RigaCalendario {
ora: string;
/** Fascia messa in risalto nel foglio del cliente (le righe gialle). */
clou?: boolean;
celle: Partial<Record<Giorno, Disciplina>>;
}
export interface Calendario {
id: string;
/** Come lo chiama il cliente: è anche l'etichetta mostrata in pagina. */
titolo: string;
sala: string;
/** Validità, ISO con offset esplicito (il server gira in UTC). `to: null` = a tempo indeterminato. */
from: string;
to: string | null;
/** Giorni in cui la sala è chiusa: colonna barrata, non vuota — vuoto e chiuso non sono lo stesso. */
chiusi: Giorno[];
/** Precisazione su una colonna, es. il sabato aperto una volta sola. */
noteGiorno?: Partial<Record<Giorno, string>>;
righe: RigaCalendario[];
}
export const calendari: Calendario[] = [
{
id: 'settembre-1-18',
titolo: "Dall'1 al 18 settembre",
sala: 'Sala piccola',
from: '2026-09-01T00:00:00+02:00',
to: '2026-09-18T23:59:59+02:00',
chiusi: ['DOM'],
noteGiorno: { SAB: 'solo il 12 settembre' },
righe: [
{ ora: '6.15', celle: {} },
{ ora: '7.00', clou: true, celle: { MAR: 'YOGA' } },
{ ora: '7.45', clou: true, celle: { MAR: 'PILATES', MER: 'REFORMER' } },
{ ora: '8.30', celle: { MER: 'REFORMER', VEN: 'MOBILITY' } },
{ ora: '9.15', celle: { VEN: 'REFORMER', SAB: 'MOBILITY' } },
{ ora: '10.00', celle: { VEN: 'REFORMER', SAB: 'PILATES' } },
{ ora: '10.45', celle: { SAB: 'REFORMER' } },
{ ora: '11.30', celle: { SAB: 'REFORMER' } },
{ ora: '12.15', clou: true, celle: { MAR: 'REFORMER', MER: 'REFORMER', GIO: 'REFORMER' } },
{ ora: '13.00', clou: true, celle: { MAR: 'REFORMER', MER: 'PILATES', GIO: 'REFORMER' } },
{ ora: '13.45', celle: {} },
{ ora: '14.30', celle: { MAR: 'REFORMER' } },
{ ora: '15.15', celle: { MAR: 'MOBILITY' } },
{ ora: '16.00', celle: {} },
{ ora: '16.45', celle: { MAR: 'REFORMER' } },
{ ora: '17.30', clou: true, celle: { MAR: 'REFORMER', GIO: 'REFORMER' } },
{ ora: '18.15', clou: true, celle: { MAR: 'REFORMER', GIO: 'REFORMER' } },
{ ora: '19.00', clou: true, celle: { MAR: 'REFORMER', GIO: 'REFORMER' } },
{ ora: '19.45', clou: true, celle: { MAR: 'REFORMER', GIO: 'PILATES' } },
],
},
{
id: 'settembre-21-in-poi',
titolo: 'Dal 21 settembre in poi',
sala: 'Sala piccola',
from: '2026-09-21T00:00:00+02:00',
to: null,
chiusi: ['DOM'],
righe: [
{ ora: '6.15', celle: { MER: 'MOBILITY' } },
{ ora: '7.00', clou: true, celle: { MAR: 'YOGA', MER: 'REFORMER' } },
{ ora: '7.45', clou: true, celle: { MAR: 'PILATES', MER: 'REFORMER', VEN: 'PILATES' } },
{ ora: '8.30', celle: { MAR: 'REFORMER', MER: 'REFORMER', VEN: 'MOBILITY' } },
{ ora: '9.15', celle: { VEN: 'REFORMER', SAB: 'MOBILITY' } },
{ ora: '10.00', celle: { VEN: 'REFORMER', SAB: 'PILATES' } },
{ ora: '10.45', celle: { SAB: 'REFORMER' } },
{ ora: '11.30', celle: { GIO: 'YOGA', SAB: 'REFORMER' } },
{ ora: '12.15', clou: true, celle: { MAR: 'REFORMER', MER: 'REFORMER', GIO: 'REFORMER' } },
{ ora: '13.00', clou: true, celle: { MAR: 'REFORMER', MER: 'PILATES', GIO: 'REFORMER' } },
{ ora: '13.45', celle: {} },
{ ora: '14.30', celle: { MAR: 'REFORMER' } },
{ ora: '15.15', celle: { MAR: 'MOBILITY' } },
{ ora: '16.00', celle: {} },
{ ora: '16.45', celle: { MAR: 'REFORMER' } },
// ⚠️ Mercoledì 17.30 e 18.15: nel foglio ricevuto quelle due celle sono coperte dal
// riquadro "Opzioni Incolla" di Excel, quindi non sono leggibili. Qui restano vuote
// perché inventarle sarebbe peggio: vanno confermate dal cliente.
{ ora: '17.30', clou: true, celle: { LUN: 'PILATES', MAR: 'REFORMER', GIO: 'REFORMER' } },
{ ora: '18.15', clou: true, celle: { LUN: 'REFORMER', MAR: 'REFORMER', GIO: 'REFORMER' } },
{ ora: '19.00', clou: true, celle: { LUN: 'REFORMER', MAR: 'REFORMER', GIO: 'REFORMER' } },
{ ora: '19.45', clou: true, celle: { LUN: 'REFORMER', MAR: 'REFORMER', GIO: 'PILATES' } },
],
},
];
/** Servizi dalla cui pagina si apre il calendario della sala. */
export const SERVIZI_CON_CALENDARIO = ['performance-class', 'holistic-class'];
/**
* Il calendario da mostrare: quello in vigore adesso, oppure — se nessuno lo è ancora — il
* primo che entrerà in vigore, segnalato come futuro. `null` solo se non ce n'è nessuno da
* mostrare, e in quel caso la voce non compare in pagina.
*
* Serve il ripiego sul futuro perché il link esiste **prima** che il primo orario cominci: un
* "nessun calendario disponibile" ad agosto non direbbe niente a chi vuole sapere gli orari
* di settembre.
*/
export function calendarioDaMostrare(now: Date, list: Calendario[] = calendari):
{ cal: Calendario; futuro: boolean } | null {
const t = now.getTime();
const attivo = list.find((c) => t >= Date.parse(c.from) && (c.to === null || t <= Date.parse(c.to)));
if (attivo) return { cal: attivo, futuro: false };
const prossimi = list.filter((c) => Date.parse(c.from) > t).sort((a, b) => Date.parse(a.from) - Date.parse(b.from));
return prossimi.length ? { cal: prossimi[0], futuro: true } : null;
}