19 Commits

Author SHA1 Message Date
d3698a506d Merge pull request 'wip-cambi' (#5) from wip-cambi into master
Reviewed-on: #5
2026-01-29 15:08:10 +01:00
1972fd37a4 Fix(cambi): valida input numerici per i cambi
- Blocca conferma se IN/OUT contengono caratteri non numerici
- Mostra un warning con messaggio dedicato
2026-01-29 13:42:10 +01:00
ea4d8ec523 Aggiorna README.md 2026-01-29 13:26:19 +01:00
f190db2161 Feat(cambi): supporto cambi multipli con UI migliorata
Consente di effettuare fino a 2 cambi simultanei per squadra con una nuova
interfaccia utente più compatta e visuale. Gli input IN sono colorati di
verde, gli OUT di rosso, e una freccia indica la direzione del cambio.

La validazione permette cambi parziali (campi vuoti) ma richiede che ogni
cambio inserito sia completo (sia IN che OUT) e che almeno un cambio sia
presente per confermare
2026-01-29 11:08:25 +01:00
9df74a760f feat(cambi): dialog squadra, scorciatoie dedicate e aggiornamenti README
Dettagli:
- richiesta squadra prima del cambio e tabella a riga singola
- scorciatoie Ctrl+C (home) e Shift+C (guest)
- conferma cambio con validazioni e sostituzione in formazione
- README aggiornato con nuovi shortcut e funzione cambi
2026-01-28 18:27:50 +01:00
44617f2f86 Aggiunge shortcut per cambi e aggiorna README.md 2026-01-28 18:14:51 +01:00
33a1534319 feat(cambi): dialog cambi con tabella IN/OUT, validazioni e aggiornamento formazione
- dialog “CAMBI” con tabella 2x2 e intestazioni IN/OUT
- etichette riga con nomi squadre
- conferma solo con righe complete (almeno un cambio)
- sostituzione OUT→IN in formazione con controlli errori
2026-01-28 18:08:18 +01:00
2e66a6cf2a style(ui): simmetria header in modalità Formazione
- Allinea l’ordine degli elementi a destra (punteggio, servizio, nome)
- Mantiene coerenza visiva tra lato sinistro e destro
2026-01-28 16:15:00 +01:00
c923bdbf64 fix(ui): stabilizza header in formazione e cambio servizio
- Riserva spazio fisso per l’icona del servizio per evitare scatti
- Stabilizza la larghezza del punteggio inline in modalità Formazione
- Migliora la coerenza visiva nelle testate home/guest
2026-01-28 16:09:36 +01:00
139dcc9c5b Fix: blocca incrementi a set concluso senza notifiche
- Rimuove il messaggio "set terminato" mantenendo il blocco incrementi
- Semplifica il controllo: stop su checkVittoria() senza flag persistente
- Evita ripetizioni dovute a shortcut (Ctrl/Shift + ↑)
2026-01-28 16:05:11 +01:00
24dda41b0d Aggiunge selezione modalità partita (2/3 o 3/5)
- UI per scegliere la modalità partita nella Home
- Logica set decisivo adattata al best-of selezionato
- README aggiornato con nuove regole e descrizione feature
2026-01-28 14:57:14 +01:00
4cbb5fb48d Corregge ripristino servizio quando si annulla un punto
Risolve il bug dove l'indicatore del servizio (palla) non veniva
ripristinato correttamente quando si tornava indietro nel punteggio.

Implementa uno storico completo che salva lo stato del servizio prima
di ogni punto, permettendo di ripristinare esattamente la situazione
precedente quando si annulla un punto (incluso servizio e rotazioni)
2026-01-28 14:35:40 +01:00
eae5cbf964 Fissa dimensioni riquadri punteggio per evitare spostamenti 2026-01-28 14:30:17 +01:00
2c6416bfe0 Aggiorna README.md 2026-01-28 13:31:42 +01:00
9a808e566d Merge pull request 'wip-formazione' (#3) from wip-formazione into master
Reviewed-on: #3
2026-01-28 12:00:45 +01:00
6c6ac7fc29 Limita il cambio palla solo a inizio set (0-0)
- Aggiunge computed property isPunteggioZeroZero per verificare lo stato del punteggio
- Crea metodo cambiaPalla() con validazione che blocca il cambio se il punteggio non è 0-0
- Disabilita il pulsante cambio palla quando il punteggio non è 0-0
- Mostra notifica di avviso se si tenta il cambio palla durante il set
- Aggiorna scorciatoia tastiera Ctrl+ArrowLeft per usare la stessa validazione
2026-01-26 13:45:22 +01:00
bbe0862241 Implementa rotazione regolamentare con cambio palla
- La formazione ruota solo quando si conquista il servizio (cambio palla)
- Aggiunge array servizioPrecedente per tracciare i cambi palla
- Fix decPunt per annullare correttamente la rotazione
- Fix resetta per pulire lo stack dei servizi precedenti
2026-01-25 17:57:22 +01:00
26d647dce7 Aggiunge configurazione manuale numeri di maglia
- Aggiunge campi input nel dialog configurazione per modificare manualmente i numeri di maglia dei giocatori
- Disegna campi da pallavolo stilizzati (220x220px) con linea dei 3 metri posizionata a 1/3 dall'alto
- Layout corrisponde alla visualizzazione sul campo (ordine rotazione [3,2,1,4,5,0])
- Proporzioni realistiche: zona anteriore 33%, zona posteriore 67%
- Sfondo marrone chiaro e bordi grigi per migliore leggibilità
2026-01-25 17:46:31 +01:00
a72bc1844e Blocca assegnazione punti al raggiungimento della vittoria
Aggiunge controllo che impedisce di assegnare ulteriori punti quando
viene raggiunta la condizione di vittoria (25 punti con 2 di vantaggio
nei set 1-4, 15 punti con 2 di vantaggio nel set decisivo)
2026-01-24 19:00:07 +01:00
4 changed files with 757 additions and 33 deletions

257
README.md
View File

@@ -1,8 +1,255 @@
# Vue 3 + Vite # Segnapunti Anto
# nvm use v20.2.0
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. Applicazione web **Progressive Web App (PWA)** per tracciare i punteggi di partite di pallavolo in tempo reale.
## Recommended IDE Setup ---
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). ## Panoramica
**Segnapunti Anto** è un'applicazione digitale per il tracciamento dei punteggi durante partite di pallavolo, ottimizzata per l'uso su tablet e smartphone. Sviluppata per il team Antoniana, l'app fornisce un'interfaccia fullscreen touch-friendly con supporto offline e controlli da tastiera.
### Funzionalità Principali
- **Gestione Completa Partite**
- Tracciamento punti in tempo reale per entrambe le squadre
- Conteggio automatico dei set (modalità 2/3 o 3/5)
- Indicatore visivo del servizio
- Blocco incremento punti a set concluso
- Cronologia punti con striscia visiva
- **Formazioni Squadra**
- Visualizzazione interattiva dei 6 giocatori in campo
- Rotazione automatica regolamentare al cambio palla
- Configurazione manuale dei numeri di maglia
- Dialog cambi con uno o due cambi (IN → OUT) e validazioni
- Supporto logica pallavolo ufficiale (25 punti + 2 di vantaggio, tie-break a 15 nel set decisivo)
- **Controlli Multimodali**
- Scorciatoie da tastiera complete (vedi sezione [Shortcuts](#shortcuts))
- Sintesi vocale per annunci punteggio in italiano (Web Speech API)
- **Personalizzazione**
- Configurazione dinamica nomi squadre
- Selettore modalità partita: al meglio di 3 o al meglio di 5
- Toggle layout orizzontale (inverti home/guest)
- Modalità visualizzazione: punteggio semplice o formazioni complete
- Nascondi/mostra controlli e cronologia
---
## Requisiti
### Requisiti di Sistema
#### Per Sviluppo
- **Sistema Operativo**: Linux, macOS, Windows (WSL2 consigliato)
- **Node.js**: v20.2.0 o superiore (LTS consigliato)
- **npm**: v9.0.0 o superiore (incluso con Node.js)
- **RAM**: Minimo 2GB, consigliato 4GB
- **Spazio Disco**: ~500MB per dipendenze e build
#### Per Deployment
- **Server Web**: Qualsiasi server statico (nginx, Apache, Vercel, Netlify)
- **HTTPS**: Obbligatorio per Service Worker e PWA (eccetto localhost)
- **Connessione Internet**: Solo per primo caricamento (poi funziona offline)
### Requisiti Browser (Utente Finale)
| Requisito | Dettaglio | Necessità |
|-----------|-----------|-----------|
| **JavaScript ES6+** | Supporto moduli, arrow functions, async/await | Obbligatorio |
| **Service Worker API** | Per funzionalità offline PWA | Obbligatorio |
| **Fullscreen API** | Per modalità schermo intero | Consigliato |
| **Web Speech API** | Per sintesi vocale punteggi | Opzionale |
| **Local Storage** | Per persistenza configurazioni | Consigliato |
### Browser Testati e Supportati
| Browser | Versione Minima | Supporto | Note |
|---------|-----------------|----------|------|
| Chrome/Chromium | 90+ | ✅ Completo | Consigliato per tutte le features |
| Firefox | 88+ | ✅ Completo | Supporto completo PWA e Speech API |
---
## Installazione e Setup
### Prerequisiti
- **Node.js** v20.2.0 (consigliato)
- **npm** o **yarn**
### Installazione con NVM (consigliato)
```bash
# Installa la versione corretta di Node.js
nvm install v20.2.0
nvm use v20.2.0
# Clona il repository
git clone <repository-url>
cd segnapunti
# Installa le dipendenze
npm install
```
---
## Comandi per Sviluppo
### Dev Server
Avvia il server di sviluppo con hot-reload:
```bash
npm run dev
```
L'applicazione sarà disponibile su [http://localhost:5173](http://localhost:5173)
### Modalità Sviluppo
- Hot Module Replacement (HMR) attivo
- Source maps per debugging
- Vue DevTools supportato
- Errori e warnings in console
---
## Comandi per Build
### Build Produzione
Genera i file ottimizzati per il deployment:
```bash
npm run build
```
**Output:**
- Cartella `/dist` con file statici ottimizzati
- Service Worker generato automaticamente
- PWA manifest configurato
- Assets minificati e con hash per cache busting
- Base path: `/segnap` (modificabile in `vite.config.js`)
### Preview Build
Anteprima locale della build di produzione:
```bash
npm run preview
```
Serve i file dalla cartella `/dist` per testare la build prima del deploy.
---
## Shortcuts
### Controlli Tastiera Squadra Home
| Scorciatoia | Azione |
|-------------|--------|
| `Ctrl + ↑` | Incrementa punti |
| `Ctrl + ↓` | Decrementa punti |
| `Ctrl + →` | Incrementa set |
| `Ctrl + C` | Apri dialog cambi |
### Controlli Tastiera Squadra Guest
| Scorciatoia | Azione |
|-------------|--------|
| `Shift + ↑` | Incrementa punti |
| `Shift + ↓` | Decrementa punti |
| `Shift + →` | Incrementa set |
| `Shift + C` | Apri dialog cambi |
### Comandi Globali
| Scorciatoia | Azione |
|-------------|--------|
| `Ctrl + ←` | Cambio palla (servizio) - **solo a 0-0** |
| `Ctrl + M` | Apri configurazione nomi squadre e formazioni |
| `Ctrl + B` | Toggle visibilità barra pulsanti |
| `Ctrl + F` | Attiva/disattiva fullscreen |
| `Ctrl + S` | Annuncio vocale punteggio corrente |
| `Ctrl + Z` | Switch tra visualizzazione formazioni e punteggio |
---
## Configurazione PWA
L'applicazione è configurata come **Progressive Web App** nel file [vite.config.js](vite.config.js):
```javascript
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: "app_segnap",
short_name: "segnap",
description: "Segnapunti standalone.",
background_color: "#eee",
theme_color: '#ffffff',
display: "fullscreen",
orientation: "landscape",
icons: [
{ src: 'segnap-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'segnap-512x512.png', sizes: '512x512', type: 'image/png' }
]
}
})
```
### Caratteristiche PWA
- **Display**: Fullscreen per massimizzare lo spazio visivo
- **Orientamento**: Landscape (orizzontale) ottimizzato per tablet
- **Auto-update**: Service Worker con aggiornamento automatico
- **Offline**: Funzionamento completo senza connessione internet
- **Installabile**: Aggiungibile alla home screen come app nativa
### Installazione PWA
**Android/Desktop (Chrome):**
- Menu → "Installa app" o icona (⊕) nella barra degli indirizzi
**iOS (Safari):**
- Share (□↑) → "Aggiungi a Home"
---
## Logica Regolamentare Pallavolo
### Vittoria Set
- **Set regolari (1-4)**: Primo a 25 punti con almeno 2 di vantaggio
- **Set decisivo**:
- Modalità 2/3: 3° set a 15 punti con almeno 2 di vantaggio
- Modalità 3/5: 5° set a 15 punti con almeno 2 di vantaggio
- **Blocco automatico**: Non consente assegnare punti oltre la vittoria
### Rotazione Formazione
La rotazione avviene **automaticamente** quando:
1. La squadra **conquista il servizio** (cambio palla)
2. Il punteggio è diverso da 0-0
**Limitazione cambio palla manuale:**
- Il cambio manuale del servizio (`Ctrl + ←`) è consentito **solo a 0-0**
- Questa limitazione previene errori nella rotazione delle formazioni
### Formazione in Campo
Visualizzazione a 6 posizioni standard:
```
Rete
┌─────┬─────┬─────┐
│ 4 │ 3 │ 2 │ ← Fila anteriore
├─────┼─────┼─────┤
│ 5 │ 6 │ 1 │ ← Fila posteriore
└─────┴─────┴─────┘
```
La rotazione avviene in senso orario: 1→2→3→4→5→6→1

View File

@@ -1,28 +1,124 @@
<section class="homepage"> <section class="homepage">
<w-dialog v-model="diaNomi.show" :width="500" @close="abilitaTastiSpeciali()"> <w-dialog v-model="diaNomi.show" :width="600" @close="abilitaTastiSpeciali()">
<w-input v-model="sp.nomi.home" type="text" class="pa3">Home</w-input> <w-input v-model="sp.nomi.home" type="text" class="pa3">Nome Home</w-input>
<w-input v-model="sp.nomi.guest" type="text" class="pa3">Guest</w-input> <w-input v-model="sp.nomi.guest" type="text" class="pa3">Nome Guest</w-input>
<w-button @click="order = !order">Inverti ordine</w-button>
<w-button bg-color="success" @click="diaNomi.show = false"> <w-flex justify-center align-center class="pa3">
<span class="mr3">Modalità partita:</span>
<w-button
@click="modalitaPartita = '2/3'"
:bg-color="modalitaPartita === '2/3' ? 'success' : 'grey-light4'"
:dark="modalitaPartita === '2/3'"
class="ma1">
2/3
</w-button>
<w-button
@click="modalitaPartita = '3/5'"
:bg-color="modalitaPartita === '3/5' ? 'success' : 'grey-light4'"
:dark="modalitaPartita === '3/5'"
class="ma1">
3/5
</w-button>
</w-flex>
<w-flex justify-space-around class="pa3">
<div class="campo-config">
<div class="text-bold mb3 text-center">Formazione Home</div>
<div class="campo-pallavolo">
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
<w-flex justify-center class="fila-anteriore">
<w-input v-model="sp.form.home[3]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[2]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[1]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
<!-- Linea dei 3 metri -->
<div class="linea-tre-metri"></div>
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
<w-flex justify-center class="fila-posteriore">
<w-input v-model="sp.form.home[4]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[5]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[0]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
</div>
</div>
<div class="campo-config">
<div class="text-bold mb3 text-center">Formazione Guest</div>
<div class="campo-pallavolo">
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
<w-flex justify-center class="fila-anteriore">
<w-input v-model="sp.form.guest[3]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[2]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[1]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
<!-- Linea dei 3 metri -->
<div class="linea-tre-metri"></div>
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
<w-flex justify-center class="fila-posteriore">
<w-input v-model="sp.form.guest[4]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[5]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[0]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
</div>
</div>
</w-flex>
<w-button @click="order = !order" class="ma2">Inverti ordine</w-button>
<w-button bg-color="success" @click="diaNomi.show = false" class="ma2">
Ok Ok
</w-button> </w-button>
</w-dialog> </w-dialog>
<w-dialog v-model="diaCambiTeam.show" :width="420" @close="abilitaTastiSpeciali()">
<div class="text-bold text-center mb2">Scegli squadra</div>
<w-flex justify-center class="pa3">
<w-button class="ma2" @click="selezionaTeamCambi('home')">{{ sp.nomi.home }}</w-button>
<w-button class="ma2" @click="selezionaTeamCambi('guest')">{{ sp.nomi.guest }}</w-button>
</w-flex>
</w-dialog>
<w-dialog v-model="diaCambi.show" :width="360" @close="chiudiDialogCambi">
<div class="cambi-dialog">
<div class="cambi-title">{{ sp.nomi[diaCambi.team] }}: CAMBIO</div>
<div class="cambi-rows">
<div class="cambi-row">
<w-input v-model="diaCambi[diaCambi.team].cambi[0].in" type="text" class="cambi-input cambi-in"></w-input>
<span class="cambi-arrow"></span>
<w-input v-model="diaCambi[diaCambi.team].cambi[0].out" type="text" class="cambi-input cambi-out"></w-input>
</div>
<div class="cambi-row">
<w-input v-model="diaCambi[diaCambi.team].cambi[1].in" type="text" class="cambi-input cambi-in"></w-input>
<span class="cambi-arrow"></span>
<w-input v-model="diaCambi[diaCambi.team].cambi[1].out" type="text" class="cambi-input cambi-out"></w-input>
</div>
</div>
</div>
<w-flex justify-end class="pa3">
<w-button bg-color="success" :disabled="!cambiConfermabili" @click="confermaCambi">
CONFERMA
</w-button>
</w-flex>
</w-dialog>
<div class="campo"> <div class="campo">
<span v-if="order"> <span v-if="order">
<!-- home guest --> <!-- home guest -->
<div class="hea home"> <div class="hea home">
<span @click="decPunt('home')" :style="{ 'float': 'left' }"> <span @click="decPunt('home')" :style="{ 'float': 'left' }">
{{ sp.nomi.home }} <img v-if="sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.home }}
<span v-if="visuForm">{{ sp.punt.home }}</span> <span class="serv-slot">
<img v-show="sp.servHome" src="/serv.png" width="25" />
</span>
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
</span> </span>
<span @click="incSet('home')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.home }}</span> <span @click="incSet('home')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.home }}</span>
</div> </div>
<div class="hea guest"> <div class="hea guest">
<span @click="decPunt('guest')" :style="{ 'float': 'right' }"> <span @click="decPunt('guest')" :style="{ 'float': 'right' }">
<img v-if="!sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.guest }} <span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
<span v-if="visuForm">{{ sp.punt.guest }}</span> <span class="serv-slot">
<img v-show="!sp.servHome" src="/serv.png" width="25" />
</span>
{{ sp.nomi.guest }}
</span> </span>
<span @click="incSet('guest')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.guest }}</span> <span @click="incSet('guest')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.guest }}</span>
</div> </div>
@@ -40,8 +136,10 @@
</div> </div>
</span> </span>
<span v-else> <span v-else>
<div class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</div> <w-flex class="punteggio-container">
<div class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</div> <w-flex justify-center align-center class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</w-flex>
<w-flex justify-center align-center class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</w-flex>
</w-flex>
</span> </span>
</span> </span>
@@ -50,16 +148,22 @@
<div class="hea guest"> <div class="hea guest">
<span @click="decPunt('guest')" :style="{ 'float': 'left' }"> <span @click="decPunt('guest')" :style="{ 'float': 'left' }">
{{ sp.nomi.guest }} <img v-if="!sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.guest }}
<span v-if="visuForm">{{ sp.punt.guest }}</span> <span class="serv-slot">
<img v-show="!sp.servHome" src="/serv.png" width="25" />
</span>
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
</span> </span>
<span @click="incSet('guest')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.guest }}</span> <span @click="incSet('guest')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.guest }}</span>
</div> </div>
<div class="hea home"> <div class="hea home">
<span @click="decPunt('home')" :style="{ 'float': 'right' }"> <span @click="decPunt('home')" :style="{ 'float': 'right' }">
<img v-if="sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.home }} <span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
<span v-if="visuForm">{{ sp.punt.home }}</span> <span class="serv-slot">
<img v-show="sp.servHome" src="/serv.png" width="25" />
</span>
{{ sp.nomi.home }}
</span> </span>
<span @click="incSet('home')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.home }}</span> <span @click="incSet('home')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.home }}</span>
</div> </div>
@@ -77,8 +181,10 @@
</div> </div>
</span> </span>
<span v-else> <span v-else>
<div class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</div> <w-flex class="punteggio-container">
<div class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</div> <w-flex justify-center align-center class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</w-flex>
<w-flex justify-center align-center class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</w-flex>
</w-flex>
</span> </span>
</span> </span>
@@ -106,7 +212,7 @@
<w-button @click="apriDialogConfig()"> <w-button @click="apriDialogConfig()">
<img src="/gear.png" width="25" /> <img src="/gear.png" width="25" />
</w-button> </w-button>
<w-button @click="sp.servHome = !sp.servHome"> <w-button @click="cambiaPalla" :disabled="!isPunteggioZeroZero">
<img src="/serv.png" width="25" /> <img src="/serv.png" width="25" />
</w-button> </w-button>
<w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta"> <w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta">
@@ -116,6 +222,9 @@
<span v-if="visuForm">PUNTEGGIO</span> <span v-if="visuForm">PUNTEGGIO</span>
<span v-if="!visuForm">FORMAZIONI</span> <span v-if="!visuForm">FORMAZIONI</span>
</w-button> </w-button>
<w-button @click="apriDialogCambi">
CAMBI
</w-button>
<w-button @click="visuStriscia = !visuStriscia"> <w-button @click="visuStriscia = !visuStriscia">
STRISCIA STRISCIA
</w-button> </w-button>

