import 'package:flutter/material.dart'; import 'package:nfc_manager/nfc_manager.dart'; import 'dart:typed_data'; import 'google_sheets_api.dart'; import 'inscription_page.dart'; import 'sheet_view_page.dart'; import 'export_page.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; final RouteObserver routeObserver = RouteObserver(); void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(MaterialApp( debugShowCheckedModeBanner: false, home: const SimpleNfcReader(), navigatorObservers: [routeObserver], )); } class SimpleNfcReader extends StatefulWidget { const SimpleNfcReader({super.key}); @override State createState() => _SimpleNfcReaderState(); String cleanQrStudentId(String qrValue) { final lastPart = qrValue.split('/').last; return lastPart.replaceAll('-001989746000', ''); } } class _SimpleNfcReaderState extends State with RouteAware { bool _isNfcActive = false; bool _isQrActive = false; String _nfcStatus = 'Prêt pour le scan NFC'; String _qrStatus = 'Prêt pour le scan QR'; final GoogleSheetsApi _sheetsApi = GoogleSheetsApi(); String? _lastScannedUid; String? _lastScanType; String? _lastStudentName; final Color _primaryColor = const Color(0xFF4361EE); final Color _successColor = const Color(0xFF06D6A0); final Color _errorColor = const Color(0xFFEF476F); final Color _warningColor = const Color(0xFFFFD166); final Color _backgroundColor = const Color(0xFFF8F9FA); // ------------------- NFC ------------------- Future _startNfc() async { bool available = await NfcManager.instance.isAvailable(); if (!available) { setState(() => _nfcStatus = '❌ NFC non disponible sur cet appareil'); return; } setState(() { _isNfcActive = true; _nfcStatus = '🔄 Approchez une carte étudiant...'; _lastScannedUid = null; _lastStudentName = null; }); NfcManager.instance.startSession( pollingOptions: { NfcPollingOption.iso14443, NfcPollingOption.iso15693, NfcPollingOption.iso18092, }, onDiscovered: (NfcTag tag) async { String? uid; try { dynamic data; try { data = (tag as dynamic).data; } catch (_) {} final idBytes = _safeLookup(data, 'id'); if (idBytes != null) uid = _bytesToHexString(idBytes); if (uid == null) { setState(() => _nfcStatus = '❌ Impossible de lire le N° de série.'); await NfcManager.instance.stopSession(); setState(() => _isNfcActive = false); return; } await NfcManager.instance.stopSession(); setState(() => _isNfcActive = false); await _processIdentifier(uid, isQr: false); } catch (e) { setState(() { _nfcStatus = 'Erreur: ${e.toString()}'; }); await NfcManager.instance.stopSession(); setState(() => _isNfcActive = false); } }, ); } void _stopNfc() { NfcManager.instance.stopSession(); setState(() { _isNfcActive = false; _nfcStatus = 'Scan NFC annulé'; }); } // ------------------- QR ------------------- Future _onQrDetected(String code) async { if (!_isQrActive) return; setState(() => _isQrActive = false); final studentId = widget.cleanQrStudentId(code); await _processIdentifier(studentId, isQr: true); } void _stopQr() { setState(() { _isQrActive = false; _qrStatus = 'Scan QR annulé'; }); } // ------------------- Traitement commun ------------------- Future _processIdentifier(String value, {required bool isQr}) async { if (isQr) { setState(() => _qrStatus = '🔄 Vérification en cours...'); } else { setState(() => _nfcStatus = '🔄 Vérification en cours...'); } String? nom; if (isQr) { nom = await _sheetsApi.findStudentNameByQr(value); } else { nom = await _sheetsApi.findStudentNameByNfcUid(value); } setState(() { _lastScannedUid = value; _lastScanType = isQr ? "QR" : "NFC"; _lastStudentName = nom; final statusMessage = nom != null ? '✅ Bienvenue, $nom !' : '❌ Étudiant inexistant.\nID: $value'; if (isQr) { _qrStatus = statusMessage; } else { _nfcStatus = statusMessage; } }); } // ------------------- Navigation ------------------- Future _navigateToInscription() async { if (_lastScannedUid != null && _lastScanType != null) { final result = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => InscriptionPage( prefilledMac: _lastScannedUid, scanType: _lastScanType!, ), ), ); if (result == true) { _resetScanner(); } } } void _resetScanner() { setState(() { _isNfcActive = false; _isQrActive = false; _lastScannedUid = null; _lastScanType = null; _lastStudentName = null; _nfcStatus = 'Prêt pour le scan NFC'; _qrStatus = 'Prêt pour le scan QR'; }); } // ------------------- Utilitaires ------------------- String _bytesToHexString(dynamic maybeBytes) { try { Uint8List bytes; if (maybeBytes is Uint8List) { bytes = maybeBytes; } else if (maybeBytes is List) { bytes = Uint8List.fromList(maybeBytes); } else { return maybeBytes.toString(); } return bytes .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) .join(''); } catch (_) { return maybeBytes.toString(); } } dynamic _safeLookup(dynamic obj, String key) { if (obj == null) return null; try { if (obj is Map) { if (obj.containsKey(key)) return obj[key]; final lower = key.toLowerCase(); if (obj.containsKey(lower)) return obj[lower]; } } catch (_) {} try { return (obj as dynamic)[key]; } catch (_) {} try { if (key == 'id') return (obj as dynamic).id; } catch (_) {} return null; } Color _getStatusColor(String status) { if (status.contains('✅')) return _successColor; if (status.contains('❌')) return _errorColor; if (status.contains('🔄')) return _warningColor; return Colors.grey; } // ------------------- Build ------------------- @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _backgroundColor, appBar: AppBar( title: const Text( 'Lecteur Étudiant', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), ), backgroundColor: _primaryColor, foregroundColor: Colors.white, elevation: 4, ), drawer: _buildDrawer(), body: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Scanner NFC Expanded( child: _buildScannerCard( title: 'Scanner NFC', icon: Icons.nfc, color: _primaryColor, status: _nfcStatus, isActive: _isNfcActive, onStart: _isQrActive ? null : _startNfc, onStop: _stopNfc, isQr: false, ), ), const SizedBox(height: 16), // Scanner QR Expanded( child: _buildScannerCard( title: 'Scanner QR Code', icon: Icons.qr_code_scanner, color: _successColor, status: _qrStatus, isActive: _isQrActive, onStart: _isNfcActive ? null : () { setState(() { _isQrActive = true; _qrStatus = '🔄 Scanner le QR code...'; _lastScannedUid = null; _lastStudentName = null; }); }, onStop: _stopQr, isQr: true, ), ), ], ), ), ); } Widget _buildScannerCard({ required String title, required IconData icon, required Color color, required String status, required bool isActive, required VoidCallback? onStart, required VoidCallback onStop, required bool isQr, }) { return Card( elevation: 8, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Stack( children: [ // Fond du scanner QR actif if (isQr && isActive) Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(20), child: MobileScanner( fit: BoxFit.cover, onDetect: (capture) async { if (!_isQrActive) return; if (capture.barcodes.isNotEmpty) { final code = capture.barcodes.first.rawValue; if (code != null) { await _onQrDetected(code); } } }, ), ), ), // Contenu principal Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: isActive && !isQr ? null : LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ color.withOpacity(0.1), color.withOpacity(0.05), ], ), ), child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Icône AnimatedContainer( duration: const Duration(milliseconds: 300), width: 100, height: 100, decoration: BoxDecoration( color: isActive && isQr ? Colors.white.withOpacity(0.2) : _getStatusColor(status).withOpacity(0.1), shape: BoxShape.circle, border: Border.all( color: isActive && isQr ? Colors.white : _getStatusColor(status).withOpacity(0.3), width: 2, ), ), child: Icon( icon, size: 50, color: isActive && isQr ? Colors.white : _getStatusColor(status), ), ), const SizedBox(height: 20), // Titre Text( title, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: isActive && isQr ? Colors.white : color, ), ), const SizedBox(height: 20), // Zone de statut Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isActive && isQr ? Colors.black.withOpacity(0.5) : _getStatusColor(status).withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: isActive && isQr ? Colors.white.withOpacity(0.3) : _getStatusColor(status).withOpacity(0.2), ), ), child: Column( children: [ Text( status, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: isActive && isQr ? Colors.white : _getStatusColor(status), height: 1.4, ), textAlign: TextAlign.center, ), if (_lastScannedUid != null && !isActive && _lastStudentName == null && ((isQr && _lastScanType == "QR") || (!isQr && _lastScanType == "NFC"))) ...[ const SizedBox(height: 12), ElevatedButton.icon( onPressed: _navigateToInscription, icon: const Icon(Icons.person_add_alt_1_rounded, size: 18), label: const Text( 'INSCRIRE CET ÉTUDIANT', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, ), ), style: ElevatedButton.styleFrom( backgroundColor: _primaryColor, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), ), ], ], ), ), const SizedBox(height: 20), // Bouton SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: isActive ? onStop : onStart, style: ElevatedButton.styleFrom( backgroundColor: isActive ? _errorColor : color, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 4, ), child: Text( isActive ? 'Arrêter' : 'Scanner', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), ], ), ), ), // Bordure active if (isActive) Positioned.fill( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all(color: color, width: 3), ), ), ), ], ), ); } // ------------------- Drawer ------------------- Widget _buildDrawer() { return Drawer( child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [_primaryColor.withOpacity(0.1), _backgroundColor], ), ), child: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: _primaryColor, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16)), ), child: Column( children: [ const CircleAvatar( radius: 30, backgroundColor: Colors.white, child: Icon(Icons.school_rounded, size: 30, color: Color(0xFF4361EE)), ), const SizedBox(height: 12), const Text('NFC Reader', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), Text('Gestion des étudiants', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 12)), ], ), ), const SizedBox(height: 16), _buildDrawerItem( icon: Icons.person_add_rounded, title: 'Inscription', onTap: () async { Navigator.of(context).pop(); if (_lastScannedUid != null && _lastScanType != null) { final result = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => InscriptionPage( prefilledMac: _lastScannedUid, scanType: _lastScanType!, ), ), ); if (result == true) { _resetScanner(); } } }, ), _buildDrawerItem( icon: Icons.table_chart_rounded, title: 'Liste des données', onTap: () async { Navigator.of(context).pop(); await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SheetViewPage())); }), const Divider(indent: 16, endIndent: 16), _buildDrawerItem( icon: Icons.info_outline_rounded, title: 'À propos', onTap: () => showAboutDialog( context: context, applicationName: 'NFC Reader', applicationVersion: '1.0.0', children: [ const Text( 'Application de gestion des étudiants via NFC') ], )), _buildDrawerItem( icon: Icons.picture_as_pdf, title: 'Exporter présence', onTap: () { Navigator.of(context).pop(); Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ExportPage())); }), const Spacer(), Padding( padding: const EdgeInsets.all(16), child: Text('Version 1.0.0', style: TextStyle(color: Colors.grey.shade600, fontSize: 12), textAlign: TextAlign.center), ), ], ), ), ), ); } Widget _buildDrawerItem( {required IconData icon, required String title, required VoidCallback onTap}) { return ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: Icon(icon, color: _primaryColor, size: 20), ), title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)), onTap: onTap, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ); } @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe(this, ModalRoute.of(context)! as PageRoute); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } @override void didPopNext() { setState(() { _nfcStatus = 'Prêt pour le scan NFC'; _qrStatus = 'Prêt pour le scan QR'; _isQrActive = false; _isNfcActive = false; _lastScannedUid = null; _lastScanType = null; _lastStudentName = null; }); } }