fix: correzioni critiche e checklist test manuali
CORREZIONI:
- Badge confrontato ESATTAMENTE come stringa (rimosso .lstrip("0"))
- Success modal si chiude quando arriva nuovo badge (fix dipendenze useCallback)
- Polling ogni 30s per invalidare sessione se server riparte
- Area carosello allargata per testi lunghi (es. russo)
DOCUMENTAZIONE:
- API_SPECIFICATION.md aggiornata: badge come stringa esatta
- Creata TEST_CHECKLIST.md con 22 test manuali
- Aggiornati piani backend e frontend
Badge sono STRINGHE, non numeri:
- "0008988288" != "8988288" (zeri iniziali significativi)
This commit is contained in:
@@ -12,6 +12,7 @@ import type {AppState, RoomInfo, User, ValidatorSession} from './types';
|
||||
// Costanti
|
||||
const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minuti
|
||||
const USER_TIMEOUT_SECONDS = 60;
|
||||
const SUCCESS_MODAL_DURATION_MS = 8000; // Aumentato per carosello più rilassato
|
||||
const STORAGE_KEY = 'focolari_validator_session';
|
||||
|
||||
function App() {
|
||||
@@ -38,6 +39,9 @@ function App() {
|
||||
// Badge non trovato (con timeout per tornare all'attesa)
|
||||
const [notFoundBadge, setNotFoundBadge] = useState<string | null>(null);
|
||||
|
||||
// Badge corrente visualizzato (per evitare ricaricamenti multipli dello stesso)
|
||||
const [currentBadgeCode, setCurrentBadgeCode] = useState<string | null>(null);
|
||||
|
||||
// ============================================
|
||||
// Session Management
|
||||
// ============================================
|
||||
@@ -120,10 +124,22 @@ function App() {
|
||||
setTimeout(() => setShowValidatorBadgeNotice(false), 4000);
|
||||
}
|
||||
} else {
|
||||
// Badge partecipante - verifica se è lo stesso già visualizzato
|
||||
if (cleanCode === currentBadgeCode) {
|
||||
console.log('[FLOW] Same badge scanned again - ignoring');
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('[FLOW] Loading participant:', cleanCode);
|
||||
// Badge partecipante - carica utente
|
||||
// Se c'era un badge non trovato, cancellalo e carica il nuovo
|
||||
setNotFoundBadge(null);
|
||||
// Se era aperta la success modal, chiudila subito
|
||||
if (showSuccessModal) {
|
||||
console.log('[FLOW] Closing success modal for new badge');
|
||||
setShowSuccessModal(false);
|
||||
setSuccessUserName(undefined);
|
||||
}
|
||||
await handleLoadUser(cleanCode);
|
||||
}
|
||||
break;
|
||||
@@ -131,7 +147,7 @@ function App() {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [appState, currentUser, validatorSession]);
|
||||
}, [appState, currentUser, validatorSession, showSuccessModal, currentBadgeCode]);
|
||||
|
||||
// ============================================
|
||||
// Initialize RFID Scanner
|
||||
@@ -153,6 +169,7 @@ function App() {
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
setNotFoundBadge(null);
|
||||
setCurrentBadgeCode(badgeCode); // Traccia il badge corrente
|
||||
setAppState('showing-user');
|
||||
|
||||
try {
|
||||
@@ -195,6 +212,7 @@ function App() {
|
||||
setSuccessUserName(userName);
|
||||
setShowSuccessModal(true);
|
||||
setCurrentUser(null);
|
||||
setCurrentBadgeCode(null); // Reset badge corrente dopo successo
|
||||
setAppState('gate-active');
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -255,6 +273,7 @@ function App() {
|
||||
const handleCancelUser = useCallback(() => {
|
||||
setCurrentUser(null);
|
||||
setNotFoundBadge(null);
|
||||
setCurrentBadgeCode(null); // Reset badge corrente
|
||||
setError(undefined);
|
||||
setAppState('gate-active');
|
||||
}, []);
|
||||
@@ -263,6 +282,7 @@ function App() {
|
||||
console.log('[App] User timeout - tornando in attesa');
|
||||
setCurrentUser(null);
|
||||
setNotFoundBadge(null);
|
||||
setCurrentBadgeCode(null); // Reset badge corrente
|
||||
setError(undefined);
|
||||
setAppState('gate-active');
|
||||
}, []);
|
||||
@@ -323,6 +343,29 @@ function App() {
|
||||
return () => clearInterval(interval);
|
||||
}, [validatorSession, clearSession]);
|
||||
|
||||
// Polling per verificare se il server è stato riavviato
|
||||
useEffect(() => {
|
||||
if (!validatorSession || !roomInfo) return;
|
||||
|
||||
const checkServerRestart = async () => {
|
||||
try {
|
||||
const info = await getRoomInfo();
|
||||
if (info.server_start_time !== validatorSession.serverStartTime) {
|
||||
console.log('[FLOW] Server restarted - invalidating session');
|
||||
clearSession();
|
||||
}
|
||||
} catch {
|
||||
// Ignora errori di connessione durante il polling
|
||||
console.warn('[FLOW] Server unreachable during polling');
|
||||
}
|
||||
};
|
||||
|
||||
// Polling ogni 30 secondi
|
||||
const interval = setInterval(checkServerRestart, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [validatorSession, roomInfo, clearSession]);
|
||||
|
||||
// ============================================
|
||||
// Render
|
||||
// ============================================
|
||||
@@ -387,6 +430,7 @@ function App() {
|
||||
isOpen={showSuccessModal}
|
||||
onClose={handleSuccessModalClose}
|
||||
userName={successUserName}
|
||||
durationMs={SUCCESS_MODAL_DURATION_MS}
|
||||
/>
|
||||
|
||||
{/* Error Modal */}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Welcome Carousel Component - Focolari Voting System
|
||||
*
|
||||
* Carosello automatico di messaggi di benvenuto multilingua.
|
||||
* Scorre automaticamente ogni 800ms durante la visualizzazione.
|
||||
* Scorrimento fluido con animazione smooth.
|
||||
*/
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
@@ -25,55 +25,51 @@ const WELCOME_MESSAGES: WelcomeMessage[] = [
|
||||
{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;
|
||||
/** Intervallo tra i messaggi in ms (default 800) */
|
||||
intervalMs?: number;
|
||||
}
|
||||
|
||||
export function WelcomeCarousel({paused = false, userName}: WelcomeCarouselProps) {
|
||||
export function WelcomeCarousel({paused = false, userName, intervalMs = 800}: WelcomeCarouselProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [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);
|
||||
setCurrentIndex((prev) => (prev + 1) % WELCOME_MESSAGES.length);
|
||||
}, intervalMs);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [paused]);
|
||||
}, [paused, intervalMs]);
|
||||
|
||||
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 className="flex flex-col items-center justify-center text-center w-full max-w-4xl mx-auto px-4">
|
||||
{/* Container con overflow hidden per animazione slide - altezza maggiore per testi lunghi */}
|
||||
<div className="relative h-40 md:h-52 w-full overflow-hidden">
|
||||
{/* Messaggio corrente */}
|
||||
<div
|
||||
key={currentIndex}
|
||||
className="absolute inset-0 flex flex-col items-center justify-center animate-slide-in-up"
|
||||
>
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold text-white mb-2 drop-shadow-lg whitespace-nowrap">
|
||||
{currentMessage.text}
|
||||
</h1>
|
||||
<p className="text-lg text-white/60 uppercase tracking-wider">
|
||||
{currentMessage.lang.toUpperCase()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nome utente */}
|
||||
{userName && (
|
||||
<div className="mt-8 pt-8 border-t border-white/30">
|
||||
<div className="mt-6 pt-6 border-t border-white/30">
|
||||
<p className="text-3xl md:text-4xl text-white font-medium">
|
||||
{userName}
|
||||
</p>
|
||||
@@ -81,18 +77,43 @@ export function WelcomeCarousel({paused = false, userName}: WelcomeCarouselProps
|
||||
)}
|
||||
|
||||
{/* Indicatori */}
|
||||
<div className="flex gap-2 mt-8">
|
||||
<div className="flex gap-2 mt-6">
|
||||
{WELCOME_MESSAGES.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
||||
className={`h-2 rounded-full transition-all duration-500 ease-out ${
|
||||
index === currentIndex
|
||||
? 'bg-white w-6'
|
||||
: 'bg-white/40'
|
||||
? 'bg-white w-8'
|
||||
: 'bg-white/30 w-2'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stili per animazione smooth */}
|
||||
<style>{`
|
||||
@keyframes slideInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
}
|
||||
.animate-slide-in-up {
|
||||
animation: slideInUp ${intervalMs}ms ease-in-out;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Schermata principale del varco attivo
|
||||
*/
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Button, CountdownTimer, Logo, NumLockBanner, RFIDStatus, UserCard} from '../components';
|
||||
import type {RFIDScannerState, RoomInfo, User} from '../types';
|
||||
|
||||
@@ -39,27 +38,6 @@ export function ActiveGateScreen({
|
||||
onUserTimeout,
|
||||
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 (
|
||||
<div className="min-h-screen flex flex-col bg-gradient-to-br from-slate-50 to-slate-100">
|
||||
{/* Header */}
|
||||
@@ -123,6 +101,17 @@ export function ActiveGateScreen({
|
||||
) : notFoundBadge ? (
|
||||
// Badge non trovato
|
||||
<div className="glass rounded-3xl p-12 md:p-16 shadow-xl text-center max-w-2xl w-full">
|
||||
{/* Timer bar in alto come per l'utente trovato */}
|
||||
<div className="mb-8">
|
||||
<CountdownTimer
|
||||
key={`not-found-${notFoundBadge}`}
|
||||
seconds={NOT_FOUND_TIMEOUT_SECONDS}
|
||||
onExpire={onUserTimeout}
|
||||
warningThreshold={15}
|
||||
dangerThreshold={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-error/10 mb-8">
|
||||
<svg
|
||||
@@ -142,14 +131,6 @@ export function ActiveGateScreen({
|
||||
<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 state
|
||||
|
||||
@@ -9,19 +9,27 @@ interface SuccessModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
userName?: string;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
// Numero messaggi nel carosello
|
||||
const CAROUSEL_MESSAGE_COUNT = 10;
|
||||
|
||||
export function SuccessModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
userName
|
||||
userName,
|
||||
durationMs = 8000
|
||||
}: SuccessModalProps) {
|
||||
// Calcola intervallo carosello per mostrare tutti i messaggi durante la durata
|
||||
const carouselIntervalMs = Math.floor(durationMs / (CAROUSEL_MESSAGE_COUNT * 1.2));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
variant="success"
|
||||
autoCloseMs={5000}
|
||||
autoCloseMs={durationMs}
|
||||
fullscreen
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen p-8">
|
||||
@@ -46,7 +54,7 @@ export function SuccessModal({
|
||||
</div>
|
||||
|
||||
{/* Carosello Messaggi Benvenuto */}
|
||||
<WelcomeCarousel userName={userName}/>
|
||||
<WelcomeCarousel userName={userName} intervalMs={carouselIntervalMs} />
|
||||
|
||||
{/* Sub text */}
|
||||
<p className="text-2xl md:text-3xl text-white/80 mt-8 animate-fade-in">
|
||||
@@ -59,7 +67,7 @@ export function SuccessModal({
|
||||
<div
|
||||
className="h-full bg-white rounded-full"
|
||||
style={{
|
||||
animation: 'shrink 5s linear forwards',
|
||||
animation: `shrink ${durationMs}ms linear forwards`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user