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 _isReading = false; bool _isQrActive = false; String _status = 'Appuyez sur un bouton pour vérifier un étudiant'; final GoogleSheetsApi _sheetsApi = GoogleSheetsApi(); String? _lastScannedUid; String? _lastScanType; // "QR" ou "NFC" String? _lastStudentName; // Stocke le nom si déjà inscrit 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(() => _status = '❌ NFC non disponible sur cet appareil'); return; } setState(() { _isReading = true; _status = '🔄 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(() => _status = '❌ Impossible de lire le N° de série.'); await NfcManager.instance.stopSession(); setState(() => _isReading = false); return; } await NfcManager.instance.stopSession(); setState(() => _isReading = false); await _processIdentifier(uid, isQr: false); } catch (e) { setState(() { _status = 'Erreur: ${e.toString()}'; }); await NfcManager.instance.stopSession(); setState(() => _isReading = false); } }, ); } // ------------------- QR ------------------- Future _onQrDetected(String code) async { setState(() => _isQrActive = false); final studentId = widget.cleanQrStudentId(code); await _processIdentifier(studentId, isQr: true); } // ------------------- Traitement commun ------------------- Future _processIdentifier(String value, {required bool isQr}) async { setState(() => _status = '🔄 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; if (nom != null) { _status = '✅ Bienvenue, $nom !'; } else { _status = '❌ Étudiant inexistant.\nID: $value'; } }); } // ------------------- 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(() { _isReading = false; _isQrActive = false; _lastScannedUid = null; _lastScanType = null; _lastStudentName = null; _status = '✅ Étudiant enregistré.\nPrêt pour un nouveau scan.'; }); } // ------------------- 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() { 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: Stack( children: [ Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( elevation: 8, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.all(32), child: Column( children: [ AnimatedContainer( duration: const Duration(milliseconds: 300), width: 120, height: 120, decoration: BoxDecoration( color: _getStatusColor().withOpacity(0.1), shape: BoxShape.circle, border: Border.all(color: _getStatusColor().withOpacity(0.3), width: 2), ), child: Icon( _isReading || _isQrActive ? Icons.qr_code : Icons.school_rounded, size: 50, color: _getStatusColor(), ), ), const SizedBox(height: 32), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: _getStatusColor().withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: _getStatusColor().withOpacity(0.2)), ), child: Column( children: [ Text( _status, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: _getStatusColor(), height: 1.4 ), textAlign: TextAlign.center, ), if (_lastScannedUid != null && !_isReading && !_isQrActive && _lastStudentName == null) ...[ const SizedBox(height: 16), ElevatedButton.icon( onPressed: _navigateToInscription, icon: const Icon(Icons.person_add_alt_1_rounded, size: 18), label: const Text('INSCRIRE CET ÉTUDIANT', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), style: ElevatedButton.styleFrom( backgroundColor: _primaryColor, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ], ], ), ), ], ), ), ), const SizedBox(height: 32), Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: _isReading || _isQrActive ? null : _startNfc, icon: const Icon(Icons.nfc), label: const Text('Scanner NFC'), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton.icon( onPressed: _isReading || _isQrActive ? null : () { setState(() { _isQrActive = true; _status = '🔄 Scanner le QR code...'; }); }, icon: const Icon(Icons.qr_code_scanner), label: const Text('Scanner QR'), ), ), ], ), ], ), ), if (_isQrActive) Positioned.fill( child: Stack( children: [ 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); } } }, ), Center( child: Container( width: 250, height: 250, decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 3), borderRadius: BorderRadius.circular(12), ), ), ), ], ), ), ], ), ); } // ------------------- 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(() { _status = 'Appuyez sur un bouton pour vérifier un étudiant'; _isQrActive = false; _isReading = false; _lastScannedUid = null; _lastScanType = null; _lastStudentName = null; }); } }