Calcoli di badge, obiettivi e percentuali spostati da route e componenti a src/lib
squadra: mediaPresenzeRosa, obiettiviCompletati, mediaProgressoObiettivi, quantiAlGrado; scout: percentualePiu; utils: percentuale (usata da profilo, RosaPresenze, CollezioneBadge). Test unitari per ciascuna. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
06c91149e7
commit
99265f6a1e
13 files changed
+106
-30
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, percentuale } from "@/lib/utils";
|
||||||
import type { Giocatore } from "@/lib/crapp-data";
|
import type { Giocatore } from "@/lib/crapp-data";
|
||||||
import {
|
import {
|
||||||
collezioneBadge,
|
collezioneBadge,
|
||||||
@@ -134,7 +134,7 @@ export function CollezioneBadge({
|
|||||||
const vicino = prossimoTraguardo(g);
|
const vicino = prossimoTraguardo(g);
|
||||||
const social = badgeSocialVinti(votiSocial, g.id);
|
const social = badgeSocialVinti(votiSocial, g.id);
|
||||||
const socialVinti = categorieSocial.filter((cat) => social[cat.id]);
|
const socialVinti = categorieSocial.filter((cat) => social[cat.id]);
|
||||||
const pct = Math.round((c.ottenuti / c.totali) * 100);
|
const pct = percentuale(c.ottenuti, c.totali);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BellRing, HelpCircle, Loader2 } from "lucide-react";
|
import { BellRing, HelpCircle, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, percentuale } from "@/lib/utils";
|
||||||
import { Card } from "@/components/crapp/ui-bits";
|
import { Card } from "@/components/crapp/ui-bits";
|
||||||
import { Avatar } from "@/components/crapp/Avatar";
|
import { Avatar } from "@/components/crapp/Avatar";
|
||||||
import { Barra } from "@/components/motion/Barra";
|
import { Barra } from "@/components/motion/Barra";
|
||||||
@@ -44,7 +44,7 @@ export function RosaPresenze({
|
|||||||
|
|
||||||
const mancanti = rosa.filter((g) => !risposte[g.id]);
|
const mancanti = rosa.filter((g) => !risposte[g.id]);
|
||||||
const risposteN = rosa.length - mancanti.length;
|
const risposteN = rosa.length - mancanti.length;
|
||||||
const perc = rosa.length ? Math.round((risposteN / rosa.length) * 100) : 0;
|
const perc = percentuale(risposteN, rosa.length);
|
||||||
const daSollecitare = mancanti.length + rosa.filter((g) => risposte[g.id] === "forse").length;
|
const daSollecitare = mancanti.length + rosa.filter((g) => risposte[g.id] === "forse").length;
|
||||||
const ordineVisibile = tipo === "evento" ? ordine.filter((s) => s !== "infortunato") : ordine;
|
const ordineVisibile = tipo === "evento" ? ordine.filter((s) => s !== "infortunato") : ordine;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BarChart3, Download, Info } from "lucide-react";
|
import { BarChart3, Download, Info } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, percentuale } from "@/lib/utils";
|
||||||
import { Card } from "@/components/crapp/ui-bits";
|
import { Card } from "@/components/crapp/ui-bits";
|
||||||
import type { Giocatore } from "@/lib/crapp-data";
|
import type { Giocatore } from "@/lib/crapp-data";
|
||||||
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
|
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
|
||||||
@@ -10,11 +10,9 @@ import {
|
|||||||
totaliPerGiocatore,
|
totaliPerGiocatore,
|
||||||
totaliSquadra,
|
totaliSquadra,
|
||||||
type ScoutMatch,
|
type ScoutMatch,
|
||||||
|
percentualePiu,
|
||||||
} from "@/lib/scout-store";
|
} from "@/lib/scout-store";
|
||||||
|
|
||||||
const pct = (piu: number, meno: number) =>
|
|
||||||
piu + meno === 0 ? null : Math.round((piu / (piu + meno)) * 100);
|
|
||||||
|
|
||||||
/** Lettura dello scout di una partita: squadra per fondamentale, set per set, dettaglio per giocatore. */
|
/** Lettura dello scout di una partita: squadra per fondamentale, set per set, dettaglio per giocatore. */
|
||||||
export function ScoutReport({
|
export function ScoutReport({
|
||||||
scout,
|
scout,
|
||||||
@@ -149,7 +147,7 @@ export function ScoutReport({
|
|||||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||||
{fondamentali.map((f) => {
|
{fondamentali.map((f) => {
|
||||||
const { piu, meno } = squadra[f.id];
|
const { piu, meno } = squadra[f.id];
|
||||||
const p = pct(piu, meno);
|
const p = percentualePiu(piu, meno);
|
||||||
return (
|
return (
|
||||||
<div key={f.id} className="rounded-2xl bg-secondary p-3">
|
<div key={f.id} className="rounded-2xl bg-secondary p-3">
|
||||||
<div className="flex items-baseline justify-between">
|
<div className="flex items-baseline justify-between">
|
||||||
|
|||||||
@@ -194,6 +194,14 @@ export function gradoRaggiunto(def: BadgeDef, valore: number): Grado | null {
|
|||||||
return grado;
|
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 {
|
export function statoBadge(def: BadgeDef, g: Giocatore): BadgeStato {
|
||||||
const valore = def.valore(g);
|
const valore = def.valore(g);
|
||||||
const grado = gradoRaggiunto(def, valore);
|
const grado = gradoRaggiunto(def, valore);
|
||||||
|
|||||||
@@ -415,6 +415,25 @@ export function progressoObiettivo(o: ObiettivoSquadra) {
|
|||||||
return Math.min(100, Math.round((o.valore / o.target) * 100));
|
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) {
|
export function obiettiviOrdinati(rosa?: Giocatore[], ctx?: ContestoObiettivi, oggi?: Date) {
|
||||||
return obiettiviSquadra(rosa, ctx, oggi).sort((a, b) => {
|
return obiettiviSquadra(rosa, ctx, oggi).sort((a, b) => {
|
||||||
const pa = progressoObiettivo(a);
|
const pa = progressoObiettivo(a);
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ export function contaValutazioni(
|
|||||||
return out;
|
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,
|
* 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.
|
* su tutta la partita o su un solo set. Chi non ha valutazioni nel periodo non compare.
|
||||||
|
|||||||
@@ -4,3 +4,8 @@ import { twMerge } from "tailwind-merge";
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
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;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Flame, Trash2, Bell, Mail, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react";
|
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 { Card, PageHeader, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
|
||||||
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
||||||
import { Avatar } from "@/components/crapp/Avatar";
|
import { Avatar } from "@/components/crapp/Avatar";
|
||||||
@@ -87,7 +87,7 @@ function ProfiloGiocatore() {
|
|||||||
|
|
||||||
const tecnici = totaliTecnici(scoutMatches, g.id);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
+12
-17
@@ -7,7 +7,13 @@ import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
|||||||
import { Avatar } from "@/components/crapp/Avatar";
|
import { Avatar } from "@/components/crapp/Avatar";
|
||||||
import { formatData, inizialiDa } from "@/lib/crapp-data";
|
import { formatData, inizialiDa } from "@/lib/crapp-data";
|
||||||
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
|
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
|
||||||
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
import {
|
||||||
|
mediaPresenzeRosa,
|
||||||
|
mediaProgressoObiettivi,
|
||||||
|
microcopyObiettivo,
|
||||||
|
obiettiviCompletati,
|
||||||
|
progressoObiettivo,
|
||||||
|
} from "@/lib/obiettivi";
|
||||||
import {
|
import {
|
||||||
useRosa,
|
useRosa,
|
||||||
useObiettivi,
|
useObiettivi,
|
||||||
@@ -45,7 +51,7 @@ import {
|
|||||||
descrizioneSoglie,
|
descrizioneSoglie,
|
||||||
gradiOrdine,
|
gradiOrdine,
|
||||||
gradoMeta,
|
gradoMeta,
|
||||||
gradoRaggiunto,
|
quantiAlGrado,
|
||||||
collezioneBadge,
|
collezioneBadge,
|
||||||
} from "@/lib/badges";
|
} from "@/lib/badges";
|
||||||
|
|
||||||
@@ -111,20 +117,14 @@ function Squadra() {
|
|||||||
const [filtroAperto, setFiltroAperto] = useState(false);
|
const [filtroAperto, setFiltroAperto] = useState(false);
|
||||||
const obiettivi = useObiettivi();
|
const obiettivi = useObiettivi();
|
||||||
const team = totaliSquadraValutazioni(scoutMatches);
|
const team = totaliSquadraValutazioni(scoutMatches);
|
||||||
const mediaPresenze = rosa.length
|
const mediaPresenze = mediaPresenzeRosa(rosa);
|
||||||
? Math.round(
|
|
||||||
(rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100,
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
const ordinati = [...rosa].sort((a, b) => valore(b, criterio) - valore(a, criterio));
|
const ordinati = [...rosa].sort((a, b) => valore(b, criterio) - valore(a, criterio));
|
||||||
const rank = classificaRank(ordinati.map((g) => valore(g, criterio)));
|
const rank = classificaRank(ordinati.map((g) => valore(g, criterio)));
|
||||||
const max = ordinati[0] ? valore(ordinati[0], criterio) || 1 : 1;
|
const max = ordinati[0] ? valore(ordinati[0], criterio) || 1 : 1;
|
||||||
const { data: csi } = useCsi();
|
const { data: csi } = useCsi();
|
||||||
const matchGiocati = csi ? partiteGiocate(csi.partite).length : scoutMatches.length;
|
const matchGiocati = csi ? partiteGiocate(csi.partite).length : scoutMatches.length;
|
||||||
const completati = obiettivi.filter((o) => progressoObiettivo(o) >= 100).length;
|
const completati = obiettiviCompletati(obiettivi);
|
||||||
const mediaObiettivi = obiettivi.length
|
const mediaObiettivi = mediaProgressoObiettivi(obiettivi);
|
||||||
? Math.round(obiettivi.reduce((s, o) => s + progressoObiettivo(o), 0) / obiettivi.length)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Ogni tab è memoizzata: aprire/chiudere una card giocatore (stato `aperto`)
|
// Ogni tab è memoizzata: aprire/chiudere una card giocatore (stato `aperto`)
|
||||||
// non deve ricalcolare badge e obiettivi delle altre sezioni.
|
// non deve ricalcolare badge e obiettivi delle altre sezioni.
|
||||||
@@ -504,12 +504,7 @@ function Squadra() {
|
|||||||
<div className="mt-2 grid grid-cols-3 gap-1.5">
|
<div className="mt-2 grid grid-cols-3 gap-1.5">
|
||||||
{gradiOrdine.map((grado) => {
|
{gradiOrdine.map((grado) => {
|
||||||
const meta = gradoMeta[grado];
|
const meta = gradoMeta[grado];
|
||||||
const quanti = rosa.filter((g) => {
|
const quanti = quantiAlGrado(b, rosa, grado);
|
||||||
const raggiunto = gradoRaggiunto(b, b.valore(g));
|
|
||||||
return raggiunto
|
|
||||||
? gradiOrdine.indexOf(raggiunto) >= gradiOrdine.indexOf(grado)
|
|
||||||
: false;
|
|
||||||
}).length;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={grado}
|
key={grado}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
mancanoPer,
|
mancanoPer,
|
||||||
microcopyBadge,
|
microcopyBadge,
|
||||||
prossimoTraguardo,
|
prossimoTraguardo,
|
||||||
|
quantiAlGrado,
|
||||||
segretiNascosti,
|
segretiNascosti,
|
||||||
statoBadge,
|
statoBadge,
|
||||||
tuttiBadge,
|
tuttiBadge,
|
||||||
@@ -247,4 +248,17 @@ assert.equal(
|
|||||||
"gli id dei badge sono unici",
|
"gli id dei badge sono unici",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- quantiAlGrado: chi ha raggiunto almeno quel grado -----------------------------
|
||||||
|
{
|
||||||
|
const def = badgeDefs[0]!;
|
||||||
|
const rosa = [g(), g(), g()];
|
||||||
|
assert.equal(quantiAlGrado(def, [], "bronzo"), 0);
|
||||||
|
const n = (grado: (typeof gradiOrdine)[number]) => 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");
|
console.log("badges: ok");
|
||||||
@@ -6,7 +6,10 @@ import type { VotoPagella } from "@/lib/pagelle";
|
|||||||
import type { MappaPresenze } from "@/lib/presenze";
|
import type { MappaPresenze } from "@/lib/presenze";
|
||||||
import {
|
import {
|
||||||
contestoVuoto,
|
contestoVuoto,
|
||||||
|
mediaPresenzeRosa,
|
||||||
|
mediaProgressoObiettivi,
|
||||||
microcopyObiettivo,
|
microcopyObiettivo,
|
||||||
|
obiettiviCompletati,
|
||||||
obiettiviOrdinati,
|
obiettiviOrdinati,
|
||||||
obiettiviSquadra,
|
obiettiviSquadra,
|
||||||
progressoObiettivo,
|
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");
|
console.log("obiettivi: ok");
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
contaPiuSenzaRicezione,
|
contaPiuSenzaRicezione,
|
||||||
haPunteggio,
|
haPunteggio,
|
||||||
parzialiFinali,
|
parzialiFinali,
|
||||||
|
percentualePiu,
|
||||||
sintesiValutazioni,
|
sintesiValutazioni,
|
||||||
contaValutazioni,
|
contaValutazioni,
|
||||||
fondamentali,
|
fondamentali,
|
||||||
@@ -269,4 +270,8 @@ assert.deepEqual(
|
|||||||
assert.deepEqual(set2.squadra.muro, { piu: 1, meno: 0 });
|
assert.deepEqual(set2.squadra.muro, { piu: 1, meno: 0 });
|
||||||
assert.deepEqual(set2.squadra.battuta, { piu: 0, meno: 0 });
|
assert.deepEqual(set2.squadra.battuta, { piu: 0, meno: 0 });
|
||||||
assert.equal(sintesiValutazioni([]).perGiocatore.length, 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");
|
console.log("scout-store sintesi: ok");
|
||||||
@@ -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 assert from "node:assert/strict";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, percentuale } from "@/lib/utils";
|
||||||
|
|
||||||
// --- cn: unisce classi statiche e condizionali --------------------------------
|
// --- cn: unisce classi statiche e condizionali --------------------------------
|
||||||
assert.equal(cn("a", "b"), "a b");
|
assert.equal(cn("a", "b"), "a b");
|
||||||
@@ -16,4 +16,10 @@ assert.equal(
|
|||||||
"classi non in conflitto convivono",
|
"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");
|
console.log("utils: ok");
|
||||||
Reference in new issue
Block a user