Toglie la proposta automatica dei palloni sugli allenamenti.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Cacciari
2026-09-05 22:44:25 +02:00
co-authored by Cursor
parent 033c45df4d
commit 61c6aaaaed
7 changed files with 49 additions and 16 deletions
+6
View File
@@ -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
- Linterruttore in Impostazioni non è più «Notifiche turno palloni»: iscrive il dispositivo
+1 -1
View File
@@ -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. | |
+6 -4
View File
@@ -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.
+5
View File
@@ -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<string, string>,
@@ -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;
+1 -1
View File
@@ -22,7 +22,7 @@ async function fetchTurni(): Promise<Record<string, string>> {
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 });
@@ -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;
+21 -10
View File
@@ -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 });