Apre il sondaggio pre-partita alle 8:00 con avviso push manuale.
Il sondaggio cacche era votabile in qualsiasi momento, anche settimane prima della partita. Ora `sondaggioAperto()` lo sblocca alle 8:00 del giorno della partita e prima la card mostra solo l'avviso di apertura. Aggiunge la route `POST /api/public/apri-sondaggio` e, per gli amministratori, il pulsante «Avvisa tutti del sondaggio» nella card: manda la push a tutti i dispositivi iscritti, con lo stesso meccanismo del sollecito presenze. Nessun invio automatico. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,14 @@ qui: sta in [ROADMAP.md](ROADMAP.md).
|
|||||||
|
|
||||||
## Versione attuale — agosto 2026
|
## Versione attuale — agosto 2026
|
||||||
|
|
||||||
|
### Il sondaggio pre-partita apre alle 8:00 del giorno della partita
|
||||||
|
|
||||||
|
- Prima era sempre votabile, anche settimane prima: ora la card resta chiusa con l'avviso di
|
||||||
|
apertura e si sblocca alle 8:00 del giorno stesso.
|
||||||
|
- Quando è aperto, gli amministratori hanno nella card il pulsante «Avvisa tutti del
|
||||||
|
sondaggio» (`POST /api/public/apri-sondaggio`), che manda la push a tutti i dispositivi
|
||||||
|
iscritti — stesso meccanismo del sollecito presenze. Nessun cron: l'invio è manuale.
|
||||||
|
|
||||||
### Le note dell'evento si leggono aprendolo
|
### Le note dell'evento si leggono aprendolo
|
||||||
|
|
||||||
- Il campo Note del form eventi si poteva scrivere ma non lo vedeva nessuno: ora compare
|
- Il campo Note del form eventi si poteva scrivere ma non lo vedeva nessuno: ora compare
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ smart in app, che usano lo stesso service worker.
|
|||||||
|
|
||||||
- **`push-config`** — espone la sola chiave pubblica VAPID.
|
- **`push-config`** — espone la sola chiave pubblica VAPID.
|
||||||
- **`push-subscribe`** — registra o rimuove l'iscrizione di un dispositivo.
|
- **`push-subscribe`** — registra o rimuove l'iscrizione di un dispositivo.
|
||||||
|
- **`apri-sondaggio`** — premuto da un admin dalla pagina partita: mette in coda su
|
||||||
|
`promemoria_push` l'avviso di apertura del sondaggio pre-partita per **tutti** i dispositivi
|
||||||
|
iscritti e manda la push (vedi [Scout Live](scout-live.md)).
|
||||||
- **`push-messaggio`** — non invia nulla: il service worker la interroga **al momento della
|
- **`push-messaggio`** — non invia nulla: il service worker la interroga **al momento della
|
||||||
ricezione** di una push (che arriva sempre "vuota", senza testo, per compatibilità) per
|
ricezione** di una push (che arriva sempre "vuota", senza testo, per compatibilità) per
|
||||||
sapere quale messaggio mostrare. Priorità: un messaggio in coda su `promemoria_push`
|
sapere quale messaggio mostrare. Priorità: un messaggio in coda su `promemoria_push`
|
||||||
|
|||||||
@@ -82,8 +82,13 @@ sezione "Report tecnico".
|
|||||||
## Sondaggio cacche
|
## Sondaggio cacche
|
||||||
|
|
||||||
`SondaggioCacche.tsx` chiede "quante cacche hai fatto prima di questa partita" (0-5+), sempre
|
`SondaggioCacche.tsx` chiede "quante cacche hai fatto prima di questa partita" (0-5+), sempre
|
||||||
modificabile, senza gating temporale reale (visibile sia prima sia dopo la partita nonostante
|
modificabile. `sondaggioAperto()` (`cacche.ts`) lo apre alle **8:00 del giorno della partita**
|
||||||
il nome). `statisticheCacche()` (`cacche.ts`) calcola media, record e `giornateTop`
|
(ora locale del dispositivo) e da lì lo lascia aperto per sempre; prima la card mostra solo
|
||||||
|
l'avviso di apertura. Quando è aperto, gli **amministratori** vedono nella card il pulsante
|
||||||
|
«Avvisa tutti del sondaggio»: chiama `POST /api/public/apri-sondaggio` e manda la push a tutti
|
||||||
|
i dispositivi iscritti, come il sollecito presenze (vedi [Notifiche](notifiche.md)). Nessun
|
||||||
|
invio automatico: parte solo quando un admin lo preme.
|
||||||
|
`statisticheCacche()` (`cacche.ts`) calcola media, record e `giornateTop`
|
||||||
(giornate con ≥3), soglia usata per un [badge](badge.md) segreto — coerente con DD-007 (badge
|
(giornate con ≥3), soglia usata per un [badge](badge.md) segreto — coerente con DD-007 (badge
|
||||||
calcolati a runtime).
|
calcolati a runtime).
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { BellRing, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Card } from "@/components/crapp/ui-bits";
|
import { Card } from "@/components/crapp/ui-bits";
|
||||||
import { nomeCompleto, useGiocatoriSquadra } from "@/lib/giocatori-squadra";
|
import { nomeCompleto, useGiocatoriSquadra } from "@/lib/giocatori-squadra";
|
||||||
import { useGiocatoreCorrente } from "@/lib/user-store";
|
import { useGiocatoreCorrente } from "@/lib/user-store";
|
||||||
import { mediaPartita, useCacche, useSalvaCacche } from "@/lib/cacche";
|
import { useIsAdmin } from "@/lib/ruoli";
|
||||||
|
import { mediaPartita, sondaggioAperto, useCacche, useSalvaCacche } from "@/lib/cacche";
|
||||||
|
|
||||||
const opzioni = [0, 1, 2, 3, 4, 5];
|
const opzioni = [0, 1, 2, 3, 4, 5];
|
||||||
|
|
||||||
/** Sondaggio goliardico pre-partita: quante cacche prima del fischio d'inizio. */
|
/** Sondaggio goliardico pre-partita: quante cacche prima del fischio d'inizio. */
|
||||||
export function SondaggioCacche({ eventoId }: { eventoId: string }) {
|
export function SondaggioCacche({
|
||||||
|
eventoId,
|
||||||
|
dataEvento,
|
||||||
|
}: {
|
||||||
|
eventoId: string;
|
||||||
|
dataEvento: string;
|
||||||
|
}) {
|
||||||
const io = useGiocatoreCorrente();
|
const io = useGiocatoreCorrente();
|
||||||
const { righe } = useCacche();
|
const { righe } = useCacche();
|
||||||
const salva = useSalvaCacche();
|
const salva = useSalvaCacche();
|
||||||
const { righe: squadra } = useGiocatoriSquadra();
|
const { righe: squadra } = useGiocatoriSquadra();
|
||||||
|
const admin = useIsAdmin();
|
||||||
|
const [avviso, setAvviso] = useState(false);
|
||||||
|
|
||||||
const dellaPartita = righe.filter((r) => r.evento_id === eventoId);
|
const dellaPartita = righe.filter((r) => r.evento_id === eventoId);
|
||||||
const mia = dellaPartita.find((r) => r.giocatore_id === io?.id);
|
const mia = dellaPartita.find((r) => r.giocatore_id === io?.id);
|
||||||
@@ -35,6 +46,41 @@ export function SondaggioCacche({ eventoId }: { eventoId: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function avvisaTutti() {
|
||||||
|
setAvviso(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/public/apri-sondaggio", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ eventoId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
|
const dati = (await res.json()) as { inviate: number; destinatari: number };
|
||||||
|
toast.success(
|
||||||
|
dati.inviate > 0
|
||||||
|
? `Notifica inviata a ${dati.inviate} dispositivi`
|
||||||
|
: "Nessun dispositivo con le notifiche attive",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
toast.error("Non sono riuscito a inviare la notifica");
|
||||||
|
} finally {
|
||||||
|
setAvviso(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sondaggioAperto(dataEvento)) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
|
💩 Sondaggio pre-partita
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Apre alle 8:00 del giorno della partita: riceverai una notifica.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
@@ -81,6 +127,18 @@ export function SondaggioCacche({ eventoId }: { eventoId: string }) {
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{admin ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={avvisaTutti}
|
||||||
|
disabled={avviso}
|
||||||
|
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-primary px-4 py-3 text-sm font-bold text-primary-foreground disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{avviso ? <Loader2 className="h-4 w-4 animate-spin" /> : <BellRing className="h-4 w-4" />}
|
||||||
|
Avvisa tutti del sondaggio
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { supabase } from "@/integrations/supabase/client";
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
import { oggiISO } from "./palloni-core";
|
||||||
|
|
||||||
|
/** Ora di apertura del sondaggio, il giorno stesso della partita. */
|
||||||
|
export const ORA_APERTURA_SONDAGGIO = 8;
|
||||||
|
|
||||||
|
/** Il sondaggio apre alle 8:00 del giorno della partita e da lì resta aperto. */
|
||||||
|
export function sondaggioAperto(dataEvento: string, adesso = new Date()): boolean {
|
||||||
|
const oggi = oggiISO(adesso);
|
||||||
|
if (dataEvento !== oggi) return dataEvento < oggi;
|
||||||
|
return adesso.getHours() >= ORA_APERTURA_SONDAGGIO;
|
||||||
|
}
|
||||||
|
|
||||||
/** Sondaggio goliardico pre-partita: quante cacche prima del match di campionato. */
|
/** Sondaggio goliardico pre-partita: quante cacche prima del match di campionato. */
|
||||||
export type RigaCacche = {
|
export type RigaCacche = {
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ export function eventoSuccessivo(eventi: Evento[], eventoId: string): Evento | u
|
|||||||
return i >= 0 ? lista[i + 1] : undefined;
|
return i >= 0 ? lista[i + 1] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function oggiISO(): string {
|
export function oggiISO(d = new Date()): string {
|
||||||
const d = new Date();
|
|
||||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
const dd = String(d.getDate()).padStart(2, "0");
|
const dd = String(d.getDate()).padStart(2, "0");
|
||||||
return `${d.getFullYear()}-${mm}-${dd}`;
|
return `${d.getFullYear()}-${mm}-${dd}`;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { Route as ScoutRouteImport } from './routes/scout'
|
|||||||
import { Route as SquadraRouteImport } from './routes/squadra'
|
import { Route as SquadraRouteImport } from './routes/squadra'
|
||||||
import { Route as AllenamentoIdRouteImport } from './routes/allenamento.$id'
|
import { Route as AllenamentoIdRouteImport } from './routes/allenamento.$id'
|
||||||
import { Route as PartitaIdRouteImport } from './routes/partita.$id'
|
import { Route as PartitaIdRouteImport } from './routes/partita.$id'
|
||||||
|
import { Route as ApiPublicApriSondaggioRouteImport } from './routes/api/public/apri-sondaggio'
|
||||||
import { Route as ApiPublicCsiRouteImport } from './routes/api/public/csi'
|
import { Route as ApiPublicCsiRouteImport } from './routes/api/public/csi'
|
||||||
import { Route as ApiPublicPromemoriaPalloniRouteImport } from './routes/api/public/promemoria-palloni'
|
import { Route as ApiPublicPromemoriaPalloniRouteImport } from './routes/api/public/promemoria-palloni'
|
||||||
import { Route as ApiPublicPushConfigRouteImport } from './routes/api/public/push-config'
|
import { Route as ApiPublicPushConfigRouteImport } from './routes/api/public/push-config'
|
||||||
@@ -82,6 +83,11 @@ const PartitaIdRoute = PartitaIdRouteImport.update({
|
|||||||
path: '/partita/$id',
|
path: '/partita/$id',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiPublicApriSondaggioRoute = ApiPublicApriSondaggioRouteImport.update({
|
||||||
|
id: '/api/public/apri-sondaggio',
|
||||||
|
path: '/api/public/apri-sondaggio',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ApiPublicCsiRoute = ApiPublicCsiRouteImport.update({
|
const ApiPublicCsiRoute = ApiPublicCsiRouteImport.update({
|
||||||
id: '/api/public/csi',
|
id: '/api/public/csi',
|
||||||
path: '/api/public/csi',
|
path: '/api/public/csi',
|
||||||
@@ -127,6 +133,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/squadra': typeof SquadraRoute
|
'/squadra': typeof SquadraRoute
|
||||||
'/allenamento/$id': typeof AllenamentoIdRoute
|
'/allenamento/$id': typeof AllenamentoIdRoute
|
||||||
'/partita/$id': typeof PartitaIdRoute
|
'/partita/$id': typeof PartitaIdRoute
|
||||||
|
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
||||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||||
@@ -146,6 +153,7 @@ export interface FileRoutesByTo {
|
|||||||
'/squadra': typeof SquadraRoute
|
'/squadra': typeof SquadraRoute
|
||||||
'/allenamento/$id': typeof AllenamentoIdRoute
|
'/allenamento/$id': typeof AllenamentoIdRoute
|
||||||
'/partita/$id': typeof PartitaIdRoute
|
'/partita/$id': typeof PartitaIdRoute
|
||||||
|
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
||||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||||
@@ -166,6 +174,7 @@ export interface FileRoutesById {
|
|||||||
'/squadra': typeof SquadraRoute
|
'/squadra': typeof SquadraRoute
|
||||||
'/allenamento/$id': typeof AllenamentoIdRoute
|
'/allenamento/$id': typeof AllenamentoIdRoute
|
||||||
'/partita/$id': typeof PartitaIdRoute
|
'/partita/$id': typeof PartitaIdRoute
|
||||||
|
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
||||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||||
@@ -187,6 +196,7 @@ export interface FileRouteTypes {
|
|||||||
| '/squadra'
|
| '/squadra'
|
||||||
| '/allenamento/$id'
|
| '/allenamento/$id'
|
||||||
| '/partita/$id'
|
| '/partita/$id'
|
||||||
|
| '/api/public/apri-sondaggio'
|
||||||
| '/api/public/csi'
|
| '/api/public/csi'
|
||||||
| '/api/public/promemoria-palloni'
|
| '/api/public/promemoria-palloni'
|
||||||
| '/api/public/push-config'
|
| '/api/public/push-config'
|
||||||
@@ -206,6 +216,7 @@ export interface FileRouteTypes {
|
|||||||
| '/squadra'
|
| '/squadra'
|
||||||
| '/allenamento/$id'
|
| '/allenamento/$id'
|
||||||
| '/partita/$id'
|
| '/partita/$id'
|
||||||
|
| '/api/public/apri-sondaggio'
|
||||||
| '/api/public/csi'
|
| '/api/public/csi'
|
||||||
| '/api/public/promemoria-palloni'
|
| '/api/public/promemoria-palloni'
|
||||||
| '/api/public/push-config'
|
| '/api/public/push-config'
|
||||||
@@ -225,6 +236,7 @@ export interface FileRouteTypes {
|
|||||||
| '/squadra'
|
| '/squadra'
|
||||||
| '/allenamento/$id'
|
| '/allenamento/$id'
|
||||||
| '/partita/$id'
|
| '/partita/$id'
|
||||||
|
| '/api/public/apri-sondaggio'
|
||||||
| '/api/public/csi'
|
| '/api/public/csi'
|
||||||
| '/api/public/promemoria-palloni'
|
| '/api/public/promemoria-palloni'
|
||||||
| '/api/public/push-config'
|
| '/api/public/push-config'
|
||||||
@@ -245,6 +257,7 @@ export interface RootRouteChildren {
|
|||||||
SquadraRoute: typeof SquadraRoute
|
SquadraRoute: typeof SquadraRoute
|
||||||
AllenamentoIdRoute: typeof AllenamentoIdRoute
|
AllenamentoIdRoute: typeof AllenamentoIdRoute
|
||||||
PartitaIdRoute: typeof PartitaIdRoute
|
PartitaIdRoute: typeof PartitaIdRoute
|
||||||
|
ApiPublicApriSondaggioRoute: typeof ApiPublicApriSondaggioRoute
|
||||||
ApiPublicCsiRoute: typeof ApiPublicCsiRoute
|
ApiPublicCsiRoute: typeof ApiPublicCsiRoute
|
||||||
ApiPublicPromemoriaPalloniRoute: typeof ApiPublicPromemoriaPalloniRoute
|
ApiPublicPromemoriaPalloniRoute: typeof ApiPublicPromemoriaPalloniRoute
|
||||||
ApiPublicPushConfigRoute: typeof ApiPublicPushConfigRoute
|
ApiPublicPushConfigRoute: typeof ApiPublicPushConfigRoute
|
||||||
@@ -332,6 +345,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PartitaIdRouteImport
|
preLoaderRoute: typeof PartitaIdRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/api/public/apri-sondaggio': {
|
||||||
|
id: '/api/public/apri-sondaggio'
|
||||||
|
path: '/api/public/apri-sondaggio'
|
||||||
|
fullPath: '/api/public/apri-sondaggio'
|
||||||
|
preLoaderRoute: typeof ApiPublicApriSondaggioRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/api/public/csi': {
|
'/api/public/csi': {
|
||||||
id: '/api/public/csi'
|
id: '/api/public/csi'
|
||||||
path: '/api/public/csi'
|
path: '/api/public/csi'
|
||||||
@@ -389,6 +409,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
SquadraRoute: SquadraRoute,
|
SquadraRoute: SquadraRoute,
|
||||||
AllenamentoIdRoute: AllenamentoIdRoute,
|
AllenamentoIdRoute: AllenamentoIdRoute,
|
||||||
PartitaIdRoute: PartitaIdRoute,
|
PartitaIdRoute: PartitaIdRoute,
|
||||||
|
ApiPublicApriSondaggioRoute: ApiPublicApriSondaggioRoute,
|
||||||
ApiPublicCsiRoute: ApiPublicCsiRoute,
|
ApiPublicCsiRoute: ApiPublicCsiRoute,
|
||||||
ApiPublicPromemoriaPalloniRoute: ApiPublicPromemoriaPalloniRoute,
|
ApiPublicPromemoriaPalloniRoute: ApiPublicPromemoriaPalloniRoute,
|
||||||
ApiPublicPushConfigRoute: ApiPublicPushConfigRoute,
|
ApiPublicPushConfigRoute: ApiPublicPushConfigRoute,
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { leggiEventi } from "@/lib/eventi.server";
|
||||||
|
import { inviaPush } from "@/lib/webpush.server";
|
||||||
|
|
||||||
|
const schema = z.object({ eventoId: z.string().min(1).max(50) });
|
||||||
|
|
||||||
|
/** Avviso "sondaggio pre-partita aperto": lo fa partire un admin dalla pagina partita. */
|
||||||
|
export const Route = createFileRoute("/api/public/apri-sondaggio")({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
POST: async ({ request }) => {
|
||||||
|
const parsed = schema.safeParse(await request.json());
|
||||||
|
if (!parsed.success) return new Response("Dati non validi", { status: 400 });
|
||||||
|
|
||||||
|
const eventi = await leggiEventi();
|
||||||
|
const partita = eventi.find((e) => e.id === parsed.data.eventoId);
|
||||||
|
if (!partita) return new Response("Evento non trovato", { status: 404 });
|
||||||
|
|
||||||
|
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||||
|
const { data: iscrizioni } = await supabaseAdmin
|
||||||
|
.from("push_subscriptions")
|
||||||
|
.select("endpoint");
|
||||||
|
|
||||||
|
const titolo = "💩 Sondaggio pre-partita aperto";
|
||||||
|
const testo = `${partita.titolo} · ore ${partita.ora}. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.`;
|
||||||
|
|
||||||
|
let inviate = 0;
|
||||||
|
for (const iscrizione of iscrizioni ?? []) {
|
||||||
|
try {
|
||||||
|
await supabaseAdmin
|
||||||
|
.from("promemoria_push")
|
||||||
|
.insert({ endpoint: iscrizione.endpoint, titolo, testo });
|
||||||
|
const stato = await inviaPush(iscrizione.endpoint);
|
||||||
|
if (stato === 404 || stato === 410) {
|
||||||
|
await supabaseAdmin
|
||||||
|
.from("push_subscriptions")
|
||||||
|
.delete()
|
||||||
|
.eq("endpoint", iscrizione.endpoint);
|
||||||
|
} else if (stato >= 200 && stato < 300) {
|
||||||
|
inviate += 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("apri-sondaggio", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ inviate, destinatari: (iscrizioni ?? []).length });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -205,7 +205,7 @@ function PartitaDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Section titolo="Sondaggio pre-partita">
|
<Section titolo="Sondaggio pre-partita">
|
||||||
<SondaggioCacche eventoId={evento.id} />
|
<SondaggioCacche eventoId={evento.id} dataEvento={evento.data} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{match ? (
|
{match ? (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
mediaPartita,
|
mediaPartita,
|
||||||
mediaStagione,
|
mediaStagione,
|
||||||
recordStagione,
|
recordStagione,
|
||||||
|
sondaggioAperto,
|
||||||
statisticheCacche,
|
statisticheCacche,
|
||||||
type RigaCacche,
|
type RigaCacche,
|
||||||
} from "@/lib/cacche";
|
} from "@/lib/cacche";
|
||||||
@@ -52,4 +53,12 @@ assert.deepEqual(
|
|||||||
"a parità vince chi l'ha fatto per primo",
|
"a parità vince chi l'ha fatto per primo",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- sondaggioAperto ---------------------------------------------------------
|
||||||
|
const alle = (h: number, m = 0) => new Date(2026, 8, 5, h, m); // 5 settembre 2026
|
||||||
|
assert.equal(sondaggioAperto("2026-09-05", alle(7, 59)), false, "prima delle 8 è chiuso");
|
||||||
|
assert.equal(sondaggioAperto("2026-09-05", alle(8)), true, "alle 8 in punto apre");
|
||||||
|
assert.equal(sondaggioAperto("2026-09-05", alle(23)), true);
|
||||||
|
assert.equal(sondaggioAperto("2026-09-06", alle(23)), false, "partita di domani: chiuso");
|
||||||
|
assert.equal(sondaggioAperto("2026-09-04", alle(0, 1)), true, "partita passata: resta aperto");
|
||||||
|
|
||||||
console.log("cacche: ok");
|
console.log("cacche: ok");
|
||||||
|
|||||||
Reference in New Issue
Block a user