4 Commits
Author SHA1 Message Date
davideandClaude Sonnet 5.5 5281c0c62b Scout: nomi mai spezzati a metà parola, riquadri a 3 colonne sotto i 400 px
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 10:45:43 +01:00
davideandClaude Sonnet 5.5 3a892a78e6 Versione 1.6.2
Layout corretto sugli schermi stretti (360-402 px) e Ace, Attacchi, Muri e Punti dai +
dello scout in Squadra e Profilo. Nessuna migration.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 10:42:39 +01:00
davideandClaude Sonnet 5.5 1e4ec9113e Squadra e Profilo: ace, attacchi, muri e punti dai + dello scout
Squadra → Stats: punti, ace e muri di squadra sommano i + delle valutazioni scout di tutte
le partite. Profilo → Season: al posto di «Turni palloni» compaiono Attacchi, Muri, Ace e
Punti (totale) del giocatore. Le partite del metodo precedente contano con le azioni.
DD-008 aggiornata: dati solo informativi, fuori da badge e classifiche.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 10:33:49 +01:00
davideandClaude Sonnet 5.5 263621456d Layout su schermi stretti: scout e griglie non tagliati a 360 px
Tabella scout senza larghezza minima (nome a capo, colonne compatte), griglie con
min-width 0, niente scroll orizzontale della pagina, campi a 16px contro lo zoom iOS.
Fixes #12

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 09:54:10 +01:00
16 changed files with 221 additions and 23 deletions

No files matched your search

