From fbf4cea2e1540d77eeef845e1b3a1083bb3cb607 Mon Sep 17 00:00:00 2001 From: "b.razafimandimbihery" Date: Fri, 30 May 2025 11:02:18 +0300 Subject: [PATCH] handle product --- lib/Views/HandleProduct.dart | 1030 ++++++++++++++-------------------- 1 file changed, 434 insertions(+), 596 deletions(-) diff --git a/lib/Views/HandleProduct.dart b/lib/Views/HandleProduct.dart index 8a8c58b..d069d3e 100644 --- a/lib/Views/HandleProduct.dart +++ b/lib/Views/HandleProduct.dart @@ -13,6 +13,7 @@ import '../Components/app_bar.dart'; import '../Models/produit.dart'; import '../Services/productDatabase.dart'; + class ProductManagementPage extends StatefulWidget { const ProductManagementPage({super.key}); @@ -31,13 +32,7 @@ class _ProductManagementPageState extends State { // Catégories prédéfinies pour l'ajout de produits final List _predefinedCategories = [ - 'Sucré', - 'Salé', - 'Jus', - 'Gateaux', - 'Snacks', - 'Boissons', - 'Non catégorisé' + 'Sucré', 'Salé', 'Jus', 'Gateaux', 'Snacks', 'Boissons', 'Non catégorisé' ]; @override @@ -53,14 +48,19 @@ class _ProductManagementPageState extends State { super.dispose(); } + + + + + + //====================================================================================================== // Ajoutez ces variables à la classe _ProductManagementPageState - bool _isImporting = false; - double _importProgress = 0.0; - String _importStatusText = ''; +bool _isImporting = false; +double _importProgress = 0.0; +String _importStatusText = ''; // Ajoutez ces méthodes à la classe _ProductManagementPageState - void _resetImportState() { setState(() { _isImporting = false; @@ -196,422 +196,288 @@ Future _importFromExcel() async { } setState(() { - _isImporting = false; + _isImporting = true; _importProgress = 0.0; - _importStatusText = ''; + _importStatusText = 'Lecture du fichier...'; }); - } - - void _showExcelCompatibilityError() { - Get.dialog( - AlertDialog( - title: const Text('Fichier Excel incompatible'), - content: const Text( - 'Ce fichier Excel contient des éléments qui ne sont pas compatibles avec notre système d\'importation.\n\n' - 'Solutions recommandées :\n' - '• Téléchargez notre modèle Excel et copiez-y vos données\n' - '• Ou exportez votre fichier en format simple: Classeur Excel .xlsx depuis Excel\n' - '• Ou créez un nouveau fichier Excel simple sans formatage complexe'), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - TextButton( - onPressed: () { - Get.back(); - _downloadExcelTemplate(); - }, - child: const Text('Télécharger modèle'), - style: TextButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ), - ], - ), - ); - } - - Future _downloadExcelTemplate() async { - try { - final excel = Excel.createExcel(); - excel.delete('Sheet1'); - excel.copy('Sheet1', 'Produits'); - excel.delete('Sheet1'); - - final sheet = excel['Produits']; - - final headers = ['Nom', 'Prix', 'Catégorie', 'Description', 'Stock']; - for (int i = 0; i < headers.length; i++) { - final cell = - sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0)); - cell.value = headers[i]; - cell.cellStyle = CellStyle( - bold: true, - backgroundColorHex: '#E8F4FD', - ); - } - - final examples = [ - ['Croissant', '1.50', 'Sucré', 'Délicieux croissant beurré', '20'], - ['Sandwich jambon', '4.00', 'Salé', 'Sandwich fait maison', '15'], - ['Jus d\'orange', '2.50', 'Jus', 'Jus d\'orange frais', '30'], - [ - 'Gâteau chocolat', - '18.00', - 'Gateaux', - 'Gâteau au chocolat portion 8 personnes', - '5' - ], - ]; - for (int row = 0; row < examples.length; row++) { - for (int col = 0; col < examples[row].length; col++) { - final cell = sheet.cell( - CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row + 1)); - cell.value = examples[row][col]; - } - } - - sheet.setColWidth(0, 20); - sheet.setColWidth(1, 10); - sheet.setColWidth(2, 15); - sheet.setColWidth(3, 30); - sheet.setColWidth(4, 10); - - final bytes = excel.save(); - - if (bytes == null) { - Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); - return; - } + final file = File(result.files.single.path!); + + if (!await file.exists()) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); + return; + } - final String? outputFile = await FilePicker.platform.saveFile( - fileName: 'modele_import_produits.xlsx', - allowedExtensions: ['xlsx'], - type: FileType.custom, - ); + setState(() { + _importProgress = 0.1; + _importStatusText = 'Vérification du fichier...'; + }); - if (outputFile != null) { - try { - await File(outputFile).writeAsBytes(bytes); - Get.snackbar( - 'Succès', - 'Modèle téléchargé avec succès\n$outputFile', - duration: const Duration(seconds: 4), - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } catch (e) { - Get.snackbar('Erreur', 'Impossible d\'écrire le fichier: $e'); - } - } - } catch (e) { - Get.snackbar('Erreur', 'Erreur lors de la création du modèle: $e'); - debugPrint('Erreur création modèle Excel: $e'); + final bytes = await file.readAsBytes(); + + if (bytes.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier Excel est vide'); + return; } - } - - Future _importFromExcel() async { - try { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['xlsx', 'xls', 'csv'], - allowMultiple: false, - ); - if (result == null || result.files.isEmpty) { - Get.snackbar('Annulé', 'Aucun fichier sélectionné'); - return; - } + setState(() { + _importProgress = 0.2; + _importStatusText = 'Décodage du fichier Excel...'; + }); + Excel excel; + try { setState(() { _isImporting = true; _importProgress = 0.0; - _importStatusText = 'Lecture du fichier...'; + _importStatusText = 'Initialisation...'; }); - final file = File(result.files.single.path!); - - if (!await file.exists()) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); + await Future.delayed(Duration(milliseconds: 50)); + excel = Excel.decodeBytes(bytes); + } catch (e) { + _resetImportState(); + debugPrint('Erreur décodage Excel: $e'); + + if (e.toString().contains('styles') || e.toString().contains('Damaged')) { + _showExcelCompatibilityError(); + return; + } else { + Get.snackbar('Erreur', 'Impossible de lire le fichier Excel. Format non supporté.'); return; } + } - setState(() { - _importProgress = 0.1; - _importStatusText = 'Vérification du fichier...'; - }); + if (excel.tables.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille'); + return; + } - final bytes = await file.readAsBytes(); + setState(() { + _importProgress = 0.3; + _importStatusText = 'Analyse des données...'; + }); - if (bytes.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier Excel est vide'); - return; - } + int successCount = 0; + int errorCount = 0; + List errorMessages = []; - setState(() { - _importProgress = 0.2; - _importStatusText = 'Décodage du fichier Excel...'; - }); + final sheetName = excel.tables.keys.first; + final sheet = excel.tables[sheetName]!; + + if (sheet.rows.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'La feuille Excel est vide'); + return; + } - Excel excel; + final totalRows = sheet.rows.length - 1; + + setState(() { + _importStatusText = 'Importation en cours... (0/$totalRows)'; + }); + + for (var i = 1; i < sheet.rows.length; i++) { try { + final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); setState(() { - _isImporting = true; - _importProgress = 0.0; - _importStatusText = 'Initialisation...'; + _importProgress = currentProgress; + _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; }); - await Future.delayed(Duration(milliseconds: 50)); - excel = Excel.decodeBytes(bytes); - } catch (e) { - _resetImportState(); - debugPrint('Erreur décodage Excel: $e'); - - if (e.toString().contains('styles') || - e.toString().contains('Damaged')) { - _showExcelCompatibilityError(); - return; - } else { - Get.snackbar('Erreur', - 'Impossible de lire le fichier Excel. Format non supporté.'); - return; - } - } - - if (excel.tables.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille'); - return; - } - - setState(() { - _importProgress = 0.3; - _importStatusText = 'Analyse des données...'; - }); - - int successCount = 0; - int errorCount = 0; - List errorMessages = []; - - final sheetName = excel.tables.keys.first; - final sheet = excel.tables[sheetName]!; - - if (sheet.rows.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'La feuille Excel est vide'); - return; - } - - final totalRows = sheet.rows.length - 1; - - setState(() { - _importStatusText = 'Importation en cours... (0/$totalRows)'; - }); - - for (var i = 1; i < sheet.rows.length; i++) { - try { - final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); - setState(() { - _importProgress = currentProgress; - _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; - }); + await Future.delayed(const Duration(milliseconds: 10)); - await Future.delayed(const Duration(milliseconds: 10)); - - final row = sheet.rows[i]; - - if (row.isEmpty || row.length < 2) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Données insuffisantes'); - continue; - } - - String? nameValue; - String? priceValue; - - if (row[0]?.value != null) { - nameValue = row[0]!.value.toString().trim(); - } + final row = sheet.rows[i]; + + if (row.isEmpty || row.length < 2) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Données insuffisantes'); + continue; + } - if (row[1]?.value != null) { - priceValue = row[1]!.value.toString().trim(); - } + String? nameValue; + String? priceValue; + + if (row[0]?.value != null) { + nameValue = row[0]!.value.toString().trim(); + } + + if (row[1]?.value != null) { + priceValue = row[1]!.value.toString().trim(); + } + + if (nameValue == null || nameValue.isEmpty) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Nom du produit manquant'); + continue; + } - if (nameValue == null || nameValue.isEmpty) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Nom du produit manquant'); - continue; - } + if (priceValue == null || priceValue.isEmpty) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Prix manquant'); + continue; + } - if (priceValue == null || priceValue.isEmpty) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Prix manquant'); - continue; - } + final name = nameValue; + final price = double.tryParse(priceValue.replaceAll(',', '.')); - final name = nameValue; - final price = double.tryParse(priceValue.replaceAll(',', '.')); + if (price == null || price <= 0) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Prix invalide ($priceValue)'); + continue; + } - if (price == null || price <= 0) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Prix invalide ($priceValue)'); - continue; + String category = 'Non catégorisé'; + if (row.length > 2 && row[2]?.value != null) { + final categoryValue = row[2]!.value.toString().trim(); + if (categoryValue.isNotEmpty) { + category = categoryValue; } + } - String category = 'Non catégorisé'; - if (row.length > 2 && row[2]?.value != null) { - final categoryValue = row[2]!.value.toString().trim(); - if (categoryValue.isNotEmpty) { - category = categoryValue; - } - } + String description = ''; + if (row.length > 3 && row[3]?.value != null) { + description = row[3]!.value.toString().trim(); + } - String description = ''; - if (row.length > 3 && row[3]?.value != null) { - description = row[3]!.value.toString().trim(); - } + int stock = 0; + if (row.length > 4 && row[4]?.value != null) { + final stockStr = row[4]!.value.toString().trim(); + stock = int.tryParse(stockStr) ?? 0; + } - int stock = 0; - if (row.length > 4 && row[4]?.value != null) { - final stockStr = row[4]!.value.toString().trim(); - stock = int.tryParse(stockStr) ?? 0; - } + String reference = _generateUniqueReference(); + var existingProduct = await _productDatabase.getProductByReference(reference); + while (existingProduct != null) { + reference = _generateUniqueReference(); + existingProduct = await _productDatabase.getProductByReference(reference); + } + + final product = Product( + name: name, + price: price, + image: '', + category: category, + description: description, + stock: stock, + qrCode: '', + reference: reference, + ); - String reference = _generateUniqueReference(); - var existingProduct = - await _productDatabase.getProductByReference(reference); - while (existingProduct != null) { - reference = _generateUniqueReference(); - existingProduct = - await _productDatabase.getProductByReference(reference); - } + setState(() { + _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; + }); + + final qrPath = await _generateAndSaveQRCode(reference); + product.qrCode = qrPath; - final product = Product( - name: name, - price: price, - image: '', - category: category, - description: description, - stock: stock, - qrCode: '', - reference: reference, - ); - - setState(() { - _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; - }); - - final qrPath = await _generateAndSaveQRCode(reference); - product.qrCode = qrPath; - - await _productDatabase.createProduct(product); - successCount++; - } catch (e) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Erreur de traitement - $e'); - debugPrint('Erreur ligne ${i + 1}: $e'); - } + await _productDatabase.createProduct(product); + successCount++; + + } catch (e) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Erreur de traitement - $e'); + debugPrint('Erreur ligne ${i + 1}: $e'); } + } - setState(() { - _importProgress = 1.0; - _importStatusText = 'Finalisation...'; - }); - - await Future.delayed(const Duration(milliseconds: 500)); + setState(() { + _importProgress = 1.0; + _importStatusText = 'Finalisation...'; + }); - _resetImportState(); + await Future.delayed(const Duration(milliseconds: 500)); - String message = '$successCount produits importés avec succès'; - if (errorCount > 0) { - message += ', $errorCount erreurs'; + _resetImportState(); - if (errorMessages.length <= 5) { - message += ':\n${errorMessages.join('\n')}'; - } + String message = '$successCount produits importés avec succès'; + if (errorCount > 0) { + message += ', $errorCount erreurs'; + + if (errorMessages.length <= 5) { + message += ':\n${errorMessages.join('\n')}'; } - - Get.snackbar( - 'Importation terminée', - message, - duration: const Duration(seconds: 6), - colorText: Colors.white, - backgroundColor: successCount > 0 ? Colors.green : Colors.orange, - ); - - // Recharger la liste des produits après importation - _loadProducts(); - } catch (e) { - _resetImportState(); - Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); - debugPrint('Erreur générale import Excel: $e'); } + + Get.snackbar( + 'Importation terminée', + message, + duration: const Duration(seconds: 6), + colorText: Colors.white, + backgroundColor: successCount > 0 ? Colors.green : Colors.orange, + ); + + // Recharger la liste des produits après importation + _loadProducts(); + + } catch (e) { + _resetImportState(); + Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); + debugPrint('Erreur générale import Excel: $e'); } +} // Ajoutez ce widget dans votre méthode build, par exemple dans la partie supérieure - Widget _buildImportProgressIndicator() { - if (!_isImporting) return const SizedBox.shrink(); - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Importation en cours...', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - ), - const SizedBox(height: 8), - LinearProgressIndicator( - value: _importProgress, - backgroundColor: Colors.blue.shade100, - valueColor: AlwaysStoppedAnimation(Colors.blue.shade600), +Widget _buildImportProgressIndicator() { + if (!_isImporting) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Importation en cours...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, ), - const SizedBox(height: 8), - Text( - _importStatusText, - style: TextStyle( - fontSize: 14, - color: Colors.blue.shade700, - ), + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: _importProgress, + backgroundColor: Colors.blue.shade100, + valueColor: AlwaysStoppedAnimation(Colors.blue.shade600), + ), + const SizedBox(height: 8), + Text( + _importStatusText, + style: TextStyle( + fontSize: 14, + color: Colors.blue.shade700, ), - const SizedBox(height: 8), - Text( - '${(_importProgress * 100).round()}%', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.blue.shade600, - ), + ), + const SizedBox(height: 8), + Text( + '${(_importProgress * 100).round()}%', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.blue.shade600, ), - ], - ), - ); - } - + ), + ], + ), + ); +} //============================================================================================================================= Future _loadProducts() async { setState(() => _isLoading = true); - + try { await _productDatabase.initDatabase(); final products = await _productDatabase.getProducts(); final categories = await _productDatabase.getCategories(); - + setState(() { _products = products; _filteredProducts = products; @@ -626,16 +492,16 @@ Future _importFromExcel() async { void _filterProducts() { final query = _searchController.text.toLowerCase(); - + setState(() { _filteredProducts = _products.where((product) { final matchesSearch = product.name.toLowerCase().contains(query) || product.description!.toLowerCase().contains(query) || product.reference!.toLowerCase().contains(query); - - final matchesCategory = _selectedCategory == 'Tous' || + + final matchesCategory = _selectedCategory == 'Tous' || product.category == _selectedCategory; - + return matchesSearch && matchesCategory; }).toList(); }); @@ -651,7 +517,7 @@ Future _importFromExcel() async { // Méthode pour générer et sauvegarder le QR Code Future _generateAndSaveQRCode(String reference) async { final qrUrl = 'https://stock.guycom.mg/$reference'; - + final validation = QrValidator.validate( data: qrUrl, version: QrVersions.auto, @@ -672,10 +538,9 @@ Future _importFromExcel() async { final directory = await getApplicationDocumentsDirectory(); final path = '${directory.path}/$reference.png'; - + try { - final picData = - await painter.toImageData(2048, format: ImageByteFormat.png); + final picData = await painter.toImageData(2048, format: ImageByteFormat.png); if (picData != null) { await File(path).writeAsBytes(picData.buffer.asUint8List()); } else { @@ -694,9 +559,8 @@ Future _importFromExcel() async { final stockController = TextEditingController(); final descriptionController = TextEditingController(); final imageController = TextEditingController(); - - String selectedCategory = - _predefinedCategories.last; // 'Non catégorisé' par défaut + + String selectedCategory = _predefinedCategories.last; // 'Non catégorisé' par défaut File? pickedImage; String? qrPreviewData; String? currentReference; @@ -724,8 +588,7 @@ Future _importFromExcel() async { color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), ), - child: - Icon(Icons.add_shopping_cart, color: Colors.green.shade700), + child: Icon(Icons.add_shopping_cart, color: Colors.green.shade700), ), const SizedBox(width: 12), const Text('Ajouter un produit'), @@ -751,13 +614,11 @@ Future _importFromExcel() async { ), child: Row( children: [ - Icon(Icons.info, - color: Colors.red.shade600, size: 16), + Icon(Icons.info, color: Colors.red.shade600, size: 16), const SizedBox(width: 8), const Text( 'Les champs marqués d\'un * sont obligatoires', - style: TextStyle( - fontSize: 12, fontWeight: FontWeight.w500), + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), ], ), @@ -781,17 +642,16 @@ Future _importFromExcel() async { }, ), const SizedBox(height: 16), - + // Prix et Stock sur la même ligne Row( children: [ Expanded( child: TextField( controller: priceController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), + keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( - labelText: 'Prix (MGA) *', + labelText: 'Prix (FCFA) *', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.attach_money), filled: true, @@ -816,14 +676,12 @@ Future _importFromExcel() async { ], ), const SizedBox(height: 16), - + // Catégorie DropdownButtonFormField( value: selectedCategory, - items: _predefinedCategories - .map((category) => DropdownMenuItem( - value: category, child: Text(category))) - .toList(), + items: _predefinedCategories.map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), onChanged: (value) { setDialogState(() => selectedCategory = value!); }, @@ -836,7 +694,7 @@ Future _importFromExcel() async { ), ), const SizedBox(height: 16), - + // Description TextField( controller: descriptionController, @@ -850,7 +708,7 @@ Future _importFromExcel() async { ), ), const SizedBox(height: 16), - + // Section Image Container( padding: const EdgeInsets.all(12), @@ -892,13 +750,10 @@ Future _importFromExcel() async { const SizedBox(width: 8), ElevatedButton.icon( onPressed: () async { - final result = await FilePicker.platform - .pickFiles(type: FileType.image); - if (result != null && - result.files.single.path != null) { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { setDialogState(() { - pickedImage = - File(result.files.single.path!); + pickedImage = File(result.files.single.path!); imageController.text = pickedImage!.path; }); } @@ -912,7 +767,7 @@ Future _importFromExcel() async { ], ), const SizedBox(height: 12), - + // Aperçu de l'image if (pickedImage != null) Center( @@ -921,13 +776,11 @@ Future _importFromExcel() async { width: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - border: - Border.all(color: Colors.grey.shade300), + border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: Image.file(pickedImage!, - fit: BoxFit.cover), + child: Image.file(pickedImage!, fit: BoxFit.cover), ), ), ), @@ -935,7 +788,7 @@ Future _importFromExcel() async { ), ), const SizedBox(height: 16), - + // Aperçu QR Code if (qrPreviewData != null) Container( @@ -949,8 +802,7 @@ Future _importFromExcel() async { children: [ Row( children: [ - Icon(Icons.qr_code_2, - color: Colors.green.shade700), + Icon(Icons.qr_code_2, color: Colors.green.shade700), const SizedBox(width: 8), Text( 'Aperçu du QR Code', @@ -980,8 +832,7 @@ Future _importFromExcel() async { const SizedBox(height: 8), Text( 'Réf: $currentReference', - style: const TextStyle( - fontSize: 10, color: Colors.grey), + style: const TextStyle(fontSize: 10, color: Colors.grey), ), ], ), @@ -1002,28 +853,25 @@ Future _importFromExcel() async { final name = nameController.text.trim(); final price = double.tryParse(priceController.text.trim()) ?? 0.0; final stock = int.tryParse(stockController.text.trim()) ?? 0; - + if (name.isEmpty || price <= 0) { Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); return; } - + try { // Générer une référence unique et vérifier son unicité - String finalReference = - currentReference ?? _generateUniqueReference(); - var existingProduct = await _productDatabase - .getProductByReference(finalReference); - + String finalReference = currentReference ?? _generateUniqueReference(); + var existingProduct = await _productDatabase.getProductByReference(finalReference); + while (existingProduct != null) { finalReference = _generateUniqueReference(); - existingProduct = await _productDatabase - .getProductByReference(finalReference); + existingProduct = await _productDatabase.getProductByReference(finalReference); } - + // Générer le QR code final qrPath = await _generateAndSaveQRCode(finalReference); - + final product = Product( name: name, price: price, @@ -1034,11 +882,11 @@ Future _importFromExcel() async { qrCode: qrPath, reference: finalReference, ); - + await _productDatabase.createProduct(product); Get.back(); Get.snackbar( - 'Succès', + 'Succès', 'Produit ajouté avec succès!\nRéférence: $finalReference', backgroundColor: Colors.green, colorText: Colors.white, @@ -1065,7 +913,7 @@ Future _importFromExcel() async { void _showQRCode(Product product) { final qrUrl = 'https://stock.guycom.mg/${product.reference}'; - + Get.dialog( AlertDialog( title: Row( @@ -1130,7 +978,7 @@ Future _importFromExcel() async { Clipboard.setData(ClipboardData(text: qrUrl)); Get.back(); Get.snackbar( - 'Copié', + 'Copié', 'URL copiée dans le presse-papiers', backgroundColor: Colors.green, colorText: Colors.white, @@ -1149,17 +997,14 @@ Future _importFromExcel() async { void _editProduct(Product product) { final nameController = TextEditingController(text: product.name); - final priceController = - TextEditingController(text: product.price.toString()); - final stockController = - TextEditingController(text: product.stock.toString()); - final descriptionController = - TextEditingController(text: product.description ?? ''); + final priceController = TextEditingController(text: product.price.toString()); + final stockController = TextEditingController(text: product.stock.toString()); + final descriptionController = TextEditingController(text: product.description ?? ''); final imageController = TextEditingController(text: product.image); - + String selectedCategory = product.category; File? pickedImage; - + Get.dialog( AlertDialog( title: const Text('Modifier le produit'), @@ -1177,16 +1022,17 @@ Future _importFromExcel() async { ), ), const SizedBox(height: 16), + TextField( controller: priceController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Prix*', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), + TextField( controller: stockController, keyboardType: TextInputType.number, @@ -1196,6 +1042,7 @@ Future _importFromExcel() async { ), ), const SizedBox(height: 16), + StatefulBuilder( builder: (context, setDialogState) { return Column( @@ -1215,14 +1062,11 @@ Future _importFromExcel() async { const SizedBox(width: 8), ElevatedButton( onPressed: () async { - final result = await FilePicker.platform - .pickFiles(type: FileType.image); - if (result != null && - result.files.single.path != null) { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { if (context.mounted) { setDialogState(() { - pickedImage = - File(result.files.single.path!); + pickedImage = File(result.files.single.path!); imageController.text = pickedImage!.path; }); } @@ -1233,6 +1077,7 @@ Future _importFromExcel() async { ], ), const SizedBox(height: 16), + if (pickedImage != null || product.image!.isNotEmpty) Container( height: 100, @@ -1246,19 +1091,16 @@ Future _importFromExcel() async { child: pickedImage != null ? Image.file(pickedImage!, fit: BoxFit.cover) : (product.image!.isNotEmpty - ? Image.file(File(product.image!), - fit: BoxFit.cover) + ? Image.file(File(product.image!), fit: BoxFit.cover) : const Icon(Icons.image, size: 50)), ), ), const SizedBox(height: 16), + DropdownButtonFormField( value: selectedCategory, - items: _categories - .skip(1) - .map((category) => DropdownMenuItem( - value: category, child: Text(category))) - .toList(), + items: _categories.skip(1).map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), onChanged: (value) { if (context.mounted) { setDialogState(() => selectedCategory = value!); @@ -1270,6 +1112,7 @@ Future _importFromExcel() async { ), ), const SizedBox(height: 16), + TextField( controller: descriptionController, maxLines: 3, @@ -1296,12 +1139,12 @@ Future _importFromExcel() async { final name = nameController.text.trim(); final price = double.tryParse(priceController.text.trim()) ?? 0.0; final stock = int.tryParse(stockController.text.trim()) ?? 0; - + if (name.isEmpty || price <= 0) { Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); return; } - + final updatedProduct = Product( id: product.id, name: name, @@ -1313,12 +1156,12 @@ Future _importFromExcel() async { qrCode: product.qrCode, reference: product.reference, ); - + try { await _productDatabase.updateProduct(updatedProduct); Get.back(); Get.snackbar( - 'Succès', + 'Succès', 'Produit modifié avec succès', backgroundColor: Colors.green, colorText: Colors.white, @@ -1352,7 +1195,7 @@ Future _importFromExcel() async { await _productDatabase.deleteProduct(product.id); Get.back(); Get.snackbar( - 'Succès', + 'Succès', 'Produit supprimé avec succès', backgroundColor: Colors.green, colorText: Colors.white, @@ -1363,8 +1206,7 @@ Future _importFromExcel() async { Get.snackbar('Erreur', 'Suppression échouée: $e'); } }, - child: - const Text('Supprimer', style: TextStyle(color: Colors.white)), + child: const Text('Supprimer', style: TextStyle(color: Colors.white)), ), ], ), @@ -1400,7 +1242,7 @@ Future _importFromExcel() async { ), ), const SizedBox(width: 16), - + // Informations du produit Expanded( child: Column( @@ -1426,8 +1268,7 @@ Future _importFromExcel() async { Row( children: [ Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(12), @@ -1468,7 +1309,7 @@ Future _importFromExcel() async { ], ), ), - + // Actions Column( children: [ @@ -1483,7 +1324,7 @@ Future _importFromExcel() async { tooltip: 'Modifier', ), IconButton( - onPressed: () => _deleteProduct(product), + onPressed: () => _deleteProduct(product), icon: const Icon(Icons.delete, color: Colors.red), tooltip: 'Supprimer', ), @@ -1498,139 +1339,136 @@ Future _importFromExcel() async { @override Widget build(BuildContext context) { return Scaffold( - appBar: const CustomAppBar(title: 'Gestion des produits'), - drawer: CustomDrawer(), - floatingActionButton: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FloatingActionButton( - heroTag: 'importBtn', - onPressed: _isImporting ? null : _importFromExcel, - mini: true, - child: const Icon(Icons.upload), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - const SizedBox(height: 8), - FloatingActionButton.extended( - heroTag: 'addBtn', - onPressed: _showAddProductDialog, - icon: const Icon(Icons.add), - label: const Text('Ajouter'), - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ], - ), - body: Column( - children: [ - // Barre de recherche et filtres - Container( - padding: const EdgeInsets.all(16), - color: Colors.grey.shade100, - child: Column( - children: [ - // Ajoutez cette Row pour les boutons d'import - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: _isImporting ? null : _importFromExcel, - icon: const Icon(Icons.upload), - label: const Text('Importer depuis Excel'), - ), - ), - const SizedBox(width: 10), - TextButton( - onPressed: _isImporting ? null : _downloadExcelTemplate, - child: const Text('Modèle'), + appBar: const CustomAppBar(title: 'Gestion des produits'), + drawer: CustomDrawer(), + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton( + heroTag: 'importBtn', + onPressed: _isImporting ? null : _importFromExcel, + mini: true, + child: const Icon(Icons.upload), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + const SizedBox(height: 8), + FloatingActionButton.extended( + heroTag: 'addBtn', + onPressed: _showAddProductDialog, + icon: const Icon(Icons.add), + label: const Text('Ajouter'), + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ], + ), + body: Column( + children: [ + // Barre de recherche et filtres + Container( + padding: const EdgeInsets.all(16), + color: Colors.grey.shade100, + child: Column( + children: [ + // Ajoutez cette Row pour les boutons d'import + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isImporting ? null : _importFromExcel, + icon: const Icon(Icons.upload), + label: const Text('Importer depuis Excel'), ), - ], - ), - const SizedBox(height: 16), - - // Barre de recherche existante - Row( - children: [ - Expanded( - child: TextField( - controller: _searchController, - decoration: InputDecoration( - labelText: 'Rechercher...', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.white, + ), + const SizedBox(width: 10), + TextButton( + onPressed: _isImporting ? null : _downloadExcelTemplate, + child: const Text('Modèle'), + ), + ], + ), + const SizedBox(height: 16), + + // Barre de recherche existante + Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + labelText: 'Rechercher...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), ), + filled: true, + fillColor: Colors.white, ), ), - const SizedBox(width: 16), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - child: DropdownButton( - value: _selectedCategory, - items: _categories - .map((category) => DropdownMenuItem( - value: category, child: Text(category))) - .toList(), - onChanged: (value) { - setState(() { - _selectedCategory = value!; - _filterProducts(); - }); - }, - underline: const SizedBox(), - hint: const Text('Catégorie'), - ), + ), + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), ), - ], - ), - const SizedBox(height: 12), - - // Indicateur de progression d'importation - _buildImportProgressIndicator(), - - // Compteur de produits - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${_filteredProducts.length} produit(s) trouvé(s)', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.grey, - ), + child: DropdownButton( + value: _selectedCategory, + items: _categories.map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), + onChanged: (value) { + setState(() { + _selectedCategory = value!; + _filterProducts(); + }); + }, + underline: const SizedBox(), + hint: const Text('Catégorie'), ), - if (_searchController.text.isNotEmpty || - _selectedCategory != 'Tous') - TextButton.icon( - onPressed: () { - setState(() { - _searchController.clear(); - _selectedCategory = 'Tous'; - _filterProducts(); - }); - }, - icon: const Icon(Icons.clear, size: 16), - label: const Text('Réinitialiser'), - style: TextButton.styleFrom( - foregroundColor: Colors.orange, - ), + ), + ], + ), + const SizedBox(height: 12), + + // Indicateur de progression d'importation + _buildImportProgressIndicator(), + + // Compteur de produits + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${_filteredProducts.length} produit(s) trouvé(s)', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + if (_searchController.text.isNotEmpty || _selectedCategory != 'Tous') + TextButton.icon( + onPressed: () { + setState(() { + _searchController.clear(); + _selectedCategory = 'Tous'; + _filterProducts(); + }); + }, + icon: const Icon(Icons.clear, size: 16), + label: const Text('Réinitialiser'), + style: TextButton.styleFrom( + foregroundColor: Colors.orange, ), - ], - ), - ], - ), + ), + ], + ), + ], ), - + ), + // Liste des produits Expanded( child: _isLoading @@ -1647,9 +1485,9 @@ Future _importFromExcel() async { ), const SizedBox(height: 16), Text( - _products.isEmpty - ? 'Aucun produit enregistré' - : 'Aucun produit trouvé pour cette recherche', + _products.isEmpty + ? 'Aucun produit enregistré' + : 'Aucun produit trouvé pour cette recherche', style: TextStyle( fontSize: 18, color: Colors.grey.shade600, @@ -1659,8 +1497,8 @@ Future _importFromExcel() async { const SizedBox(height: 8), Text( _products.isEmpty - ? 'Commencez par ajouter votre premier produit' - : 'Essayez de modifier vos critères de recherche', + ? 'Commencez par ajouter votre premier produit' + : 'Essayez de modifier vos critères de recherche', style: TextStyle( fontSize: 14, color: Colors.grey.shade500, @@ -1700,4 +1538,4 @@ Future _importFromExcel() async { ), ); } -} +} \ No newline at end of file