Implementa le notifiche email via Gmail da un worker Docker (DD-036)
Migration m22: coda notifiche_email_coda, preferenze_utente, trigger che accoda ogni notifica di notifiche_utente e tre funzioni RPC riservate alla service role. Worker in mailer/ (bun + nodemailer, solo polling in uscita) con backoff, tetto giornaliero e healthcheck. Interruttore «Email» in Profilo → Opzioni, acceso di default. Test unit e di integrazione; documentazione aggiornata (DD-036 accettata). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a0b611459
commit
b7d3fde225
24 files changed
+2138
-186
No files matched your search
+39
-1
@@ -1,7 +1,7 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Flame, Trash2, Bell, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react";
|
||||
import { Flame, Trash2, Bell, Mail, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Card, PageHeader, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
|
||||
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "@/lib/giocatori-squadra";
|
||||
import { esci } from "@/lib/auth";
|
||||
import { useIsAdmin } from "@/lib/ruoli";
|
||||
import { useEmailNotifiche } from "@/lib/preferenze-utente";
|
||||
import { Reveal } from "@/components/motion/Reveal";
|
||||
import { version as APP_VERSION } from "../../package.json";
|
||||
|
||||
@@ -255,6 +256,7 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) {
|
||||
const [notifiche, setNotifiche] = useState(false);
|
||||
const [inCorso, setInCorso] = useState(false);
|
||||
const [supportate, setSupportate] = useState(true);
|
||||
const email = useEmailNotifiche();
|
||||
|
||||
useEffect(() => {
|
||||
setSupportate(pushSupportato());
|
||||
@@ -283,6 +285,17 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cambiaEmail() {
|
||||
if (email.imposta.isPending) return;
|
||||
const attive = !email.attive;
|
||||
try {
|
||||
await email.imposta.mutateAsync(attive);
|
||||
toast.success(attive ? "Email attive" : "Email disattivate");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "Impossibile salvare la preferenza");
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await esci();
|
||||
@@ -322,6 +335,31 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) {
|
||||
<Bell className="h-4 w-4" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={cambiaEmail}
|
||||
disabled={!email.pronta || email.imposta.isPending}
|
||||
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm disabled:opacity-60"
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate">Email</span>
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{email.attive
|
||||
? "Le notifiche arrivano anche via email"
|
||||
: "Nessuna email, solo notifiche in app"}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"grid h-8 w-8 shrink-0 place-items-center rounded-xl",
|
||||
email.attive
|
||||
? "bg-accent-grad text-accent-foreground"
|
||||
: "bg-secondary text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
</span>
|
||||
</button>
|
||||
{admin ? (
|
||||
<Link
|
||||
to="/admin"
|
||||
|
||||
Reference in new issue
Block a user