style(referto): riprendi il layout del modulo ufficiale FIPAV

Cornice nera, barre teal "SET N" per set, badge A/B per squadre e
box risultato finale in stile tabella, per avvicinare il referto
stampabile all'aspetto del referto cartaceo FIPAV mantenendo solo
i dati che l'app traccia realmente.
This commit is contained in:
2026-07-03 11:31:55 +02:00
parent f6f3dd11d4
commit 5bdd8ad986
3 changed files with 97 additions and 46 deletions
+80 -28
View File
@@ -87,17 +87,31 @@ export function buildRefertoHtml(state, now = new Date()) {
return `
<div class="set-section">
<div class="set-barra"><span>SET</span><span class="set-numero">${i + 1}</span></div>
<div class="set-corpo">
<div class="set-header">
Set ${i + 1} &nbsp;—&nbsp; ${nomi.home} <strong>${h}</strong> · <strong>${g}</strong> ${nomi.guest}${etichettaVinc}
${nomi.home} <strong>${h}</strong> · <strong>${g}</strong> ${nomi.guest}${etichettaVinc}
</div>
<div class="form-inizio">
<div class="form-inizio-label">Formazione di partenza</div>
${formazioneHtml(s.formInizio)}
</div>${eventiHtml(s)}
<div class="punti-grid">${puntiHtml || '<em style="color:#999;font-size:11px">Nessun punto registrato</em>'}</div>
</div>
</div>`
}).join('')
const risultatoSetHtml = setReali.map((s, i) => {
let h = 0, g = 0
for (const c of s.ris) c === 'h' ? h++ : g++
const vinc = vincitoreSet(s)
return `<tr>
<td class="ris-set-n">${i + 1}</td>
<td class="ris-set-p ${vinc === 'h' ? 'ris-vinc' : ''}">${h}</td>
<td class="ris-set-p ${vinc === 'g' ? 'ris-vinc' : ''}">${g}</td>
</tr>`
}).join('')
const html = `<!DOCTYPE html>
<html lang="it">
<head>
@@ -105,55 +119,93 @@ export function buildRefertoHtml(state, now = new Date()) {
<title>Referto — ${nomi.home} vs ${nomi.guest}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, Helvetica, sans-serif; color: #111; background: #fff; padding: 28px; font-size: 14px; }
@media print { body { padding: 12px; } }
:root { --teal: #23b8dc; --teal-dark: #0f8faf; }
body { font-family: Arial, Helvetica, sans-serif; color: #111; background: #fff; padding: 20px; font-size: 14px; }
@media print { body { padding: 8px; } }
.header { text-align: center; border-bottom: 2px solid #111; padding-bottom: 12px; margin-bottom: 20px; }
.titolo { font-size: 18px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; }
.meta { font-size: 12px; color: #666; margin-top: 5px; }
.foglio { border: 3px solid #111; padding: 14px; }
.squadre-row { display: flex; justify-content: space-around; align-items: center; margin: 18px 0 6px; }
.nome-sq { font-size: 22px; font-weight: bold; text-align: center; max-width: 40%; }
.vs { font-size: 16px; color: #999; }
.risultato { text-align: center; font-size: 36px; font-weight: bold; letter-spacing: 6px; margin-bottom: 4px; }
.modalita-label { text-align: center; font-size: 12px; color: #888; margin-bottom: 22px; }
.header { display: flex; align-items: center; justify-content: space-between; gap: 16px; border: 2px solid #111; padding: 10px 16px; margin-bottom: 14px; }
.titolo { font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; }
.meta { font-size: 11px; color: #333; margin-top: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
.set-section { margin-bottom: 12px; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; }
.set-header { background: #f5f5f5; padding: 7px 12px; font-size: 13px; border-bottom: 1px solid #ddd; }
.punti-grid { display: flex; flex-wrap: wrap; gap: 3px; padding: 8px 10px; }
.punto { display: inline-block; padding: 2px 5px; border-radius: 3px; font-size: 11px; font-family: 'Courier New', monospace; white-space: nowrap; }
.squadre-row { display: flex; align-items: center; gap: 14px; }
.badge-ab { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; border: 2px solid #111; font-weight: bold; font-size: 12px; flex-shrink: 0; }
.nome-sq { font-size: 18px; font-weight: bold; }
.vs { font-size: 12px; color: #666; font-weight: bold; }
.risultato-riepilogo { display: flex; align-items: center; gap: 10px; }
.risultato { font-size: 30px; font-weight: bold; letter-spacing: 3px; }
.modalita-label { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 0.5px; }
.set-section { display: flex; margin-bottom: 10px; border: 2px solid #111; }
.set-barra { flex: 0 0 26px; background: var(--teal); color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; border-right: 2px solid #111; }
.set-barra span:first-child { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: bold; font-size: 11px; letter-spacing: 2px; }
.set-numero { font-weight: bold; font-size: 15px; }
.set-corpo { flex: 1; min-width: 0; }
.set-header { background: #eaf7fb; padding: 6px 12px; font-size: 13px; border-bottom: 1px solid #111; }
.punti-grid { display: flex; flex-wrap: wrap; gap: 0; padding: 0; border-top: 1px solid #111; }
.punto { display: inline-flex; align-items: center; justify-content: center; min-width: 34px; padding: 3px 4px; font-size: 11px; font-family: 'Courier New', monospace; white-space: nowrap; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; }
.punto-h { background: #d0e8ff; color: #003a6e; }
.punto-g { background: #ffddc0; color: #6e2700; }
.eventi-set { padding: 8px 10px; border-bottom: 1px solid #eee; }
.eventi-label { font-size: 11px; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; color: #888; margin-bottom: 5px; }
.eventi-set { padding: 6px 12px; border-bottom: 1px solid #111; }
.eventi-label { font-size: 10px; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; color: #0f8faf; margin-bottom: 4px; }
.eventi-lista { list-style: none; }
.evento { font-size: 12px; padding: 2px 0; }
.evento { font-size: 12px; padding: 1px 0; }
.form-inizio { padding: 8px 10px; border-bottom: 1px solid #eee; background: #fafafa; }
.form-inizio-label { font-size: 11px; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; color: #888; margin-bottom: 7px; }
.form-inizio { padding: 6px 12px; border-bottom: 1px solid #111; background: #fafafa; }
.form-inizio-label { font-size: 10px; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; color: #0f8faf; margin-bottom: 6px; }
.form-row { display: flex; gap: 40px; }
.form-team { flex: 1; }
.form-team-name { font-weight: bold; font-size: 13px; margin-bottom: 6px; }
.form-team-name { font-weight: bold; font-size: 12px; margin-bottom: 5px; }
.giocatori { display: flex; gap: 5px; flex-wrap: wrap; }
.giocatore { background: #f0f0f0; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; border: 1px solid #ccc; }
.giocatore { background: #fff; border-radius: 50%; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 11px; border: 2px solid #111; }
.finale { display: flex; gap: 14px; align-items: flex-start; margin-top: 4px; }
.finale-box { border: 2px solid #111; flex: 0 0 220px; }
.finale-box-header { background: var(--teal); color: #fff; text-align: center; font-weight: bold; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; padding: 5px; }
.finale-tabella { width: 100%; border-collapse: collapse; font-size: 12px; }
.finale-tabella th, .finale-tabella td { border: 1px solid #111; padding: 4px 8px; text-align: center; }
.finale-tabella th { background: #eaf7fb; font-size: 10px; text-transform: uppercase; }
.ris-vinc { font-weight: bold; background: #d0e8ff; }
.finale-vince { padding: 8px; text-align: center; font-size: 14px; font-weight: bold; border-top: 2px solid #111; }
</style>
</head>
<body>
<div class="foglio">
<div class="header">
<div>
<div class="titolo">Referto di Gara</div>
<div class="meta">${dataOra} &nbsp;·&nbsp; Modalità: ${modalitaPartita}</div>
</div>
<div class="squadre-row">
<div class="nome-sq">${nomi.home}</div>
<div class="vs">vs</div>
<div class="nome-sq">${nomi.guest}</div>
<span class="badge-ab">A</span>
<span class="nome-sq">${nomi.home}</span>
<span class="vs">vs</span>
<span class="nome-sq">${nomi.guest}</span>
<span class="badge-ab">B</span>
</div>
</div>
<div class="risultato">${setVinti.home} ${setVinti.guest}</div>
<div class="modalita-label">set vinti</div>
${setsHtml}
<div class="finale">
<div class="finale-box">
<div class="finale-box-header">Risultato finale</div>
<table class="finale-tabella">
<thead><tr><th>Set</th><th>A</th><th>B</th></tr></thead>
<tbody>${risultatoSetHtml}</tbody>
</table>
<div class="finale-vince">Vince ${setVinti.home >= setVinti.guest ? nomi.home : nomi.guest} &nbsp;${setVinti.home} ${setVinti.guest}</div>
</div>
<div class="risultato-riepilogo">
<div class="risultato">${setVinti.home} ${setVinti.guest}</div>
<div class="modalita-label">set vinti</div>
</div>
</div>
</div>
</body>
</html>`
+1 -2
View File
@@ -40,8 +40,7 @@ test.describe('Referto di fine partita', () => {
// Verifica il contenuto del referto
const body = popup.locator('body');
await expect(body).toContainText('Referto di Gara');
await expect(body).toContainText('Set 1');
await expect(body).toContainText('Set 2');
await expect(popup.locator('.set-numero')).toHaveText(['1', '2']);
// Home ha vinto 2 set a 0
await expect(body).toContainText('2 0');
});
+4 -4
View File
@@ -21,10 +21,10 @@ describe('buildRefertoHtml (referto.js)', () => {
{ serv: 'h', ris: 'h'.repeat(25) + 'g'.repeat(18), vinc: 'h' },
]
const html = buildRefertoHtml(statoConSet(striscia), NOW)
// due set reali → "Set 1" e "Set 2", mai "Set 3"
expect(html).toContain('Set 1')
expect(html).toContain('Set 2')
expect(html).not.toContain('Set 3')
// due set reali → numeri "1" e "2" nella barra SET, mai "3"
expect(html).toContain('<span class="set-numero">1</span>')
expect(html).toContain('<span class="set-numero">2</span>')
expect(html).not.toContain('<span class="set-numero">3</span>')
})
it('calcola il punteggio finale di ogni set dalla ris', () => {