import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:file_picker/file_picker.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:excel/excel.dart' hide Border; import '../Components/appDrawer.dart'; import '../Components/app_bar.dart'; import '../Models/produit.dart'; import '../Services/productDatabase.dart'; class ProductManagementPage extends StatefulWidget { const ProductManagementPage({super.key}); @override _ProductManagementPageState createState() => _ProductManagementPageState(); } class _ProductManagementPageState extends State { final ProductDatabase _productDatabase = ProductDatabase.instance; List _products = []; List _filteredProducts = []; final TextEditingController _searchController = TextEditingController(); String _selectedCategory = 'Tous'; List _categories = ['Tous']; bool _isLoading = true; // Catégories prédéfinies pour l'ajout de produits final List _predefinedCategories = [ 'Sucré', 'Salé', 'Jus', 'Gateaux', 'Snacks', 'Boissons', 'Non catégorisé' ]; @override void initState() { super.initState(); _loadProducts(); _searchController.addListener(_filterProducts); } @override void dispose() { _searchController.dispose(); super.dispose(); } //====================================================================================================== // Ajoutez ces variables à la classe _ProductManagementPageState bool _isImporting = false; double _importProgress = 0.0; String _importStatusText = ''; // Ajoutez ces méthodes à la classe _ProductManagementPageState void _resetImportState() { setState(() { _isImporting = false; _importProgress = 0.0; _importStatusText = ''; }); } 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 { // Créez un nouveau fichier Excel final excel = Excel.createExcel(); // Supprimez la feuille par défaut si elle existe excel.delete('Sheet1'); // Créez une nouvelle feuille nommée "Produits" final sheet = excel['Produits']; // Ajoutez les en-têtes 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', ); } // Ajoutez des exemples de données 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]; } } // Définissez la largeur des colonnes sheet.setColWidth(0, 20); sheet.setColWidth(1, 10); sheet.setColWidth(2, 15); sheet.setColWidth(3, 30); sheet.setColWidth(4, 10); // Sauvegardez le fichier Excel final bytes = excel.save(); if (bytes == null) { Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); return; } // Demandez à l'utilisateur où sauvegarder le fichier final String? outputFile = await FilePicker.platform.saveFile( fileName: 'modele_import_produits.xlsx', allowedExtensions: ['xlsx'], type: FileType.custom, ); if (outputFile != null) { try { // Écrivez les données dans le fichier 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'); } } 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(() { _isImporting = true; _importProgress = 0.0; _importStatusText = 'Lecture du fichier...'; }); final file = File(result.files.single.path!); if (!await file.exists()) { _resetImportState(); Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); return; } setState(() { _importProgress = 0.1; _importStatusText = 'Vérification du fichier...'; }); final bytes = await file.readAsBytes(); if (bytes.isEmpty) { _resetImportState(); Get.snackbar('Erreur', 'Le fichier Excel est vide'); return; } setState(() { _importProgress = 0.2; _importStatusText = 'Décodage du fichier Excel...'; }); Excel excel; try { setState(() { _isImporting = true; _importProgress = 0.0; _importStatusText = 'Initialisation...'; }); 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)); 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(); } 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 (priceValue == null || priceValue.isEmpty) { errorCount++; errorMessages.add('Ligne ${i + 1}: Prix manquant'); continue; } final name = nameValue; final price = double.tryParse(priceValue.replaceAll(',', '.')); 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 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; } 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, ); 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'); } } setState(() { _importProgress = 1.0; _importStatusText = 'Finalisation...'; }); await Future.delayed(const Duration(milliseconds: 500)); _resetImportState(); 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'); } } // 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), ), 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, ), ), ], ), ); } //============================================================================================================================= 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; _categories = ['Tous', ...categories]; _isLoading = false; }); } catch (e) { setState(() => _isLoading = false); Get.snackbar('Erreur', 'Impossible de charger les produits: $e'); } } 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' || product.category == _selectedCategory; return matchesSearch && matchesCategory; }).toList(); }); } // Méthode pour générer une référence unique String _generateUniqueReference() { final timestamp = DateTime.now().millisecondsSinceEpoch; final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0'); return 'PROD_${timestamp}${randomSuffix}'; } // 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, errorCorrectionLevel: QrErrorCorrectLevel.L, ); if (validation.status != QrValidationStatus.valid) { throw Exception('Données QR invalides: ${validation.error}'); } final qrCode = validation.qrCode!; final painter = QrPainter.withQr( qr: qrCode, color: Colors.black, emptyColor: Colors.white, gapless: true, ); final directory = await getApplicationDocumentsDirectory(); final path = '${directory.path}/$reference.png'; try { final picData = await painter.toImageData(2048, format: ImageByteFormat.png); if (picData != null) { await File(path).writeAsBytes(picData.buffer.asUint8List()); } else { throw Exception('Impossible de générer l\'image QR'); } } catch (e) { throw Exception('Erreur lors de la génération du QR code: $e'); } return path; } void _showAddProductDialog() { final nameController = TextEditingController(); final priceController = TextEditingController(); final stockController = TextEditingController(); final descriptionController = TextEditingController(); final imageController = TextEditingController(); String selectedCategory = _predefinedCategories.last; // 'Non catégorisé' par défaut File? pickedImage; String? qrPreviewData; String? currentReference; // Fonction pour mettre à jour le QR preview void updateQrPreview() { if (nameController.text.isNotEmpty) { if (currentReference == null) { currentReference = _generateUniqueReference(); } qrPreviewData = 'https://stock.guycom.mg/$currentReference'; } else { currentReference = null; qrPreviewData = null; } } Get.dialog( AlertDialog( title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.add_shopping_cart, color: Colors.green.shade700), ), const SizedBox(width: 12), const Text('Ajouter un produit'), ], ), content: Container( width: 600, constraints: const BoxConstraints(maxHeight: 600), child: SingleChildScrollView( child: StatefulBuilder( builder: (context, setDialogState) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Champs obligatoires Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200), ), child: Row( children: [ 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), ), ], ), ), const SizedBox(height: 16), // Nom du produit TextField( controller: nameController, decoration: InputDecoration( labelText: 'Nom du produit *', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.shopping_bag), filled: true, fillColor: Colors.grey.shade50, ), onChanged: (value) { setDialogState(() { updateQrPreview(); }); }, ), 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), decoration: InputDecoration( labelText: 'Prix (FCFA) *', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.attach_money), filled: true, fillColor: Colors.grey.shade50, ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: stockController, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: 'Stock initial', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.inventory), filled: true, fillColor: Colors.grey.shade50, ), ), ), ], ), const SizedBox(height: 16), // Catégorie DropdownButtonFormField( value: selectedCategory, items: _predefinedCategories.map((category) => DropdownMenuItem(value: category, child: Text(category))).toList(), onChanged: (value) { setDialogState(() => selectedCategory = value!); }, decoration: InputDecoration( labelText: 'Catégorie', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.category), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 16), // Description TextField( controller: descriptionController, maxLines: 3, decoration: InputDecoration( labelText: 'Description', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.description), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 16), // Section Image Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.image, color: Colors.blue.shade700), const SizedBox(width: 8), Text( 'Image du produit (optionnel)', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.blue.shade700, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: imageController, decoration: const InputDecoration( labelText: 'Chemin de l\'image', border: OutlineInputBorder(), isDense: true, ), readOnly: true, ), ), 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) { setDialogState(() { pickedImage = File(result.files.single.path!); imageController.text = pickedImage!.path; }); } }, icon: const Icon(Icons.folder_open, size: 16), label: const Text('Choisir'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.all(12), ), ), ], ), const SizedBox(height: 12), // Aperçu de l'image if (pickedImage != null) Center( child: Container( height: 100, width: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file(pickedImage!, fit: BoxFit.cover), ), ), ), ], ), ), const SizedBox(height: 16), // Aperçu QR Code if (qrPreviewData != null) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade200), ), child: Column( children: [ Row( children: [ Icon(Icons.qr_code_2, color: Colors.green.shade700), const SizedBox(width: 8), Text( 'Aperçu du QR Code', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.green.shade700, ), ), ], ), const SizedBox(height: 12), Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: QrImageView( data: qrPreviewData!, version: QrVersions.auto, size: 80, backgroundColor: Colors.white, ), ), ), const SizedBox(height: 8), Text( 'Réf: $currentReference', style: const TextStyle(fontSize: 10, color: Colors.grey), ), ], ), ), ], ); }, ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton.icon( onPressed: () 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); while (existingProduct != null) { finalReference = _generateUniqueReference(); existingProduct = await _productDatabase.getProductByReference(finalReference); } // Générer le QR code final qrPath = await _generateAndSaveQRCode(finalReference); final product = Product( name: name, price: price, image: imageController.text, category: selectedCategory, description: descriptionController.text.trim(), stock: stock, qrCode: qrPath, reference: finalReference, ); await _productDatabase.createProduct(product); Get.back(); Get.snackbar( 'Succès', 'Produit ajouté avec succès!\nRéférence: $finalReference', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 4), icon: const Icon(Icons.check_circle, color: Colors.white), ); _loadProducts(); } catch (e) { Get.snackbar('Erreur', 'Ajout du produit échoué: $e'); } }, icon: const Icon(Icons.save), label: const Text('Ajouter le produit'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), ), ], ), ); } void _showQRCode(Product product) { final qrUrl = 'https://stock.guycom.mg/${product.reference}'; Get.dialog( AlertDialog( title: Row( children: [ const Icon(Icons.qr_code_2, color: Colors.blue), const SizedBox(width: 8), Expanded( child: Text( 'QR Code - ${product.name}', style: const TextStyle(fontSize: 18), ), ), ], ), content: Container( width: 300, child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: QrImageView( data: qrUrl, version: QrVersions.auto, size: 200, backgroundColor: Colors.white, ), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Text( 'Référence: ${product.reference}', style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( qrUrl, style: const TextStyle(fontSize: 12, color: Colors.grey), textAlign: TextAlign.center, ), ], ), ), ], ), ), actions: [ TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: qrUrl)); Get.back(); Get.snackbar( 'Copié', 'URL copiée dans le presse-papiers', backgroundColor: Colors.green, colorText: Colors.white, ); }, child: const Text('Copier URL'), ), TextButton( onPressed: () => Get.back(), child: const Text('Fermer'), ), ], ), ); } 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 imageController = TextEditingController(text: product.image); String selectedCategory = product.category; File? pickedImage; Get.dialog( AlertDialog( title: const Text('Modifier le produit'), content: Container( width: 500, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: nameController, decoration: const InputDecoration( labelText: 'Nom du produit*', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: priceController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Prix*', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: stockController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Stock', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), StatefulBuilder( builder: (context, setDialogState) { return Column( children: [ Row( children: [ Expanded( child: TextField( controller: imageController, decoration: const InputDecoration( labelText: 'Image', border: OutlineInputBorder(), ), readOnly: true, ), ), const SizedBox(width: 8), ElevatedButton( onPressed: () async { 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!); imageController.text = pickedImage!.path; }); } } }, child: const Text('Choisir'), ), ], ), const SizedBox(height: 16), if (pickedImage != null || product.image!.isNotEmpty) Container( height: 100, width: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: pickedImage != null ? Image.file(pickedImage!, fit: BoxFit.cover) : (product.image!.isNotEmpty ? 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(), onChanged: (value) { if (context.mounted) { setDialogState(() => selectedCategory = value!); } }, decoration: const InputDecoration( labelText: 'Catégorie', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: descriptionController, maxLines: 3, decoration: const InputDecoration( labelText: 'Description', border: OutlineInputBorder(), ), ), ], ); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton( onPressed: () 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, price: price, image: imageController.text, category: selectedCategory, description: descriptionController.text.trim(), stock: stock, qrCode: product.qrCode, reference: product.reference, ); try { await _productDatabase.updateProduct(updatedProduct); Get.back(); Get.snackbar( 'Succès', 'Produit modifié avec succès', backgroundColor: Colors.green, colorText: Colors.white, ); _loadProducts(); } catch (e) { Get.snackbar('Erreur', 'Modification échouée: $e'); } }, child: const Text('Sauvegarder'), ), ], ), ); } void _deleteProduct(Product product) { Get.dialog( AlertDialog( title: const Text('Confirmer la suppression'), content: Text('Êtes-vous sûr de vouloir supprimer "${product.name}" ?'), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () async { try { await _productDatabase.deleteProduct(product.id); Get.back(); Get.snackbar( 'Succès', 'Produit supprimé avec succès', backgroundColor: Colors.green, colorText: Colors.white, ); _loadProducts(); } catch (e) { Get.back(); Get.snackbar('Erreur', 'Suppression échouée: $e'); } }, child: const Text('Supprimer', style: TextStyle(color: Colors.white)), ), ], ), ); } Widget _buildProductCard(Product product) { return Card( margin: const EdgeInsets.all(8), elevation: 4, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ // Image du produit Container( width: 80, height: 80, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: product.image!.isNotEmpty ? Image.file( File(product.image!), fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => const Icon(Icons.image, size: 40), ) : const Icon(Icons.image, size: 40), ), ), const SizedBox(width: 16), // Informations du produit Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '${NumberFormat('#,##0').format(product.price)} FCFA', style: const TextStyle( fontSize: 16, color: Colors.green, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(12), ), child: Text( product.category, style: TextStyle( fontSize: 12, color: Colors.blue.shade800, ), ), ), const SizedBox(width: 8), Text( 'Stock: ${product.stock}', style: TextStyle( fontSize: 12, color: product.stock! > 0 ? Colors.green : Colors.red, fontWeight: FontWeight.w500, ), ), ], ), if (product.description!.isNotEmpty) ...[ const SizedBox(height: 4), Text( product.description!, style: const TextStyle(fontSize: 12, color: Colors.grey), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], const SizedBox(height: 4), Text( 'Réf: ${product.reference}', style: const TextStyle(fontSize: 10, color: Colors.grey), ), ], ), ), // Actions Column( children: [ IconButton( onPressed: () => _showQRCode(product), icon: const Icon(Icons.qr_code_2, color: Colors.blue), tooltip: 'Voir QR Code', ), IconButton( onPressed: () => _editProduct(product), icon: const Icon(Icons.edit, color: Colors.orange), tooltip: 'Modifier', ), IconButton( onPressed: () => _deleteProduct(product), icon: const Icon(Icons.delete, color: Colors.red), tooltip: 'Supprimer', ), ], ), ], ), ), ); } @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'), ), ], ), 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(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 ? const Center(child: CircularProgressIndicator()) : _filteredProducts.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.inventory_2_outlined, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( _products.isEmpty ? 'Aucun produit enregistré' : 'Aucun produit trouvé pour cette recherche', style: TextStyle( fontSize: 18, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Text( _products.isEmpty ? 'Commencez par ajouter votre premier produit' : 'Essayez de modifier vos critères de recherche', style: TextStyle( fontSize: 14, color: Colors.grey.shade500, ), ), if (_products.isEmpty) ...[ const SizedBox(height: 24), ElevatedButton.icon( onPressed: _showAddProductDialog, icon: const Icon(Icons.add), label: const Text('Ajouter un produit'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12, ), ), ), ], ], ), ) : RefreshIndicator( onRefresh: _loadProducts, child: ListView.builder( itemCount: _filteredProducts.length, itemBuilder: (context, index) { final product = _filteredProducts[index]; return _buildProductCard(product); }, ), ), ), ], ), ); } }