+4 -2
View File
@@ -9,8 +9,10 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
## Stato generale ## Stato generale
- **Versione:** 1.6.1 su `main`: risultati dello scout leggibili nella pagina - **Versione:** 1.6.2 su `main`: layout corretto sugli schermi stretti (360-402 px) e
partita, classifica cacche completa e pagina partita a sezioni a scomparsa, senza migration. La 1.6.0 Ace, Attacchi, Muri e Punti dai + dello scout in Squadra e Profilo, senza migration. La 1.6.1
(tag `v1.6.1`): risultati dello scout leggibili nella pagina partita, classifica cacche
completa e pagina partita a sezioni a scomparsa. La 1.6.0
(tag `v1.6.0`, in produzione dal 02/11/2026): voto MVP definitivo, segreto e a (tag `v1.6.0`, in produzione dal 02/11/2026): voto MVP definitivo, segreto e a
tempo (DD-051, DD-052) e Scout live a valutazioni − / + (DD-053). La 1.5.0: campionato CSI sulla stagione 2026/27 (nuovi `project_id` e `team_id`, tempo (DD-051, DD-052) e Scout live a valutazioni − / + (DD-053). La 1.5.0: campionato CSI sulla stagione 2026/27 (nuovi `project_id` e `team_id`,
nessuna Coppa). La 1.4.2: ogni evento può avere un link Google Maps del luogo, mostrato come nessuna Coppa). La 1.4.2: ogni evento può avere un link Google Maps del luogo, mostrato come
+18
View File
@@ -13,6 +13,24 @@ versioni `0.y.z` erano pre-release.
## [Non rilasciato] ## [Non rilasciato]
## [1.6.2] - 2026-11-11
Layout corretto sugli schermi stretti e statistiche tecniche dai + dello scout in Squadra e
Profilo. Nessuna migration.
### Modificato
- Squadra → Stats: «Punti squadra», «Ace squadra» e «Muri squadra» sommano i + dello scout di
tutte le partite (ace = + di battuta, muri = + di muro, punti = + di attacco, battuta e muro);
le partite del metodo precedente contano con le loro azioni.
- Profilo → Season: al posto di «Turni palloni» compaiono Ace, Attacchi, Muri e Punti (totale)
del giocatore, somma dei + dello scout di tutte le partite (DD-008: solo informativi).
- Layout su schermi stretti (360-402 px, iPhone e Android): niente più pagina tagliata in
larghezza. La tabella per giocatore dello scout sta nello schermo (il nome va a capo su due righe), le
griglie di riquadri non sfondano con nomi ed etichette lunghe, la pagina non scorre in
orizzontale e i campi di testo sono a 16 px, così iOS non zooma al tocco. I nomi non si spezzano
mai a metà parola; nello scout live i riquadri dei giocatori sono a 3 colonne sotto i 400 px.
## [1.6.1] - 2026-11-06 ## [1.6.1] - 2026-11-06
Risultati dello scout leggibili nell'app, classifica cacche completa e pagina partita a sezioni Risultati dello scout leggibili nell'app, classifica cacche completa e pagina partita a sezioni
+3
View File
@@ -17,6 +17,9 @@ Le statistiche **personali** in profilo e squadra devono essere **eque per tutti
- Badge e obiettivi usano presenze, MVP, pagelle, serie, cacche — metriche accessibili a tutti. - Badge e obiettivi usano presenze, MVP, pagelle, serie, cacche — metriche accessibili a tutti.
- Lo scout resta strumento tecnico, non gioco. - Lo scout resta strumento tecnico, non gioco.
- Aggiornamento 2026-11-11: in Profilo → Season compaiono, come semplice informazione personale,
i totali di ace, attacchi, muri e punti del giocatore ricavati dai + dello scout. Non entrano
in badge, obiettivi o classifiche: la regola sulla leva competitiva resta valida.
**Riesame** **Riesame**
Se la squadra chiede esplicitamente classifiche tecniche per ruolo. Se la squadra chiede esplicitamente classifiche tecniche per ruolo.
+8
View File
@@ -285,8 +285,16 @@ Sezione già presente. Contiene.
- Voto medio - Voto medio
- MVP - MVP
- Serie - Serie
- **Ace, Attacchi, Muri e Punti (totale)**: somma, su tutte le partite scoutate, dei soli **+**
assegnati a quel giocatore nello scout live (ace = + di battuta, attacchi = + di attacco,
muri = + di muro; punti = ace + attacchi + muri, la ricezione non conta). Le partite scoutate
col metodo precedente, senza valutazioni, contano con le azioni registrate; se una partita ha
entrambe, valgono solo le valutazioni (`totaliTecnici()` in `scout-store.ts`). Il «Turni
palloni» non compare più qui: il turno resta nel banner palloni e nel badge dedicato.
- Altre statistiche disponibili - Altre statistiche disponibili
Sono numeri informativi: non alimentano badge, obiettivi né classifiche (DD-008).
### Badge ### Badge
Sezione già presente. Sezione già presente.
+7
View File
@@ -82,6 +82,13 @@ successivo non salta — 1, 1, 2, non 1, 1, 3); `dettaglioClassifica()` sceglie
sottostatistica mostrare sotto il nome, coerente col criterio selezionato (es. "voti sottostatistica mostrare sotto il nome, coerente col criterio selezionato (es. "voti
pagella" per il criterio media voto, non sempre "presenze consecutive"). pagella" per il criterio media voto, non sempre "presenze consecutive").
In cima alla tab, i riquadri «Punti squadra», «Ace squadra» e «Muri squadra» sommano **tutte le
partite scoutate** (`totaliSquadraValutazioni()`, che usa `totaliTecnici()`, in `scout-store.ts`), contando solo le valutazioni
**+** dello scout live: «Ace» = + di battuta, «Muri» = + di muro, «Punti» = + di attacco, battuta e
muro (la ricezione non è un punto messo a terra, come nel riquadro del giocatore). Le partite
scoutate col metodo precedente, senza valutazioni, contano con le loro azioni registrate (punti,
ace, muri); se una partita ha entrambe, valgono solo le valutazioni, così non si conta due volte.
Le altre tab di `/squadra` (Rosa, Obiettivi, Badge) sono viste diverse sugli stessi dati di Le altre tab di `/squadra` (Rosa, Obiettivi, Badge) sono viste diverse sugli stessi dati di
`useRosa()`/`useObiettivi()`/`badges.ts`: non introducono altra logica di dominio, solo `useRosa()`/`useObiettivi()`/`badges.ts`: non introducono altra logica di dominio, solo
presentazione — le rispettive specifiche stanno in `badge.md` e `obiettivi-squadra.md`. presentazione — le rispettive specifiche stanno in `badge.md` e `obiettivi-squadra.md`.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "crapp", "name": "crapp",
"version": "1.6.1", "version": "1.6.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "crapp", "name": "crapp",
"version": "1.6.1", "version": "1.6.2",
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.111.0", "@supabase/supabase-js": "^2.111.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "crapp", "name": "crapp",
"version": "1.6.1", "version": "1.6.2",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"type": "module", "type": "module",
+10 -10
View File
@@ -124,7 +124,7 @@ export function ScoutReport({
<div <div
role="tablist" role="tablist"
aria-label="Periodo" aria-label="Periodo"
className="mt-3 grid grid-flow-col auto-cols-fr gap-1 rounded-2xl bg-secondary p-1" className="mt-3 grid grid-flow-col auto-cols-fr gap-0.5 rounded-2xl bg-secondary p-1"
> >
{[undefined, ...sets].map((s) => ( {[undefined, ...sets].map((s) => (
<button <button
@@ -134,7 +134,7 @@ export function ScoutReport({
aria-selected={s === set} aria-selected={s === set}
onClick={() => setSet(s)} onClick={() => setSet(s)}
className={cn( className={cn(
"min-w-0 truncate rounded-xl px-1 py-2 text-[13px] font-bold uppercase transition-colors", "min-w-0 truncate rounded-xl px-0 py-2 text-[13px] font-bold uppercase transition-colors",
s === set s === set
? "bg-primary text-primary-foreground shadow-card" ? "bg-primary text-primary-foreground shadow-card"
: "text-muted-foreground", : "text-muted-foreground",
@@ -185,13 +185,13 @@ export function ScoutReport({
Nessuna valutazione in questo set. Nessuna valutazione in questo set.
</p> </p>
) : ( ) : (
<div className="mt-2 overflow-x-auto rounded-2xl bg-secondary/60"> <div className="mt-2 overflow-hidden rounded-2xl bg-secondary/60">
<table className="w-full min-w-[22rem] text-[13px]"> <table className="w-full table-fixed text-[13px]">
<thead> <thead>
<tr className="text-left text-[13px] uppercase text-muted-foreground"> <tr className="text-left text-[13px] uppercase text-muted-foreground">
<th className="px-3 py-2 font-bold">Giocatore</th> <th className="whitespace-nowrap px-2 py-2 font-bold">Giocatore</th>
{fondamentali.map((f) => ( {fondamentali.map((f) => (
<th key={f.id} className="px-2 py-2 text-center font-bold"> <th key={f.id} className="w-11 px-0 py-2 text-center font-bold">
{f.label.slice(0, 3)} {f.label.slice(0, 3)}
</th> </th>
))} ))}
@@ -200,16 +200,16 @@ export function ScoutReport({
<tbody className="divide-y divide-border"> <tbody className="divide-y divide-border">
{righe.map(({ g, conteggi }) => ( {righe.map(({ g, conteggi }) => (
<tr key={g.id}> <tr key={g.id}>
<td className="max-w-40 truncate px-3 py-2 font-semibold"> <td className="px-2 py-2 font-semibold leading-tight [overflow-wrap:normal]">
<span className="mr-1.5 tabular-nums text-muted-foreground"> <span className="mr-1.5 tabular-nums text-muted-foreground">
#{g.numero} #{g.numero}
</span> </span>
{g.nome} {g.nome}
</td> </td>
{fondamentali.map((f) => ( {fondamentali.map((f) => (
<td key={f.id} className="px-2 py-2 text-center font-semibold tabular-nums"> <td key={f.id} className="px-0 py-2 text-center font-semibold tabular-nums">
<span className="text-success">{conteggi[f.id].piu}</span> <span className="text-success">{conteggi[f.id].piu}</span>
<span className="text-muted-foreground"> / </span> <span className="text-muted-foreground">/</span>
<span className="text-destructive">{conteggi[f.id].meno}</span> <span className="text-destructive">{conteggi[f.id].meno}</span>
</td> </td>
))} ))}
@@ -238,7 +238,7 @@ export function ScoutReport({
{ l: "Muri", v: totaliAzioni.muri }, { l: "Muri", v: totaliAzioni.muri },
{ l: "Errori", v: totaliAzioni.errori }, { l: "Errori", v: totaliAzioni.errori },
].map((t) => ( ].map((t) => (
<div key={t.l} className="rounded-2xl bg-secondary p-2.5"> <div key={t.l} className="rounded-2xl bg-secondary px-1 py-2.5">
<p className="font-display text-xl leading-none">{t.v}</p> <p className="font-display text-xl leading-none">{t.v}</p>
<p className="mt-1 text-[13px] font-semibold uppercase text-muted-foreground"> <p className="mt-1 text-[13px] font-semibold uppercase text-muted-foreground">
{t.l} {t.l}
+3 -1
View File
@@ -339,9 +339,11 @@ export function SezioneTendina({
* `input[type="date"]` ha al suo interno segmenti e icona nativi che, anche * `input[type="date"]` ha al suo interno segmenti e icona nativi che, anche
* con `appearance-none`, restano un filo più alti di un input di testo a * con `appearance-none`, restano un filo più alti di un input di testo a
* parità di padding — un'altezza esplicita allinea tutti i campi tra loro. * parità di padding — un'altezza esplicita allinea tutti i campi tra loro.
* `text-base` (16px) non è estetica: sotto i 16px iOS Safari zooma la pagina al focus e,
* dopo il blur, la lascia ingrandita e tagliata in larghezza.
*/ */
export const classiInput = export const classiInput =
"h-10 w-full min-w-0 rounded-xl border border-border bg-background px-3 py-2 text-[15px]"; "h-10 w-full min-w-0 rounded-xl border border-border bg-background px-3 py-2 text-base";
/** /**
* Etichetta + controllo di un form. `min-w-0`: dentro `grid-cols-2` questa label * Etichetta + controllo di un form. `min-w-0`: dentro `grid-cols-2` questa label
+40
View File
@@ -291,6 +291,46 @@ export function totaliSquadra(matches: ScoutMatch[]) {
return out; return out;
} }
export type TotaliTecnici = { punti: number; ace: number; attacchi: number; muri: number };
/**
* Somma, su tutte le partite scoutate, delle sole valutazioni **+** (battuta → ace, attacco →
* attacchi, muro → muri; punti = ace + attacchi + muri, non la ricezione). Con `giocatoreId`
* conta solo quel giocatore, altrimenti tutta la squadra. Una partita senza valutazioni (metodo
* precedente) conta con le sue azioni; se ha entrambe valgono solo le valutazioni, per non
* contare due volte.
*/
export function totaliTecnici(matches: ScoutMatch[], giocatoreId?: string): TotaliTecnici {
const out = { punti: 0, ace: 0, attacchi: 0, muri: 0 };
for (const m of matches) {
if (m.valutazioni.length === 0) {
for (const [id, t] of totaliPerGiocatore(m.azioni)) {
if (giocatoreId !== undefined && id !== giocatoreId) continue;
out.ace += t.ace;
out.muri += t.muri;
out.attacchi += t.punti - t.ace - t.muri;
out.punti += t.punti;
}
continue;
}
for (const v of m.valutazioni) {
if (v.segno !== "+" || v.fondamentale === "ricezione") continue;
if (giocatoreId !== undefined && v.giocatoreId !== giocatoreId) continue;
if (v.fondamentale === "battuta") out.ace += 1;
if (v.fondamentale === "attacco") out.attacchi += 1;
if (v.fondamentale === "muro") out.muri += 1;
out.punti += 1;
}
}
return out;
}
/** Punti, ace e muri di squadra per la tab Stats di `/squadra` (vedi `totaliTecnici`). */
export function totaliSquadraValutazioni(matches: ScoutMatch[]) {
const { punti, ace, muri } = totaliTecnici(matches);
return { punti, ace, muri };
}
/** /**
* MVP ricavati dalle partite scoutate (mappa matchId → nome vincitore). * MVP ricavati dalle partite scoutate (mappa matchId → nome vincitore).
* Le presenze personali restano su `risposte_presenze`, non sullo scout. * Le presenze personali restano su `risposte_presenze`, non sullo scout.
+9 -2
View File
@@ -33,6 +33,7 @@ import {
type GiocatoreSquadra, type GiocatoreSquadra,
} from "@/lib/giocatori-squadra"; } from "@/lib/giocatori-squadra";
import { esci } from "@/lib/auth"; import { esci } from "@/lib/auth";
import { totaliTecnici, useScoutMatches } from "@/lib/scout-store";
import { useIsAdmin } from "@/lib/ruoli"; import { useIsAdmin } from "@/lib/ruoli";
import { useEmailNotifiche } from "@/lib/preferenze-utente"; import { useEmailNotifiche } from "@/lib/preferenze-utente";
import { Reveal } from "@/components/motion/Reveal"; import { Reveal } from "@/components/motion/Reveal";
@@ -80,9 +81,12 @@ function ProfiloGiocatore() {
const votiSocial = useVotiSocial(); const votiSocial = useVotiSocial();
const g = useIo(); const g = useIo();
const ultimoMese = usePresenzeUltimoMese(g?.id); const ultimoMese = usePresenzeUltimoMese(g?.id);
const scoutMatches = useScoutMatches();
if (!g) return null; if (!g) return null;
const tecnici = totaliTecnici(scoutMatches, g.id);
const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0; const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0;
return ( return (
@@ -122,9 +126,12 @@ function ProfiloGiocatore() {
} }
label="Presenze di fila" label="Presenze di fila"
/> />
<StatTile valore={g.mediaVoto || "—"} label="Media voto" /> <StatTile valore={tecnici.attacchi} label="Attacchi" />
<StatTile valore={tecnici.muri} label="Muri" />
<StatTile valore={tecnici.ace} label="Ace" />
<StatTile valore={tecnici.punti} label="Punti" hint="totale" />
<StatTile valore={g.mvp} label="MVP" /> <StatTile valore={g.mvp} label="MVP" />
<StatTile valore={g.palloni} label="Turni palloni" /> <StatTile valore={g.mediaVoto || "—"} label="Media voto" />
</div> </div>
<SerieGriglia g={g} /> <SerieGriglia g={g} />
</div> </div>
+5 -3
View File
@@ -384,7 +384,7 @@ function ScoutBoard({
</Select> </Select>
</div> </div>
</div> </div>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-3 gap-2 min-[400px]:grid-cols-4">
{convocati.map((g) => { {convocati.map((g) => {
const attivo = selezionato === g.id; const attivo = selezionato === g.id;
const piu = contaPiuSenzaRicezione(valutazioni, g.id, setCorrente); const piu = contaPiuSenzaRicezione(valutazioni, g.id, setCorrente);
@@ -394,12 +394,14 @@ function ScoutBoard({
type="button" type="button"
onClick={() => setSelezionato(attivo ? null : g.id)} onClick={() => setSelezionato(attivo ? null : g.id)}
className={cn( className={cn(
"rounded-2xl p-2 text-center transition-all active:scale-95", "rounded-2xl p-1.5 text-center transition-all active:scale-95",
attivo ? "bg-accent text-accent-foreground shadow-pop" : "bg-card shadow-card", attivo ? "bg-accent text-accent-foreground shadow-pop" : "bg-card shadow-card",
)} )}
> >
<p className="font-display text-2xl leading-none">{g.numero}</p> <p className="font-display text-2xl leading-none">{g.numero}</p>
<p className="mt-1 text-[13px] font-bold leading-tight">{g.nome}</p> <p className="mt-1 text-[13px] font-bold leading-tight [overflow-wrap:normal]">
{g.nome}
</p>
<p <p
className={cn( className={cn(
"text-[13px] tabular-nums", "text-[13px] tabular-nums",
+2 -2
View File
@@ -23,7 +23,7 @@ import {
useGiocatoriSquadra, useGiocatoriSquadra,
} from "@/lib/giocatori-squadra"; } from "@/lib/giocatori-squadra";
import { useSonoAllenatore } from "@/lib/user-store"; import { useSonoAllenatore } from "@/lib/user-store";
import { totaliSquadra, useScoutMatches } from "@/lib/scout-store"; import { totaliSquadraValutazioni, useScoutMatches } from "@/lib/scout-store";
import { useCsi } from "@/lib/csi"; import { useCsi } from "@/lib/csi";
import { partiteGiocate } from "@/lib/csi-core"; import { partiteGiocate } from "@/lib/csi-core";
import { mediaSquadra, usePagelle } from "@/lib/pagelle"; import { mediaSquadra, usePagelle } from "@/lib/pagelle";
@@ -110,7 +110,7 @@ function Squadra() {
const [criterio, setCriterio] = useState<Criterio>("presenze"); const [criterio, setCriterio] = useState<Criterio>("presenze");
const [filtroAperto, setFiltroAperto] = useState(false); const [filtroAperto, setFiltroAperto] = useState(false);
const obiettivi = useObiettivi(); const obiettivi = useObiettivi();
const team = totaliSquadra(scoutMatches); const team = totaliSquadraValutazioni(scoutMatches);
const mediaPresenze = rosa.length const mediaPresenze = rosa.length
? Math.round( ? Math.round(
(rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100, (rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100,
+20
View File
@@ -520,11 +520,31 @@
border-color: var(--color-border); border-color: var(--color-border);
} }
/*
* Mai scroll orizzontale della pagina (iPhone 360-402 px). `clip` e non `hidden`:
* non crea un contenitore di scroll, quindi `position: sticky` continua a funzionare.
*/
html,
body {
max-width: 100%;
overflow-x: clip;
}
/*
* Gli elementi di una griglia hanno `min-width: auto`: una colonna `1fr` non scende sotto
* la parola più lunga e sfonda la riga. Con `min-width: 0` la colonna resta nel suo posto
* e `overflow-wrap` spezza solo la parola che davvero non ci sta.
*/
.grid > * {
min-width: 0;
}
body { body {
background-color: var(--color-background); background-color: var(--color-background);
color: var(--color-foreground); color: var(--color-foreground);
font-family: var(--font-sans); font-family: var(--font-sans);
line-height: 1.5; line-height: 1.5;
overflow-wrap: break-word;
font-optical-sizing: auto; font-optical-sizing: auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
/* Niente pull-to-refresh accidentale né rimbalzo che scopre il fondo. */ /* Niente pull-to-refresh accidentale né rimbalzo che scopre il fondo. */
+33
View File
@@ -0,0 +1,33 @@
/**
* Check dei requisiti di layout per schermi stretti (360 px): `bun test/unit/layout-stretto.test.ts`.
*
* Non c'è un driver browser (vedi `test/README.md`), quindi si presidiano le due regole che,
* se tolte, fanno tornare la pagina tagliata in larghezza su iPhone: i campi a 16 px (sotto,
* iOS zooma al focus e non torna indietro) e la rete di sicurezza sull'overflow orizzontale.
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { classiInput } from "@/components/crapp/ui-bits";
assert.match(classiInput, /\btext-base\b/, "i campi devono essere a 16px (zoom automatico iOS)");
assert.doesNotMatch(classiInput, /text-\[1[0-5]px\]|text-sm|text-xs/);
const css = readFileSync(new URL("../../src/styles.css", import.meta.url), "utf8");
assert.match(css, /overflow-x:\s*clip/, "html/body devono impedire lo scroll orizzontale");
assert.match(
css,
/\.grid\s*>\s*\*\s*\{\s*min-width:\s*0/,
"gli elementi di griglia devono poter restringersi",
);
const scout = readFileSync(
new URL("../../src/components/crapp/ScoutReport.tsx", import.meta.url),
"utf8",
);
assert.doesNotMatch(
scout,
/min-w-\[\d+rem\]/,
"la tabella scout non deve imporre una larghezza minima",
);
console.log("layout-stretto: ok");
+56
View File
@@ -12,6 +12,8 @@ import {
giocatoriConScout, giocatoriConScout,
totaliPerGiocatore, totaliPerGiocatore,
totaliSquadra, totaliSquadra,
totaliSquadraValutazioni,
totaliTecnici,
type Azione, type Azione,
type Valutazione, type Valutazione,
type ScoutMatch, type ScoutMatch,
@@ -67,6 +69,60 @@ assert.deepEqual(totaliSquadra([match("m1", 3, 0, azioni), match("m2", 3, 1, azi
}); });
assert.deepEqual(totaliSquadra([]), { punti: 0, ace: 0, muri: 0, errori: 0 }); assert.deepEqual(totaliSquadra([]), { punti: 0, ace: 0, muri: 0, errori: 0 });
// --- totaliSquadraValutazioni: solo i + dello scout, fallback sulle azioni ------
const vl = (fondamentale: Valutazione["fondamentale"], segno: "+" | "-", giocatoreId = "g1") =>
({
id: Math.random().toString(),
giocatoreId,
fondamentale,
segno,
set: 1,
ts: 0,
}) as Valutazione;
const conVal = {
...match("v1", 3, 0),
valutazioni: [
vl("battuta", "+"),
vl("battuta", "+", "g2"),
vl("battuta", "-"),
vl("muro", "+"),
vl("muro", "-"),
vl("attacco", "+"),
vl("attacco", "+", "g2"),
vl("attacco", "-"),
vl("ricezione", "+"),
vl("ricezione", "+"),
],
};
assert.deepEqual(totaliSquadraValutazioni([conVal]), { punti: 5, ace: 2, muri: 1 });
assert.deepEqual(
totaliSquadraValutazioni([conVal, conVal]),
{ punti: 10, ace: 4, muri: 2 },
"somma su più partite",
);
assert.deepEqual(
totaliSquadraValutazioni([match("a1", 3, 0, azioni)]),
{ punti: 4, ace: 1, muri: 1 },
"senza valutazioni: contano le azioni del metodo precedente",
);
assert.deepEqual(
totaliSquadraValutazioni([{ ...conVal, azioni }]),
{ punti: 5, ace: 2, muri: 1 },
"con entrambe valgono solo le valutazioni",
);
assert.deepEqual(totaliSquadraValutazioni([]), { punti: 0, ace: 0, muri: 0 });
// --- totaliTecnici: per giocatore, con attacchi -------------------------------
assert.deepEqual(totaliTecnici([conVal], "g1"), { punti: 3, ace: 1, attacchi: 1, muri: 1 });
assert.deepEqual(totaliTecnici([conVal], "g2"), { punti: 2, ace: 1, attacchi: 1, muri: 0 });
assert.deepEqual(totaliTecnici([conVal], "assente"), { punti: 0, ace: 0, attacchi: 0, muri: 0 });
assert.deepEqual(totaliTecnici([conVal, conVal]), { punti: 10, ace: 4, attacchi: 4, muri: 2 });
assert.deepEqual(
totaliTecnici([match("a1", 3, 0, azioni)], "g1"),
{ punti: 3, ace: 1, attacchi: 1, muri: 1 },
"metodo precedente: ace, muro e attacco dalle azioni",
);
// --- giocatoriConScout: solo MVP da scout, presenze separate ----------------- // --- giocatoriConScout: solo MVP da scout, presenze separate -----------------
const base = giocatori.find((g) => g.id === "g1")!; const base = giocatori.find((g) => g.id === "g1")!;
const conScout = giocatoriConScout([match("m1", 3, 0, azioni)], { m1: base.nome }); const conScout = giocatoriConScout([match("m1", 3, 0, azioni)], { m1: base.nome });