From 19ddf715e698ac0b2580e8fe420ac7a906fcf010 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 26 Oct 2026 08:12:07 +0100 Subject: [PATCH] Link Google Maps del luogo degli eventi (m34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colonna facoltativa eventi_app.link_maps con CHECK che accetta solo URL https di Google Maps. Campo nel form di /eventi (admin e allenatori), validazione con linkMapsValido() e test. Le schede di partita, allenamento ed evento mostrano «Apri su Google Maps» solo se il link c'è; card e notifiche non cambiano. Documentazione: calendario.md, DATABASE.md, CHANGELOG. La migration m34 non è ancora applicata in produzione: va fatto db push prima di rilasciare questo codice, che legge la nuova colonna. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 9 ++++++ docs/DATABASE.md | 14 ++++----- docs/modules/calendario.md | 7 +++++ src/integrations/supabase/types.ts | 3 ++ src/lib/eventi.server.ts | 2 +- src/lib/eventi.ts | 19 +++++++++++- src/routes/allenamento.$id.tsx | 10 ++++++ src/routes/eventi.tsx | 25 ++++++++++++++- src/routes/evento.$id.tsx | 10 ++++++ src/routes/partita.$id.tsx | 10 ++++++ .../20261026120000_m34_link_maps_evento.sql | 16 ++++++++++ test/unit/eventi.test.ts | 31 +++++++++++++++++++ 12 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 supabase/migrations/20261026120000_m34_link_maps_evento.sql diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 24b2bab..7fcda85 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,15 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +Una migration, `m34`, **non ancora applicata in produzione**. + +### Aggiunto + +- Link Google Maps del luogo: in «Gestione eventi» (admin e allenatori) un campo facoltativo + accetta il link copiato da Google Maps; nelle schede di partita, allenamento ed evento compare + «Apri su Google Maps» solo se il link c'è. Non compare nella card né nelle notifiche. Il link + è validato (solo https di Google Maps) sia nell'app sia da un CHECK sul database. + ## [1.4.1] - 2026-10-19 Gli eventi con dei convocati si vedono solo a loro, e «Rosa e presenze» elenca solo i convocati. diff --git a/docs/DATABASE.md b/docs/DATABASE.md index fd41d24..78ed8a5 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -74,13 +74,13 @@ Colonne di `giocatori_squadra` che meritano una nota: ## Eventi e presenze -| Tabella | Scopo | Note | -| ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `eventi_app` | Eventi gestionali usati dall'app. | Modello in uso. Cancellare un evento pulisce a cascata, via trigger, tutte le tabelle collegate: `risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` (DD-029). `bonifica_dati_evento_orfani()` (solo service role) ripulisce eventuali righe orfane. | -| `risposte_presenze` | Risposte dei giocatori agli eventi. | `risposto_il` è l'istante della **prima** risposta e un trigger lo rende immutabile: oggi nessuna statistica lo usa (la serie «Conferme 24h» è stata rimossa il 2026-10-06). `aggiornato_il` è l'ultima modifica. | -| `cacche_partita` | Sondaggio prepartita. | Usato per statistiche e badge segreti. | -| `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello «nuovo» con autenticazione e vincoli, non adottato (DD-014). | -| `presenze` | Presenze agli eventi. | Come sopra (DD-014). | +| Tabella | Scopo | Note | +| ------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `eventi_app` | Eventi gestionali usati dall'app. | Modello in uso. `link_maps` (M34) è il link Google Maps facoltativo del luogo, con CHECK che accetta solo URL https di Google Maps. Cancellare un evento pulisce a cascata, via trigger, tutte le tabelle collegate: `risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` (DD-029). `bonifica_dati_evento_orfani()` (solo service role) ripulisce eventuali righe orfane. | +| `risposte_presenze` | Risposte dei giocatori agli eventi. | `risposto_il` è l'istante della **prima** risposta e un trigger lo rende immutabile: oggi nessuna statistica lo usa (la serie «Conferme 24h» è stata rimossa il 2026-10-06). `aggiornato_il` è l'ultima modifica. | +| `cacche_partita` | Sondaggio prepartita. | Usato per statistiche e badge segreti. | +| `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello «nuovo» con autenticazione e vincoli, non adottato (DD-014). | +| `presenze` | Presenze agli eventi. | Come sopra (DD-014). | ## Scout diff --git a/docs/modules/calendario.md b/docs/modules/calendario.md index 3cf77d9..68c749c 100644 --- a/docs/modules/calendario.md +++ b/docs/modules/calendario.md @@ -53,6 +53,13 @@ calendario cambia raramente). `EventoCard.tsx` è la card riusata da entrambe le extra-campo, nessun link per gli eventi virtuali (compleanni), che non hanno una riga in `eventi_app`. +**Link Google Maps.** `eventi_app.link_maps` (M34, facoltativo) si imposta dal form di `/eventi` +sotto «Luogo»: si incolla il link di «Condividi» di Google Maps. `linkMapsValido()` in `eventi.ts` +accetta solo URL https di Google Maps (`maps.app.goo.gl`, `goo.gl/maps`, `google.*/maps`, +`maps.google.*`) e lo stesso criterio è un CHECK sul database. Le tre schede di dettaglio mostrano +«Apri su Google Maps» (nuova scheda) solo se il link c'è; non c'è un link generato dal testo del +luogo, e né la card né le notifiche lo mostrano. + Le tre schede di dettaglio (`allenamento.$id.tsx`, `partita.$id.tsx`, `evento.$id.tsx`) condividono la stessa struttura — intestazione, note, conferme, `RosaPresenze` per l'elenco nominativo di chi ha risposto — con `TurnoPalloni` in più per allenamenti e partite (vedi diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 251a58c..4fec40f 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -145,6 +145,7 @@ export type Database = { data: string; id: string; luogo: string; + link_maps: string | null; note: string; ora: string; pagelle_chiuse: boolean; @@ -160,6 +161,7 @@ export type Database = { data: string; id: string; luogo?: string; + link_maps?: string | null; note?: string; ora?: string; pagelle_chiuse?: boolean; @@ -175,6 +177,7 @@ export type Database = { data?: string; id?: string; luogo?: string; + link_maps?: string | null; note?: string; ora?: string; pagelle_chiuse?: boolean; diff --git a/src/lib/eventi.server.ts b/src/lib/eventi.server.ts index 7012441..824ab14 100644 --- a/src/lib/eventi.server.ts +++ b/src/lib/eventi.server.ts @@ -1,7 +1,7 @@ import { daRiga, type Evento, type RigaEvento } from "./eventi"; const COLONNE = - "id, tipo, titolo, luogo, data, ora, note, convocati, campionato, casa, pagelle_chiuse"; + "id, tipo, titolo, luogo, link_maps, data, ora, note, convocati, campionato, casa, pagelle_chiuse"; /** Lettura eventi lato server (route API): stessa conversione del client. */ export async function leggiEventi(): Promise { diff --git a/src/lib/eventi.ts b/src/lib/eventi.ts index a0f1477..923824e 100644 --- a/src/lib/eventi.ts +++ b/src/lib/eventi.ts @@ -9,6 +9,8 @@ export type Evento = { tipo: EventoTipo; titolo: string; luogo: string; + /** Link Google Maps del luogo; assente o vuoto = nessun link. */ + linkMaps?: string | undefined; data: string; ora: string; note: string; @@ -30,6 +32,7 @@ export type RigaEvento = { tipo: string; titolo: string; luogo: string; + link_maps?: string | null; data: string; ora: string; note: string | null; @@ -47,6 +50,7 @@ export function daRiga(r: RigaEvento): Evento { tipo: (r.tipo as EventoTipo) ?? "evento", titolo: r.titolo, luogo: r.luogo ?? "", + linkMaps: r.link_maps ?? "", data: r.data, ora: r.ora ?? "", note: r.note ?? "", @@ -59,7 +63,7 @@ export function daRiga(r: RigaEvento): Evento { } const COLONNE = - "id, tipo, titolo, luogo, data, ora, note, convocati, campionato, casa, pagelle_chiuse, creato_il"; + "id, tipo, titolo, luogo, link_maps, data, ora, note, convocati, campionato, casa, pagelle_chiuse, creato_il"; /** Categoria mostrata in interfaccia: le amichevoli sono partite fuori campionato. */ export type CategoriaEvento = "allenamento" | "partita" | "amichevole" | "evento"; @@ -76,6 +80,17 @@ export function daCategoria(c: CategoriaEvento): Pick { @@ -124,6 +139,7 @@ export function eventoVuoto(): Evento { tipo: "allenamento", titolo: "", luogo: "", + linkMaps: "", data: new Date().toISOString().slice(0, 10), ora: "20:30", note: "", @@ -144,6 +160,7 @@ export function useSalvaEvento() { tipo: evento.tipo, titolo: evento.titolo, luogo: evento.luogo, + link_maps: evento.linkMaps?.trim() || null, data: evento.data, ora: evento.ora, note: evento.note, diff --git a/src/routes/allenamento.$id.tsx b/src/routes/allenamento.$id.tsx index f58adc6..43dfb2c 100644 --- a/src/routes/allenamento.$id.tsx +++ b/src/routes/allenamento.$id.tsx @@ -75,6 +75,16 @@ function AllenamentoDetail() { {evento.luogo} + {evento.linkMaps ? ( + + Apri su Google Maps + + ) : null} {evento.note ? ( diff --git a/src/routes/eventi.tsx b/src/routes/eventi.tsx index bc65a71..965221d 100644 --- a/src/routes/eventi.tsx +++ b/src/routes/eventi.tsx @@ -22,6 +22,7 @@ import { categoriaEvento, daCategoria, eventoVuoto, + linkMapsValido, useEliminaEvento, useSalvaEvento, type CategoriaEvento, @@ -111,6 +112,12 @@ function GestioneEventi() { toast.error("Serve un titolo per l'evento"); return; } + if ((bozza.linkMaps ?? "").trim() && !linkMapsValido(bozza.linkMaps!.trim())) { + toast.error( + "Il link deve essere di Google Maps (https://maps.app.goo.gl/… o google.com/maps…)", + ); + return; + } if (modificaEsistente) { setConfermaModifica(true); return; @@ -121,7 +128,11 @@ function GestioneEventi() { async function conferma() { if (!bozza) return; try { - await salva.mutateAsync({ ...bozza, titolo: bozza.titolo.trim() }); + await salva.mutateAsync({ + ...bozza, + titolo: bozza.titolo.trim(), + linkMaps: (bozza.linkMaps ?? "").trim(), + }); toast.success("Evento salvato"); setBozza(null); } catch { @@ -373,6 +384,18 @@ function GestioneEventi() { /> + + aggiorna({ linkMaps: e.target.value })} + className={classiInput} + /> + +