diff --git a/src/components/crapp/BarraSottosezioni.tsx b/src/components/crapp/BarraSottosezioni.tsx
index a919737..1984376 100644
--- a/src/components/crapp/BarraSottosezioni.tsx
+++ b/src/components/crapp/BarraSottosezioni.tsx
@@ -90,9 +90,7 @@ export function BarraSottosezioni({
aria-label="Sottosezioni"
className={cn(
"flex flex-nowrap",
- riempiLarghezza
- ? "w-full"
- : "w-max min-w-full",
+ riempiLarghezza ? "w-full" : "w-max min-w-full",
sottolineatura ? "gap-1 px-2 py-1.5" : "snap-x snap-mandatory gap-1.5 px-5",
)}
>
diff --git a/src/components/crapp/EventoCard.tsx b/src/components/crapp/EventoCard.tsx
index b63c3aa..993dadf 100644
--- a/src/components/crapp/EventoCard.tsx
+++ b/src/components/crapp/EventoCard.tsx
@@ -88,8 +88,7 @@ export function EventoCard({
const passato = evento.data < dataOggi();
const cliccabile = Boolean(linkTo);
/** Solo gli eventi extra-campo non hanno scheda dedicata: le note restano sulla card. */
- const noteCard =
- evento.tipo === "evento" ? evento.note.trim() : "";
+ const noteCard = evento.tipo === "evento" ? evento.note.trim() : "";
const stati =
evento.tipo === "evento"
? // Se resta un vecchio "infortunato", mostra il bottone solo per poterlo togliere.
@@ -193,10 +192,7 @@ export function EventoCard({
{io ? (
diff --git a/src/routes/calendario.tsx b/src/routes/calendario.tsx
index 4d508a3..fe65169 100644
--- a/src/routes/calendario.tsx
+++ b/src/routes/calendario.tsx
@@ -182,7 +182,7 @@ function Calendario() {
{g}
))}
- {/*
+ {/*
Il mese si cambia anche con lo swipe: il punto d'arrivo si
decide proiettando la velocità di rilascio (come la
decelerazione dello scroll iOS), non dalla posizione del dito.
@@ -195,112 +195,112 @@ function Calendario() {
riga e sulle colonne di bordo. Il padding sta dentro il riquadro
di ritaglio, i margini negativi rimettono la griglia dov'era.
*/}
-
-
- {
- const arrivo = info.offset.x + proietta(info.velocity.x);
- if (arrivo < -60) successivo();
- else if (arrivo > 60) precedente();
- }}
- initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * 48 }}
- animate={{ opacity: 1, x: 0 }}
- exit={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * -48 }}
- transition={ridotto ? { duration: 0.2 } : molla.foglio}
- className="grid touch-pan-y grid-cols-7 gap-1"
- >
- {Array.from({ length: offsetLunedi }).map((_, i) => (
-
- ))}
- {Array.from({ length: giorni }).map((_, i) => {
- const giorno = i + 1;
- const eventiGiorno = eventiPerGiorno.get(giorno) ?? [];
- const haEventi = eventiGiorno.length > 0;
- // Attenzione: si conta per **tipo**, non per numero di
- // eventi. Due partite nello stesso giorno restano una cella
- // rossa piena; si divide solo se i tipi sono diversi.
- const tipiGiorno = Array.from(new Set(eventiGiorno.map((e) => e.tipo)));
- // Più tipi: la cella si divide in bande a taglio netto (gli
- // stop sono duplicati apposta, non è una sfumatura), una per
- // tipo, in diagonale.
- const sfondo =
- tipiGiorno.length > 1
- ? `linear-gradient(135deg, ${tipiGiorno
- .map((t, idx) => {
- const da = (idx / tipiGiorno.length) * 100;
- const a = ((idx + 1) / tipiGiorno.length) * 100;
- return `${coloreTipo[t]} ${da}%, ${coloreTipo[t]} ${a}%`;
- })
- .join(", ")})`
- : undefined;
- const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
- const isOggi =
- !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
- const Cella = haEventi ? "button" : "div";
- return (
- apriGiorno(giorno) : undefined}
- style={sfondo ? { backgroundImage: sfondo } : undefined}
- className={cn(
- "relative grid aspect-square place-items-center rounded-xl text-sm font-semibold",
- tipo === "partita" && "bg-accent text-accent-foreground",
- tipo === "allenamento" && "bg-training text-training-foreground",
- tipo === "evento" && "bg-warning text-warning-foreground",
- tipo === "compleanno" && "bg-success text-success-foreground",
- !tipo && !haEventi && "text-muted-foreground",
- !tipo && haEventi && "text-foreground",
- haEventi && "cursor-pointer transition-transform active:scale-90",
- isOggi && "ring-2 ring-foreground ring-offset-1 ring-offset-card",
- )}
- aria-label={
- haEventi
- ? `${giorno} ${mesiIT[mese]}: ${eventiGiorno.length} ${eventiGiorno.length === 1 ? "evento" : "eventi"}`
- : undefined
- }
- aria-current={isOggi ? "date" : undefined}
- >
- {/*
+
+
+ {
+ const arrivo = info.offset.x + proietta(info.velocity.x);
+ if (arrivo < -60) successivo();
+ else if (arrivo > 60) precedente();
+ }}
+ initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * 48 }}
+ animate={{ opacity: 1, x: 0 }}
+ exit={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * -48 }}
+ transition={ridotto ? { duration: 0.2 } : molla.foglio}
+ className="grid touch-pan-y grid-cols-7 gap-1"
+ >
+ {Array.from({ length: offsetLunedi }).map((_, i) => (
+
+ ))}
+ {Array.from({ length: giorni }).map((_, i) => {
+ const giorno = i + 1;
+ const eventiGiorno = eventiPerGiorno.get(giorno) ?? [];
+ const haEventi = eventiGiorno.length > 0;
+ // Attenzione: si conta per **tipo**, non per numero di
+ // eventi. Due partite nello stesso giorno restano una cella
+ // rossa piena; si divide solo se i tipi sono diversi.
+ const tipiGiorno = Array.from(new Set(eventiGiorno.map((e) => e.tipo)));
+ // Più tipi: la cella si divide in bande a taglio netto (gli
+ // stop sono duplicati apposta, non è una sfumatura), una per
+ // tipo, in diagonale.
+ const sfondo =
+ tipiGiorno.length > 1
+ ? `linear-gradient(135deg, ${tipiGiorno
+ .map((t, idx) => {
+ const da = (idx / tipiGiorno.length) * 100;
+ const a = ((idx + 1) / tipiGiorno.length) * 100;
+ return `${coloreTipo[t]} ${da}%, ${coloreTipo[t]} ${a}%`;
+ })
+ .join(", ")})`
+ : undefined;
+ const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
+ const isOggi =
+ !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
+ const Cella = haEventi ? "button" : "div";
+ return (
+ apriGiorno(giorno) : undefined}
+ style={sfondo ? { backgroundImage: sfondo } : undefined}
+ className={cn(
+ "relative grid aspect-square place-items-center rounded-xl text-sm font-semibold",
+ tipo === "partita" && "bg-accent text-accent-foreground",
+ tipo === "allenamento" && "bg-training text-training-foreground",
+ tipo === "evento" && "bg-warning text-warning-foreground",
+ tipo === "compleanno" && "bg-success text-success-foreground",
+ !tipo && !haEventi && "text-muted-foreground",
+ !tipo && haEventi && "text-foreground",
+ haEventi && "cursor-pointer transition-transform active:scale-90",
+ isOggi && "ring-2 ring-foreground ring-offset-1 ring-offset-card",
+ )}
+ aria-label={
+ haEventi
+ ? `${giorno} ${mesiIT[mese]}: ${eventiGiorno.length} ${eventiGiorno.length === 1 ? "evento" : "eventi"}`
+ : undefined
+ }
+ aria-current={isOggi ? "date" : undefined}
+ >
+ {/*
Sulle celle divise il numero sta direttamente sulle
bande, staccato dal fondo da un alone bianco: è la
resa scelta, il colore deve restare pieno e visibile
fino al bordo.
*/}
-
- {giorno}
-
-
- );
- })}
-
-
-
-
- Scorri a destra o sinistra per cambiare mese.
-
-
-
- Partita
-
-
- Allenamento
-
-
- Eventi
-
-
- Compleanni
-
-
-
-
+
+ {giorno}
+
+
+ );
+ })}
+
+
+
+
+ Scorri a destra o sinistra per cambiare mese.
+
+
+
+ Partita
+
+
+ Allenamento
+
+
+ Eventi
+
+
+ Compleanni
+
+
+
+
{admin ? (
diff --git a/test/integration/obiettivi.test.ts b/test/integration/obiettivi.test.ts
index 5902b9c..232780c 100644
--- a/test/integration/obiettivi.test.ts
+++ b/test/integration/obiettivi.test.ts
@@ -96,7 +96,10 @@ if (!locale) {
giocatore_id: g.id,
stato: presenti.includes(g.id) ? "presente" : "assente",
}));
- const inseritePresenze = await rest("risposte_presenze", { method: "POST", body: JSON.stringify(righe) });
+ const inseritePresenze = await rest("risposte_presenze", {
+ method: "POST",
+ body: JSON.stringify(righe),
+ });
if (!inseritePresenze.ok) {
throw new Error(`inserimento presenze fallito: ${await inseritePresenze.text()}`);
}
@@ -195,7 +198,11 @@ if (!locale) {
// Tutta la rosa presente all'allenamento, nessuno alla partita: 50% aggregato sui due.
const risposte = [
- ...giocatori.map((g) => ({ evento_id: allenamentoId, giocatore_id: g.id, stato: "presente" })),
+ ...giocatori.map((g) => ({
+ evento_id: allenamentoId,
+ giocatore_id: g.id,
+ stato: "presente",
+ })),
...giocatori.map((g) => ({ evento_id: partitaId, giocatore_id: g.id, stato: "assente" })),
];
const scritte = await rest("risposte_presenze", {
@@ -379,65 +386,68 @@ if (!locale) {
},
);
- await prova(
- "o7 somma presenze calcolate da eventi/risposte reali del database",
- async () => {
- // o7 non calcola nulla da `ctx`: somma `g.presenze`, un campo già calcolato a monte da
- // `contaPresenzeGiocatore()` (che in produzione alimenta `useRosa()`). Qui si esercita
- // la stessa funzione pura sui dati appena scritti, per verificare l'intera catena
- // DB -> contaPresenzeGiocatore -> o7, non solo la somma finale.
- const allenamentoId = `${PREFISSO}-o7-allenamento`;
- const partitaId = `${PREFISSO}-o7-partita`;
- const OGGI_STR = "2099-01-20";
+ await prova("o7 somma presenze calcolate da eventi/risposte reali del database", async () => {
+ // o7 non calcola nulla da `ctx`: somma `g.presenze`, un campo già calcolato a monte da
+ // `contaPresenzeGiocatore()` (che in produzione alimenta `useRosa()`). Qui si esercita
+ // la stessa funzione pura sui dati appena scritti, per verificare l'intera catena
+ // DB -> contaPresenzeGiocatore -> o7, non solo la somma finale.
+ const allenamentoId = `${PREFISSO}-o7-allenamento`;
+ const partitaId = `${PREFISSO}-o7-partita`;
+ const OGGI_STR = "2099-01-20";
- for (const [id, tipo, data] of [
- [allenamentoId, "allenamento", "2099-01-05"],
- [partitaId, "partita", "2099-01-08"],
- ] as const) {
- const inserito = await rest("eventi_app", {
- method: "POST",
- body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o7 (${tipo})`, data }),
- });
- if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
- }
+ for (const [id, tipo, data] of [
+ [allenamentoId, "allenamento", "2099-01-05"],
+ [partitaId, "partita", "2099-01-08"],
+ ] as const) {
+ const inserito = await rest("eventi_app", {
+ method: "POST",
+ body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o7 (${tipo})`, data }),
+ });
+ if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
+ }
- // g1: presente ai due eventi (2 presenze). g2: presente e in ritardo (2 presenze,
- // il ritardo conta). g3: assente a entrambi (0 presenze).
- const [g1, g2, g3] = giocatori;
- const righe = [
- { evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
- { evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
- { evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" },
- { evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" },
- { evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" },
- { evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" },
- ];
- const inserite = await rest("risposte_presenze", { method: "POST", body: JSON.stringify(righe) });
- if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
+ // g1: presente ai due eventi (2 presenze). g2: presente e in ritardo (2 presenze,
+ // il ritardo conta). g3: assente a entrambi (0 presenze).
+ const [g1, g2, g3] = giocatori;
+ const righe = [
+ { evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
+ { evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
+ { evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" },
+ { evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" },
+ { evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" },
+ { evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" },
+ ];
+ const inserite = await rest("risposte_presenze", {
+ method: "POST",
+ body: JSON.stringify(righe),
+ });
+ if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
- const eventiReali = (await leggiEventi()).filter(
- (e) => e.id === allenamentoId || e.id === partitaId,
- );
- const presenzeReali = {
- ...(await leggiPresenze(allenamentoId)),
- ...(await leggiPresenze(partitaId)),
- };
+ const eventiReali = (await leggiEventi()).filter(
+ (e) => e.id === allenamentoId || e.id === partitaId,
+ );
+ const presenzeReali = {
+ ...(await leggiPresenze(allenamentoId)),
+ ...(await leggiPresenze(partitaId)),
+ };
- const rosaConPresenzeReali = [g1!, g2!, g3!].map((g) => ({
- ...g,
- presenze: contaPresenzeGiocatore(g.id, eventiReali, presenzeReali, OGGI_STR),
- }));
+ const rosaConPresenzeReali = [g1!, g2!, g3!].map((g) => ({
+ ...g,
+ presenze: contaPresenzeGiocatore(g.id, eventiReali, presenzeReali, OGGI_STR),
+ }));
- const o7 = obiettiviSquadra(rosaConPresenzeReali, { eventi: [], presenze: {}, pagelle: [] })
- .find((o) => o.id === "o7")!;
+ const o7 = obiettiviSquadra(rosaConPresenzeReali, {
+ eventi: [],
+ presenze: {},
+ pagelle: [],
+ }).find((o) => o.id === "o7")!;
- assert.equal(
- o7.valore,
- 4,
- "g1 (2) + g2 (2, il ritardo conta) + g3 (0) = 4, calcolate dal database",
- );
- },
- );
+ assert.equal(
+ o7.valore,
+ 4,
+ "g1 (2) + g2 (2, il ritardo conta) + g3 (0) = 4, calcolate dal database",
+ );
+ });
await prova("o12/o13 media e conteggio pagelle vere lette da pagelle_voti", async () => {
const matchId = `${PREFISSO}-o12-m1`;
@@ -522,20 +532,26 @@ if (!locale) {
});
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
- const eventiReali = (await leggiEventi()).filter((e) =>
- eventi.some(([id]) => id === e.id),
- );
+ const eventiReali = (await leggiEventi()).filter((e) => eventi.some(([id]) => id === e.id));
const presenzeReali: MappaPresenze = {};
for (const [id] of eventi) Object.assign(presenzeReali, await leggiPresenze(id));
const rosaConSerieReali = [g1!, g2!, g3!].map((g) => ({
...g,
- serieAllenamenti: serieConsecutiva(g.id, eventiReali, presenzeReali, "allenamento", OGGI_STR),
+ serieAllenamenti: serieConsecutiva(
+ g.id,
+ eventiReali,
+ presenzeReali,
+ "allenamento",
+ OGGI_STR,
+ ),
}));
- const o11 = obiettiviSquadra(rosaConSerieReali, { eventi: [], presenze: {}, pagelle: [] }).find(
- (o) => o.id === "o11",
- )!;
+ const o11 = obiettiviSquadra(rosaConSerieReali, {
+ eventi: [],
+ presenze: {},
+ pagelle: [],
+ }).find((o) => o.id === "o11")!;
assert.equal(
o11.valore,
1,
diff --git a/test/unit/obiettivi.test.ts b/test/unit/obiettivi.test.ts
index 81115c8..24fad30 100644
--- a/test/unit/obiettivi.test.ts
+++ b/test/unit/obiettivi.test.ts
@@ -126,7 +126,10 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
assert.equal(o1Agosto.scadenza, "2026-08-31", "scadenza = ultimo giorno del mese");
const o1Settembre = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_SETTEMBRE), "o1");
- assert.ok(o1Settembre.titolo.includes("settembre"), "titolo o1 riflette il mese iniettato (settembre)");
+ assert.ok(
+ o1Settembre.titolo.includes("settembre"),
+ "titolo o1 riflette il mese iniettato (settembre)",
+ );
assert.equal(o1Settembre.scadenza, "2026-09-30", "scadenza = ultimo giorno di settembre (30 gg)");
// La scadenza di o2 ("Tutti rispondono alle convocazioni") era una data fissa
@@ -197,10 +200,7 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
);
const dueEventiRisposte: ContestoObiettivi = {
- eventi: [
- evento("dr1", "2026-08-03", "allenamento"),
- evento("dr2", "2026-08-17", "partita"),
- ],
+ eventi: [evento("dr1", "2026-08-03", "allenamento"), evento("dr2", "2026-08-17", "partita")],
presenze: {
dr1: Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])),
dr2: {},