main.dart 20,6 ko
Newer Older
Papa THIAM's avatar
Papa THIAM a validé
import 'package:flutter/material.dart';
import 'package:nfc_manager/nfc_manager.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';
import 'package:mobile_scanner/mobile_scanner.dart';
merroukmaryem's avatar
merroukmaryem a validé

final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
Papa THIAM's avatar
Papa THIAM a validé

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MaterialApp(
    home: const SimpleNfcReader(),
    navigatorObservers: [routeObserver],
Papa THIAM's avatar
Papa THIAM a validé
}

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

  State<SimpleNfcReader> createState() => _SimpleNfcReaderState();

  String cleanQrStudentId(String qrValue) {
    final lastPart = qrValue.split('/').last;
    return lastPart.replaceAll('-001989746000', '');
  }
Papa THIAM's avatar
Papa THIAM a validé

merroukmaryem's avatar
merroukmaryem a validé
class _SimpleNfcReaderState extends State<SimpleNfcReader> with RouteAware {
  bool _isNfcActive = false;
  bool _isQrActive = false;
merroukmaryem's avatar
merroukmaryem a validé
  String _nfcStatus = 'Prêt pour le scan NFC';
  String _qrStatus = 'Prêt pour le scan QR';
  final GoogleSheetsApi _sheetsApi = GoogleSheetsApi();
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
  String? _lastScannedUid;
merroukmaryem's avatar
merroukmaryem a validé
  String? _lastScanType;
  String? _lastStudentName;
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé

  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<void> _startNfc() async {
    bool available = await NfcManager.instance.isAvailable();
    if (!available) {
merroukmaryem's avatar
merroukmaryem a validé
      setState(() => _nfcStatus = '❌ NFC non disponible sur cet appareil');
merroukmaryem's avatar
merroukmaryem a validé
      _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 {
merroukmaryem's avatar
merroukmaryem a validé
          try {
            data = (tag as dynamic).data;
          } catch (_) {}
          final idBytes = _safeLookup(data, 'id');
          if (idBytes != null) uid = _bytesToHexString(idBytes);
merroukmaryem's avatar
merroukmaryem a validé
            setState(() => _nfcStatus = '❌ Impossible de lire le N° de série.');
            await NfcManager.instance.stopSession();
merroukmaryem's avatar
merroukmaryem a validé
            setState(() => _isNfcActive = false);
          await NfcManager.instance.stopSession();
merroukmaryem's avatar
merroukmaryem a validé
          setState(() => _isNfcActive = false);
          await _processIdentifier(uid, isQr: false);
merroukmaryem's avatar
merroukmaryem a validé
            _nfcStatus = 'Erreur: ${e.toString()}';
          });
          await NfcManager.instance.stopSession();
merroukmaryem's avatar
merroukmaryem a validé
          setState(() => _isNfcActive = false);
merroukmaryem's avatar
merroukmaryem a validé
  void _stopNfc() {
    NfcManager.instance.stopSession();
    setState(() {
      _isNfcActive = false;
      _nfcStatus = 'Scan NFC annulé';
    });
  }

  // ------------------- QR -------------------
  Future<void> _onQrDetected(String code) async {
merroukmaryem's avatar
merroukmaryem a validé
    if (!_isQrActive) return;
    setState(() => _isQrActive = false);
    final studentId = widget.cleanQrStudentId(code);
    await _processIdentifier(studentId, isQr: true);
  }

merroukmaryem's avatar
merroukmaryem a validé
  void _stopQr() {
    setState(() {
      _isQrActive = false;
      _qrStatus = 'Scan QR annulé';
    });
  }

  // ------------------- Traitement commun -------------------
  Future<void> _processIdentifier(String value, {required bool isQr}) async {
merroukmaryem's avatar
merroukmaryem a validé
    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;
merroukmaryem's avatar
merroukmaryem a validé

      final statusMessage = nom != null
          ? '✅ Bienvenue, $nom !'
          : '❌ Étudiant inexistant.\nID: $value';

      if (isQr) {
        _qrStatus = statusMessage;
merroukmaryem's avatar
merroukmaryem a validé
        _nfcStatus = statusMessage;
      }
    });
  }

  // ------------------- Navigation -------------------
  Future<void> _navigateToInscription() async {
    if (_lastScannedUid != null && _lastScanType != null) {
      final result = await Navigator.of(context).push(
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
        MaterialPageRoute(
          builder: (_) => InscriptionPage(
            prefilledMac: _lastScannedUid,
            scanType: _lastScanType!,
          ),

      if (result == true) {
        _resetScanner();
      }
  void _resetScanner() {
    setState(() {
merroukmaryem's avatar
merroukmaryem a validé
      _isNfcActive = false;
      _isQrActive = false;
      _lastScannedUid = null;
      _lastScanType = null;
      _lastStudentName = null;
merroukmaryem's avatar
merroukmaryem a validé
      _nfcStatus = 'Prêt pour le scan NFC';
      _qrStatus = 'Prêt pour le scan QR';
  // ------------------- Utilitaires -------------------
  String _bytesToHexString(dynamic maybeBytes) {
    try {
      Uint8List bytes;
merroukmaryem's avatar
merroukmaryem a validé
      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é
  }

merroukmaryem's avatar
merroukmaryem a validé
  Color _getStatusColor(String status) {
    if (status.contains('✅')) return _successColor;
    if (status.contains('❌')) return _errorColor;
    if (status.contains('🔄')) return _warningColor;
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
    return Colors.grey;
  }

  // ------------------- Build -------------------
Papa THIAM's avatar
Papa THIAM a validé
  @override
  Widget build(BuildContext context) {
    return Scaffold(
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
      backgroundColor: _backgroundColor,
      appBar: AppBar(
        title: const Text(
          'Lecteur Étudiant',
          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
        ),
        backgroundColor: _primaryColor,
        foregroundColor: Colors.white,
        elevation: 4,
      ),
      drawer: _buildDrawer(),
merroukmaryem's avatar
merroukmaryem a validé
      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(
merroukmaryem's avatar
merroukmaryem a validé
          // 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: [
merroukmaryem's avatar
merroukmaryem a validé
                        Text(
                          status,
                          style: TextStyle(
                            fontSize: 15,
                            fontWeight: FontWeight.w500,
                            color: isActive && isQr
                                ? Colors.white
                                : _getStatusColor(status),
                            height: 1.4,
merroukmaryem's avatar
merroukmaryem a validé
                          textAlign: TextAlign.center,
merroukmaryem's avatar
merroukmaryem a validé
                        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),
merroukmaryem's avatar
merroukmaryem a validé
                            ),
merroukmaryem's avatar
merroukmaryem a validé
                        ],
merroukmaryem's avatar
merroukmaryem a validé
                  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,
merroukmaryem's avatar
merroukmaryem a validé
                      child: Text(
                        isActive ? 'Arrêter' : 'Scanner',
                        style: const TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        ),
merroukmaryem's avatar
merroukmaryem a validé
                  ),
                ],
              ),
merroukmaryem's avatar
merroukmaryem a validé
          // Bordure active
          if (isActive)
            Positioned.fill(
merroukmaryem's avatar
merroukmaryem a validé
              child: Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(20),
                  border: Border.all(color: color, width: 3),
                ),
  // ------------------- Drawer -------------------
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
  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: [
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
              Container(
                padding: const EdgeInsets.all(20),
                decoration: BoxDecoration(
                  color: _primaryColor,
                  borderRadius: const BorderRadius.only(
                      bottomLeft: Radius.circular(16),
                      bottomRight: Radius.circular(16)),
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
                ),
                child: Column(
                  children: [
                    const CircleAvatar(
                      radius: 30,
                      backgroundColor: Colors.white,
merroukmaryem's avatar
merroukmaryem a validé
                      child: Icon(Icons.school_rounded,
                          size: 30, color: Color(0xFF4361EE)),
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
                    ),
                    const SizedBox(height: 12),
                    const Text('NFC Reader',
merroukmaryem's avatar
merroukmaryem a validé
                        style: TextStyle(
                            color: Colors.white,
                            fontSize: 18,
                            fontWeight: FontWeight.bold)),
                    Text('Gestion des étudiants',
merroukmaryem's avatar
merroukmaryem a validé
                        style: TextStyle(
                            color: Colors.white.withOpacity(0.8),
                            fontSize: 12)),
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
              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();
merroukmaryem's avatar
merroukmaryem a validé
                    await Navigator.of(context).push(
                        MaterialPageRoute(builder: (_) => const SheetViewPage()));
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
              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',
merroukmaryem's avatar
merroukmaryem a validé
                    children: [
                      const Text(
                          'Application de gestion des étudiants via NFC')
                    ],
                  icon: Icons.picture_as_pdf,
                  title: 'Exporter présence',
                  onTap: () {
                    Navigator.of(context).pop();
merroukmaryem's avatar
merroukmaryem a validé
                    Navigator.of(context).push(
                        MaterialPageRoute(builder: (_) => const ExportPage()));
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
              const Spacer(),
              Padding(
                padding: const EdgeInsets.all(16),
                child: Text('Version 1.0.0',
merroukmaryem's avatar
merroukmaryem a validé
                    style:
                    TextStyle(color: Colors.grey.shade600, fontSize: 12),
                    textAlign: TextAlign.center),
merroukmaryem's avatar
merroukmaryem a validé
  Widget _buildDrawerItem(
      {required IconData icon,
        required String title,
        required VoidCallback onTap}) {
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
    return ListTile(
      leading: Container(
        padding: const EdgeInsets.all(8),
        decoration: BoxDecoration(
            color: _primaryColor.withOpacity(0.1),
            borderRadius: BorderRadius.circular(8)),
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
        child: Icon(icon, color: _primaryColor, size: 20),
      ),
      title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
      onTap: onTap,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
Papa THIAM's avatar
Papa THIAM a validé
    );
  }
  @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(() {
merroukmaryem's avatar
merroukmaryem a validé
      _nfcStatus = 'Prêt pour le scan NFC';
      _qrStatus = 'Prêt pour le scan QR';
      _isQrActive = false;
merroukmaryem's avatar
merroukmaryem a validé
      _isNfcActive = false;
      _lastScannedUid = null;
      _lastScanType = null;
      _lastStudentName = null;
    });
  }