Allinea l'app ai nuovi testi e ai destinatari dei pulsanti (DD-040)

I pulsanti «Avvisa chi è di turno» e «Sollecita» usano gli stessi testi dei job
del database. Il sollecito manuale segue i convocati e dice la risposta attuale
e chi lo ha chiesto. L'ordine degli eventi dei palloni diventa data, ora, id.
Le righe del corpo di una notifica vanno a capo nel centro notifiche.

Test unitari dei testi, dei destinatari e delle date; il test delle route
verifica testi, convocati e assenza di email per i palloni.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-09-30 14:03:50 +02:00
1 parent 563142a30a
commit 9b4e3e4b45
12 files changed
+363 -48

No files matched your search

+10 -1
View File
@@ -1,6 +1,6 @@
/** Check dei dati di base della rosa: `bun test/unit/crapp-data.test.ts`. */
import assert from "node:assert/strict";
import { formatData, giocatori, statoMeta } from "@/lib/crapp-data";
import { formatData, formatDataNumerica, giocatori, statoMeta } from "@/lib/crapp-data";
// --- rosa --------------------------------------------------------------------
assert.ok(giocatori.length > 0, "la rosa non è vuota");
@@ -29,6 +29,15 @@ for (const g of giocatori) {
// --- formatData --------------------------------------------------------------
assert.equal(formatData("2026-09-01"), "mar 01 settembre");
// --- formatDataNumerica: la data dei testi delle notifiche, come to_char(…, 'DD/MM/YYYY') ----
assert.equal(formatDataNumerica("2026-09-01"), "01/09/2026");
assert.equal(formatDataNumerica("2026-12-31"), "31/12/2026");
assert.equal(
formatDataNumerica("2026-03-29"),
"29/03/2026",
"nessuna conversione di fuso: il giorno del cambio ora resta quello scritto",
);
assert.equal(formatData("2026-01-31"), "sab 31 gennaio");
assert.ok(
!formatData("2026-03-29").includes("28"),
+18
View File
@@ -197,6 +197,24 @@ await prova("il testo dell'admin non inietta HTML e conserva gli a-capo", () =>
assert.equal(escapeHtml(`<&>"'`), "&lt;&amp;&gt;&quot;&#39;");
});
await prova(
"il corpo a righe etichettate delle notifiche automatiche (DD-040) resta leggibile",
() => {
const corpo = "Data: domani, 01/10/2026\nOra: 21:00\nLuogo: PalaCRAP";
const m = componiMail(
notifica({
oggetto: "Promemoria evento di domani: Allenamento",
testo: corpo,
id_evento: "ev-1",
}),
OPZ,
);
assert.equal(m.subject, "Promemoria evento di domani: Allenamento");
assert.ok(m.text.startsWith(`${corpo}\n\nApri CrAPP: `), "in testo semplice le righe restano");
assert.match(m.html, /Data: domani, 01\/10\/2026<br>Ora: 21:00<br>Luogo: PalaCRAP/);
},
);
await prova("titolo con HTML: l'intestazione della mail è sempre esclusa dal markup", () => {
const m = componiMail(notifica({ oggetto: "<b>Ciao</b>" }), OPZ);
assert.ok(!m.html.includes("<b>"));
+31
View File
@@ -1,10 +1,12 @@
/** Check delle notifiche in-app (M17): `bun test/unit/notifiche-utente.test.ts`. */
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import {
contatoreBadge,
daRiga,
pallinoNotifiche,
type RigaNotifica,
type TipoNotifica,
} from "@/lib/notifiche-utente";
// --- daRiga: conversione database -> modello applicativo ---------------------
@@ -48,4 +50,33 @@ assert.deepEqual(pallinoNotifiche(30, 12), { testo: "9+", daLeggere: true });
// Tutte lette: il pallino resta per poterle ancora aprire ed eliminare.
assert.deepEqual(pallinoNotifiche(3, 0), { testo: "3", daLeggere: false });
// --- i tipi automatici di DD-040 attraversano daRiga senza perdere le righe del testo -----
const tipiNuovi: TipoNotifica[] = [
"turno_palloni_12h",
"turno_palloni_6h",
"turno_palloni_3h",
"turno_palloni_revocato",
"sollecita_presenze_24h",
"sollecita_presenze_12h",
"sollecita_presenze_6h",
];
const corpoMultiriga = "Data: 01/10/2026\nOra: 21:00\nLuogo: PalaCRAP";
for (const tipo of tipiNuovi) {
const n = daRiga({ ...riga, tipo, corpo: corpoMultiriga });
assert.equal(n.tipo, tipo);
assert.equal(n.corpo, corpoMultiriga, `${tipo}: il corpo a più righe arriva intatto`);
}
// Le righe del corpo vanno a capo solo se la riga della notifica lo chiede al CSS: senza
// `whitespace-pre-line` il testo a più righe diventerebbe un paragrafo unico.
const uiBits = readFileSync(
new URL("../../src/components/crapp/ui-bits.tsx", import.meta.url),
"utf8",
);
assert.match(
uiBits,
/whitespace-pre-line[^"]*"[^>]*>\s*\{notifica\.corpo\}/,
"il corpo della notifica mantiene gli a capo",
);
console.log("notifiche-utente: ok");
+49 -3
View File
@@ -179,20 +179,46 @@ assert.deepEqual(
);
// --- avvisiPalloniEvento ------------------------------------------------------
// Il promemoria per un evento scelto dall'admin: due destinatari, con testi diversi.
// L'avviso per un evento scelto: due destinatari, con testi diversi (DD-040). Gli stessi testi dei
// job automatici del database: `test/integration/notifiche-automatiche.test.ts` li confronta.
const avvisi = avvisiPalloniEvento(turniPush, eventiPush, "p2");
assert.deepEqual(
avvisi.map((a) => a.giocatoreId),
["g2", "g1"],
"l'incaricato di questo evento, e chi ha i palloni dalla volta prima",
);
assert.match(avvisi[0]!.titolo, /prendere i palloni/);
assert.match(avvisi[1]!.titolo, /Porta i palloni/);
assert.equal(avvisi[0]!.titolo, "Turno palloni: incarico assegnato");
assert.equal(avvisi[1]!.titolo, "Turno palloni: riconsegna");
assert.equal(
avvisi[0]!.testo,
[
"Evento: Evento p2",
"Data: 02/02/2026, ore 21:00",
"Incarico: custodia dei palloni al termine dell'evento",
"Riconsegna: 03/02/2026",
].join("\n"),
"incarico: evento, data numerica, incarico e data di riconsegna dell'evento successivo",
);
assert.equal(
avvisi[1]!.testo,
[
"Evento: Evento p2",
"Data: 02/02/2026, ore 21:00",
"Incarico: riconsegna dei palloni in custodia dal turno precedente",
].join("\n"),
"riconsegna: nessuna riga «Riconsegna», chi la riceve li riporta",
);
assert.ok(
avvisi.every((a) => !/oggi/i.test(a.testo)),
"il testo nomina l'evento, non 'oggi': può arrivare giorni prima",
);
const ultimo = avvisiPalloniEvento(turniPush, eventiPush, "p3");
assert.ok(
ultimo.every((a) => !a.testo.includes("Riconsegna:")),
"l'ultimo evento non ha un successivo: la riga «Riconsegna» manca",
);
assert.deepEqual(
avvisiPalloniEvento(turniPush, eventiPush, "p1").map((a) => a.giocatoreId),
["g1"],
@@ -213,6 +239,26 @@ assert.deepEqual(
[],
"senza turni assegnati non c'è nessuno da avvisare",
);
assert.deepEqual(
avvisiPalloniEvento({ p1: "g1" }, eventiPush, "p2"),
[],
"senza un incaricato per l'evento non parte nulla, nemmeno la riconsegna (DD-040)",
);
// --- ordine: a parità di data conta l'ora, poi l'id --------------------------
const stessoGiorno: Evento[] = [
{ ...evento("z", "2026-06-01"), ora: "21:00" },
{ ...evento("y", "2026-06-01"), ora: "18:30" },
{ ...evento("x", "2026-06-01"), ora: "21:00" },
evento("a", "2026-05-31"),
];
assert.deepEqual(
eventiPalloni(stessoGiorno).map((e) => e.id),
["a", "y", "x", "z"],
"data, poi ora, poi id: lo stesso ordine dei job del database",
);
assert.equal(eventoPrecedente(stessoGiorno, "x")?.id, "y");
assert.equal(eventoSuccessivo(stessoGiorno, "y")?.id, "x");
// --- serieConsecutivaPalloni: volte consecutive in cui li ha portati lui -----
const eventiSerie: Evento[] = [
+60
View File
@@ -9,6 +9,7 @@ import {
includeInfortunato,
serieConferme,
serieConsecutiva,
testoSollecito,
totaliEventiGiocatore,
type MappaPresenze,
type MappaTempiRisposta,
@@ -158,6 +159,65 @@ assert.deepEqual(
);
assert.deepEqual(destinatariSollecito([], risposte), [], "rosa vuota, nessun destinatario");
// --- il sollecito segue i convocati (DD-040) ----------------------------------
assert.deepEqual(
destinatariSollecito(squadra, risposte, ["g1", "g2", "g3"]),
["g2", "g3"],
"solo i convocati senza risposta o con «forse»: g1 ha risposto, g4 non è convocato",
);
assert.deepEqual(
destinatariSollecito(squadra, risposte, ["g4"]),
[],
"l'unico convocato ha risposto: nessun sollecito",
);
assert.deepEqual(
destinatariSollecito(squadra, [], ["g3", "g5", "g6", "g9"]),
["g3"],
"convocati inattivi, allenatori o fuori rosa restano esclusi anche se convocati",
);
assert.deepEqual(
destinatariSollecito(squadra, risposte, []),
["g2", "g3"],
"nessun convocato indicato vale per tutta la rosa, come nel database",
);
// --- testo del sollecito -----------------------------------------------------
const eventoSollecito = {
titolo: "Allenamento del giovedì",
data: "2026-10-01",
ora: "21:00",
luogo: "PalaCRAP",
};
const automatico = testoSollecito(eventoSollecito, undefined);
assert.equal(automatico.titolo, "Conferma di partecipazione richiesta: Allenamento del giovedì");
assert.equal(
automatico.testo,
[
"Data: 01/10/2026",
"Ora: 21:00",
"Luogo: PalaCRAP",
"Risposta attuale: nessuna",
"Azione: indicare presente, assente o in ritardo",
].join("\n"),
);
assert.match(testoSollecito(eventoSollecito, "forse").testo, /Risposta attuale: forse/);
assert.match(
testoSollecito(eventoSollecito, "assente").testo,
/Risposta attuale: nessuna/,
"qualunque stato diverso da «forse» si legge come nessuna risposta",
);
const manuale = testoSollecito(eventoSollecito, undefined, "Mario Rossi");
assert.equal(
manuale.testo.split("\n")[3],
"Richiesta di: Mario Rossi",
"il sollecito manuale nomina chi lo chiede, tra il luogo e la risposta attuale",
);
assert.ok(!automatico.testo.includes("Richiesta di"), "l'automatico non ha un mittente");
assert.ok(
!testoSollecito({ ...eventoSollecito, luogo: " " }, undefined).testo.includes("Luogo:"),
"senza luogo la riga manca",
);
// --- serie conferme: risposta entro 24h dalla convocazione --------------------
const convocati = (id: string, data: string, creatoIl?: string): Evento => ({
...ev(id, "allenamento", data),