main.dart 15,3 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';
Papa THIAM's avatar
Papa THIAM a validé

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MaterialApp(
    debugShowCheckedModeBanner: false,
    home: SimpleNfcReader(),
  ));
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();
Papa THIAM's avatar
Papa THIAM a validé

class _SimpleNfcReaderState extends State<SimpleNfcReader> {
  bool _isReading = false;
  String _status = 'Appuyez sur le bouton pour vérifier un étudiant';
  final GoogleSheetsApi _sheetsApi = GoogleSheetsApi();
  final Map<String, String> _info = {};
Papa THIAM's avatar
Papa THIAM a validé

mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
  // 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<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...';
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
      _lastScannedUid = null; // Reset de l'UID
    NfcManager.instance.startSession(
      pollingOptions: {
        NfcPollingOption.iso14443,
        NfcPollingOption.iso15693,
        NfcPollingOption.iso18092,
      },
      onDiscovered: (NfcTag tag) async {
          dynamic data;
          try { data = (tag as dynamic).data; } catch (_) {}
Papa THIAM's avatar
Papa THIAM a validé

          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 !';
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
              _lastScannedUid = null; // Reset car étudiant trouvé
            } else {
              _status = '❌ Étudiant inexistant.\nN°: $uid';
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
              _lastScannedUid = uid; // Stocke l'UID pour l'inscription
        } catch (e) {
          setState(() {
            _status = 'Erreur: ${e.toString()}';
          });
          await NfcManager.instance.stopSession();
        }
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
  // 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<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;
  }

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 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: [
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
              // 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,
                      ),
                    ),
                  ],
                ),
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé

              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'];
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
                  await Navigator.of(context).push(
                    MaterialPageRoute(
                      builder: (_) => InscriptionPage(prefilledMac: prefMac),
                    ),
                  );
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé

              _buildDrawerItem(
                icon: Icons.table_chart_rounded,
                title: 'Liste des données',
                onTap: () async {
                  Navigator.of(context).pop();
mouhamed lamine kebe's avatar
mouhamed lamine kebe 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',
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
                  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()),
                  );
                },
              ),
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
              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,
mouhamed lamine kebe's avatar
mouhamed lamine kebe a validé
    );
  }

  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),
Papa THIAM's avatar
Papa THIAM a validé
        ),
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),
      ),
      onTap: onTap,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
Papa THIAM's avatar
Papa THIAM a validé
      ),
    );
  }