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 _isNfcActive = false;
String _nfcStatus = 'Prêt pour le scan NFC';
String _qrStatus = 'Prêt pour le scan QR';
final GoogleSheetsApi _sheetsApi = GoogleSheetsApi();
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(() => _nfcStatus = '❌ NFC non disponible sur cet appareil');
return;
}
setState(() {
_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 {
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(() => _nfcStatus = '❌ Impossible de lire le N° de série.');
await NfcManager.instance.stopSession();
return;
}
await NfcManager.instance.stopSession();
await _processIdentifier(uid, isQr: false);
} catch (e) {
setState(() {
});
await NfcManager.instance.stopSession();
}
},
);
}
void _stopNfc() {
NfcManager.instance.stopSession();
setState(() {
_isNfcActive = false;
_nfcStatus = 'Scan NFC annulé';
});
}
// ------------------- QR -------------------
Future<void> _onQrDetected(String code) async {
setState(() => _isQrActive = false);
final studentId = widget.cleanQrStudentId(code);
await _processIdentifier(studentId, isQr: true);
}
void _stopQr() {
setState(() {
_isQrActive = false;
_qrStatus = 'Scan QR annulé';
});
}
// ------------------- Traitement commun -------------------
Future<void> _processIdentifier(String value, {required bool isQr}) async {
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;
final statusMessage = nom != null
? '✅ Bienvenue, $nom !'
: '❌ Étudiant inexistant.\nID: $value';
if (isQr) {
_qrStatus = statusMessage;
}
});
}
// ------------------- 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(() {
_isQrActive = false;
_lastScannedUid = null;
_lastScanType = null;
_lastStudentName = null;
_nfcStatus = 'Prêt pour le scan NFC';
_qrStatus = 'Prêt pour le scan QR';
// ------------------- 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(String status) {
if (status.contains('✅')) return _successColor;
if (status.contains('❌')) return _errorColor;
if (status.contains('🔄')) return _warningColor;
// ------------------- 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(),
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
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
303
304
305
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(
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
// 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: [
Text(
status,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: isActive && isQr
? Colors.white
: _getStatusColor(status),
height: 1.4,
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),
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,
child: Text(
isActive ? 'Arrêter' : 'Scanner',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color, width: 3),
),
// ------------------- 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)),
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),
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(() {
_nfcStatus = 'Prêt pour le scan NFC';
_qrStatus = 'Prêt pour le scan QR';
_lastScannedUid = null;
_lastScanType = null;
_lastStudentName = null;
});
}