fix: correzioni critiche e checklist test manuali
CORREZIONI:
- Badge confrontato ESATTAMENTE come stringa (rimosso .lstrip("0"))
- Success modal si chiude quando arriva nuovo badge (fix dipendenze useCallback)
- Polling ogni 30s per invalidare sessione se server riparte
- Area carosello allargata per testi lunghi (es. russo)
DOCUMENTAZIONE:
- API_SPECIFICATION.md aggiornata: badge come stringa esatta
- Creata TEST_CHECKLIST.md con 22 test manuali
- Aggiornati piani backend e frontend
Badge sono STRINGHE, non numeri:
- "0008988288" != "8988288" (zeri iniziali significativi)
This commit is contained in:
158
TEST_CHECKLIST.md
Normal file
158
TEST_CHECKLIST.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# ✅ Checklist Test Manuali - Focolari Voting System
|
||||||
|
|
||||||
|
## Pre-requisiti
|
||||||
|
- [ ] Backend avviato con `./dev.sh server`
|
||||||
|
- [ ] Browser aperto su `http://localhost:8000`
|
||||||
|
- [ ] Lettore RFID collegato (o usa tastiera per simulare)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Test Login Validatore
|
||||||
|
|
||||||
|
### T1: Login con password corretta
|
||||||
|
1. Passa un badge qualsiasi (es. `ò0008988288_`)
|
||||||
|
2. Inserisci password: `focolari`
|
||||||
|
3. **Atteso:** Accesso al varco attivo ✅
|
||||||
|
|
||||||
|
### T2: Login con password errata
|
||||||
|
1. Passa un badge qualsiasi
|
||||||
|
2. Inserisci password sbagliata
|
||||||
|
3. **Atteso:** Messaggio errore rosso, rimane su schermata password ❌
|
||||||
|
|
||||||
|
### T3: Annulla login
|
||||||
|
1. Passa un badge
|
||||||
|
2. Clicca "Annulla"
|
||||||
|
3. **Atteso:** Torna a "Passa badge validatore" ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 Test Anagrafica Utenti
|
||||||
|
|
||||||
|
### T4: Badge ammesso trovato
|
||||||
|
1. Login come validatore
|
||||||
|
2. Passa badge `ò0008988288_` (Marco Bianchi)
|
||||||
|
3. **Atteso:** Card verde con "Utente ammesso all'ingresso" ✅
|
||||||
|
|
||||||
|
### T5: Badge NON ammesso trovato
|
||||||
|
1. Passa badge `ò0000514162_` (Giuseppe Verdi)
|
||||||
|
2. **Atteso:** Card rossa lampeggiante "ACCESSO NON CONSENTITO" ⚠️
|
||||||
|
|
||||||
|
### T6: Badge non esistente nel DB
|
||||||
|
1. Passa badge `ò0006478281_`
|
||||||
|
2. **Atteso:** Schermata errore con badge in grassetto, countdown 30s ❌
|
||||||
|
|
||||||
|
### T7: Stesso badge passato due volte
|
||||||
|
1. Visualizza un utente (es. Marco Bianchi)
|
||||||
|
2. Passa lo STESSO badge di nuovo
|
||||||
|
3. **Atteso:** Nessun ricaricamento, utente rimane visualizzato ✅
|
||||||
|
|
||||||
|
### T8: Badge diverso sostituisce utente corrente
|
||||||
|
1. Visualizza Marco Bianchi (`0008988288`)
|
||||||
|
2. Passa Laura Rossi (`ò0007399575_`)
|
||||||
|
3. **Atteso:** Card cambia mostrando Laura Rossi ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✓ Test Conferma Ingresso
|
||||||
|
|
||||||
|
### T9: Conferma utente ammesso
|
||||||
|
1. Visualizza Marco Bianchi (ammesso)
|
||||||
|
2. Passa badge VALIDATORE (quello usato per login)
|
||||||
|
3. **Atteso:** Modal verde con carosello "Benvenuto/Welcome/..." per 8 secondi ✅
|
||||||
|
|
||||||
|
### T10: Badge validatore su utente NON ammesso
|
||||||
|
1. Visualizza Giuseppe Verdi (non ammesso)
|
||||||
|
2. Passa badge validatore
|
||||||
|
3. **Atteso:** Banner arancione "Badge validatore rilevato, se cambiato fare logout" ⚠️
|
||||||
|
|
||||||
|
### T11: Badge validatore senza utente visualizzato
|
||||||
|
1. Torna alla schermata "In attesa partecipante"
|
||||||
|
2. Passa badge validatore
|
||||||
|
3. **Atteso:** Banner arancione come sopra ⚠️
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎠 Test Success Modal
|
||||||
|
|
||||||
|
### T12: Carosello scorre tutte le lingue
|
||||||
|
1. Conferma ingresso di un utente ammesso
|
||||||
|
2. Osserva il carosello
|
||||||
|
3. **Atteso:** Scorre IT→EN→FR→DE→ES→PT→ZH→JA→AR→RU con animazione smooth ✅
|
||||||
|
|
||||||
|
### T13: Badge durante carosello chiude modal
|
||||||
|
1. Durante il carosello, passa un NUOVO badge
|
||||||
|
2. **Atteso:** Modal si chiude immediatamente, carica nuovo utente ✅
|
||||||
|
|
||||||
|
### T14: Carosello non troppo stretto
|
||||||
|
1. Osserva le scritte durante il carosello
|
||||||
|
2. **Atteso:** "Добро пожаловать!" (russo) deve essere visibile per intero ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏱️ Test Timeout e Sessione
|
||||||
|
|
||||||
|
### T15: Timeout utente (60s)
|
||||||
|
1. Visualizza un utente
|
||||||
|
2. Aspetta 60 secondi
|
||||||
|
3. **Atteso:** Torna automaticamente a "In attesa partecipante" ✅
|
||||||
|
|
||||||
|
### T16: Timeout badge non trovato (30s)
|
||||||
|
1. Passa badge inesistente (`0006478281`)
|
||||||
|
2. Aspetta 30 secondi
|
||||||
|
3. **Atteso:** Torna a "In attesa partecipante", barra countdown visiva ✅
|
||||||
|
|
||||||
|
### T17: Logout manuale
|
||||||
|
1. Clicca "Esci" nell'header
|
||||||
|
2. **Atteso:** Torna a "Passa badge validatore" ✅
|
||||||
|
|
||||||
|
### T18: Sessione invalidata al riavvio server
|
||||||
|
1. Login come validatore, vai al varco attivo
|
||||||
|
2. **Ferma il server** (Ctrl+C)
|
||||||
|
3. **Riavvia il server** (`./dev.sh server`)
|
||||||
|
4. Aspetta ~30 secondi (polling)
|
||||||
|
5. **Atteso:** Torna automaticamente a "Passa badge validatore" ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Test Debug
|
||||||
|
|
||||||
|
### T19: Pagina debug accessibile
|
||||||
|
1. Vai a `http://localhost:8000/debug`
|
||||||
|
2. **Atteso:** Pagina con log tasti, stato scanner, buffer corrente ✅
|
||||||
|
|
||||||
|
### T20: Log tasti funzionante
|
||||||
|
1. Nella pagina debug, premi tasti sulla tastiera
|
||||||
|
2. **Atteso:** Tasti appaiono nella lista eventi ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Test UI/UX
|
||||||
|
|
||||||
|
### T21: NumLock banner su desktop
|
||||||
|
1. Su browser desktop, verifica schermata "In attesa partecipante"
|
||||||
|
2. **Atteso:** Banner giallo con stato NumLock visibile ✅
|
||||||
|
|
||||||
|
### T22: Occhio toggle password
|
||||||
|
1. Nella schermata password validatore
|
||||||
|
2. Clicca l'icona occhio
|
||||||
|
3. **Atteso:** Password visibile/nascosta ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Badge di Test
|
||||||
|
|
||||||
|
| Formato RFID | Badge | Nome | Risultato Atteso |
|
||||||
|
|----------------------|------------|----------------|----------------------|
|
||||||
|
| `ò0008988288_` | 0008988288 | Marco Bianchi | ✅ Ammesso |
|
||||||
|
| `ò0007399575_` | 0007399575 | Laura Rossi | ✅ Ammessa |
|
||||||
|
| `ò0000514162_` | 0000514162 | Giuseppe Verdi | ❌ Non ammesso |
|
||||||
|
| `ò0006478281_` | 0006478281 | - | ⚠️ Non trovato (404) |
|
||||||
|
|
||||||
|
**Password validatore:** `focolari`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note
|
||||||
|
- I badge sono stringhe, gli zeri iniziali sono significativi
|
||||||
|
- Il pattern RFID italiano usa `ò` come start e `_` come end (+ Enter)
|
||||||
|
- Il pattern US usa `;` come start e `?` come end (+ Enter)
|
||||||
@@ -39,6 +39,7 @@ VotoFocolari/
|
|||||||
├── backend-mock/
|
├── backend-mock/
|
||||||
│ ├── main.py # Entry point con argparse
|
│ ├── main.py # Entry point con argparse
|
||||||
│ ├── Pipfile # Dipendenze Python
|
│ ├── Pipfile # Dipendenze Python
|
||||||
|
│ ├── API_SPECIFICATION.md # Specifiche per backend reale
|
||||||
│ ├── api/routes.py # Endpoint API
|
│ ├── api/routes.py # Endpoint API
|
||||||
│ ├── schemas/models.py # Modelli Pydantic
|
│ ├── schemas/models.py # Modelli Pydantic
|
||||||
│ └── data/
|
│ └── data/
|
||||||
@@ -65,14 +66,15 @@ VotoFocolari/
|
|||||||
1. **Login Validatore**: Passa badge + inserisci password → Sessione 30 min
|
1. **Login Validatore**: Passa badge + inserisci password → Sessione 30 min
|
||||||
2. **Attesa Partecipante**: Schermata grande "Passa il badge"
|
2. **Attesa Partecipante**: Schermata grande "Passa il badge"
|
||||||
3. **Visualizzazione Utente**: Card con foto, nome, ruolo, stato ammissione
|
3. **Visualizzazione Utente**: Card con foto, nome, ruolo, stato ammissione
|
||||||
4. **Conferma Ingresso**: Validatore ripassa il badge → Carosello benvenuto multilingua
|
4. **Conferma Ingresso**: Validatore ripassa il badge → Carosello benvenuto multilingua (8s)
|
||||||
|
|
||||||
### Gestione RFID
|
### Gestione RFID
|
||||||
|
|
||||||
- **Multi-pattern**: Supporta layout tastiera US (`;?`) e IT (`ò_`)
|
- **Multi-pattern**: Supporta layout tastiera US (`;?`) e IT (`ò_`), con `\n` dopo fine sequenza
|
||||||
- **Timeout 2.5s**: Per scansioni accidentali
|
- **Timeout 2.5s**: Per scansioni accidentali
|
||||||
- **ESC annulla**: Scansione in corso
|
- **ESC annulla**: Scansione in corso
|
||||||
- **Enter handling**: Gestito automaticamente
|
- **Enter handling**: Gestito automaticamente
|
||||||
|
- **Stesso badge ignorato**: Se passato più volte di seguito
|
||||||
|
|
||||||
### Sicurezza Sessioni
|
### Sicurezza Sessioni
|
||||||
|
|
||||||
@@ -125,6 +127,7 @@ VotoFocolari/
|
|||||||
| Chiamate API | `frontend/src/services/api.ts` |
|
| Chiamate API | `frontend/src/services/api.ts` |
|
||||||
| Endpoint backend | `backend-mock/api/routes.py` |
|
| Endpoint backend | `backend-mock/api/routes.py` |
|
||||||
| Dati mock utenti | `backend-mock/data/users_default.json` |
|
| Dati mock utenti | `backend-mock/data/users_default.json` |
|
||||||
|
| Specifiche API produzione | `backend-mock/API_SPECIFICATION.md` |
|
||||||
| Configurazione Vite | `frontend/vite.config.ts` |
|
| Configurazione Vite | `frontend/vite.config.ts` |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -142,6 +145,10 @@ VotoFocolari/
|
|||||||
|
|
||||||
4. **NumLock**: Su desktop, viene mostrato un banner per ricordare di attivare NumLock.
|
4. **NumLock**: Su desktop, viene mostrato un banner per ricordare di attivare NumLock.
|
||||||
|
|
||||||
|
5. **Badge Duplicati**: Se lo stesso badge viene passato più volte di seguito, viene ignorato (no ricaricamento).
|
||||||
|
|
||||||
|
6. **Success Modal Interrompibile**: Se durante il carosello di benvenuto si passa un nuovo badge, la modal si chiude e viene caricato subito il nuovo utente.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TODO (da concordare con committenti)
|
## TODO (da concordare con committenti)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ backend-mock/
|
|||||||
- [x] `POST /login-validate` - verifica solo password validatore
|
- [x] `POST /login-validate` - verifica solo password validatore
|
||||||
- [x] `GET /anagrafica/{badge_code}` - ricerca utente
|
- [x] `GET /anagrafica/{badge_code}` - ricerca utente
|
||||||
- [x] Pulizia caratteri sentinel dal badge
|
- [x] Pulizia caratteri sentinel dal badge
|
||||||
- [x] Confronto con e senza zeri iniziali
|
- [x] **Confronto ESATTO come stringa** (zeri iniziali significativi)
|
||||||
- [x] Warning automatico se non ammesso
|
- [x] Warning automatico se non ammesso
|
||||||
- [x] `POST /entry-request` - registrazione ingresso
|
- [x] `POST /entry-request` - registrazione ingresso
|
||||||
- [x] Verifica password validatore
|
- [x] Verifica password validatore
|
||||||
@@ -173,3 +173,17 @@ curl -X POST http://localhost:8000/entry-request \
|
|||||||
## ✅ BACKEND COMPLETATO
|
## ✅ BACKEND COMPLETATO
|
||||||
|
|
||||||
Tutti i task sono stati implementati e testati.
|
Tutti i task sono stati implementati e testati.
|
||||||
|
|
||||||
|
### 📄 Documentazione API
|
||||||
|
Per le specifiche complete da implementare nel backend reale, vedere:
|
||||||
|
**`backend-mock/API_SPECIFICATION.md`**
|
||||||
|
|
||||||
|
Questo documento contiene:
|
||||||
|
- Descrizione completa di tutti gli endpoint
|
||||||
|
- Schema request/response JSON
|
||||||
|
- Codici di errore e gestione
|
||||||
|
- Meccanismo invalidazione sessioni (server_start_time)
|
||||||
|
- Considerazioni di sicurezza
|
||||||
|
- Struttura database suggerita
|
||||||
|
- Casi di test minimi
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ Ottimizzata per tablet in orizzontale.
|
|||||||
- [x] `RFIDStatus.tsx` - indicatore stato scanner
|
- [x] `RFIDStatus.tsx` - indicatore stato scanner
|
||||||
- [x] `UserCard.tsx` - card utente con foto e ruolo
|
- [x] `UserCard.tsx` - card utente con foto e ruolo
|
||||||
- [x] `CountdownTimer.tsx` - timer con progress bar
|
- [x] `CountdownTimer.tsx` - timer con progress bar
|
||||||
- [x] `WelcomeCarousel.tsx` - carosello messaggi multilingua
|
- [x] `WelcomeCarousel.tsx` - carosello messaggi multilingua con **animazione smooth sliding**
|
||||||
- [x] `NumLockBanner.tsx` - avviso NumLock per desktop
|
- [x] `NumLockBanner.tsx` - avviso NumLock per desktop
|
||||||
|
|
||||||
### 7. Schermate (`screens/`)
|
### 7. Schermate (`screens/`)
|
||||||
@@ -80,10 +80,10 @@ Ottimizzata per tablet in orizzontale.
|
|||||||
- [x] `ValidatorLoginScreen.tsx` - attesa badge + password + NumLockBanner
|
- [x] `ValidatorLoginScreen.tsx` - attesa badge + password + NumLockBanner
|
||||||
- [x] `ActiveGateScreen.tsx` - varco attivo:
|
- [x] `ActiveGateScreen.tsx` - varco attivo:
|
||||||
- [x] Card utente (layout largo per tablet)
|
- [x] Card utente (layout largo per tablet)
|
||||||
- [x] **Schermata "badge non trovato"** con countdown 30s
|
- [x] **Schermata "badge non trovato"** con countdown barra visiva (30s)
|
||||||
- [x] **Notifica badge validatore ignorato**
|
- [x] **Notifica badge validatore ignorato**
|
||||||
- [x] NumLockBanner
|
- [x] NumLockBanner
|
||||||
- [x] `SuccessModal.tsx` - conferma ingresso con carosello
|
- [x] `SuccessModal.tsx` - conferma ingresso con carosello (**durata aumentata 8s**)
|
||||||
- [x] `ErrorModal.tsx` - errore fullscreen
|
- [x] `ErrorModal.tsx` - errore fullscreen
|
||||||
- [x] `DebugScreen.tsx` - pagina diagnostica RFID
|
- [x] `DebugScreen.tsx` - pagina diagnostica RFID
|
||||||
|
|
||||||
@@ -95,10 +95,13 @@ Ottimizzata per tablet in orizzontale.
|
|||||||
- [x] **Qualsiasi badge può essere validatore** (verificato con password)
|
- [x] **Qualsiasi badge può essere validatore** (verificato con password)
|
||||||
- [x] Password salvata in sessione per conferme ingresso
|
- [x] Password salvata in sessione per conferme ingresso
|
||||||
- [x] **Invalidazione sessione se server riparte** (serverStartTime)
|
- [x] **Invalidazione sessione se server riparte** (serverStartTime)
|
||||||
|
- [x] **Polling periodico (30s) per verificare server restart**
|
||||||
- [x] Timeout sessione 30 minuti
|
- [x] Timeout sessione 30 minuti
|
||||||
- [x] Timeout utente 60 secondi
|
- [x] Timeout utente 60 secondi
|
||||||
- [x] **Timeout badge non trovato 30 secondi**
|
- [x] **Timeout badge non trovato 30 secondi**
|
||||||
|
- [x] **Ignora stesso badge passato più volte** (no ricaricamento)
|
||||||
- [x] Cambio rapido badge partecipante
|
- [x] Cambio rapido badge partecipante
|
||||||
|
- [x] **Badge durante success modal chiude modal e carica nuovo utente** (fix dipendenze useCallback)
|
||||||
- [x] Conferma con badge validatore (quello della sessione)
|
- [x] Conferma con badge validatore (quello della sessione)
|
||||||
- [x] **Notifica se badge validatore rippassato senza utente**
|
- [x] **Notifica se badge validatore rippassato senza utente**
|
||||||
- [x] Logging transizioni con prefisso `[FLOW]`
|
- [x] Logging transizioni con prefisso `[FLOW]`
|
||||||
@@ -107,9 +110,10 @@ Ottimizzata per tablet in orizzontale.
|
|||||||
|
|
||||||
- [x] Componente `WelcomeCarousel.tsx`
|
- [x] Componente `WelcomeCarousel.tsx`
|
||||||
- [x] 10 lingue supportate
|
- [x] 10 lingue supportate
|
||||||
- [x] Scorrimento automatico ogni 800ms
|
- [x] **Animazione smooth sliding** (slide up/down)
|
||||||
|
- [x] Scorrimento automatico (intervallo calcolato dinamicamente)
|
||||||
- [x] Modale fullscreen verde
|
- [x] Modale fullscreen verde
|
||||||
- [x] Durata totale: 5 secondi
|
- [x] **Durata totale: 8 secondi** (più rilassato)
|
||||||
|
|
||||||
### 10. Debug & Diagnostica
|
### 10. Debug & Diagnostica
|
||||||
|
|
||||||
@@ -135,4 +139,46 @@ Ottimizzata per tablet in orizzontale.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🧪 TODO: Test Automatici
|
||||||
|
|
||||||
|
### Test da Implementare
|
||||||
|
|
||||||
|
- [ ] **Test RFID Scanner:**
|
||||||
|
- [ ] Rilevamento pattern US (`;` → `?`)
|
||||||
|
- [ ] Rilevamento pattern IT (`ò` → `_`)
|
||||||
|
- [ ] Timeout scansione incompleta
|
||||||
|
- [ ] ESC annulla scansione
|
||||||
|
- [ ] Enter post-completamento ignorato
|
||||||
|
|
||||||
|
- [ ] **Test Flow Validatore:**
|
||||||
|
- [ ] Login con password corretta
|
||||||
|
- [ ] Login con password errata
|
||||||
|
- [ ] Sessione persistente in localStorage
|
||||||
|
- [ ] Invalidazione sessione al riavvio server
|
||||||
|
- [ ] Logout manuale
|
||||||
|
|
||||||
|
- [ ] **Test Flow Partecipante:**
|
||||||
|
- [ ] Badge trovato ammesso → mostra card verde
|
||||||
|
- [ ] Badge trovato non ammesso → mostra card rossa + warning
|
||||||
|
- [ ] Badge non trovato → mostra schermata errore con countdown
|
||||||
|
- [ ] Stesso badge passato più volte → ignorato
|
||||||
|
- [ ] Badge diverso passato → cambia utente visualizzato
|
||||||
|
- [ ] Timeout 60s → torna in attesa
|
||||||
|
|
||||||
|
- [ ] **Test Conferma Ingresso:**
|
||||||
|
- [ ] Badge validatore su utente ammesso → success modal
|
||||||
|
- [ ] Badge validatore su utente NON ammesso → notifica ignorato
|
||||||
|
- [ ] Badge validatore senza utente → notifica ignorato
|
||||||
|
- [ ] **⚠️ IMPORTANTE:** Simulare bug che bypassa frontend → backend DEVE bloccare
|
||||||
|
|
||||||
|
- [ ] **Test Success Modal:**
|
||||||
|
- [ ] Carosello scorre tutte le lingue
|
||||||
|
- [ ] Durata corretta (8s)
|
||||||
|
- [ ] Badge durante modal → chiude modal e carica nuovo utente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✅ FRONTEND COMPLETATO
|
## ✅ FRONTEND COMPLETATO
|
||||||
|
|
||||||
|
Tutte le funzionalità principali sono state implementate. Rimangono da sviluppare i test automatici.
|
||||||
|
|
||||||
|
|||||||
325
backend-mock/API_SPECIFICATION.md
Normal file
325
backend-mock/API_SPECIFICATION.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# 📄 Specifiche API Backend - Focolari Voting System
|
||||||
|
|
||||||
|
Questo documento descrive le specifiche che il backend reale deve implementare per funzionare correttamente con il frontend del sistema di controllo accessi.
|
||||||
|
|
||||||
|
**Versione:** 1.0
|
||||||
|
**Data:** Gennaio 2026
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Configurazione Server
|
||||||
|
|
||||||
|
### CORS
|
||||||
|
Il server deve abilitare CORS con le seguenti impostazioni:
|
||||||
|
- **Origins:** `*` (o lista specifica di domini autorizzati)
|
||||||
|
- **Methods:** `GET, POST, OPTIONS`
|
||||||
|
- **Headers:** `Content-Type, Authorization`
|
||||||
|
|
||||||
|
### Serving Frontend
|
||||||
|
Il backend dovrebbe servire il frontend buildato (file statici) dalla root `/`.
|
||||||
|
Questo permette di avere un unico endpoint per frontend e API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Meccanismo di Invalidazione Sessioni
|
||||||
|
|
||||||
|
### Server Start Time
|
||||||
|
All'avvio, il server deve generare un **timestamp univoco** (es. Unix timestamp in secondi).
|
||||||
|
Questo valore viene restituito nell'endpoint `/info-room` e serve al frontend per invalidare sessioni obsolete.
|
||||||
|
|
||||||
|
**Comportamento:**
|
||||||
|
1. Il frontend salva `server_start_time` nella sessione locale
|
||||||
|
2. Al caricamento successivo, confronta il valore salvato con quello attuale
|
||||||
|
3. Se differiscono, la sessione viene invalidata (il server è stato riavviato)
|
||||||
|
|
||||||
|
Questo garantisce che:
|
||||||
|
- Un riavvio del server forza il re-login di tutti i validatori
|
||||||
|
- Sessioni zombie non rimangono attive dopo manutenzione
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 Endpoint API
|
||||||
|
|
||||||
|
### 1. `GET /info-room`
|
||||||
|
|
||||||
|
Restituisce informazioni sulla sala/meeting corrente e lo stato del server.
|
||||||
|
|
||||||
|
#### Response `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"room_name": "Sala Assemblea",
|
||||||
|
"meeting_id": "VOT-2024",
|
||||||
|
"server_start_time": 1737100800
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Campo | Tipo | Descrizione |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `room_name` | string | Nome della sala visualizzato nell'header |
|
||||||
|
| `meeting_id` | string | Identificativo del meeting/votazione |
|
||||||
|
| `server_start_time` | integer | Unix timestamp dell'avvio server (per invalidazione sessioni) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `POST /login-validate`
|
||||||
|
|
||||||
|
Verifica le credenziali del validatore. **IMPORTANTE:** Qualsiasi badge può diventare un badge validatore se la password è corretta.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"badge": "0007399575",
|
||||||
|
"password": "focolari"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Campo | Tipo | Descrizione |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `badge` | string | Codice badge scansionato (numerico, senza sentinel) |
|
||||||
|
| `password` | string | Password inserita dall'utente |
|
||||||
|
|
||||||
|
#### Response `200 OK` (Successo)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Login effettuato con successo",
|
||||||
|
"token": "optional-jwt-token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response `401 Unauthorized` (Errore)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Password non valida"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:**
|
||||||
|
- La password è l'unico fattore di autenticazione
|
||||||
|
- Il badge viene memorizzato dal frontend come "badge validatore" per conferme successive
|
||||||
|
- Il token è opzionale (per future implementazioni di autenticazione JWT)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `GET /anagrafica/{badge_code}`
|
||||||
|
|
||||||
|
Recupera i dati anagrafici di un utente dato il suo codice badge.
|
||||||
|
|
||||||
|
#### Path Parameters
|
||||||
|
- `badge_code`: Codice badge (stringa, es. "0008988288")
|
||||||
|
|
||||||
|
**IMPORTANTE - Confronto Badge:**
|
||||||
|
Il badge è una **stringa**, non un numero. Va confrontato **esattamente** carattere per carattere.
|
||||||
|
Gli zeri iniziali sono significativi: `"0008988288"` e `"8988288"` sono badge **diversi**.
|
||||||
|
|
||||||
|
#### Response `200 OK` (Utente trovato e ammesso)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"badge_code": "0008988288",
|
||||||
|
"nome": "Marco",
|
||||||
|
"cognome": "Bianchi",
|
||||||
|
"url_foto": "https://example.com/foto.jpg",
|
||||||
|
"ruolo": "Votante",
|
||||||
|
"ammesso": true # <-- utente ammesso all'ingresso
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response `200 OK` (Utente trovato ma NON ammesso)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"badge_code": "0000514162",
|
||||||
|
"nome": "Giuseppe",
|
||||||
|
"cognome": "Verdi",
|
||||||
|
"url_foto": "https://example.com/foto.jpg",
|
||||||
|
"ruolo": "Tecnico",
|
||||||
|
"ammesso": false, #<-- utente NON ammesso all'ingresso
|
||||||
|
"warning": "Utente non ammesso all'ingresso"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response `404 Not Found` (Utente non trovato)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Badge non trovato nel sistema"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Campo Response | Tipo | Descrizione |
|
||||||
|
|----------------|------|-------------|
|
||||||
|
| `badge_code` | string | Codice badge |
|
||||||
|
| `nome` | string | Nome dell'utente |
|
||||||
|
| `cognome` | string | Cognome dell'utente |
|
||||||
|
| `url_foto` | string | URL immagine profilo (può essere placeholder) |
|
||||||
|
| `ruolo` | string | Ruolo dell'utente (es. "Votante", "Tecnico", "Ospite") |
|
||||||
|
| `ammesso` | boolean | `true` se autorizzato all'ingresso |
|
||||||
|
| `warning` | string? | Opzionale, presente se `ammesso: false` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `POST /entry-request`
|
||||||
|
|
||||||
|
Registra l'ingresso di un utente. Richiede conferma del validatore.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_badge": "0008988288",
|
||||||
|
"validator_password": "focolari"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Campo | Tipo | Descrizione |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `user_badge` | string | Badge dell'utente che sta entrando |
|
||||||
|
| `validator_password` | string | Password del validatore (ri-verifica) |
|
||||||
|
|
||||||
|
#### Response `200 OK` (Successo)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Ingresso registrato con successo"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response `401 Unauthorized` (Password errata)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Password validatore non valida"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response `403 Forbidden` (Utente non ammesso)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Utente non autorizzato all'ingresso"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response `404 Not Found` (Badge non trovato)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Badge utente non trovato"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANTE - Sicurezza:**
|
||||||
|
Anche se il frontend non dovrebbe permettere di inviare entry-request per utenti non ammessi, il backend **DEVE** sempre verificare:
|
||||||
|
1. Che la password validatore sia corretta
|
||||||
|
2. Che l'utente esista
|
||||||
|
3. Che l'utente sia ammesso (`ammesso: true`)
|
||||||
|
|
||||||
|
Non fidarsi mai del frontend per la validazione!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Considerazioni di Sicurezza
|
||||||
|
|
||||||
|
### Validazione Lato Backend
|
||||||
|
Il backend deve **sempre** eseguire tutte le validazioni, indipendentemente da cosa fa il frontend:
|
||||||
|
|
||||||
|
1. **Login:** Verificare che la password sia corretta
|
||||||
|
2. **Entry:**
|
||||||
|
- Verificare password validatore
|
||||||
|
- Verificare che utente esista
|
||||||
|
- Verificare che utente sia ammesso
|
||||||
|
- Loggare l'operazione per audit
|
||||||
|
|
||||||
|
### Logging e Audit
|
||||||
|
Si raccomanda di loggare:
|
||||||
|
- Ogni tentativo di login (successo/fallimento)
|
||||||
|
- Ogni registrazione di ingresso
|
||||||
|
- Badge non trovati (potenziale tentativo di accesso non autorizzato)
|
||||||
|
|
||||||
|
### Protezione Rate Limiting
|
||||||
|
Implementare rate limiting su:
|
||||||
|
- `/login-validate`: max 5 tentativi/minuto per IP
|
||||||
|
- `/entry-request`: max 30 richieste/minuto per IP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Struttura Dati Suggerita
|
||||||
|
|
||||||
|
### Database Utenti
|
||||||
|
```sql
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
badge_code VARCHAR(20) UNIQUE NOT NULL,
|
||||||
|
nome VARCHAR(100) NOT NULL,
|
||||||
|
cognome VARCHAR(100) NOT NULL,
|
||||||
|
url_foto VARCHAR(500),
|
||||||
|
ruolo VARCHAR(50) NOT NULL,
|
||||||
|
ammesso BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_badge_code ON users(badge_code);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabella Accessi (Audit Log)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE access_log (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_badge VARCHAR(20) NOT NULL,
|
||||||
|
validator_badge VARCHAR(20) NOT NULL,
|
||||||
|
room_id VARCHAR(50),
|
||||||
|
action VARCHAR(20) NOT NULL, -- 'entry', 'denied', 'not_found'
|
||||||
|
timestamp TIMESTAMP DEFAULT NOW(),
|
||||||
|
ip_address VARCHAR(45)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Test e Validazione
|
||||||
|
|
||||||
|
### Casi di Test Minimi
|
||||||
|
|
||||||
|
1. **Login con password corretta** → 200 OK
|
||||||
|
2. **Login con password errata** → 401 Unauthorized
|
||||||
|
3. **Anagrafica badge esistente ammesso** → 200 OK con `ammesso: true`
|
||||||
|
4. **Anagrafica badge esistente non ammesso** → 200 OK con `ammesso: false` + warning
|
||||||
|
5. **Anagrafica badge inesistente** → 404 Not Found
|
||||||
|
6. **Entry utente ammesso con password corretta** → 200 OK
|
||||||
|
7. **Entry utente ammesso con password errata** → 401 Unauthorized
|
||||||
|
8. **Entry utente NON ammesso** → 403 Forbidden (anche se password corretta!)
|
||||||
|
9. **Entry badge inesistente** → 404 Not Found
|
||||||
|
|
||||||
|
### Badge di Test (Mock)
|
||||||
|
```
|
||||||
|
0008988288 - Marco Bianchi (Votante, ammesso)
|
||||||
|
0007399575 - Laura Rossi (Votante, ammessa)
|
||||||
|
0000514162 - Giuseppe Verdi (Tecnico, NON ammesso)
|
||||||
|
0006478281 - NON nel database (per test 404)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Note Implementative
|
||||||
|
|
||||||
|
### Content-Type
|
||||||
|
Tutte le richieste e risposte usano `application/json`.
|
||||||
|
|
||||||
|
### Codici Errore HTTP
|
||||||
|
- `200`: Successo
|
||||||
|
- `401`: Non autorizzato (password errata)
|
||||||
|
- `403`: Vietato (utente non ammesso)
|
||||||
|
- `404`: Risorsa non trovata
|
||||||
|
- `500`: Errore interno server
|
||||||
|
|
||||||
|
### Formato Errori
|
||||||
|
Tutti gli errori devono restituire un JSON con campo `detail`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Messaggio di errore descrittivo"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Changelog
|
||||||
|
|
||||||
|
### v1.0 (Gennaio 2026)
|
||||||
|
- Specifica iniziale
|
||||||
|
- Endpoint: `/info-room`, `/login-validate`, `/anagrafica/{badge}`, `/entry-request`
|
||||||
|
- Meccanismo invalidazione sessioni con `server_start_time`
|
||||||
@@ -46,10 +46,11 @@ def clean_badge(badge: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def find_user(badge_code: str) -> dict | None:
|
def find_user(badge_code: str) -> dict | None:
|
||||||
"""Cerca un utente per badge code"""
|
"""Cerca un utente per badge code (confronto esatto come stringa)"""
|
||||||
clean = clean_badge(badge_code)
|
clean = clean_badge(badge_code)
|
||||||
for user in _data["users"]:
|
for user in _data["users"]:
|
||||||
if user["badge_code"] == clean or user["badge_code"].lstrip("0") == clean.lstrip("0"):
|
# Confronto esatto: il badge è una stringa, non un numero
|
||||||
|
if user["badge_code"] == clean:
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,6 @@
|
|||||||
"meeting_id": "VOT-2024"
|
"meeting_id": "VOT-2024"
|
||||||
},
|
},
|
||||||
"users": [
|
"users": [
|
||||||
{
|
|
||||||
"badge_code": "0008988288",
|
|
||||||
"nome": "Marco",
|
|
||||||
"cognome": "Bianchi",
|
|
||||||
"url_foto": "https://randomuser.me/api/portraits/men/1.jpg",
|
|
||||||
"ruolo": "Votante",
|
|
||||||
"ammesso": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"badge_code": "0007399575",
|
"badge_code": "0007399575",
|
||||||
"nome": "Laura",
|
"nome": "Laura",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {AppState, RoomInfo, User, ValidatorSession} from './types';
|
|||||||
// Costanti
|
// Costanti
|
||||||
const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minuti
|
const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minuti
|
||||||
const USER_TIMEOUT_SECONDS = 60;
|
const USER_TIMEOUT_SECONDS = 60;
|
||||||
|
const SUCCESS_MODAL_DURATION_MS = 8000; // Aumentato per carosello più rilassato
|
||||||
const STORAGE_KEY = 'focolari_validator_session';
|
const STORAGE_KEY = 'focolari_validator_session';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -38,6 +39,9 @@ function App() {
|
|||||||
// Badge non trovato (con timeout per tornare all'attesa)
|
// Badge non trovato (con timeout per tornare all'attesa)
|
||||||
const [notFoundBadge, setNotFoundBadge] = useState<string | null>(null);
|
const [notFoundBadge, setNotFoundBadge] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Badge corrente visualizzato (per evitare ricaricamenti multipli dello stesso)
|
||||||
|
const [currentBadgeCode, setCurrentBadgeCode] = useState<string | null>(null);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Session Management
|
// Session Management
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -120,10 +124,22 @@ function App() {
|
|||||||
setTimeout(() => setShowValidatorBadgeNotice(false), 4000);
|
setTimeout(() => setShowValidatorBadgeNotice(false), 4000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Badge partecipante - verifica se è lo stesso già visualizzato
|
||||||
|
if (cleanCode === currentBadgeCode) {
|
||||||
|
console.log('[FLOW] Same badge scanned again - ignoring');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[FLOW] Loading participant:', cleanCode);
|
console.log('[FLOW] Loading participant:', cleanCode);
|
||||||
// Badge partecipante - carica utente
|
// Badge partecipante - carica utente
|
||||||
// Se c'era un badge non trovato, cancellalo e carica il nuovo
|
// Se c'era un badge non trovato, cancellalo e carica il nuovo
|
||||||
setNotFoundBadge(null);
|
setNotFoundBadge(null);
|
||||||
|
// Se era aperta la success modal, chiudila subito
|
||||||
|
if (showSuccessModal) {
|
||||||
|
console.log('[FLOW] Closing success modal for new badge');
|
||||||
|
setShowSuccessModal(false);
|
||||||
|
setSuccessUserName(undefined);
|
||||||
|
}
|
||||||
await handleLoadUser(cleanCode);
|
await handleLoadUser(cleanCode);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -131,7 +147,7 @@ function App() {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [appState, currentUser, validatorSession]);
|
}, [appState, currentUser, validatorSession, showSuccessModal, currentBadgeCode]);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Initialize RFID Scanner
|
// Initialize RFID Scanner
|
||||||
@@ -153,6 +169,7 @@ function App() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setNotFoundBadge(null);
|
setNotFoundBadge(null);
|
||||||
|
setCurrentBadgeCode(badgeCode); // Traccia il badge corrente
|
||||||
setAppState('showing-user');
|
setAppState('showing-user');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -195,6 +212,7 @@ function App() {
|
|||||||
setSuccessUserName(userName);
|
setSuccessUserName(userName);
|
||||||
setShowSuccessModal(true);
|
setShowSuccessModal(true);
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
|
setCurrentBadgeCode(null); // Reset badge corrente dopo successo
|
||||||
setAppState('gate-active');
|
setAppState('gate-active');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -255,6 +273,7 @@ function App() {
|
|||||||
const handleCancelUser = useCallback(() => {
|
const handleCancelUser = useCallback(() => {
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
setNotFoundBadge(null);
|
setNotFoundBadge(null);
|
||||||
|
setCurrentBadgeCode(null); // Reset badge corrente
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setAppState('gate-active');
|
setAppState('gate-active');
|
||||||
}, []);
|
}, []);
|
||||||
@@ -263,6 +282,7 @@ function App() {
|
|||||||
console.log('[App] User timeout - tornando in attesa');
|
console.log('[App] User timeout - tornando in attesa');
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
setNotFoundBadge(null);
|
setNotFoundBadge(null);
|
||||||
|
setCurrentBadgeCode(null); // Reset badge corrente
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setAppState('gate-active');
|
setAppState('gate-active');
|
||||||
}, []);
|
}, []);
|
||||||
@@ -323,6 +343,29 @@ function App() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [validatorSession, clearSession]);
|
}, [validatorSession, clearSession]);
|
||||||
|
|
||||||
|
// Polling per verificare se il server è stato riavviato
|
||||||
|
useEffect(() => {
|
||||||
|
if (!validatorSession || !roomInfo) return;
|
||||||
|
|
||||||
|
const checkServerRestart = async () => {
|
||||||
|
try {
|
||||||
|
const info = await getRoomInfo();
|
||||||
|
if (info.server_start_time !== validatorSession.serverStartTime) {
|
||||||
|
console.log('[FLOW] Server restarted - invalidating session');
|
||||||
|
clearSession();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignora errori di connessione durante il polling
|
||||||
|
console.warn('[FLOW] Server unreachable during polling');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Polling ogni 30 secondi
|
||||||
|
const interval = setInterval(checkServerRestart, 30000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [validatorSession, roomInfo, clearSession]);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Render
|
// Render
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -387,6 +430,7 @@ function App() {
|
|||||||
isOpen={showSuccessModal}
|
isOpen={showSuccessModal}
|
||||||
onClose={handleSuccessModalClose}
|
onClose={handleSuccessModalClose}
|
||||||
userName={successUserName}
|
userName={successUserName}
|
||||||
|
durationMs={SUCCESS_MODAL_DURATION_MS}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Error Modal */}
|
{/* Error Modal */}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Welcome Carousel Component - Focolari Voting System
|
* Welcome Carousel Component - Focolari Voting System
|
||||||
*
|
*
|
||||||
* Carosello automatico di messaggi di benvenuto multilingua.
|
* Carosello automatico di messaggi di benvenuto multilingua.
|
||||||
* Scorre automaticamente ogni 800ms durante la visualizzazione.
|
* Scorrimento fluido con animazione smooth.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
@@ -25,55 +25,51 @@ const WELCOME_MESSAGES: WelcomeMessage[] = [
|
|||||||
{lang: 'ru', text: 'Добро пожаловать!'},
|
{lang: 'ru', text: 'Добро пожаловать!'},
|
||||||
];
|
];
|
||||||
|
|
||||||
const CAROUSEL_INTERVAL_MS = 800;
|
|
||||||
|
|
||||||
interface WelcomeCarouselProps {
|
interface WelcomeCarouselProps {
|
||||||
/** Se true, il carosello è in pausa */
|
/** Se true, il carosello è in pausa */
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
/** Nome dell'utente da visualizzare (opzionale) */
|
/** Nome dell'utente da visualizzare (opzionale) */
|
||||||
userName?: string;
|
userName?: string;
|
||||||
|
/** Intervallo tra i messaggi in ms (default 800) */
|
||||||
|
intervalMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WelcomeCarousel({paused = false, userName}: WelcomeCarouselProps) {
|
export function WelcomeCarousel({paused = false, userName, intervalMs = 800}: WelcomeCarouselProps) {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (paused) return;
|
if (paused) return;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setIsTransitioning(true);
|
setCurrentIndex((prev) => (prev + 1) % WELCOME_MESSAGES.length);
|
||||||
|
}, intervalMs);
|
||||||
setTimeout(() => {
|
|
||||||
setCurrentIndex((prev) => (prev + 1) % WELCOME_MESSAGES.length);
|
|
||||||
setIsTransitioning(false);
|
|
||||||
}, 150); // Durata transizione fade
|
|
||||||
}, CAROUSEL_INTERVAL_MS);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [paused]);
|
}, [paused, intervalMs]);
|
||||||
|
|
||||||
const currentMessage = WELCOME_MESSAGES[currentIndex];
|
const currentMessage = WELCOME_MESSAGES[currentIndex];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center text-center">
|
<div className="flex flex-col items-center justify-center text-center w-full max-w-4xl mx-auto px-4">
|
||||||
{/* Messaggio di benvenuto */}
|
{/* Container con overflow hidden per animazione slide - altezza maggiore per testi lunghi */}
|
||||||
<div
|
<div className="relative h-40 md:h-52 w-full overflow-hidden">
|
||||||
className={`transition-opacity duration-150 ${
|
{/* Messaggio corrente */}
|
||||||
isTransitioning ? 'opacity-0' : 'opacity-100'
|
<div
|
||||||
}`}
|
key={currentIndex}
|
||||||
>
|
className="absolute inset-0 flex flex-col items-center justify-center animate-slide-in-up"
|
||||||
<h1 className="text-6xl md:text-8xl font-bold text-white mb-4 drop-shadow-lg">
|
>
|
||||||
{currentMessage.text}
|
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold text-white mb-2 drop-shadow-lg whitespace-nowrap">
|
||||||
</h1>
|
{currentMessage.text}
|
||||||
<p className="text-xl text-white/70 uppercase tracking-wider">
|
</h1>
|
||||||
{currentMessage.lang.toUpperCase()}
|
<p className="text-lg text-white/60 uppercase tracking-wider">
|
||||||
</p>
|
{currentMessage.lang.toUpperCase()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nome utente */}
|
{/* Nome utente */}
|
||||||
{userName && (
|
{userName && (
|
||||||
<div className="mt-8 pt-8 border-t border-white/30">
|
<div className="mt-6 pt-6 border-t border-white/30">
|
||||||
<p className="text-3xl md:text-4xl text-white font-medium">
|
<p className="text-3xl md:text-4xl text-white font-medium">
|
||||||
{userName}
|
{userName}
|
||||||
</p>
|
</p>
|
||||||
@@ -81,18 +77,43 @@ export function WelcomeCarousel({paused = false, userName}: WelcomeCarouselProps
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Indicatori */}
|
{/* Indicatori */}
|
||||||
<div className="flex gap-2 mt-8">
|
<div className="flex gap-2 mt-6">
|
||||||
{WELCOME_MESSAGES.map((_, index) => (
|
{WELCOME_MESSAGES.map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
className={`h-2 rounded-full transition-all duration-500 ease-out ${
|
||||||
index === currentIndex
|
index === currentIndex
|
||||||
? 'bg-white w-6'
|
? 'bg-white w-8'
|
||||||
: 'bg-white/40'
|
: 'bg-white/30 w-2'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Stili per animazione smooth */}
|
||||||
|
<style>{`
|
||||||
|
@keyframes slideInUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(40px);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animate-slide-in-up {
|
||||||
|
animation: slideInUp ${intervalMs}ms ease-in-out;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
* Schermata principale del varco attivo
|
* Schermata principale del varco attivo
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useEffect, useState} from 'react';
|
|
||||||
import {Button, CountdownTimer, Logo, NumLockBanner, RFIDStatus, UserCard} from '../components';
|
import {Button, CountdownTimer, Logo, NumLockBanner, RFIDStatus, UserCard} from '../components';
|
||||||
import type {RFIDScannerState, RoomInfo, User} from '../types';
|
import type {RFIDScannerState, RoomInfo, User} from '../types';
|
||||||
|
|
||||||
@@ -39,27 +38,6 @@ export function ActiveGateScreen({
|
|||||||
onUserTimeout,
|
onUserTimeout,
|
||||||
showValidatorBadgeNotice = false,
|
showValidatorBadgeNotice = false,
|
||||||
}: ActiveGateScreenProps) {
|
}: ActiveGateScreenProps) {
|
||||||
// Timer per countdown badge non trovato
|
|
||||||
const [notFoundCountdown, setNotFoundCountdown] = useState(NOT_FOUND_TIMEOUT_SECONDS);
|
|
||||||
|
|
||||||
// Reset countdown quando cambia notFoundBadge
|
|
||||||
useEffect(() => {
|
|
||||||
if (notFoundBadge) {
|
|
||||||
setNotFoundCountdown(NOT_FOUND_TIMEOUT_SECONDS);
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setNotFoundCountdown(prev => {
|
|
||||||
if (prev <= 1) {
|
|
||||||
clearInterval(interval);
|
|
||||||
onUserTimeout();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return prev - 1;
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [notFoundBadge, onUserTimeout]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-gradient-to-br from-slate-50 to-slate-100">
|
<div className="min-h-screen flex flex-col bg-gradient-to-br from-slate-50 to-slate-100">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -123,6 +101,17 @@ export function ActiveGateScreen({
|
|||||||
) : notFoundBadge ? (
|
) : notFoundBadge ? (
|
||||||
// Badge non trovato
|
// Badge non trovato
|
||||||
<div className="glass rounded-3xl p-12 md:p-16 shadow-xl text-center max-w-2xl w-full">
|
<div className="glass rounded-3xl p-12 md:p-16 shadow-xl text-center max-w-2xl w-full">
|
||||||
|
{/* Timer bar in alto come per l'utente trovato */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<CountdownTimer
|
||||||
|
key={`not-found-${notFoundBadge}`}
|
||||||
|
seconds={NOT_FOUND_TIMEOUT_SECONDS}
|
||||||
|
onExpire={onUserTimeout}
|
||||||
|
warningThreshold={15}
|
||||||
|
dangerThreshold={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-error/10 mb-8">
|
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-error/10 mb-8">
|
||||||
<svg
|
<svg
|
||||||
@@ -142,14 +131,6 @@ export function ActiveGateScreen({
|
|||||||
<p className="text-2xl text-gray-700 mb-2">Utente con badge:</p>
|
<p className="text-2xl text-gray-700 mb-2">Utente con badge:</p>
|
||||||
<p className="text-3xl font-bold text-error font-mono mb-4">{notFoundBadge}</p>
|
<p className="text-3xl font-bold text-error font-mono mb-4">{notFoundBadge}</p>
|
||||||
<p className="text-2xl text-gray-700">non trovato nel sistema</p>
|
<p className="text-2xl text-gray-700">non trovato nel sistema</p>
|
||||||
|
|
||||||
{/* Footer con countdown */}
|
|
||||||
<div className="mt-10 pt-6 border-t border-gray-200">
|
|
||||||
<p className="text-gray-500">
|
|
||||||
Ritorno all'attesa in <span
|
|
||||||
className="font-bold text-focolare-blue">{notFoundCountdown}</span> secondi
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
// Error state
|
// Error state
|
||||||
|
|||||||
@@ -9,19 +9,27 @@ interface SuccessModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
|
durationMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Numero messaggi nel carosello
|
||||||
|
const CAROUSEL_MESSAGE_COUNT = 10;
|
||||||
|
|
||||||
export function SuccessModal({
|
export function SuccessModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
userName
|
userName,
|
||||||
|
durationMs = 8000
|
||||||
}: SuccessModalProps) {
|
}: SuccessModalProps) {
|
||||||
|
// Calcola intervallo carosello per mostrare tutti i messaggi durante la durata
|
||||||
|
const carouselIntervalMs = Math.floor(durationMs / (CAROUSEL_MESSAGE_COUNT * 1.2));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
variant="success"
|
variant="success"
|
||||||
autoCloseMs={5000}
|
autoCloseMs={durationMs}
|
||||||
fullscreen
|
fullscreen
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen p-8">
|
<div className="flex flex-col items-center justify-center min-h-screen p-8">
|
||||||
@@ -46,7 +54,7 @@ export function SuccessModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Carosello Messaggi Benvenuto */}
|
{/* Carosello Messaggi Benvenuto */}
|
||||||
<WelcomeCarousel userName={userName}/>
|
<WelcomeCarousel userName={userName} intervalMs={carouselIntervalMs} />
|
||||||
|
|
||||||
{/* Sub text */}
|
{/* Sub text */}
|
||||||
<p className="text-2xl md:text-3xl text-white/80 mt-8 animate-fade-in">
|
<p className="text-2xl md:text-3xl text-white/80 mt-8 animate-fade-in">
|
||||||
@@ -59,7 +67,7 @@ export function SuccessModal({
|
|||||||
<div
|
<div
|
||||||
className="h-full bg-white rounded-full"
|
className="h-full bg-white rounded-full"
|
||||||
style={{
|
style={{
|
||||||
animation: 'shrink 5s linear forwards',
|
animation: `shrink ${durationMs}ms linear forwards`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user