diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 90babd9..b3c1544 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,12 @@ qui: sta in [ROADMAP.md](ROADMAP.md). ## Versione attuale — agosto 2026 +### Palloni: allenamenti senza proposta automatica + +- `completaTurni` non assegna più gli allenamenti: restano «da assegnare» finché non si + sceglie a mano. Le partite tengono la rotazione. Migration M10 cancella eventuali turni + salvati su allenamenti da oggi in poi. + ### Profilo: etichetta notifiche allineata al comportamento - L’interruttore in Impostazioni non è più «Notifiche turno palloni»: iscrive il dispositivo diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 9fa40a5..92647cb 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -52,7 +52,7 @@ non in questo file. | Tabella | Scopo | Note | | -------------------- | --------------------------------------------- | ---- | -| `turni_palloni` | Gestione dei turni palloni. | | +| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)); M10 azzera i turni salvati su allenamenti da oggi in poi. | | `push_subscriptions` | Dispositivi registrati per le notifiche Push. | | | `promemoria_push` | Storico dei promemoria inviati. | | diff --git a/docs/modules/palloni.md b/docs/modules/palloni.md index 4b3673e..af674b0 100644 --- a/docs/modules/palloni.md +++ b/docs/modules/palloni.md @@ -25,14 +25,16 @@ compaiono. ## Implementazione -- `completaTurni()` (`palloni-core.ts`) propone, per ogni evento senza turno già salvato, il - candidato con meno turni fatti, poi quello che non lo fa da più tempo, poi per ordine - alfabetico — un algoritmo greedy, non un ordine fisso né solo per data. +- `completaTurni()` (`palloni-core.ts`) propone, per ogni **partita** o evento extra senza + turno già salvato, il candidato con meno turni fatti, poi quello che non lo fa da più + tempo, poi per ordine alfabetico — un algoritmo greedy, non un ordine fisso né solo per + data. Gli **allenamenti** non ricevono proposta automatica: restano «da assegnare» finché + qualcuno non sceglie un incaricato in `TurnoPalloni` (scelta della squadra). - `useAssegnaTurno()` (`palloni.ts`) conferma una proposta o riassegna manualmente, con upsert su `evento_id`. - Il conteggio "quante volte hai portato i palloni" mostrato nel profilo e nei badge è ricalcolato a runtime da `conteggioTurni()` su turni salvati **più proposte non ancora - confermate** — non è uno storico in tabella dedicata. + confermate** (partite/eventi) — non è uno storico in tabella dedicata. - `TurnoPalloni.tsx` mostra/assegna il turno sulla card di un evento; `PromemoriaPalloni.tsx` è il banner in Home per il giocatore di turno. diff --git a/src/lib/palloni-core.ts b/src/lib/palloni-core.ts index 1522d63..92c5ff8 100644 --- a/src/lib/palloni-core.ts +++ b/src/lib/palloni-core.ts @@ -17,6 +17,9 @@ export function eventiPalloni(eventi: Evento[]): Evento[] { /** * Completa i turni mancanti proponendo, a rotazione, chi ha portato i palloni * meno volte (a parità, chi non lo fa da più tempo). + * + * Gli **allenamenti** non vengono proposti: restano «da assegnare» finché qualcuno + * non conferma un incaricato a mano. Su partite ed eventi extra la rotazione resta. */ export function completaTurni( turni: Record, @@ -35,6 +38,8 @@ export function completaTurni( return; } if (assegnato) return; + // Allenamenti: niente proposta automatica — li assegna la squadra a mano. + if (evento.tipo === "allenamento") return; const scelto = rosa.slice().sort((a, b) => { const ca = conteggio.get(a.id) ?? 0; diff --git a/src/lib/palloni.ts b/src/lib/palloni.ts index 33a2627..9b49067 100644 --- a/src/lib/palloni.ts +++ b/src/lib/palloni.ts @@ -22,7 +22,7 @@ async function fetchTurni(): Promise> { return mappa; } -/** Turni salvati + proposta automatica a rotazione per gli eventi non ancora assegnati. */ +/** Turni salvati + proposta automatica (solo partite/eventi) per i mancanti. */ export function useTurniPalloni() { // Cambia raramente: una lettura per sessione è sufficiente. const query = useQuery({ queryKey: TURNI_KEY, queryFn: fetchTurni, staleTime: 30 * 60_000 }); diff --git a/supabase/migrations/20260905220000_m10_azzera_turni_palloni_allenamenti.sql b/supabase/migrations/20260905220000_m10_azzera_turni_palloni_allenamenti.sql new file mode 100644 index 0000000..0eb247f --- /dev/null +++ b/supabase/migrations/20260905220000_m10_azzera_turni_palloni_allenamenti.sql @@ -0,0 +1,9 @@ +-- M10: gli allenamenti non devono più avere turni palloni pre-assegnati. +-- La proposta automatica lato app non li riempie più; qui si azzerano eventuali +-- eventuali già salvati su allenamenti da oggi in poi, così restano «da assegnare». + +DELETE FROM public.turni_palloni AS t +USING public.eventi_app AS e +WHERE t.evento_id = e.id + AND e.tipo = 'allenamento' + AND e.data >= (CURRENT_DATE AT TIME ZONE 'Europe/Rome')::date; diff --git a/test/unit/palloni-core.test.ts b/test/unit/palloni-core.test.ts index d6217c8..4b44448 100644 --- a/test/unit/palloni-core.test.ts +++ b/test/unit/palloni-core.test.ts @@ -60,25 +60,26 @@ assert.deepEqual( "il compleanno dello stesso giorno non richiede palloni", ); -// --- completaTurni: assegna i mancanti, rispetta quelli già decisi ----------- +// --- completaTurni: niente proposta sugli allenamenti; partite sì ------------ const turni = completaTurni({ e2: "g5" }, eventi, rosa); assert.equal(turni["e2"], "g5", "il turno già assegnato non viene toccato"); -assert.equal(Object.keys(turni).length, 3, "tutti gli eventi hanno un incaricato"); -assert.ok( - giocatori.some((g) => g.id === turni["e1"]), - "assegna a un giocatore reale", -); -assert.notEqual(turni["e1"], turni["e3"], "non tocca due volte di fila alla stessa persona"); +assert.equal(turni["e1"], undefined, "allenamento senza salvataggio resta da assegnare"); +assert.equal(turni["e3"], undefined, "allenamento futuro resta da assegnare"); +assert.equal(Object.keys(turni).length, 1, "solo i turni salvati / partite proposte"); + +const conAllenamentoSalvato = completaTurni({ e1: "g3", e2: "g5" }, eventi, rosa); +assert.equal(conAllenamentoSalvato["e1"], "g3", "un allenamento salvato resta"); +assert.equal(conAllenamentoSalvato["e2"], "g5"); const molti = Array.from({ length: giocatori.length + 2 }, (_, i) => - evento(`x${i}`, `2026-10-${String(i + 1).padStart(2, "0")}`), + evento(`x${i}`, `2026-10-${String(i + 1).padStart(2, "0")}`, "partita"), ); const rotazione = conteggioTurni(completaTurni({}, molti, rosa)); const carichi = Object.values(rotazione); assert.equal( Math.max(...carichi) - Math.min(...carichi), 1, - "su un giro completo il carico resta bilanciato", + "su un giro completo di partite il carico resta bilanciato", ); assert.equal( Object.keys(rotazione).length, @@ -86,10 +87,20 @@ assert.equal( "nessuno viene saltato prima che tutti abbiano fatto un turno", ); +const soloAllenamenti = Array.from({ length: 5 }, (_, i) => + evento(`a${i}`, `2026-11-${String(i + 1).padStart(2, "0")}`), +); +assert.deepEqual( + completaTurni({}, soloAllenamenti, rosa), + {}, + "solo allenamenti: nessuna proposta automatica", +); + // Un turno salvato per un giocatore non più in rosa non deve rompere il conteggio. const conFantasma = completaTurni({ e1: "gXX" }, eventi, rosa); assert.equal(conFantasma["e1"], "gXX", "il turno storico resta com'è"); -assert.equal(Object.keys(conFantasma).length, 3); +assert.ok(conFantasma["e2"], "la partita riceve comunque una proposta"); +assert.equal(conFantasma["e3"], undefined, "l'allenamento senza salvataggio resta vuoto"); // --- conteggioTurni ---------------------------------------------------------- assert.deepEqual(conteggioTurni({ a: "g1", b: "g1", c: "g2" }), { g1: 2, g2: 1 });