main.dart 19,9 ko
Newer Older
Papa THIAM's avatar
Papa THIAM a validé
import 'package:flutter/material.dart';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
import 'dart:typed_data';
import 'google_sheets_api.dart';
import 'inscription_page.dart';
import 'sheet_view_page.dart';
Papa THIAM's avatar
Papa THIAM a validé

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(),
    );
  }
Papa THIAM's avatar
Papa THIAM a validé
}

class ScannerHomePage extends StatefulWidget {
  const ScannerHomePage({super.key});
Papa THIAM's avatar
Papa THIAM a validé

  State<ScannerHomePage> createState() => _ScannerHomePageState();
}

class _ScannerHomePageState extends State<ScannerHomePage> {
  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),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
Papa THIAM's avatar
Papa THIAM a validé

// Page NFC Scanner
class NfcScannerPage extends StatefulWidget {
  const NfcScannerPage({super.key});
  @override
  State<NfcScannerPage> createState() => _NfcScannerPageState();
}

class _NfcScannerPageState extends State<NfcScannerPage> {
  bool _isReading = false;
  String _status = 'Prêt';
  final GoogleSheetsApi _sheetsApi = GoogleSheetsApi();
  final Map<String, String> _info = {};
  Future<void> _startNfc() async {
    bool available = await NfcManager.instance.isAvailable();
    if (!available) {
      setState(() => _status = '❌ NFC non disponible sur cet appareil');
      _isReading = true;
      _status = '🔄 Approchez une carte étudiant...';
    NfcManager.instance.startSession(
      pollingOptions: {
        NfcPollingOption.iso14443,
        NfcPollingOption.iso15693,
        NfcPollingOption.iso18092,
      },
      onDiscovered: (NfcTag tag) async {
          try { data = (tag as dynamic).data; } catch (_) {}

          final idBytes = _safeLookup(data, 'id');
          if (idBytes != null) {
            uid = _bytesToHexString(idBytes);
          }
            setState(() => _status = '❌ Impossible de lire le N° de série.');
            await NfcManager.instance.stopSession();
            _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();
        }

  String _bytesToHexString(dynamic maybeBytes) {
    try {
      Uint8List bytes;
      if (maybeBytes is Uint8List) bytes = maybeBytes;
      else if (maybeBytes is List<int>) 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;
Papa THIAM's avatar
Papa THIAM a validé
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
          Card(
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
            elevation: 3,
merroukmaryem's avatar
merroukmaryem a validé
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                children: [
merroukmaryem's avatar
merroukmaryem a validé
                    decoration: BoxDecoration(
                      color: MyApp.primary.withOpacity(0.12),
                      borderRadius: BorderRadius.circular(16),
merroukmaryem's avatar
merroukmaryem a validé
                    ),
                    child: Icon(_isReading ? Icons.nfc : Icons.nfc_outlined, size: 48, color: MyApp.primary),
merroukmaryem's avatar
merroukmaryem a validé
                  ),
                  const SizedBox(height: 20),
                  Text(
                    _status,
                    textAlign: TextAlign.center,
                    style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
merroukmaryem's avatar
merroukmaryem a validé
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Dernier UID: ${_info['Numéro de série'] ?? '-'}',
                    style: TextStyle(color: Colors.grey.shade600),
merroukmaryem's avatar
merroukmaryem a validé
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton.icon(
                      onPressed: _isReading ? null : _startNfc,
                      icon: const Icon(Icons.nfc),
                      label: Text(_isReading ? 'Lecture...' : 'Scanner une carte NFC'),
merroukmaryem's avatar
merroukmaryem a validé
                      style: ElevatedButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 16),
merroukmaryem's avatar
merroukmaryem a validé
                  ),
                ],
              ),
// Page QR Scanner
class QrScannerPage extends StatefulWidget {
  const QrScannerPage({super.key});
  @override
  State<QrScannerPage> createState() => _QrScannerPageState();
}
class _QrScannerPageState extends State<QrScannerPage> {
  MobileScannerController? _scannerController;
  bool _isScanning = false;
  String _status = 'Prêt à scanner';
  String? _lastQr;
  void initState() {
    super.initState();
    _scannerController = MobileScannerController();
  }

  @override
  void dispose() {
  void _startScanning() {
    setState(() {
      _isScanning = true;
      _status = 'Positionnez le QR code devant la caméra';
    });
  }

  void _stopScanning() {
      _isScanning = false;
      _status = 'Prêt à scanner';

  void _onQrDetect(BarcodeCapture capture) {
    final List<Barcode> 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'),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}