import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:excel/excel.dart' hide Border; import 'package:flutter/services.dart'; import '../Components/appDrawer.dart'; import '../Components/app_bar.dart'; import '../Models/produit.dart'; import '../Services/productDatabase.dart'; class AddProductPage extends StatefulWidget { const AddProductPage({super.key}); @override _AddProductPageState createState() => _AddProductPageState(); } class _AddProductPageState extends State { final TextEditingController _nameController = TextEditingController(); final TextEditingController _priceController = TextEditingController(); final TextEditingController _imageController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); final TextEditingController _stockController = TextEditingController(); final List _categories = ['Sucré', 'Salé', 'Jus', 'Gateaux', 'Non catégorisé']; String? _selectedCategory; File? _pickedImage; String? _qrData; String? _currentReference; // Ajout pour stocker la référence actuelle late ProductDatabase _productDatabase; // Variables pour la barre de progression bool _isImporting = false; double _importProgress = 0.0; String _importStatusText = ''; @override void initState() { super.initState(); _productDatabase = ProductDatabase.instance; _productDatabase.initDatabase(); _nameController.addListener(_updateQrData); } @override void dispose() { _nameController.removeListener(_updateQrData); _nameController.dispose(); _priceController.dispose(); _imageController.dispose(); _descriptionController.dispose(); _stockController.dispose(); super.dispose(); } // 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}'; } void _updateQrData() { if (_nameController.text.isNotEmpty) { // Générer une nouvelle référence si elle n'existe pas encore if (_currentReference == null) { _currentReference = _generateUniqueReference(); } setState(() { // Utiliser la référence courante dans l'URL du QR code _qrData = 'https://stock.guycom.mg/$_currentReference'; }); } else { setState(() { _currentReference = null; _qrData = null; }); } } Future _selectImage() async { final result = await FilePicker.platform.pickFiles(type: FileType.image); if (result != null && result.files.single.path != null) { setState(() { _pickedImage = File(result.files.single.path!); _imageController.text = _pickedImage!.path; }); } } // Assurez-vous aussi que _generateAndSaveQRCode utilise bien la référence passée : Future _generateAndSaveQRCode(String reference) async { final qrUrl = 'https://stock.guycom.mg/$reference'; // Utilise le paramètre 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'; // Utilise le paramètre reference 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 _addProduct() async { final name = _nameController.text.trim(); final price = double.tryParse(_priceController.text.trim()) ?? 0.0; final image = _imageController.text.trim(); final category = _selectedCategory ?? 'Non catégorisé'; final description = _descriptionController.text.trim(); final stock = int.tryParse(_stockController.text.trim()) ?? 0; if (name.isEmpty || price <= 0) { Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); return; } // Utiliser la référence générée ou en créer une nouvelle String finalReference = _currentReference ?? _generateUniqueReference(); // Vérifier l'unicité de la référence en base var existingProduct = await _productDatabase.getProductByReference(finalReference); // Si la référence existe déjà, en générer une nouvelle while (existingProduct != null) { finalReference = _generateUniqueReference(); existingProduct = await _productDatabase.getProductByReference(finalReference); } // Mettre à jour la référence courante avec la référence finale _currentReference = finalReference; // Générer le QR code avec la référence finale final qrPath = await _generateAndSaveQRCode(finalReference); final product = Product( name: name, price: price, image: image, category: category, description: description, qrCode: qrPath, reference: finalReference, // Utiliser la référence finale stock: stock, ); try { await _productDatabase.createProduct(product); Get.snackbar('Succès', 'Produit ajouté avec succès\nRéférence: $finalReference'); setState(() { _nameController.clear(); _priceController.clear(); _imageController.clear(); _descriptionController.clear(); _stockController.clear(); _selectedCategory = null; _pickedImage = null; _qrData = null; _currentReference = null; // Reset de la référence }); } catch (e) { Get.snackbar('Erreur', 'Ajout du produit échoué : $e'); print(e); } } // Méthode pour réinitialiser l'état d'importation void _resetImportState() { setState(() { _isImporting = false; _importProgress = 0.0; _importStatusText = ''; }); } 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; } // Démarrer la progression setState(() { _isImporting = true; _importProgress = 0.0; _importStatusText = 'Lecture du fichier...'; }); final file = File(result.files.single.path!); // Vérifier que le fichier existe 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(); // Vérifier que le fichier n'est pas vide 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 { // Initialisation setState(() { _isImporting = true; _importProgress = 0.0; _importStatusText = 'Initialisation...'; }); // Petit délai pour permettre au build de s'exécuter 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 = []; // Prendre la première feuille disponible 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; // -1 pour exclure l'en-tête setState(() { _importStatusText = 'Importation en cours... (0/$totalRows)'; }); // Ignorer la première ligne (en-têtes) et traiter les données for (var i = 1; i < sheet.rows.length; i++) { try { // Mettre à jour la progression final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); setState(() { _importProgress = currentProgress; _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; }); // Petite pause pour permettre à l'UI de se mettre à jour await Future.delayed(const Duration(milliseconds: 10)); final row = sheet.rows[i]; // Vérifier que la ligne a au moins les colonnes obligatoires (nom et prix) if (row.isEmpty || row.length < 2) { errorCount++; errorMessages.add('Ligne ${i + 1}: Données insuffisantes'); continue; } // Extraire les valeurs avec vérifications sécurisées final nameCell = row[0]; final priceCell = row[1]; // Extraction sécurisée des valeurs String? nameValue; String? priceValue; if (nameCell?.value != null) { nameValue = nameCell!.value.toString().trim(); } if (priceCell?.value != null) { priceValue = priceCell!.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; // Remplacer les virgules par des points pour les décimaux final price = double.tryParse(priceValue.replaceAll(',', '.')); if (price == null || price <= 0) { errorCount++; errorMessages.add('Ligne ${i + 1}: Prix invalide ($priceValue)'); continue; } // Extraire les autres colonnes optionnelles de manière sécurisée 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; } // Générer une référence unique et vérifier son unicité String reference = _generateUniqueReference(); // Vérifier l'unicité en base de données var existingProduct = await _productDatabase.getProductByReference(reference); while (existingProduct != null) { reference = _generateUniqueReference(); existingProduct = await _productDatabase.getProductByReference(reference); } // Créer le produit final product = Product( name: name, price: price, image: '', // Pas d'image lors de l'import category: category, description: description, stock: stock, qrCode: '', // Sera généré après reference: reference, ); // Générer et sauvegarder le QR code avec la nouvelle URL setState(() { _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; }); final qrPath = await _generateAndSaveQRCode(reference); product.qrCode = qrPath; // Sauvegarder en base de données await _productDatabase.createProduct(product); successCount++; } catch (e) { errorCount++; errorMessages.add('Ligne ${i + 1}: Erreur de traitement - $e'); debugPrint('Erreur ligne ${i + 1}: $e'); } } // Finalisation setState(() { _importProgress = 1.0; _importStatusText = 'Finalisation...'; }); await Future.delayed(const Duration(milliseconds: 500)); // Réinitialiser l'état d'importation _resetImportState(); // Afficher le résultat String message = '$successCount produits importés avec succès'; if (errorCount > 0) { message += ', $errorCount erreurs'; // Afficher les détails des erreurs si pas trop nombreuses 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, ); } catch (e) { _resetImportState(); Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); debugPrint('Erreur générale import Excel: $e'); } } 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éer un fichier Excel temporaire comme modèle final excel = Excel.createExcel(); // Supprimer la feuille par défaut et créer une nouvelle excel.delete('Sheet1'); excel.copy('Sheet1', 'Produits'); excel.delete('Sheet1'); final sheet = excel['Produits']; // Ajouter les en-têtes avec du style 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', ); } // Ajouter des exemples 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]; } } // Ajuster la largeur des colonnes sheet.setColWidth(0, 20); // Nom sheet.setColWidth(1, 10); // Prix sheet.setColWidth(2, 15); // Catégorie sheet.setColWidth(3, 30); // Description sheet.setColWidth(4, 10); // Stock // Sauvegarder en mémoire final bytes = excel.save(); if (bytes == null) { Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); return; } // Demander où sauvegarder final String? outputFile = await FilePicker.platform.saveFile( fileName: 'modele_import_produits.xlsx', allowedExtensions: ['xlsx'], type: FileType.custom, ); 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'); } } Widget _displayImage() { if (_pickedImage != null) { return ClipRRect( borderRadius: BorderRadius.circular(8.0), child: Image.file( _pickedImage!, width: 100, height: 100, fit: BoxFit.cover, ), ); } else { return Container( width: 100, height: 100, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(Icons.image, size: 32, color: Colors.grey), Text('Aucune image', style: TextStyle(color: Colors.grey)), ], ), decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(8.0), )); } } @override Widget build(BuildContext context) { return Scaffold( appBar: const CustomAppBar(title: 'Ajouter un produit'), drawer: CustomDrawer(), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Ajouter un produit', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), const SizedBox(height: 16), // Boutons d'importation 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 progression if (_isImporting) ...[ 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, ), ), ], ), ), const SizedBox(height: 16), ], const Divider(), const SizedBox(height: 16), // Formulaire d'ajout manuel TextField( controller: _nameController, enabled: !_isImporting, decoration: const InputDecoration( labelText: 'Nom du produit*', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _priceController, enabled: !_isImporting, keyboardType: TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Prix*', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _stockController, enabled: !_isImporting, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Stock', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), // Section image (optionnelle) Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextField( controller: _imageController, enabled: !_isImporting, decoration: const InputDecoration( labelText: 'Chemin de l\'image (optionnel)', border: OutlineInputBorder(), ), readOnly: true, ), ), const SizedBox(width: 8), ElevatedButton( onPressed: _isImporting ? null : _selectImage, child: const Text('Sélectionner'), ), ], ), const SizedBox(height: 16), _displayImage(), const SizedBox(height: 16), DropdownButtonFormField( value: _selectedCategory, items: _categories .map((c) => DropdownMenuItem(value: c, child: Text(c))) .toList(), onChanged: _isImporting ? null : (value) => setState(() => _selectedCategory = value), decoration: const InputDecoration( labelText: 'Catégorie', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _descriptionController, enabled: !_isImporting, maxLines: 3, decoration: const InputDecoration( labelText: 'Description (optionnel)', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), if (_qrData != null) ...[ const Text('Aperçu du QR Code :'), const SizedBox(height: 8), Center( child: QrImageView( data: _qrData!, version: QrVersions.auto, size: 120, ), ), const SizedBox(height: 8), Center( child: Text( _qrData!, style: const TextStyle(fontSize: 12, color: Colors.grey), textAlign: TextAlign.center, ), ), ], const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isImporting ? null : _addProduct, child: const Text('Ajouter le produit'), ), ), ], ), ), ); } }