Promemoria senza ritorni e tre canali per ogni notifica (DD-037, DD-038)
m23: un registro (promemoria_eventi_generati) ricorda i promemoria a 24 e 3 ore già generati, così eliminare la notifica non li fa rigenerare dal cron né riaccodare la mail a ogni giro. m24: la push dei promemoria, che nascono nel database, la manda il worker mailer/ con lo stesso modulo dell'app (coda notifiche_push_coda, esiti, backoff, iscrizioni scadute eliminate, push scartate oltre 3 ore). Il sondaggio pre-partita scrive anche notifiche_utente (tipo sondaggio_cacche): compare in-app e arriva per email. L'immagine del worker si costruisce dalla radice del repository per includere src/lib/webpush.server.ts. Test: 9 di integrazione per m23, 17 per la coda push, 6 per la route del sondaggio, nuovi casi unitari sul worker; prova end-to-end con un servizio push finto. Documentazione e catalogo delle notifiche aggiornati. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
58eba642f7
commit
e9013b703b
27 files changed
+2076
-92
No files matched your search
@@ -69,7 +69,7 @@ export function SondaggioCacche({
|
||||
toast.success(
|
||||
dati.inviate > 0
|
||||
? `Notifica inviata a ${dati.inviate} dispositivi`
|
||||
: "Nessun dispositivo con le notifiche attive",
|
||||
: "Avviso inviato in app e per email (nessun dispositivo con le notifiche attive)",
|
||||
);
|
||||
} catch {
|
||||
toast.error("Non sono riuscito a inviare la notifica");
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { inRosa, type TipoMembro } from "./giocatori-squadra";
|
||||
import { oggiISO } from "./palloni-core";
|
||||
|
||||
/**
|
||||
* Avviso «sondaggio pre-partita aperto» (push, centro notifiche ed email, DD-038): stesso testo su
|
||||
* tutti e tre i canali.
|
||||
*/
|
||||
export function avvisoSondaggio(partita: { titolo: string; ora: string }): {
|
||||
titolo: string;
|
||||
testo: string;
|
||||
} {
|
||||
return {
|
||||
titolo: "💩 Sondaggio pre-partita aperto",
|
||||
testo: `${partita.titolo} · ore ${partita.ora}. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Chi riceve l'avviso in-app e per email: i giocatori attivi, mai gli allenatori, che il sondaggio
|
||||
* non riguarda (DD-034). Non dipende dai dispositivi iscritti alla push: l'avviso in-app e la mail
|
||||
* arrivano anche a chi non ha attivato le notifiche.
|
||||
*/
|
||||
export function destinatariSondaggio(
|
||||
squadra: Array<{ id: string; attivo: boolean; tipo: TipoMembro }>,
|
||||
): string[] {
|
||||
return squadra.filter(inRosa).map((g) => g.id);
|
||||
}
|
||||
|
||||
/** Ora di apertura del sondaggio, il giorno stesso della partita. */
|
||||
export const ORA_APERTURA_SONDAGGIO = 8;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useGiocatoreId } from "./user-store";
|
||||
|
||||
/**
|
||||
* Centro notifiche in-app (M17): pallino sull'avatar del profilo, con il numero delle non
|
||||
* lette. Quattro sorgenti riempiono `notifiche_utente` (lato database, mai dal client): un
|
||||
* lette. Cinque sorgenti riempiono `notifiche_utente` (lato database, mai dal client): un
|
||||
* messaggio libero dell'admin, un promemoria automatico prima di un evento (24h e 3h), il
|
||||
* turno palloni e il sollecito presenze (questi due avviati a mano da un admin, in
|
||||
* parallelo alla push esistente). Qui si legge e si segna come letto, con
|
||||
@@ -16,7 +16,8 @@ export type TipoNotifica =
|
||||
| "evento_promemoria_24h"
|
||||
| "evento_promemoria_3h"
|
||||
| "turno_palloni"
|
||||
| "sollecita_presenze";
|
||||
| "sollecita_presenze"
|
||||
| "sondaggio_cacche";
|
||||
|
||||
export type NotificaUtente = {
|
||||
id: string;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { z } from "zod";
|
||||
import { richiediAdmin } from "@/lib/auth-route.server";
|
||||
import { avvisoSondaggio, destinatariSondaggio } from "@/lib/cacche";
|
||||
import { leggiEventi } from "@/lib/eventi.server";
|
||||
import { inviaPush } from "@/lib/webpush.server";
|
||||
import { isAllenatore } from "@/lib/giocatori-squadra";
|
||||
@@ -27,14 +29,30 @@ export const Route = createFileRoute("/api/public/apri-sondaggio")({
|
||||
const { data: tutte } = await supabaseAdmin
|
||||
.from("push_subscriptions")
|
||||
.select("endpoint, p256dh, auth, giocatore_id");
|
||||
const squadra = await leggiGiocatoriSquadra();
|
||||
// Il sondaggio cacche non riguarda l'allenatore, che non le vede (DD-034).
|
||||
const allenatori = new Set(
|
||||
(await leggiGiocatoriSquadra()).filter(isAllenatore).map((g) => g.id),
|
||||
);
|
||||
const allenatori = new Set(squadra.filter(isAllenatore).map((g) => g.id));
|
||||
const iscrizioni = (tutte ?? []).filter((i) => !allenatori.has(i.giocatore_id));
|
||||
|
||||
const titolo = "💩 Sondaggio pre-partita aperto";
|
||||
const testo = `${partita.titolo} · ore ${partita.ora}. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.`;
|
||||
const { titolo, testo } = avvisoSondaggio(partita);
|
||||
|
||||
// Storico in-app ed email (DD-038): indipendente dai dispositivi iscritti alla push. Un
|
||||
// upsert perché ripremere il pulsante per la stessa partita deve riportare la notifica a
|
||||
// non letta, non fallire per il vincolo UNIQUE; la mail riparte dal trigger di M22.
|
||||
// `types.ts` non include ancora `notifiche_utente`, stesso aggiramento delle altre route.
|
||||
const client = supabaseAdmin as unknown as SupabaseClient;
|
||||
await client.from("notifiche_utente").upsert(
|
||||
destinatariSondaggio(squadra).map((giocatoreId) => ({
|
||||
giocatore_id: giocatoreId,
|
||||
tipo: "sondaggio_cacche",
|
||||
titolo,
|
||||
corpo: testo,
|
||||
evento_id: partita.id,
|
||||
letta: false,
|
||||
creato_il: new Date().toISOString(),
|
||||
})),
|
||||
{ onConflict: "giocatore_id,evento_id,tipo" },
|
||||
);
|
||||
|
||||
let inviate = 0;
|
||||
for (const iscrizione of iscrizioni) {
|
||||
|
||||
Reference in new issue
Block a user