Files
segnapunti/src/components/ControllerPage.vue
T

742 lines
20 KiB
Vue
Raw Normal View History

2026-02-10 00:42:48 +01:00
<template>
<section class="controller-page">
<!-- Barra di stato connessione -->
2026-02-10 00:42:48 +01:00
<div class="conn-bar" :class="{ connected: wsConnected }">
<span class="dot"></span>
{{ wsConnected ? 'Connesso' : 'Connessione...' }}
</div>
<!-- Anteprima punteggio -->
2026-02-10 00:42:48 +01:00
<div class="score-preview">
<div class="team-score" :class="primaSquadra + '-bg'" @click="sendAction({ type: 'incPunt', team: primaSquadra })">
<div class="team-name">{{ state.sp.nomi[primaSquadra] }}</div>
<div class="team-pts">{{ punt[primaSquadra] }}</div>
<div class="team-set">SET {{ set[primaSquadra] }}</div>
<img v-show="primaSquadra === 'home' ? servHome : !servHome" src="/serv.png" class="serv-icon" alt="Servizio" />
2026-02-10 00:42:48 +01:00
</div>
<div class="team-score" :class="secondaSquadra + '-bg'" @click="sendAction({ type: 'incPunt', team: secondaSquadra })">
<div class="team-name">{{ state.sp.nomi[secondaSquadra] }}</div>
<div class="team-pts">{{ punt[secondaSquadra] }}</div>
<div class="team-set">SET {{ set[secondaSquadra] }}</div>
<img v-show="secondaSquadra === 'home' ? servHome : !servHome" src="/serv.png" class="serv-icon" alt="Servizio" />
2026-02-10 00:42:48 +01:00
</div>
</div>
<!-- Riga annulla punto -->
2026-02-10 00:42:48 +01:00
<div class="undo-row">
<button class="btn btn-undo" @click="sendAction({ type: 'decPunt' })">
ANNULLA PUNTO
2026-02-10 00:42:48 +01:00
</button>
</div>
<!-- Pulsanti set -->
2026-02-10 00:42:48 +01:00
<div class="action-row">
<button class="btn btn-set home-bg" @click="sendAction({ type: 'incSet', team: 'home' })">
SET {{ state.sp.nomi.home }}
</button>
<button class="btn btn-set guest-bg" @click="sendAction({ type: 'incSet', team: 'guest' })">
SET {{ state.sp.nomi.guest }}
</button>
</div>
<!-- Controlli principali -->
2026-02-10 00:42:48 +01:00
<div class="controls">
<button class="btn btn-ctrl" @click="sendAction({ type: 'cambiaPalla' })" :disabled="!isPunteggioZeroZero">
Cambio Palla
2026-02-10 00:42:48 +01:00
</button>
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleFormazione' })">
{{ state.visuForm ? 'Punteggio' : 'Formazioni' }}
2026-02-10 00:42:48 +01:00
</button>
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleStriscia' })">
Striscia {{ state.visuStriscia ? 'ON' : 'OFF' }}
2026-02-10 00:42:48 +01:00
</button>
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleOrder' })">
Inverti
2026-02-10 00:42:48 +01:00
</button>
<button class="btn btn-ctrl" @click="speak()">
Voce
2026-02-10 00:42:48 +01:00
</button>
<button class="btn btn-ctrl" @click="openConfig()">
Config
2026-02-10 00:42:48 +01:00
</button>
<button class="btn btn-ctrl" @click="openCambiTeam()">
Cambi
2026-02-10 00:42:48 +01:00
</button>
<button class="btn btn-danger" @click="confirmReset = true">
Reset
2026-02-10 00:42:48 +01:00
</button>
</div>
<!-- Finestra conferma reset -->
2026-02-10 00:42:48 +01:00
<div class="overlay" v-if="confirmReset" @click.self="confirmReset = false">
<div class="dialog">
<div class="dialog-title">Azzero punteggio?</div>
<div class="dialog-buttons">
<button class="btn btn-cancel" @click="confirmReset = false">NO</button>
<button class="btn btn-confirm" @click="doReset()">SI</button>
</div>
</div>
</div>
<!-- Finestra set vinto / partita finita -->
<div class="overlay" v-if="showSetVinto">
<div class="dialog">
<div class="dialog-title">{{ isPartitaFinita ? 'PARTITA FINITA' : 'SET VINTO' }}</div>
<div class="dialog-winner">{{ state.sp.nomi[setVintoTeam] }}</div>
<div class="dialog-subtitle" v-if="!isPartitaFinita">Configura le formazioni per il prossimo set</div>
<div class="dialog-buttons">
<button class="btn btn-cancel" @click="undoUltimoPoint()">INDIETRO</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>
</div>
</div>
</div>
<!-- Finestra configurazione -->
2026-02-10 00:42:48 +01:00
<div class="overlay" v-if="showConfig" @click.self="showConfig = false">
<div class="dialog dialog-config">
<div class="dialog-title">Configurazione</div>
<div class="form-group">
<label>Nome Home</label>
<input type="text" v-model="configData.nomeHome" class="input-field" />
</div>
<div class="form-group">
<label>Nome Guest</label>
<input type="text" v-model="configData.nomeGuest" class="input-field" />
</div>
<div class="form-group">
<label>Modalità partita</label>
<div class="mode-buttons">
<button :class="['btn', 'btn-mode', configData.modalita === '2/3' ? 'active' : '']"
@click="configData.modalita = '2/3'">2/3</button>
<button :class="['btn', 'btn-mode', configData.modalita === '3/5' ? 'active' : '']"
@click="configData.modalita = '3/5'">3/5</button>
<button :class="['btn', 'btn-mode', configData.modalita === 'amichevole' ? 'active' : '']"
@click="configData.modalita = 'amichevole'">Amichevole</button>
2026-02-10 00:42:48 +01:00
</div>
</div>
<div class="form-group">
<label>Formazione Home</label>
<div class="form-grid">
<div class="form-row">
<input type="text" v-model="configData.formHome[3]" class="input-num" />
<input type="text" v-model="configData.formHome[2]" class="input-num" />
<input type="text" v-model="configData.formHome[1]" class="input-num" />
</div>
<div class="form-line"></div>
<div class="form-row">
<input type="text" v-model="configData.formHome[4]" class="input-num" />
<input type="text" v-model="configData.formHome[5]" class="input-num" />
<input type="text" v-model="configData.formHome[0]" class="input-num" />
</div>
</div>
</div>
<div class="form-group">
<label>Formazione Guest</label>
<div class="form-grid">
<div class="form-row">
<input type="text" v-model="configData.formGuest[3]" class="input-num" />
<input type="text" v-model="configData.formGuest[2]" class="input-num" />
<input type="text" v-model="configData.formGuest[1]" class="input-num" />
</div>
<div class="form-line"></div>
<div class="form-row">
<input type="text" v-model="configData.formGuest[4]" class="input-num" />
<input type="text" v-model="configData.formGuest[5]" class="input-num" />
<input type="text" v-model="configData.formGuest[0]" class="input-num" />
</div>
</div>
</div>
<div class="dialog-buttons">
<button class="btn btn-cancel" @click="showConfig = false">Annulla</button>
<button class="btn btn-confirm" @click="saveConfig()">OK</button>
</div>
</div>
</div>
<!-- Selezione squadra per cambi -->
2026-02-10 00:42:48 +01:00
<div class="overlay" v-if="showCambiTeam" @click.self="showCambiTeam = false">
<div class="dialog">
<div class="dialog-title">Scegli squadra</div>
<div class="dialog-buttons">
<button class="btn btn-set home-bg" @click="openCambi('home')">{{ state.sp.nomi.home }}</button>
<button class="btn btn-set guest-bg" @click="openCambi('guest')">{{ state.sp.nomi.guest }}</button>
</div>
</div>
</div>
<!-- Finestra gestione cambi -->
2026-02-10 00:42:48 +01:00
<div class="overlay" v-if="showCambi" @click.self="closeCambi()">
<div class="dialog">
<div class="dialog-title">{{ state.sp.nomi[cambiTeam] }}: CAMBIO</div>
<div class="cambi-container">
<div class="cambio-row" v-for="(c, i) in cambiData" :key="i">
<input type="text" v-model="c.in" placeholder="IN" class="input-num cambi-in-field" />
<span class="cambio-arrow"></span>
<input type="text" v-model="c.out" placeholder="OUT" class="input-num cambi-out-field" />
</div>
</div>
<div v-if="cambiError" class="cambi-error">{{ cambiError }}</div>
2026-02-10 00:42:48 +01:00
<div class="dialog-buttons">
<button class="btn btn-cancel" @click="closeCambi()">Annulla</button>
<button class="btn btn-confirm" :disabled="!cambiValid" @click="confermaCambi()">CONFERMA</button>
</div>
</div>
</div>
</section>
</template>
<script>
import { createWsMixin } from '../wsMixin.js'
2026-02-10 00:42:48 +01:00
export default {
name: "ControllerPage",
mixins: [createWsMixin('controller')],
2026-02-10 00:42:48 +01:00
data() {
return {
confirmReset: false,
showSetVinto: false,
setVintoTeam: null,
2026-02-10 00:42:48 +01:00
showConfig: false,
showCambiTeam: false,
showCambi: false,
cambiTeam: "home",
cambiData: [{ in: "", out: "" }, { in: "", out: "" }],
cambiError: "",
2026-02-10 00:42:48 +01:00
configData: {
nomeHome: "", nomeGuest: "", modalita: "3/5",
2026-02-10 00:42:48 +01:00
formHome: ["1", "2", "3", "4", "5", "6"],
formGuest: ["1", "2", "3", "4", "5", "6"],
},
}
},
computed: {
primaSquadra() { return this.state.order ? 'home' : 'guest' },
secondaSquadra() { return this.state.order ? 'guest' : 'home' },
2026-02-10 00:42:48 +01:00
isPunteggioZeroZero() {
return this.state.sp.striscia.at(-1).ris === ''
2026-02-10 00:42:48 +01:00
},
squadraVincente() {
const { home, guest } = this.punt
const sv = this.set
const totSet = sv.home + sv.guest
const isSetDecisivo = this.state.modalitaPartita === '2/3' ? totSet >= 2 : totSet >= 4
const soglia = isSetDecisivo ? 15 : 25
if (home >= soglia && home - guest >= 2) return 'home'
if (guest >= soglia && guest - home >= 2) return 'guest'
return null
},
isPartitaFinita() {
if (!this.setVintoTeam || this.state.modalitaPartita === 'amichevole') return false
const setsToWin = this.state.modalitaPartita === '2/3' ? 2 : 3
return this.set[this.setVintoTeam] + 1 >= setsToWin
},
2026-02-10 00:42:48 +01:00
cambiValid() {
let hasComplete = false
for (const c of this.cambiData) {
const cin = (c.in || "").trim(), cout = (c.out || "").trim()
if (!cin && !cout) continue
if (!cin || !cout) return false
2026-02-10 00:42:48 +01:00
hasComplete = true
}
return hasComplete
},
2026-02-10 00:42:48 +01:00
},
watch: {
squadraVincente(val) {
if (val && !this.showSetVinto) {
this.setVintoTeam = val
this.showSetVinto = true
}
},
},
2026-02-10 00:42:48 +01:00
methods: {
onWsMessage(msg) {
if (msg.type === 'error') this.showErrorFeedback(msg.message)
},
2026-02-10 00:42:48 +01:00
sendAction(action) {
if (!action?.type) return
if (!this.sendWs({ type: 'action', action })) this.showErrorFeedback('Non connesso al server')
2026-02-10 00:42:48 +01:00
},
showErrorFeedback(message) {
console.error('[Controller] Error:', message)
},
2026-02-10 00:42:48 +01:00
doReset() {
this.sendAction({ type: 'resetta' })
this.confirmReset = false
this.configData = {
nomeHome: this.state.sp.nomi.home,
nomeGuest: this.state.sp.nomi.guest,
modalita: this.state.modalitaPartita,
formHome: ["1", "2", "3", "4", "5", "6"],
formGuest: ["1", "2", "3", "4", "5", "6"],
}
this.showConfig = true
2026-02-10 00:42:48 +01:00
},
undoUltimoPoint() {
this.sendAction({ type: 'decPunt' })
this.showSetVinto = false
},
doNuovoSet() {
this.sendAction({ type: 'nuovoSet', team: this.setVintoTeam })
this.showSetVinto = false
this.configData = {
nomeHome: this.state.sp.nomi.home,
nomeGuest: this.state.sp.nomi.guest,
modalita: this.state.modalitaPartita,
formHome: ["1", "2", "3", "4", "5", "6"],
formGuest: ["1", "2", "3", "4", "5", "6"],
}
this.showConfig = true
},
2026-02-10 00:42:48 +01:00
openConfig() {
this.configData = {
nomeHome: this.state.sp.nomi.home,
nomeGuest: this.state.sp.nomi.guest,
modalita: this.state.modalitaPartita,
formHome: [...this.state.sp.form.home],
formGuest: [...this.state.sp.form.guest],
}
2026-02-10 00:42:48 +01:00
this.showConfig = true
},
saveConfig() {
this.sendAction({ type: 'setNomi', home: this.configData.nomeHome, guest: this.configData.nomeGuest })
this.sendAction({ type: 'setModalita', modalita: this.configData.modalita })
this.sendAction({ type: 'setFormazione', team: 'home', form: this.configData.formHome })
this.sendAction({ type: 'setFormazione', team: 'guest', form: this.configData.formGuest })
this.showConfig = false
},
openCambiTeam() { this.showCambiTeam = true },
2026-02-10 00:42:48 +01:00
openCambi(team) {
this.showCambiTeam = false
this.cambiTeam = team
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
this.cambiError = ""
2026-02-10 00:42:48 +01:00
this.showCambi = true
},
closeCambi() {
this.showCambi = false
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
this.cambiError = ""
2026-02-10 00:42:48 +01:00
},
confermaCambi() {
if (!this.cambiValid) return
this.cambiError = ""
2026-02-10 00:42:48 +01:00
const cambi = this.cambiData
.filter(c => (c.in || "").trim() && (c.out || "").trim())
.map(c => ({ in: c.in.trim(), out: c.out.trim() }))
const formSimulata = this.state.sp.form[this.cambiTeam].map(v => String(v).trim())
for (const cambio of cambi) {
if (!/^\d+$/.test(cambio.in) || !/^\d+$/.test(cambio.out)) {
this.cambiError = "I numeri dei giocatori devono essere cifre"; return
}
if (cambio.in === cambio.out) {
this.cambiError = `Il giocatore ${cambio.in} non può sostituire sé stesso`; return
}
if (formSimulata.includes(cambio.in)) {
this.cambiError = `Il giocatore ${cambio.in} è già in formazione`; return
}
if (!formSimulata.includes(cambio.out)) {
this.cambiError = `Il giocatore ${cambio.out} non è in formazione`; return
}
formSimulata.splice(formSimulata.indexOf(cambio.out), 1, cambio.in)
}
2026-02-10 00:42:48 +01:00
this.sendAction({ type: 'confermaCambi', team: this.cambiTeam, cambi })
this.closeCambi()
},
speak() {
const { home, guest } = this.punt
const text = home + guest === 0 ? "zero a zero"
: home === guest ? `${home} pari`
: this.servHome ? `${home} a ${guest}` : `${guest} a ${home}`
this.sendWs({ type: 'speak', text })
},
},
2026-02-10 00:42:48 +01:00
}
</script>
<style scoped>
.controller-page {
min-height: 100vh;
background: #111;
color: #fff;
padding: 8px;
padding-top: 36px;
font-family: 'Inter', system-ui, sans-serif;
-webkit-user-select: none;
user-select: none;
}
/* Barra stato connessione */
2026-02-10 00:42:48 +01:00
.conn-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
background: #c62828;
color: white;
z-index: 200;
transition: background 0.3s;
}
.conn-bar.connected {
background: #2e7d32;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: white;
}
/* Anteprima punteggio */
2026-02-10 00:42:48 +01:00
.score-preview {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.team-score {
flex: 1;
border-radius: 16px;
padding: 16px 12px;
text-align: center;
cursor: pointer;
position: relative;
transition: transform 0.1s;
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.team-score:active {
transform: scale(0.97);
}
.home-bg {
background: linear-gradient(145deg, #1a1a1a, #333);
border: 2px solid #fdd835;
color: #fdd835;
}
.guest-bg {
background: linear-gradient(145deg, #0d47a1, #1565c0);
border: 2px solid #64b5f6;
color: #fff;
}
.team-name {
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 4px;
}
.team-pts {
font-size: 56px;
font-weight: 900;
line-height: 1;
}
.team-set {
font-size: 13px;
font-weight: 600;
opacity: 0.75;
margin-top: 4px;
}
.serv-icon {
position: absolute;
top: 8px;
right: 8px;
width: 20px;
height: 20px;
}
/* Riga annulla punto */
2026-02-10 00:42:48 +01:00
.undo-row {
margin-bottom: 8px;
}
.btn-undo {
width: 100%;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
color: #ffab91;
padding: 10px;
font-size: 14px;
font-weight: 700;
border-radius: 12px;
cursor: pointer;
}
.btn-undo:active {
background: rgba(255,100,50,0.2);
}
/* Pulsanti set */
2026-02-10 00:42:48 +01:00
.action-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.btn-set {
flex: 1;
padding: 12px;
font-size: 13px;
font-weight: 700;
border-radius: 12px;
cursor: pointer;
border: none;
text-transform: uppercase;
}
.btn-set:active {
transform: scale(0.97);
}
/* Griglia controlli */
2026-02-10 00:42:48 +01:00
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.btn {
border: none;
font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.btn-ctrl {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
color: #e0e0e0;
padding: 14px 8px;
font-size: 14px;
font-weight: 600;
border-radius: 12px;
transition: background 0.15s;
}
.btn-ctrl:active {
background: rgba(255,255,255,0.18);
}
.btn-ctrl:disabled {
opacity: 0.35;
}
.btn-danger {
background: rgba(198, 40, 40, 0.25);
border: 1px solid rgba(239, 83, 80, 0.4);
color: #ff8a80;
2026-02-10 00:42:48 +01:00
padding: 14px 8px;
font-size: 14px;
font-weight: 600;
border-radius: 12px;
}
.btn-danger:active {
background: rgba(198, 40, 40, 0.45);
}
/* Overlay e finestre modali */
2026-02-10 00:42:48 +01:00
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 300;
padding: 16px;
}
.dialog {
background: #1e1e1e;
border-radius: 20px;
padding: 24px;
width: 100%;
max-width: 400px;
border: 1px solid rgba(255,255,255,0.12);
}
.dialog-winner {
font-size: 28px;
font-weight: 900;
text-align: center;
margin-bottom: 8px;
}
.dialog-subtitle {
font-size: 13px;
color: #aaa;
text-align: center;
margin-bottom: 4px;
}
2026-02-10 00:42:48 +01:00
.dialog-config {
max-height: 85vh;
overflow-y: auto;
}
.dialog-title {
font-size: 18px;
font-weight: 800;
text-align: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255,255,255,0.12);
}
.dialog-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
}
.btn-cancel {
flex: 1;
background: rgba(255,255,255,0.08);
color: #aaa;
padding: 12px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
}
.btn-confirm {
flex: 1;
background: #2e7d32;
color: white;
padding: 12px;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
}
.btn-confirm:disabled {
opacity: 0.4;
}
/* Gruppi form */
2026-02-10 00:42:48 +01:00
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: #aaa;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.input-field {
width: 100%;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
color: white;
padding: 10px 14px;
border-radius: 10px;
font-size: 16px;
box-sizing: border-box;
}
.input-field:focus {
outline: none;
border-color: #64b5f6;
}
.input-num {
width: 52px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
color: white;
padding: 8px;
border-radius: 8px;
font-size: 18px;
text-align: center;
box-sizing: border-box;
}
.input-num:focus {
outline: none;
border-color: #64b5f6;
}
/* Griglia formazione */
2026-02-10 00:42:48 +01:00
.form-grid {
background: rgba(205, 133, 63, 0.15);
border: 2px solid rgba(255,255,255,0.15);
border-radius: 12px;
padding: 12px;
}
.form-row {
display: flex;
justify-content: center;
gap: 10px;
padding: 8px 0;
}
.form-line {
border-top: 1px dashed rgba(255,255,255,0.3);
margin: 4px 0;
}
/* Pulsanti modalita */
2026-02-10 00:42:48 +01:00
.mode-buttons {
display: flex;
gap: 8px;
}
.btn-mode {
flex: 1;
padding: 10px;
background: rgba(255,255,255,0.08);
color: #aaa;
border-radius: 10px;
font-size: 16px;
font-weight: 700;
transition: all 0.15s;
}
.btn-mode.active {
background: #2e7d32;
color: white;
}
/* Sezione cambi */
2026-02-10 00:42:48 +01:00
.cambi-container {
display: flex;
flex-direction: column;
gap: 14px;
padding: 8px 0;
}
.cambio-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.cambio-arrow {
font-size: 20px;
font-weight: 700;
color: #aaa;
}
.cambi-in-field {
background: rgba(120, 200, 120, 0.2) !important;
border-color: rgba(120, 200, 120, 0.4) !important;
}
.cambi-out-field {
background: rgba(200, 120, 120, 0.2) !important;
border-color: rgba(200, 120, 120, 0.4) !important;
}
.cambi-error {
color: #ff6b6b;
font-size: 14px;
text-align: center;
padding: 8px 0;
font-weight: 600;
}
2026-02-10 00:42:48 +01:00
</style>