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'; void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const MaterialApp( debugShowCheckedModeBanner: false, home: SimpleNfcReader(), )); } class SimpleNfcReader extends StatefulWidget { const SimpleNfcReader({super.key}); @override State createState() => _SimpleNfcReaderState(); } class _SimpleNfcReaderState extends State { bool _isReading = false; String _status = 'Appuyez sur le bouton pour vérifier un étudiant'; final GoogleSheetsApi _sheetsApi = GoogleSheetsApi(); final Map _info = {}; // Nouvelle variable pour stocker l'UID quand l'étudiant n'est pas trouvé String? _lastScannedUid; // Couleurs personnalisées 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); 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; // Reset de l'UID }); 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; } setState(() => _status = '🔄 N° de série: $uid\nVérification en cours...'); await NfcManager.instance.stopSession(); String? nomEtudiant = await _sheetsApi.findStudentNameByNfcUid(uid); setState(() { if (nomEtudiant != null) { _status = '✅ Bienvenue, $nomEtudiant !'; _lastScannedUid = null; // Reset car étudiant trouvé } else { _status = '❌ Étudiant inexistant.\nN°: $uid'; _lastScannedUid = uid; // Stocke l'UID pour l'inscription } }); } catch (e) { setState(() { _status = 'Erreur: ${e.toString()}'; }); await NfcManager.instance.stopSession(); } setState(() => _isReading = false); }, ); } // Fonction pour naviguer vers la page d'inscription void _navigateToInscription() { if (_lastScannedUid != null) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => InscriptionPage(prefilledMac: _lastScannedUid), ), ); } } 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; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _backgroundColor, appBar: AppBar( title: const Text( 'Lecteur NFC Étudiant', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 20, ), ), backgroundColor: _primaryColor, foregroundColor: Colors.white, elevation: 4, ), drawer: _buildDrawer(), body: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Carte principale Card( elevation: 8, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), child: Padding( padding: const EdgeInsets.all(32), child: Column( children: [ // Icone animée 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 ? Icons.nfc : Icons.school_rounded, size: 50, color: _getStatusColor(), ), ), const SizedBox(height: 32), // Statut 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, ), // Bouton d'inscription uniquement si étudiant non trouvé if (_lastScannedUid != null && !_isReading) ...[ const SizedBox(height: 16), Container( height: 45, child: 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 Spacer(), // Bouton de scan principal Container( height: 70, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: _primaryColor.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: ElevatedButton.icon( onPressed: _isReading ? null : _startNfc, icon: _isReading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : const Icon(Icons.nfc, size: 24), label: Text( _isReading ? 'LECTURE EN COURS...' : 'SCANNER LA CARTE', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1, ), ), style: ElevatedButton.styleFrom( backgroundColor: _primaryColor, foregroundColor: Colors.white, disabledBackgroundColor: Colors.grey, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), padding: const EdgeInsets.symmetric(vertical: 20), ), ), ), ], ), ), ); } 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: [ // En-tête du drawer 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), // Options du menu _buildDrawerItem( icon: Icons.person_add_rounded, title: 'Inscription', onTap: () async { Navigator.of(context).pop(); final prefMac = _info['Numéro de série'] ?? _info['UID brute']; await Navigator.of(context).push( MaterialPageRoute( builder: (_) => InscriptionPage(prefilledMac: prefMac), ), ); }, ), _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(); // fermer le drawer Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ExportPage()), ); }, ), const Spacer(), // Pied de page 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), ), ); } }