main.dart 16,7 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';
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é

class _SimpleNfcReaderState extends State<SimpleNfcReader> with RouteAware{
  bool _isQrActive = false;
  String _status = 'Appuyez sur un bouton pour vérifier un étudiant';
  final GoogleSheetsApi _sheetsApi = GoogleSheetsApi();
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
  String? _lastScannedUid;
  String? _lastScanType;      // "QR" ou "NFC"
  String? _lastStudentName;   // Stocke le nom si déjà inscrit
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) {
      setState(() => _status = '❌ NFC non disponible sur cet appareil');
      return;
    }
      _status = '🔄 Approchez une carte étudiant...';
      _lastScannedUid = null;
      _lastStudentName = null;
    NfcManager.instance.startSession(
      pollingOptions: {
        NfcPollingOption.iso14443,
        NfcPollingOption.iso15693,
        NfcPollingOption.iso18092,
      },
      onDiscovered: (NfcTag tag) async {
          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<void> _onQrDetected(String code) async {
    setState(() => _isQrActive = false);
    final studentId = widget.cleanQrStudentId(code);
    await _processIdentifier(studentId, isQr: true);
  }

  // ------------------- Traitement commun -------------------
  Future<void> _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<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(() {
      _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<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é
  }

mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
  Color _getStatusColor() {
    if (_status.contains('✅')) return _successColor;
    if (_status.contains('❌')) return _errorColor;
    if (_status.contains('🔄')) return _warningColor;
    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(),
      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 -------------------
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,
                      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',
                        style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
                    Text('Gestion des étudiants',
                        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();
                    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',
                    children: [const Text('Application de gestion des étudiants via NFC')],
                  )),
                  icon: Icons.picture_as_pdf,
                  title: 'Exporter présence',
                  onTap: () {
                    Navigator.of(context).pop();
                    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',
                    style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
                    textAlign: TextAlign.center),
  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(() {
      _status = 'Appuyez sur un bouton pour vérifier un étudiant';
      _isQrActive = false;
      _isReading = false;
      _lastScannedUid = null;
      _lastScanType = null;
      _lastStudentName = null;
    });
  }