Aggiunge il centro notifiche in-app accanto al profilo
Icona a campana con badge delle non lette, indipendente dal canale push: copre messaggio libero admin, promemoria evento (24h/3h prima), turno palloni e sollecito presenze. Ogni notifica si rimuove con uno swipe o la ×, non c'è pulizia automatica delle vecchie (DD-030). Migration m17_notifiche_utente non ancora applicata in produzione. Co-Authored-By: Claude Mythos <noreply@anthropic.com>
This commit is contained in:
1 parent
0314560885
commit
4d036f532d
15 files changed
+1184
-11
No files matched your search
@@ -1,4 +1,5 @@
|
||||
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 { inviaPush } from "@/lib/webpush.server";
|
||||
@@ -19,6 +20,9 @@ export const Route = createFileRoute("/api/public/notifica-personalizzata")({
|
||||
if (!parsed.success) return new Response("Dati non validi", { status: 400 });
|
||||
|
||||
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||
// `types.ts` non include ancora `notifiche_utente` (M17): stesso aggiramento di
|
||||
// `giocatori-squadra.server.ts` finché non viene rigenerato.
|
||||
const client = supabaseAdmin as unknown as SupabaseClient;
|
||||
|
||||
let query = supabaseAdmin.from("push_subscriptions").select("endpoint, p256dh, auth");
|
||||
if (parsed.data.giocatoreId) {
|
||||
@@ -46,6 +50,29 @@ export const Route = createFileRoute("/api/public/notifica-personalizzata")({
|
||||
}
|
||||
}
|
||||
|
||||
// Storico in-app (M17): indipendente dall'esito della push, e non limitato a chi ha
|
||||
// un dispositivo iscritto. Senza `giocatoreId` va a tutta la rosa attiva.
|
||||
let destinatariNotifica: string[];
|
||||
if (parsed.data.giocatoreId) {
|
||||
destinatariNotifica = [parsed.data.giocatoreId];
|
||||
} else {
|
||||
const { data: rosa } = await client
|
||||
.from("giocatori_squadra")
|
||||
.select("id")
|
||||
.eq("attivo", true);
|
||||
destinatariNotifica = ((rosa ?? []) as Array<{ id: string }>).map((r) => r.id);
|
||||
}
|
||||
if (destinatariNotifica.length > 0) {
|
||||
await client.from("notifiche_utente").insert(
|
||||
destinatariNotifica.map((giocatoreId) => ({
|
||||
giocatore_id: giocatoreId,
|
||||
tipo: "admin",
|
||||
titolo,
|
||||
corpo: testo,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.json({ inviate, destinatari: iscrizioni?.length ?? 0 });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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 { nomeCompleto } from "@/lib/giocatori-squadra";
|
||||
@@ -44,6 +45,24 @@ export const Route = createFileRoute("/api/public/promemoria-palloni")({
|
||||
const avvisi = avvisiPalloniEvento(turni, eventi, evento.id);
|
||||
if (avvisi.length === 0) return Response.json({ inviate: 0, destinatari: 0 });
|
||||
|
||||
// Storico in-app (M17): un upsert perché ripremere il pulsante deve riportare la
|
||||
// notifica a non letta con il testo aggiornato, non fallire per il vincolo UNIQUE.
|
||||
// `types.ts` non include ancora `notifiche_utente`, stesso aggiramento di
|
||||
// `giocatori-squadra.server.ts`.
|
||||
const client = supabaseAdmin as unknown as SupabaseClient;
|
||||
await client.from("notifiche_utente").upsert(
|
||||
avvisi.map((a) => ({
|
||||
giocatore_id: a.giocatoreId,
|
||||
tipo: "turno_palloni",
|
||||
titolo: a.titolo,
|
||||
corpo: a.testo,
|
||||
evento_id: evento.id,
|
||||
letta: false,
|
||||
creato_il: new Date().toISOString(),
|
||||
})),
|
||||
{ onConflict: "giocatore_id,evento_id,tipo" },
|
||||
);
|
||||
|
||||
const { data: iscrizioni } = await supabaseAdmin
|
||||
.from("push_subscriptions")
|
||||
.select("endpoint, giocatore_id, p256dh, auth")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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 { formatData } from "@/lib/crapp-data";
|
||||
@@ -48,6 +49,23 @@ export const Route = createFileRoute("/api/public/sollecita-presenze")({
|
||||
parsed.data.da ? `${parsed.data.da} chiede` : "Serve"
|
||||
} una conferma: presente, assente o in ritardo?`;
|
||||
|
||||
// Storico in-app (M17): un upsert perché ripremere il pulsante deve riportare la
|
||||
// notifica a non letta, non fallire per il vincolo UNIQUE. `types.ts` non include
|
||||
// ancora `notifiche_utente`, stesso aggiramento di `giocatori-squadra.server.ts`.
|
||||
const client = supabaseAdmin as unknown as SupabaseClient;
|
||||
await client.from("notifiche_utente").upsert(
|
||||
destinatari.map((giocatoreId) => ({
|
||||
giocatore_id: giocatoreId,
|
||||
tipo: "sollecita_presenze",
|
||||
titolo,
|
||||
corpo: testo,
|
||||
evento_id: evento.id,
|
||||
letta: false,
|
||||
creato_il: new Date().toISOString(),
|
||||
})),
|
||||
{ onConflict: "giocatore_id,evento_id,tipo" },
|
||||
);
|
||||
|
||||
let inviate = 0;
|
||||
for (const iscrizione of iscrizioni ?? []) {
|
||||
try {
|
||||
|
||||
+12
-2
@@ -2,7 +2,14 @@ import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { Flame, ChevronRight } from "lucide-react";
|
||||
import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
|
||||
import { PromemoriaPalloni } from "@/components/crapp/PromemoriaPalloni";
|
||||
import { Card, LinkProfilo, Section, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
|
||||
import {
|
||||
Card,
|
||||
IconaNotifiche,
|
||||
LinkProfilo,
|
||||
Section,
|
||||
StatTile,
|
||||
TeamLogo,
|
||||
} from "@/components/crapp/ui-bits";
|
||||
import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo";
|
||||
import { Reveal } from "@/components/motion/Reveal";
|
||||
import { Barra } from "@/components/motion/Barra";
|
||||
@@ -83,7 +90,10 @@ function Index() {
|
||||
<h1 className="font-display-lg text-4xl uppercase leading-none">CRAP Volley</h1>
|
||||
</div>
|
||||
</div>
|
||||
<LinkProfilo />
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<IconaNotifiche />
|
||||
<LinkProfilo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-3 gap-2 text-center">
|
||||
|
||||
Reference in new issue
Block a user