Initial independent version of CrAPP
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { completaTurni, eventiPalloni, eventoPrecedente, oggiISO } from "@/lib/palloni-core";
|
||||
import { inviaPush } from "@/lib/webpush.server";
|
||||
import { leggiEventi } from "@/lib/eventi.server";
|
||||
|
||||
export const Route = createFileRoute("/api/public/promemoria-palloni")({
|
||||
server: {
|
||||
handlers: {
|
||||
POST: async () => {
|
||||
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||
|
||||
const { data: righe } = await supabaseAdmin
|
||||
.from("turni_palloni")
|
||||
.select("evento_id, giocatore_id");
|
||||
const salvati: Record<string, string> = {};
|
||||
for (const riga of righe ?? []) salvati[riga.evento_id] = riga.giocatore_id;
|
||||
const eventi = await leggiEventi();
|
||||
const turni = completaTurni(salvati, eventi);
|
||||
|
||||
const oggi = oggiISO();
|
||||
const destinatari = new Set<string>();
|
||||
for (const evento of eventiPalloni(eventi)) {
|
||||
if (evento.data !== oggi) continue;
|
||||
const incaricato = turni[evento.id];
|
||||
if (incaricato) destinatari.add(incaricato);
|
||||
const prima = eventoPrecedente(eventi, evento.id);
|
||||
const precedente = prima ? turni[prima.id] : undefined;
|
||||
if (precedente) destinatari.add(precedente);
|
||||
}
|
||||
|
||||
if (destinatari.size === 0) return Response.json({ inviate: 0 });
|
||||
|
||||
const { data: iscrizioni } = await supabaseAdmin
|
||||
.from("push_subscriptions")
|
||||
.select("endpoint, giocatore_id")
|
||||
.in("giocatore_id", [...destinatari]);
|
||||
|
||||
let inviate = 0;
|
||||
for (const iscrizione of iscrizioni ?? []) {
|
||||
try {
|
||||
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("promemoria-palloni", error);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ inviate });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/api/public/push-config")({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async () => {
|
||||
const publicKey = process.env["VAPID_PUBLIC_KEY"] ?? null;
|
||||
return Response.json({ publicKey });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
import { formatData, giocatori } from "@/lib/crapp-data";
|
||||
import {
|
||||
completaTurni,
|
||||
eventiPalloni,
|
||||
eventoPrecedente,
|
||||
eventoSuccessivo,
|
||||
oggiISO,
|
||||
} from "@/lib/palloni-core";
|
||||
import { leggiEventi } from "@/lib/eventi.server";
|
||||
|
||||
const schema = z.object({ endpoint: z.string().url().max(1000) });
|
||||
|
||||
export const Route = createFileRoute("/api/public/push-messaggio")({
|
||||
server: {
|
||||
handlers: {
|
||||
POST: async ({ request }) => {
|
||||
const parsed = schema.safeParse(await request.json());
|
||||
if (!parsed.success) return new Response("Dati non validi", { status: 400 });
|
||||
|
||||
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||
|
||||
// Messaggio in coda (es. sollecito presenze): ha la precedenza e viene consumato.
|
||||
const { data: promemoria } = await supabaseAdmin
|
||||
.from("promemoria_push")
|
||||
.select("id, titolo, testo")
|
||||
.eq("endpoint", parsed.data.endpoint)
|
||||
.order("creato_il", { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (promemoria) {
|
||||
await supabaseAdmin.from("promemoria_push").delete().eq("endpoint", parsed.data.endpoint);
|
||||
return Response.json({ title: promemoria.titolo, body: promemoria.testo });
|
||||
}
|
||||
|
||||
const { data: iscrizione } = await supabaseAdmin
|
||||
.from("push_subscriptions")
|
||||
.select("giocatore_id")
|
||||
.eq("endpoint", parsed.data.endpoint)
|
||||
.maybeSingle();
|
||||
|
||||
if (!iscrizione) return Response.json({ title: "CrAPP", body: "Controlla il turno palloni." });
|
||||
|
||||
const { data: righe } = await supabaseAdmin
|
||||
.from("turni_palloni")
|
||||
.select("evento_id, giocatore_id");
|
||||
const salvati: Record<string, string> = {};
|
||||
for (const riga of righe ?? []) salvati[riga.evento_id] = riga.giocatore_id;
|
||||
const eventi = await leggiEventi();
|
||||
const turni = completaTurni(salvati, eventi);
|
||||
|
||||
const oggi = oggiISO();
|
||||
const mioId = iscrizione.giocatore_id;
|
||||
const nome = giocatori.find((g) => g.id === mioId)?.nome ?? "";
|
||||
|
||||
for (const evento of eventiPalloni(eventi)) {
|
||||
if (evento.data !== oggi) continue;
|
||||
const prima = eventoPrecedente(eventi, evento.id);
|
||||
if (prima && turni[prima.id] === mioId) {
|
||||
return Response.json({
|
||||
title: "Porta i palloni oggi",
|
||||
body: `${evento.titolo} · ${evento.ora}. I palloni li hai tu dalla volta scorsa.`,
|
||||
});
|
||||
}
|
||||
if (turni[evento.id] === mioId) {
|
||||
const dopo = eventoSuccessivo(eventi, evento.id);
|
||||
return Response.json({
|
||||
title: "Tocca a te prendere i palloni",
|
||||
body: dopo
|
||||
? `A fine ${evento.titolo} porta a casa i palloni e riportali il ${formatData(dopo.data)}.`
|
||||
: `A fine ${evento.titolo} porta a casa i palloni.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
title: "CrAPP · Turno palloni",
|
||||
body: nome ? `${nome}, controlla il turno palloni nel calendario.` : "Controlla il calendario.",
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemaIscrizione = z.object({
|
||||
endpoint: z.string().url().max(1000),
|
||||
giocatoreId: z.string().min(1).max(50),
|
||||
p256dh: z.string().min(1).max(500),
|
||||
auth: z.string().min(1).max(500),
|
||||
});
|
||||
|
||||
const schemaCancellazione = z.object({ endpoint: z.string().url().max(1000) });
|
||||
|
||||
export const Route = createFileRoute("/api/public/push-subscribe")({
|
||||
server: {
|
||||
handlers: {
|
||||
POST: async ({ request }) => {
|
||||
const parsed = schemaIscrizione.safeParse(await request.json());
|
||||
if (!parsed.success) return new Response("Dati non validi", { status: 400 });
|
||||
|
||||
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||
const { error } = await supabaseAdmin.from("push_subscriptions").upsert(
|
||||
{
|
||||
endpoint: parsed.data.endpoint,
|
||||
giocatore_id: parsed.data.giocatoreId,
|
||||
p256dh: parsed.data.p256dh,
|
||||
auth: parsed.data.auth,
|
||||
},
|
||||
{ onConflict: "endpoint" },
|
||||
);
|
||||
if (error) {
|
||||
console.error("push-subscribe", error);
|
||||
return new Response("Errore salvataggio", { status: 500 });
|
||||
}
|
||||
return Response.json({ ok: true });
|
||||
},
|
||||
DELETE: async ({ request }) => {
|
||||
const parsed = schemaCancellazione.safeParse(await request.json());
|
||||
if (!parsed.success) return new Response("Dati non validi", { status: 400 });
|
||||
|
||||
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||
await supabaseAdmin.from("push_subscriptions").delete().eq("endpoint", parsed.data.endpoint);
|
||||
return Response.json({ ok: true });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
import { formatData, giocatori } from "@/lib/crapp-data";
|
||||
import { leggiEventi } from "@/lib/eventi.server";
|
||||
import { inviaPush } from "@/lib/webpush.server";
|
||||
|
||||
const schema = z.object({
|
||||
eventoId: z.string().min(1).max(50),
|
||||
da: z.string().min(1).max(60).optional(),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/api/public/sollecita-presenze")({
|
||||
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 evento = eventi.find((e) => e.id === parsed.data.eventoId);
|
||||
if (!evento) return new Response("Evento non trovato", { status: 404 });
|
||||
|
||||
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||
|
||||
const { data: righe } = await supabaseAdmin
|
||||
.from("risposte_presenze")
|
||||
.select("giocatore_id, stato")
|
||||
.eq("evento_id", evento.id);
|
||||
|
||||
const stati = new Map((righe ?? []).map((r) => [r.giocatore_id, r.stato]));
|
||||
const destinatari = giocatori
|
||||
.filter((g) => {
|
||||
const stato = stati.get(g.id);
|
||||
return stato === undefined || stato === "forse";
|
||||
})
|
||||
.map((g) => g.id);
|
||||
|
||||
if (destinatari.length === 0) return Response.json({ inviate: 0, destinatari: 0 });
|
||||
|
||||
const { data: iscrizioni } = await supabaseAdmin
|
||||
.from("push_subscriptions")
|
||||
.select("endpoint, giocatore_id")
|
||||
.in("giocatore_id", destinatari);
|
||||
|
||||
const titolo = "Manca la tua risposta";
|
||||
const testo = `${evento.titolo} · ${formatData(evento.data)} ore ${evento.ora}. ${
|
||||
parsed.data.da ? `${parsed.data.da} chiede` : "Serve"
|
||||
} una conferma: presente, assente o in ritardo?`;
|
||||
|
||||
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("sollecita-presenze", error);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ inviate, destinatari: destinatari.length });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user