diff --git a/src/lib/badges.ts b/src/lib/badges.ts
index 178a070..6999cff 100644
--- a/src/lib/badges.ts
+++ b/src/lib/badges.ts
@@ -194,6 +194,14 @@ export function gradoRaggiunto(def: BadgeDef, valore: number): Grado | null {
return grado;
}
+/** Quanti giocatori della rosa hanno raggiunto almeno `grado` per il badge `def`. */
+export function quantiAlGrado(def: BadgeDef, rosa: Giocatore[], grado: Grado): number {
+ return rosa.filter((g) => {
+ const raggiunto = gradoRaggiunto(def, def.valore(g));
+ return raggiunto ? gradiOrdine.indexOf(raggiunto) >= gradiOrdine.indexOf(grado) : false;
+ }).length;
+}
+
export function statoBadge(def: BadgeDef, g: Giocatore): BadgeStato {
const valore = def.valore(g);
const grado = gradoRaggiunto(def, valore);
diff --git a/src/lib/obiettivi.ts b/src/lib/obiettivi.ts
index 7c9064f..ac0e681 100644
--- a/src/lib/obiettivi.ts
+++ b/src/lib/obiettivi.ts
@@ -415,6 +415,25 @@ export function progressoObiettivo(o: ObiettivoSquadra) {
return Math.min(100, Math.round((o.valore / o.target) * 100));
}
+export function obiettiviCompletati(obiettivi: ObiettivoSquadra[]) {
+ return obiettivi.filter((o) => progressoObiettivo(o) >= 100).length;
+}
+
+export function mediaProgressoObiettivi(obiettivi: ObiettivoSquadra[]) {
+ return obiettivi.length
+ ? Math.round(obiettivi.reduce((s, o) => s + progressoObiettivo(o), 0) / obiettivi.length)
+ : 0;
+}
+
+/** Media, in percentuale, della quota di eventi a cui ogni giocatore della rosa ha partecipato. */
+export function mediaPresenzeRosa(rosa: Giocatore[]) {
+ return rosa.length
+ ? Math.round(
+ (rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100,
+ )
+ : 0;
+}
+
export function obiettiviOrdinati(rosa?: Giocatore[], ctx?: ContestoObiettivi, oggi?: Date) {
return obiettiviSquadra(rosa, ctx, oggi).sort((a, b) => {
const pa = progressoObiettivo(a);
diff --git a/src/lib/scout-store.ts b/src/lib/scout-store.ts
index 01efaae..3fb3641 100644
--- a/src/lib/scout-store.ts
+++ b/src/lib/scout-store.ts
@@ -96,6 +96,11 @@ export function contaValutazioni(
return out;
}
+/** Percentuale di + sul totale (+ e −) di un fondamentale; `null` se non ci sono valutazioni. */
+export function percentualePiu(piu: number, meno: number): number | null {
+ return piu + meno === 0 ? null : Math.round((piu / (piu + meno)) * 100);
+}
+
/**
* Riepilogo delle valutazioni per la lettura nell'app: totali di squadra e per giocatore,
* su tutta la partita o su un solo set. Chi non ha valutazioni nel periodo non compare.
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a5ef193..0ac12a2 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -4,3 +4,8 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+
+/** Percentuale intera di `parte` su `totale`; 0 se il totale è 0. */
+export function percentuale(parte: number, totale: number): number {
+ return totale ? Math.round((parte / totale) * 100) : 0;
+}
diff --git a/src/routes/profilo.tsx b/src/routes/profilo.tsx
index 2b94190..be6b58d 100644
--- a/src/routes/profilo.tsx
+++ b/src/routes/profilo.tsx
@@ -2,7 +2,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { Flame, Trash2, Bell, Mail, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react";
-import { cn } from "@/lib/utils";
+import { cn, percentuale } from "@/lib/utils";
import { Card, PageHeader, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
import { Avatar } from "@/components/crapp/Avatar";
@@ -87,7 +87,7 @@ function ProfiloGiocatore() {
const tecnici = totaliTecnici(scoutMatches, g.id);
- const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0;
+ const percPresenze = percentuale(g.presenze, g.totaliEventi);
return (
<>
diff --git a/src/routes/squadra.tsx b/src/routes/squadra.tsx
index 472ed07..a9141cf 100644
--- a/src/routes/squadra.tsx
+++ b/src/routes/squadra.tsx
@@ -7,7 +7,13 @@ import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
import { Avatar } from "@/components/crapp/Avatar";
import { formatData, inizialiDa } from "@/lib/crapp-data";
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
-import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
+import {
+ mediaPresenzeRosa,
+ mediaProgressoObiettivi,
+ microcopyObiettivo,
+ obiettiviCompletati,
+ progressoObiettivo,
+} from "@/lib/obiettivi";
import {
useRosa,
useObiettivi,
@@ -45,7 +51,7 @@ import {
descrizioneSoglie,
gradiOrdine,
gradoMeta,
- gradoRaggiunto,
+ quantiAlGrado,
collezioneBadge,
} from "@/lib/badges";
@@ -111,20 +117,14 @@ function Squadra() {
const [filtroAperto, setFiltroAperto] = useState(false);
const obiettivi = useObiettivi();
const team = totaliSquadraValutazioni(scoutMatches);
- const mediaPresenze = rosa.length
- ? Math.round(
- (rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100,
- )
- : 0;
+ const mediaPresenze = mediaPresenzeRosa(rosa);
const ordinati = [...rosa].sort((a, b) => valore(b, criterio) - valore(a, criterio));
const rank = classificaRank(ordinati.map((g) => valore(g, criterio)));
const max = ordinati[0] ? valore(ordinati[0], criterio) || 1 : 1;
const { data: csi } = useCsi();
const matchGiocati = csi ? partiteGiocate(csi.partite).length : scoutMatches.length;
- const completati = obiettivi.filter((o) => progressoObiettivo(o) >= 100).length;
- const mediaObiettivi = obiettivi.length
- ? Math.round(obiettivi.reduce((s, o) => s + progressoObiettivo(o), 0) / obiettivi.length)
- : 0;
+ const completati = obiettiviCompletati(obiettivi);
+ const mediaObiettivi = mediaProgressoObiettivi(obiettivi);
// Ogni tab è memoizzata: aprire/chiudere una card giocatore (stato `aperto`)
// non deve ricalcolare badge e obiettivi delle altre sezioni.
@@ -504,12 +504,7 @@ function Squadra() {
{gradiOrdine.map((grado) => {
const meta = gradoMeta[grado];
- const quanti = rosa.filter((g) => {
- const raggiunto = gradoRaggiunto(b, b.valore(g));
- return raggiunto
- ? gradiOrdine.indexOf(raggiunto) >= gradiOrdine.indexOf(grado)
- : false;
- }).length;
+ const quanti = quantiAlGrado(b, rosa, grado);
return (
quantiAlGrado(def, rosa, grado);
+ assert.ok(
+ n("bronzo") >= n("argento") && n("argento") >= n("oro"),
+ "i gradi alti non superano i bassi",
+ );
+ assert.equal(n("bronzo"), rosa.filter((x) => gradoRaggiunto(def, def.valore(x)) !== null).length);
+}
+
console.log("badges: ok");
diff --git a/test/unit/obiettivi.test.ts b/test/unit/obiettivi.test.ts
index 77da3cc..6e29f5a 100644
--- a/test/unit/obiettivi.test.ts
+++ b/test/unit/obiettivi.test.ts
@@ -6,7 +6,10 @@ import type { VotoPagella } from "@/lib/pagelle";
import type { MappaPresenze } from "@/lib/presenze";
import {
contestoVuoto,
+ mediaPresenzeRosa,
+ mediaProgressoObiettivi,
microcopyObiettivo,
+ obiettiviCompletati,
obiettiviOrdinati,
obiettiviSquadra,
progressoObiettivo,
@@ -735,4 +738,22 @@ for (const ob of obiettiviSquadra(giocatori, contestoVuoto)) {
}
}
+// --- riepiloghi usati dalla pagina Squadra -----------------------------------------
+{
+ const ob = (valore: number): ObiettivoSquadra =>
+ ({ ...obiettiviSquadra(giocatori, contestoVuoto)[0]!, valore, target: 10 }) as ObiettivoSquadra;
+ assert.equal(obiettiviCompletati([]), 0);
+ assert.equal(mediaProgressoObiettivi([]), 0);
+ assert.equal(obiettiviCompletati([ob(10), ob(15), ob(3)]), 2);
+ assert.equal(mediaProgressoObiettivi([ob(10), ob(0)]), 50);
+ assert.equal(mediaPresenzeRosa([]), 0);
+ const p = (presenze: number, totaliEventi: number) => ({
+ ...giocatori[0]!,
+ presenze,
+ totaliEventi,
+ });
+ assert.equal(mediaPresenzeRosa([p(5, 10), p(10, 10)]), 75);
+ assert.equal(mediaPresenzeRosa([p(0, 0)]), 0, "nessun evento: niente divisione per zero");
+}
+
console.log("obiettivi: ok");
diff --git a/test/unit/scout-store.test.ts b/test/unit/scout-store.test.ts
index 316426c..7958a01 100644
--- a/test/unit/scout-store.test.ts
+++ b/test/unit/scout-store.test.ts
@@ -6,6 +6,7 @@ import {
contaPiuSenzaRicezione,
haPunteggio,
parzialiFinali,
+ percentualePiu,
sintesiValutazioni,
contaValutazioni,
fondamentali,
@@ -269,4 +270,8 @@ assert.deepEqual(
assert.deepEqual(set2.squadra.muro, { piu: 1, meno: 0 });
assert.deepEqual(set2.squadra.battuta, { piu: 0, meno: 0 });
assert.equal(sintesiValutazioni([]).perGiocatore.length, 0);
+assert.equal(percentualePiu(0, 0), null);
+assert.equal(percentualePiu(3, 1), 75);
+assert.equal(percentualePiu(0, 4), 0);
+
console.log("scout-store sintesi: ok");
diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts
index 5a3217a..16e5dc3 100644
--- a/test/unit/utils.test.ts
+++ b/test/unit/utils.test.ts
@@ -1,6 +1,6 @@
-/** Check dell'helper di classi CSS: `bun test/unit/utils.test.ts`. */
+/** Check dell'helper di utils (classi CSS e percentuale): `bun test/unit/utils.test.ts`. */
import assert from "node:assert/strict";
-import { cn } from "@/lib/utils";
+import { cn, percentuale } from "@/lib/utils";
// --- cn: unisce classi statiche e condizionali --------------------------------
assert.equal(cn("a", "b"), "a b");
@@ -16,4 +16,10 @@ assert.equal(
"classi non in conflitto convivono",
);
+// --- percentuale -----------------------------------------------------------------
+assert.equal(percentuale(1, 0), 0, "totale 0: nessuna divisione per zero");
+assert.equal(percentuale(1, 3), 33);
+assert.equal(percentuale(2, 3), 67);
+assert.equal(percentuale(5, 5), 100);
+
console.log("utils: ok");