import 'package:flutter/material.dart'; import 'package:nfc_manager/nfc_manager.dart'; import 'package:mobile_scanner/mobile_scanner.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 MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); static const Color primary = Color(0xFF4361EE); static const Color accent = Color(0xFF06D6A0); @override Widget build(BuildContext context) { final base = ThemeData.light(); return MaterialApp( debugShowCheckedModeBanner: false, title: 'NFC Student Manager', theme: base.copyWith( useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: primary, primary: primary, secondary: accent, ), appBarTheme: const AppBarTheme( elevation: 2, backgroundColor: primary, foregroundColor: Colors.white, ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: accent, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ), inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), ), ), home: const ScannerHomePage(), ); } } class ScannerHomePage extends StatefulWidget { const ScannerHomePage({super.key}); @override State createState() => _ScannerHomePageState(); } class _ScannerHomePageState extends State { final PageController _pageController = PageController(); int _currentPage = 0; @override void dispose() { _pageController.dispose(); super.dispose(); } void _onPageChanged(int page) { setState(() => _currentPage = page); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Gestion Étudiants NFC'), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.refresh_rounded), onPressed: () => setState(() {}), tooltip: 'Rafraîchir', ) ], ), drawer: Drawer( child: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DrawerHeader( decoration: const BoxDecoration(color: MyApp.primary), child: Row( children: [ CircleAvatar(radius: 28, backgroundColor: Colors.white70, child: const Icon(Icons.school, color: MyApp.primary)), const SizedBox(width: 12), const Expanded( child: Text('NFC Manager', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), ) ], ), ), ListTile( leading: const Icon(Icons.home_outlined), title: const Text('Accueil'), onTap: () => Navigator.of(context).pop(), ), ListTile( leading: const Icon(Icons.person_add), title: const Text('Inscription'), onTap: () { Navigator.of(context).pop(); Navigator.of(context).push(MaterialPageRoute(builder: (_) => InscriptionPage(scanType: 'NFC'))); }, ), ListTile( leading: const Icon(Icons.table_chart), title: const Text('Liste des données'), onTap: () { Navigator.of(context).pop(); Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SheetViewPage())); }, ), ListTile( leading: const Icon(Icons.picture_as_pdf_rounded), title: const Text('Exporter en PDF'), onTap: () { Navigator.of(context).pop(); Navigator.of(context).push(MaterialPageRoute(builder: (_) => const ExportPage())); }, ), const Spacer(), ListTile( leading: const Icon(Icons.info_outline), title: const Text('À propos'), onTap: () => showAboutDialog( context: context, applicationName: 'NFC Student Manager', children: const [Text('Gestion des inscriptions via NFC et Google Sheets')], ), ), ], ), ), ), body: Container( color: const Color(0xFFF6F8FA), child: Column( children: [ // Indicateur de page (tabs) Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Expanded( child: GestureDetector( onTap: () => _pageController.animateToPage(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut), child: Container( padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: _currentPage == 0 ? MyApp.primary : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.nfc, color: _currentPage == 0 ? Colors.white : Colors.grey, size: 20), const SizedBox(width: 8), Text( 'NFC', style: TextStyle( color: _currentPage == 0 ? Colors.white : Colors.grey, fontWeight: _currentPage == 0 ? FontWeight.bold : FontWeight.normal, fontSize: 16, ), ), ], ), ), ), ), Expanded( child: GestureDetector( onTap: () => _pageController.animateToPage(1, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut), child: Container( padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: _currentPage == 1 ? MyApp.primary : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.qr_code, color: _currentPage == 1 ? Colors.white : Colors.grey, size: 20), const SizedBox(width: 8), Text( 'QR Code', style: TextStyle( color: _currentPage == 1 ? Colors.white : Colors.grey, fontWeight: _currentPage == 1 ? FontWeight.bold : FontWeight.normal, fontSize: 16, ), ), ], ), ), ), ), ], ), ), // Pages swipables Expanded( child: PageView( controller: _pageController, onPageChanged: _onPageChanged, children: const [ NfcScannerPage(), QrScannerPage(), ], ), ), // Boutons d'action rapide Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SheetViewPage())), icon: const Icon(Icons.table_rows_rounded), label: const Text('Voir la liste'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => InscriptionPage(scanType: _currentPage == 0 ? 'NFC' : 'QR'))), icon: const Icon(Icons.person_add_alt_1_outlined), label: const Text('Inscrire'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), ], ), ), ], ), ), ); } } // Page NFC Scanner class NfcScannerPage extends StatefulWidget { const NfcScannerPage({super.key}); @override State createState() => _NfcScannerPageState(); } class _NfcScannerPageState extends State { bool _isReading = false; String _status = 'Prêt'; final GoogleSheetsApi _sheetsApi = GoogleSheetsApi(); final Map _info = {}; 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...'; }); 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...'; if (uid != null) _info['Numéro de série'] = uid; }); await NfcManager.instance.stopSession(); String? nomEtudiant = await _sheetsApi.findStudentNameByNfcUid(uid); setState(() { if (nomEtudiant != null) { _status = '✅ Bienvenue, $nomEtudiant !'; } else { _status = '❌ Étudiant inexistant.\nN°: $uid'; } }); } catch (e) { setState(() => _status = 'Erreur: ${e.toString()}'); await NfcManager.instance.stopSession(); } setState(() => _isReading = false); }, ); } 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; } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 3, child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: MyApp.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(16), ), child: Icon(_isReading ? Icons.nfc : Icons.nfc_outlined, size: 48, color: MyApp.primary), ), const SizedBox(height: 20), Text( _status, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( 'Dernier UID: ${_info['Numéro de série'] ?? '-'}', style: TextStyle(color: Colors.grey.shade600), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isReading ? null : _startNfc, icon: const Icon(Icons.nfc), label: Text(_isReading ? 'Lecture...' : 'Scanner une carte NFC'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), ), ), ], ), ), ), ], ), ); } } // Page QR Scanner class QrScannerPage extends StatefulWidget { const QrScannerPage({super.key}); @override State createState() => _QrScannerPageState(); } class _QrScannerPageState extends State { MobileScannerController? _scannerController; bool _isScanning = false; String _status = 'Prêt à scanner'; String? _lastQr; @override void initState() { super.initState(); _scannerController = MobileScannerController(); } @override void dispose() { _scannerController?.dispose(); super.dispose(); } void _startScanning() { setState(() { _isScanning = true; _status = 'Positionnez le QR code devant la caméra'; }); } void _stopScanning() { setState(() { _isScanning = false; _status = 'Prêt à scanner'; }); } void _onQrDetect(BarcodeCapture capture) { final List barcodes = capture.barcodes; if (barcodes.isEmpty) return; final String? code = barcodes.first.rawValue; if (code != null && code.isNotEmpty) { setState(() { _lastQr = code; _status = 'QR Code détecté: $code'; }); _stopScanning(); // Naviguer vers la page d'inscription avec le QR pré-rempli Navigator.of(context).push( MaterialPageRoute( builder: (_) => InscriptionPage(prefilledMac: code, scanType: 'QR'), ), ); } } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (!_isScanning) Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 3, child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: MyApp.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(16), ), child: const Icon(Icons.qr_code_scanner, size: 48, color: MyApp.primary), ), const SizedBox(height: 20), Text( _status, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( 'Dernier QR: ${_lastQr ?? '-'}', style: TextStyle(color: Colors.grey.shade600), textAlign: TextAlign.center, ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _startScanning, icon: const Icon(Icons.qr_code_scanner), label: const Text('Scanner un QR code'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), ), ), ], ), ), ) else Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 3, clipBehavior: Clip.antiAlias, child: Column( children: [ SizedBox( height: 300, child: MobileScanner( controller: _scannerController, onDetect: _onQrDetect, ), ), Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Text( _status, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), textAlign: TextAlign.center, ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _stopScanning, icon: const Icon(Icons.close), label: const Text('Annuler'), ), ), ], ), ), ], ), ), ], ), ); } }