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

+92 -5
View File
@@ -77,6 +77,7 @@ if (!locale) {
const ALLENATORE = "g6";
const PARTITA = `${PREFISSO}-partita`;
const PRESENZE = `${PREFISSO}-presenze`;
const PRESENZE_CONVOCATI = `${PREFISSO}-presenze-convocati`;
const PALLONI_1 = `${PREFISSO}-palloni-1`;
const PALLONI_2 = `${PREFISSO}-palloni-2`;
const GENERICO = `${PREFISSO}-generico`;
@@ -150,10 +151,10 @@ if (!locale) {
/** A quali slot è arrivata una push dall'ultimo `svuota()`, senza ripetizioni e in ordine. */
const destinatariPush = () => [...new Set(ricevute)].sort();
type Notifica = { giocatore_id: string; titolo: string; tipo: string };
type Notifica = { giocatore_id: string; titolo: string; corpo: string; tipo: string };
const notifiche = (filtro: string) =>
leggi<Notifica>(
`notifiche_utente?${filtro}&select=giocatore_id,titolo,tipo&order=giocatore_id`,
`notifiche_utente?${filtro}&select=giocatore_id,titolo,corpo,tipo&order=giocatore_id`,
);
const slot = (righe: Notifica[]) => righe.map((r) => r.giocatore_id);
@@ -191,6 +192,7 @@ if (!locale) {
for (const [id, tipo, data] of [
[PARTITA, "partita", "2031-04-01"],
[PRESENZE, "allenamento", "2031-04-02"],
[PRESENZE_CONVOCATI, "partita", "2031-04-03"],
[PALLONI_1, "allenamento", "2031-05-01"],
[PALLONI_2, "allenamento", "2031-05-08"],
[GENERICO, "evento", "2031-05-09"],
@@ -201,7 +203,8 @@ if (!locale) {
titolo: `${PREFISSO} ${id}`,
data,
ora: "20:00",
convocati: [],
luogo: id === PRESENZE ? "PalaCRAP" : "",
convocati: id === PRESENZE_CONVOCATI ? [G_QUATTRO, G_CINQUE] : [],
});
}
@@ -373,6 +376,72 @@ if (!locale) {
},
);
await prova(
"il sollecito manuale ha il testo nuovo, con chi lo chiede e la risposta attuale",
async () => {
svuota();
const res = await chiama("sollecita-presenze", tokenAdmin, {
eventoId: PRESENZE,
da: "Mario Rossi",
});
assert.equal(res.status, 200);
const righe = await notifiche(`evento_id=eq.${PRESENZE}&tipo=eq.sollecita_presenze`);
const cinque = righe.find((r) => r.giocatore_id === G_CINQUE);
const altro = righe.find((r) => r.giocatore_id !== G_CINQUE);
assert.equal(
cinque?.titolo,
`Conferma di partecipazione richiesta: ${PREFISSO} ${PRESENZE}`,
);
assert.equal(
cinque?.corpo,
[
"Data: 02/04/2031",
"Ora: 20:00",
"Luogo: PalaCRAP",
"Richiesta di: Mario Rossi",
"Risposta attuale: forse",
"Azione: indicare presente, assente o in ritardo",
].join("\n"),
);
assert.match(altro?.corpo ?? "", /Risposta attuale: nessuna/, "chi non ha risposto");
const conId = await leggi<{ id: string }>(
`notifiche_utente?evento_id=eq.${PRESENZE}&tipo=eq.sollecita_presenze&select=id`,
);
const email = await leggi<{ notifica_id: string }>(
`notifiche_email_coda?notifica_id=in.(${conId.map((r) => r.id).join(",")})&select=notifica_id`,
);
assert.equal(email.length, conId.length, "il sollecito manuale parte anche per email");
assert.equal(
righe.filter((r) => r.giocatore_id === G_CINQUE).length,
1,
"ripremere il pulsante riscrive la notifica, non la duplica",
);
assert.deepEqual(destinatariPush(), [G_CINQUE]);
},
);
await prova(
"il sollecito manuale segue i convocati, come quello automatico (DD-040)",
async () => {
const res = await chiama("sollecita-presenze", tokenAdmin, {
eventoId: PRESENZE_CONVOCATI,
});
assert.equal(res.status, 200);
const righe = await notifiche(
`evento_id=eq.${PRESENZE_CONVOCATI}&tipo=eq.sollecita_presenze`,
);
assert.deepEqual(
slot(righe),
[G_QUATTRO, G_CINQUE],
"solo i convocati: i non convocati non ricevono nulla, nemmeno senza risposta",
);
assert.ok(
!righe[0]!.corpo.includes("Richiesta di"),
"senza nome, senza la riga «Richiesta di»",
);
},
);
await prova("un giocatore non può sollecitare le presenze", async () => {
const res = await chiama("sollecita-presenze", giocatore.token, { eventoId: PRESENZE });
assert.ok(!res.ok, `stato ${res.status}`);
@@ -390,8 +459,25 @@ if (!locale) {
const righe = await notifiche(`evento_id=eq.${PALLONI_2}&tipo=eq.turno_palloni`);
assert.deepEqual(slot(righe), [G_UNO, G_DUE]);
const per = new Map(righe.map((r) => [r.giocatore_id, r.titolo]));
assert.equal(per.get(G_DUE), "Tocca a te prendere i palloni");
assert.equal(per.get(G_UNO), "Porta i palloni");
assert.equal(per.get(G_DUE), "Turno palloni: incarico assegnato");
assert.equal(per.get(G_UNO), "Turno palloni: riconsegna");
const corpoDue = righe.find((r) => r.giocatore_id === G_DUE)?.corpo ?? "";
assert.match(
corpoDue,
/^Evento: test-destinatari-notifiche test-destinatari-notifiche-palloni-2\n/,
);
assert.match(corpoDue, /\nData: 08\/05\/2031, ore 20:00\n/);
assert.match(corpoDue, /Incarico: custodia dei palloni al termine dell'evento/);
// DD-040: il turno palloni arriva come push e in-app, mai per email.
const conId = await leggi<{ id: string }>(
`notifiche_utente?evento_id=eq.${PALLONI_2}&tipo=eq.turno_palloni&select=id`,
);
assert.equal(conId.length, righe.length);
const email = await leggi<{ notifica_id: string }>(
`notifiche_email_coda?notifica_id=in.(${conId.map((r) => r.id).join(",")})&select=notifica_id`,
);
assert.equal(email.length, 0, "nessuna email per il turno palloni");
});
await prova(
@@ -427,6 +513,7 @@ if (!locale) {
for (const id of [
PARTITA,
PRESENZE,
PRESENZE_CONVOCATI,
PALLONI_1,
PALLONI_2,
GENERICO,