feat: prototipo generazione referto PDF a fine partita

Aggiunge un bottone "REFERTO" nel modal PARTITA FINITA che apre una
nuova scheda con il referto di gara in HTML e avvia il dialogo di
stampa del browser (Salva come PDF).

Il referto include: punteggio per set, formazione di partenza per ogni
set (salvata automaticamente al primo punto), andamento punto per punto
con colori per squadra, data e ora di generazione.

Nota: funzionalità prototipale — layout, contenuti e dettagli sono
ancora da perfezionare sulla base dell'utilizzo reale.
This commit is contained in:
2026-06-20 23:53:33 +02:00
parent e212fb4654
commit ddf68010a4
3 changed files with 156 additions and 0 deletions
+16
View File
@@ -167,6 +167,7 @@
<div class="dialog-subtitle" v-if="!isPartitaFinita">Configura le formazioni per il prossimo set</div> <div class="dialog-subtitle" v-if="!isPartitaFinita">Configura le formazioni per il prossimo set</div>
<div class="dialog-buttons"> <div class="dialog-buttons">
<button class="btn btn-cancel" @click="undoUltimoPoint()">INDIETRO</button> <button class="btn btn-cancel" @click="undoUltimoPoint()">INDIETRO</button>
<button v-if="isPartitaFinita" class="btn btn-secondary" @click="generaReferto(state)">REFERTO</button>
<button v-if="isPartitaFinita" class="btn btn-confirm" @click="showSetVinto = false">CHIUDI</button> <button v-if="isPartitaFinita" class="btn btn-confirm" @click="showSetVinto = false">CHIUDI</button>
<button v-else class="btn btn-confirm" @click="doNuovoSet()">VAI AL SET SUCCESSIVO</button> <button v-else class="btn btn-confirm" @click="doNuovoSet()">VAI AL SET SUCCESSIVO</button>
</div> </div>
@@ -274,6 +275,7 @@
<script> <script>
import { createWsMixin } from '../wsMixin.js' import { createWsMixin } from '../wsMixin.js'
import { generaReferto } from '../referto.js'
export default { export default {
name: "ControllerPage", name: "ControllerPage",
@@ -348,6 +350,7 @@ export default {
window.removeEventListener('orientationchange', this._resizeHandler) window.removeEventListener('orientationchange', this._resizeHandler)
}, },
methods: { methods: {
generaReferto,
onWsMessage(msg) { onWsMessage(msg) {
if (msg.type === 'error') this.showErrorFeedback(msg.message) if (msg.type === 'error') this.showErrorFeedback(msg.message)
}, },
@@ -891,6 +894,19 @@ export default {
font-family: inherit; font-family: inherit;
} }
.btn-secondary {
flex: 1;
background: rgba(255,255,255,0.14);
color: #ccc;
padding: 12px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
border: 1px solid rgba(255,255,255,0.2);
cursor: pointer;
font-family: inherit;
}
.btn-confirm { .btn-confirm {
flex: 1; flex: 1;
background: #2e7d32; background: #2e7d32;
+8
View File
@@ -62,6 +62,13 @@ export function applyAction(state, action) {
const servHome = servizio(s.sp.striscia) const servHome = servizio(s.sp.striscia)
const cambioPalla = (team === "home") !== servHome const cambioPalla = (team === "home") !== servHome
const setCorrente = s.sp.striscia.at(-1)
if (setCorrente.ris === '') {
setCorrente.formInizio = {
home: [...s.sp.form.home],
guest: [...s.sp.form.guest],
}
}
s.sp.striscia.at(-1).ris += team === 'home' ? 'h' : 'g' s.sp.striscia.at(-1).ris += team === 'home' ? 'h' : 'g'
if (cambioPalla) { if (cambioPalla) {
@@ -82,6 +89,7 @@ export function applyAction(state, action) {
const wasCambioPalla = lastScorerShort !== prevServerShort const wasCambioPalla = lastScorerShort !== prevServerShort
currentSet.ris = currentSet.ris.slice(0, -1) currentSet.ris = currentSet.ris.slice(0, -1)
if (currentSet.ris === '') delete currentSet.formInizio
const lastScorer = lastScorerShort === 'h' ? 'home' : 'guest' const lastScorer = lastScorerShort === 'h' ? 'home' : 'guest'
+132
View File
@@ -0,0 +1,132 @@
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
}
export function generaReferto(state) {
const { sp, modalitaPartita } = state
const { nomi, striscia, form } = 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 = new Date().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">${nomi.home}</div>
<div class="giocatori">${formazioneSet.home.map(giocatoreHtml).join('')}</div>
</div>
<div class="form-team">
<div class="form-team-name">${nomi.guest}</div>
<div class="giocatori">${formazioneSet.guest.map(giocatoreHtml).join('')}</div>
</div>
</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 ? ` &nbsp;·&nbsp; vinto da <strong>${nomeVinc}</strong>` : ''
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-header">
Set ${i + 1} &nbsp;—&nbsp; ${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>
<div class="punti-grid">${puntiHtml || '<em style="color:#999;font-size:11px">Nessun punto registrato</em>'}</div>
</div>`
}).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; }
body { font-family: Arial, Helvetica, sans-serif; color: #111; background: #fff; padding: 28px; font-size: 14px; }
@media print { body { padding: 12px; } }
.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; }
.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; }
.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; }
.punto-h { background: #d0e8ff; color: #003a6e; }
.punto-g { background: #ffddc0; color: #6e2700; }
.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-row { display: flex; gap: 40px; }
.form-team { flex: 1; }
.form-team-name { font-weight: bold; font-size: 13px; margin-bottom: 6px; }
.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; }
</style>
</head>
<body>
<div class="header">
<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>
</div>
<div class="risultato">${setVinti.home} ${setVinti.guest}</div>
<div class="modalita-label">set vinti</div>
${setsHtml}
</body>
</html>`
const w = window.open('', '_blank')
w.document.write(html)
w.document.close()
w.print()
}