View File

@@ -11,9 +11,19 @@ export default {
home: "", home: "",
guest: "", guest: "",
}, },
diaCambi: {
show: false,
team: "home",
guest: { cambi: [{ in: "", out: "" }, { in: "", out: "" }] },
home: { cambi: [{ in: "", out: "" }, { in: "", out: "" }] },
},
diaCambiTeam: {
show: false,
},
visuForm: false, visuForm: false,
visuButt: true, visuButt: true,
visuStriscia: true, visuStriscia: true,
modalitaPartita: "3/5", // "2/3" o "3/5"
sp: { sp: {
striscia: { home: [0], guest: [0] }, striscia: { home: [0], guest: [0] },
servHome: true, servHome: true,
@@ -24,6 +34,7 @@ export default {
home: ["1", "2", "3", "4", "5", "6"], home: ["1", "2", "3", "4", "5", "6"],
guest: ["1", "2", "3", "4", "5", "6"], guest: ["1", "2", "3", "4", "5", "6"],
}, },
storicoServizio: [], // Stack per tracciare lo stato del servizio prima di ogni punto
}, },
} }
}, },
@@ -37,6 +48,33 @@ export default {
} }
this.abilitaTastiSpeciali(); this.abilitaTastiSpeciali();
}, },
computed: {
isPunteggioZeroZero() {
return this.sp.punt.home === 0 && this.sp.punt.guest === 0;
},
cambiConfermabili() {
const team = this.diaCambi.team;
const cambi = this.diaCambi[team].cambi || [];
let hasComplete = false;
let allValid = true;
cambi.forEach((cambio) => {
const teamIn = (cambio.in || "").trim();
const teamOut = (cambio.out || "").trim();
if (!teamIn && !teamOut) {
return;
}
if (!teamIn || !teamOut) {
allValid = false;
return;
}
hasComplete = true;
});
return allValid && hasComplete;
}
},
methods: { methods: {
closeApp() { closeApp() {
var win = window.open("", "_self"); var win = window.open("", "_self");
@@ -66,6 +104,14 @@ export default {
guest: ["1", "2", "3", "4", "5", "6"], guest: ["1", "2", "3", "4", "5", "6"],
} }
this.sp.striscia = { home: [0], guest: [0] } this.sp.striscia = { home: [0], guest: [0] }
this.sp.storicoServizio = []
},
cambiaPalla() {
if (!this.isPunteggioZeroZero) {
this.$waveui.notify("Cambio palla consentito solo a inizio set (0-0)", "warning");
return;
}
this.sp.servHome = !this.sp.servHome;
}, },
incSet(team) { incSet(team) {
if (this.sp.set[team] == 2) { if (this.sp.set[team] == 2) {
@@ -75,6 +121,17 @@ export default {
} }
}, },
incPunt(team) { incPunt(team) {
// Se il set è già terminato, evita ulteriori incrementi
if (this.checkVittoria()) {
return;
}
// Salva lo stato del servizio PRIMA di modificarlo
this.sp.storicoServizio.push({
servHome: this.sp.servHome,
cambioPalla: (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome)
});
this.sp.punt[team]++; this.sp.punt[team]++;
if (team == 'home') { if (team == 'home') {
this.sp.striscia.home.push(this.sp.punt.home) this.sp.striscia.home.push(this.sp.punt.home)
@@ -83,20 +140,68 @@ export default {
this.sp.striscia.guest.push(this.sp.punt.guest) this.sp.striscia.guest.push(this.sp.punt.guest)
this.sp.striscia.home.push(' ') this.sp.striscia.home.push(' ')
} }
// Ruota la formazione solo se c'è cambio palla (conquista del servizio)
const cambioPalla = (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome);
if (cambioPalla) {
this.sp.form[team].push(this.sp.form[team].shift());
}
this.sp.servHome = (team == "home"); this.sp.servHome = (team == "home");
this.sp.form[team].push(this.sp.form[team].shift()); },
checkVittoria() {
const puntHome = this.sp.punt.home;
const puntGuest = this.sp.punt.guest;
const setHome = this.sp.set.home;
const setGuest = this.sp.set.guest;
const totSet = setHome + setGuest;
// Determina se siamo nel set decisivo in base alla modalità partita
let isSetDecisivo = false;
if (this.modalitaPartita === "2/3") {
// Tie-break al 3° set (quando totSet >= 2)
isSetDecisivo = totSet >= 2;
} else {
// Tie-break al 5° set (quando totSet >= 4)
isSetDecisivo = totSet >= 4;
}
const punteggioVittoria = isSetDecisivo ? 15 : 25;
// Vittoria con punteggio >= 25 (o 15 per set decisivo) e almeno 2 punti di vantaggio
if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) {
return true; // Home ha vinto
}
if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) {
return true; // Guest ha vinto
}
return false;
}, },
decPunt() { decPunt() {
if (this.sp.striscia.home.length > 1) { if (this.sp.striscia.home.length > 1 && this.sp.storicoServizio.length > 0) {
var tmpHome = this.sp.striscia.home.pop() var tmpHome = this.sp.striscia.home.pop()
var tmpGuest = this.sp.striscia.guest.pop() var tmpGuest = this.sp.striscia.guest.pop()
var statoServizio = this.sp.storicoServizio.pop() // Recupera lo stato completo del servizio
if (tmpHome == ' ') { if (tmpHome == ' ') {
this.sp.punt.guest-- this.sp.punt.guest--
this.sp.form.guest.unshift(this.sp.form.guest.pop()); // Ruota indietro solo se c'era stato un cambio palla
if (statoServizio.cambioPalla) {
this.sp.form.guest.unshift(this.sp.form.guest.pop());
}
} else { } else {
this.sp.punt.home-- this.sp.punt.home--
this.sp.form.home.unshift(this.sp.form.home.pop()); // Ruota indietro solo se c'era stato un cambio palla
if (statoServizio.cambioPalla) {
this.sp.form.home.unshift(this.sp.form.home.pop());
}
} }
// Ripristina il servizio allo stato precedente
this.sp.servHome = statoServizio.servHome;
} }
}, },
// decPunt(team) { // decPunt(team) {
@@ -140,6 +245,79 @@ export default {
this.disabilitaTastiSpeciali(); this.disabilitaTastiSpeciali();
this.diaNomi.show = true; this.diaNomi.show = true;
}, },
resettaCambi(team) {
const teams = team ? [team] : ["home", "guest"];
teams.forEach((t) => {
this.diaCambi[t].cambi.forEach((cambio) => {
cambio.in = "";
cambio.out = "";
});
});
},
apriDialogCambi() {
this.disabilitaTastiSpeciali();
this.diaCambiTeam.show = true;
},
apriDialogCambiTeam(team) {
this.disabilitaTastiSpeciali();
this.diaCambi.team = team;
this.resettaCambi(team);
this.diaCambi.show = true;
},
selezionaTeamCambi(team) {
this.diaCambiTeam.show = false;
this.apriDialogCambiTeam(team);
},
chiudiDialogCambi() {
this.diaCambi.show = false;
this.resettaCambi(this.diaCambi.team);
this.abilitaTastiSpeciali();
},
confermaCambi() {
if (!this.cambiConfermabili) {
return;
}
const team = this.diaCambi.team;
const cambi = (this.diaCambi[team].cambi || [])
.map((cambio) => ({
team,
in: (cambio.in || "").trim(),
out: (cambio.out || "").trim(),
}))
.filter((cambio) => cambio.in || cambio.out);
const form = this.sp.form[team].map((val) => String(val).trim());
const formAggiornata = [...form];
for (const cambio of cambi) {
if (!/^\d+$/.test(cambio.in) || !/^\d+$/.test(cambio.out)) {
this.$waveui.notify("Inserisci solo numeri nei campi", "warning");
return;
}
if (cambio.in === cambio.out) {
this.$waveui.notify(`Numero IN e OUT uguali per ${cambio.team}`, "warning");
return;
}
if (formAggiornata.includes(cambio.in)) {
this.$waveui.notify(`Numero ${cambio.in} già presente in formazione ${cambio.team}`, "warning");
return;
}
if (!formAggiornata.includes(cambio.out)) {
this.$waveui.notify(`Numero ${cambio.out} non presente in formazione ${cambio.team}`, "warning");
return;
}
const idx = formAggiornata.findIndex((val) => String(val).trim() === cambio.out);
if (idx !== -1) {
formAggiornata.splice(idx, 1, cambio.in);
}
}
this.sp.form[team] = formAggiornata;
this.chiudiDialogCambi();
},
disabilitaTastiSpeciali() { disabilitaTastiSpeciali() {
window.removeEventListener("keydown", this.funzioneTastiSpeciali); window.removeEventListener("keydown", this.funzioneTastiSpeciali);
}, },
@@ -147,32 +325,82 @@ export default {
window.addEventListener("keydown", this.funzioneTastiSpeciali); window.addEventListener("keydown", this.funzioneTastiSpeciali);
}, },
funzioneTastiSpeciali(e) { funzioneTastiSpeciali(e) {
e.preventDefault(); if (this.diaNomi.show || this.diaCambi.show || this.diaCambiTeam.show) {
return;
}
const target = e.target;
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
const elements = [target, ...path].filter(Boolean);
const isTypingField = elements.some((el) => {
if (!el || !el.tagName) {
return false;
}
const tag = String(el.tagName).toLowerCase();
if (tag === "input" || tag === "textarea") {
return true;
}
if (el.isContentEditable) {
return true;
}
if (el.classList && (el.classList.contains("w-input") || el.classList.contains("w-textarea"))) {
return true;
}
const contentEditable = el.getAttribute && el.getAttribute("contenteditable");
return contentEditable === "true";
});
if (isTypingField) {
return;
}
let handled = false;
if (e.ctrlKey && e.key == "m") { if (e.ctrlKey && e.key == "m") {
this.diaNomi.show = true this.diaNomi.show = true
handled = true;
} else if (e.ctrlKey && e.key == "b") { } else if (e.ctrlKey && e.key == "b") {
this.visuButt = !this.visuButt this.visuButt = !this.visuButt
handled = true;
} else if (e.ctrlKey && e.key == "f") { } else if (e.ctrlKey && e.key == "f") {
document.documentElement.requestFullscreen(); document.documentElement.requestFullscreen();
handled = true;
} else if (e.ctrlKey && e.key == "s") { } else if (e.ctrlKey && e.key == "s") {
this.speak(); this.speak();
handled = true;
} else if (e.ctrlKey && e.key == "z") { } else if (e.ctrlKey && e.key == "z") {
this.visuForm = !this.visuForm this.visuForm = !this.visuForm
handled = true;
} else if (e.ctrlKey && e.key == "ArrowUp") { } else if (e.ctrlKey && e.key == "ArrowUp") {
this.incPunt("home") this.incPunt("home")
handled = true;
} else if (e.ctrlKey && e.key == "ArrowDown") { } else if (e.ctrlKey && e.key == "ArrowDown") {
this.decPunt("home") this.decPunt("home")
handled = true;
} else if (e.ctrlKey && e.key == "ArrowRight") { } else if (e.ctrlKey && e.key == "ArrowRight") {
this.incSet("home") this.incSet("home")
handled = true;
} else if (e.shiftKey && e.key == "ArrowUp") { } else if (e.shiftKey && e.key == "ArrowUp") {
this.incPunt("guest") this.incPunt("guest")
handled = true;
} else if (e.shiftKey && e.key == "ArrowDown") { } else if (e.shiftKey && e.key == "ArrowDown") {
this.decPunt("guest") this.decPunt("guest")
handled = true;
} else if (e.shiftKey && e.key == "ArrowRight") { } else if (e.shiftKey && e.key == "ArrowRight") {
this.incSet("guest") this.incSet("guest")
handled = true;
} else if (e.ctrlKey && e.key == "ArrowLeft") { } else if (e.ctrlKey && e.key == "ArrowLeft") {
this.sp.servHome = !this.sp.servHome this.cambiaPalla()
handled = true;
} else if (e.ctrlKey && (e.key == "c" || e.key == "C")) {
this.apriDialogCambiTeam("home")
handled = true;
} else if (e.shiftKey && (e.key == "c" || e.key == "C")) {
this.apriDialogCambiTeam("guest")
handled = true;
} else { return false } } else { return false }
if (handled) {
e.preventDefault();
}
} }
} }
} }

View File

@@ -58,6 +58,19 @@ button:focus-visible {
padding-right: 10px; padding-right: 10px;
border-radius: 5px; border-radius: 5px;
} }
.score-inline {
display: inline-block;
min-width: 3ch;
text-align: center;
}
.serv-slot {
display: inline-flex;
width: 25px;
height: 25px;
align-items: center;
justify-content: center;
vertical-align: middle;
}
.tal { .tal {
text-align: left; text-align: left;
} }
@@ -80,8 +93,23 @@ button:focus-visible {
float: left; float: left;
width: 50%; width: 50%;
} }
.punteggio-container {
width: 100%;
height: 100%;
display: flex;
}
.punt { .punt {
font-size: 60vh; font-size: 60vh;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
min-height: 50vh;
min-width: 50vw;
max-width: 50vw;
overflow: hidden;
box-sizing: border-box;
} }
.form { .form {
font-size: 5vh; font-size: 5vh;
@@ -122,3 +150,115 @@ button:focus-visible {
color: blue; color: blue;
border-radius: 5px; border-radius: 5px;
} }
.campo-config {
display: flex;
flex-direction: column;
align-items: center;
}
.campo-pallavolo {
border: 3px solid #999;
background-color: rgba(205, 133, 63, 0.25);
position: relative;
width: 220px;
height: 220px;
display: flex;
flex-direction: column;
padding: 0;
}
.fila-anteriore {
height: 33.33%;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.fila-posteriore {
height: 66.67%;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.linea-tre-metri {
border-top: 2px solid #666;
width: 100%;
height: 0;
margin: 0;
}
.cambi-rows {
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px 0;
}
.cambi-dialog {
padding: 8px 6px 2px;
}
.cambi-title {
text-align: center;
font-weight: 800;
letter-spacing: 0.5px;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
.cambi-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.cambi-arrow {
font-weight: 700;
font-size: 18px;
line-height: 1;
padding: 6px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
white-space: nowrap;
}
.cambi-input {
min-width: 48px;
max-width: 64px;
}
.cambi-input input,
.cambi-input .w-input__input,
.cambi-input .w-input__field {
border: 2px solid rgba(255, 255, 255, 0.35);
border-radius: 8px;
padding: 6px 10px;
color: #000;
text-align: center;
box-sizing: border-box;
}
.cambi-in input,
.cambi-in .w-input__input,
.cambi-in .w-input__field {
background: rgba(120, 200, 120, 0.4);
}
.cambi-out input,
.cambi-out .w-input__input,
.cambi-out .w-input__field {
background: rgba(200, 120, 120, 0.4);
}
.cambi-input input:focus,
.cambi-input .w-input__input:focus,
.cambi-input .w-input__field:focus {
border-color: rgba(255, 255, 255, 0.7);
outline: none;
}