10 Commits

Author SHA1 Message Date
04969a45ea docs: aggiunge CHANGELOG v1.0.0 e corregge sequenza rotazione in README.md 2026-02-10 22:23:44 +01:00
3789f25d0d Feat(config): navigazione tastiera per dialog configurazione
Implementa ordine tabindex personalizzato per inserimento formazioni:
- Focus automatico su primo campo all'apertura (Ctrl+M)
- Sequenza Tab: nomi squadre → zone 1-6 home → zone 1-6 guest
- Navigazione ciclica con Tab/Shift+Tab all'interno del dialog
- Bottoni esclusi dalla navigazione (tabindex="-1")
- Handler dedicato per gestione focus e prevenzione navigazione esterna
2026-02-09 23:49:52 +01:00
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
5 changed files with 642 additions and 23 deletions

282
CHANGELOG.md Normal file
View File

@@ -0,0 +1,282 @@
# Changelog
Tutte le modifiche significative a questo progetto sono documentate in questo file.
Il formato si basa su [Keep a Changelog](https://keepachangelog.com/it/1.0.0/),
e questo progetto aderisce al [Versionamento Semantico](https://semver.org/lang/it/).
---
## [1.0.0] - 2026-02-10
Rilascio iniziale di **Segnapunti Anto**, un'applicazione web Progressive Web App (PWA) professionale per il tracciamento in tempo reale dei punteggi durante partite di pallavolo.
### Funzionalità Principali
#### Gestione Partite e Punteggi
- **Tracciamento punti in tempo reale** per entrambe le squadre (casa e ospite)
- **Conteggio automatico dei set** con supporto per modalità al meglio di 3 o al meglio di 5
- **Logica regolamentare completa**:
- Set regolari: primo a 25 punti con almeno 2 di vantaggio
- Set decisivo (tie-break): primo a 15 punti con almeno 2 di vantaggio
- Blocco automatico assegnazione punti al raggiungimento della vittoria
- **Indicatore visivo del servizio** per identificare quale squadra è al servizio
- **Cronologia punti** con striscia visiva per seguire l'andamento della partita
- Possibilità di **incrementare e decrementare punti** e **set**
- **Annullamento punti** con ripristino automatico del servizio precedente
#### Formazioni e Rotazioni
- **Visualizzazione interattiva** della formazione in campo con 6 giocatori per squadra
- **Rotazione automatica regolamentare** quando la squadra conquista il servizio (cambio palla)
- **Configurazione manuale dei numeri di maglia** per ogni giocatore
- **Sistema di cambi giocatori**:
- Dialog dedicato per effettuare i cambi
- Supporto per cambi singoli o multipli
- Tabella IN/OUT con validazione degli input
- Verifica che i numeri di maglia siano numerici
- Scorciatoie da tastiera dedicate per squadra casa e ospite
- **Limitazione cambio palla manuale** solo a inizio set (0-0) per prevenire errori nella rotazione
#### Interfaccia Utente e Personalizzazione
- **Interfaccia fullscreen touch-friendly** ottimizzata per tablet e smartphone
- **Layout responsive** con media queries per schermi piccoli (<768px)
- **Modalità di visualizzazione**:
- Punteggio semplice
- Formazioni complete con posizioni giocatori
- Toggle tra le due modalità con scorciatoia da tastiera
- **Personalizzazione squadre**:
- Configurazione dinamica dei nomi squadre
- Inversione layout orizzontale (scambio posizione casa/ospite)
- Configurazione numeri di maglia giocatori
- **Controlli nascondibili**: possibilità di nascondere/mostrare barra pulsanti e cronologia
- **Stabilizzazione UI**: dimensioni fisse per riquadri punteggio per evitare spostamenti durante gli aggiornamenti
#### Controlli e Accessibilità
- **Controlli da tastiera completi** con scorciatoie dedicate:
- **Squadra Casa**: `Ctrl + ↑/↓` (punti), `Ctrl + →` (set), `Ctrl + C` (cambi)
- **Squadra Ospite**: `Shift + ↑/↓` (punti), `Shift + →` (set), `Shift + C` (cambi)
- **Comandi globali**:
- `Ctrl + ←` (cambio palla, solo a 0-0)
- `Ctrl + M` (configurazione)
- `Ctrl + B` (toggle barra pulsanti)
- `Ctrl + F` (fullscreen)
- `Ctrl + S` (annuncio vocale)
- `Ctrl + Z` (switch visualizzazione)
- **Sintesi vocale** per annunci punteggio in italiano usando Web Speech API
- **Sistema di alert professionale** usando Wave UI per notifiche e conferme
#### Progressive Web App (PWA)
- **Installabile** come app nativa su smartphone, tablet e desktop
- **Funzionamento offline completo** grazie ai Service Worker
- **Auto-update automatico** per ricevere nuovi aggiornamenti senza reinstallazione
- **Display fullscreen** per massimizzare lo spazio visivo
- **Orientamento landscape ottimizzato** per utilizzo su tablet
- **Orientamento sensor landscape** con supporto rotazione 180°
- **Icone PWA personalizzate** (192x192 e 512x512)
- **Prevenzione scroll indesiderato** su mobile (overscroll-behavior)
- **Supporto 100dvh** e position:fixed per layout mobile stabile
- **Blocco scroll** per evitare ricariche accidentali con swipe-down
#### Build e Deployment
- **Build APK Android** tramite Capacitor per distribuzione nativa
- **Setup automatico icone Android** con script dedicato per multi-densità (ldpi, mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi)
- **Configurazione Capacitor** ottimizzata per landscape senza splash screen
- **Output unificato** in `/dist/android` per build Android
- **Base path configurabile** (`/segnap`) per deployment su sottocartelle
### Tecnologie e Dipendenze
#### Stack Tecnologico
- **Vue 3.4.38** - Framework JavaScript reattivo
- **Vite 5.4.10** - Build tool e dev server veloce
- **Wave UI 3.17.0** - Libreria UI components per alert e dialogs
- **NoSleep.js 0.12.0** - Prevenzione standby durante le partite
- **vite-plugin-pwa 0.20.5** - Plugin per generazione PWA
- **Capacitor 6.2.0** - Framework per build app native Android/iOS
#### Ambiente di Sviluppo
- **Node.js 20.x LTS** (consigliato v20.2.0)
- **npm 9.0.0+** per gestione dipendenze
- **NVM support** per gestione versioni Node.js
- **Hot Module Replacement (HMR)** in modalità sviluppo
- **Source maps** per debugging
- **Vue DevTools** supportato
#### Browser Supportati
- **Chrome/Chromium 90+** - Supporto completo (consigliato)
- **Firefox 88+** - Supporto completo
### Build e Comandi
#### Comandi Disponibili
- `npm run dev` - Server di sviluppo con hot-reload su http://localhost:5173
- `npm run build` - Build di produzione ottimizzata in `/dist`
- `npm run preview` - Anteprima locale della build di produzione
#### Output Build
- File statici ottimizzati e minificati
- Service Worker generato automaticamente
- PWA manifest configurato
- Assets con hash per cache busting
- Permessi automatici per cartella dist in ambiente Docker
### Documentazione
- **README.md completo** con:
- Panoramica funzionalità
- Requisiti di sistema dettagliati
- Istruzioni di installazione e setup con NVM
- Guida ai comandi per sviluppo e build
- Tabella completa shortcuts tastiera
- Configurazione PWA documentata
- Spiegazione logica regolamentare pallavolo
- Browser testati e supportati
- **Encoding corretto** per caratteri accentati italiani
### Miglioramenti Architetturali
- **Separazione componenti**: HomePage estratta in componente dedicato
- **Rimozione codice legacy**: eliminati file non utilizzati (HelloWorld.vue)
- **Refactoring CSS**: file style.css organizzato e ottimizzato
- **Gestione servizio migliorata**: variabile booleana `servHome` per gestione cambio servizio
- **Validazione input**: controlli su numeri di maglia e cambi giocatori
- **Gestione errori**: prevenzione incrementi a set concluso senza notifiche spam
### Note di Sviluppo
- **Orientamento sensor landscape**: permette rotazione 180° del dispositivo
- **Fix tentati audio mobile**: implementati ma richiedono ancora debug (WIP)
- Aggiunto supporto @capacitor-community/text-to-speech per audio nativo
- Implementato fallback Web API su desktop, plugin nativo su mobile
- Correzione lingua da 'it_IT' a 'it-IT'
- **Java 17 → 21**: aggiornamento per compatibilità plugin TTS
- **ImageMagick in Dockerfile**: per generazione automatica icone Android
### Configurazione
- **Base path**: `/segnap` (configurabile in vite.config.js)
- **Tema PWA**: background `#eee`, theme color `#ffffff`
- **Display**: fullscreen landscape
- **Manifest name**: app_segnap
- **Short name**: segnap
---
## Architettura e Funzionamento Interno
### Come Funziona l'Applicazione Web
L'applicazione è una **Single Page Application (SPA)** completamente **client-side**:
- **Server**: Serve solo file statici (nessun backend, nessun database)
- **Esecuzione**: Tutto gira nel browser dell'utente
- **Stato**: Memorizzato solo nella RAM del browser (si perde al refresh)
- **Offline**: Funziona completamente offline dopo la prima visita (Service Worker)
#### Build e Deployment
```bash
npm run build
```
1. Vite compila il codice Vue/JavaScript
2. Ottimizza e minifica JavaScript e CSS
3. Genera Service Worker e manifest PWA
4. Output in `/dist` con file statici pronti
**Deploy**: Copia `/dist` su web server statico (nginx, Apache, Vercel, Netlify). Non serve Node.js, PHP o database sul server.
#### Funzionamento Runtime
**Prima visita:**
1. Browser scarica index.html dal server
2. Carica bundle JavaScript e CSS
3. Vue.js inizializza l'applicazione
4. Service Worker cachea tutti gli asset
5. App pronta (nessuna ulteriore chiamata al server)
**Visite successive:**
1. Service Worker serve i file dalla cache locale
2. App si avvia istantaneamente anche senza internet
3. In background verifica se esistono aggiornamenti
4. Aggiornamenti applicati al prossimo refresh
#### Gestione Stato
Tutto lo stato della partita vive nella memoria del browser:
```javascript
data() {
return {
sp: {
punt: { home: 0, guest: 0 },
set: { home: 0, guest: 0 },
servHome: true,
form: { home: ["1","2","3","4","5","6"], guest: ["1","2","3","4","5","6"] }
}
}
}
```
**Limitazioni:**
- Refresh della pagina azzera tutto lo stato
- Nessuna sincronizzazione tra dispositivi
- Nessuno storico partite
**Vantaggi:**
- Privacy totale (nessun dato esce dal dispositivo)
- Velocità massima (nessuna latenza di rete)
- Funzionamento offline completo
**Possibili evoluzioni:**
- Persistenza con localStorage
- Backend con database per multi-dispositivo
- WebSocket per sincronizzazione real-time
### Note Tecniche
**Problemi noti:**
- Audio mobile: sintesi vocale non funziona su alcuni dispositivi Android
- Persistenza: lo stato si perde al refresh della pagina
- Codice legacy: presenza di file HomePage duplicati
**Architettura futura:**
- Stato centralizzato con Pinia/Vuex
- Architettura client-server con WebSocket per display remoto
- Persistenza con localStorage o database
- Testing con Vitest e Cypress
---
## Informazioni sul Progetto
**Nome progetto**: Segnapunti Anto
**Descrizione**: Applicazione web PWA per tracciare i punteggi di partite di pallavolo in tempo reale
**Sviluppato per**: Team Antoniana
**Licenza**: Privata
**Repository**: https://github.com/[username]/segnapunti
---
### Come Leggere Questo Changelog
- **Funzionalità Principali**: nuove caratteristiche aggiunte all'applicazione
- **Tecnologie e Dipendenze**: stack tecnologico e versioni utilizzate
- **Build e Comandi**: istruzioni per compilare ed eseguire il progetto
- **Documentazione**: miglioramenti alla documentazione utente e sviluppatore
- **Miglioramenti Architetturali**: refactoring e ottimizzazioni del codice
- **Note di Sviluppo**: work in progress e problemi noti
---
### Supporto e Contributi
Per segnalare bug o richiedere funzionalità, aprire una issue nel repository del progetto.
Per contribuire al progetto:
1. Fork del repository
2. Creare un branch per la feature (`git checkout -b feature/nome-feature`)
3. Commit delle modifiche (`git commit -m 'Aggiunge nome-feature'`)
4. Push al branch (`git push origin feature/nome-feature`)
5. Aprire una Pull Request
---
**Data primo commit**: 2024
**Data rilascio 1.0.0**: 2026-02-10

View File

@@ -14,12 +14,14 @@ Applicazione web **Progressive Web App (PWA)** per tracciare i punteggi di parti
- Tracciamento punti in tempo reale per entrambe le squadre - Tracciamento punti in tempo reale per entrambe le squadre
- Conteggio automatico dei set (modalità 2/3 o 3/5) - Conteggio automatico dei set (modalità 2/3 o 3/5)
- Indicatore visivo del servizio - Indicatore visivo del servizio
- Blocco incremento punti a set concluso
- Cronologia punti con striscia visiva - Cronologia punti con striscia visiva
- **Formazioni Squadra** - **Formazioni Squadra**
- Visualizzazione interattiva dei 6 giocatori in campo - Visualizzazione interattiva dei 6 giocatori in campo
- Rotazione automatica regolamentare al cambio palla - Rotazione automatica regolamentare al cambio palla
- Configurazione manuale dei numeri di maglia - 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) - Supporto logica pallavolo ufficiale (25 punti + 2 di vantaggio, tie-break a 15 nel set decisivo)
- **Controlli Multimodali** - **Controlli Multimodali**
@@ -152,6 +154,7 @@ Serve i file dalla cartella `/dist` per testare la build prima del deploy.
| `Ctrl + ↑` | Incrementa punti | | `Ctrl + ↑` | Incrementa punti |
| `Ctrl + ↓` | Decrementa punti | | `Ctrl + ↓` | Decrementa punti |
| `Ctrl + →` | Incrementa set | | `Ctrl + →` | Incrementa set |
| `Ctrl + C` | Apri dialog cambi |
### Controlli Tastiera Squadra Guest ### Controlli Tastiera Squadra Guest
@@ -160,6 +163,7 @@ Serve i file dalla cartella `/dist` per testare la build prima del deploy.
| `Shift + ↑` | Incrementa punti | | `Shift + ↑` | Incrementa punti |
| `Shift + ↓` | Decrementa punti | | `Shift + ↓` | Decrementa punti |
| `Shift + →` | Incrementa set | | `Shift + →` | Incrementa set |
| `Shift + C` | Apri dialog cambi |
### Comandi Globali ### Comandi Globali
@@ -248,4 +252,4 @@ Visualizzazione a 6 posizioni standard:
└─────┴─────┴─────┘ └─────┴─────┴─────┘
``` ```
La rotazione avviene in senso orario: 1→2→3→4→5→6→1 La rotazione avviene in senso orario: 1→6→5→4→3→2→1

View File

@@ -1,7 +1,7 @@
<section class="homepage"> <section class="homepage">
<w-dialog v-model="diaNomi.show" :width="600" @close="abilitaTastiSpeciali()"> <w-dialog v-model="diaNomi.show" :width="600" @close="chiudiDialogConfig()">
<w-input v-model="sp.nomi.home" type="text" class="pa3">Nome Home</w-input> <w-input v-model="sp.nomi.home" type="text" class="pa3" tabindex="1">Nome Home</w-input>
<w-input v-model="sp.nomi.guest" type="text" class="pa3">Nome Guest</w-input> <w-input v-model="sp.nomi.guest" type="text" class="pa3" tabindex="2">Nome Guest</w-input>
<w-flex justify-center align-center class="pa3"> <w-flex justify-center align-center class="pa3">
<span class="mr3">Modalità partita:</span> <span class="mr3">Modalità partita:</span>
@@ -9,14 +9,16 @@
@click="modalitaPartita = '2/3'" @click="modalitaPartita = '2/3'"
:bg-color="modalitaPartita === '2/3' ? 'success' : 'grey-light4'" :bg-color="modalitaPartita === '2/3' ? 'success' : 'grey-light4'"
:dark="modalitaPartita === '2/3'" :dark="modalitaPartita === '2/3'"
class="ma1"> class="ma1"
tabindex="-1">
2/3 2/3
</w-button> </w-button>
<w-button <w-button
@click="modalitaPartita = '3/5'" @click="modalitaPartita = '3/5'"
:bg-color="modalitaPartita === '3/5' ? 'success' : 'grey-light4'" :bg-color="modalitaPartita === '3/5' ? 'success' : 'grey-light4'"
:dark="modalitaPartita === '3/5'" :dark="modalitaPartita === '3/5'"
class="ma1"> class="ma1"
tabindex="-1">
3/5 3/5
</w-button> </w-button>
</w-flex> </w-flex>
@@ -27,17 +29,17 @@
<div class="campo-pallavolo"> <div class="campo-pallavolo">
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) --> <!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
<w-flex justify-center class="fila-anteriore"> <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[3]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="6"></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[2]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="5"></w-input>
<w-input v-model="sp.form.home[1]" 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" tabindex="4"></w-input>
</w-flex> </w-flex>
<!-- Linea dei 3 metri --> <!-- Linea dei 3 metri -->
<div class="linea-tre-metri"></div> <div class="linea-tre-metri"></div>
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) --> <!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
<w-flex justify-center class="fila-posteriore"> <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[4]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="7"></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[5]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="8"></w-input>
<w-input v-model="sp.form.home[0]" 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" tabindex="3"></w-input>
</w-flex> </w-flex>
</div> </div>
</div> </div>
@@ -47,27 +49,56 @@
<div class="campo-pallavolo"> <div class="campo-pallavolo">
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) --> <!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
<w-flex justify-center class="fila-anteriore"> <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[3]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="12"></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[2]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="11"></w-input>
<w-input v-model="sp.form.guest[1]" 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" tabindex="10"></w-input>
</w-flex> </w-flex>
<!-- Linea dei 3 metri --> <!-- Linea dei 3 metri -->
<div class="linea-tre-metri"></div> <div class="linea-tre-metri"></div>
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) --> <!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
<w-flex justify-center class="fila-posteriore"> <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[4]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="13"></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[5]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="14"></w-input>
<w-input v-model="sp.form.guest[0]" 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" tabindex="9"></w-input>
</w-flex> </w-flex>
</div> </div>
</div> </div>
</w-flex> </w-flex>
<w-button @click="order = !order" class="ma2">Inverti ordine</w-button> <w-button @click="order = !order" class="ma2" tabindex="-1">Inverti ordine</w-button>
<w-button bg-color="success" @click="diaNomi.show = false" class="ma2"> <w-button bg-color="success" @click="diaNomi.show = false" class="ma2" tabindex="-1">
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">
@@ -85,11 +116,11 @@
<div class="hea guest"> <div class="hea guest">
<span @click="decPunt('guest')" :style="{ 'float': 'right' }"> <span @click="decPunt('guest')" :style="{ 'float': 'right' }">
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
<span class="serv-slot"> <span class="serv-slot">
<img v-show="!sp.servHome" src="/serv.png" width="25" /> <img v-show="!sp.servHome" src="/serv.png" width="25" />
</span> </span>
{{ sp.nomi.guest }} {{ sp.nomi.guest }}
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
</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>
@@ -130,11 +161,11 @@
<div class="hea home"> <div class="hea home">
<span @click="decPunt('home')" :style="{ 'float': 'right' }"> <span @click="decPunt('home')" :style="{ 'float': 'right' }">
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
<span class="serv-slot"> <span class="serv-slot">
<img v-show="sp.servHome" src="/serv.png" width="25" /> <img v-show="sp.servHome" src="/serv.png" width="25" />
</span> </span>
{{ sp.nomi.home }} {{ sp.nomi.home }}
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
</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>
@@ -193,6 +224,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,6 +11,15 @@ 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,
@@ -42,6 +51,28 @@ export default {
computed: { computed: {
isPunteggioZeroZero() { isPunteggioZeroZero() {
return this.sp.punt.home === 0 && this.sp.punt.guest === 0; 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: {
@@ -213,6 +244,152 @@ export default {
apriDialogConfig() { apriDialogConfig() {
this.disabilitaTastiSpeciali(); this.disabilitaTastiSpeciali();
this.diaNomi.show = true; this.diaNomi.show = true;
// Aggiungi gestore Tab per il dialog
this.dialogConfigTabHandler = (e) => {
if (e.key === 'Tab' && this.diaNomi.show) {
e.preventDefault();
e.stopPropagation();
const dialog = document.querySelector('.w-dialog');
if (!dialog) return;
const allInputs = Array.from(dialog.querySelectorAll('input[type="text"]'))
.sort((a, b) => {
const tabA = parseInt(a.closest('[tabindex]')?.getAttribute('tabindex') || '0');
const tabB = parseInt(b.closest('[tabindex]')?.getAttribute('tabindex') || '0');
return tabA - tabB;
});
if (allInputs.length === 0) return;
// Verifica se il focus è già dentro il dialog
const focusInDialog = dialog.contains(document.activeElement);
// Se non è nel dialog o non è in un input, vai al primo
if (!focusInDialog || !allInputs.includes(document.activeElement)) {
allInputs[0].focus();
return;
}
// Navigazione normale tra i campi
const currentIndex = allInputs.indexOf(document.activeElement);
let nextIndex;
if (e.shiftKey) {
nextIndex = currentIndex <= 0 ? allInputs.length - 1 : currentIndex - 1;
} else {
nextIndex = currentIndex >= allInputs.length - 1 ? 0 : currentIndex + 1;
}
if (allInputs[nextIndex]) {
allInputs[nextIndex].focus();
}
}
};
window.addEventListener('keydown', this.dialogConfigTabHandler, true);
// Focus immediato + retry con timeout
this.$nextTick(() => {
const focusFirst = () => {
const dialog = document.querySelector('.w-dialog');
if (dialog) {
const firstInput = dialog.querySelector('input[type="text"]');
if (firstInput) {
firstInput.focus();
firstInput.select();
return true;
}
}
return false;
};
// Prova immediatamente
if (!focusFirst()) {
// Se fallisce, riprova dopo un breve delay
setTimeout(focusFirst, 100);
}
});
},
chiudiDialogConfig() {
if (this.dialogConfigTabHandler) {
window.removeEventListener('keydown', this.dialogConfigTabHandler, true);
this.dialogConfigTabHandler = null;
}
this.abilitaTastiSpeciali();
},
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);
@@ -221,32 +398,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.cambiaPalla() 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

@@ -190,3 +190,75 @@ button:focus-visible {
height: 0; height: 0;
margin: 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;
}