Files
segnapunti/src/referto.js
T
davide 35c49247a8 style(referto): allinea i colori squadra a quelli del segnapunti live
Punti in striscia e nomi in "Formazione di partenza" ora usano lo
stesso giallo (#f5c518) per la squadra di casa e blu (#2196f3) per
gli ospiti già usati in display/controller, invece di una palette
scollegata dal resto dell'app.
2026-07-15 10:25:57 +02:00

235 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function vincitoreSet(s) {
if (s.vinc === 'h' || s.vinc === 'g') return s.vinc
let h = 0, g = 0
for (const c of s.ris) c === 'h' ? h++ : g++
if (h > g) return 'h'
if (g > h) return 'g'
return null
}
// Costruisce l'HTML del referto (funzione pura, testabile).
// `now` è iniettabile per rendere deterministica la data nei test.
export function buildRefertoHtml(state, now = new Date()) {
const { sp, modalitaPartita } = state
const { nomi, striscia } = sp
const setReali = striscia.filter(s => !s._phantom)
const setVinti = { home: 0, guest: 0 }
for (const s of setReali) {
const v = vincitoreSet(s)
if (v === 'h') setVinti.home++
else if (v === 'g') setVinti.guest++
}
const dataOra = now.toLocaleString('it-IT', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})
const giocatoreHtml = (n) => `<div class="giocatore">${n}</div>`
const formazioneHtml = (formazioneSet) => {
if (!formazioneSet) return '<em style="color:#999;font-size:11px">non disponibile</em>'
return `
<div class="form-row">
<div class="form-team">
<div class="form-team-name form-team-name-h">${nomi.home}</div>
<div class="giocatori">${formazioneSet.home.map(giocatoreHtml).join('')}</div>
</div>
<div class="form-team">
<div class="form-team-name form-team-name-g">${nomi.guest}</div>
<div class="giocatori">${formazioneSet.guest.map(giocatoreHtml).join('')}</div>
</div>
</div>`
}
const nomeTeam = (team) => team === 'home' ? nomi.home : nomi.guest
const eventiHtml = (s) => {
const eventi = [
...(s.timeouts ?? []).map(t => ({ tipo: 'timeout', ...t })),
...(s.cambi ?? []).map(c => ({ tipo: 'cambio', ...c })),
].sort((a, b) => (a.punteggio.home + a.punteggio.guest) - (b.punteggio.home + b.punteggio.guest))
if (eventi.length === 0) return ''
const righe = eventi.map(e => {
const sul = `sul ${e.punteggio.home}-${e.punteggio.guest}`
const testo = e.tipo === 'timeout'
? `Time out <strong>${nomeTeam(e.team)}</strong> ${sul}`
: `Cambio <strong>${nomeTeam(e.team)}</strong>: entra ${e.in} per ${e.out} ${sul}`
return `<li class="evento evento-${e.tipo}">${testo}</li>`
}).join('')
return `
<div class="eventi-set">
<div class="eventi-label">Cambi e time out</div>
<ul class="eventi-lista">${righe}</ul>
</div>`
}
const setsHtml = setReali.map((s, i) => {
let h = 0, g = 0
const punti = []
for (const c of s.ris) {
c === 'h' ? h++ : g++
punti.push({ chi: c, h, g })
}
const vinc = vincitoreSet(s)
const nomeVinc = vinc === 'h' ? nomi.home : vinc === 'g' ? nomi.guest : ''
const etichettaVinc = nomeVinc ? `<span class="set-vinc">vinto da <strong>${nomeVinc}</strong></span>` : ''
const servIcon = '<img src="/serv-nero.png" class="serv-icon" alt="Servizio">'
const servHomeIcon = s.serv === 'h' ? servIcon : ''
const servGuestIcon = s.serv === 'g' ? servIcon : ''
const puntiHtml = punti.map(p =>
`<span class="punto punto-${p.chi}">${p.h}-${p.g}</span>`
).join('')
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">
<span>${servHomeIcon}${nomi.home} <strong>${h}</strong> · <strong>${g}</strong> ${nomi.guest}${servGuestIcon}</span>
${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>
<meta charset="UTF-8">
<title>Referto — ${nomi.home} vs ${nomi.guest}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
: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; } }
.foglio { border: 3px solid #111; padding: 14px; }
.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; }
.squadre-row { display: flex; align-items: center; gap: 14px; }
.badge-ab { display: inline-flex; align-items: center; justify-content: center; height: 22px; padding: 0 8px; border-radius: 11px; 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; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.set-vinc { flex-shrink: 0; }
.serv-icon { width: 12px; height: 12px; vertical-align: middle; margin: 0 3px; }
.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: #f5c518; color: #111; }
.punto-g { background: #2196f3; color: #fff; }
.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: 1px 0; }
.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: 12px; margin-bottom: 5px; }
.form-team-name-h { color: #f5c518; }
.form-team-name-g { color: #2196f3; }
.giocatori { display: flex; gap: 5px; flex-wrap: wrap; }
.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">
<span class="badge-ab">Casa</span>
<span class="nome-sq">${nomi.home}</span>
<span class="vs">vs</span>
<span class="nome-sq">${nomi.guest}</span>
<span class="badge-ab">Ospiti</span>
</div>
</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>${nomi.home}</th><th>${nomi.guest}</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>`
return html
}
// Apre il referto in una nuova scheda e avvia la stampa (effetto collaterale,
// solo browser). La generazione dell'HTML è delegata a buildRefertoHtml.
export function generaReferto(state) {
const html = buildRefertoHtml(state)
const w = window.open('', '_blank')
w.document.write(html)
w.document.close()
// Aspetta il caricamento delle immagini (icona servizio) prima di stampare,
// altrimenti la stampa parte con risorse ancora in caricamento e appaiono vuote.
w.addEventListener('load', () => w.print())
}