diff --git a/Code.gs b/Code.gs index cf481f28623403fcbf276df37f1fc8bfecf9dd3b..797423cb633d3c563e91feeac4821ceed8249e50 100644 --- a/Code.gs +++ b/Code.gs @@ -37,42 +37,28 @@ function doPost(e) { if (rowIndex == -1) { // --- SCENARIO A: STUDENT NOT FOUND -> NEW ROW (ENTRY) --- + // This handles the "first one will be when the student enters" requirement var newRow = [ - formatDate(scanTime), // A: Timestamp (Date Only) - studentId, // B: ID - leoId, // C: Leo - prenom, // D: First Name - nom, // E: Last Name - formatTime(scanTime),// F: Entry Time - "" // G: Exit Time (Empty) + formatDate(scanTime), // A: Timestamp (Date Only) + studentId, // B: ID + leoId, // C: Leo + prenom, // D: First Name + nom, // E: Last Name + formatTime(scanTime), // F: Entry Time (Fixed Column 6) + "" // G: Exit Time (Empty initially) ]; sheet.appendRow(newRow); - status = "Entrée (Nouveau)"; + status = "Entrée (Start)"; } else { - // --- SCENARIO B: STUDENT FOUND -> ADD TIME TO EXISTING ROW --- + // --- SCENARIO B: STUDENT FOUND -> UPDATE EXIT COLUMN --- - // Get the entire row data to find the next empty column - var lastCol = Math.max(sheet.getLastColumn(), 10); // Check at least 10 cols - var rowValues = sheet.getRange(rowIndex, 1, 1, lastCol).getValues()[0]; - - var targetCol = -1; - - // Start checking from Index 6 (Column G) for the first empty cell - for (var i = 6; i < rowValues.length; i++) { - if (rowValues[i] === "") { - targetCol = i + 1; // Found empty spot - break; - } - } - - // If row is full, append to the very end - if (targetCol === -1) { - targetCol = sheet.getLastColumn() + 1; - } + // We explicitly target Column 7 (G) for the Exit time. + // This fulfills the requirement: "override whatever was in the second column cell" + var exitColumnIndex = 7; - // Write the timestamp in the found column - sheet.getRange(rowIndex, targetCol).setValue(formatTime(scanTime)); - status = "Sortie/Update (Col " + targetCol + ")"; + // Write/Overwrite the timestamp in Column G + sheet.getRange(rowIndex, exitColumnIndex).setValue(formatTime(scanTime)); + status = "Sortie (Updated)"; } results.push({ @@ -103,13 +89,11 @@ function doPost(e) { function findRowIndex(sheet, idToFind) { var data = sheet.getDataRange().getValues(); // Clean the ID we are looking for: String -> Trim -> UpperCase - var searchId = String(idToFind).trim().toUpperCase(); - + var searchId = String(idToFind).trim().toUpperCase(); // Loop through rows (skip header row 0) for (var i = 1; i < data.length; i++) { // Clean the ID in the sheet (Column B is index 1) var sheetId = String(data[i][1]).trim().toUpperCase(); - if (sheetId === searchId) { return i + 1; // Return the actual row number (1-based) } diff --git a/lib/components/sync_status_indicator.dart b/lib/components/sync_status_indicator.dart index 08f24ef4091b78a2901adab22ccb54dba068fe06..6fabb9dfe827f240fe5e1d597971cd004cc368d1 100644 --- a/lib/components/sync_status_indicator.dart +++ b/lib/components/sync_status_indicator.dart @@ -1,15 +1,63 @@ +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; import '../utils/locator.dart'; import '../services/attendance_service.dart'; +import '../models/student.dart'; class SyncStatusIndicator extends StatelessWidget { const SyncStatusIndicator({super.key}); + /// Generates a CSV string from the queue and triggers the system share sheet + Future _exportQueueToCsv(BuildContext context) async { + final service = getIt(); + final List students = service.queueSnapshot; + + if (students.isEmpty) return; + + try { + // 1. Build CSV Content + final StringBuffer csvBuffer = StringBuffer(); + // Header + csvBuffer.writeln("Prenom,Nom,No Etudiant,No Leo,Timestamp"); + + // Rows + for (final s in students) { + // Sanitize strings to avoid CSV breakage (remove commas from names if any) + final p = s.firstName.replaceAll(',', ''); + final n = s.lastName.replaceAll(',', ''); + final id = s.studentId; + final leo = s.leoId; + final time = s.entryTime?.toIso8601String() ?? ""; + + csvBuffer.writeln("$p,$n,$id,$leo,$time"); + } + + // 2. Save to Temporary File + final directory = await getTemporaryDirectory(); + final path = + "${directory.path}/backup_attendance_${DateTime.now().millisecondsSinceEpoch}.csv"; + final File file = File(path); + await file.writeAsString(csvBuffer.toString()); + + // 3. Share File (Allows user to save to Drive, Email, WhatsApp, etc.) + await Share.shareXFiles([ + XFile(path), + ], text: 'Backup Émargement - ${students.length} étudiants'); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("Erreur d'export : $e"))); + } + } + } + @override Widget build(BuildContext context) { final service = getIt(); - // Le composant écoute l'observable return StreamBuilder( stream: service.queueLengthStream, initialData: 0, @@ -21,24 +69,62 @@ class SyncStatusIndicator extends StatelessWidget { color: isSynced ? Colors.green[100] : Colors.orange[100], child: Padding( padding: const EdgeInsets.all(12.0), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - isSynced ? Icons.check_circle : Icons.sync_problem, - color: isSynced ? Colors.green : Colors.orange, + // Status Row + Row( + children: [ + Icon( + isSynced ? Icons.check_circle : Icons.sync_problem, + color: isSynced ? Colors.green : Colors.deepOrange, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + isSynced + ? "Toutes les données sont synchronisées." + : "Attention: $count étudiant(s) en attente.", + style: TextStyle( + color: isSynced + ? Colors.green[900] + : Colors.deepOrange[900], + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), - const SizedBox(width: 10), - Expanded( - child: Text( - isSynced - ? "Toutes les données sont synchronisées." - : "Attention: $count étudiant(s) en attente de synchro. Ne fermez pas l'appli !", - style: TextStyle( - color: isSynced ? Colors.green[900] : Colors.orange[900], - fontWeight: FontWeight.bold, + + // [NEW] Export Button (Only visible if not synced) + if (!isSynced) ...[ + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => _exportQueueToCsv(context), + icon: const Icon(Icons.save_alt, color: Colors.black87), + label: const Text( + "SAUVEGARDER LE CSV DE SECOURS", + style: TextStyle(color: Colors.black87), + ), + style: OutlinedButton.styleFrom( + backgroundColor: Colors.white.withOpacity(0.5), + side: const BorderSide(color: Colors.black54), + ), ), ), - ), + const Padding( + padding: EdgeInsets.only(top: 4.0), + child: Text( + "Utilisez ce bouton si la connexion échoue pour ne pas perdre les scans.", + style: TextStyle( + fontSize: 11, + fontStyle: FontStyle.italic, + ), + ), + ), + ], ], ), ), diff --git a/lib/services/attendance_service.dart b/lib/services/attendance_service.dart index d5e6584b9104fb9134ed44b3bedb173affab497c..572cca1330451ed0d265af871cd47e27c15742a4 100644 --- a/lib/services/attendance_service.dart +++ b/lib/services/attendance_service.dart @@ -18,6 +18,9 @@ class AttendanceService { ); Stream get queueLengthStream => _queueLengthSubject.stream; + // Used for exporting data manually in case of network failure. + List get queueSnapshot => _uploadQueue.toList(); + // Timer for batch flushing Timer? _flushTimer; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d23d05881b554326e645083799ab9bfc5e..f6f23bfe970ffe22ab2e64b10b6ae24575915cda 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87a7eb61e17463f7406106f6c413533cecf..f16b4c34213acd9dbc719b4548786853e6e9503b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/pubspec.lock b/pubspec.lock index abf6b1d1951b462c60febee69f7c2369ea305224..5339b02bf034bb47be8ea3ad7f688476b8185a71 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://pub.dev" + source: hosted + version: "0.3.5+1" crypto: dependency: transitive description: @@ -65,6 +73,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" fixnum: dependency: transitive description: @@ -192,6 +216,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -216,6 +248,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -240,6 +328,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + url: "https://pub.dev" + source: hosted + version: "10.1.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" sky_engine: dependency: transitive description: flutter @@ -301,6 +405,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: "direct main" description: @@ -333,6 +469,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.9.2 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 135029f4eca4ca2f93d8d2ffdefc52a43d2ff008..b9db18d100a382e761f560d82a854425ce7f3e58 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,8 @@ dependencies: uuid: ^4.0.0 flutter_dotenv: ^5.1.0 mobile_scanner: ^5.0.0 + path_provider: ^2.1.1 + share_plus: ^10.0.0 # nfc_manager: ^3.3.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d4680af388f28db8742ef7fb8246e2bb1fffb..c3384ec52327e838ae40b4fdc60f87ab05f49bc6 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,12 @@ #include "generated_plugin_registrant.h" +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c30c16703f640bc38523e56204ade09399e..01d383628b88f141a5faf8d32a0379aea57e0181 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + share_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST