Aggiunge la sezione Notifiche in admin e converte la dashboard a tab
Nuovo endpoint admin-only /api/public/notifiche-attive per contare e elencare i giocatori con almeno un dispositivo iscritto alle notifiche push. La dashboard admin passa da sezioni impilate a un menu a pillole scorrevole (BarraSottosezioni), come già fatto in Classifica e Squadra. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Set<string>> {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import { Route as PartitaCsiIdRouteImport } from './routes/partita-csi.$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 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 ApiPublicNotificheAttiveRouteImport } from './routes/api/public/notifiche-attive'
|
||||||
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'
|
||||||
import { Route as ApiPublicPushSubscribeRouteImport } from './routes/api/public/push-subscribe'
|
import { Route as ApiPublicPushSubscribeRouteImport } from './routes/api/public/push-subscribe'
|
||||||
@@ -99,6 +100,12 @@ const ApiPublicCsiRoute = ApiPublicCsiRouteImport.update({
|
|||||||
path: '/api/public/csi',
|
path: '/api/public/csi',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiPublicNotificheAttiveRoute =
|
||||||
|
ApiPublicNotificheAttiveRouteImport.update({
|
||||||
|
id: '/api/public/notifiche-attive',
|
||||||
|
path: '/api/public/notifiche-attive',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ApiPublicPromemoriaPalloniRoute =
|
const ApiPublicPromemoriaPalloniRoute =
|
||||||
ApiPublicPromemoriaPalloniRouteImport.update({
|
ApiPublicPromemoriaPalloniRouteImport.update({
|
||||||
id: '/api/public/promemoria-palloni',
|
id: '/api/public/promemoria-palloni',
|
||||||
@@ -142,6 +149,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/partita/$id': typeof PartitaIdRoute
|
'/partita/$id': typeof PartitaIdRoute
|
||||||
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
||||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||||
|
'/api/public/notifiche-attive': typeof ApiPublicNotificheAttiveRoute
|
||||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||||
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
|
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
|
||||||
@@ -163,6 +171,7 @@ export interface FileRoutesByTo {
|
|||||||
'/partita/$id': typeof PartitaIdRoute
|
'/partita/$id': typeof PartitaIdRoute
|
||||||
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
||||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||||
|
'/api/public/notifiche-attive': typeof ApiPublicNotificheAttiveRoute
|
||||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||||
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
|
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
|
||||||
@@ -185,6 +194,7 @@ export interface FileRoutesById {
|
|||||||
'/partita/$id': typeof PartitaIdRoute
|
'/partita/$id': typeof PartitaIdRoute
|
||||||
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
'/api/public/apri-sondaggio': typeof ApiPublicApriSondaggioRoute
|
||||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||||
|
'/api/public/notifiche-attive': typeof ApiPublicNotificheAttiveRoute
|
||||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||||
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
|
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
|
||||||
@@ -208,6 +218,7 @@ export interface FileRouteTypes {
|
|||||||
| '/partita/$id'
|
| '/partita/$id'
|
||||||
| '/api/public/apri-sondaggio'
|
| '/api/public/apri-sondaggio'
|
||||||
| '/api/public/csi'
|
| '/api/public/csi'
|
||||||
|
| '/api/public/notifiche-attive'
|
||||||
| '/api/public/promemoria-palloni'
|
| '/api/public/promemoria-palloni'
|
||||||
| '/api/public/push-config'
|
| '/api/public/push-config'
|
||||||
| '/api/public/push-subscribe'
|
| '/api/public/push-subscribe'
|
||||||
@@ -229,6 +240,7 @@ export interface FileRouteTypes {
|
|||||||
| '/partita/$id'
|
| '/partita/$id'
|
||||||
| '/api/public/apri-sondaggio'
|
| '/api/public/apri-sondaggio'
|
||||||
| '/api/public/csi'
|
| '/api/public/csi'
|
||||||
|
| '/api/public/notifiche-attive'
|
||||||
| '/api/public/promemoria-palloni'
|
| '/api/public/promemoria-palloni'
|
||||||
| '/api/public/push-config'
|
| '/api/public/push-config'
|
||||||
| '/api/public/push-subscribe'
|
| '/api/public/push-subscribe'
|
||||||
@@ -250,6 +262,7 @@ export interface FileRouteTypes {
|
|||||||
| '/partita/$id'
|
| '/partita/$id'
|
||||||
| '/api/public/apri-sondaggio'
|
| '/api/public/apri-sondaggio'
|
||||||
| '/api/public/csi'
|
| '/api/public/csi'
|
||||||
|
| '/api/public/notifiche-attive'
|
||||||
| '/api/public/promemoria-palloni'
|
| '/api/public/promemoria-palloni'
|
||||||
| '/api/public/push-config'
|
| '/api/public/push-config'
|
||||||
| '/api/public/push-subscribe'
|
| '/api/public/push-subscribe'
|
||||||
@@ -272,6 +285,7 @@ export interface RootRouteChildren {
|
|||||||
PartitaIdRoute: typeof PartitaIdRoute
|
PartitaIdRoute: typeof PartitaIdRoute
|
||||||
ApiPublicApriSondaggioRoute: typeof ApiPublicApriSondaggioRoute
|
ApiPublicApriSondaggioRoute: typeof ApiPublicApriSondaggioRoute
|
||||||
ApiPublicCsiRoute: typeof ApiPublicCsiRoute
|
ApiPublicCsiRoute: typeof ApiPublicCsiRoute
|
||||||
|
ApiPublicNotificheAttiveRoute: typeof ApiPublicNotificheAttiveRoute
|
||||||
ApiPublicPromemoriaPalloniRoute: typeof ApiPublicPromemoriaPalloniRoute
|
ApiPublicPromemoriaPalloniRoute: typeof ApiPublicPromemoriaPalloniRoute
|
||||||
ApiPublicPushConfigRoute: typeof ApiPublicPushConfigRoute
|
ApiPublicPushConfigRoute: typeof ApiPublicPushConfigRoute
|
||||||
ApiPublicPushSubscribeRoute: typeof ApiPublicPushSubscribeRoute
|
ApiPublicPushSubscribeRoute: typeof ApiPublicPushSubscribeRoute
|
||||||
@@ -379,6 +393,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ApiPublicCsiRouteImport
|
preLoaderRoute: typeof ApiPublicCsiRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
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': {
|
'/api/public/promemoria-palloni': {
|
||||||
id: '/api/public/promemoria-palloni'
|
id: '/api/public/promemoria-palloni'
|
||||||
path: '/api/public/promemoria-palloni'
|
path: '/api/public/promemoria-palloni'
|
||||||
@@ -432,6 +453,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
PartitaIdRoute: PartitaIdRoute,
|
PartitaIdRoute: PartitaIdRoute,
|
||||||
ApiPublicApriSondaggioRoute: ApiPublicApriSondaggioRoute,
|
ApiPublicApriSondaggioRoute: ApiPublicApriSondaggioRoute,
|
||||||
ApiPublicCsiRoute: ApiPublicCsiRoute,
|
ApiPublicCsiRoute: ApiPublicCsiRoute,
|
||||||
|
ApiPublicNotificheAttiveRoute: ApiPublicNotificheAttiveRoute,
|
||||||
ApiPublicPromemoriaPalloniRoute: ApiPublicPromemoriaPalloniRoute,
|
ApiPublicPromemoriaPalloniRoute: ApiPublicPromemoriaPalloniRoute,
|
||||||
ApiPublicPushConfigRoute: ApiPublicPushConfigRoute,
|
ApiPublicPushConfigRoute: ApiPublicPushConfigRoute,
|
||||||
ApiPublicPushSubscribeRoute: ApiPublicPushSubscribeRoute,
|
ApiPublicPushSubscribeRoute: ApiPublicPushSubscribeRoute,
|
||||||
|
|||||||
+84
-54
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import {
|
import {
|
||||||
BadgeCheck,
|
BadgeCheck,
|
||||||
|
Bell,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Download,
|
Download,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -16,14 +17,8 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import { Campo, classiInput, PageHeader, Select, StatTile } from "@/components/crapp/ui-bits";
|
||||||
Campo,
|
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
||||||
classiInput,
|
|
||||||
PageHeader,
|
|
||||||
Section,
|
|
||||||
Select,
|
|
||||||
StatTile,
|
|
||||||
} from "@/components/crapp/ui-bits";
|
|
||||||
import { CampiProfilo } from "@/components/crapp/ProfiloAmministrativo";
|
import { CampiProfilo } from "@/components/crapp/ProfiloAmministrativo";
|
||||||
import {
|
import {
|
||||||
nomeCompleto,
|
nomeCompleto,
|
||||||
@@ -54,6 +49,7 @@ import {
|
|||||||
import { oggiISO } from "@/lib/palloni-core";
|
import { oggiISO } from "@/lib/palloni-core";
|
||||||
import { scaricaCsv } from "@/lib/scout-export";
|
import { scaricaCsv } from "@/lib/scout-export";
|
||||||
import { useIsAdmin } from "@/lib/ruoli";
|
import { useIsAdmin } from "@/lib/ruoli";
|
||||||
|
import { useNotificheAttive } from "@/lib/notifiche-admin";
|
||||||
import { Reveal } from "@/components/motion/Reveal";
|
import { Reveal } from "@/components/motion/Reveal";
|
||||||
|
|
||||||
export const Route = createFileRoute("/admin")({
|
export const Route = createFileRoute("/admin")({
|
||||||
@@ -623,6 +619,7 @@ function Dashboard() {
|
|||||||
const admin = useIsAdmin();
|
const admin = useIsAdmin();
|
||||||
const { righe: squadra } = useGiocatoriSquadra();
|
const { righe: squadra } = useGiocatoriSquadra();
|
||||||
const { profili, isPending } = useProfili();
|
const { profili, isPending } = useProfili();
|
||||||
|
const { data: notificheAttive } = useNotificheAttive();
|
||||||
const oggi = oggiISO();
|
const oggi = oggiISO();
|
||||||
|
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
@@ -649,56 +646,89 @@ function Dashboard() {
|
|||||||
).length;
|
).length;
|
||||||
const tesserati = attivi.filter((g) => g.numeroTessera).length;
|
const tesserati = attivi.filter((g) => g.numeroTessera).length;
|
||||||
|
|
||||||
|
const contenutoSquadra = (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<StatTile valore={attivi.length} label="Giocatori" />
|
||||||
|
<StatTile valore={`${completi}/${attivi.length}`} label="Profili completi" />
|
||||||
|
<StatTile
|
||||||
|
valore={`${certificatiOk}/${attivi.length}`}
|
||||||
|
label="Certificati validi"
|
||||||
|
hint="non scaduti"
|
||||||
|
/>
|
||||||
|
<StatTile valore={`${tesserati}/${attivi.length}`} label="Tesserati" hint="CSI" />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => scaricaCsv(`tesseramento-csi-${oggi}.csv`, csvTesseramento(attivi, profili))}
|
||||||
|
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" /> Esporta CSV tesseramento
|
||||||
|
</button>
|
||||||
|
<AggiungiGiocatore righe={squadra} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const contenutoProfili = isPending ? (
|
||||||
|
<p className="rounded-2xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
||||||
|
Caricamento…
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{attivi.map((g, i) => (
|
||||||
|
<SchedaGiocatore key={g.id} g={g} profilo={profili[g.id]} oggi={oggi} indice={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const contenutoDisattivati = (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{disattivi.map((g) => (
|
||||||
|
<GiocatoreDisattivato key={g.id} g={g} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const contenutoNotifiche = notificheAttive ? (
|
||||||
|
<>
|
||||||
|
<StatTile
|
||||||
|
valore={`${attivi.filter((g) => notificheAttive.has(g.id)).length}/${attivi.length}`}
|
||||||
|
label="Notifiche attive"
|
||||||
|
/>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{attivi
|
||||||
|
.filter((g) => notificheAttive.has(g.id))
|
||||||
|
.map((g) => (
|
||||||
|
<div key={g.id} className="flex items-center gap-2 rounded-2xl bg-card p-3 shadow-card">
|
||||||
|
<Bell className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
<p className="truncate text-sm font-semibold leading-tight">{nomeCompleto(g)}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="rounded-2xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
||||||
|
Caricamento…
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader titolo="Dashboard" sottotitolo="Profili e tesseramento" />
|
<PageHeader titolo="Dashboard" sottotitolo="Profili e tesseramento" />
|
||||||
|
|
||||||
<Section titolo="Squadra">
|
<BarraSottosezioni
|
||||||
<div className="grid grid-cols-2 gap-2">
|
defaultId="squadra"
|
||||||
<StatTile valore={attivi.length} label="Giocatori" />
|
variante="sottolineatura"
|
||||||
<StatTile valore={`${completi}/${attivi.length}`} label="Profili completi" />
|
riempiLarghezza
|
||||||
<StatTile
|
voci={[
|
||||||
valore={`${certificatiOk}/${attivi.length}`}
|
{ id: "squadra", label: "Squadra", contenuto: contenutoSquadra },
|
||||||
label="Certificati validi"
|
{ id: "profili", label: "Profili", contenuto: contenutoProfili },
|
||||||
hint="non scaduti"
|
...(disattivi.length > 0
|
||||||
/>
|
? [{ id: "disattivati", label: "Disattivati", contenuto: contenutoDisattivati }]
|
||||||
<StatTile valore={`${tesserati}/${attivi.length}`} label="Tesserati" hint="CSI" />
|
: []),
|
||||||
</div>
|
{ id: "notifiche", label: "Notifiche", contenuto: contenutoNotifiche },
|
||||||
<button
|
]}
|
||||||
type="button"
|
/>
|
||||||
onClick={() =>
|
|
||||||
scaricaCsv(`tesseramento-csi-${oggi}.csv`, csvTesseramento(attivi, profili))
|
|
||||||
}
|
|
||||||
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4" /> Esporta CSV tesseramento
|
|
||||||
</button>
|
|
||||||
<AggiungiGiocatore righe={squadra} />
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section titolo="Profili" indice={1}>
|
|
||||||
{isPending ? (
|
|
||||||
<p className="rounded-2xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
|
||||||
Caricamento…
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{attivi.map((g, i) => (
|
|
||||||
<SchedaGiocatore key={g.id} g={g} profilo={profili[g.id]} oggi={oggi} indice={i} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{disattivi.length > 0 ? (
|
|
||||||
<Section titolo="Giocatori disattivati" indice={2}>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{disattivi.map((g) => (
|
|
||||||
<GiocatoreDisattivato key={g.id} g={g} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user