feat: setup iniziale sistema controllo accessi Focolari

Struttura progetto:
- Backend mock Python (FastAPI) con API per gestione varchi
- Frontend React + TypeScript + Vite + Tailwind CSS
- Documentazione e piani di sviluppo

Backend (backend-mock/):
- API REST: /info-room, /login-validate, /anagrafica, /entry-request
- Dati mock: 7 utenti, validatore (999999/focolari)
- CORS abilitato, docs OpenAPI automatiche
- Configurazione pipenv per ambiente virtuale

Frontend (frontend/):
- State machine completa per flusso accesso varco
- Hook useRFIDScanner per lettura badge (pattern singolo)
- Componenti UI: Logo, Button, Input, Modal, UserCard, Timer
- Schermate: Loading, Login, ActiveGate, Success/Error Modal
- Design system con colori Focolari
- Ottimizzato per tablet touch

Documentazione (ai-prompts/):
- Welcome guide per futuri agenti
- Piano sviluppo backend e frontend con checklist

DA COMPLETARE:
- Hook RFID multi-pattern (US/IT/altri layout tastiera)
- Pagina /debug per diagnostica in loco
- Logging console strutturato
This commit is contained in:
2026-01-17 18:20:55 +01:00
commit 21b509c6ba
40 changed files with 7453 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
/**
* Focolari Voting System - RFID Scanner Hook
*
* Questo hook gestisce la lettura di badge RFID tramite lettori USB
* che emulano una tastiera. Il protocollo prevede:
* - Carattere di inizio: `;`
* - Carattere di fine: `?`
* - Esempio: `;00012345?`
*
* L'hook funziona indipendentemente dal focus dell'applicazione.
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import type { RFIDScannerState, RFIDScanResult } from '../types';
// Costanti
const START_SENTINEL = ';';
const END_SENTINEL = '?';
const TIMEOUT_MS = 3000; // 3 secondi di timeout
interface UseRFIDScannerOptions {
/** Callback chiamato quando un badge viene letto con successo */
onScan: (code: string) => void;
/** Callback opzionale chiamato in caso di timeout */
onTimeout?: () => void;
/** Callback opzionale chiamato quando inizia la scansione */
onScanStart?: () => void;
/** Se true, previene l'input nei campi di testo durante la scansione */
preventDefaultOnScan?: boolean;
/** Se true, l'hook è disabilitato */
disabled?: boolean;
}
interface UseRFIDScannerReturn {
/** Stato corrente dello scanner */
state: RFIDScannerState;
/** Buffer corrente (solo per debug) */
buffer: string;
/** Ultimo codice scansionato */
lastScan: RFIDScanResult | null;
/** Reset manuale dello scanner */
reset: () => void;
}
export function useRFIDScanner({
onScan,
onTimeout,
onScanStart,
preventDefaultOnScan = true,
disabled = false,
}: UseRFIDScannerOptions): UseRFIDScannerReturn {
const [state, setState] = useState<RFIDScannerState>('idle');
const [buffer, setBuffer] = useState<string>('');
const [lastScan, setLastScan] = useState<RFIDScanResult | null>(null);
// Refs per mantenere i valori aggiornati nei callback
const bufferRef = useRef<string>('');
const stateRef = useRef<RFIDScannerState>('idle');
const timeoutRef = useRef<number | null>(null);
// Sync refs con state
useEffect(() => {
bufferRef.current = buffer;
}, [buffer]);
useEffect(() => {
stateRef.current = state;
}, [state]);
/**
* Pulisce il timeout attivo
*/
const clearScanTimeout = useCallback(() => {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
/**
* Resetta lo scanner allo stato idle
*/
const reset = useCallback(() => {
clearScanTimeout();
setState('idle');
setBuffer('');
bufferRef.current = '';
stateRef.current = 'idle';
}, [clearScanTimeout]);
/**
* Avvia il timeout di sicurezza
*/
const startTimeout = useCallback(() => {
clearScanTimeout();
timeoutRef.current = window.setTimeout(() => {
console.warn('[RFID Scanner] Timeout - lettura incompleta scartata');
onTimeout?.();
reset();
}, TIMEOUT_MS);
}, [clearScanTimeout, onTimeout, reset]);
/**
* Handler principale per gli eventi keydown
*/
useEffect(() => {
if (disabled) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
const key = event.key;
// Ignora tasti speciali (frecce, funzione, ecc.)
if (key.length > 1 && key !== START_SENTINEL && key !== END_SENTINEL) {
// Eccezione per Backspace in stato scanning: ignora ma non resetta
if (key === 'Backspace' && stateRef.current === 'scanning') {
if (preventDefaultOnScan) {
event.preventDefault();
}
}
return;
}
// STATO IDLE: attende il carattere di inizio
if (stateRef.current === 'idle') {
if (key === START_SENTINEL) {
console.log('[RFID Scanner] Start sentinel rilevato - inizio scansione');
if (preventDefaultOnScan) {
event.preventDefault();
}
setState('scanning');
setBuffer('');
bufferRef.current = '';
startTimeout();
onScanStart?.();
}
// Altrimenti ignora il tasto (comportamento normale)
return;
}
// STATO SCANNING: accumula i caratteri o termina
if (stateRef.current === 'scanning') {
if (preventDefaultOnScan) {
event.preventDefault();
}
if (key === END_SENTINEL) {
// Fine della scansione
clearScanTimeout();
const scannedCode = bufferRef.current.trim();
if (scannedCode.length > 0) {
console.log('[RFID Scanner] Codice scansionato:', scannedCode);
const result: RFIDScanResult = {
code: scannedCode,
timestamp: Date.now(),
};
setLastScan(result);
onScan(scannedCode);
} else {
console.warn('[RFID Scanner] Codice vuoto scartato');
}
reset();
} else if (key === START_SENTINEL) {
// Nuovo start sentinel durante scansione: resetta e ricomincia
console.log('[RFID Scanner] Nuovo start sentinel - reset buffer');
setBuffer('');
bufferRef.current = '';
startTimeout();
} else {
// Accumula il carattere nel buffer
const newBuffer = bufferRef.current + key;
setBuffer(newBuffer);
bufferRef.current = newBuffer;
}
}
};
// Aggiungi listener globale
window.addEventListener('keydown', handleKeyDown, { capture: true });
// Cleanup
return () => {
window.removeEventListener('keydown', handleKeyDown, { capture: true });
clearScanTimeout();
};
}, [
disabled,
onScan,
onScanStart,
preventDefaultOnScan,
clearScanTimeout,
reset,
startTimeout,
]);
// Cleanup al unmount
useEffect(() => {
return () => {
clearScanTimeout();
};
}, [clearScanTimeout]);
return {
state,
buffer,
lastScan,
reset,
};
}
export default useRFIDScanner;