diff --git a/src/lib/notifiche-admin.ts b/src/lib/notifiche-admin.ts new file mode 100644 index 0000000..6fd4954 --- /dev/null +++ b/src/lib/notifiche-admin.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; +import { intestazioniAutenticate } from "./auth"; + +const NOTIFICHE_ATTIVE_KEY = ["notifiche-attive"] as const; + +async function fetchNotificheAttive(): Promise> { + const res = await fetch("/api/public/notifiche-attive", { + headers: await intestazioniAutenticate(), + }); + if (!res.ok) throw new Error("Impossibile leggere le notifiche attive"); + const { giocatoreIds } = (await res.json()) as { giocatoreIds: string[] }; + return new Set(giocatoreIds); +} + +/** Insieme degli id giocatore con almeno un dispositivo iscritto alle notifiche push. */ +export function useNotificheAttive() { + return useQuery({ + queryKey: NOTIFICHE_ATTIVE_KEY, + queryFn: fetchNotificheAttive, + staleTime: 5 * 60_000, + }); +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 99c8484..b99d831 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -23,6 +23,7 @@ import { Route as PartitaCsiIdRouteImport } from './routes/partita-csi.$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 ApiPublicNotificheAttiveRouteImport } from './routes/api/public/notifiche-attive' import { Route as ApiPublicPromemoriaPalloniRouteImport } from './routes/api/public/promemoria-palloni' import { Route as ApiPublicPushConfigRouteImport } from './routes/api/public/push-config' import { Route as ApiPublicPushSubscribeRouteImport } from './routes/api/public/push-subscribe' @@ -99,6 +100,12 @@ const ApiPublicCsiRoute = ApiPublicCsiRouteImport.update({ path: '/api/public/csi', getParentRoute: () => rootRouteImport, } as any) +const ApiPublicNotificheAttiveRoute = + ApiPublicNotificheAttiveRouteImport.update({ + id: '/api/public/notifiche-attive', + path: '/api/public/notifiche-attive', + getParentRoute: () => rootRouteImport, + } as any) const ApiPublicPromemoriaPalloniRoute = ApiPublicPromemoriaPalloniRouteImport.update({ id: '/api/public/promemoria-palloni', @@ -142,6 +149,7 @@ export interface FileRoutesByFullPath { '/partita/$id': typeof PartitaIdRoute '/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute '/api/public/csi': typeof ApiPublicCsiRoute + '/api/public/notifiche-attive': typeof ApiPublicNotificheAttiveRoute '/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute '/api/public/push-config': typeof ApiPublicPushConfigRoute '/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute @@ -163,6 +171,7 @@ export interface FileRoutesByTo { '/partita/$id': typeof PartitaIdRoute '/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute '/api/public/csi': typeof ApiPublicCsiRoute + '/api/public/notifiche-attive': typeof ApiPublicNotificheAttiveRoute '/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute '/api/public/push-config': typeof ApiPublicPushConfigRoute '/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute @@ -185,6 +194,7 @@ export interface FileRoutesById { '/partita/$id': typeof PartitaIdRoute '/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute '/api/public/csi': typeof ApiPublicCsiRoute + '/api/public/notifiche-attive': typeof ApiPublicNotificheAttiveRoute '/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute '/api/public/push-config': typeof ApiPublicPushConfigRoute '/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute @@ -208,6 +218,7 @@ export interface FileRouteTypes { | '/partita/$id' | '/api/public/apri-sondaggio' | '/api/public/csi' + | '/api/public/notifiche-attive' | '/api/public/promemoria-palloni' | '/api/public/push-config' | '/api/public/push-subscribe' @@ -229,6 +240,7 @@ export interface FileRouteTypes { | '/partita/$id' | '/api/public/apri-sondaggio' | '/api/public/csi' + | '/api/public/notifiche-attive' | '/api/public/promemoria-palloni' | '/api/public/push-config' | '/api/public/push-subscribe' @@ -250,6 +262,7 @@ export interface FileRouteTypes { | '/partita/$id' | '/api/public/apri-sondaggio' | '/api/public/csi' + | '/api/public/notifiche-attive' | '/api/public/promemoria-palloni' | '/api/public/push-config' | '/api/public/push-subscribe' @@ -272,6 +285,7 @@ export interface RootRouteChildren { PartitaIdRoute: typeof PartitaIdRoute ApiPublicApriSondaggioRoute: typeof ApiPublicApriSondaggioRoute ApiPublicCsiRoute: typeof ApiPublicCsiRoute + ApiPublicNotificheAttiveRoute: typeof ApiPublicNotificheAttiveRoute ApiPublicPromemoriaPalloniRoute: typeof ApiPublicPromemoriaPalloniRoute ApiPublicPushConfigRoute: typeof ApiPublicPushConfigRoute ApiPublicPushSubscribeRoute: typeof ApiPublicPushSubscribeRoute @@ -379,6 +393,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiPublicCsiRouteImport parentRoute: typeof rootRouteImport } + '/api/public/notifiche-attive': { + id: '/api/public/notifiche-attive' + path: '/api/public/notifiche-attive' + fullPath: '/api/public/notifiche-attive' + preLoaderRoute: typeof ApiPublicNotificheAttiveRouteImport + parentRoute: typeof rootRouteImport + } '/api/public/promemoria-palloni': { id: '/api/public/promemoria-palloni' path: '/api/public/promemoria-palloni' @@ -432,6 +453,7 @@ const rootRouteChildren: RootRouteChildren = { PartitaIdRoute: PartitaIdRoute, ApiPublicApriSondaggioRoute: ApiPublicApriSondaggioRoute, ApiPublicCsiRoute: ApiPublicCsiRoute, + ApiPublicNotificheAttiveRoute: ApiPublicNotificheAttiveRoute, ApiPublicPromemoriaPalloniRoute: ApiPublicPromemoriaPalloniRoute, ApiPublicPushConfigRoute: ApiPublicPushConfigRoute, ApiPublicPushSubscribeRoute: ApiPublicPushSubscribeRoute, diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx index c0b7785..8cf9468 100644 --- a/src/routes/admin.tsx +++ b/src/routes/admin.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { BadgeCheck, + Bell, ChevronDown, Download, FileText, @@ -16,14 +17,8 @@ import { } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; -import { - Campo, - classiInput, - PageHeader, - Section, - Select, - StatTile, -} from "@/components/crapp/ui-bits"; +import { Campo, classiInput, PageHeader, Select, StatTile } from "@/components/crapp/ui-bits"; +import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni"; import { CampiProfilo } from "@/components/crapp/ProfiloAmministrativo"; import { nomeCompleto, @@ -54,6 +49,7 @@ import { import { oggiISO } from "@/lib/palloni-core"; import { scaricaCsv } from "@/lib/scout-export"; import { useIsAdmin } from "@/lib/ruoli"; +import { useNotificheAttive } from "@/lib/notifiche-admin"; import { Reveal } from "@/components/motion/Reveal"; export const Route = createFileRoute("/admin")({ @@ -623,6 +619,7 @@ function Dashboard() { const admin = useIsAdmin(); const { righe: squadra } = useGiocatoriSquadra(); const { profili, isPending } = useProfili(); + const { data: notificheAttive } = useNotificheAttive(); const oggi = oggiISO(); if (!admin) { @@ -649,56 +646,89 @@ function Dashboard() { ).length; const tesserati = attivi.filter((g) => g.numeroTessera).length; + const contenutoSquadra = ( + <> +
+ + + + +
+ + + + ); + + const contenutoProfili = isPending ? ( +

+ Caricamento… +

+ ) : ( +
+ {attivi.map((g, i) => ( + + ))} +
+ ); + + const contenutoDisattivati = ( +
+ {disattivi.map((g) => ( + + ))} +
+ ); + + const contenutoNotifiche = notificheAttive ? ( + <> + notificheAttive.has(g.id)).length}/${attivi.length}`} + label="Notifiche attive" + /> +
+ {attivi + .filter((g) => notificheAttive.has(g.id)) + .map((g) => ( +
+ +

{nomeCompleto(g)}

+
+ ))} +
+ + ) : ( +

+ Caricamento… +

+ ); + return ( <> -
-
- - - - -
- - -
- -
- {isPending ? ( -

- Caricamento… -

- ) : ( -
- {attivi.map((g, i) => ( - - ))} -
- )} -
- - {disattivi.length > 0 ? ( -
-
- {disattivi.map((g) => ( - - ))} -
-
- ) : null} + 0 + ? [{ id: "disattivati", label: "Disattivati", contenuto: contenutoDisattivati }] + : []), + { id: "notifiche", label: "Notifiche", contenuto: contenutoNotifiche }, + ]} + /> ); } diff --git a/src/routes/api/public/notifiche-attive.ts b/src/routes/api/public/notifiche-attive.ts new file mode 100644 index 0000000..c97d683 --- /dev/null +++ b/src/routes/api/public/notifiche-attive.ts @@ -0,0 +1,25 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { richiediAdmin } from "@/lib/auth-route.server"; + +export const Route = createFileRoute("/api/public/notifiche-attive")({ + server: { + handlers: { + GET: async ({ request }) => { + const negato = await richiediAdmin(request); + if (negato) return negato; + + const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); + const { data, error } = await supabaseAdmin + .from("push_subscriptions") + .select("giocatore_id"); + if (error) { + console.error("notifiche-attive", error); + return new Response("Errore lettura", { status: 500 }); + } + + const giocatoreIds = [...new Set((data ?? []).map((r) => r.giocatore_id))]; + return Response.json({ giocatoreIds }); + }, + }, + }, +});