feat: Controllo accessi RFID completo con gestione sessioni
- Aggiunto supporto multi-pattern RFID (US/IT layout) - Implementata invalidazione sessioni al restart del server - Schermata "badge non trovato" con countdown 30s - Notifica quando badge validatore passato senza utente - Database aggiornato con badge reali di test - Layout ottimizzato per tablet orizzontale - Banner NumLock per desktop - Toggle visibilità password - Carosello benvenuto multilingua (10 lingue) - Pagina debug RFID (/debug)
This commit is contained in:
47
README.md
47
README.md
@@ -4,39 +4,58 @@ Sistema di controllo accessi per le assemblee di voto del **Movimento dei Focola
|
|||||||
|
|
||||||
## 📖 Descrizione
|
## 📖 Descrizione
|
||||||
|
|
||||||
Applicazione web ottimizzata per tablet che gestisce i varchi d'ingresso alle sale votazione. Il sistema utilizza lettori RFID USB (che emulano tastiera) per identificare validatori e partecipanti.
|
Applicazione web ottimizzata per tablet che gestisce i varchi d'ingresso alle sale votazione. Il sistema utilizza
|
||||||
|
lettori RFID USB (che emulano tastiera) per identificare validatori e partecipanti.
|
||||||
|
|
||||||
## 🏗️ Struttura Progetto
|
## 🏗️ Struttura Progetto
|
||||||
|
|
||||||
```
|
```
|
||||||
VotoFocolari/
|
VotoFocolari/
|
||||||
|
├── dev.sh # Script sviluppo (install, dev, server, build, ...)
|
||||||
├── ai-prompts/ # Documentazione sviluppo e prompt
|
├── ai-prompts/ # Documentazione sviluppo e prompt
|
||||||
├── backend-mock/ # API mock in Python FastAPI
|
├── backend-mock/ # API mock in Python FastAPI
|
||||||
|
│ └── static/ # Frontend buildato (generato)
|
||||||
└── frontend/ # App React + TypeScript + Tailwind
|
└── frontend/ # App React + TypeScript + Tailwind
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Backend
|
### Setup Iniziale
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend-mock
|
./dev.sh install
|
||||||
pipenv install
|
|
||||||
pipenv run start
|
|
||||||
# Server: http://localhost:8000
|
|
||||||
# Docs API: http://localhost:8000/docs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend
|
### Sviluppo (hot reload)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
./dev.sh dev
|
||||||
npm install
|
# Backend API: http://localhost:8000
|
||||||
npm run dev
|
# Frontend Dev: http://localhost:5173
|
||||||
# App: http://localhost:5173
|
```
|
||||||
|
|
||||||
|
### Produzione Locale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev.sh server
|
||||||
|
# App completa: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Altri Comandi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev.sh build # Solo build frontend
|
||||||
|
./dev.sh backend # Solo backend (API)
|
||||||
|
./dev.sh frontend # Solo frontend dev
|
||||||
|
./dev.sh shell # Shell pipenv backend
|
||||||
|
./dev.sh clean # Pulisce build e cache
|
||||||
|
./dev.sh help # Mostra tutti i comandi
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📚 Documentazione
|
## 📚 Documentazione
|
||||||
|
|
||||||
Per dettagli tecnici, consulta la cartella `ai-prompts/`:
|
Per dettagli tecnici, consulta la cartella `ai-prompts/`:
|
||||||
|
|
||||||
- `00-welcome-agent.md` - Panoramica progetto
|
- `00-welcome-agent.md` - Panoramica progetto
|
||||||
- `01-backend-plan.md` - Piano sviluppo backend
|
- `01-backend-plan.md` - Piano sviluppo backend
|
||||||
- `02-frontend-plan.md` - Piano sviluppo frontend
|
- `02-frontend-plan.md` - Piano sviluppo frontend
|
||||||
@@ -46,6 +65,10 @@ Per dettagli tecnici, consulta la cartella `ai-prompts/`:
|
|||||||
- **Badge Validatore:** `999999`
|
- **Badge Validatore:** `999999`
|
||||||
- **Password:** `focolari`
|
- **Password:** `focolari`
|
||||||
|
|
||||||
|
## 🔍 Debug
|
||||||
|
|
||||||
|
Accedi a `/debug` per diagnostica RFID in tempo reale.
|
||||||
|
|
||||||
## 📄 Licenza
|
## 📄 Licenza
|
||||||
|
|
||||||
Progetto privato - Movimento dei Focolari
|
Progetto privato - Movimento dei Focolari
|
||||||
|
|||||||
@@ -1,206 +1,150 @@
|
|||||||
# 🎯 Focolari Voting System - Guida Agente
|
# 🗳️ Focolari Voting System - Welcome Agent
|
||||||
|
|
||||||
## Panoramica Progetto
|
## Panoramica Progetto
|
||||||
|
|
||||||
**Nome:** Sistema Controllo Accessi "Focolari Voting System"
|
Sistema di controllo accessi per le assemblee di voto del **Movimento dei Focolari**.
|
||||||
**Committente:** Movimento dei Focolari
|
Applicazione web ottimizzata per **tablet in orizzontale** che gestisce i varchi d'ingresso alle sale votazione tramite
|
||||||
**Scopo:** Gestione dei varchi di accesso per le assemblee di voto del Movimento.
|
**lettori RFID USB** (emulano tastiera).
|
||||||
|
|
||||||
### Contesto d'Uso
|
|
||||||
|
|
||||||
Il sistema funziona su **tablet Android** (via browser Chrome) posizionati ai varchi d'ingresso delle sale votazione. Ogni tablet è collegato a un **lettore RFID USB** che emula tastiera.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏗️ Architettura
|
## Stack Tecnologico
|
||||||
|
|
||||||
|
### Backend Mock
|
||||||
|
|
||||||
|
- **Python 3.11+** con **FastAPI**
|
||||||
|
- **Pydantic** per validazione dati
|
||||||
|
- **Uvicorn** come server ASGI
|
||||||
|
- **Pipenv** per gestione dipendenze
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **React 18** + **TypeScript**
|
||||||
|
- **Vite** come build tool
|
||||||
|
- **Tailwind CSS 4** per styling
|
||||||
|
- **React Router** per navigazione
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Struttura Progetto
|
||||||
|
|
||||||
```
|
```
|
||||||
VotoFocolari/
|
VotoFocolari/
|
||||||
├── ai-prompts/ # Documentazione e piani di sviluppo
|
├── dev.sh # Script di sviluppo (install, dev, server, ...)
|
||||||
├── backend-mock/ # Python FastAPI (server mock)
|
├── README.md
|
||||||
|
├── ai-prompts/ # Documentazione sviluppo
|
||||||
|
│ ├── 00-welcome-agent.md # Questo file
|
||||||
|
│ ├── 01-backend-plan.md # Piano backend
|
||||||
|
│ └── 02-frontend-plan.md # Piano frontend
|
||||||
|
├── backend-mock/
|
||||||
│ ├── main.py # Entry point con argparse
|
│ ├── main.py # Entry point con argparse
|
||||||
│ ├── Pipfile # Dipendenze pipenv
|
│ ├── Pipfile # Dipendenze Python
|
||||||
│ ├── api/ # Routes FastAPI
|
│ ├── api/routes.py # Endpoint API
|
||||||
│ ├── schemas/ # Modelli Pydantic
|
│ ├── schemas/models.py # Modelli Pydantic
|
||||||
│ └── data/ # Dataset JSON (default, test)
|
│ └── data/
|
||||||
└── frontend/ # React + TypeScript + Vite + Tailwind
|
│ ├── users_default.json
|
||||||
|
│ └── users_test.json
|
||||||
|
└── frontend/
|
||||||
|
├── package.json
|
||||||
|
├── vite.config.ts
|
||||||
└── src/
|
└── src/
|
||||||
├── App.tsx # State machine principale
|
├── App.tsx # State machine principale
|
||||||
├── hooks/ # Custom hooks (RFID scanner)
|
├── hooks/ # useRFIDScanner
|
||||||
├── components/ # UI components
|
├── components/ # UI components
|
||||||
├── screens/ # Schermate complete
|
├── screens/ # Schermate
|
||||||
├── services/ # API layer
|
├── services/ # API client
|
||||||
├── types/ # TypeScript definitions
|
└── types/ # TypeScript types
|
||||||
└── tests/ # Test automatici use case
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Stack Tecnologico
|
## Funzionalità Principali
|
||||||
|
|
||||||
### Backend Mock
|
### Flusso Utente
|
||||||
- **Python 3.10+**
|
|
||||||
- **FastAPI** - Framework web asincrono
|
|
||||||
- **Uvicorn** - ASGI server
|
|
||||||
- **Pydantic** - Validazione dati
|
|
||||||
- **pipenv** - Gestione ambiente virtuale
|
|
||||||
- **argparse** - Parametri CLI (porta, dataset)
|
|
||||||
|
|
||||||
### Frontend
|
1. **Login Validatore**: Passa badge + inserisci password → Sessione 30 min
|
||||||
- **React 19** - UI Library
|
2. **Attesa Partecipante**: Schermata grande "Passa il badge"
|
||||||
- **TypeScript 5.9** - Type safety
|
3. **Visualizzazione Utente**: Card con foto, nome, ruolo, stato ammissione
|
||||||
- **Vite 7** - Build tool
|
4. **Conferma Ingresso**: Validatore ripassa il badge → Carosello benvenuto multilingua
|
||||||
- **Tailwind CSS 4** - Styling utility-first
|
|
||||||
- **React Router** - Routing (/, /debug)
|
### Gestione RFID
|
||||||
- **Vitest** - Testing framework
|
|
||||||
- **Design:** Ottimizzato per touch tablet
|
- **Multi-pattern**: Supporta layout tastiera US (`;?`) e IT (`ò_`)
|
||||||
|
- **Timeout 2.5s**: Per scansioni accidentali
|
||||||
|
- **ESC annulla**: Scansione in corso
|
||||||
|
- **Enter handling**: Gestito automaticamente
|
||||||
|
|
||||||
|
### Sicurezza Sessioni
|
||||||
|
|
||||||
|
- Sessione salvata in localStorage
|
||||||
|
- **Invalidazione automatica** se il server riparte
|
||||||
|
- Timeout 30 minuti
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎨 Design System
|
## Comandi Rapidi
|
||||||
|
|
||||||
| Colore | Hex | Uso |
|
|
||||||
|--------|-----|-----|
|
|
||||||
| Blu Istituzionale | `#0072CE` | Colore primario, brand |
|
|
||||||
| Arancio Accento | `#F5A623` | Azioni secondarie |
|
|
||||||
| Giallo | `#FFD700` | Evidenze |
|
|
||||||
| Verde Success | `#22C55E` | Conferme, ammesso |
|
|
||||||
| Rosso Error | `#EF4444` | Errori, non ammesso |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📟 Logica RFID (CRITICA)
|
|
||||||
|
|
||||||
Il lettore RFID simula una tastiera. **Non possiamo distinguerlo dalla digitazione umana** in base alla velocità.
|
|
||||||
|
|
||||||
### Protocollo
|
|
||||||
- **Formato:** `<start_sentinel><codice><end_sentinel>`
|
|
||||||
- **Esempio US:** `;00012345?`
|
|
||||||
- **Esempio IT:** `ò00012345_`
|
|
||||||
|
|
||||||
### Pattern Supportati
|
|
||||||
```typescript
|
|
||||||
const VALID_PATTERNS = [
|
|
||||||
{ start: ';', end: '?' }, // Layout US
|
|
||||||
{ start: 'ò', end: '_' }, // Layout IT
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### Strategia
|
|
||||||
1. Ascolto globale `keydown`
|
|
||||||
2. Se ricevo un carattere `start` → avvio buffer + timeout (2.5s)
|
|
||||||
3. Accumulo caratteri nel buffer
|
|
||||||
4. Se ricevo il corretto `end` → emetto codice pulito
|
|
||||||
5. Se timeout scade → scarto buffer
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 State Machine Applicativa
|
|
||||||
|
|
||||||
```
|
|
||||||
LOADING → WAITING_VALIDATOR → [badge validatore]
|
|
||||||
→ VALIDATOR_PASSWORD → [password OK]
|
|
||||||
→ GATE_ACTIVE → [badge partecipante]
|
|
||||||
→ SHOWING_USER → [badge validatore]
|
|
||||||
→ SUCCESS_MODAL (5s, carosello multilingua) → GATE_ACTIVE
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌍 Messaggi Benvenuto (Frontend)
|
|
||||||
|
|
||||||
Il backend risponde in modo **asettico** (solo `success: true`).
|
|
||||||
Il frontend mostra un **carosello automatico** di messaggi multilingua:
|
|
||||||
|
|
||||||
- Italiano, English, Français, Deutsch, Español, Português, 中文, 日本語
|
|
||||||
|
|
||||||
Scorrimento ogni ~2 secondi, durata modale 5 secondi.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 File Chiave
|
|
||||||
|
|
||||||
| File | Descrizione |
|
|
||||||
|------|-------------|
|
|
||||||
| `backend-mock/main.py` | Entry point con argparse |
|
|
||||||
| `backend-mock/api/routes.py` | Definizione endpoint |
|
|
||||||
| `backend-mock/schemas/models.py` | Modelli Pydantic |
|
|
||||||
| `backend-mock/data/*.json` | Dataset utenti |
|
|
||||||
| `frontend/src/hooks/useRFIDScanner.ts` | Cuore del sistema - gestione lettore |
|
|
||||||
| `frontend/src/App.tsx` | State machine e orchestrazione |
|
|
||||||
| `frontend/src/components/WelcomeCarousel.tsx` | Carosello multilingua |
|
|
||||||
| `frontend/src/screens/DebugScreen.tsx` | Diagnostica RFID |
|
|
||||||
| `frontend/src/tests/` | Test automatici use case |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
```bash
|
```bash
|
||||||
cd backend-mock
|
# Setup iniziale
|
||||||
pipenv install
|
./dev.sh install
|
||||||
pipenv run python main.py # Default
|
|
||||||
pipenv run python main.py -p 9000 # Porta custom
|
|
||||||
pipenv run python main.py -d data/test.json # Dataset test
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend
|
# Sviluppo (hot reload)
|
||||||
```bash
|
./dev.sh dev
|
||||||
cd frontend
|
# Frontend: http://localhost:5173
|
||||||
npm install
|
# Backend: http://localhost:8000
|
||||||
npm run dev # Sviluppo
|
|
||||||
npm run test # Test suite
|
# Produzione locale
|
||||||
npm run test:ui # Test con UI
|
./dev.sh server
|
||||||
|
# App completa: http://localhost:8000
|
||||||
|
|
||||||
|
# Debug RFID
|
||||||
|
# Vai a http://localhost:8000/debug
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Test Automatici
|
## Badge di Test
|
||||||
|
|
||||||
Suite di test per validazione use case:
|
| Badge | Nome | Ruolo | Ammesso |
|
||||||
|
|--------------|----------------|---------|---------------|
|
||||||
|
| `0008988288` | Marco Bianchi | Votante | ✅ |
|
||||||
|
| `0007399575` | Laura Rossi | Votante | ✅ |
|
||||||
|
| `0000514162` | Giuseppe Verdi | Tecnico | ❌ |
|
||||||
|
| `0006478281` | - | - | ⚠️ Non nel DB |
|
||||||
|
|
||||||
| Test | Descrizione |
|
**Password validatore:** `focolari`
|
||||||
|------|-------------|
|
|
||||||
| UC01 | Login validatore |
|
|
||||||
| UC02 | Accesso partecipante ammesso |
|
|
||||||
| UC03 | Accesso negato |
|
|
||||||
| UC04 | Timeout sessione |
|
|
||||||
| UC05 | Cambio rapido badge |
|
|
||||||
| UC06 | Pattern RFID multipli |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔍 Debug
|
## Dove Trovare Cosa
|
||||||
|
|
||||||
### Console Browser
|
| Cosa cerchi | Dove guardare |
|
||||||
Tutti i log sono prefissati:
|
|----------------------------|----------------------------------------|
|
||||||
- `[RFID]` - Eventi lettore badge
|
| Logica flusso applicazione | `frontend/src/App.tsx` |
|
||||||
- `[FLOW]` - Transizioni stato
|
| Hook lettura RFID | `frontend/src/hooks/useRFIDScanner.ts` |
|
||||||
- `[API]` - Chiamate HTTP
|
| Chiamate API | `frontend/src/services/api.ts` |
|
||||||
|
| Endpoint backend | `backend-mock/api/routes.py` |
|
||||||
### Pagina Debug (`/debug`)
|
| Dati mock utenti | `backend-mock/data/users_default.json` |
|
||||||
Accesso: logo cliccabile 5 volte.
|
| Configurazione Vite | `frontend/vite.config.ts` |
|
||||||
Mostra in tempo reale:
|
|
||||||
- Ultimi 20 tasti premuti
|
|
||||||
- Stato scanner (idle/scanning)
|
|
||||||
- Buffer corrente
|
|
||||||
- Pattern attivo
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ Note Importanti
|
## Note Importanti
|
||||||
|
|
||||||
1. **Badge Validatore:** `999999` con password `focolari`
|
1. **Layout Tastiera**: Il lettore RFID potrebbe inviare caratteri diversi in base al layout OS. La pagina `/debug`
|
||||||
2. **Sessione:** 30 minuti di timeout, salvata in localStorage
|
aiuta a diagnosticare.
|
||||||
3. **Timeout utente:** 60 secondi sulla schermata decisione
|
|
||||||
4. **Multi-layout:** Il sistema supporta RFID su tastiera US e IT
|
2. **Server Restart**: Quando il server riparte, tutte le sessioni frontend vengono invalidate (controllato via
|
||||||
5. **Backend asettico:** Nessun messaggio multilingua dal server
|
`server_start_time`).
|
||||||
6. **Git:** Il progetto sfrutta un Repository per versionare e sviluppare, ma solo l'utente può eseguire comandi git, eccetto se richiesto diversamente, l'agente può solo chiedere di eseguire o suggerire cosa fare, ma mai prendere iniziative
|
|
||||||
|
3. **Badge Validatore**: Qualsiasi badge può diventare validatore se la password è corretta. Il badge viene memorizzato
|
||||||
|
nella sessione.
|
||||||
|
|
||||||
|
4. **NumLock**: Su desktop, viene mostrato un banner per ricordare di attivare NumLock.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Documentazione Correlata
|
## TODO (da concordare con committenti)
|
||||||
|
|
||||||
- `01-backend-plan.md` - Piano sviluppo backend
|
- [ ] Verificare se il badge validatore debba essere validato anche lato server
|
||||||
- `02-frontend-plan.md` - Piano sviluppo frontend
|
- [ ] Test automatici E2E per regression detection
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ Struttura modulare con separazione tra API, modelli e dati.
|
|||||||
backend-mock/
|
backend-mock/
|
||||||
├── main.py # Entry point con argparse
|
├── main.py # Entry point con argparse
|
||||||
├── Pipfile # Dipendenze pipenv
|
├── Pipfile # Dipendenze pipenv
|
||||||
├── requirements.txt # Backup dipendenze
|
|
||||||
├── .gitignore
|
├── .gitignore
|
||||||
├── api/
|
├── api/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
@@ -22,7 +21,7 @@ backend-mock/
|
|||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ └── models.py # Modelli Pydantic
|
│ └── models.py # Modelli Pydantic
|
||||||
└── data/
|
└── data/
|
||||||
├── users_default.json # Dataset utenti default
|
├── users_default.json # Dataset utenti default (badge reali)
|
||||||
└── users_test.json # Dataset per test
|
└── users_test.json # Dataset per test
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -35,36 +34,40 @@ backend-mock/
|
|||||||
- [x] Creare cartella `backend-mock/`
|
- [x] Creare cartella `backend-mock/`
|
||||||
- [x] Creare `Pipfile` per pipenv
|
- [x] Creare `Pipfile` per pipenv
|
||||||
- [x] Configurare `.gitignore` per Python
|
- [x] Configurare `.gitignore` per Python
|
||||||
- [ ] Creare struttura cartelle (`api/`, `schemas/`, `data/`)
|
- [x] Creare struttura cartelle (`api/`, `schemas/`, `data/`)
|
||||||
|
|
||||||
### 2. Modelli Pydantic (`schemas/models.py`)
|
### 2. Modelli Pydantic (`schemas/models.py`)
|
||||||
|
|
||||||
- [x] `LoginRequest` - badge + password
|
- [x] `LoginRequest` - badge + password
|
||||||
- [x] `EntryRequest` - user_badge + validator_password
|
- [x] `EntryRequest` - user_badge + validator_password
|
||||||
- [x] `UserResponse` - dati utente + warning opzionale
|
- [x] `UserResponse` - dati utente + warning opzionale
|
||||||
- [x] `RoomInfoResponse` - nome sala + meeting_id
|
- [x] `RoomInfoResponse` - nome sala + meeting_id + **server_start_time**
|
||||||
- [x] `LoginResponse` - success + message + token
|
- [x] `LoginResponse` - success + message + token
|
||||||
- [x] `EntryResponse` - success + message (SENZA welcome_message)
|
- [x] `EntryResponse` - success + message (SENZA welcome_message)
|
||||||
- [ ] **DA FARE:** Spostare modelli in file dedicato
|
- [x] Spostare modelli in file dedicato
|
||||||
|
|
||||||
### 3. Dati Mock (`data/*.json`)
|
### 3. Dati Mock (`data/*.json`)
|
||||||
|
|
||||||
- [x] Costanti validatore (badge `999999`, password `focolari`)
|
- [x] Password validatore (solo password, badge gestito dal frontend)
|
||||||
- [x] Lista utenti mock (7 utenti con dati realistici)
|
- [x] Lista utenti mock con **badge reali**:
|
||||||
- [x] Mix di ruoli: Votante, Tecnico, Ospite
|
- `0008988288` - Marco Bianchi (Votante, ammesso)
|
||||||
- [x] Alcuni con `ammesso: false` per test
|
- `0007399575` - Laura Rossi (Votante, ammessa)
|
||||||
- [x] URL foto placeholder (randomuser.me)
|
- `0000514162` - Giuseppe Verdi (Tecnico, NON ammesso)
|
||||||
- [ ] **DA FARE:** Estrarre dati in file JSON separato
|
- `0006478281` - **NON nel DB** (per test "non trovato")
|
||||||
- [ ] **DA FARE:** Creare dataset alternativo per test
|
- [x] Estrarre dati in file JSON separato
|
||||||
- [ ] **DA FARE:** Caricare dati dinamicamente all'avvio
|
- [x] Creare dataset alternativo per test (`users_test.json`)
|
||||||
|
- [x] Caricare dati dinamicamente all'avvio
|
||||||
|
|
||||||
**Nota:** I messaggi di benvenuto multilingua sono stati **RIMOSSI** dal backend.
|
**Nota:** I messaggi di benvenuto multilingua sono gestiti dal frontend con carosello.
|
||||||
Il frontend gestirà autonomamente la visualizzazione internazionale con carosello.
|
|
||||||
|
**TODO (da concordare con committenti):**
|
||||||
|
|
||||||
|
- Valutare se `login-validate` debba ricevere e verificare anche il badge del validatore
|
||||||
|
|
||||||
### 4. Routes API (`api/routes.py`)
|
### 4. Routes API (`api/routes.py`)
|
||||||
|
|
||||||
- [x] `GET /info-room` - info sala
|
- [x] `GET /info-room` - info sala + **server_start_time** per invalidare sessioni
|
||||||
- [x] `POST /login-validate` - autenticazione 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 con e senza zeri iniziali
|
||||||
@@ -73,27 +76,25 @@ Il frontend gestirà autonomamente la visualizzazione internazionale con carosel
|
|||||||
- [x] Verifica password validatore
|
- [x] Verifica password validatore
|
||||||
- [x] Verifica utente ammesso
|
- [x] Verifica utente ammesso
|
||||||
- [x] Risposta asettica (solo success + message)
|
- [x] Risposta asettica (solo success + message)
|
||||||
- [ ] **DA FARE:** Spostare routes in file dedicato
|
- [x] Spostare routes in file dedicato
|
||||||
|
|
||||||
### 5. Entry Point (`main.py`)
|
### 5. Entry Point (`main.py`)
|
||||||
|
|
||||||
- [x] Blocco `if __name__ == "__main__"`
|
- [x] Blocco `if __name__ == "__main__"`
|
||||||
- [x] Configurazione uvicorn (host, port)
|
- [x] Configurazione uvicorn (host, port)
|
||||||
- [x] Messaggi console all'avvio
|
- [x] Messaggi console all'avvio
|
||||||
- [ ] **DA FARE:** Implementare argparse con opzioni:
|
- [x] Implementare argparse con opzioni:
|
||||||
- `--port` / `-p` : porta server (default: 8000)
|
- `--port` / `-p` : porta server (default: 8000)
|
||||||
- `--data` / `-d` : path file JSON dati (default: `data/users_default.json`)
|
- `--data` / `-d` : path file JSON dati (default: `data/users_default.json`)
|
||||||
- `--host` : host binding (default: `0.0.0.0`)
|
- `--host` : host binding (default: `0.0.0.0`)
|
||||||
- [ ] **DA FARE:** Caricamento dinamico dati da JSON
|
- [x] Caricamento dinamico dati da JSON
|
||||||
- [ ] **DA FARE:** Import routes da modulo `api`
|
- [x] Import routes da modulo `api`
|
||||||
|
|
||||||
### 6. Struttura Base FastAPI
|
### 6. Invalidazione Sessioni
|
||||||
|
|
||||||
- [x] Import FastAPI e dipendenze
|
- [x] `SERVER_START_TIME` generato all'avvio del server
|
||||||
- [x] Configurazione app con titolo e descrizione
|
- [x] Restituito in `/info-room` per permettere al frontend di invalidare sessioni vecchie
|
||||||
- [x] Middleware CORS (allow all origins)
|
- [x] Se il server riparte, tutte le sessioni frontend vengono invalidate
|
||||||
- [x] Endpoint root `/` per health check
|
|
||||||
- [ ] **DA FARE:** Refactor in struttura modulare
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -101,19 +102,16 @@ Il frontend gestirà autonomamente la visualizzazione internazionale con carosel
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"validator": {
|
"validator_password": "focolari",
|
||||||
"badge": "999999",
|
|
||||||
"password": "focolari"
|
|
||||||
},
|
|
||||||
"room": {
|
"room": {
|
||||||
"room_name": "Sala Assemblea",
|
"room_name": "Sala Assemblea",
|
||||||
"meeting_id": "VOT-2024"
|
"meeting_id": "VOT-2024"
|
||||||
},
|
},
|
||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"badge_code": "000001",
|
"badge_code": "0008988288",
|
||||||
"nome": "Maria",
|
"nome": "Marco",
|
||||||
"cognome": "Rossi",
|
"cognome": "Bianchi",
|
||||||
"url_foto": "https://...",
|
"url_foto": "https://...",
|
||||||
"ruolo": "Votante",
|
"ruolo": "Votante",
|
||||||
"ammesso": true
|
"ammesso": true
|
||||||
@@ -126,25 +124,22 @@ Il frontend gestirà autonomamente la visualizzazione internazionale con carosel
|
|||||||
|
|
||||||
## Comandi Esecuzione
|
## Comandi Esecuzione
|
||||||
|
|
||||||
### Avvio Standard
|
### Via Script (consigliato)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev.sh server # Avvia server con frontend
|
||||||
|
./dev.sh server -p 9000 # Porta custom
|
||||||
|
./dev.sh backend # Solo API, no frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manuale
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend-mock
|
cd backend-mock
|
||||||
pipenv install
|
pipenv install
|
||||||
pipenv run python main.py
|
pipenv run python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Avvio con Parametri Custom
|
|
||||||
```bash
|
|
||||||
# Porta diversa
|
|
||||||
pipenv run python main.py --port 9000
|
|
||||||
|
|
||||||
# Dataset test
|
|
||||||
pipenv run python main.py --data data/users_test.json
|
|
||||||
|
|
||||||
# Combinato
|
|
||||||
pipenv run python main.py -p 9000 -d data/users_test.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test Rapidi
|
## Test Rapidi
|
||||||
@@ -153,29 +148,28 @@ pipenv run python main.py -p 9000 -d data/users_test.json
|
|||||||
# Health check
|
# Health check
|
||||||
curl http://localhost:8000/
|
curl http://localhost:8000/
|
||||||
|
|
||||||
# Info sala
|
# Info sala (include server_start_time)
|
||||||
curl http://localhost:8000/info-room
|
curl http://localhost:8000/info-room
|
||||||
|
|
||||||
# Ricerca utente
|
# Ricerca utente reale
|
||||||
curl http://localhost:8000/anagrafica/000001
|
curl http://localhost:8000/anagrafica/0008988288
|
||||||
|
|
||||||
|
# Badge non trovato
|
||||||
|
curl http://localhost:8000/anagrafica/0006478281
|
||||||
|
|
||||||
# Login validatore
|
# Login validatore
|
||||||
curl -X POST http://localhost:8000/login-validate \
|
curl -X POST http://localhost:8000/login-validate \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"badge": "999999", "password": "focolari"}'
|
-d '{"badge": "qualsiasi", "password": "focolari"}'
|
||||||
|
|
||||||
# Richiesta ingresso (risposta asettica)
|
# Richiesta ingresso
|
||||||
curl -X POST http://localhost:8000/entry-request \
|
curl -X POST http://localhost:8000/entry-request \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"user_badge": "000001", "validator_password": "focolari"}'
|
-d '{"user_badge": "0008988288", "validator_password": "focolari"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Note Implementative
|
## ✅ BACKEND COMPLETATO
|
||||||
|
|
||||||
- Gli endpoint puliscono automaticamente i caratteri `;`, `?`, `ò`, `_` dai badge
|
Tutti i task sono stati implementati e testati.
|
||||||
- Il confronto badge avviene sia con che senza zeri iniziali
|
|
||||||
- Le risposte seguono lo standard HTTP (200 OK, 401 Unauthorized, 404 Not Found, 403 Forbidden)
|
|
||||||
- La documentazione OpenAPI è auto-generata su `/docs`
|
|
||||||
- **Risposta `/entry-request`:** JSON asettico `{ success: true, message: "..." }` senza messaggi multilingua
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Obiettivo
|
## Obiettivo
|
||||||
|
|
||||||
Applicazione React per tablet che gestisce il flusso di accesso ai varchi votazione, con lettura badge RFID.
|
Applicazione React per tablet che gestisce il flusso di accesso ai varchi votazione, con lettura badge RFID.
|
||||||
Include sistema di test automatici per validazione e regression detection.
|
Ottimizzata per tablet in orizzontale.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,8 +14,11 @@ Include sistema di test automatici per validazione e regression detection.
|
|||||||
- [x] Inizializzare Vite + React + TypeScript
|
- [x] Inizializzare Vite + React + TypeScript
|
||||||
- [x] Installare Tailwind CSS 4
|
- [x] Installare Tailwind CSS 4
|
||||||
- [x] Configurare `tsconfig.json`
|
- [x] Configurare `tsconfig.json`
|
||||||
- [x] Configurare `vite.config.ts`
|
- [x] Configurare `vite.config.ts` con proxy API
|
||||||
- [x] Configurare `.gitignore`
|
- [x] Configurare `.gitignore`
|
||||||
|
- [x] Path relativi per build (base: './')
|
||||||
|
- [x] Output build in `frontend/dist/`
|
||||||
|
- [x] Favicon con logo Focolari
|
||||||
|
|
||||||
### 2. Design System
|
### 2. Design System
|
||||||
|
|
||||||
@@ -29,15 +32,14 @@ Include sistema di test automatici per validazione e regression detection.
|
|||||||
|
|
||||||
### 3. Tipi TypeScript (`types/index.ts`)
|
### 3. Tipi TypeScript (`types/index.ts`)
|
||||||
|
|
||||||
- [x] `RoomInfo` - info sala
|
- [x] `RoomInfo` - info sala + **server_start_time**
|
||||||
- [x] `User` - dati utente
|
- [x] `User` - dati utente
|
||||||
- [x] `LoginRequest/Response`
|
- [x] `LoginRequest/Response`
|
||||||
- [x] `EntryRequest/Response` (SENZA welcome_message)
|
- [x] `EntryRequest/Response` (SENZA welcome_message)
|
||||||
- [x] `AppState` - stati applicazione
|
- [x] `AppState` - stati applicazione
|
||||||
- [x] `ValidatorSession` - sessione validatore
|
- [x] `ValidatorSession` - sessione validatore + **serverStartTime**
|
||||||
- [x] `RFIDScannerState` - stato scanner
|
- [x] `RFIDScannerState` - stato scanner
|
||||||
- [x] `RFIDScanResult` - risultato scan
|
- [x] `RFIDScanResult` - risultato scan
|
||||||
- [ ] **DA FARE:** Aggiornare `EntryResponse` rimuovendo `welcome_message`
|
|
||||||
|
|
||||||
### 4. API Service (`services/api.ts`)
|
### 4. API Service (`services/api.ts`)
|
||||||
|
|
||||||
@@ -47,259 +49,90 @@ Include sistema di test automatici per validazione e regression detection.
|
|||||||
- [x] `loginValidator()` - POST /login-validate
|
- [x] `loginValidator()` - POST /login-validate
|
||||||
- [x] `getUserByBadge()` - GET /anagrafica/{badge}
|
- [x] `getUserByBadge()` - GET /anagrafica/{badge}
|
||||||
- [x] `requestEntry()` - POST /entry-request
|
- [x] `requestEntry()` - POST /entry-request
|
||||||
- [ ] **DA FARE:** Logging con prefisso `[API]`
|
- [x] Logging con prefisso `[API]`
|
||||||
|
- [x] Path relativi (proxy Vite in dev, stesso server in prod)
|
||||||
|
|
||||||
### 5. Hook RFID Scanner (`hooks/useRFIDScanner.ts`)
|
### 5. Hook RFID Scanner (`hooks/useRFIDScanner.ts`)
|
||||||
|
|
||||||
#### Implementazione Base (v1)
|
- [x] Supporto pattern multipli (US: `;?`, IT: `ò_`)
|
||||||
|
- [x] Rilevamento automatico pattern in uso
|
||||||
- [x] Listener `keydown` globale
|
- [x] Gestione Enter post-completamento
|
||||||
- [x] Stati: idle → scanning → idle
|
- [x] Timeout 2.5s per scansioni accidentali
|
||||||
- [x] Singolo pattern (`;` / `?`)
|
- [x] ESC annulla scansione in corso
|
||||||
- [x] Timeout sicurezza (3s)
|
- [x] Logging avanzato con prefisso `[RFID]`
|
||||||
- [x] `preventDefault` durante scan
|
- [x] Export `keyLog` per debug
|
||||||
- [x] Callback `onScan`, `onTimeout`, `onScanStart`
|
|
||||||
|
|
||||||
#### ⚠️ AGGIORNAMENTO RICHIESTO (v2) - Multi-Pattern
|
|
||||||
|
|
||||||
- [ ] Supporto pattern multipli (US, IT, altri)
|
|
||||||
```typescript
|
|
||||||
const VALID_PATTERNS = [
|
|
||||||
{ start: ';', end: '?' }, // Layout US
|
|
||||||
{ start: 'ò', end: '_' }, // Layout IT
|
|
||||||
];
|
|
||||||
```
|
|
||||||
- [ ] Rilevamento automatico pattern in uso
|
|
||||||
- [ ] Memorizzazione pattern attivo durante scan
|
|
||||||
- [ ] Validazione `end` solo per pattern corretto
|
|
||||||
- [ ] Logging avanzato con prefisso `[RFID]`
|
|
||||||
- [ ] Esportare info pattern attivo per debug page
|
|
||||||
|
|
||||||
### 6. Componenti UI (`components/`)
|
### 6. Componenti UI (`components/`)
|
||||||
|
|
||||||
- [x] `Logo.tsx` - logo Focolari
|
- [x] `Logo.tsx` - logo Focolari
|
||||||
- [x] `Button.tsx` - varianti primary/secondary/danger
|
- [x] `Button.tsx` - varianti primary/secondary/danger
|
||||||
- [x] `Input.tsx` - campo input styled
|
- [x] `Input.tsx` - campo input styled + **toggle password visibility**
|
||||||
- [x] `Modal.tsx` - modale base
|
- [x] `Modal.tsx` - modale base
|
||||||
- [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] `index.ts` - barrel export
|
- [x] `WelcomeCarousel.tsx` - carosello messaggi multilingua
|
||||||
- [ ] **DA FARE:** `WelcomeCarousel.tsx` - carosello messaggi multilingua
|
- [x] `NumLockBanner.tsx` - avviso NumLock per desktop
|
||||||
|
|
||||||
### 7. Schermate (`screens/`)
|
### 7. Schermate (`screens/`)
|
||||||
|
|
||||||
- [x] `LoadingScreen.tsx` - caricamento iniziale
|
- [x] `LoadingScreen.tsx` - caricamento iniziale + ping automatico
|
||||||
- [x] `ValidatorLoginScreen.tsx` - attesa badge + password
|
- [x] `ValidatorLoginScreen.tsx` - attesa badge + password + NumLockBanner
|
||||||
- [x] `ActiveGateScreen.tsx` - varco attivo + scheda utente
|
- [x] `ActiveGateScreen.tsx` - varco attivo:
|
||||||
- [x] `SuccessModal.tsx` - conferma ingresso fullscreen
|
- [x] Card utente (layout largo per tablet)
|
||||||
|
- [x] **Schermata "badge non trovato"** con countdown 30s
|
||||||
|
- [x] **Notifica badge validatore ignorato**
|
||||||
|
- [x] NumLockBanner
|
||||||
|
- [x] `SuccessModal.tsx` - conferma ingresso con carosello
|
||||||
- [x] `ErrorModal.tsx` - errore fullscreen
|
- [x] `ErrorModal.tsx` - errore fullscreen
|
||||||
- [x] `index.ts` - barrel export
|
- [x] `DebugScreen.tsx` - pagina diagnostica RFID
|
||||||
- [ ] **DA FARE:** `DebugScreen.tsx` - pagina diagnostica RFID
|
|
||||||
|
|
||||||
### 8. State Machine (`App.tsx`)
|
### 8. State Machine (`App.tsx`)
|
||||||
|
|
||||||
- [x] Stati applicazione gestiti
|
- [x] Stati applicazione gestiti
|
||||||
- [x] Integrazione `useRFIDScanner`
|
- [x] Integrazione `useRFIDScanner`
|
||||||
- [x] Gestione sessione validatore (localStorage)
|
- [x] Gestione sessione validatore (localStorage)
|
||||||
|
- [x] **Qualsiasi badge può essere validatore** (verificato con password)
|
||||||
|
- [x] Password salvata in sessione per conferme ingresso
|
||||||
|
- [x] **Invalidazione sessione se server riparte** (serverStartTime)
|
||||||
- [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] Cambio rapido badge partecipante
|
- [x] Cambio rapido badge partecipante
|
||||||
- [x] Conferma con badge validatore
|
- [x] Conferma con badge validatore (quello della sessione)
|
||||||
- [ ] **DA FARE:** Logging transizioni con prefisso `[FLOW]`
|
- [x] **Notifica se badge validatore rippassato senza utente**
|
||||||
|
- [x] Logging transizioni con prefisso `[FLOW]`
|
||||||
|
|
||||||
### 9. Modale Successo - Carosello Internazionale
|
### 9. Modale Successo - Carosello Internazionale
|
||||||
|
|
||||||
#### ⚠️ MODIFICA RICHIESTA
|
- [x] Componente `WelcomeCarousel.tsx`
|
||||||
|
- [x] 10 lingue supportate
|
||||||
Il backend **NON** restituisce più messaggi multilingua.
|
- [x] Scorrimento automatico ogni 800ms
|
||||||
Il frontend gestisce autonomamente la visualizzazione con un **carosello automatico**.
|
- [x] Modale fullscreen verde
|
||||||
|
- [x] Durata totale: 5 secondi
|
||||||
**Specifiche:**
|
|
||||||
- [ ] Creare componente `WelcomeCarousel.tsx`
|
|
||||||
- [ ] Lista messaggi di benvenuto in diverse lingue:
|
|
||||||
```typescript
|
|
||||||
const WELCOME_MESSAGES = [
|
|
||||||
{ lang: 'it', text: 'Benvenuto!' },
|
|
||||||
{ lang: 'en', text: 'Welcome!' },
|
|
||||||
{ lang: 'fr', text: 'Bienvenue!' },
|
|
||||||
{ lang: 'de', text: 'Willkommen!' },
|
|
||||||
{ lang: 'es', text: 'Bienvenido!' },
|
|
||||||
{ lang: 'pt', text: 'Bem-vindo!' },
|
|
||||||
{ lang: 'zh', text: '欢迎!' },
|
|
||||||
{ lang: 'ja', text: 'ようこそ!' },
|
|
||||||
];
|
|
||||||
```
|
|
||||||
- [ ] Scorrimento automatico (es. ogni 2 secondi)
|
|
||||||
- [ ] Animazione transizione fluida (fade o slide)
|
|
||||||
- [ ] Modale fullscreen verde con carosello al centro
|
|
||||||
- [ ] Durata totale modale: 5 secondi
|
|
||||||
|
|
||||||
### 10. Debug & Diagnostica
|
### 10. Debug & Diagnostica
|
||||||
|
|
||||||
#### ⚠️ DA IMPLEMENTARE
|
- [x] Pagina `/debug` dedicata
|
||||||
|
- [x] Logging console strutturato `[RFID]`, `[FLOW]`, `[API]`
|
||||||
- [ ] Pagina `/debug` dedicata
|
|
||||||
- [ ] Log ultimi 20 tasti premuti (key + code)
|
|
||||||
- [ ] Stato scanner real-time (idle/scanning)
|
|
||||||
- [ ] Buffer corrente
|
|
||||||
- [ ] Pattern attivo (US/IT/...)
|
|
||||||
- [ ] Ultimo codice rilevato
|
|
||||||
- [ ] Timestamp eventi
|
|
||||||
- [ ] Link/pulsante nascosto per accesso debug (es. logo cliccabile 5 volte)
|
|
||||||
- [ ] Logging console strutturato:
|
|
||||||
- [ ] `[RFID]` - eventi scanner
|
|
||||||
- [ ] `[FLOW]` - transizioni stato
|
|
||||||
- [ ] `[API]` - chiamate HTTP
|
|
||||||
|
|
||||||
### 11. Routing
|
### 11. Routing
|
||||||
|
|
||||||
- [ ] Installare React Router
|
- [x] React Router
|
||||||
- [ ] Route principale `/`
|
- [x] Route principale `/`
|
||||||
- [ ] Route debug `/debug`
|
- [x] Route debug `/debug`
|
||||||
|
|
||||||
### 12. Test Automatici (E2E / Use Case Validation)
|
|
||||||
|
|
||||||
#### ⚠️ NUOVA SEZIONE
|
|
||||||
|
|
||||||
Sistema di test per validazione formale dei flussi e regression detection.
|
|
||||||
|
|
||||||
**Struttura:**
|
|
||||||
```
|
|
||||||
frontend/
|
|
||||||
├── src/
|
|
||||||
│ └── tests/
|
|
||||||
│ ├── usecase/
|
|
||||||
│ │ ├── UC01_ValidatorLogin.test.ts
|
|
||||||
│ │ ├── UC02_ParticipantAccess.test.ts
|
|
||||||
│ │ ├── UC03_DeniedAccess.test.ts
|
|
||||||
│ │ ├── UC04_SessionTimeout.test.ts
|
|
||||||
│ │ ├── UC05_QuickBadgeSwitch.test.ts
|
|
||||||
│ │ └── UC06_RFIDMultiPattern.test.ts
|
|
||||||
│ └── helpers/
|
|
||||||
│ ├── mockRFID.ts # Simulatore eventi tastiera RFID
|
|
||||||
│ └── testUtils.ts # Utility comuni
|
|
||||||
```
|
|
||||||
|
|
||||||
**Use Case da Testare:**
|
|
||||||
|
|
||||||
- [ ] **UC01 - Login Validatore**
|
|
||||||
- Simula badge validatore (`;999999?`)
|
|
||||||
- Inserisce password corretta
|
|
||||||
- Verifica transizione a stato `gate-active`
|
|
||||||
- Verifica sessione salvata in localStorage
|
|
||||||
|
|
||||||
- [ ] **UC02 - Accesso Partecipante Ammesso**
|
|
||||||
- Da stato `gate-active`
|
|
||||||
- Simula badge partecipante ammesso
|
|
||||||
- Verifica caricamento dati utente
|
|
||||||
- Simula badge validatore per conferma
|
|
||||||
- Verifica modale successo con carosello
|
|
||||||
- Verifica ritorno a `gate-active`
|
|
||||||
|
|
||||||
- [ ] **UC03 - Accesso Negato**
|
|
||||||
- Simula badge partecipante NON ammesso
|
|
||||||
- Verifica visualizzazione warning rosso
|
|
||||||
- Verifica che conferma validatore sia bloccata
|
|
||||||
- Verifica pulsante annulla funzionante
|
|
||||||
|
|
||||||
- [ ] **UC04 - Timeout Sessione**
|
|
||||||
- Verifica scadenza sessione 30 minuti
|
|
||||||
- Verifica redirect a login validatore
|
|
||||||
- Verifica pulizia localStorage
|
|
||||||
|
|
||||||
- [ ] **UC05 - Cambio Rapido Badge**
|
|
||||||
- Con utente a schermo
|
|
||||||
- Simula nuovo badge partecipante
|
|
||||||
- Verifica sostituzione immediata dati
|
|
||||||
|
|
||||||
- [ ] **UC06 - Pattern RFID Multipli**
|
|
||||||
- Testa pattern US (`;` / `?`)
|
|
||||||
- Testa pattern IT (`ò` / `_`)
|
|
||||||
- Verifica stesso risultato finale
|
|
||||||
|
|
||||||
**Dipendenze Test:**
|
|
||||||
```bash
|
|
||||||
npm install -D vitest @testing-library/react @testing-library/user-event jsdom
|
|
||||||
```
|
|
||||||
|
|
||||||
**Script npm:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"test": "vitest",
|
|
||||||
"test:ui": "vitest --ui",
|
|
||||||
"test:coverage": "vitest --coverage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Correzioni Necessarie
|
## Badge di Test
|
||||||
|
|
||||||
### Hook RFID - Da Aggiornare
|
| Badge | Nome | Ruolo | Ammesso |
|
||||||
|
|--------------|----------------|---------|---------------|
|
||||||
Il file `useRFIDScanner.ts` attualmente supporta **solo** il pattern US (`;` / `?`).
|
| `0008988288` | Marco Bianchi | Votante | ✅ Sì |
|
||||||
|
| `0007399575` | Laura Rossi | Votante | ✅ Sì |
|
||||||
**Modifiche richieste:**
|
| `0000514162` | Giuseppe Verdi | Tecnico | ❌ No |
|
||||||
|
| `0006478281` | - | - | ⚠️ Non nel DB |
|
||||||
1. Aggiungere costante `VALID_PATTERNS` con pattern multipli
|
|
||||||
2. Modificare logica `handleKeyDown` per:
|
|
||||||
- Riconoscere qualsiasi `start` sentinel
|
|
||||||
- Memorizzare quale pattern è in uso
|
|
||||||
- Validare solo con l'`end` corrispondente
|
|
||||||
3. Aggiungere stato `activePattern` per debug
|
|
||||||
4. Migliorare logging
|
|
||||||
|
|
||||||
### Success Modal - Da Aggiornare
|
|
||||||
|
|
||||||
Attualmente usa `welcome_message` dal backend.
|
|
||||||
|
|
||||||
**Modifiche richieste:**
|
|
||||||
|
|
||||||
1. Rimuovere dipendenza da `welcome_message` API
|
|
||||||
2. Implementare `WelcomeCarousel` con messaggi hardcoded
|
|
||||||
3. Carosello auto-scroll ogni 2 secondi
|
|
||||||
4. Animazioni fluide tra messaggi
|
|
||||||
|
|
||||||
### Logging Console
|
|
||||||
|
|
||||||
Attualmente i log usano messaggi generici. Aggiornare tutti i `console.log` con prefissi standardizzati.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dipendenze da Aggiungere
|
## ✅ FRONTEND COMPLETATO
|
||||||
|
|
||||||
```bash
|
|
||||||
# Routing
|
|
||||||
npm install react-router-dom
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
npm install -D vitest @testing-library/react @testing-library/user-event jsdom @vitest/ui @vitest/coverage-v8
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comandi Esecuzione
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
npm run dev # Sviluppo
|
|
||||||
npm run build # Build produzione
|
|
||||||
npm run test # Test suite
|
|
||||||
npm run test:ui # Test con UI interattiva
|
|
||||||
npm run test:coverage # Coverage report
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Note UI/UX
|
|
||||||
|
|
||||||
- Font grandi per leggibilità tablet
|
|
||||||
- Pulsanti touch-friendly (min 48px)
|
|
||||||
- Feedback visivo immediato su azioni
|
|
||||||
- Animazioni fluide ma non invasive
|
|
||||||
- Supporto landscape e portrait
|
|
||||||
- Carosello benvenuto: transizioni smooth, leggibilità massima
|
|
||||||
|
|||||||
2
backend-mock/.gitignore
vendored
2
backend-mock/.gitignore
vendored
@@ -28,3 +28,5 @@ Thumbs.db
|
|||||||
# Local environment
|
# Local environment
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ verify_ssl = true
|
|||||||
name = "pypi"
|
name = "pypi"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
fastapi = ">=0.109.0"
|
fastapi = "*"
|
||||||
uvicorn = {extras = ["standard"], version = ">=0.27.0"}
|
uvicorn = "*"
|
||||||
pydantic = ">=2.5.0"
|
pydantic = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.10"
|
python_version = "3.14"
|
||||||
|
|
||||||
[scripts]
|
[scripts]
|
||||||
start = "python main.py"
|
start = "python main.py"
|
||||||
|
|||||||
7
backend-mock/api/__init__.py
Normal file
7
backend-mock/api/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Focolari Voting System - API Package
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .routes import router, init_data
|
||||||
|
|
||||||
|
__all__ = ["router", "init_data"]
|
||||||
153
backend-mock/api/routes.py
Normal file
153
backend-mock/api/routes.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
Focolari Voting System - API Routes
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from schemas import (
|
||||||
|
LoginRequest,
|
||||||
|
EntryRequest,
|
||||||
|
UserResponse,
|
||||||
|
RoomInfoResponse,
|
||||||
|
LoginResponse,
|
||||||
|
EntryResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Timestamp di avvio del server (per invalidare sessioni frontend)
|
||||||
|
SERVER_START_TIME = int(time.time() * 1000)
|
||||||
|
|
||||||
|
# Dati caricati dinamicamente dal main
|
||||||
|
_data = {
|
||||||
|
"validator_password": "",
|
||||||
|
"room": {"room_name": "", "meeting_id": ""},
|
||||||
|
"users": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Caratteri sentinel da pulire (tutti i layout supportati)
|
||||||
|
SENTINEL_CHARS = [";", "?", "ò", "_", "ç", "+"]
|
||||||
|
|
||||||
|
|
||||||
|
def init_data(data: dict):
|
||||||
|
"""Inizializza i dati caricati dal file JSON"""
|
||||||
|
global _data
|
||||||
|
_data = data
|
||||||
|
|
||||||
|
|
||||||
|
def clean_badge(badge: str) -> str:
|
||||||
|
"""Rimuove i caratteri sentinel dal badge"""
|
||||||
|
clean = badge.strip()
|
||||||
|
for char in SENTINEL_CHARS:
|
||||||
|
clean = clean.replace(char, "")
|
||||||
|
return clean
|
||||||
|
|
||||||
|
|
||||||
|
def find_user(badge_code: str) -> dict | None:
|
||||||
|
"""Cerca un utente per badge code"""
|
||||||
|
clean = clean_badge(badge_code)
|
||||||
|
for user in _data["users"]:
|
||||||
|
if user["badge_code"] == clean or user["badge_code"].lstrip("0") == clean.lstrip("0"):
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/info-room", response_model=RoomInfoResponse)
|
||||||
|
async def get_room_info():
|
||||||
|
"""Restituisce le informazioni sulla sala e la riunione corrente."""
|
||||||
|
return RoomInfoResponse(
|
||||||
|
room_name=_data["room"]["room_name"],
|
||||||
|
meeting_id=_data["room"]["meeting_id"],
|
||||||
|
server_start_time=SERVER_START_TIME
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login-validate", response_model=LoginResponse)
|
||||||
|
async def login_validate(request: LoginRequest):
|
||||||
|
"""
|
||||||
|
Valida la password del validatore.
|
||||||
|
|
||||||
|
Il badge viene passato dal frontend ma attualmente non viene verificato
|
||||||
|
lato server - serve solo per essere memorizzato nella sessione frontend.
|
||||||
|
|
||||||
|
TODO: Concordare con committenti se il badge debba essere verificato
|
||||||
|
anche lato server (es. lista badge validatori autorizzati).
|
||||||
|
"""
|
||||||
|
if request.password != _data["validator_password"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Password non corretta"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Il badge viene restituito per conferma, ma non è validato lato server
|
||||||
|
clean = clean_badge(request.badge) if request.badge else "unknown"
|
||||||
|
|
||||||
|
return LoginResponse(
|
||||||
|
success=True,
|
||||||
|
message="Login validatore effettuato con successo",
|
||||||
|
token=f"focolare-token-{clean}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/anagrafica/{badge_code}", response_model=UserResponse)
|
||||||
|
async def get_user_anagrafica(badge_code: str):
|
||||||
|
"""
|
||||||
|
Cerca un utente tramite il suo badge code.
|
||||||
|
"""
|
||||||
|
user = find_user(badge_code)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
clean = clean_badge(badge_code)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Utente con badge {clean} non trovato nel sistema"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = UserResponse(
|
||||||
|
badge_code=user["badge_code"],
|
||||||
|
nome=user["nome"],
|
||||||
|
cognome=user["cognome"],
|
||||||
|
url_foto=user["url_foto"],
|
||||||
|
ruolo=user["ruolo"],
|
||||||
|
ammesso=user["ammesso"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user["ammesso"]:
|
||||||
|
response.warning = "ATTENZIONE: Questo utente NON è autorizzato all'ingresso!"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/entry-request", response_model=EntryResponse)
|
||||||
|
async def process_entry_request(request: EntryRequest):
|
||||||
|
"""
|
||||||
|
Processa una richiesta di ingresso.
|
||||||
|
Risposta asettica senza messaggi multilingua.
|
||||||
|
"""
|
||||||
|
if request.validator_password != _data["validator_password"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Password validatore non corretta"
|
||||||
|
)
|
||||||
|
|
||||||
|
user = find_user(request.user_badge)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
clean = clean_badge(request.user_badge)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Utente con badge {clean} non trovato"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user["ammesso"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail=f"L'utente {user['nome']} {user['cognome']} NON è autorizzato all'ingresso"
|
||||||
|
)
|
||||||
|
|
||||||
|
return EntryResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"Ingresso registrato per {user['nome']} {user['cognome']}"
|
||||||
|
)
|
||||||
33
backend-mock/data/users_default.json
Normal file
33
backend-mock/data/users_default.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"validator_password": "focolari",
|
||||||
|
"room": {
|
||||||
|
"room_name": "Sala Assemblea",
|
||||||
|
"meeting_id": "VOT-2024"
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"nome": "Laura",
|
||||||
|
"cognome": "Rossi",
|
||||||
|
"url_foto": "https://randomuser.me/api/portraits/women/2.jpg",
|
||||||
|
"ruolo": "Votante",
|
||||||
|
"ammesso": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"badge_code": "0000514162",
|
||||||
|
"nome": "Giuseppe",
|
||||||
|
"cognome": "Verdi",
|
||||||
|
"url_foto": "https://randomuser.me/api/portraits/men/3.jpg",
|
||||||
|
"ruolo": "Tecnico",
|
||||||
|
"ammesso": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
33
backend-mock/data/users_test.json
Normal file
33
backend-mock/data/users_test.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"validator_password": "test123",
|
||||||
|
"room": {
|
||||||
|
"room_name": "Sala Test",
|
||||||
|
"meeting_id": "TEST-001"
|
||||||
|
},
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"badge_code": "111111",
|
||||||
|
"nome": "Test",
|
||||||
|
"cognome": "Ammesso",
|
||||||
|
"url_foto": "https://randomuser.me/api/portraits/lego/1.jpg",
|
||||||
|
"ruolo": "Votante",
|
||||||
|
"ammesso": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"badge_code": "222222",
|
||||||
|
"nome": "Test",
|
||||||
|
"cognome": "NonAmmesso",
|
||||||
|
"url_foto": "https://randomuser.me/api/portraits/lego/2.jpg",
|
||||||
|
"ruolo": "Ospite",
|
||||||
|
"ammesso": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"badge_code": "333333",
|
||||||
|
"nome": "Test",
|
||||||
|
"cognome": "Tecnico",
|
||||||
|
"url_foto": "https://randomuser.me/api/portraits/lego/3.jpg",
|
||||||
|
"ruolo": "Tecnico",
|
||||||
|
"ammesso": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,277 +1,191 @@
|
|||||||
"""
|
"""
|
||||||
Focolari Voting System - Backend Mock
|
Focolari Voting System - Backend Mock
|
||||||
Sistema di controllo accessi per votazioni del Movimento dei Focolari
|
Sistema di controllo accessi per votazioni del Movimento dei Focolari
|
||||||
|
|
||||||
|
Utilizzo:
|
||||||
|
python main.py # Default: porta 8000, dati default
|
||||||
|
python main.py -p 9000 # Porta custom
|
||||||
|
python main.py -d data/users_test.json # Dataset custom
|
||||||
|
python main.py -p 9000 -d data/users_test.json
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
import argparse
|
||||||
from fastapi import FastAPI, HTTPException
|
import json
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
import sys
|
||||||
from pydantic import BaseModel
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
app = FastAPI(
|
import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from api.routes import router, init_data
|
||||||
|
|
||||||
|
# Configurazione default
|
||||||
|
DEFAULT_PORT = 8000
|
||||||
|
DEFAULT_HOST = "0.0.0.0"
|
||||||
|
DEFAULT_DATA = "data/users_default.json"
|
||||||
|
STATIC_DIR = Path(__file__).parent.parent / "frontend" / "dist"
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(data_path: str) -> dict:
|
||||||
|
"""Carica i dati dal file JSON"""
|
||||||
|
path = Path(data_path)
|
||||||
|
|
||||||
|
if not path.is_absolute():
|
||||||
|
# Path relativo alla directory del main.py
|
||||||
|
base_dir = Path(__file__).parent
|
||||||
|
path = base_dir / path
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
print(f"❌ Errore: File dati non trovato: {path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(f"📂 Dati caricati da: {path}")
|
||||||
|
print(f" - Password validatore: {'*' * len(data['validator_password'])}")
|
||||||
|
print(f" - Sala: {data['room']['room_name']}")
|
||||||
|
print(f" - Utenti: {len(data['users'])}")
|
||||||
|
return data
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"❌ Errore parsing JSON: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except KeyError as e:
|
||||||
|
print(f"❌ Errore struttura JSON: chiave mancante {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(data: dict, serve_frontend: bool = True) -> FastAPI:
|
||||||
|
"""Crea e configura l'applicazione FastAPI"""
|
||||||
|
app = FastAPI(
|
||||||
title="Focolari Voting System API",
|
title="Focolari Voting System API",
|
||||||
description="Backend mock per il sistema di controllo accessi",
|
description="Backend mock per il sistema di controllo accessi",
|
||||||
version="1.0.0"
|
version="1.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
# CORS abilitato per tutti
|
# CORS abilitato per tutti (utile in sviluppo)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["*"],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# ============================================
|
# Inizializza i dati nelle routes
|
||||||
# MODELLI PYDANTIC
|
init_data(data)
|
||||||
# ============================================
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
# Registra le routes API
|
||||||
badge: str
|
app.include_router(router)
|
||||||
password: str
|
|
||||||
|
|
||||||
class EntryRequest(BaseModel):
|
# Serve frontend statico se la cartella esiste
|
||||||
user_badge: str
|
if serve_frontend and STATIC_DIR.exists():
|
||||||
validator_password: str
|
print(f"🌐 Frontend statico servito da: {STATIC_DIR}")
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
# Serve index.html per la root e tutte le route SPA
|
||||||
badge_code: str
|
@app.get("/")
|
||||||
nome: str
|
async def serve_index():
|
||||||
cognome: str
|
return FileResponse(STATIC_DIR / "index.html")
|
||||||
url_foto: str
|
|
||||||
ruolo: str
|
|
||||||
ammesso: bool
|
|
||||||
warning: Optional[str] = None
|
|
||||||
|
|
||||||
class RoomInfoResponse(BaseModel):
|
@app.get("/debug")
|
||||||
room_name: str
|
async def serve_debug():
|
||||||
meeting_id: str
|
return FileResponse(STATIC_DIR / "index.html")
|
||||||
|
|
||||||
class LoginResponse(BaseModel):
|
# Monta i file statici (JS, CSS, assets) - DEVE essere dopo le route
|
||||||
success: bool
|
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
|
||||||
message: str
|
|
||||||
token: Optional[str] = None
|
|
||||||
|
|
||||||
class EntryResponse(BaseModel):
|
# Fallback per altri file statici nella root (favicon, ecc.)
|
||||||
success: bool
|
@app.get("/{filename:path}")
|
||||||
message: str
|
async def serve_static(filename: str):
|
||||||
welcome_message: Optional[str] = None
|
file_path = STATIC_DIR / filename
|
||||||
|
if file_path.exists() and file_path.is_file():
|
||||||
# ============================================
|
return FileResponse(file_path)
|
||||||
# DATI MOCK
|
# Per route SPA sconosciute, serve index.html
|
||||||
# ============================================
|
return FileResponse(STATIC_DIR / "index.html")
|
||||||
|
else:
|
||||||
# Credenziali Validatore
|
# API-only mode
|
||||||
VALIDATOR_BADGE = "999999"
|
@app.get("/")
|
||||||
VALIDATOR_PASSWORD = "focolari"
|
async def root():
|
||||||
MOCK_TOKEN = "focolare-validator-token-2024"
|
|
||||||
|
|
||||||
# Lista utenti mock
|
|
||||||
USERS_DB = [
|
|
||||||
{
|
|
||||||
"badge_code": "000001",
|
|
||||||
"nome": "Maria",
|
|
||||||
"cognome": "Rossi",
|
|
||||||
"url_foto": "https://randomuser.me/api/portraits/women/1.jpg",
|
|
||||||
"ruolo": "Votante",
|
|
||||||
"ammesso": True
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"badge_code": "000002",
|
|
||||||
"nome": "Giuseppe",
|
|
||||||
"cognome": "Bianchi",
|
|
||||||
"url_foto": "https://randomuser.me/api/portraits/men/2.jpg",
|
|
||||||
"ruolo": "Votante",
|
|
||||||
"ammesso": True
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"badge_code": "000003",
|
|
||||||
"nome": "Anna",
|
|
||||||
"cognome": "Verdi",
|
|
||||||
"url_foto": "https://randomuser.me/api/portraits/women/3.jpg",
|
|
||||||
"ruolo": "Tecnico",
|
|
||||||
"ammesso": True
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"badge_code": "000004",
|
|
||||||
"nome": "Francesco",
|
|
||||||
"cognome": "Neri",
|
|
||||||
"url_foto": "https://randomuser.me/api/portraits/men/4.jpg",
|
|
||||||
"ruolo": "Ospite",
|
|
||||||
"ammesso": False # Non ammesso!
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"badge_code": "000005",
|
|
||||||
"nome": "Lucia",
|
|
||||||
"cognome": "Gialli",
|
|
||||||
"url_foto": "https://randomuser.me/api/portraits/women/5.jpg",
|
|
||||||
"ruolo": "Votante",
|
|
||||||
"ammesso": True
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"badge_code": "000006",
|
|
||||||
"nome": "Paolo",
|
|
||||||
"cognome": "Blu",
|
|
||||||
"url_foto": "https://randomuser.me/api/portraits/men/6.jpg",
|
|
||||||
"ruolo": "Votante",
|
|
||||||
"ammesso": False # Non ammesso!
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"badge_code": "123456",
|
|
||||||
"nome": "Teresa",
|
|
||||||
"cognome": "Martini",
|
|
||||||
"url_foto": "https://randomuser.me/api/portraits/women/7.jpg",
|
|
||||||
"ruolo": "Votante",
|
|
||||||
"ammesso": True
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Messaggi di benvenuto multilingua
|
|
||||||
WELCOME_MESSAGES = [
|
|
||||||
"Benvenuto! / Welcome!",
|
|
||||||
"Bienvenue! / Willkommen!",
|
|
||||||
"Bienvenido! / Bem-vindo!",
|
|
||||||
"欢迎! / 歓迎!",
|
|
||||||
"Добро пожаловать! / مرحبا!"
|
|
||||||
]
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ENDPOINTS
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Endpoint di test per verificare che il server sia attivo"""
|
"""Endpoint di test per verificare che il server sia attivo"""
|
||||||
return {"status": "ok", "message": "Focolari Voting System API is running"}
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Focolari Voting System API is running",
|
||||||
|
"room": data["room"]["room_name"],
|
||||||
|
"frontend": "not built - run 'npm run build' in frontend/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
@app.get("/info-room", response_model=RoomInfoResponse)
|
def parse_args():
|
||||||
async def get_room_info():
|
"""Parse degli argomenti da linea di comando"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Focolari Voting System - Backend Mock Server",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Esempi:
|
||||||
|
python main.py # Avvio standard
|
||||||
|
python main.py -p 9000 # Porta 9000
|
||||||
|
python main.py -d data/users_test.json # Dataset test
|
||||||
|
python main.py --host 127.0.0.1 -p 8080 # Solo localhost
|
||||||
|
python main.py --api-only # Solo API, no frontend
|
||||||
"""
|
"""
|
||||||
Restituisce le informazioni sulla sala e la riunione corrente.
|
|
||||||
"""
|
|
||||||
return RoomInfoResponse(
|
|
||||||
room_name="Sala Assemblea",
|
|
||||||
meeting_id="VOT-2024"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
@app.post("/login-validate", response_model=LoginResponse)
|
"-p", "--port",
|
||||||
async def login_validate(request: LoginRequest):
|
type=int,
|
||||||
"""
|
default=DEFAULT_PORT,
|
||||||
Valida le credenziali del validatore.
|
help=f"Porta del server (default: {DEFAULT_PORT})"
|
||||||
Il badge deve essere quello del validatore (999999) e la password corretta.
|
|
||||||
"""
|
|
||||||
# Pulisci il badge da eventuali caratteri sentinel
|
|
||||||
clean_badge = request.badge.strip().replace(";", "").replace("?", "")
|
|
||||||
|
|
||||||
if clean_badge != VALIDATOR_BADGE:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail="Badge validatore non riconosciuto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.password != VALIDATOR_PASSWORD:
|
parser.add_argument(
|
||||||
raise HTTPException(
|
"-d", "--data",
|
||||||
status_code=401,
|
type=str,
|
||||||
detail="Password non corretta"
|
default=DEFAULT_DATA,
|
||||||
|
help=f"Path al file JSON con i dati (default: {DEFAULT_DATA})"
|
||||||
)
|
)
|
||||||
|
|
||||||
return LoginResponse(
|
parser.add_argument(
|
||||||
success=True,
|
"--host",
|
||||||
message="Login validatore effettuato con successo",
|
type=str,
|
||||||
token=MOCK_TOKEN
|
default=DEFAULT_HOST,
|
||||||
|
help=f"Host di binding (default: {DEFAULT_HOST})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
@app.get("/anagrafica/{badge_code}", response_model=UserResponse)
|
"--api-only",
|
||||||
async def get_user_anagrafica(badge_code: str):
|
action="store_true",
|
||||||
"""
|
help="Avvia solo le API senza servire il frontend"
|
||||||
Cerca un utente tramite il suo badge code.
|
|
||||||
Restituisce i dati anagrafici e un warning se non è ammesso.
|
|
||||||
"""
|
|
||||||
# Pulisci il badge da eventuali caratteri sentinel
|
|
||||||
clean_badge = badge_code.strip().replace(";", "").replace("?", "")
|
|
||||||
|
|
||||||
# Normalizza: rimuovi zeri iniziali per il confronto, ma cerca anche con zeri
|
|
||||||
for user in USERS_DB:
|
|
||||||
if user["badge_code"] == clean_badge or user["badge_code"].lstrip("0") == clean_badge.lstrip("0"):
|
|
||||||
response = UserResponse(
|
|
||||||
badge_code=user["badge_code"],
|
|
||||||
nome=user["nome"],
|
|
||||||
cognome=user["cognome"],
|
|
||||||
url_foto=user["url_foto"],
|
|
||||||
ruolo=user["ruolo"],
|
|
||||||
ammesso=user["ammesso"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Aggiungi warning se non ammesso
|
return parser.parse_args()
|
||||||
if not user["ammesso"]:
|
|
||||||
response.warning = "ATTENZIONE: Questo utente NON è autorizzato all'ingresso!"
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Utente non trovato
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=404,
|
|
||||||
detail=f"Utente con badge {clean_badge} non trovato nel sistema"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/entry-request", response_model=EntryResponse)
|
def main():
|
||||||
async def process_entry_request(request: EntryRequest):
|
"""Entry point principale"""
|
||||||
"""
|
args = parse_args()
|
||||||
Processa una richiesta di ingresso.
|
|
||||||
Richiede il badge dell'utente e la password del validatore.
|
|
||||||
"""
|
|
||||||
# Pulisci i dati
|
|
||||||
clean_user_badge = request.user_badge.strip().replace(";", "").replace("?", "")
|
|
||||||
|
|
||||||
# Verifica password validatore
|
print("🚀 Avvio Focolari Voting System Backend...")
|
||||||
if request.validator_password != VALIDATOR_PASSWORD:
|
print("=" * 50)
|
||||||
raise HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail="Password validatore non corretta"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cerca l'utente
|
# Carica i dati
|
||||||
user_found = None
|
data = load_data(args.data)
|
||||||
for user in USERS_DB:
|
|
||||||
if user["badge_code"] == clean_user_badge or user["badge_code"].lstrip("0") == clean_user_badge.lstrip("0"):
|
|
||||||
user_found = user
|
|
||||||
break
|
|
||||||
|
|
||||||
if not user_found:
|
print("=" * 50)
|
||||||
raise HTTPException(
|
print(f"📍 Server in ascolto su http://{args.host}:{args.port}")
|
||||||
status_code=404,
|
print(f"📚 Documentazione API su http://{args.host}:{args.port}/docs")
|
||||||
detail=f"Utente con badge {clean_user_badge} non trovato"
|
if not args.api_only and STATIC_DIR.exists():
|
||||||
)
|
print(f"🌐 Frontend disponibile su http://{args.host}:{args.port}/")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
if not user_found["ammesso"]:
|
# Crea e avvia l'app
|
||||||
raise HTTPException(
|
app = create_app(data, serve_frontend=not args.api_only)
|
||||||
status_code=403,
|
uvicorn.run(app, host=args.host, port=args.port)
|
||||||
detail=f"L'utente {user_found['nome']} {user_found['cognome']} NON è autorizzato all'ingresso"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Successo! Genera messaggio di benvenuto casuale
|
|
||||||
welcome = random.choice(WELCOME_MESSAGES)
|
|
||||||
|
|
||||||
return EntryResponse(
|
|
||||||
success=True,
|
|
||||||
message=f"Ingresso registrato per {user_found['nome']} {user_found['cognome']}",
|
|
||||||
welcome_message=welcome
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# AVVIO SERVER
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
main()
|
||||||
print("🚀 Avvio Focolari Voting System Backend...")
|
|
||||||
print("📍 Server in ascolto su http://localhost:8000")
|
|
||||||
print("📚 Documentazione API su http://localhost:8000/docs")
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
||||||
|
|||||||
21
backend-mock/schemas/__init__.py
Normal file
21
backend-mock/schemas/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""
|
||||||
|
Focolari Voting System - Schemas Package
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
LoginRequest,
|
||||||
|
EntryRequest,
|
||||||
|
UserResponse,
|
||||||
|
RoomInfoResponse,
|
||||||
|
LoginResponse,
|
||||||
|
EntryResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LoginRequest",
|
||||||
|
"EntryRequest",
|
||||||
|
"UserResponse",
|
||||||
|
"RoomInfoResponse",
|
||||||
|
"LoginResponse",
|
||||||
|
"EntryResponse",
|
||||||
|
]
|
||||||
50
backend-mock/schemas/models.py
Normal file
50
backend-mock/schemas/models.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Focolari Voting System - Modelli Pydantic
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
"""Richiesta login validatore"""
|
||||||
|
badge: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class EntryRequest(BaseModel):
|
||||||
|
"""Richiesta ingresso partecipante"""
|
||||||
|
user_badge: str
|
||||||
|
validator_password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
"""Risposta dati utente"""
|
||||||
|
badge_code: str
|
||||||
|
nome: str
|
||||||
|
cognome: str
|
||||||
|
url_foto: str
|
||||||
|
ruolo: Literal["Tecnico", "Votante", "Ospite"]
|
||||||
|
ammesso: bool
|
||||||
|
warning: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoomInfoResponse(BaseModel):
|
||||||
|
"""Risposta info sala"""
|
||||||
|
room_name: str
|
||||||
|
meeting_id: str
|
||||||
|
server_start_time: int # Timestamp avvio server per invalidare sessioni
|
||||||
|
|
||||||
|
|
||||||
|
class LoginResponse(BaseModel):
|
||||||
|
"""Risposta login"""
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
token: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class EntryResponse(BaseModel):
|
||||||
|
"""Risposta richiesta ingresso (asettica, senza welcome_message)"""
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
258
dev.sh
Executable file
258
dev.sh
Executable file
@@ -0,0 +1,258 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================
|
||||||
|
# Focolari Voting System - Script di Sviluppo
|
||||||
|
# ============================================
|
||||||
|
#
|
||||||
|
# Utilizzo:
|
||||||
|
# ./dev.sh install - Installa dipendenze frontend e backend
|
||||||
|
# ./dev.sh dev - Avvia frontend (dev) + backend (api-only)
|
||||||
|
# ./dev.sh build - Builda il frontend
|
||||||
|
# ./dev.sh server - Builda frontend (se necessario) e avvia server completo
|
||||||
|
# ./dev.sh backend - Avvia solo il backend (api-only)
|
||||||
|
# ./dev.sh frontend - Avvia solo il frontend in dev mode
|
||||||
|
# ./dev.sh shell - Apre shell pipenv del backend
|
||||||
|
# ./dev.sh clean - Pulisce build e cache
|
||||||
|
# ./dev.sh help - Mostra questo messaggio
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Directory del progetto
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BACKEND_DIR="$SCRIPT_DIR/backend-mock"
|
||||||
|
FRONTEND_DIR="$SCRIPT_DIR/frontend"
|
||||||
|
FRONTEND_DIST="$FRONTEND_DIR/dist"
|
||||||
|
|
||||||
|
# Colori per output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funzioni di utilità
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verifica prerequisiti
|
||||||
|
check_prereqs() {
|
||||||
|
command -v node >/dev/null 2>&1 || error "Node.js non trovato. Installalo prima."
|
||||||
|
command -v npm >/dev/null 2>&1 || error "npm non trovato. Installalo prima."
|
||||||
|
command -v python3 >/dev/null 2>&1 || error "Python3 non trovato. Installalo prima."
|
||||||
|
command -v pipenv >/dev/null 2>&1 || error "pipenv non trovato. Installa con: pip install pipenv"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installa dipendenze
|
||||||
|
cmd_install() {
|
||||||
|
info "Installazione dipendenze..."
|
||||||
|
|
||||||
|
check_prereqs
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
info "Installazione dipendenze backend (pipenv)..."
|
||||||
|
cd "$BACKEND_DIR"
|
||||||
|
pipenv install
|
||||||
|
success "Backend installato"
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
info "Installazione dipendenze frontend (npm)..."
|
||||||
|
cd "$FRONTEND_DIR"
|
||||||
|
npm install
|
||||||
|
success "Frontend installato"
|
||||||
|
|
||||||
|
success "Tutte le dipendenze installate!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
cmd_build() {
|
||||||
|
info "Build frontend..."
|
||||||
|
cd "$FRONTEND_DIR"
|
||||||
|
npm run build
|
||||||
|
success "Frontend buildato in $FRONTEND_DIST"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verifica se serve rebuild
|
||||||
|
needs_rebuild() {
|
||||||
|
# Se dist non esiste, serve build
|
||||||
|
if [ ! -d "$FRONTEND_DIST" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Se dist/index.html non esiste, serve build
|
||||||
|
if [ ! -f "$FRONTEND_DIST/index.html" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Controlla se ci sono file frontend più recenti della build
|
||||||
|
local latest_src=$(find "$FRONTEND_DIR/src" -type f -newer "$FRONTEND_DIST/index.html" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$latest_src" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Controlla anche index.html e config
|
||||||
|
if [ "$FRONTEND_DIR/index.html" -nt "$FRONTEND_DIST/index.html" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$FRONTEND_DIR/vite.config.ts" -nt "$FRONTEND_DIST/index.html" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Server completo (build + backend)
|
||||||
|
cmd_server() {
|
||||||
|
check_prereqs
|
||||||
|
|
||||||
|
# Rebuild se necessario
|
||||||
|
if needs_rebuild; then
|
||||||
|
warn "Rilevati cambiamenti nel frontend, rebuild in corso..."
|
||||||
|
cmd_build
|
||||||
|
else
|
||||||
|
info "Frontend già aggiornato, skip build"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Avvia backend con frontend
|
||||||
|
info "Avvio server completo..."
|
||||||
|
cd "$BACKEND_DIR"
|
||||||
|
pipenv run python main.py "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Solo backend
|
||||||
|
cmd_backend() {
|
||||||
|
check_prereqs
|
||||||
|
info "Avvio backend (API only)..."
|
||||||
|
cd "$BACKEND_DIR"
|
||||||
|
pipenv run python main.py --api-only "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Solo frontend dev
|
||||||
|
cmd_frontend() {
|
||||||
|
check_prereqs
|
||||||
|
info "Avvio frontend in dev mode..."
|
||||||
|
cd "$FRONTEND_DIR"
|
||||||
|
npm run dev
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dev mode: backend api-only + frontend dev (in parallelo)
|
||||||
|
cmd_dev() {
|
||||||
|
check_prereqs
|
||||||
|
|
||||||
|
info "Avvio ambiente di sviluppo..."
|
||||||
|
info "Backend API: http://localhost:8000"
|
||||||
|
info "Frontend Dev: http://localhost:5173"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Avvia backend in background
|
||||||
|
cd "$BACKEND_DIR"
|
||||||
|
pipenv run python main.py --api-only &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
|
||||||
|
# Trap per cleanup
|
||||||
|
trap "kill $BACKEND_PID 2>/dev/null" EXIT
|
||||||
|
|
||||||
|
# Avvia frontend
|
||||||
|
cd "$FRONTEND_DIR"
|
||||||
|
npm run dev
|
||||||
|
}
|
||||||
|
|
||||||
|
# Shell pipenv
|
||||||
|
cmd_shell() {
|
||||||
|
info "Apertura shell pipenv backend..."
|
||||||
|
cd "$BACKEND_DIR"
|
||||||
|
pipenv shell
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pulizia
|
||||||
|
cmd_clean() {
|
||||||
|
info "Pulizia build e cache..."
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
rm -rf "$FRONTEND_DIR/node_modules/.vite"
|
||||||
|
rm -rf "$FRONTEND_DIST"
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
find "$BACKEND_DIR" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find "$BACKEND_DIR" -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
success "Pulizia completata"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Help
|
||||||
|
cmd_help() {
|
||||||
|
echo "============================================"
|
||||||
|
echo "Focolari Voting System - Script di Sviluppo"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
echo "Utilizzo: ./dev.sh <comando> [opzioni]"
|
||||||
|
echo ""
|
||||||
|
echo "Comandi disponibili:"
|
||||||
|
echo " install Installa dipendenze frontend e backend"
|
||||||
|
echo " dev Avvia frontend (dev) + backend (api-only) in parallelo"
|
||||||
|
echo " build Builda il frontend per produzione"
|
||||||
|
echo " server Builda frontend (se cambiato) e avvia server completo"
|
||||||
|
echo " backend Avvia solo il backend (api-only)"
|
||||||
|
echo " frontend Avvia solo il frontend in dev mode"
|
||||||
|
echo " shell Apre shell pipenv del backend"
|
||||||
|
echo " clean Pulisce build e cache"
|
||||||
|
echo " help Mostra questo messaggio"
|
||||||
|
echo ""
|
||||||
|
echo "Esempi:"
|
||||||
|
echo " ./dev.sh install # Setup iniziale"
|
||||||
|
echo " ./dev.sh dev # Sviluppo (hot reload)"
|
||||||
|
echo " ./dev.sh server # Produzione locale"
|
||||||
|
echo " ./dev.sh server -p 9000 # Server su porta 9000"
|
||||||
|
echo " ./dev.sh server -d data/users_test.json # Con dataset test"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
case "${1:-help}" in
|
||||||
|
install)
|
||||||
|
cmd_install
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
cmd_build
|
||||||
|
;;
|
||||||
|
server)
|
||||||
|
shift
|
||||||
|
cmd_server "$@"
|
||||||
|
;;
|
||||||
|
backend)
|
||||||
|
shift
|
||||||
|
cmd_backend "$@"
|
||||||
|
;;
|
||||||
|
frontend)
|
||||||
|
cmd_frontend
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
cmd_dev
|
||||||
|
;;
|
||||||
|
shell)
|
||||||
|
cmd_shell
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
cmd_clean
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
cmd_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Comando sconosciuto: $1. Usa './dev.sh help' per la lista comandi."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -4,12 +4,16 @@ This template provides a minimal setup to get React working in Vite with HMR and
|
|||||||
|
|
||||||
Currently, two official plugins are available:
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react)
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used
|
||||||
|
in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc)
|
||||||
|
uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
## React Compiler
|
## React Compiler
|
||||||
|
|
||||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it,
|
||||||
|
see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
@@ -43,7 +47,10 @@ export default defineConfig([
|
|||||||
])
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
You can also
|
||||||
|
install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x)
|
||||||
|
and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom)
|
||||||
|
for React-specific lint rules:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// eslint.config.js
|
// eslint.config.js
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import globals from 'globals'
|
|||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import {defineConfig, globalIgnores} from 'eslint/config'
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist']),
|
globalIgnores(['dist']),
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="it">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8"/>
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link href="./favicon.jpg" rel="icon" type="image/jpeg"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title>frontend</title>
|
<title>Focolari Voting System</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script src="/src/main.tsx" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
60
frontend/package-lock.json
generated
60
frontend/package-lock.json
generated
@@ -9,7 +9,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@@ -2265,6 +2266,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3496,6 +3510,44 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
||||||
|
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz",
|
||||||
|
"integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.12.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -3567,6 +3619,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
|||||||
BIN
frontend/public/favicon.jpg
Normal file
BIN
frontend/public/favicon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -3,27 +3,13 @@
|
|||||||
* State Machine per il controllo accessi
|
* State Machine per il controllo accessi
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import {useCallback, useEffect, useState} from 'react';
|
||||||
import { useRFIDScanner } from './hooks/useRFIDScanner';
|
import {useRFIDScanner} from './hooks/useRFIDScanner';
|
||||||
import {
|
import {ActiveGateScreen, ErrorModal, LoadingScreen, SuccessModal, ValidatorLoginScreen,} from './screens';
|
||||||
LoadingScreen,
|
import {ApiError, getRoomInfo, getUserByBadge, loginValidator, requestEntry,} from './services/api';
|
||||||
ValidatorLoginScreen,
|
import type {AppState, RoomInfo, User, ValidatorSession} from './types';
|
||||||
ActiveGateScreen,
|
|
||||||
SuccessModal,
|
|
||||||
ErrorModal,
|
|
||||||
} from './screens';
|
|
||||||
import {
|
|
||||||
getRoomInfo,
|
|
||||||
loginValidator,
|
|
||||||
getUserByBadge,
|
|
||||||
requestEntry,
|
|
||||||
ApiError,
|
|
||||||
} from './services/api';
|
|
||||||
import type { AppState, RoomInfo, User, ValidatorSession } from './types';
|
|
||||||
|
|
||||||
// Costanti
|
// Costanti
|
||||||
const VALIDATOR_BADGE = '999999';
|
|
||||||
const VALIDATOR_PASSWORD = 'focolari';
|
|
||||||
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 STORAGE_KEY = 'focolari_validator_session';
|
const STORAGE_KEY = 'focolari_validator_session';
|
||||||
@@ -42,10 +28,16 @@ function App() {
|
|||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
const [successMessage, setSuccessMessage] = useState('');
|
const [successUserName, setSuccessUserName] = useState<string | undefined>(undefined);
|
||||||
const [showErrorModal, setShowErrorModal] = useState(false);
|
const [showErrorModal, setShowErrorModal] = useState(false);
|
||||||
const [errorModalMessage, setErrorModalMessage] = useState('');
|
const [errorModalMessage, setErrorModalMessage] = useState('');
|
||||||
|
|
||||||
|
// Notifica badge validatore ignorato
|
||||||
|
const [showValidatorBadgeNotice, setShowValidatorBadgeNotice] = useState(false);
|
||||||
|
|
||||||
|
// Badge non trovato (con timeout per tornare all'attesa)
|
||||||
|
const [notFoundBadge, setNotFoundBadge] = useState<string | null>(null);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Session Management
|
// Session Management
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -63,7 +55,7 @@ function App() {
|
|||||||
setAppState('waiting-validator');
|
setAppState('waiting-validator');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadSession = useCallback((): ValidatorSession | null => {
|
const loadSession = useCallback((serverStartTime?: number): ValidatorSession | null => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
if (!stored) return null;
|
if (!stored) return null;
|
||||||
@@ -72,6 +64,14 @@ function App() {
|
|||||||
|
|
||||||
// Check if session is expired
|
// Check if session is expired
|
||||||
if (Date.now() > session.expiresAt) {
|
if (Date.now() > session.expiresAt) {
|
||||||
|
console.log('[FLOW] Session expired by time');
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if server restarted (invalidate old sessions)
|
||||||
|
if (serverStartTime && session.serverStartTime !== serverStartTime) {
|
||||||
|
console.log('[FLOW] Session invalidated - server restarted');
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -87,35 +87,43 @@ function App() {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
const handleRFIDScan = useCallback(async (code: string) => {
|
const handleRFIDScan = useCallback(async (code: string) => {
|
||||||
console.log('[App] Badge scansionato:', code);
|
console.log('[RFID] Badge scansionato:', code);
|
||||||
|
|
||||||
// Pulisci il codice
|
// Pulisci il codice
|
||||||
const cleanCode = code.trim();
|
const cleanCode = code.trim();
|
||||||
|
|
||||||
switch (appState) {
|
switch (appState) {
|
||||||
case 'waiting-validator':
|
case 'waiting-validator':
|
||||||
// Verifica se è un badge validatore
|
// Qualsiasi badge può essere un validatore - verrà verificato con la password
|
||||||
if (cleanCode === VALIDATOR_BADGE) {
|
console.log('[FLOW] Transition: waiting-validator -> validator-password');
|
||||||
setPendingValidatorBadge(cleanCode);
|
setPendingValidatorBadge(cleanCode);
|
||||||
setAppState('validator-password');
|
setAppState('validator-password');
|
||||||
} else {
|
|
||||||
setError('Badge validatore non riconosciuto');
|
|
||||||
setTimeout(() => setError(undefined), 3000);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'validator-password':
|
case 'validator-password':
|
||||||
// Ignora badge durante inserimento password
|
// Ignora badge durante inserimento password
|
||||||
|
console.log('[FLOW] Badge ignorato durante inserimento password');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'gate-active':
|
case 'gate-active':
|
||||||
case 'showing-user':
|
case 'showing-user':
|
||||||
// Se è il badge del validatore e c'è un utente a schermo
|
// Se è il badge del validatore attuale
|
||||||
if (cleanCode === VALIDATOR_BADGE && currentUser && currentUser.ammesso) {
|
if (validatorSession && cleanCode === validatorSession.badge) {
|
||||||
|
if (currentUser && currentUser.ammesso) {
|
||||||
|
console.log('[FLOW] Validator badge detected - confirming entry');
|
||||||
// Conferma ingresso
|
// Conferma ingresso
|
||||||
await handleEntryConfirm();
|
await handleEntryConfirm();
|
||||||
} else if (cleanCode !== VALIDATOR_BADGE) {
|
} else {
|
||||||
// Nuovo badge partecipante - carica utente
|
// Badge validatore passato senza utente ammesso - mostra notifica
|
||||||
|
console.log('[FLOW] Validator badge ignored - showing notice');
|
||||||
|
setShowValidatorBadgeNotice(true);
|
||||||
|
setTimeout(() => setShowValidatorBadgeNotice(false), 4000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[FLOW] Loading participant:', cleanCode);
|
||||||
|
// Badge partecipante - carica utente
|
||||||
|
// Se c'era un badge non trovato, cancellalo e carica il nuovo
|
||||||
|
setNotFoundBadge(null);
|
||||||
await handleLoadUser(cleanCode);
|
await handleLoadUser(cleanCode);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -123,13 +131,13 @@ function App() {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [appState, currentUser]);
|
}, [appState, currentUser, validatorSession]);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Initialize RFID Scanner
|
// Initialize RFID Scanner
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
const { state: rfidState, buffer: rfidBuffer } = useRFIDScanner({
|
const {state: rfidState, buffer: rfidBuffer} = useRFIDScanner({
|
||||||
onScan: handleRFIDScan,
|
onScan: handleRFIDScan,
|
||||||
onTimeout: () => {
|
onTimeout: () => {
|
||||||
console.warn('[App] RFID timeout - lettura incompleta');
|
console.warn('[App] RFID timeout - lettura incompleta');
|
||||||
@@ -144,17 +152,25 @@ function App() {
|
|||||||
const handleLoadUser = useCallback(async (badgeCode: string) => {
|
const handleLoadUser = useCallback(async (badgeCode: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
setNotFoundBadge(null);
|
||||||
setAppState('showing-user');
|
setAppState('showing-user');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await getUserByBadge(badgeCode);
|
const user = await getUserByBadge(badgeCode);
|
||||||
setCurrentUser(user);
|
setCurrentUser(user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError && err.statusCode === 404) {
|
||||||
|
// Utente non trovato - mostra schermata speciale con timeout
|
||||||
|
console.log('[FLOW] Badge not found:', badgeCode);
|
||||||
|
setNotFoundBadge(badgeCode);
|
||||||
|
setCurrentUser(null);
|
||||||
|
} else {
|
||||||
const message = err instanceof ApiError
|
const message = err instanceof ApiError
|
||||||
? err.detail || err.message
|
? err.detail || err.message
|
||||||
: 'Errore durante il caricamento utente';
|
: 'Errore durante il caricamento utente';
|
||||||
setError(message);
|
setError(message);
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -165,14 +181,18 @@ function App() {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
// Salva il nome utente prima di pulirlo
|
||||||
|
const userName = `${currentUser.nome} ${currentUser.cognome}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await requestEntry(
|
const response = await requestEntry(
|
||||||
currentUser.badge_code,
|
currentUser.badge_code,
|
||||||
VALIDATOR_PASSWORD
|
validatorSession.password // Uso la password dalla sessione
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setSuccessMessage(response.welcome_message || 'Benvenuto!');
|
console.log('[FLOW] Entry confirmed for:', userName);
|
||||||
|
setSuccessUserName(userName);
|
||||||
setShowSuccessModal(true);
|
setShowSuccessModal(true);
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
setAppState('gate-active');
|
setAppState('gate-active');
|
||||||
@@ -189,7 +209,7 @@ function App() {
|
|||||||
}, [currentUser, validatorSession]);
|
}, [currentUser, validatorSession]);
|
||||||
|
|
||||||
const handlePasswordSubmit = useCallback(async (password: string) => {
|
const handlePasswordSubmit = useCallback(async (password: string) => {
|
||||||
if (!pendingValidatorBadge) return;
|
if (!pendingValidatorBadge || !roomInfo) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
@@ -198,11 +218,14 @@ function App() {
|
|||||||
const response = await loginValidator(pendingValidatorBadge, password);
|
const response = await loginValidator(pendingValidatorBadge, password);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
console.log('[FLOW] Validator authenticated, badge:', pendingValidatorBadge);
|
||||||
const session: ValidatorSession = {
|
const session: ValidatorSession = {
|
||||||
badge: pendingValidatorBadge,
|
badge: pendingValidatorBadge,
|
||||||
|
password: password, // Salvo la password per le conferme ingresso
|
||||||
token: response.token || '',
|
token: response.token || '',
|
||||||
loginTime: Date.now(),
|
loginTime: Date.now(),
|
||||||
expiresAt: Date.now() + SESSION_DURATION_MS,
|
expiresAt: Date.now() + SESSION_DURATION_MS,
|
||||||
|
serverStartTime: roomInfo.server_start_time, // Per invalidare se server riparte
|
||||||
};
|
};
|
||||||
|
|
||||||
saveSession(session);
|
saveSession(session);
|
||||||
@@ -217,7 +240,7 @@ function App() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [pendingValidatorBadge, saveSession]);
|
}, [pendingValidatorBadge, saveSession, roomInfo]);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// UI Handlers
|
// UI Handlers
|
||||||
@@ -231,6 +254,7 @@ function App() {
|
|||||||
|
|
||||||
const handleCancelUser = useCallback(() => {
|
const handleCancelUser = useCallback(() => {
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
|
setNotFoundBadge(null);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setAppState('gate-active');
|
setAppState('gate-active');
|
||||||
}, []);
|
}, []);
|
||||||
@@ -238,13 +262,14 @@ function App() {
|
|||||||
const handleUserTimeout = useCallback(() => {
|
const handleUserTimeout = useCallback(() => {
|
||||||
console.log('[App] User timeout - tornando in attesa');
|
console.log('[App] User timeout - tornando in attesa');
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
|
setNotFoundBadge(null);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setAppState('gate-active');
|
setAppState('gate-active');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSuccessModalClose = useCallback(() => {
|
const handleSuccessModalClose = useCallback(() => {
|
||||||
setShowSuccessModal(false);
|
setShowSuccessModal(false);
|
||||||
setSuccessMessage('');
|
setSuccessUserName(undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleErrorModalClose = useCallback(() => {
|
const handleErrorModalClose = useCallback(() => {
|
||||||
@@ -263,8 +288,8 @@ function App() {
|
|||||||
const info = await getRoomInfo();
|
const info = await getRoomInfo();
|
||||||
setRoomInfo(info);
|
setRoomInfo(info);
|
||||||
|
|
||||||
// Verifica sessione esistente
|
// Verifica sessione esistente (passa serverStartTime per invalidazione)
|
||||||
const existingSession = loadSession();
|
const existingSession = loadSession(info.server_start_time);
|
||||||
|
|
||||||
if (existingSession) {
|
if (existingSession) {
|
||||||
setValidatorSession(existingSession);
|
setValidatorSession(existingSession);
|
||||||
@@ -347,20 +372,21 @@ function App() {
|
|||||||
rfidState={rfidState}
|
rfidState={rfidState}
|
||||||
rfidBuffer={rfidBuffer}
|
rfidBuffer={rfidBuffer}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
|
notFoundBadge={notFoundBadge}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
onCancelUser={handleCancelUser}
|
onCancelUser={handleCancelUser}
|
||||||
onLogout={clearSession}
|
onLogout={clearSession}
|
||||||
userTimeoutSeconds={USER_TIMEOUT_SECONDS}
|
userTimeoutSeconds={USER_TIMEOUT_SECONDS}
|
||||||
onUserTimeout={handleUserTimeout}
|
onUserTimeout={handleUserTimeout}
|
||||||
|
showValidatorBadgeNotice={showValidatorBadgeNotice}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Success Modal */}
|
{/* Success Modal */}
|
||||||
<SuccessModal
|
<SuccessModal
|
||||||
isOpen={showSuccessModal}
|
isOpen={showSuccessModal}
|
||||||
onClose={handleSuccessModalClose}
|
onClose={handleSuccessModalClose}
|
||||||
welcomeMessage={successMessage}
|
userName={successUserName}
|
||||||
userName={currentUser ? `${currentUser.nome} ${currentUser.cognome}` : undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Error Modal */}
|
{/* Error Modal */}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function Button({
|
|||||||
children,
|
children,
|
||||||
className = '',
|
className = '',
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
'font-semibold rounded-xl transition-all duration-200 ' +
|
'font-semibold rounded-xl transition-all duration-200 ' +
|
||||||
'focus:outline-none focus:ring-4 focus:ring-offset-2 ' +
|
'focus:outline-none focus:ring-4 focus:ring-offset-2 ' +
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Countdown Timer Component - Focolari Voting System
|
* Countdown Timer Component - Focolari Voting System
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import {useCallback, useEffect, useState} from 'react';
|
||||||
|
|
||||||
interface CountdownTimerProps {
|
interface CountdownTimerProps {
|
||||||
/** Secondi totali */
|
/** Secondi totali */
|
||||||
@@ -26,7 +26,7 @@ export function CountdownTimer({
|
|||||||
showBar = true,
|
showBar = true,
|
||||||
warningThreshold = 30,
|
warningThreshold = 30,
|
||||||
dangerThreshold = 10,
|
dangerThreshold = 10,
|
||||||
}: CountdownTimerProps) {
|
}: CountdownTimerProps) {
|
||||||
const [remaining, setRemaining] = useState(seconds);
|
const [remaining, setRemaining] = useState(seconds);
|
||||||
|
|
||||||
const formatTime = useCallback((secs: number): string => {
|
const formatTime = useCallback((secs: number): string => {
|
||||||
@@ -90,7 +90,7 @@ export function CountdownTimer({
|
|||||||
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full transition-all duration-1000 ease-linear ${getBarColorClass()}`}
|
className={`h-full transition-all duration-1000 ease-linear ${getBarColorClass()}`}
|
||||||
style={{ width: `${progressPercent}%` }}
|
style={{width: `${progressPercent}%`}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Input Component - Focolari Voting System
|
* Input Component - Focolari Voting System
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef } from 'react';
|
import {forwardRef, useState} from 'react';
|
||||||
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -11,8 +11,12 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ label, error, fullWidth = true, className = '', ...props }, ref) => {
|
({label, error, fullWidth = true, className = '', type, ...props}, ref) => {
|
||||||
const widthClass = fullWidth ? 'w-full' : '';
|
const widthClass = fullWidth ? 'w-full' : '';
|
||||||
|
const isPassword = type === 'password';
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const inputType = isPassword && showPassword ? 'text' : type;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${widthClass} ${className}`}>
|
<div className={`${widthClass} ${className}`}>
|
||||||
@@ -21,13 +25,16 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
type={inputType}
|
||||||
className={`
|
className={`
|
||||||
w-full px-4 py-3 text-lg
|
w-full px-4 py-3 text-lg
|
||||||
border-2 rounded-xl
|
border-2 rounded-xl
|
||||||
transition-all duration-200
|
transition-all duration-200
|
||||||
focus:outline-none focus:ring-4 focus:ring-focolare-blue/30
|
focus:outline-none focus:ring-4 focus:ring-focolare-blue/30
|
||||||
|
${isPassword ? 'pr-12' : ''}
|
||||||
${error
|
${error
|
||||||
? 'border-error focus:border-error'
|
? 'border-error focus:border-error'
|
||||||
: 'border-gray-300 focus:border-focolare-blue'
|
: 'border-gray-300 focus:border-focolare-blue'
|
||||||
@@ -35,6 +42,32 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
`.trim()}
|
`.trim()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
{isPassword && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-gray-500 hover:text-gray-700 transition-colors"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={showPassword ? 'Nascondi password' : 'Mostra password'}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
// Icona occhio barrato (nascondi)
|
||||||
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
// Icona occhio (mostra)
|
||||||
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="mt-2 text-sm text-error font-medium">{error}</p>
|
<p className="mt-2 text-sm text-error font-medium">{error}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface LogoProps {
|
|||||||
showText?: boolean;
|
showText?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Logo({ size = 'md', showText = true }: LogoProps) {
|
export function Logo({size = 'md', showText = true}: LogoProps) {
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'h-10 w-10',
|
sm: 'h-10 w-10',
|
||||||
md: 'h-14 w-14',
|
md: 'h-14 w-14',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Modal Component - Focolari Voting System
|
* Modal Component - Focolari Voting System
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import {useEffect} from 'react';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -20,7 +20,7 @@ export function Modal({
|
|||||||
autoCloseMs,
|
autoCloseMs,
|
||||||
fullscreen = false,
|
fullscreen = false,
|
||||||
children,
|
children,
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
// Auto-close functionality
|
// Auto-close functionality
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || !autoCloseMs || !onClose) {
|
if (!isOpen || !autoCloseMs || !onClose) {
|
||||||
|
|||||||
132
frontend/src/components/NumLockBanner.tsx
Normal file
132
frontend/src/components/NumLockBanner.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* NumLock Banner Component - Focolari Voting System
|
||||||
|
*
|
||||||
|
* Mostra un banner di avviso su browser desktop quando NumLock
|
||||||
|
* potrebbe essere necessario per il corretto funzionamento del lettore RFID.
|
||||||
|
* Non viene mostrato su dispositivi touch (tablet, smartphone).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
interface NumLockBannerProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NumLockBanner({className = ''}: NumLockBannerProps) {
|
||||||
|
const [isDesktop, setIsDesktop] = useState(false);
|
||||||
|
const [numLockState, setNumLockState] = useState<boolean | null>(null);
|
||||||
|
const [dismissed, setDismissed] = useState(false);
|
||||||
|
|
||||||
|
// Rileva se siamo su desktop (non touch device)
|
||||||
|
useEffect(() => {
|
||||||
|
const checkIfDesktop = () => {
|
||||||
|
// Controlla se è un dispositivo touch
|
||||||
|
const isTouchDevice =
|
||||||
|
'ontouchstart' in window ||
|
||||||
|
navigator.maxTouchPoints > 0 ||
|
||||||
|
// @ts-expect-error - msMaxTouchPoints è specifico di IE/Edge
|
||||||
|
navigator.msMaxTouchPoints > 0;
|
||||||
|
|
||||||
|
// Controlla user agent per mobile/tablet
|
||||||
|
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
||||||
|
const isMobileUA = mobileRegex.test(navigator.userAgent);
|
||||||
|
|
||||||
|
// È desktop solo se non è touch E non ha UA mobile
|
||||||
|
setIsDesktop(!isTouchDevice && !isMobileUA);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkIfDesktop();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Ascolta eventi tastiera per rilevare stato NumLock
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDesktop) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// getModifierState restituisce lo stato di NumLock
|
||||||
|
const numLock = event.getModifierState('NumLock');
|
||||||
|
setNumLockState(numLock);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aggiungi listener
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isDesktop]);
|
||||||
|
|
||||||
|
// Non mostrare su mobile/tablet o se dismissato
|
||||||
|
if (!isDesktop || dismissed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-amber-100 border border-amber-300 rounded-lg p-4 ${className}`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
{/* Icona */}
|
||||||
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-amber-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contenuto */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-semibold text-amber-800">
|
||||||
|
⌨️ Modalità Desktop Rilevata
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-amber-700 mt-1">
|
||||||
|
Per il corretto funzionamento del lettore RFID, assicurati che il tasto
|
||||||
|
<strong> Bloc Num (NumLock) </strong> sia attivo.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Stato NumLock */}
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<span className="text-sm text-amber-700">Stato attuale:</span>
|
||||||
|
{numLockState === null ? (
|
||||||
|
<span className="text-sm text-amber-600 italic">
|
||||||
|
Premi un tasto per rilevare...
|
||||||
|
</span>
|
||||||
|
) : numLockState ? (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 text-sm font-medium text-green-700 bg-green-100 px-2 py-0.5 rounded">
|
||||||
|
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||||
|
NumLock ATTIVO ✓
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 text-sm font-medium text-red-700 bg-red-100 px-2 py-0.5 rounded">
|
||||||
|
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
|
||||||
|
NumLock DISATTIVO ✗
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pulsante chiudi */}
|
||||||
|
<button
|
||||||
|
onClick={() => setDismissed(true)}
|
||||||
|
className="flex-shrink-0 text-amber-500 hover:text-amber-700 transition-colors"
|
||||||
|
aria-label="Chiudi avviso"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NumLockBanner;
|
||||||
@@ -3,18 +3,18 @@
|
|||||||
* Mostra lo stato del lettore RFID
|
* Mostra lo stato del lettore RFID
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RFIDScannerState } from '../types';
|
import type {RFIDScannerState} from '../types';
|
||||||
|
|
||||||
interface RFIDStatusProps {
|
interface RFIDStatusProps {
|
||||||
state: RFIDScannerState;
|
state: RFIDScannerState;
|
||||||
buffer?: string;
|
buffer?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RFIDStatus({ state, buffer }: RFIDStatusProps) {
|
export function RFIDStatus({state, buffer}: RFIDStatusProps) {
|
||||||
if (state === 'idle') {
|
if (state === 'idle') {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 text-gray-400">
|
<div className="flex items-center gap-2 text-gray-400">
|
||||||
<div className="h-3 w-3 rounded-full bg-gray-300" />
|
<div className="h-3 w-3 rounded-full bg-gray-300"/>
|
||||||
<span className="text-sm">RFID Pronto</span>
|
<span className="text-sm">RFID Pronto</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -22,7 +22,7 @@ export function RFIDStatus({ state, buffer }: RFIDStatusProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 text-focolare-orange">
|
<div className="flex items-center gap-2 text-focolare-orange">
|
||||||
<div className="h-3 w-3 rounded-full bg-focolare-orange animate-pulse" />
|
<div className="h-3 w-3 rounded-full bg-focolare-orange animate-pulse"/>
|
||||||
<span className="text-sm font-semibold">
|
<span className="text-sm font-semibold">
|
||||||
Lettura in corso... {buffer && `(${buffer.length} caratteri)`}
|
Lettura in corso... {buffer && `(${buffer.length} caratteri)`}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
* User Card Component - Focolari Voting System
|
* User Card Component - Focolari Voting System
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { User } from '../types';
|
import type {User} from '../types';
|
||||||
|
|
||||||
interface UserCardProps {
|
interface UserCardProps {
|
||||||
user: User;
|
user: User;
|
||||||
size?: 'compact' | 'full';
|
size?: 'compact' | 'full';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserCard({ user, size = 'full' }: UserCardProps) {
|
export function UserCard({user, size = 'full'}: UserCardProps) {
|
||||||
const roleColors: Record<string, string> = {
|
const roleColors: Record<string, string> = {
|
||||||
Votante: 'bg-focolare-blue text-white',
|
Votante: 'bg-focolare-blue text-white',
|
||||||
Tecnico: 'bg-focolare-orange text-white',
|
Tecnico: 'bg-focolare-orange text-white',
|
||||||
|
|||||||
100
frontend/src/components/WelcomeCarousel.tsx
Normal file
100
frontend/src/components/WelcomeCarousel.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Welcome Carousel Component - Focolari Voting System
|
||||||
|
*
|
||||||
|
* Carosello automatico di messaggi di benvenuto multilingua.
|
||||||
|
* Scorre automaticamente ogni 800ms durante la visualizzazione.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
interface WelcomeMessage {
|
||||||
|
lang: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WELCOME_MESSAGES: WelcomeMessage[] = [
|
||||||
|
{lang: 'it', text: 'Benvenuto!'},
|
||||||
|
{lang: 'en', text: 'Welcome!'},
|
||||||
|
{lang: 'fr', text: 'Bienvenue!'},
|
||||||
|
{lang: 'de', text: 'Willkommen!'},
|
||||||
|
{lang: 'es', text: '¡Bienvenido!'},
|
||||||
|
{lang: 'pt', text: 'Bem-vindo!'},
|
||||||
|
{lang: 'zh', text: '欢迎!'},
|
||||||
|
{lang: 'ja', text: 'ようこそ!'},
|
||||||
|
{lang: 'ar', text: '!مرحباً'},
|
||||||
|
{lang: 'ru', text: 'Добро пожаловать!'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CAROUSEL_INTERVAL_MS = 800;
|
||||||
|
|
||||||
|
interface WelcomeCarouselProps {
|
||||||
|
/** Se true, il carosello è in pausa */
|
||||||
|
paused?: boolean;
|
||||||
|
/** Nome dell'utente da visualizzare (opzionale) */
|
||||||
|
userName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WelcomeCarousel({paused = false, userName}: WelcomeCarouselProps) {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (paused) return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setIsTransitioning(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentIndex((prev) => (prev + 1) % WELCOME_MESSAGES.length);
|
||||||
|
setIsTransitioning(false);
|
||||||
|
}, 150); // Durata transizione fade
|
||||||
|
}, CAROUSEL_INTERVAL_MS);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [paused]);
|
||||||
|
|
||||||
|
const currentMessage = WELCOME_MESSAGES[currentIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center text-center">
|
||||||
|
{/* Messaggio di benvenuto */}
|
||||||
|
<div
|
||||||
|
className={`transition-opacity duration-150 ${
|
||||||
|
isTransitioning ? 'opacity-0' : 'opacity-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<h1 className="text-6xl md:text-8xl font-bold text-white mb-4 drop-shadow-lg">
|
||||||
|
{currentMessage.text}
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-white/70 uppercase tracking-wider">
|
||||||
|
{currentMessage.lang.toUpperCase()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nome utente */}
|
||||||
|
{userName && (
|
||||||
|
<div className="mt-8 pt-8 border-t border-white/30">
|
||||||
|
<p className="text-3xl md:text-4xl text-white font-medium">
|
||||||
|
{userName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Indicatori */}
|
||||||
|
<div className="flex gap-2 mt-8">
|
||||||
|
{WELCOME_MESSAGES.map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
||||||
|
index === currentIndex
|
||||||
|
? 'bg-white w-6'
|
||||||
|
: 'bg-white/40'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WelcomeCarousel;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
// Components barrel export
|
// Components barrel export
|
||||||
export { Logo } from './Logo';
|
export {Logo} from './Logo';
|
||||||
export { UserCard } from './UserCard';
|
export {UserCard} from './UserCard';
|
||||||
export { CountdownTimer } from './CountdownTimer';
|
export {CountdownTimer} from './CountdownTimer';
|
||||||
export { Modal } from './Modal';
|
export {Modal} from './Modal';
|
||||||
export { RFIDStatus } from './RFIDStatus';
|
export {RFIDStatus} from './RFIDStatus';
|
||||||
export { Button } from './Button';
|
export {Button} from './Button';
|
||||||
export { Input } from './Input';
|
export {Input} from './Input';
|
||||||
|
export {WelcomeCarousel} from './WelcomeCarousel';
|
||||||
|
export {NumLockBanner} from './NumLockBanner';
|
||||||
|
|||||||
@@ -1,22 +1,53 @@
|
|||||||
/**
|
/**
|
||||||
* Focolari Voting System - RFID Scanner Hook
|
* Focolari Voting System - RFID Scanner Hook (v2 - Multi-Pattern)
|
||||||
*
|
*
|
||||||
* Questo hook gestisce la lettura di badge RFID tramite lettori USB
|
* Questo hook gestisce la lettura di badge RFID tramite lettori USB
|
||||||
* che emulano una tastiera. Il protocollo prevede:
|
* che emulano una tastiera. Supporta pattern multipli per diversi layout:
|
||||||
* - Carattere di inizio: `;`
|
* - Layout US: `;` → `?`
|
||||||
* - Carattere di fine: `?`
|
* - Layout IT: `ò` → `_`
|
||||||
* - Esempio: `;00012345?`
|
*
|
||||||
|
* NOTA: Il lettore RFID invia Enter (\n) dopo l'ultimo carattere.
|
||||||
|
* L'hook lo gestisce ignorando l'Enter immediatamente dopo il completamento.
|
||||||
*
|
*
|
||||||
* L'hook funziona indipendentemente dal focus dell'applicazione.
|
* L'hook funziona indipendentemente dal focus dell'applicazione.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import type { RFIDScannerState, RFIDScanResult } from '../types';
|
import type {RFIDScannerState, RFIDScanResult} from '../types';
|
||||||
|
|
||||||
// Costanti
|
// ============================================
|
||||||
const START_SENTINEL = ';';
|
// CONFIGURAZIONE PATTERN RFID
|
||||||
const END_SENTINEL = '?';
|
// ============================================
|
||||||
const TIMEOUT_MS = 3000; // 3 secondi di timeout
|
|
||||||
|
export interface RFIDPattern {
|
||||||
|
name: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VALID_PATTERNS: RFIDPattern[] = [
|
||||||
|
{name: 'US', start: ';', end: '?'},
|
||||||
|
{name: 'IT', start: 'ò', end: '_'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 2500; // 2.5 secondi di timeout
|
||||||
|
const ENTER_GRACE_PERIOD_MS = 100; // Ignora Enter entro 100ms dal completamento
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// LOGGING
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
const log = (message: string, ...args: unknown[]) => {
|
||||||
|
console.log(`[RFID] ${message}`, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logWarn = (message: string, ...args: unknown[]) => {
|
||||||
|
console.warn(`[RFID] ${message}`, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TIPI
|
||||||
|
// ============================================
|
||||||
|
|
||||||
interface UseRFIDScannerOptions {
|
interface UseRFIDScannerOptions {
|
||||||
/** Callback chiamato quando un badge viene letto con successo */
|
/** Callback chiamato quando un badge viene letto con successo */
|
||||||
@@ -31,32 +62,52 @@ interface UseRFIDScannerOptions {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseRFIDScannerReturn {
|
export interface UseRFIDScannerReturn {
|
||||||
/** Stato corrente dello scanner */
|
/** Stato corrente dello scanner */
|
||||||
state: RFIDScannerState;
|
state: RFIDScannerState;
|
||||||
/** Buffer corrente (solo per debug) */
|
/** Buffer corrente (solo per debug) */
|
||||||
buffer: string;
|
buffer: string;
|
||||||
/** Ultimo codice scansionato */
|
/** Ultimo codice scansionato */
|
||||||
lastScan: RFIDScanResult | null;
|
lastScan: RFIDScanResult | null;
|
||||||
|
/** Pattern attualmente in uso (solo durante scanning) */
|
||||||
|
activePattern: RFIDPattern | null;
|
||||||
/** Reset manuale dello scanner */
|
/** Reset manuale dello scanner */
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
|
/** Ultimi eventi tastiera (per debug) */
|
||||||
|
keyLog: KeyLogEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KeyLogEntry {
|
||||||
|
key: string;
|
||||||
|
code: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_KEY_LOG = 20;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// HOOK
|
||||||
|
// ============================================
|
||||||
|
|
||||||
export function useRFIDScanner({
|
export function useRFIDScanner({
|
||||||
onScan,
|
onScan,
|
||||||
onTimeout,
|
onTimeout,
|
||||||
onScanStart,
|
onScanStart,
|
||||||
preventDefaultOnScan = true,
|
preventDefaultOnScan = true,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: UseRFIDScannerOptions): UseRFIDScannerReturn {
|
}: UseRFIDScannerOptions): UseRFIDScannerReturn {
|
||||||
const [state, setState] = useState<RFIDScannerState>('idle');
|
const [state, setState] = useState<RFIDScannerState>('idle');
|
||||||
const [buffer, setBuffer] = useState<string>('');
|
const [buffer, setBuffer] = useState<string>('');
|
||||||
const [lastScan, setLastScan] = useState<RFIDScanResult | null>(null);
|
const [lastScan, setLastScan] = useState<RFIDScanResult | null>(null);
|
||||||
|
const [activePattern, setActivePattern] = useState<RFIDPattern | null>(null);
|
||||||
|
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
|
||||||
|
|
||||||
// Refs per mantenere i valori aggiornati nei callback
|
// Refs per mantenere i valori aggiornati nei callback
|
||||||
const bufferRef = useRef<string>('');
|
const bufferRef = useRef<string>('');
|
||||||
const stateRef = useRef<RFIDScannerState>('idle');
|
const stateRef = useRef<RFIDScannerState>('idle');
|
||||||
|
const activePatternRef = useRef<RFIDPattern | null>(null);
|
||||||
const timeoutRef = useRef<number | null>(null);
|
const timeoutRef = useRef<number | null>(null);
|
||||||
|
const lastCompletionRef = useRef<number>(0);
|
||||||
|
|
||||||
// Sync refs con state
|
// Sync refs con state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -67,6 +118,21 @@ export function useRFIDScanner({
|
|||||||
stateRef.current = state;
|
stateRef.current = state;
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
activePatternRef.current = activePattern;
|
||||||
|
}, [activePattern]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggiunge un evento al log tastiera
|
||||||
|
*/
|
||||||
|
const addKeyLog = useCallback((key: string, code: string) => {
|
||||||
|
setKeyLog(prev => {
|
||||||
|
const newEntry: KeyLogEntry = {key, code, timestamp: Date.now()};
|
||||||
|
const updated = [newEntry, ...prev].slice(0, MAX_KEY_LOG);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pulisce il timeout attivo
|
* Pulisce il timeout attivo
|
||||||
*/
|
*/
|
||||||
@@ -84,8 +150,10 @@ export function useRFIDScanner({
|
|||||||
clearScanTimeout();
|
clearScanTimeout();
|
||||||
setState('idle');
|
setState('idle');
|
||||||
setBuffer('');
|
setBuffer('');
|
||||||
|
setActivePattern(null);
|
||||||
bufferRef.current = '';
|
bufferRef.current = '';
|
||||||
stateRef.current = 'idle';
|
stateRef.current = 'idle';
|
||||||
|
activePatternRef.current = null;
|
||||||
}, [clearScanTimeout]);
|
}, [clearScanTimeout]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,12 +162,19 @@ export function useRFIDScanner({
|
|||||||
const startTimeout = useCallback(() => {
|
const startTimeout = useCallback(() => {
|
||||||
clearScanTimeout();
|
clearScanTimeout();
|
||||||
timeoutRef.current = window.setTimeout(() => {
|
timeoutRef.current = window.setTimeout(() => {
|
||||||
console.warn('[RFID Scanner] Timeout - lettura incompleta scartata');
|
logWarn('Buffer timeout - clearing data');
|
||||||
onTimeout?.();
|
onTimeout?.();
|
||||||
reset();
|
reset();
|
||||||
}, TIMEOUT_MS);
|
}, TIMEOUT_MS);
|
||||||
}, [clearScanTimeout, onTimeout, reset]);
|
}, [clearScanTimeout, onTimeout, reset]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trova il pattern che corrisponde al carattere start
|
||||||
|
*/
|
||||||
|
const findPatternByStart = useCallback((char: string): RFIDPattern | undefined => {
|
||||||
|
return VALID_PATTERNS.find(p => p.start === char);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler principale per gli eventi keydown
|
* Handler principale per gli eventi keydown
|
||||||
*/
|
*/
|
||||||
@@ -110,11 +185,37 @@ export function useRFIDScanner({
|
|||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
|
const code = event.code;
|
||||||
|
|
||||||
// Ignora tasti speciali (frecce, funzione, ecc.)
|
// Log per debug
|
||||||
if (key.length > 1 && key !== START_SENTINEL && key !== END_SENTINEL) {
|
addKeyLog(key, code);
|
||||||
// Eccezione per Backspace in stato scanning: ignora ma non resetta
|
|
||||||
if (key === 'Backspace' && stateRef.current === 'scanning') {
|
// Gestione Enter dopo completamento (grace period)
|
||||||
|
if (key === 'Enter' && Date.now() - lastCompletionRef.current < ENTER_GRACE_PERIOD_MS) {
|
||||||
|
log('Enter ignorato (post-completion grace period)');
|
||||||
|
if (preventDefaultOnScan) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestione ESC: annulla scansione in corso
|
||||||
|
if (key === 'Escape' && stateRef.current === 'scanning') {
|
||||||
|
log('Scansione annullata con ESC');
|
||||||
|
if (preventDefaultOnScan) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignora tasti speciali (frecce, funzione, ecc.) ma non i sentinel
|
||||||
|
const isStartSentinel = VALID_PATTERNS.some(p => p.start === key);
|
||||||
|
const isEndSentinel = activePatternRef.current?.end === key;
|
||||||
|
|
||||||
|
if (key.length > 1 && !isStartSentinel && !isEndSentinel) {
|
||||||
|
// Eccezione per Backspace/Enter in stato scanning: ignora ma non resetta
|
||||||
|
if ((key === 'Backspace' || key === 'Enter') && stateRef.current === 'scanning') {
|
||||||
if (preventDefaultOnScan) {
|
if (preventDefaultOnScan) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -122,18 +223,22 @@ export function useRFIDScanner({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// STATO IDLE: attende il carattere di inizio
|
// STATO IDLE: attende un carattere start di qualsiasi pattern
|
||||||
if (stateRef.current === 'idle') {
|
if (stateRef.current === 'idle') {
|
||||||
if (key === START_SENTINEL) {
|
const pattern = findPatternByStart(key);
|
||||||
console.log('[RFID Scanner] Start sentinel rilevato - inizio scansione');
|
|
||||||
|
if (pattern) {
|
||||||
|
log(`Start sentinel detected: '${key}' (pattern ${pattern.name})`);
|
||||||
|
|
||||||
if (preventDefaultOnScan) {
|
if (preventDefaultOnScan) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
setState('scanning');
|
setState('scanning');
|
||||||
|
setActivePattern(pattern);
|
||||||
setBuffer('');
|
setBuffer('');
|
||||||
bufferRef.current = '';
|
bufferRef.current = '';
|
||||||
|
activePatternRef.current = pattern;
|
||||||
startTimeout();
|
startTimeout();
|
||||||
onScanStart?.();
|
onScanStart?.();
|
||||||
}
|
}
|
||||||
@@ -147,14 +252,18 @@ export function useRFIDScanner({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === END_SENTINEL) {
|
const currentPattern = activePatternRef.current;
|
||||||
|
|
||||||
|
// Verifica se è l'end sentinel del pattern attivo
|
||||||
|
if (currentPattern && key === currentPattern.end) {
|
||||||
// Fine della scansione
|
// Fine della scansione
|
||||||
clearScanTimeout();
|
clearScanTimeout();
|
||||||
|
lastCompletionRef.current = Date.now();
|
||||||
|
|
||||||
const scannedCode = bufferRef.current.trim();
|
const scannedCode = bufferRef.current.trim();
|
||||||
|
|
||||||
if (scannedCode.length > 0) {
|
if (scannedCode.length > 0) {
|
||||||
console.log('[RFID Scanner] Codice scansionato:', scannedCode);
|
log(`Scan complete (pattern ${currentPattern.name}): ${scannedCode}`);
|
||||||
|
|
||||||
const result: RFIDScanResult = {
|
const result: RFIDScanResult = {
|
||||||
code: scannedCode,
|
code: scannedCode,
|
||||||
@@ -164,15 +273,18 @@ export function useRFIDScanner({
|
|||||||
setLastScan(result);
|
setLastScan(result);
|
||||||
onScan(scannedCode);
|
onScan(scannedCode);
|
||||||
} else {
|
} else {
|
||||||
console.warn('[RFID Scanner] Codice vuoto scartato');
|
logWarn('Empty code discarded');
|
||||||
}
|
}
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
} else if (key === START_SENTINEL) {
|
} else if (findPatternByStart(key)) {
|
||||||
// Nuovo start sentinel durante scansione: resetta e ricomincia
|
// Nuovo start sentinel durante scansione: resetta e ricomincia con nuovo pattern
|
||||||
console.log('[RFID Scanner] Nuovo start sentinel - reset buffer');
|
const newPattern = findPatternByStart(key)!;
|
||||||
|
log(`New start sentinel during scan - switching to pattern ${newPattern.name}`);
|
||||||
setBuffer('');
|
setBuffer('');
|
||||||
bufferRef.current = '';
|
bufferRef.current = '';
|
||||||
|
setActivePattern(newPattern);
|
||||||
|
activePatternRef.current = newPattern;
|
||||||
startTimeout();
|
startTimeout();
|
||||||
} else {
|
} else {
|
||||||
// Accumula il carattere nel buffer
|
// Accumula il carattere nel buffer
|
||||||
@@ -184,11 +296,11 @@ export function useRFIDScanner({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Aggiungi listener globale
|
// Aggiungi listener globale
|
||||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
window.addEventListener('keydown', handleKeyDown, {capture: true});
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
window.removeEventListener('keydown', handleKeyDown, {capture: true});
|
||||||
clearScanTimeout();
|
clearScanTimeout();
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
@@ -199,6 +311,8 @@ export function useRFIDScanner({
|
|||||||
clearScanTimeout,
|
clearScanTimeout,
|
||||||
reset,
|
reset,
|
||||||
startTimeout,
|
startTimeout,
|
||||||
|
findPatternByStart,
|
||||||
|
addKeyLog,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Cleanup al unmount
|
// Cleanup al unmount
|
||||||
@@ -212,7 +326,9 @@ export function useRFIDScanner({
|
|||||||
state,
|
state,
|
||||||
buffer,
|
buffer,
|
||||||
lastScan,
|
lastScan,
|
||||||
|
activePattern,
|
||||||
reset,
|
reset,
|
||||||
|
keyLog,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import { StrictMode } from 'react'
|
import {StrictMode} from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import {createRoot} from 'react-dom/client'
|
||||||
|
import {BrowserRouter, Route, Routes} from 'react-router-dom'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import {DebugScreen} from './screens'
|
||||||
|
|
||||||
|
function DebugWrapper() {
|
||||||
|
return <DebugScreen onBack={() => window.location.href = '/'}/>
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<App/>}/>
|
||||||
|
<Route path="/debug" element={<DebugWrapper/>}/>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,20 +3,26 @@
|
|||||||
* Schermata principale del varco attivo
|
* Schermata principale del varco attivo
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logo, Button, RFIDStatus, UserCard, CountdownTimer } from '../components';
|
import {useEffect, useState} from 'react';
|
||||||
import type { RoomInfo, User, RFIDScannerState } from '../types';
|
import {Button, CountdownTimer, Logo, NumLockBanner, RFIDStatus, UserCard} from '../components';
|
||||||
|
import type {RFIDScannerState, RoomInfo, User} from '../types';
|
||||||
|
|
||||||
|
// Timeout per badge non trovato (30 secondi)
|
||||||
|
const NOT_FOUND_TIMEOUT_SECONDS = 30;
|
||||||
|
|
||||||
interface ActiveGateScreenProps {
|
interface ActiveGateScreenProps {
|
||||||
roomInfo: RoomInfo;
|
roomInfo: RoomInfo;
|
||||||
rfidState: RFIDScannerState;
|
rfidState: RFIDScannerState;
|
||||||
rfidBuffer: string;
|
rfidBuffer: string;
|
||||||
currentUser: User | null;
|
currentUser: User | null;
|
||||||
|
notFoundBadge: string | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
onCancelUser: () => void;
|
onCancelUser: () => void;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
userTimeoutSeconds?: number;
|
userTimeoutSeconds?: number;
|
||||||
onUserTimeout: () => void;
|
onUserTimeout: () => void;
|
||||||
|
showValidatorBadgeNotice?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActiveGateScreen({
|
export function ActiveGateScreen({
|
||||||
@@ -24,18 +30,42 @@ export function ActiveGateScreen({
|
|||||||
rfidState,
|
rfidState,
|
||||||
rfidBuffer,
|
rfidBuffer,
|
||||||
currentUser,
|
currentUser,
|
||||||
|
notFoundBadge,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
onCancelUser,
|
onCancelUser,
|
||||||
onLogout,
|
onLogout,
|
||||||
userTimeoutSeconds = 60,
|
userTimeoutSeconds = 60,
|
||||||
onUserTimeout,
|
onUserTimeout,
|
||||||
}: ActiveGateScreenProps) {
|
showValidatorBadgeNotice = false,
|
||||||
|
}: 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 */}
|
||||||
<header className="p-4 md:p-6 flex items-center justify-between border-b bg-white/80 backdrop-blur shadow-sm">
|
<header
|
||||||
<Logo size="md" />
|
className="p-4 md:p-6 flex items-center justify-between border-b bg-white/80 backdrop-blur shadow-sm">
|
||||||
|
<Logo size="md"/>
|
||||||
<div className="flex items-center gap-4 md:gap-8">
|
<div className="flex items-center gap-4 md:gap-8">
|
||||||
<div className="text-right hidden sm:block">
|
<div className="text-right hidden sm:block">
|
||||||
<p className="text-lg font-semibold text-gray-800">{roomInfo.room_name}</p>
|
<p className="text-lg font-semibold text-gray-800">{roomInfo.room_name}</p>
|
||||||
@@ -51,12 +81,23 @@ export function ActiveGateScreen({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* Notifica Badge Validatore Ignorato */}
|
||||||
|
{showValidatorBadgeNotice && (
|
||||||
|
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-50 animate-fade-in">
|
||||||
|
<div className="bg-amber-100 border border-amber-400 text-amber-800 px-6 py-3 rounded-xl shadow-lg">
|
||||||
|
<p className="font-semibold">Badge validatore rilevato</p>
|
||||||
|
<p className="text-sm">Se il validatore è cambiato, esci e rilogga con il nuovo badge.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 flex items-center justify-center p-4 md:p-8">
|
<main className="flex-1 flex items-center justify-center p-4 md:p-8">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
// Loading state
|
// Loading state
|
||||||
<div className="glass rounded-3xl p-12 shadow-xl animate-fade-in text-center">
|
<div className="glass rounded-3xl p-12 shadow-xl animate-fade-in text-center">
|
||||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-focolare-blue/10 mb-6">
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-focolare-blue/10 mb-6">
|
||||||
<svg
|
<svg
|
||||||
className="animate-spin h-10 w-10 text-focolare-blue"
|
className="animate-spin h-10 w-10 text-focolare-blue"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -79,10 +120,42 @@ export function ActiveGateScreen({
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-xl text-gray-600">Caricamento dati...</p>
|
<p className="text-xl text-gray-600">Caricamento dati...</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : notFoundBadge ? (
|
||||||
|
// Badge non trovato
|
||||||
|
<div className="glass rounded-3xl p-12 md:p-16 shadow-xl text-center max-w-2xl w-full">
|
||||||
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-error/10 mb-8">
|
||||||
|
<svg
|
||||||
|
className="h-16 w-16 text-error"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<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-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>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
// Error state
|
// Error state
|
||||||
<div className="glass rounded-3xl p-12 shadow-xl animate-slide-up text-center max-w-md">
|
<div className="glass rounded-3xl p-12 shadow-xl text-center max-w-md">
|
||||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-error/10 mb-6">
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-error/10 mb-6">
|
||||||
<svg
|
<svg
|
||||||
className="h-10 w-10 text-error"
|
className="h-10 w-10 text-error"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -104,7 +177,7 @@ export function ActiveGateScreen({
|
|||||||
</div>
|
</div>
|
||||||
) : currentUser ? (
|
) : currentUser ? (
|
||||||
// User found - Decision screen
|
// User found - Decision screen
|
||||||
<div className="w-full max-w-4xl animate-slide-up">
|
<div className="w-full max-w-5xl animate-slide-up">
|
||||||
<div className="glass rounded-3xl p-6 md:p-10 shadow-xl">
|
<div className="glass rounded-3xl p-6 md:p-10 shadow-xl">
|
||||||
{/* Timer bar */}
|
{/* Timer bar */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -117,7 +190,7 @@ export function ActiveGateScreen({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Card */}
|
{/* User Card */}
|
||||||
<UserCard user={currentUser} size="full" />
|
<UserCard user={currentUser} size="full"/>
|
||||||
|
|
||||||
{/* Action Hint */}
|
{/* Action Hint */}
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
@@ -127,7 +200,9 @@ export function ActiveGateScreen({
|
|||||||
✓ Utente ammesso all'ingresso
|
✓ Utente ammesso all'ingresso
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-gray-600">
|
<p className="text-lg text-gray-600">
|
||||||
Passa il <span className="font-bold text-focolare-blue">badge VALIDATORE</span> per confermare l'accesso
|
Passa il <span
|
||||||
|
className="font-bold text-focolare-blue">badge VALIDATORE</span> per
|
||||||
|
confermare l'accesso
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -156,10 +231,12 @@ export function ActiveGateScreen({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Idle - Waiting for participant
|
// Idle - Waiting for participant
|
||||||
<div className="glass rounded-3xl p-12 shadow-xl animate-fade-in text-center max-w-xl">
|
<div
|
||||||
<div className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-focolare-blue/10 mb-8">
|
className="glass rounded-3xl p-12 md:p-16 shadow-xl animate-fade-in text-center max-w-3xl w-full">
|
||||||
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-40 h-40 rounded-full bg-focolare-blue/10 mb-10">
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 text-focolare-blue"
|
className="w-20 h-20 text-focolare-blue"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -173,35 +250,40 @@ export function ActiveGateScreen({
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold text-focolare-blue mb-4">
|
<h1 className="text-5xl font-bold text-focolare-blue mb-6">
|
||||||
Varco Attivo
|
Varco Attivo
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-2xl text-gray-600 mb-8">
|
<p className="text-3xl text-gray-600 mb-10">
|
||||||
In attesa del partecipante...
|
In attesa del partecipante...
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="py-8 px-6 bg-focolare-orange/10 rounded-2xl border-2 border-dashed border-focolare-orange/40">
|
<div
|
||||||
|
className="py-10 px-8 bg-focolare-orange/10 rounded-2xl border-2 border-dashed border-focolare-orange/40">
|
||||||
<div className="flex items-center justify-center gap-4">
|
<div className="flex items-center justify-center gap-4">
|
||||||
<svg
|
<svg
|
||||||
className="w-10 h-10 text-focolare-orange animate-pulse"
|
className="w-12 h-12 text-focolare-orange animate-pulse"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/>
|
<path
|
||||||
|
d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-2xl text-focolare-orange font-medium">
|
<span className="text-3xl text-focolare-orange font-medium">
|
||||||
Passa il badge
|
Passa il badge
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Banner NumLock per desktop */}
|
||||||
|
<NumLockBanner className="mt-8"/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Footer with RFID Status */}
|
{/* Footer with RFID Status */}
|
||||||
<footer className="p-4 border-t bg-white/50 flex items-center justify-between">
|
<footer className="p-4 border-t bg-white/50 flex items-center justify-between">
|
||||||
<RFIDStatus state={rfidState} buffer={rfidBuffer} />
|
<RFIDStatus state={rfidState} buffer={rfidBuffer}/>
|
||||||
<span className="text-sm text-gray-400">
|
<span className="text-sm text-gray-400">
|
||||||
Varco attivo • {new Date().toLocaleTimeString('it-IT')}
|
Varco attivo • {new Date().toLocaleTimeString('it-IT')}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
208
frontend/src/screens/DebugScreen.tsx
Normal file
208
frontend/src/screens/DebugScreen.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* Debug Screen - Focolari Voting System
|
||||||
|
*
|
||||||
|
* Pagina diagnostica per debug del lettore RFID.
|
||||||
|
* Mostra in tempo reale gli eventi tastiera e lo stato dello scanner.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useRFIDScanner, VALID_PATTERNS} from '../hooks/useRFIDScanner';
|
||||||
|
import {Button, Logo} from '../components';
|
||||||
|
|
||||||
|
interface DebugScreenProps {
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DebugScreen({onBack}: DebugScreenProps) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
buffer,
|
||||||
|
lastScan,
|
||||||
|
activePattern,
|
||||||
|
keyLog,
|
||||||
|
reset,
|
||||||
|
} = useRFIDScanner({
|
||||||
|
onScan: (code) => {
|
||||||
|
console.log('[DEBUG] Scan received:', code);
|
||||||
|
},
|
||||||
|
onTimeout: () => {
|
||||||
|
console.log('[DEBUG] Timeout occurred');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatTimestamp = (ts: number) => {
|
||||||
|
const date = new Date(ts);
|
||||||
|
return date.toLocaleTimeString('it-IT', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
fractionalSecondDigits: 3,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-900 text-white p-4 md:p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Logo size="sm"/>
|
||||||
|
<h1 className="text-2xl font-bold text-white">Debug RFID</h1>
|
||||||
|
</div>
|
||||||
|
<Button variant="secondary" onClick={onBack}>
|
||||||
|
← Torna all'app
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Scanner Status */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-focolare-orange">
|
||||||
|
📡 Stato Scanner
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* State */}
|
||||||
|
<div className="flex items-center justify-between p-3 bg-slate-700 rounded-lg">
|
||||||
|
<span className="text-slate-300">Stato</span>
|
||||||
|
<span className={`px-3 py-1 rounded-full font-mono font-bold ${
|
||||||
|
state === 'scanning'
|
||||||
|
? 'bg-focolare-orange text-white animate-pulse'
|
||||||
|
: 'bg-slate-600 text-slate-300'
|
||||||
|
}`}>
|
||||||
|
{state.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Pattern */}
|
||||||
|
<div className="flex items-center justify-between p-3 bg-slate-700 rounded-lg">
|
||||||
|
<span className="text-slate-300">Pattern Attivo</span>
|
||||||
|
<span className="font-mono text-lg">
|
||||||
|
{activePattern
|
||||||
|
? `${activePattern.name} (${activePattern.start} → ${activePattern.end})`
|
||||||
|
: '—'
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buffer */}
|
||||||
|
<div className="p-3 bg-slate-700 rounded-lg">
|
||||||
|
<span className="text-slate-300 block mb-2">Buffer Corrente</span>
|
||||||
|
<div className="font-mono text-2xl bg-slate-900 p-3 rounded min-h-[50px] break-all">
|
||||||
|
{buffer || <span className="text-slate-500">(vuoto)</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Last Scan */}
|
||||||
|
<div className="p-3 bg-slate-700 rounded-lg">
|
||||||
|
<span className="text-slate-300 block mb-2">Ultimo Codice Rilevato</span>
|
||||||
|
{lastScan ? (
|
||||||
|
<div className="bg-green-900/50 border border-green-500 rounded p-3">
|
||||||
|
<p className="font-mono text-2xl text-green-400">{lastScan.code}</p>
|
||||||
|
<p className="text-sm text-green-300 mt-1">
|
||||||
|
{formatTimestamp(lastScan.timestamp)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-slate-900 p-3 rounded text-slate-500">
|
||||||
|
Nessuna scansione
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reset Button */}
|
||||||
|
<Button variant="secondary" onClick={reset} className="w-full">
|
||||||
|
Reset Scanner
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Log */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-focolare-blue">
|
||||||
|
⌨️ Log Tastiera (ultimi 20)
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 rounded-lg overflow-hidden">
|
||||||
|
<div className="grid grid-cols-3 gap-2 p-3 bg-slate-700 text-sm font-semibold">
|
||||||
|
<span>Ora</span>
|
||||||
|
<span>Key</span>
|
||||||
|
<span>Code</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[400px] overflow-y-auto">
|
||||||
|
{keyLog.length === 0 ? (
|
||||||
|
<div className="p-4 text-slate-500 text-center">
|
||||||
|
Premi un tasto per vedere i log...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
keyLog.map((entry, index) => {
|
||||||
|
const isStartSentinel = VALID_PATTERNS.some(p => p.start === entry.key);
|
||||||
|
const isEndSentinel = VALID_PATTERNS.some(p => p.end === entry.key);
|
||||||
|
|
||||||
|
let rowClass = 'bg-slate-900';
|
||||||
|
if (isStartSentinel) rowClass = 'bg-green-900/30';
|
||||||
|
if (isEndSentinel) rowClass = 'bg-blue-900/30';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${entry.timestamp}-${index}`}
|
||||||
|
className={`grid grid-cols-3 gap-2 p-2 border-b border-slate-700 text-sm ${rowClass}`}
|
||||||
|
>
|
||||||
|
<span className="text-slate-400 font-mono text-xs">
|
||||||
|
{formatTimestamp(entry.timestamp)}
|
||||||
|
</span>
|
||||||
|
<span className={`font-mono ${
|
||||||
|
isStartSentinel ? 'text-green-400 font-bold' :
|
||||||
|
isEndSentinel ? 'text-blue-400 font-bold' :
|
||||||
|
'text-white'
|
||||||
|
}`}>
|
||||||
|
{entry.key === ' ' ? '(space)' :
|
||||||
|
entry.key === 'Enter' ? '↵ Enter' :
|
||||||
|
entry.key}
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-400 font-mono text-xs">
|
||||||
|
{entry.code}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pattern Reference */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6 lg:col-span-2">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-focolare-yellow">
|
||||||
|
📋 Pattern Supportati
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{VALID_PATTERNS.map((pattern) => (
|
||||||
|
<div
|
||||||
|
key={pattern.name}
|
||||||
|
className="bg-slate-700 rounded-lg p-4 text-center"
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-bold mb-2">{pattern.name}</h3>
|
||||||
|
<div className="flex items-center justify-center gap-4 text-3xl font-mono">
|
||||||
|
<span className="text-green-400">{pattern.start}</span>
|
||||||
|
<span className="text-slate-500">→</span>
|
||||||
|
<span className="text-blue-400">{pattern.end}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-400 mt-2">
|
||||||
|
Esempio: {pattern.start}123456{pattern.end}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-4 text-sm text-slate-400">
|
||||||
|
💡 <strong>Nota:</strong> Il lettore RFID invia anche Enter (↵) dopo l'ultimo carattere.
|
||||||
|
L'hook lo gestisce automaticamente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DebugScreen;
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Modal per errori
|
* Modal per errori
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Modal, Button } from '../components';
|
import {Button, Modal} from '../components';
|
||||||
|
|
||||||
interface ErrorModalProps {
|
interface ErrorModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -17,7 +17,7 @@ export function ErrorModal({
|
|||||||
onClose,
|
onClose,
|
||||||
title = 'Errore',
|
title = 'Errore',
|
||||||
message
|
message
|
||||||
}: ErrorModalProps) {
|
}: ErrorModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -28,7 +28,8 @@ export function ErrorModal({
|
|||||||
<div className="text-center text-white p-8 max-w-2xl">
|
<div className="text-center text-white p-8 max-w-2xl">
|
||||||
{/* Error Icon */}
|
{/* Error Icon */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-white/20 animate-pulse-error">
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-white/20 animate-pulse-error">
|
||||||
<svg
|
<svg
|
||||||
className="w-20 h-20 text-white"
|
className="w-20 h-20 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
* Loading Screen - Focolari Voting System
|
* Loading Screen - Focolari Voting System
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logo } from '../components';
|
import {useCallback, useEffect, useState} from 'react';
|
||||||
|
import {Logo} from '../components';
|
||||||
|
import {checkServerHealth} from '../services/api';
|
||||||
|
|
||||||
interface LoadingScreenProps {
|
interface LoadingScreenProps {
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -14,11 +16,54 @@ export function LoadingScreen({
|
|||||||
message = 'Connessione al server...',
|
message = 'Connessione al server...',
|
||||||
error,
|
error,
|
||||||
onRetry
|
onRetry
|
||||||
}: LoadingScreenProps) {
|
}: LoadingScreenProps) {
|
||||||
|
const [isRetrying, setIsRetrying] = useState(false);
|
||||||
|
const [serverStatus, setServerStatus] = useState<'checking' | 'online' | 'offline'>('checking');
|
||||||
|
|
||||||
|
// Ping automatico quando c'è un errore
|
||||||
|
useEffect(() => {
|
||||||
|
if (!error) return;
|
||||||
|
|
||||||
|
const checkServer = async () => {
|
||||||
|
setServerStatus('checking');
|
||||||
|
const isOnline = await checkServerHealth();
|
||||||
|
setServerStatus(isOnline ? 'online' : 'offline');
|
||||||
|
};
|
||||||
|
|
||||||
|
checkServer();
|
||||||
|
|
||||||
|
// Ripeti il ping ogni 3 secondi
|
||||||
|
const interval = setInterval(checkServer, 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
const handleRetry = useCallback(async () => {
|
||||||
|
if (!onRetry) return;
|
||||||
|
|
||||||
|
setIsRetrying(true);
|
||||||
|
const isOnline = await checkServerHealth();
|
||||||
|
|
||||||
|
if (isOnline) {
|
||||||
|
onRetry();
|
||||||
|
} else {
|
||||||
|
setServerStatus('offline');
|
||||||
|
}
|
||||||
|
setIsRetrying(false);
|
||||||
|
}, [onRetry]);
|
||||||
|
|
||||||
|
// Se il server torna online, riprova automaticamente
|
||||||
|
useEffect(() => {
|
||||||
|
if (serverStatus === 'online' && error && onRetry) {
|
||||||
|
console.log('[FLOW] Server tornato online, ricarico...');
|
||||||
|
onRetry();
|
||||||
|
}
|
||||||
|
}, [serverStatus, error, onRetry]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center p-8 bg-gradient-to-br from-focolare-blue/5 to-focolare-blue/20">
|
<div
|
||||||
|
className="min-h-screen flex flex-col items-center justify-center p-8 bg-gradient-to-br from-focolare-blue/5 to-focolare-blue/20">
|
||||||
<div className="glass rounded-3xl p-12 shadow-xl animate-fade-in max-w-lg w-full text-center">
|
<div className="glass rounded-3xl p-12 shadow-xl animate-fade-in max-w-lg w-full text-center">
|
||||||
<Logo size="lg" showText={false} />
|
<Logo size="lg" showText={false}/>
|
||||||
|
|
||||||
<h1 className="mt-6 text-3xl font-bold text-focolare-blue">
|
<h1 className="mt-6 text-3xl font-bold text-focolare-blue">
|
||||||
Focolari Voting System
|
Focolari Voting System
|
||||||
@@ -27,7 +72,8 @@ export function LoadingScreen({
|
|||||||
{!error ? (
|
{!error ? (
|
||||||
<>
|
<>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-focolare-blue/10">
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-focolare-blue/10">
|
||||||
<svg
|
<svg
|
||||||
className="animate-spin h-8 w-8 text-focolare-blue"
|
className="animate-spin h-8 w-8 text-focolare-blue"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -70,14 +116,32 @@ export function LoadingScreen({
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-4 text-error text-lg font-semibold">{error}</p>
|
<p className="mt-4 text-error text-lg font-semibold">{error}</p>
|
||||||
|
|
||||||
|
{/* Server status indicator */}
|
||||||
|
<div className="mt-4 flex items-center justify-center gap-2">
|
||||||
|
<span className={`w-3 h-3 rounded-full ${
|
||||||
|
serverStatus === 'checking' ? 'bg-yellow-500 animate-pulse' :
|
||||||
|
serverStatus === 'online' ? 'bg-green-500' :
|
||||||
|
'bg-red-500'
|
||||||
|
}`}/>
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{serverStatus === 'checking' && 'Verifica connessione...'}
|
||||||
|
{serverStatus === 'online' && 'Server raggiungibile - riconnessione...'}
|
||||||
|
{serverStatus === 'offline' && 'Server non raggiungibile'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{onRetry && (
|
{onRetry && (
|
||||||
<button
|
<button
|
||||||
onClick={onRetry}
|
onClick={handleRetry}
|
||||||
|
disabled={isRetrying}
|
||||||
className="mt-6 px-8 py-3 bg-focolare-blue text-white rounded-xl
|
className="mt-6 px-8 py-3 bg-focolare-blue text-white rounded-xl
|
||||||
font-semibold hover:bg-focolare-blue-dark transition-colors"
|
font-semibold hover:bg-focolare-blue/90 transition-colors
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Riprova
|
{isRetrying ? 'Verifica in corso...' : 'Riprova'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Success Modal - Focolari Voting System
|
* Success Modal - Focolari Voting System
|
||||||
* Modal fullscreen per conferma ingresso
|
* Modal fullscreen per conferma ingresso con carosello multilingua
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Modal } from '../components';
|
import {Modal, WelcomeCarousel} from '../components';
|
||||||
|
|
||||||
interface SuccessModalProps {
|
interface SuccessModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
welcomeMessage: string;
|
|
||||||
userName?: string;
|
userName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SuccessModal({
|
export function SuccessModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
welcomeMessage,
|
|
||||||
userName
|
userName
|
||||||
}: SuccessModalProps) {
|
}: SuccessModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -26,10 +24,11 @@ export function SuccessModal({
|
|||||||
autoCloseMs={5000}
|
autoCloseMs={5000}
|
||||||
fullscreen
|
fullscreen
|
||||||
>
|
>
|
||||||
<div className="text-center text-white p-8">
|
<div className="flex flex-col items-center justify-center min-h-screen p-8">
|
||||||
{/* Success Icon */}
|
{/* Success Icon */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-white/20 animate-pulse-glow">
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-white/20 animate-pulse-glow">
|
||||||
<svg
|
<svg
|
||||||
className="w-20 h-20 text-white"
|
className="w-20 h-20 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -46,26 +45,17 @@ export function SuccessModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Name */}
|
{/* Carosello Messaggi Benvenuto */}
|
||||||
{userName && (
|
<WelcomeCarousel userName={userName}/>
|
||||||
<h2 className="text-4xl md:text-5xl font-bold mb-4 animate-slide-up">
|
|
||||||
{userName}
|
|
||||||
</h2>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Welcome Message */}
|
|
||||||
<h1 className="text-5xl md:text-7xl font-bold mb-8 animate-slide-up">
|
|
||||||
{welcomeMessage}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Sub text */}
|
{/* Sub text */}
|
||||||
<p className="text-2xl md:text-3xl opacity-80 animate-fade-in">
|
<p className="text-2xl md:text-3xl text-white/80 mt-8 animate-fade-in">
|
||||||
Ingresso registrato con successo
|
Ingresso registrato con successo
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Auto-close indicator */}
|
{/* Auto-close indicator */}
|
||||||
<div className="mt-12">
|
<div className="mt-12 w-full max-w-md">
|
||||||
<div className="w-64 h-2 bg-white/30 rounded-full mx-auto overflow-hidden">
|
<div className="w-full h-2 bg-white/30 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-white rounded-full"
|
className="h-full bg-white rounded-full"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* Validator Login Screen - Focolari Voting System
|
* Validator Login Screen - Focolari Voting System
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import {useEffect, useRef, useState} from 'react';
|
||||||
import { Logo, Button, Input, RFIDStatus } from '../components';
|
import {Button, Input, Logo, NumLockBanner, RFIDStatus} from '../components';
|
||||||
import type { RoomInfo, RFIDScannerState } from '../types';
|
import type {RFIDScannerState, RoomInfo} from '../types';
|
||||||
|
|
||||||
interface ValidatorLoginScreenProps {
|
interface ValidatorLoginScreenProps {
|
||||||
roomInfo: RoomInfo;
|
roomInfo: RoomInfo;
|
||||||
@@ -26,7 +26,7 @@ export function ValidatorLoginScreen({
|
|||||||
onCancel,
|
onCancel,
|
||||||
error,
|
error,
|
||||||
loading = false,
|
loading = false,
|
||||||
}: ValidatorLoginScreenProps) {
|
}: ValidatorLoginScreenProps) {
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export function ValidatorLoginScreen({
|
|||||||
<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 */}
|
||||||
<header className="p-6 flex items-center justify-between border-b bg-white/80 backdrop-blur shadow-sm">
|
<header className="p-6 flex items-center justify-between border-b bg-white/80 backdrop-blur shadow-sm">
|
||||||
<Logo size="md" />
|
<Logo size="md"/>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-lg font-semibold text-gray-800">{roomInfo.room_name}</p>
|
<p className="text-lg font-semibold text-gray-800">{roomInfo.room_name}</p>
|
||||||
<p className="text-sm text-gray-500">ID: {roomInfo.meeting_id}</p>
|
<p className="text-sm text-gray-500">ID: {roomInfo.meeting_id}</p>
|
||||||
@@ -62,7 +62,8 @@ export function ValidatorLoginScreen({
|
|||||||
// Attesa badge validatore
|
// Attesa badge validatore
|
||||||
<>
|
<>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="inline-flex items-center justify-center w-24 h-24 rounded-full bg-focolare-blue/10 mb-6">
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-24 h-24 rounded-full bg-focolare-blue/10 mb-6">
|
||||||
<svg
|
<svg
|
||||||
className="w-12 h-12 text-focolare-blue"
|
className="w-12 h-12 text-focolare-blue"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -83,30 +84,44 @@ export function ValidatorLoginScreen({
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 mb-8">
|
<p className="text-xl text-gray-600 mb-8">
|
||||||
Passa il badge del <span className="font-semibold text-focolare-blue">Validatore</span> per iniziare
|
Passa il badge del <span
|
||||||
|
className="font-semibold text-focolare-blue">Validatore</span> per iniziare
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="py-8 px-6 bg-focolare-blue/5 rounded-2xl border-2 border-dashed border-focolare-blue/30">
|
<div
|
||||||
|
className="py-8 px-6 bg-focolare-blue/5 rounded-2xl border-2 border-dashed border-focolare-blue/30">
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
<svg
|
<svg
|
||||||
className="w-8 h-8 text-focolare-blue animate-pulse"
|
className="w-8 h-8 text-focolare-blue animate-pulse"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/>
|
<path
|
||||||
|
d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-xl text-focolare-blue font-medium">
|
<span className="text-xl text-focolare-blue font-medium">
|
||||||
In attesa del badge...
|
In attesa del badge...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Banner NumLock per desktop */}
|
||||||
|
<NumLockBanner className="mt-6"/>
|
||||||
|
|
||||||
|
{/* Messaggio errore */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
// Form password
|
// Form password
|
||||||
<>
|
<>
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-success/10 mb-4">
|
<div
|
||||||
|
className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-success/10 mb-4">
|
||||||
<svg
|
<svg
|
||||||
className="w-10 h-10 text-success"
|
className="w-10 h-10 text-success"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -170,7 +185,7 @@ export function ValidatorLoginScreen({
|
|||||||
|
|
||||||
{/* Footer with RFID Status */}
|
{/* Footer with RFID Status */}
|
||||||
<footer className="p-4 border-t bg-white/50 flex items-center justify-center">
|
<footer className="p-4 border-t bg-white/50 flex items-center justify-center">
|
||||||
<RFIDStatus state={rfidState} buffer={rfidBuffer} />
|
<RFIDStatus state={rfidState} buffer={rfidBuffer}/>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Screens barrel export
|
// Screens barrel export
|
||||||
export { LoadingScreen } from './LoadingScreen';
|
export {LoadingScreen} from './LoadingScreen';
|
||||||
export { ValidatorLoginScreen } from './ValidatorLoginScreen';
|
export {ValidatorLoginScreen} from './ValidatorLoginScreen';
|
||||||
export { ActiveGateScreen } from './ActiveGateScreen';
|
export {ActiveGateScreen} from './ActiveGateScreen';
|
||||||
export { SuccessModal } from './SuccessModal';
|
export {SuccessModal} from './SuccessModal';
|
||||||
export { ErrorModal } from './ErrorModal';
|
export {ErrorModal} from './ErrorModal';
|
||||||
|
export {DebugScreen} from './DebugScreen';
|
||||||
|
|||||||
@@ -2,28 +2,39 @@
|
|||||||
* Focolari Voting System - API Service
|
* Focolari Voting System - API Service
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {EntryRequest, EntryResponse, LoginRequest, LoginResponse, RoomInfo, User} from '../types';
|
||||||
RoomInfo,
|
|
||||||
User,
|
|
||||||
LoginRequest,
|
|
||||||
LoginResponse,
|
|
||||||
EntryRequest,
|
|
||||||
EntryResponse
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
const API_BASE_URL = 'http://localhost:8000';
|
// Path relativi: funziona sia in dev (proxy Vite) che in produzione (stesso server)
|
||||||
|
const API_BASE_URL = '';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// LOGGING
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
const log = (message: string, ...args: unknown[]) => {
|
||||||
|
console.log(`[API] ${message}`, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logError = (message: string, ...args: unknown[]) => {
|
||||||
|
console.error(`[API] ${message}`, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom error class for API errors
|
* Custom error class for API errors
|
||||||
*/
|
*/
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
|
public statusCode: number;
|
||||||
|
public detail?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public statusCode: number,
|
statusCode: number,
|
||||||
public detail?: string
|
detail?: string
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'ApiError';
|
this.name = 'ApiError';
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.detail = detail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +45,8 @@ async function apiFetch<T>(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
options?: RequestInit
|
options?: RequestInit
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
log(`Fetching ${options?.method || 'GET'} ${endpoint}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
...options,
|
...options,
|
||||||
@@ -45,6 +58,7 @@ async function apiFetch<T>(
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
logError(`Error ${response.status}: ${errorData.detail || 'Unknown error'}`);
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
errorData.detail || `HTTP Error ${response.status}`,
|
errorData.detail || `HTTP Error ${response.status}`,
|
||||||
response.status,
|
response.status,
|
||||||
@@ -52,11 +66,14 @@ async function apiFetch<T>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
const data = await response.json();
|
||||||
|
log(`Response OK from ${endpoint}`);
|
||||||
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiError) {
|
if (error instanceof ApiError) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
logError('Connection error:', error);
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
'Errore di connessione al server',
|
'Errore di connessione al server',
|
||||||
0,
|
0,
|
||||||
@@ -85,7 +102,8 @@ export async function loginValidator(
|
|||||||
badge: string,
|
badge: string,
|
||||||
password: string
|
password: string
|
||||||
): Promise<LoginResponse> {
|
): Promise<LoginResponse> {
|
||||||
const payload: LoginRequest = { badge, password };
|
log(`Login attempt for badge: ${badge}`);
|
||||||
|
const payload: LoginRequest = {badge, password};
|
||||||
return apiFetch<LoginResponse>('/login-validate', {
|
return apiFetch<LoginResponse>('/login-validate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -97,6 +115,7 @@ export async function loginValidator(
|
|||||||
* Ottiene i dati anagrafici di un utente tramite badge
|
* Ottiene i dati anagrafici di un utente tramite badge
|
||||||
*/
|
*/
|
||||||
export async function getUserByBadge(badgeCode: string): Promise<User> {
|
export async function getUserByBadge(badgeCode: string): Promise<User> {
|
||||||
|
log(`Fetching anagrafica for badge: ${badgeCode}`);
|
||||||
return apiFetch<User>(`/anagrafica/${encodeURIComponent(badgeCode)}`);
|
return apiFetch<User>(`/anagrafica/${encodeURIComponent(badgeCode)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +127,7 @@ export async function requestEntry(
|
|||||||
userBadge: string,
|
userBadge: string,
|
||||||
validatorPassword: string
|
validatorPassword: string
|
||||||
): Promise<EntryResponse> {
|
): Promise<EntryResponse> {
|
||||||
|
log(`Entry request for badge: ${userBadge}`);
|
||||||
const payload: EntryRequest = {
|
const payload: EntryRequest = {
|
||||||
user_badge: userBadge,
|
user_badge: userBadge,
|
||||||
validator_password: validatorPassword,
|
validator_password: validatorPassword,
|
||||||
|
|||||||
@@ -1,101 +1,103 @@
|
|||||||
/**
|
/**
|
||||||
}
|
* Focolari Voting System - TypeScript Types
|
||||||
variant?: 'success' | 'error' | 'info';
|
*/
|
||||||
children: React.ReactNode;
|
|
||||||
onClose?: () => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
export interface ModalProps {
|
|
||||||
|
|
||||||
}
|
|
||||||
paused?: boolean;
|
|
||||||
onExpire: () => void;
|
|
||||||
seconds: number;
|
|
||||||
export interface TimerProps {
|
|
||||||
|
|
||||||
}
|
|
||||||
showWarning?: boolean;
|
|
||||||
user: User;
|
|
||||||
export interface UserCardProps {
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Component Props Types
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
}
|
|
||||||
timestamp: number;
|
|
||||||
code: string;
|
|
||||||
export interface RFIDScanResult {
|
|
||||||
|
|
||||||
export type RFIDScannerState = 'idle' | 'scanning';
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// RFID Scanner Types
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
}
|
|
||||||
expiresAt: number;
|
|
||||||
loginTime: number;
|
|
||||||
token: string;
|
|
||||||
badge: string;
|
|
||||||
export interface ValidatorSession {
|
|
||||||
|
|
||||||
| 'entry-error';
|
|
||||||
| 'entry-success'
|
|
||||||
| 'showing-user'
|
|
||||||
| 'gate-active'
|
|
||||||
| 'validator-password'
|
|
||||||
| 'waiting-validator'
|
|
||||||
| 'loading'
|
|
||||||
export type AppState =
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Application State Types
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
}
|
|
||||||
validator_password: string;
|
|
||||||
user_badge: string;
|
|
||||||
export interface EntryRequest {
|
|
||||||
|
|
||||||
}
|
|
||||||
password: string;
|
|
||||||
badge: string;
|
|
||||||
export interface LoginRequest {
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Request Types
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
}
|
|
||||||
welcome_message?: string;
|
|
||||||
message: string;
|
|
||||||
success: boolean;
|
|
||||||
export interface EntryResponse {
|
|
||||||
|
|
||||||
}
|
|
||||||
token?: string;
|
|
||||||
message: string;
|
|
||||||
success: boolean;
|
|
||||||
export interface LoginResponse {
|
|
||||||
|
|
||||||
}
|
|
||||||
warning?: string;
|
|
||||||
ammesso: boolean;
|
|
||||||
ruolo: 'Tecnico' | 'Votante' | 'Ospite';
|
|
||||||
url_foto: string;
|
|
||||||
cognome: string;
|
|
||||||
nome: string;
|
|
||||||
badge_code: string;
|
|
||||||
export interface User {
|
|
||||||
|
|
||||||
}
|
|
||||||
meeting_id: string;
|
|
||||||
room_name: string;
|
|
||||||
export interface RoomInfo {
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// API Response Types
|
// API Response Types
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
*/
|
export interface RoomInfo {
|
||||||
* Focolari Voting System - TypeScript Types
|
room_name: string;
|
||||||
|
meeting_id: string;
|
||||||
|
server_start_time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
badge_code: string;
|
||||||
|
nome: string;
|
||||||
|
cognome: string;
|
||||||
|
url_foto: string;
|
||||||
|
ruolo: 'Tecnico' | 'Votante' | 'Ospite';
|
||||||
|
ammesso: boolean;
|
||||||
|
warning?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntryResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Request Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
badge: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntryRequest {
|
||||||
|
user_badge: string;
|
||||||
|
validator_password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Application State Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export type AppState =
|
||||||
|
| 'loading'
|
||||||
|
| 'waiting-validator'
|
||||||
|
| 'validator-password'
|
||||||
|
| 'gate-active'
|
||||||
|
| 'showing-user'
|
||||||
|
| 'entry-success'
|
||||||
|
| 'entry-error';
|
||||||
|
|
||||||
|
export interface ValidatorSession {
|
||||||
|
badge: string;
|
||||||
|
password: string;
|
||||||
|
token: string;
|
||||||
|
loginTime: number;
|
||||||
|
expiresAt: number;
|
||||||
|
serverStartTime: number; // Per invalidare se il server riparte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RFID Scanner Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export type RFIDScannerState = 'idle' | 'scanning';
|
||||||
|
|
||||||
|
export interface RFIDScanResult {
|
||||||
|
code: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Component Props Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface UserCardProps {
|
||||||
|
user: User;
|
||||||
|
showWarning?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimerProps {
|
||||||
|
seconds: number;
|
||||||
|
onExpire: () => void;
|
||||||
|
paused?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
variant?: 'success' | 'error' | 'info';
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["vite/client"],
|
"types": [
|
||||||
|
"vite/client"
|
||||||
|
],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
@@ -15,7 +20,6 @@
|
|||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
@@ -24,5 +28,7 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{
|
||||||
{ "path": "./tsconfig.node.json" }
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,20 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
"target": "ES2023",
|
"target": "ES2023",
|
||||||
"lib": ["ES2023"],
|
"lib": [
|
||||||
|
"ES2023"
|
||||||
|
],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["node"],
|
"types": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
@@ -22,5 +24,7 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,40 @@
|
|||||||
import { defineConfig } from 'vite'
|
import {defineConfig} from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
|
||||||
|
// Path relativi per funzionare su qualsiasi IP/porta
|
||||||
|
base: './',
|
||||||
|
|
||||||
|
// Build nella cartella dist del frontend
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Proxy API in sviluppo verso il backend (porta 8000)
|
||||||
|
// In produzione il backend serve direttamente il frontend
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/info-room': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/login-validate': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/anagrafica': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/entry-request': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user