Newer
Older
import 'package:nfc_manager/nfc_manager.dart';
import 'dart:typed_data';
import 'google_sheets_api.dart';
import 'inscription_page.dart';
import 'sheet_view_page.dart';
import 'export_page.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
WidgetsFlutterBinding.ensureInitialized();
debugShowCheckedModeBanner: false,
home: const SimpleNfcReader(),
navigatorObservers: [routeObserver],
));
class SimpleNfcReader extends StatefulWidget {
const SimpleNfcReader({super.key});
@override
State<SimpleNfcReader> createState() => _SimpleNfcReaderState();
String cleanQrStudentId(String qrValue) {
final lastPart = qrValue.split('/').last;
return lastPart.replaceAll('-001989746000', '');
}
}
class _SimpleNfcReaderState extends State<SimpleNfcReader> with RouteAware{
bool _isReading = false;
bool _isQrActive = false;
String _status = 'Appuyez sur un bouton pour vérifier un étudiant';
final GoogleSheetsApi _sheetsApi = GoogleSheetsApi();
String? _lastScanType; // "QR" ou "NFC"
String? _lastStudentName; // Stocke le nom si déjà inscrit
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;
}
setState(() {
_isReading = true;
_status = '🔄 Approchez une carte étudiant...';
_lastScannedUid = null;
_lastStudentName = null;
});
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;
}
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);
}
},
);
}
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// ------------------- 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(
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;
Color _getStatusColor() {
if (_status.contains('✅')) return _successColor;
if (_status.contains('❌')) return _errorColor;
if (_status.contains('🔄')) return _warningColor;
return Colors.grey;
}
// ------------------- Build -------------------
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _backgroundColor,
appBar: AppBar(
title: const Text(
'Lecteur Étudiant',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
backgroundColor: _primaryColor,
foregroundColor: Colors.white,
elevation: 4,
),
drawer: _buildDrawer(),
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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)),
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
],
],
),
),
],
),
),
),
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 -------------------
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: [
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 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)),
),
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()));
}),
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')],
)),
_buildDrawerItem(
icon: Icons.picture_as_pdf,
title: 'Exporter présence',
onTap: () {
Navigator.of(context).pop();
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => const ExportPage()));
}),
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}) {
return ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: _primaryColor, size: 20),
),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
@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;
});
}