diff --git a/lib/Views/HandleProduct.dart b/lib/Views/HandleProduct.dart index 8579e6a..7f814a0 100644 --- a/lib/Views/HandleProduct.dart +++ b/lib/Views/HandleProduct.dart @@ -27,7 +27,7 @@ class ProductManagementPage extends StatefulWidget { class _ProductManagementPageState extends State { final AppDatabase _productDatabase = AppDatabase.instance; final UserController _userController = Get.find(); - + List _products = []; List _filteredProducts = []; final TextEditingController _searchController = TextEditingController(); @@ -36,7 +36,7 @@ class _ProductManagementPageState extends State { bool _isLoading = true; List> _pointsDeVente = []; String? _selectedPointDeVente; - + // Variables pour le scanner QRViewController? _qrController; bool _isScanning = false; @@ -45,7 +45,13 @@ class _ProductManagementPageState extends State { // Catégories prédéfinies pour l'ajout de produits final List _predefinedCategories = [ - 'Smartphone', 'Tablette', 'Accessoires', 'Multimedia', 'Informatique', 'Laptop', 'Non catégorisé' + 'Smartphone', + 'Tablette', + 'Accessoires', + 'Multimedia', + 'Informatique', + 'Laptop', + 'Non catégorisé' ]; // Variables pour l'import Excel (conservées du code original) @@ -67,770 +73,808 @@ class _ProductManagementPageState extends State { _searchController.dispose(); super.dispose(); } -void _showAddProductDialog() { - final nameController = TextEditingController(); - final priceController = TextEditingController(); - final stockController = TextEditingController(); - final descriptionController = TextEditingController(); - final imageController = TextEditingController(); - final referenceController = TextEditingController(); - final marqueController = TextEditingController(); - final ramController = TextEditingController(); - final memoireInterneController = TextEditingController(); - final imeiController = TextEditingController(); - final newPointDeVenteController = TextEditingController(); - - String? selectedPointDeVente; - List> pointsDeVente = []; - bool isLoadingPoints = true; - String selectedCategory = _predefinedCategories.last; // 'Non catégorisé' par défaut - File? pickedImage; - String? qrPreviewData; - bool autoGenerateReference = true; - bool showAddNewPoint = false; - - // Fonction pour mettre à jour le QR preview - void updateQrPreview() { - if (nameController.text.isNotEmpty) { - final reference = autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim(); - if (reference.isNotEmpty) { - qrPreviewData = 'https://stock.guycom.mg/$reference'; + + void _showAddProductDialog() { + final nameController = TextEditingController(); + final priceController = TextEditingController(); + final stockController = TextEditingController(); + final descriptionController = TextEditingController(); + final imageController = TextEditingController(); + final referenceController = TextEditingController(); + final marqueController = TextEditingController(); + final ramController = TextEditingController(); + final memoireInterneController = TextEditingController(); + final imeiController = TextEditingController(); + final newPointDeVenteController = TextEditingController(); + + String? selectedPointDeVente; + List> pointsDeVente = []; + bool isLoadingPoints = true; + String selectedCategory = + _predefinedCategories.last; // 'Non catégorisé' par défaut + File? pickedImage; + String? qrPreviewData; + bool autoGenerateReference = true; + bool showAddNewPoint = false; + + // Fonction pour mettre à jour le QR preview + void updateQrPreview() { + if (nameController.text.isNotEmpty) { + final reference = autoGenerateReference + ? _generateUniqueReference() + : referenceController.text.trim(); + if (reference.isNotEmpty) { + qrPreviewData = 'https://stock.guycom.mg/$reference'; + } else { + qrPreviewData = null; + } } else { qrPreviewData = null; } - } else { - qrPreviewData = null; } - } -final AppDatabase _productDatabase = AppDatabase.instance; - Future loadPointsDeVente(StateSetter setDialogState) async { - try { - final result = await _productDatabase.getPointsDeVente(); - setDialogState(() { - // Ajouter l'option "Aucun" à la liste - pointsDeVente = [ - {'id': null, 'nom': 'Aucun'}, // Option pour pointDeVenteId null - ...result - ]; - isLoadingPoints = false; - if (selectedPointDeVente == null && result.isNotEmpty) { - selectedPointDeVente = 'Aucun'; // Par défaut, sélectionner "Aucun" - } - }); - } catch (e) { - setDialogState(() { - isLoadingPoints = false; - }); - Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); + + final AppDatabase _productDatabase = AppDatabase.instance; + Future loadPointsDeVente(StateSetter setDialogState) async { + try { + final result = await _productDatabase.getPointsDeVente(); + setDialogState(() { + // Ajouter l'option "Aucun" à la liste + pointsDeVente = [ + {'id': null, 'nom': 'Aucun'}, // Option pour pointDeVenteId null + ...result + ]; + isLoadingPoints = false; + if (selectedPointDeVente == null && result.isNotEmpty) { + selectedPointDeVente = 'Aucun'; // Par défaut, sélectionner "Aucun" + } + }); + } catch (e) { + setDialogState(() { + isLoadingPoints = false; + }); + Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); + } } - } - Get.dialog( - AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), + 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), ), - 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) { - // Charger les points de vente une seule fois - if (isLoadingPoints && pointsDeVente.isEmpty) { - WidgetsBinding.instance.addPostFrameCallback((_) { - loadPointsDeVente(setDialogState); - }); - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Champs obligatoires - + 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) { + // Charger les points de vente une seule fois + if (isLoadingPoints && pointsDeVente.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + loadPointsDeVente(setDialogState); + }); + } + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champs obligatoires - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.teal.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.teal.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.store, color: Colors.teal.shade700), - const SizedBox(width: 8), - Text( - 'Point de vente', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.teal.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - - if (isLoadingPoints) - const Center(child: CircularProgressIndicator()) - else - Column( + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.teal.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.teal.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - if (!showAddNewPoint) ...[ - DropdownButtonFormField( - value: selectedPointDeVente, - items: pointsDeVente.map((point) { - return DropdownMenuItem( - value: point['nom'] as String, - child: Text(point['nom'] as String), - ); - }).toList(), - onChanged: (value) { - setDialogState(() => selectedPointDeVente = value); - }, - decoration: const InputDecoration( - labelText: 'Sélectionner un point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.store), - filled: true, - fillColor: Colors.white, - ), + Icon(Icons.store, color: Colors.teal.shade700), + const SizedBox(width: 8), + Text( + 'Point de vente', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.teal.shade700, ), - const SizedBox(height: 8), - Row( - children: [ - TextButton.icon( - onPressed: () { - setDialogState(() { - showAddNewPoint = true; - newPointDeVenteController.clear(); - }); - }, - icon: const Icon(Icons.add, size: 16), - label: const Text('Ajouter nouveau point'), - style: TextButton.styleFrom( - foregroundColor: Colors.teal.shade700, - ), - ), - const Spacer(), - TextButton.icon( - onPressed: () => loadPointsDeVente(setDialogState), - icon: const Icon(Icons.refresh, size: 16), - label: const Text('Actualiser'), + ), + ], + ), + const SizedBox(height: 12), + if (isLoadingPoints) + const Center(child: CircularProgressIndicator()) + else + Column( + children: [ + if (!showAddNewPoint) ...[ + DropdownButtonFormField( + value: selectedPointDeVente, + items: pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + setDialogState( + () => selectedPointDeVente = value); + }, + decoration: const InputDecoration( + labelText: + 'Sélectionner un point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.store), + filled: true, + fillColor: Colors.white, ), - ], - ), - ], - - if (showAddNewPoint) ...[ - DropdownButtonFormField( - value: selectedPointDeVente, - items: pointsDeVente.map((point) { - return DropdownMenuItem( - value: point['nom'] as String, - child: Text(point['nom'] as String), - ); - }).toList(), - onChanged: (value) { - setDialogState(() => selectedPointDeVente = value); - }, - decoration: const InputDecoration( - labelText: 'Sélectionner un point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.store), - filled: true, - fillColor: Colors.white, ), - ), - const SizedBox(height: 8), - Row( - children: [ - TextButton.icon( - onPressed: () { - setDialogState(() { - showAddNewPoint = true; - newPointDeVenteController.clear(); - }); - }, - icon: const Icon(Icons.add, size: 16), - label: const Text('Ajouter nouveau point'), - style: TextButton.styleFrom( - foregroundColor: Colors.teal.shade700, + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: () { + setDialogState(() { + showAddNewPoint = true; + newPointDeVenteController.clear(); + }); + }, + icon: const Icon(Icons.add, size: 16), + label: + const Text('Ajouter nouveau point'), + style: TextButton.styleFrom( + foregroundColor: Colors.teal.shade700, + ), ), + const Spacer(), + TextButton.icon( + onPressed: () => + loadPointsDeVente(setDialogState), + icon: + const Icon(Icons.refresh, size: 16), + label: const Text('Actualiser'), + ), + ], + ), + ], + if (showAddNewPoint) ...[ + DropdownButtonFormField( + value: selectedPointDeVente, + items: pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + setDialogState( + () => selectedPointDeVente = value); + }, + decoration: const InputDecoration( + labelText: + 'Sélectionner un point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.store), + filled: true, + fillColor: Colors.white, ), - const Spacer(), - TextButton.icon( - onPressed: () => loadPointsDeVente(setDialogState), - icon: const Icon(Icons.refresh, size: 16), - label: const Text('Actualiser'), - ), - ], - ), - ], - - if (showAddNewPoint) ...[ - TextField( - controller: newPointDeVenteController, - decoration: const InputDecoration( - labelText: 'Nom du nouveau point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.add_business), - filled: true, - fillColor: Colors.white, ), - ), - const SizedBox(height: 8), - Row( - children: [ - TextButton( - onPressed: () { - setDialogState(() { - showAddNewPoint = false; - newPointDeVenteController.clear(); - }); - }, - child: const Text('Annuler'), + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: () { + setDialogState(() { + showAddNewPoint = true; + newPointDeVenteController.clear(); + }); + }, + icon: const Icon(Icons.add, size: 16), + label: + const Text('Ajouter nouveau point'), + style: TextButton.styleFrom( + foregroundColor: Colors.teal.shade700, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: () => + loadPointsDeVente(setDialogState), + icon: + const Icon(Icons.refresh, size: 16), + label: const Text('Actualiser'), + ), + ], + ), + ], + if (showAddNewPoint) ...[ + TextField( + controller: newPointDeVenteController, + decoration: const InputDecoration( + labelText: + 'Nom du nouveau point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.add_business), + filled: true, + fillColor: Colors.white, ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: () async { - final nom = newPointDeVenteController.text.trim(); - if (nom.isNotEmpty) { - try { - final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); - if (id != null) { - setDialogState(() { - showAddNewPoint = false; - selectedPointDeVente = nom; - newPointDeVenteController.clear(); - }); - // Recharger la liste - await loadPointsDeVente(setDialogState); - Get.snackbar( - 'Succès', - 'Point de vente "$nom" créé avec succès', - backgroundColor: Colors.green, - colorText: Colors.white, - ); + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton( + onPressed: () { + setDialogState(() { + showAddNewPoint = false; + newPointDeVenteController.clear(); + }); + }, + child: const Text('Annuler'), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () async { + final nom = newPointDeVenteController + .text + .trim(); + if (nom.isNotEmpty) { + try { + final id = await _productDatabase + .getOrCreatePointDeVenteByNom( + nom); + if (id != null) { + setDialogState(() { + showAddNewPoint = false; + selectedPointDeVente = nom; + newPointDeVenteController + .clear(); + }); + // Recharger la liste + await loadPointsDeVente( + setDialogState); + Get.snackbar( + 'Succès', + 'Point de vente "$nom" créé avec succès', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } + } catch (e) { + Get.snackbar('Erreur', + 'Impossible de créer le point de vente: $e'); } - } catch (e) { - Get.snackbar('Erreur', 'Impossible de créer le point de vente: $e'); } - } - }, - icon: const Icon(Icons.save, size: 16), - label: const Text('Créer'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.teal, - foregroundColor: Colors.white, + }, + icon: const Icon(Icons.save, size: 16), + label: const Text('Créer'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, + foregroundColor: Colors.white, + ), ), - ), - ], - ), + ], + ), + ], ], - ], - ), - ], + ), + ], + ), ), - ), - 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, + 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(); + }); + }, ), - 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 (MGA) *', - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.attach_money), - filled: true, - fillColor: Colors.grey.shade50, + 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 (MGA) *', + 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(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), + + // 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), - - // Section Référence - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.purple.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.purple.shade200), + 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, + ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.confirmation_number, color: Colors.purple.shade700), - const SizedBox(width: 8), - Text( - 'Référence du produit', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.purple.shade700, + const SizedBox(height: 16), + + // Section Référence + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.purple.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.purple.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.confirmation_number, + color: Colors.purple.shade700), + const SizedBox(width: 8), + Text( + 'Référence du produit', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.purple.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Checkbox( + value: autoGenerateReference, + onChanged: (value) { + setDialogState(() { + autoGenerateReference = value!; + updateQrPreview(); + }); + }, + ), + const Text('Générer automatiquement'), + ], + ), + const SizedBox(height: 8), + if (!autoGenerateReference) + TextField( + controller: referenceController, + decoration: const InputDecoration( + labelText: 'Référence *', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.tag), + filled: true, + fillColor: Colors.white, ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Checkbox( - value: autoGenerateReference, onChanged: (value) { setDialogState(() { - autoGenerateReference = value!; updateQrPreview(); }); }, ), - const Text('Générer automatiquement'), - ], - ), - const SizedBox(height: 8), - if (!autoGenerateReference) + if (autoGenerateReference) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Référence générée automatiquement', + style: TextStyle(color: Colors.grey.shade700), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Nouveaux champs (Marque, RAM, Mémoire interne, IMEI) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.memory, color: Colors.orange.shade700), + const SizedBox(width: 8), + Text( + 'Spécifications techniques', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), TextField( - controller: referenceController, + controller: marqueController, decoration: const InputDecoration( - labelText: 'Référence *', + labelText: 'Marque', border: OutlineInputBorder(), - prefixIcon: Icon(Icons.tag), + prefixIcon: Icon(Icons.branding_watermark), filled: true, fillColor: Colors.white, ), - onChanged: (value) { - setDialogState(() { - updateQrPreview(); - }); - }, ), - if (autoGenerateReference) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - 'Référence générée automatiquement', - style: TextStyle(color: Colors.grey.shade700), - ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: ramController, + decoration: const InputDecoration( + labelText: 'RAM', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.memory), + filled: true, + fillColor: Colors.white, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: memoireInterneController, + decoration: const InputDecoration( + labelText: 'Mémoire interne', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.storage), + filled: true, + fillColor: Colors.white, + ), + ), + ), + ], ), - ], - ), - ), - const SizedBox(height: 16), - - // Nouveaux champs (Marque, RAM, Mémoire interne, IMEI) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.orange.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.orange.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.memory, color: Colors.orange.shade700), - const SizedBox(width: 8), - Text( - 'Spécifications techniques', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.orange.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - TextField( - controller: marqueController, - decoration: const InputDecoration( - labelText: 'Marque', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.branding_watermark), - filled: true, - fillColor: Colors.white, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: TextField( - controller: ramController, - decoration: const InputDecoration( - labelText: 'RAM', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.memory), - filled: true, - fillColor: Colors.white, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: memoireInterneController, - decoration: const InputDecoration( - labelText: 'Mémoire interne', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.storage), - filled: true, - fillColor: Colors.white, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - TextField( - controller: imeiController, - decoration: const InputDecoration( - labelText: 'IMEI (pour téléphones)', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.smartphone), - filled: true, - fillColor: Colors.white, - ), - ), - ], - ), - ), - 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: 8), + TextField( + controller: imeiController, + decoration: const InputDecoration( + labelText: 'IMEI (pour téléphones)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.smartphone), + filled: true, + fillColor: Colors.white, ), ), - ], + ], + ), ), - ), - const SizedBox(height: 16), - - // Aperçu QR Code - if (qrPreviewData != null) + const SizedBox(height: 16), + + // Section Image Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.green.shade50, + color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.green.shade200), + border: Border.all(color: Colors.blue.shade200), ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(Icons.qr_code_2, color: Colors.green.shade700), + Icon(Icons.image, color: Colors.blue.shade700), const SizedBox(width: 8), Text( - 'Aperçu du QR Code', + 'Image du produit (optionnel)', style: TextStyle( fontWeight: FontWeight.w600, - color: Colors.green.shade700, + color: Colors.blue.shade700, ), ), ], ), const SizedBox(height: 12), - Center( - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), + Row( + children: [ + Expanded( + child: TextField( + controller: imageController, + decoration: const InputDecoration( + labelText: 'Chemin de l\'image', + border: OutlineInputBorder(), + isDense: true, + ), + readOnly: true, + ), ), - child: QrImageView( - data: qrPreviewData!, - version: QrVersions.auto, - size: 80, - backgroundColor: Colors.white, + 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: 8), - Text( - 'Réf: ${autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim()}', - style: const TextStyle(fontSize: 10, color: Colors.grey), + ], ), + 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: ${autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim()}', + 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; - } - - // Vérification de la référence - String finalReference; - if (autoGenerateReference) { - finalReference = _generateUniqueReference(); - } else { - finalReference = referenceController.text.trim(); - if (finalReference.isEmpty) { - Get.snackbar('Erreur', 'La référence est obligatoire'); + 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; } - - final existingProduct = await _productDatabase.getProductByReference(finalReference); - if (existingProduct != null) { - Get.snackbar('Erreur', 'Cette référence existe déjà'); - return; + + // Vérification de la référence + String finalReference; + if (autoGenerateReference) { + finalReference = _generateUniqueReference(); + } else { + finalReference = referenceController.text.trim(); + if (finalReference.isEmpty) { + Get.snackbar('Erreur', 'La référence est obligatoire'); + return; + } + + final existingProduct = await _productDatabase + .getProductByReference(finalReference); + if (existingProduct != null) { + Get.snackbar('Erreur', 'Cette référence existe déjà'); + return; + } } - } - // Gérer le point de vente - int? pointDeVenteId; - String? finalPointDeVenteNom; - - if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { - finalPointDeVenteNom = newPointDeVenteController.text.trim(); - } else if (selectedPointDeVente != null && selectedPointDeVente != 'Aucun') { - finalPointDeVenteNom = selectedPointDeVente; - } - - if (finalPointDeVenteNom != null) { - pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(finalPointDeVenteNom); - } - // Si "Aucun" est sélectionné, pointDeVenteId reste null - - try { - 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, - marque: marqueController.text.trim(), - ram: ramController.text.trim(), - memoireInterne: memoireInterneController.text.trim(), - imei: imeiController.text.trim(), - pointDeVenteId: pointDeVenteId, // Peut être null si "Aucun" - ); - - await _productDatabase.createProduct(product); - Get.back(); - Get.snackbar( - 'Succès', - 'Produit ajouté avec succès!\nRéférence: $finalReference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 4), - icon: const Icon(Icons.check_circle, color: Colors.white), - ); - _loadProducts(); - _loadPointsDeVente(); - } 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), + // Gérer le point de vente + int? pointDeVenteId; + String? finalPointDeVenteNom; + + if (showAddNewPoint && + newPointDeVenteController.text.trim().isNotEmpty) { + finalPointDeVenteNom = newPointDeVenteController.text.trim(); + } else if (selectedPointDeVente != null && + selectedPointDeVente != 'Aucun') { + finalPointDeVenteNom = selectedPointDeVente; + } + + if (finalPointDeVenteNom != null) { + pointDeVenteId = await _productDatabase + .getOrCreatePointDeVenteByNom(finalPointDeVenteNom); + } + // Si "Aucun" est sélectionné, pointDeVenteId reste null + + try { + 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, + marque: marqueController.text.trim(), + ram: ramController.text.trim(), + memoireInterne: memoireInterneController.text.trim(), + imei: imeiController.text.trim(), + pointDeVenteId: pointDeVenteId, // Peut être null si "Aucun" + ); + + await _productDatabase.createProduct(product); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit ajouté avec succès!\nRéférence: $finalReference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 4), + icon: const Icon(Icons.check_circle, color: Colors.white), + ); + _loadProducts(); + _loadPointsDeVente(); + } 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), + ), ), - ), - ], - ), - ); -} + ], + ), + ); + } + // === FONCTIONS DE SCAN POUR ATTRIBUTION POINT DE VENTE === -String? _getColumnValue(List row, Map mapping, String field) { - if (!mapping.containsKey(field)) return null; - - int columnIndex = mapping[field]!; - if (columnIndex >= row.length || row[columnIndex]?.value == null) return null; - - return row[columnIndex]!.value.toString().trim(); -} + String? _getColumnValue( + List row, Map mapping, String field) { + if (!mapping.containsKey(field)) return null; + + int columnIndex = mapping[field]!; + if (columnIndex >= row.length || row[columnIndex]?.value == null) + return null; + + return row[columnIndex]!.value.toString().trim(); + } + void _startPointDeVenteAssignmentScanning() { if (_isScanning) return; - + // Vérifier que l'utilisateur a un point de vente if (_userController.pointDeVenteId <= 0) { Get.snackbar( @@ -844,7 +888,7 @@ String? _getColumnValue(List row, Map mapping, String field) ); return; } - + setState(() { _isScanning = true; }); @@ -855,189 +899,199 @@ String? _getColumnValue(List row, Map mapping, String field) }); }); } -Map _normalizeRowData(List row, Map mapping, int rowIndex) { - final normalizedData = {}; - - // Fonction interne pour nettoyer et normaliser les valeurs - String? _cleanValue(String? value) { - if (value == null) return null; - return value.toString().trim(); - } - - // Fonction simple pour les nombres (maintenant ils sont corrects) - double? _normalizeNumber(String? value) { - if (value == null || value.isEmpty) return null; - - // Remplacer les virgules par des points et supprimer les espaces - final cleaned = value.replaceAll(',', '.').replaceAll(RegExp(r'\s+'), ''); - - // Supprimer les caractères non numériques sauf le point - final numericString = cleaned.replaceAll(RegExp(r'[^0-9.]'), ''); - - return double.tryParse(numericString); - } - - // Normalisation du nom - if (mapping.containsKey('name')) { - final name = _cleanValue(_getColumnValue(row, mapping, 'name')); - if (name != null && name.isNotEmpty) { - normalizedData['name'] = name; + + Map _normalizeRowData( + List row, Map mapping, int rowIndex) { + final normalizedData = {}; + + // Fonction interne pour nettoyer et normaliser les valeurs + String? _cleanValue(String? value) { + if (value == null) return null; + return value.toString().trim(); } - } - - // Normalisation du prix (maintenant simple car corrigé en amont) - if (mapping.containsKey('price')) { - final priceValue = _cleanValue(_getColumnValue(row, mapping, 'price')); - final price = _normalizeNumber(priceValue); - if (price != null && price > 0) { - normalizedData['price'] = price; - print('✅ Prix normalisé: $price'); + + // Fonction simple pour les nombres (maintenant ils sont corrects) + double? _normalizeNumber(String? value) { + if (value == null || value.isEmpty) return null; + + // Remplacer les virgules par des points et supprimer les espaces + final cleaned = value.replaceAll(',', '.').replaceAll(RegExp(r'\s+'), ''); + + // Supprimer les caractères non numériques sauf le point + final numericString = cleaned.replaceAll(RegExp(r'[^0-9.]'), ''); + + return double.tryParse(numericString); } - } - - // Normalisation de la référence - if (mapping.containsKey('reference')) { - final reference = _cleanValue(_getColumnValue(row, mapping, 'reference')); - if (reference != null && reference.isNotEmpty) { - normalizedData['reference'] = reference; + + // Normalisation du nom + if (mapping.containsKey('name')) { + final name = _cleanValue(_getColumnValue(row, mapping, 'name')); + if (name != null && name.isNotEmpty) { + normalizedData['name'] = name; + } + } + + // Normalisation du prix (maintenant simple car corrigé en amont) + if (mapping.containsKey('price')) { + final priceValue = _cleanValue(_getColumnValue(row, mapping, 'price')); + final price = _normalizeNumber(priceValue); + if (price != null && price > 0) { + normalizedData['price'] = price; + print('✅ Prix normalisé: $price'); + } + } + + // Normalisation de la référence + if (mapping.containsKey('reference')) { + final reference = _cleanValue(_getColumnValue(row, mapping, 'reference')); + if (reference != null && reference.isNotEmpty) { + normalizedData['reference'] = reference; + } else { + // Génération automatique si non fournie + normalizedData['reference'] = _generateUniqueReference(); + } + } + + // Normalisation de la catégorie + if (mapping.containsKey('category')) { + final category = _cleanValue(_getColumnValue(row, mapping, 'category')); + normalizedData['category'] = category ?? 'Non catégorisé'; } else { - // Génération automatique si non fournie - normalizedData['reference'] = _generateUniqueReference(); + normalizedData['category'] = 'Non catégorisé'; } - } - - // Normalisation de la catégorie - if (mapping.containsKey('category')) { - final category = _cleanValue(_getColumnValue(row, mapping, 'category')); - normalizedData['category'] = category ?? 'Non catégorisé'; - } else { - normalizedData['category'] = 'Non catégorisé'; - } - - // Normalisation de la marque - if (mapping.containsKey('marque')) { - final marque = _cleanValue(_getColumnValue(row, mapping, 'marque')); - if (marque != null && marque.isNotEmpty) { - normalizedData['marque'] = marque; + + // Normalisation de la marque + if (mapping.containsKey('marque')) { + final marque = _cleanValue(_getColumnValue(row, mapping, 'marque')); + if (marque != null && marque.isNotEmpty) { + normalizedData['marque'] = marque; + } } - } - - // Normalisation de la RAM - if (mapping.containsKey('ram')) { - final ram = _cleanValue(_getColumnValue(row, mapping, 'ram')); - if (ram != null && ram.isNotEmpty) { - // Standardisation du format (ex: "8 Go", "16GB" -> "8 Go", "16 Go") - final ramValue = ram.replaceAll('GB', 'Go').replaceAll('go', 'Go'); - normalizedData['ram'] = ramValue; + + // Normalisation de la RAM + if (mapping.containsKey('ram')) { + final ram = _cleanValue(_getColumnValue(row, mapping, 'ram')); + if (ram != null && ram.isNotEmpty) { + // Standardisation du format (ex: "8 Go", "16GB" -> "8 Go", "16 Go") + final ramValue = ram.replaceAll('GB', 'Go').replaceAll('go', 'Go'); + normalizedData['ram'] = ramValue; + } } - } - - // Normalisation de la mémoire interne - if (mapping.containsKey('memoire_interne')) { - final memoire = _cleanValue(_getColumnValue(row, mapping, 'memoire_interne')); - if (memoire != null && memoire.isNotEmpty) { - // Standardisation du format (ex: "256GB" -> "256 Go") - final memoireValue = memoire.replaceAll('GB', 'Go').replaceAll('go', 'Go'); - normalizedData['memoire_interne'] = memoireValue; + + // Normalisation de la mémoire interne + if (mapping.containsKey('memoire_interne')) { + final memoire = + _cleanValue(_getColumnValue(row, mapping, 'memoire_interne')); + if (memoire != null && memoire.isNotEmpty) { + // Standardisation du format (ex: "256GB" -> "256 Go") + final memoireValue = + memoire.replaceAll('GB', 'Go').replaceAll('go', 'Go'); + normalizedData['memoire_interne'] = memoireValue; + } } - } - - // Normalisation de l'IMEI - if (mapping.containsKey('imei')) { - final imei = _cleanValue(_getColumnValue(row, mapping, 'imei')); - if (imei != null && imei.isNotEmpty) { - // Suppression des espaces et tirets dans l'IMEI - final imeiValue = imei.replaceAll(RegExp(r'[\s-]'), ''); - if (imeiValue.length >= 15) { - normalizedData['imei'] = imeiValue.substring(0, 15); - } else { - normalizedData['imei'] = imeiValue; + + // Normalisation de l'IMEI + if (mapping.containsKey('imei')) { + final imei = _cleanValue(_getColumnValue(row, mapping, 'imei')); + if (imei != null && imei.isNotEmpty) { + // Suppression des espaces et tirets dans l'IMEI + final imeiValue = imei.replaceAll(RegExp(r'[\s-]'), ''); + if (imeiValue.length >= 15) { + normalizedData['imei'] = imeiValue.substring(0, 15); + } else { + normalizedData['imei'] = imeiValue; + } } } - } - - // Normalisation du point de vente - if (mapping.containsKey('point_de_vente')) { - final pv = _cleanValue(_getColumnValue(row, mapping, 'point_de_vente')); - if (pv != null && pv.isNotEmpty) { - // Suppression des espaces superflus - normalizedData['point_de_vente'] = pv.replaceAll(RegExp(r'\s+'), ' ').trim(); + + // Normalisation du point de vente + if (mapping.containsKey('point_de_vente')) { + final pv = _cleanValue(_getColumnValue(row, mapping, 'point_de_vente')); + if (pv != null && pv.isNotEmpty) { + // Suppression des espaces superflus + normalizedData['point_de_vente'] = + pv.replaceAll(RegExp(r'\s+'), ' ').trim(); + } } + + // Valeurs par défaut + normalizedData['description'] = ''; // Description toujours vide + normalizedData['stock'] = 1; // Stock toujours à 1 + + // Validation des données obligatoires + if (normalizedData['name'] == null || normalizedData['price'] == null) { + throw Exception( + 'Ligne ${rowIndex + 1}: Données obligatoires manquantes (nom ou prix)'); + } + + return normalizedData; } - - // Valeurs par défaut - normalizedData['description'] = ''; // Description toujours vide - normalizedData['stock'] = 1; // Stock toujours à 1 - - // Validation des données obligatoires - if (normalizedData['name'] == null || normalizedData['price'] == null) { - throw Exception('Ligne ${rowIndex + 1}: Données obligatoires manquantes (nom ou prix)'); - } - - return normalizedData; -} + // Méthode pour mapper les en-têtes aux colonnes (CORRIGÉE) -Map _mapHeaders(List headerRow) { - Map columnMapping = {}; - - for (int i = 0; i < headerRow.length; i++) { - if (headerRow[i]?.value == null) continue; - - String header = headerRow[i]!.value.toString().trim().toUpperCase(); - - // Debug : afficher chaque en-tête trouvé - print('En-tête trouvé: "$header" à la colonne $i'); - - // Mapping amélioré pour gérer les variations - if ((header.contains('NOM') && (header.contains('PRODUIT') || header.contains('DU'))) || - header == 'NOM DU PRODUITS' || header == 'NOM') { - columnMapping['name'] = i; - print('→ Mappé vers name'); - } - else if ((header.contains('REFERENCE') && (header.contains('PRODUIT') || header.contains('PRODUITS'))) || - header == 'REFERENCE PRODUITS' || header == 'REFERENCE') { - columnMapping['reference'] = i; - print('→ Mappé vers reference'); - } - else if ((header.contains('CATEGORIES') && (header.contains('PRODUIT') || header.contains('PRODUITS'))) || - header == 'CATEGORIES PRODUITS' || header == 'CATEGORIE' || header == 'CATEGORY') { - columnMapping['category'] = i; - print('→ Mappé vers category'); - } - else if (header == 'MARQUE' || header == 'BRAND') { - columnMapping['marque'] = i; - print('→ Mappé vers marque'); - } - else if (header == 'RAM' || header.contains('MEMOIRE RAM')) { - columnMapping['ram'] = i; - print('→ Mappé vers ram'); - } - else if (header == 'INTERNE' || header.contains('MEMOIRE INTERNE') || header.contains('STOCKAGE')) { - columnMapping['memoire_interne'] = i; - print('→ Mappé vers memoire_interne'); - } - else if (header == 'IMEI' || header.contains('NUMERO IMEI')) { - columnMapping['imei'] = i; - print('→ Mappé vers imei'); - } - else if (header == 'PRIX' || header == 'PRICE') { - columnMapping['price'] = i; - print('→ Mappé vers price'); - } - else if (header == 'BOUTIQUE' || header.contains('POINT DE VENTE') || header == 'MAGASIN') { - columnMapping['point_de_vente'] = i; - print('→ Mappé vers point_de_vente'); - } - else { - print('→ Non reconnu'); + Map _mapHeaders(List headerRow) { + Map columnMapping = {}; + + for (int i = 0; i < headerRow.length; i++) { + if (headerRow[i]?.value == null) continue; + + String header = headerRow[i]!.value.toString().trim().toUpperCase(); + + // Debug : afficher chaque en-tête trouvé + print('En-tête trouvé: "$header" à la colonne $i'); + + // Mapping amélioré pour gérer les variations + if ((header.contains('NOM') && + (header.contains('PRODUIT') || header.contains('DU'))) || + header == 'NOM DU PRODUITS' || + header == 'NOM') { + columnMapping['name'] = i; + print('→ Mappé vers name'); + } else if ((header.contains('REFERENCE') && + (header.contains('PRODUIT') || header.contains('PRODUITS'))) || + header == 'REFERENCE PRODUITS' || + header == 'REFERENCE') { + columnMapping['reference'] = i; + print('→ Mappé vers reference'); + } else if ((header.contains('CATEGORIES') && + (header.contains('PRODUIT') || header.contains('PRODUITS'))) || + header == 'CATEGORIES PRODUITS' || + header == 'CATEGORIE' || + header == 'CATEGORY') { + columnMapping['category'] = i; + print('→ Mappé vers category'); + } else if (header == 'MARQUE' || header == 'BRAND') { + columnMapping['marque'] = i; + print('→ Mappé vers marque'); + } else if (header == 'RAM' || header.contains('MEMOIRE RAM')) { + columnMapping['ram'] = i; + print('→ Mappé vers ram'); + } else if (header == 'INTERNE' || + header.contains('MEMOIRE INTERNE') || + header.contains('STOCKAGE')) { + columnMapping['memoire_interne'] = i; + print('→ Mappé vers memoire_interne'); + } else if (header == 'IMEI' || header.contains('NUMERO IMEI')) { + columnMapping['imei'] = i; + print('→ Mappé vers imei'); + } else if (header == 'PRIX' || header == 'PRICE') { + columnMapping['price'] = i; + print('→ Mappé vers price'); + } else if (header == 'BOUTIQUE' || + header.contains('POINT DE VENTE') || + header == 'MAGASIN') { + columnMapping['point_de_vente'] = i; + print('→ Mappé vers point_de_vente'); + } else { + print('→ Non reconnu'); + } } + + // Debug : afficher le mapping final + print('Mapping final: $columnMapping'); + + return columnMapping; } - - // Debug : afficher le mapping final - print('Mapping final: $columnMapping'); - - return columnMapping; -} + Widget _buildAssignmentScannerPage() { return Scaffold( appBar: AppBar( @@ -1080,7 +1134,7 @@ Map _mapHeaders(List headerRow) { cutOutSize: 250, ), ), - + // Instructions overlay Positioned( bottom: 100, @@ -1095,7 +1149,8 @@ Map _mapHeaders(List headerRow) { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.assignment, color: Colors.orange.shade300, size: 40), + Icon(Icons.assignment, + color: Colors.orange.shade300, size: 40), const SizedBox(height: 8), const Text( 'Scanner l\'IMEI pour assigner au point de vente', @@ -1127,15 +1182,15 @@ Map _mapHeaders(List headerRow) { void _onAssignmentQRViewCreated(QRViewController controller) { _qrController = controller; - + controller.scannedDataStream.listen((scanData) { if (scanData.code != null && scanData.code!.isNotEmpty) { // Pauser le scanner pour éviter les scans multiples controller.pauseCamera(); - + // Fermer la page du scanner Get.back(); - + // Traiter le résultat _assignProductToUserPointDeVente(scanData.code!); } @@ -1144,7 +1199,7 @@ Map _mapHeaders(List headerRow) { Future _assignProductToUserPointDeVente(String scannedImei) async { if (_isAssigning) return; - + setState(() { _isAssigning = true; }); @@ -1178,7 +1233,8 @@ Map _mapHeaders(List headerRow) { await Future.delayed(const Duration(milliseconds: 300)); // Chercher le produit avec l'IMEI scanné - Product? foundProduct = await _productDatabase.getProductByIMEI(scannedImei); + Product? foundProduct = + await _productDatabase.getProductByIMEI(scannedImei); // Fermer l'indicateur de chargement Get.back(); @@ -1209,7 +1265,8 @@ Map _mapHeaders(List headerRow) { ram: foundProduct.ram, memoireInterne: foundProduct.memoireInterne, imei: foundProduct.imei, - pointDeVenteId: _userController.pointDeVenteId, // Nouveau point de vente + pointDeVenteId: + _userController.pointDeVenteId, // Nouveau point de vente ); await _productDatabase.updateProduct(updatedProduct); @@ -1219,11 +1276,10 @@ Map _mapHeaders(List headerRow) { // Afficher le dialogue de succès _showAssignmentSuccessDialog(foundProduct); - } catch (e) { // Fermer l'indicateur de chargement si il est encore ouvert if (Get.isDialogOpen!) Get.back(); - + Get.snackbar( 'Erreur', 'Une erreur est survenue: ${e.toString()}', @@ -1454,7 +1510,7 @@ Map _mapHeaders(List headerRow) { Widget _buildAssignmentScanCard() { final isMobile = MediaQuery.of(context).size.width < 600; - + return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 8), @@ -1502,8 +1558,10 @@ Map _mapHeaders(List headerRow) { ), ), ElevatedButton.icon( - onPressed: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) - ? null + onPressed: (_isScanning || + _isAssigning || + _userController.pointDeVenteId <= 0) + ? null : _startPointDeVenteAssignmentScanning, icon: (_isScanning || _isAssigning) ? const SizedBox( @@ -1515,13 +1573,17 @@ Map _mapHeaders(List headerRow) { ), ) : const Icon(Icons.qr_code_scanner, size: 18), - label: Text((_isScanning || _isAssigning) ? 'Scan...' : 'Assigner'), + label: + Text((_isScanning || _isAssigning) ? 'Scan...' : 'Assigner'), style: ElevatedButton.styleFrom( - backgroundColor: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) - ? Colors.grey + backgroundColor: (_isScanning || + _isAssigning || + _userController.pointDeVenteId <= 0) + ? Colors.grey : Colors.orange.shade700, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ], @@ -1532,7 +1594,7 @@ Map _mapHeaders(List headerRow) { // === FONCTIONS CONSERVÉES DU CODE ORIGINAL === // [Conservez toutes les autres méthodes du code original ici] - + // Réinitialisation de l'état d'import void _resetImportState() { setState(() { @@ -1558,12 +1620,12 @@ Map _mapHeaders(List headerRow) { 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; @@ -1578,16 +1640,16 @@ Map _mapHeaders(List headerRow) { 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(); }); @@ -1599,753 +1661,788 @@ Map _mapHeaders(List headerRow) { final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0'); return 'PROD_${timestamp}${randomSuffix}'; } -Future _downloadExcelTemplate() async { - try { - final excel = Excel.createExcel(); - final sheet = excel['Sheet1']; - - // En-têtes modifiés sans DESCRIPTION et STOCK - final headers = [ - 'ID PRODUITS', // Sera ignoré lors de l'import - 'NOM DU PRODUITS', // name - 'REFERENCE PRODUITS', // reference - 'CATEGORIES PRODUITS', // category - 'MARQUE', // marque - 'RAM', // ram - 'INTERNE', // memoire_interne - 'IMEI', // imei - 'PRIX', // price - 'BOUTIQUE', // point_de_vente - ]; - - // Ajouter les en-têtes avec style - 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', - horizontalAlign: HorizontalAlign.Center, - ); - } - - // Exemples modifiés sans DESCRIPTION et STOCK - final examples = [ - [ - '1', // ID PRODUITS (sera ignoré) - 'Smartphone Galaxy S24', // NOM DU PRODUITS - 'SGS24-001', // REFERENCE PRODUITS - 'Téléphone', // CATEGORIES PRODUITS - 'Samsung', // MARQUE - '8 Go', // RAM - '256 Go', // INTERNE - '123456789012345', // IMEI - '1200.00', // PRIX - '405A', // BOUTIQUE - ], - [ - '2', // ID PRODUITS - 'iPhone 15 Pro', // NOM DU PRODUITS - 'IP15P-001', // REFERENCE PRODUITS - 'Téléphone', // CATEGORIES PRODUITS - 'Apple', // MARQUE - '8 Go', // RAM - '512 Go', // INTERNE - '987654321098765', // IMEI - '1599.00', // PRIX - '405B', // BOUTIQUE - ], - [ - '3', // ID PRODUITS - 'MacBook Pro 14"', // NOM DU PRODUITS - 'MBP14-001', // REFERENCE PRODUITS - 'Informatique', // CATEGORIES PRODUITS - 'Apple', // MARQUE - '16 Go', // RAM - '1 To', // INTERNE - '', // IMEI (vide pour un ordinateur) - '2499.00', // PRIX - 'S405A', // BOUTIQUE - ], - [ - '4', // ID PRODUITS - 'iPad Air', // NOM DU PRODUITS - 'IPA-001', // REFERENCE PRODUITS - 'Tablette', // CATEGORIES PRODUITS - 'Apple', // MARQUE - '8 Go', // RAM - '256 Go', // INTERNE - '456789123456789', // IMEI - '699.00', // PRIX - '405A', // BOUTIQUE - ], - [ - '5', // ID PRODUITS - 'Gaming Laptop ROG', // NOM DU PRODUITS - 'ROG-001', // REFERENCE PRODUITS - 'Informatique', // CATEGORIES PRODUITS - 'ASUS', // MARQUE - '32 Go', // RAM - '1 To', // INTERNE - '', // IMEI (vide) - '1899.00', // PRIX - '405B', // BOUTIQUE - ] - ]; - - // Ajouter les exemples - 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]; - - // Style pour les données (prix en gras) - if (col == 8) { // Colonne PRIX + + Future _downloadExcelTemplate() async { + try { + final excel = Excel.createExcel(); + final sheet = excel['Sheet1']; + + // En-têtes modifiés sans DESCRIPTION et STOCK + final headers = [ + 'ID PRODUITS', // Sera ignoré lors de l'import + 'NOM DU PRODUITS', // name + 'REFERENCE PRODUITS', // reference + 'CATEGORIES PRODUITS', // category + 'MARQUE', // marque + 'RAM', // ram + 'INTERNE', // memoire_interne + 'IMEI', // imei + 'PRIX', // price + 'BOUTIQUE', // point_de_vente + ]; + + // Ajouter les en-têtes avec style + 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', + horizontalAlign: HorizontalAlign.Center, + ); + } + + // Exemples modifiés sans DESCRIPTION et STOCK + final examples = [ + [ + '1', // ID PRODUITS (sera ignoré) + 'Smartphone Galaxy S24', // NOM DU PRODUITS + 'SGS24-001', // REFERENCE PRODUITS + 'Téléphone', // CATEGORIES PRODUITS + 'Samsung', // MARQUE + '8 Go', // RAM + '256 Go', // INTERNE + '123456789012345', // IMEI + '1200.00', // PRIX + '405A', // BOUTIQUE + ], + [ + '2', // ID PRODUITS + 'iPhone 15 Pro', // NOM DU PRODUITS + 'IP15P-001', // REFERENCE PRODUITS + 'Téléphone', // CATEGORIES PRODUITS + 'Apple', // MARQUE + '8 Go', // RAM + '512 Go', // INTERNE + '987654321098765', // IMEI + '1599.00', // PRIX + '405B', // BOUTIQUE + ], + [ + '3', // ID PRODUITS + 'MacBook Pro 14"', // NOM DU PRODUITS + 'MBP14-001', // REFERENCE PRODUITS + 'Informatique', // CATEGORIES PRODUITS + 'Apple', // MARQUE + '16 Go', // RAM + '1 To', // INTERNE + '', // IMEI (vide pour un ordinateur) + '2499.00', // PRIX + 'S405A', // BOUTIQUE + ], + [ + '4', // ID PRODUITS + 'iPad Air', // NOM DU PRODUITS + 'IPA-001', // REFERENCE PRODUITS + 'Tablette', // CATEGORIES PRODUITS + 'Apple', // MARQUE + '8 Go', // RAM + '256 Go', // INTERNE + '456789123456789', // IMEI + '699.00', // PRIX + '405A', // BOUTIQUE + ], + [ + '5', // ID PRODUITS + 'Gaming Laptop ROG', // NOM DU PRODUITS + 'ROG-001', // REFERENCE PRODUITS + 'Informatique', // CATEGORIES PRODUITS + 'ASUS', // MARQUE + '32 Go', // RAM + '1 To', // INTERNE + '', // IMEI (vide) + '1899.00', // PRIX + '405B', // BOUTIQUE + ] + ]; + + // Ajouter les exemples + 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]; + + // Style pour les données (prix en gras) + if (col == 8) { + // Colonne PRIX + cell.cellStyle = CellStyle( + bold: true, + ); + } + } + } + + // Ajuster la largeur des colonnes (sans DESCRIPTION et STOCK) + sheet.setColWidth(0, 12); // ID PRODUITS + sheet.setColWidth(1, 25); // NOM DU PRODUITS + sheet.setColWidth(2, 18); // REFERENCE PRODUITS + sheet.setColWidth(3, 18); // CATEGORIES PRODUITS + sheet.setColWidth(4, 15); // MARQUE + sheet.setColWidth(5, 10); // RAM + sheet.setColWidth(6, 12); // INTERNE + sheet.setColWidth(7, 18); // IMEI + sheet.setColWidth(8, 12); // PRIX + sheet.setColWidth(9, 12); // BOUTIQUE + + // Ajouter une feuille d'instructions mise à jour + final instructionSheet = excel['Instructions']; + + final instructions = [ + ['INSTRUCTIONS D\'IMPORTATION'], + [''], + ['Format des colonnes:'], + ['• ID PRODUITS: Numéro d\'identification (ignoré lors de l\'import)'], + ['• NOM DU PRODUITS: Nom du produit (OBLIGATOIRE)'], + ['• REFERENCE PRODUITS: Référence unique du produit'], + ['• CATEGORIES PRODUITS: Catégorie du produit'], + ['• MARQUE: Marque du produit'], + ['• RAM: Mémoire RAM (ex: "8 Go", "16 Go")'], + ['• INTERNE: Stockage interne (ex: "256 Go", "1 To")'], + ['• IMEI: Numéro IMEI (pour les appareils mobiles)'], + ['• PRIX: Prix du produit en euros (OBLIGATOIRE)'], + ['• BOUTIQUE: Code du point de vente'], + [''], + ['Remarques importantes:'], + ['• Les colonnes NOM DU PRODUITS et PRIX sont obligatoires'], + ['• Si CATEGORIES PRODUITS est vide, "Non catégorisé" sera utilisé'], + [ + '• Si REFERENCE PRODUITS est vide, une référence sera générée automatiquement' + ], + ['• Le stock sera automatiquement initialisé à 1 pour chaque produit'], + ['• La description sera automatiquement vide pour chaque produit'], + ['• Les colonnes peuvent être dans n\'importe quel ordre'], + ['• Vous pouvez supprimer les colonnes non utilisées'], + [''], + ['Formats acceptés:'], + ['• PRIX: 1200.00 ou 1200,00 ou 1200'], + ['• RAM/INTERNE: Texte libre (ex: "8 Go", "256 Go", "1 To")'], + ]; + + for (int i = 0; i < instructions.length; i++) { + final cell = instructionSheet + .cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i)); + cell.value = instructions[i][0]; + + if (i == 0) { + // Titre cell.cellStyle = CellStyle( bold: true, + fontSize: 16, + backgroundColorHex: '#4CAF50', + fontColorHex: '#FFFFFF', + ); + } else if (instructions[i][0].startsWith('•')) { + // Points de liste + cell.cellStyle = CellStyle( + italic: true, + ); + } else if (instructions[i][0].endsWith(':')) { + // Sous-titres + cell.cellStyle = CellStyle( + bold: true, + backgroundColorHex: '#F5F5F5', ); } } - } - - // Ajuster la largeur des colonnes (sans DESCRIPTION et STOCK) - sheet.setColWidth(0, 12); // ID PRODUITS - sheet.setColWidth(1, 25); // NOM DU PRODUITS - sheet.setColWidth(2, 18); // REFERENCE PRODUITS - sheet.setColWidth(3, 18); // CATEGORIES PRODUITS - sheet.setColWidth(4, 15); // MARQUE - sheet.setColWidth(5, 10); // RAM - sheet.setColWidth(6, 12); // INTERNE - sheet.setColWidth(7, 18); // IMEI - sheet.setColWidth(8, 12); // PRIX - sheet.setColWidth(9, 12); // BOUTIQUE - - // Ajouter une feuille d'instructions mise à jour - final instructionSheet = excel['Instructions']; - - final instructions = [ - ['INSTRUCTIONS D\'IMPORTATION'], - [''], - ['Format des colonnes:'], - ['• ID PRODUITS: Numéro d\'identification (ignoré lors de l\'import)'], - ['• NOM DU PRODUITS: Nom du produit (OBLIGATOIRE)'], - ['• REFERENCE PRODUITS: Référence unique du produit'], - ['• CATEGORIES PRODUITS: Catégorie du produit'], - ['• MARQUE: Marque du produit'], - ['• RAM: Mémoire RAM (ex: "8 Go", "16 Go")'], - ['• INTERNE: Stockage interne (ex: "256 Go", "1 To")'], - ['• IMEI: Numéro IMEI (pour les appareils mobiles)'], - ['• PRIX: Prix du produit en euros (OBLIGATOIRE)'], - ['• BOUTIQUE: Code du point de vente'], - [''], - ['Remarques importantes:'], - ['• Les colonnes NOM DU PRODUITS et PRIX sont obligatoires'], - ['• Si CATEGORIES PRODUITS est vide, "Non catégorisé" sera utilisé'], - ['• Si REFERENCE PRODUITS est vide, une référence sera générée automatiquement'], - ['• Le stock sera automatiquement initialisé à 1 pour chaque produit'], - ['• La description sera automatiquement vide pour chaque produit'], - ['• Les colonnes peuvent être dans n\'importe quel ordre'], - ['• Vous pouvez supprimer les colonnes non utilisées'], - [''], - ['Formats acceptés:'], - ['• PRIX: 1200.00 ou 1200,00 ou 1200'], - ['• RAM/INTERNE: Texte libre (ex: "8 Go", "256 Go", "1 To")'], - ]; - - for (int i = 0; i < instructions.length; i++) { - final cell = instructionSheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i)); - cell.value = instructions[i][0]; - - if (i == 0) { // Titre - cell.cellStyle = CellStyle( - bold: true, - fontSize: 16, - backgroundColorHex: '#4CAF50', - fontColorHex: '#FFFFFF', - ); - } else if (instructions[i][0].startsWith('•')) { // Points de liste - cell.cellStyle = CellStyle( - italic: true, - ); - } else if (instructions[i][0].endsWith(':')) { // Sous-titres - cell.cellStyle = CellStyle( - bold: true, - backgroundColorHex: '#F5F5F5', - ); + + // Ajuster la largeur de la colonne instructions + instructionSheet.setColWidth(0, 80); + + final bytes = excel.save(); + + if (bytes == null) { + Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); + return; } - } - - // Ajuster la largeur de la colonne instructions - instructionSheet.setColWidth(0, 80); - - final bytes = excel.save(); - - if (bytes == null) { - Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); - return; - } - - final String? outputFile = await FilePicker.platform.saveFile( - fileName: 'modele_import_produits_v3.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\n\nConsultez l\'onglet "Instructions" pour plus d\'informations.', - duration: const Duration(seconds: 6), - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } catch (e) { - Get.snackbar('Erreur', 'Impossible d\'écrire le fichier: $e'); + + final String? outputFile = await FilePicker.platform.saveFile( + fileName: 'modele_import_produits_v3.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\n\nConsultez l\'onglet "Instructions" pour plus d\'informations.', + duration: const Duration(seconds: 6), + 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'); } - } catch (e) { - Get.snackbar('Erreur', 'Erreur lors de la création du modèle: $e'); - debugPrint('Erreur création modèle Excel: $e'); } -} - // Détecter si une valeur semble être un format de date/temps suspect -bool _isSuspiciousDateFormat(dynamic value) { - if (value == null) return false; - - String valueStr = value.toString(); - - // Détecter les formats de date suspects qui devraient être des nombres - if (valueStr.contains('-') && valueStr.contains(':')) { - // Format DateTime détecté - print('🔍 Format DateTime suspect: $valueStr'); - return true; - } - - // Détecter les très grands nombres (timestamps en millisecondes) - if (valueStr.length > 10 && !valueStr.contains('.')) { - double? numValue = double.tryParse(valueStr); - if (numValue != null && numValue > 10000000000) { - print('🔍 Grand nombre suspect: $valueStr'); + bool _isSuspiciousDateFormat(dynamic value) { + if (value == null) return false; + + String valueStr = value.toString(); + + // Détecter les formats de date suspects qui devraient être des nombres + if (valueStr.contains('-') && valueStr.contains(':')) { + // Format DateTime détecté + print('🔍 Format DateTime suspect: $valueStr'); return true; } + + // Détecter les très grands nombres (timestamps en millisecondes) + if (valueStr.length > 10 && !valueStr.contains('.')) { + double? numValue = double.tryParse(valueStr); + if (numValue != null && numValue > 10000000000) { + print('🔍 Grand nombre suspect: $valueStr'); + return true; + } + } + + return false; } - - return false; -} + // Identifier les colonnes qui devraient contenir des nombres -List _identifyNumberColumns(List headerRow) { - List numberColumns = []; - - for (int i = 0; i < headerRow.length; i++) { - if (headerRow[i]?.value == null) continue; - - String header = headerRow[i]!.value.toString().trim().toUpperCase(); - - // Identifier les en-têtes qui correspondent à des valeurs numériques - if (_isNumericHeader(header)) { - numberColumns.add(i); - print('📊 Colonne numérique: "$header" (index $i)'); + List _identifyNumberColumns(List headerRow) { + List numberColumns = []; + + for (int i = 0; i < headerRow.length; i++) { + if (headerRow[i]?.value == null) continue; + + String header = headerRow[i]!.value.toString().trim().toUpperCase(); + + // Identifier les en-têtes qui correspondent à des valeurs numériques + if (_isNumericHeader(header)) { + numberColumns.add(i); + print('📊 Colonne numérique: "$header" (index $i)'); + } } + + return numberColumns; } - - return numberColumns; -} + // Vérifier si un en-tête correspond à une colonne numérique -bool _isNumericHeader(String header) { - List numericHeaders = [ - 'PRIX', 'PRICE', 'COST', 'COUT', - 'MONTANT', 'AMOUNT', 'TOTAL', - 'QUANTITE', 'QUANTITY', 'QTE', - 'STOCK', 'NOMBRE', 'NUMBER', - 'TAILLE', 'SIZE', 'POIDS', 'WEIGHT', - 'RAM', 'MEMOIRE', 'STORAGE', 'STOCKAGE' - ]; - - return numericHeaders.any((keyword) => header.contains(keyword)); -} + bool _isNumericHeader(String header) { + List numericHeaders = [ + 'PRIX', + 'PRICE', + 'COST', + 'COUT', + 'MONTANT', + 'AMOUNT', + 'TOTAL', + 'QUANTITE', + 'QUANTITY', + 'QTE', + 'STOCK', + 'NOMBRE', + 'NUMBER', + 'TAILLE', + 'SIZE', + 'POIDS', + 'WEIGHT', + 'RAM', + 'MEMOIRE', + 'STORAGE', + 'STOCKAGE' + ]; + + return numericHeaders.any((keyword) => header.contains(keyword)); + } + // Fonction de débogage pour analyser le fichier Excel -void _debugExcelFile(Excel excel) { - print('=== DEBUG EXCEL FILE ==='); - print('Nombre de feuilles: ${excel.tables.length}'); - - for (var sheetName in excel.tables.keys) { - print('Feuille: $sheetName'); - var sheet = excel.tables[sheetName]!; - print('Nombre de lignes: ${sheet.rows.length}'); - - if (sheet.rows.isNotEmpty) { - print('En-têtes (première ligne):'); - for (int i = 0; i < sheet.rows[0].length; i++) { - var cellValue = sheet.rows[0][i]?.value; - print(' Colonne $i: "$cellValue" (${cellValue.runtimeType})'); - } - - if (sheet.rows.length > 1) { - print('Première ligne de données:'); - for (int i = 0; i < sheet.rows[1].length; i++) { - var cellValue = sheet.rows[1][i]?.value; - print(' Colonne $i: "$cellValue"'); + void _debugExcelFile(Excel excel) { + print('=== DEBUG EXCEL FILE ==='); + print('Nombre de feuilles: ${excel.tables.length}'); + + for (var sheetName in excel.tables.keys) { + print('Feuille: $sheetName'); + var sheet = excel.tables[sheetName]!; + print('Nombre de lignes: ${sheet.rows.length}'); + + if (sheet.rows.isNotEmpty) { + print('En-têtes (première ligne):'); + for (int i = 0; i < sheet.rows[0].length; i++) { + var cellValue = sheet.rows[0][i]?.value; + print(' Colonne $i: "$cellValue" (${cellValue.runtimeType})'); + } + + if (sheet.rows.length > 1) { + print('Première ligne de données:'); + for (int i = 0; i < sheet.rows[1].length; i++) { + var cellValue = sheet.rows[1][i]?.value; + print(' Colonne $i: "$cellValue"'); + } } } } + print('=== FIN DEBUG ==='); } - print('=== FIN DEBUG ==='); -} -Excel _fixExcelNumberFormats(Excel excel) { - print('🔧 Correction des formats de cellules Excel...'); - - for (var sheetName in excel.tables.keys) { - print('📋 Traitement de la feuille: $sheetName'); - var sheet = excel.tables[sheetName]!; - - if (sheet.rows.isEmpty) continue; - - // Analyser la première ligne pour identifier les colonnes de prix/nombres - List numberColumns = _identifyNumberColumns(sheet.rows[0]); - print('🔢 Colonnes numériques détectées: $numberColumns'); - - // Corriger chaque ligne de données (ignorer la ligne d'en-tête) - for (int rowIndex = 1; rowIndex < sheet.rows.length; rowIndex++) { - var row = sheet.rows[rowIndex]; - - for (int colIndex in numberColumns) { - if (colIndex < row.length && row[colIndex] != null) { - var cell = row[colIndex]!; - var originalValue = cell.value; - - // Détecter si la cellule a un format de date/temps suspect - if (_isSuspiciousDateFormat(originalValue)) { - print('⚠️ Cellule suspecte détectée en ($rowIndex, $colIndex): $originalValue'); - - // Convertir la valeur corrompue en nombre standard - var correctedValue = _convertSuspiciousValue(originalValue); - if (correctedValue != null) { - print('✅ Correction: $originalValue → $correctedValue'); - - // Créer une nouvelle cellule avec la valeur corrigée - excel.updateCell(sheetName, - CellIndex.indexByColumnRow(columnIndex: colIndex, rowIndex: rowIndex), - correctedValue - ); + + Excel _fixExcelNumberFormats(Excel excel) { + print('🔧 Correction des formats de cellules Excel...'); + + for (var sheetName in excel.tables.keys) { + print('📋 Traitement de la feuille: $sheetName'); + var sheet = excel.tables[sheetName]!; + + if (sheet.rows.isEmpty) continue; + + // Analyser la première ligne pour identifier les colonnes de prix/nombres + List numberColumns = _identifyNumberColumns(sheet.rows[0]); + print('🔢 Colonnes numériques détectées: $numberColumns'); + + // Corriger chaque ligne de données (ignorer la ligne d'en-tête) + for (int rowIndex = 1; rowIndex < sheet.rows.length; rowIndex++) { + var row = sheet.rows[rowIndex]; + + for (int colIndex in numberColumns) { + if (colIndex < row.length && row[colIndex] != null) { + var cell = row[colIndex]!; + var originalValue = cell.value; + + // Détecter si la cellule a un format de date/temps suspect + if (_isSuspiciousDateFormat(originalValue)) { + print( + '⚠️ Cellule suspecte détectée en ($rowIndex, $colIndex): $originalValue'); + + // Convertir la valeur corrompue en nombre standard + var correctedValue = _convertSuspiciousValue(originalValue); + if (correctedValue != null) { + print('✅ Correction: $originalValue → $correctedValue'); + + // Créer une nouvelle cellule avec la valeur corrigée + excel.updateCell( + sheetName, + CellIndex.indexByColumnRow( + columnIndex: colIndex, rowIndex: rowIndex), + correctedValue); + } } } } } } - } - - print('✅ Correction des formats terminée'); - return excel; -} -double? _convertSuspiciousValue(dynamic suspiciousValue) { - if (suspiciousValue == null) return null; - - String valueStr = suspiciousValue.toString(); - - // Cas 1: Format DateTime (ex: "3953-06-05T00:00:00.000") - if (valueStr.contains('-') && valueStr.contains(':')) { - return _convertDateTimeToNumber(valueStr); - } - - // Cas 2: Grand nombre (ex: "39530605000000") - if (valueStr.length > 10) { - return _convertLargeNumberToPrice(valueStr); - } - - return null; -} -// Convertir un format DateTime en nombre -double? _convertDateTimeToNumber(String dateTimeStr) { - try { - print('🔄 Conversion DateTime: $dateTimeStr'); - - // Nettoyer la chaîne - String cleanDateString = dateTimeStr.replaceAll('+', ''); - final dateTime = DateTime.parse(cleanDateString); - - // Excel epoch: 1er janvier 1900 - final excelEpoch = DateTime(1900, 1, 1); - - // Calculer le nombre de jours depuis l'epoch Excel - final daysDifference = dateTime.difference(excelEpoch).inDays; - - // Appliquer la correction pour le bug Excel (+2) - final correctedValue = daysDifference + 2; - - print('→ Jours calculés: $daysDifference → Corrigé: $correctedValue'); - - if (correctedValue > 0 && correctedValue < 100000000) { - return correctedValue.toDouble(); - } - } catch (e) { - print('❌ Erreur conversion DateTime: $e'); + print('✅ Correction des formats terminée'); + return excel; } - - return null; -} -// Convertir un grand nombre en prix -double? _convertLargeNumberToPrice(String largeNumberStr) { - try { - print('🔄 Conversion grand nombre: $largeNumberStr'); - - double? numValue = double.tryParse(largeNumberStr); - if (numValue == null) return null; - - // Si le nombre se termine par 000000 (microsecondes), les supprimer - if (largeNumberStr.endsWith('000000') && largeNumberStr.length > 10) { - String withoutMicros = largeNumberStr.substring(0, largeNumberStr.length - 6); - double? daysSinceExcel = double.tryParse(withoutMicros); - - if (daysSinceExcel != null && daysSinceExcel > 1000 && daysSinceExcel < 10000000) { - // Appliquer la correction du décalage Excel (+2) - double correctedPrice = daysSinceExcel + 2; - print('→ Conversion: $largeNumberStr → $withoutMicros → $correctedPrice'); - return correctedPrice; - } + double? _convertSuspiciousValue(dynamic suspiciousValue) { + if (suspiciousValue == null) return null; + + String valueStr = suspiciousValue.toString(); + + // Cas 1: Format DateTime (ex: "3953-06-05T00:00:00.000") + if (valueStr.contains('-') && valueStr.contains(':')) { + return _convertDateTimeToNumber(valueStr); } - - // Table de correspondance pour les cas connus - Map knownConversions = { - '39530605000000': 750000, - '170950519000000': 5550000, - }; - - if (knownConversions.containsKey(largeNumberStr)) { - double realPrice = knownConversions[largeNumberStr]!; - print('→ Conversion via table: $largeNumberStr → $realPrice'); - return realPrice; + + // Cas 2: Grand nombre (ex: "39530605000000") + if (valueStr.length > 10) { + return _convertLargeNumberToPrice(valueStr); } - - } catch (e) { - print('❌ Erreur conversion grand nombre: $e'); + + return null; } - - return null; -} +// Convertir un format DateTime en nombre + double? _convertDateTimeToNumber(String dateTimeStr) { + try { + print('🔄 Conversion DateTime: $dateTimeStr'); -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 _importFromExcel() async { - try { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['xlsx', 'xls','csv'], - allowMultiple: false, - ); + // Nettoyer la chaîne + String cleanDateString = dateTimeStr.replaceAll('+', ''); + final dateTime = DateTime.parse(cleanDateString); - if (result == null || result.files.isEmpty) { - Get.snackbar('Annulé', 'Aucun fichier sélectionné'); - return; - } + // Excel epoch: 1er janvier 1900 + final excelEpoch = DateTime(1900, 1, 1); - setState(() { - _isImporting = true; - _importProgress = 0.0; - _importStatusText = 'Lecture du fichier...'; - }); + // Calculer le nombre de jours depuis l'epoch Excel + final daysDifference = dateTime.difference(excelEpoch).inDays; - final file = File(result.files.single.path!); - - if (!await file.exists()) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); - return; - } + // Appliquer la correction pour le bug Excel (+2) + final correctedValue = daysDifference + 2; - setState(() { - _importProgress = 0.1; - _importStatusText = 'Vérification du fichier...'; - }); + print('→ Jours calculés: $daysDifference → Corrigé: $correctedValue'); - final bytes = await file.readAsBytes(); - - if (bytes.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier Excel est vide'); - return; + if (correctedValue > 0 && correctedValue < 100000000) { + return correctedValue.toDouble(); + } + } catch (e) { + print('❌ Erreur conversion DateTime: $e'); } - setState(() { - _importProgress = 0.2; - _importStatusText = 'Décodage du fichier Excel...'; - }); + return null; + } - Excel excel; +// Convertir un grand nombre en prix + double? _convertLargeNumberToPrice(String largeNumberStr) { try { - excel = Excel.decodeBytes(bytes); - _debugExcelFile(excel); - } 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; + print('🔄 Conversion grand nombre: $largeNumberStr'); + + double? numValue = double.tryParse(largeNumberStr); + if (numValue == null) return null; + + // Si le nombre se termine par 000000 (microsecondes), les supprimer + if (largeNumberStr.endsWith('000000') && largeNumberStr.length > 10) { + String withoutMicros = + largeNumberStr.substring(0, largeNumberStr.length - 6); + double? daysSinceExcel = double.tryParse(withoutMicros); + + if (daysSinceExcel != null && + daysSinceExcel > 1000 && + daysSinceExcel < 10000000) { + // Appliquer la correction du décalage Excel (+2) + double correctedPrice = daysSinceExcel + 2; + print( + '→ Conversion: $largeNumberStr → $withoutMicros → $correctedPrice'); + return correctedPrice; + } } - } - // ✨ NOUVELLE ÉTAPE: Corriger les formats de cellules - setState(() { - _importProgress = 0.25; - _importStatusText = 'Correction des formats de cellules...'; - }); - - excel = _fixExcelNumberFormats(excel); + // Table de correspondance pour les cas connus + Map knownConversions = { + '39530605000000': 750000, + '170950519000000': 5550000, + }; - if (excel.tables.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille'); - return; + if (knownConversions.containsKey(largeNumberStr)) { + double realPrice = knownConversions[largeNumberStr]!; + print('→ Conversion via table: $largeNumberStr → $realPrice'); + return realPrice; + } + } catch (e) { + print('❌ Erreur conversion grand nombre: $e'); } - setState(() { - _importProgress = 0.3; - _importStatusText = 'Analyse des données...'; - }); + return null; + } - 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; - } + 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, + ), + ), + ], + ), + ); + } - // Détection automatique des colonnes - final headerRow = sheet.rows[0]; - final columnMapping = _mapHeaders(headerRow); - - // Vérification des colonnes obligatoires - if (!columnMapping.containsKey('name')) { - _resetImportState(); - Get.snackbar('Erreur', 'Colonne "Nom du produit" non trouvée dans le fichier'); - return; - } - - if (!columnMapping.containsKey('price')) { - _resetImportState(); - Get.snackbar('Erreur', 'Colonne "Prix" non trouvée dans le fichier'); - return; - } + Future _importFromExcel() async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xlsx', 'xls', 'csv'], + allowMultiple: false, + ); - int successCount = 0; - int errorCount = 0; - List errorMessages = []; + if (result == null || result.files.isEmpty) { + Get.snackbar('Annulé', 'Aucun fichier sélectionné'); + return; + } - final totalRows = sheet.rows.length - 1; - - setState(() { - _importStatusText = 'Importation en cours... (0/$totalRows)'; - }); + setState(() { + _isImporting = true; + _importProgress = 0.0; + _importStatusText = 'Lecture du fichier...'; + }); - 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)'; - }); + final file = File(result.files.single.path!); - await Future.delayed(const Duration(milliseconds: 10)); + if (!await file.exists()) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); + return; + } - final row = sheet.rows[i]; - - if (row.isEmpty) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Ligne vide'); - continue; - } + setState(() { + _importProgress = 0.1; + _importStatusText = 'Vérification du fichier...'; + }); - // Normalisation des données (maintenant les prix sont corrects) - final normalizedData = _normalizeRowData(row, columnMapping, i); - - // Vérification de la référence - if (normalizedData['imei'] != null) { - var existingProduct = await _productDatabase.getProductByIMEI(normalizedData['imei']); - if (existingProduct != null) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: imei déjà existante (${normalizedData['imei']})'); - continue; - } - } + final bytes = await file.readAsBytes(); - // Création du point de vente si nécessaire - int? pointDeVenteId; - if (normalizedData['point_de_vente'] != null) { - pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(normalizedData['point_de_vente']); - if (pointDeVenteId == null) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Impossible de créer le point de vente ${normalizedData['point_de_vente']}'); - continue; - } - } + if (bytes.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier Excel est vide'); + return; + } - setState(() { - _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; - }); - - // Création du produit avec les données normalisées - final product = Product( - name: normalizedData['name'], - price: normalizedData['price'], - image: '', - category: normalizedData['category'], - description: normalizedData['description'], - stock: normalizedData['stock'], - qrCode: '', - reference: normalizedData['reference'], - marque: normalizedData['marque'], - ram: normalizedData['ram'], - memoireInterne: normalizedData['memoire_interne'], - imei: normalizedData['imei'], - pointDeVenteId: pointDeVenteId, - ); + setState(() { + _importProgress = 0.2; + _importStatusText = 'Décodage du fichier Excel...'; + }); - await _productDatabase.createProduct(product); - successCount++; - + Excel excel; + try { + excel = Excel.decodeBytes(bytes); + _debugExcelFile(excel); } catch (e) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: ${e.toString()}'); - debugPrint('Erreur ligne ${i + 1}: $e'); - } - } + _resetImportState(); + debugPrint('Erreur décodage Excel: $e'); - setState(() { - _importProgress = 1.0; - _importStatusText = 'Finalisation...'; - }); + 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; + } + } - await Future.delayed(const Duration(milliseconds: 500)); + // ✨ NOUVELLE ÉTAPE: Corriger les formats de cellules + setState(() { + _importProgress = 0.25; + _importStatusText = 'Correction des formats de cellules...'; + }); - _resetImportState(); + excel = _fixExcelNumberFormats(excel); - String message = '$successCount produits importés avec succès'; - if (errorCount > 0) { - message += ', $errorCount erreurs'; - - if (errorMessages.length <= 5) { - message += ':\n${errorMessages.join('\n')}'; + if (excel.tables.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille'); + return; } - } - 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(); - print(errorMessages); - } catch (e) { - _resetImportState(); - Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); - debugPrint('Erreur générale import Excel: $e'); - } -} + setState(() { + _importProgress = 0.3; + _importStatusText = 'Analyse des données...'; + }); + final sheetName = excel.tables.keys.first; + final sheet = excel.tables[sheetName]!; -// 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, + if (sheet.rows.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'La feuille Excel est vide'); + return; + } + + // Détection automatique des colonnes + final headerRow = sheet.rows[0]; + final columnMapping = _mapHeaders(headerRow); + + // Vérification des colonnes obligatoires + if (!columnMapping.containsKey('name')) { + _resetImportState(); + Get.snackbar( + 'Erreur', 'Colonne "Nom du produit" non trouvée dans le fichier'); + return; + } + + if (!columnMapping.containsKey('price')) { + _resetImportState(); + Get.snackbar('Erreur', 'Colonne "Prix" non trouvée dans le fichier'); + return; + } + + int successCount = 0; + int errorCount = 0; + List errorMessages = []; + + 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) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Ligne vide'); + continue; + } + + // Normalisation des données (maintenant les prix sont corrects) + final normalizedData = _normalizeRowData(row, columnMapping, i); + + // Vérification de la référence + if (normalizedData['imei'] != null) { + var existingProduct = + await _productDatabase.getProductByIMEI(normalizedData['imei']); + if (existingProduct != null) { + errorCount++; + errorMessages.add( + 'Ligne ${i + 1}: imei déjà existante (${normalizedData['imei']})'); + continue; + } + } + + // Création du point de vente si nécessaire + int? pointDeVenteId; + if (normalizedData['point_de_vente'] != null) { + pointDeVenteId = await _productDatabase + .getOrCreatePointDeVenteByNom(normalizedData['point_de_vente']); + if (pointDeVenteId == null) { + errorCount++; + errorMessages.add( + 'Ligne ${i + 1}: Impossible de créer le point de vente ${normalizedData['point_de_vente']}'); + continue; + } + } + + setState(() { + _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; + }); + + // Création du produit avec les données normalisées + final product = Product( + name: normalizedData['name'], + price: normalizedData['price'], + image: '', + category: normalizedData['category'], + description: normalizedData['description'], + stock: normalizedData['stock'], + qrCode: '', + reference: normalizedData['reference'], + marque: normalizedData['marque'], + ram: normalizedData['ram'], + memoireInterne: normalizedData['memoire_interne'], + imei: normalizedData['imei'], + pointDeVenteId: pointDeVenteId, + ); + + await _productDatabase.createProduct(product); + successCount++; + } catch (e) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: ${e.toString()}'); + 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(); + print(errorMessages); + } 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), + LinearProgressIndicator( + value: _importProgress, + backgroundColor: Colors.blue.shade100, + valueColor: AlwaysStoppedAnimation(Colors.blue.shade600), ), - ), - const SizedBox(height: 8), - Text( - '${(_importProgress * 100).round()}%', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: 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, + ), + ), + ], + ), + ); + } //============================================================================================================================= - - - - Widget _buildProductCardContent(Product product, String pointDeVenteText) { - final isCurrentUserPointDeVente = product.pointDeVenteId == _userController.pointDeVenteId; - + final isCurrentUserPointDeVente = + product.pointDeVenteId == _userController.pointDeVenteId; + return InkWell( onTap: () => _showProductDetailsDialog(context, product), child: Card( @@ -2353,7 +2450,7 @@ Widget _buildImportProgressIndicator() { elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), - side: isCurrentUserPointDeVente + side: isCurrentUserPointDeVente ? BorderSide(color: Colors.orange.shade300, width: 2) : BorderSide.none, ), @@ -2384,7 +2481,7 @@ Widget _buildImportProgressIndicator() { ), ), const SizedBox(width: 16), - + // Informations du produit Expanded( child: Column( @@ -2429,14 +2526,17 @@ Widget _buildImportProgressIndicator() { 'Stock: ${product.stock}', style: TextStyle( fontSize: 12, - color: product.stock! > 0 ? Colors.green : Colors.red, + color: product.stock! > 0 + ? Colors.green + : Colors.red, fontWeight: FontWeight.w500, ), ), ], ), // Afficher l'IMEI si disponible - if (product.imei != null && product.imei!.isNotEmpty) ...[ + if (product.imei != null && + product.imei!.isNotEmpty) ...[ const SizedBox(height: 4), Text( 'IMEI: ${product.imei}', @@ -2450,15 +2550,18 @@ Widget _buildImportProgressIndicator() { ], ), ), - + // Actions Column( children: [ // Bouton d'assignation rapide si l'utilisateur a un point de vente - if (_userController.pointDeVenteId > 0 && !isCurrentUserPointDeVente && product.imei != null) + if (_userController.pointDeVenteId > 0 && + !isCurrentUserPointDeVente && + product.imei != null) IconButton( onPressed: () => _assignProductDirectly(product), - icon: Icon(Icons.assignment, color: Colors.orange.shade700), + icon: Icon(Icons.assignment, + color: Colors.orange.shade700), tooltip: 'Assigner à mon point de vente', ), IconButton( @@ -2484,24 +2587,29 @@ Widget _buildImportProgressIndicator() { // Ligne du point de vente avec indication visuelle Row( children: [ - Icon( - Icons.store, - size: 16, - color: isCurrentUserPointDeVente ? Colors.orange.shade700 : Colors.grey - ), + Icon(Icons.store, + size: 16, + color: isCurrentUserPointDeVente + ? Colors.orange.shade700 + : Colors.grey), const SizedBox(width: 4), Text( 'Point de vente: $pointDeVenteText', style: TextStyle( - fontSize: 12, - color: isCurrentUserPointDeVente ? Colors.orange.shade700 : Colors.grey, - fontWeight: isCurrentUserPointDeVente ? FontWeight.w600 : FontWeight.normal, + fontSize: 12, + color: isCurrentUserPointDeVente + ? Colors.orange.shade700 + : Colors.grey, + fontWeight: isCurrentUserPointDeVente + ? FontWeight.w600 + : FontWeight.normal, ), ), const Spacer(), if (isCurrentUserPointDeVente) Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(8), @@ -2518,7 +2626,8 @@ Widget _buildImportProgressIndicator() { if (pointDeVenteText == 'Non spécifié') TextButton( onPressed: () => _showAddPointDeVenteDialog(product), - child: const Text('Ajouter', style: TextStyle(fontSize: 12)), + child: + const Text('Ajouter', style: TextStyle(fontSize: 12)), ), ], ), @@ -2532,7 +2641,7 @@ Widget _buildImportProgressIndicator() { // Assignation directe d'un produit (via le bouton sur la carte) Future _assignProductDirectly(Product product) async { if (_isAssigning) return; - + // Confirmer l'action final confirm = await Get.dialog( AlertDialog( @@ -2621,7 +2730,6 @@ Widget _buildImportProgressIndicator() { duration: const Duration(seconds: 3), icon: const Icon(Icons.check_circle, color: Colors.white), ); - } catch (e) { Get.snackbar( 'Erreur', @@ -2639,928 +2747,988 @@ Widget _buildImportProgressIndicator() { } // Méthodes placeholder pour les fonctions manquantes - void _showQRCode(Product product) { - // État pour contrôler le type d'affichage (true = URL complète, false = référence seulement) - RxBool showFullUrl = true.obs; - - Get.dialog( - Obx(() { - // Données du QR code selon l'état - final qrData = showFullUrl.value - ? 'https://stock.guycom.mg/${product.reference}' - : product.reference!; - - return 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, + void _showQRCode(Product product) { + // État pour contrôler le type d'affichage (true = URL complète, false = référence seulement) + RxBool showFullUrl = true.obs; + + Get.dialog( + Obx(() { + // Données du QR code selon l'état + final qrData = showFullUrl.value + ? 'https://stock.guycom.mg/${product.reference}' + : product.reference!; + + return AlertDialog( + title: Row( children: [ - // Bouton pour basculer entre URL et référence - ElevatedButton.icon( - onPressed: () { - showFullUrl.value = !showFullUrl.value; - }, - icon: Icon( - showFullUrl.value ? Icons.link : Icons.tag, - size: 16, - ), - label: Text( - showFullUrl.value ? 'URL/Référence' : 'Référence', - style: const TextStyle(fontSize: 14), - ), - style: ElevatedButton.styleFrom( - backgroundColor: showFullUrl.value ? Colors.blue : Colors.green, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 36), + 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), ), ), - const SizedBox(height: 16), - - // Container du QR Code - 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: qrData, - version: QrVersions.auto, - size: 200, - backgroundColor: Colors.white, + ], + ), + content: Container( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Bouton pour basculer entre URL et référence + ElevatedButton.icon( + onPressed: () { + showFullUrl.value = !showFullUrl.value; + }, + icon: Icon( + showFullUrl.value ? Icons.link : Icons.tag, + size: 16, + ), + label: Text( + showFullUrl.value ? 'URL/Référence' : 'Référence', + style: const TextStyle(fontSize: 14), + ), + style: ElevatedButton.styleFrom( + backgroundColor: + showFullUrl.value ? Colors.blue : Colors.green, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 36), + ), ), - ), - const SizedBox(height: 16), - - // Affichage des données actuelles - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(8), + const SizedBox(height: 16), + + // Container du QR Code + 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: qrData, + version: QrVersions.auto, + size: 200, + backgroundColor: Colors.white, + ), ), - child: Column( - children: [ - Text( - showFullUrl.value ? 'URL Complète' : 'Référence Seulement', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - Text( - qrData, - style: const TextStyle(fontSize: 12, color: Colors.grey), - textAlign: TextAlign.center, - ), - ], + const SizedBox(height: 16), + + // Affichage des données actuelles + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Text( + showFullUrl.value + ? 'URL Complète' + : 'Référence Seulement', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + qrData, + style: + const TextStyle(fontSize: 12, color: Colors.grey), + textAlign: TextAlign.center, + ), + ], + ), ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: qrData)); - Get.back(); - Get.snackbar( - 'Copié', - '${showFullUrl.value ? "URL" : "Référence"} copiée dans le presse-papiers', - backgroundColor: Colors.green, - colorText: Colors.white, - ); - }, - child: Text('Copier ${showFullUrl.value ? "URL" : "Référence"}'), - ), - TextButton( - onPressed: () => _generatePDF(product, qrData), - child: const Text('Imprimer en PDF'), - ), - TextButton( - onPressed: () => Get.back(), - child: const Text('Fermer'), + ], + ), ), - ], - ); - }), - ); -} + actions: [ + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: qrData)); + Get.back(); + Get.snackbar( + 'Copié', + '${showFullUrl.value ? "URL" : "Référence"} copiée dans le presse-papiers', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + }, + child: Text('Copier ${showFullUrl.value ? "URL" : "Référence"}'), + ), + TextButton( + onPressed: () => _generatePDF(product, qrData), + child: const Text('Imprimer en PDF'), + ), + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ], + ); + }), + ); + } Future _generatePDF(Product product, String qrUrl) async { - final pdf = pw.Document(); + final pdf = pw.Document(); - pdf.addPage( - pw.Page( - build: (pw.Context context) { - return pw.Center( - child: pw.Column( - children: [ - // pw.Text('QR Code - ${product.name}', style: pw.TextStyle(fontSize: 20)), - pw.SizedBox(height: 20), - pw.BarcodeWidget( - barcode: pw.Barcode.qrCode(), - data: qrUrl, - width: 200, - height: 200, - ), - pw.SizedBox(height: 20), - // pw.Text('URL/Référence: $qrUrl', style: pw.TextStyle(fontSize: 12)), - // pw.SizedBox(height: 10), - // pw.Text('Référence: ${product.reference}', style: pw.TextStyle(fontSize: 12)), - ], - ), - ); - }, - ), - ); + pdf.addPage( + pw.Page( + build: (pw.Context context) { + return pw.Center( + child: pw.Column( + children: [ + // pw.Text('QR Code - ${product.name}', style: pw.TextStyle(fontSize: 20)), + pw.SizedBox(height: 20), + pw.BarcodeWidget( + barcode: pw.Barcode.qrCode(), + data: qrUrl, + width: 200, + height: 200, + ), + pw.SizedBox(height: 20), + // pw.Text('URL/Référence: $qrUrl', style: pw.TextStyle(fontSize: 12)), + pw.SizedBox(height: 10), + pw.Text('Référence: ${product.reference}', + style: pw.TextStyle(fontSize: 12)), + ], + ), + ); + }, + ), + ); - final output = await getTemporaryDirectory(); - final file = File("${output.path}/qrcode.pdf"); - await file.writeAsBytes(await pdf.save()); + final output = await getTemporaryDirectory(); + final file = File("${output.path}/qrcode.pdf"); + await file.writeAsBytes(await pdf.save()); - OpenFile.open(file.path); -} + OpenFile.open(file.path); + } 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); - final referenceController = TextEditingController(text: product.reference ?? ''); - final marqueController = TextEditingController(text: product.marque ?? ''); - final ramController = TextEditingController(text: product.ram ?? ''); - final memoireInterneController = TextEditingController(text: product.memoireInterne ?? ''); - final imeiController = TextEditingController(text: product.imei ?? ''); - final newPointDeVenteController = TextEditingController(); - - String? selectedPointDeVente; - List> pointsDeVente = []; - bool isLoadingPoints = true; - // Initialiser la catégorie sélectionnée de manière sécurisée - String selectedCategory = _predefinedCategories.contains(product.category) - ? product.category - : _predefinedCategories.last; // 'Non catégorisé' par défaut - File? pickedImage; - String? qrPreviewData; - bool showAddNewPoint = false; - - // Fonction pour mettre à jour le QR preview - void updateQrPreview() { - if (nameController.text.isNotEmpty && referenceController.text.isNotEmpty) { - qrPreviewData = 'https://stock.guycom.mg/${referenceController.text.trim()}'; - } else { - qrPreviewData = null; + 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); + final referenceController = + TextEditingController(text: product.reference ?? ''); + final marqueController = TextEditingController(text: product.marque ?? ''); + final ramController = TextEditingController(text: product.ram ?? ''); + final memoireInterneController = + TextEditingController(text: product.memoireInterne ?? ''); + final imeiController = TextEditingController(text: product.imei ?? ''); + final newPointDeVenteController = TextEditingController(); + + String? selectedPointDeVente; + List> pointsDeVente = []; + bool isLoadingPoints = true; + // Initialiser la catégorie sélectionnée de manière sécurisée + String selectedCategory = _predefinedCategories.contains(product.category) + ? product.category + : _predefinedCategories.last; // 'Non catégorisé' par défaut + File? pickedImage; + String? qrPreviewData; + bool showAddNewPoint = false; + + // Fonction pour mettre à jour le QR preview + void updateQrPreview() { + if (nameController.text.isNotEmpty && + referenceController.text.isNotEmpty) { + qrPreviewData = + 'https://stock.guycom.mg/${referenceController.text.trim()}'; + } else { + qrPreviewData = null; + } } - } - // Charger les points de vente - Future loadPointsDeVente(StateSetter setDialogState) async { - try { - final result = await _productDatabase.getPointsDeVente(); - setDialogState(() { - // Ajouter l'option "Aucun" à la liste - pointsDeVente = [ - {'id': null, 'nom': 'Aucun'}, - ...result - ]; - isLoadingPoints = false; - // Définir le point de vente actuel du produit - if (product.pointDeVenteId != null) { - final currentPointDeVente = result.firstWhere( - (point) => point['id'] == product.pointDeVenteId, - orElse: () => {}, - ); - if (currentPointDeVente.isNotEmpty) { - selectedPointDeVente = currentPointDeVente['nom'] as String; + // Charger les points de vente + Future loadPointsDeVente(StateSetter setDialogState) async { + try { + final result = await _productDatabase.getPointsDeVente(); + setDialogState(() { + // Ajouter l'option "Aucun" à la liste + pointsDeVente = [ + {'id': null, 'nom': 'Aucun'}, + ...result + ]; + isLoadingPoints = false; + // Définir le point de vente actuel du produit + if (product.pointDeVenteId != null) { + final currentPointDeVente = result.firstWhere( + (point) => point['id'] == product.pointDeVenteId, + orElse: () => {}, + ); + if (currentPointDeVente.isNotEmpty) { + selectedPointDeVente = currentPointDeVente['nom'] as String; + } + } else { + selectedPointDeVente = + 'Aucun'; // Si aucun point de vente, sélectionner "Aucun" } - } else { - selectedPointDeVente = 'Aucun'; // Si aucun point de vente, sélectionner "Aucun" - } - }); - } catch (e) { - setDialogState(() { - isLoadingPoints = false; - }); - Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); + }); + } catch (e) { + setDialogState(() { + isLoadingPoints = false; + }); + Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); + } } - } - // Initialiser le QR preview - updateQrPreview(); + // Initialiser le QR preview + updateQrPreview(); - Get.dialog( - AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.orange.shade100, - borderRadius: BorderRadius.circular(8), + Get.dialog( + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.edit, color: Colors.orange.shade700), ), - child: Icon(Icons.edit, color: Colors.orange.shade700), - ), - const SizedBox(width: 12), - const Text('Modifier le produit'), - ], - ), - content: Container( - width: 600, - constraints: const BoxConstraints(maxHeight: 600), - child: SingleChildScrollView( - child: StatefulBuilder( - builder: (context, setDialogState) { - if (isLoadingPoints && pointsDeVente.isEmpty) { - WidgetsBinding.instance.addPostFrameCallback((_) { - loadPointsDeVente(setDialogState); - }); - } + const SizedBox(width: 12), + const Text('Modifier le produit'), + ], + ), + content: Container( + width: 600, + constraints: const BoxConstraints(maxHeight: 600), + child: SingleChildScrollView( + child: StatefulBuilder( + builder: (context, setDialogState) { + if (isLoadingPoints && pointsDeVente.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + loadPointsDeVente(setDialogState); + }); + } - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Champs obligatoires - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.orange.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.orange.shade200), + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champs obligatoires + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade200), + ), + child: Row( + children: [ + Icon(Icons.info, + color: Colors.orange.shade600, size: 16), + const SizedBox(width: 8), + const Text( + 'Les champs marqués d\'un * sont obligatoires', + style: TextStyle( + fontSize: 12, fontWeight: FontWeight.w500), + ), + ], + ), ), - child: Row( - children: [ - Icon(Icons.info, color: Colors.orange.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), + const SizedBox(height: 16), - // Section Point de vente - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.teal.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.teal.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.store, color: Colors.teal.shade700), - const SizedBox(width: 8), - Text( - 'Point de vente', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.teal.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - - if (isLoadingPoints) - const Center(child: CircularProgressIndicator()) - else - Column( + // Section Point de vente + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.teal.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.teal.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - if (!showAddNewPoint) ...[ - DropdownButtonFormField( - value: selectedPointDeVente, - items: pointsDeVente.map((point) { - return DropdownMenuItem( - value: point['nom'] as String, - child: Text(point['nom'] as String), - ); - }).toList(), - onChanged: (value) { - setDialogState(() => selectedPointDeVente = value); - }, - decoration: const InputDecoration( - labelText: 'Sélectionner un point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.store), - filled: true, - fillColor: Colors.white, - ), + Icon(Icons.store, color: Colors.teal.shade700), + const SizedBox(width: 8), + Text( + 'Point de vente', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.teal.shade700, ), - const SizedBox(height: 8), - Row( - children: [ - TextButton.icon( - onPressed: () { - setDialogState(() { - showAddNewPoint = true; - newPointDeVenteController.clear(); - }); - }, - icon: const Icon(Icons.add, size: 16), - label: const Text('Ajouter nouveau point'), - style: TextButton.styleFrom( - foregroundColor: Colors.teal.shade700, - ), - ), - const Spacer(), - TextButton.icon( - onPressed: () => loadPointsDeVente(setDialogState), - icon: const Icon(Icons.refresh, size: 16), - label: const Text('Actualiser'), + ), + ], + ), + const SizedBox(height: 12), + if (isLoadingPoints) + const Center(child: CircularProgressIndicator()) + else + Column( + children: [ + if (!showAddNewPoint) ...[ + DropdownButtonFormField( + value: selectedPointDeVente, + items: pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + setDialogState( + () => selectedPointDeVente = value); + }, + decoration: const InputDecoration( + labelText: + 'Sélectionner un point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.store), + filled: true, + fillColor: Colors.white, ), - ], - ), - ], - - if (showAddNewPoint) ...[ - DropdownButtonFormField( - value: selectedPointDeVente, - items: pointsDeVente.map((point) { - return DropdownMenuItem( - value: point['nom'] as String, - child: Text(point['nom'] as String), - ); - }).toList(), - onChanged: (value) { - setDialogState(() => selectedPointDeVente = value); - }, - decoration: const InputDecoration( - labelText: 'Sélectionner un point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.store), - filled: true, - fillColor: Colors.white, ), - ), - const SizedBox(height: 8), - Row( - children: [ - TextButton.icon( - onPressed: () { - setDialogState(() { - showAddNewPoint = true; - newPointDeVenteController.clear(); - }); - }, - icon: const Icon(Icons.add, size: 16), - label: const Text('Ajouter nouveau point'), - style: TextButton.styleFrom( - foregroundColor: Colors.teal.shade700, + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: () { + setDialogState(() { + showAddNewPoint = true; + newPointDeVenteController.clear(); + }); + }, + icon: const Icon(Icons.add, size: 16), + label: + const Text('Ajouter nouveau point'), + style: TextButton.styleFrom( + foregroundColor: Colors.teal.shade700, + ), ), + const Spacer(), + TextButton.icon( + onPressed: () => + loadPointsDeVente(setDialogState), + icon: + const Icon(Icons.refresh, size: 16), + label: const Text('Actualiser'), + ), + ], + ), + ], + if (showAddNewPoint) ...[ + DropdownButtonFormField( + value: selectedPointDeVente, + items: pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + setDialogState( + () => selectedPointDeVente = value); + }, + decoration: const InputDecoration( + labelText: + 'Sélectionner un point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.store), + filled: true, + fillColor: Colors.white, ), - const Spacer(), - TextButton.icon( - onPressed: () => loadPointsDeVente(setDialogState), - icon: const Icon(Icons.refresh, size: 16), - label: const Text('Actualiser'), - ), - ], - ), - ], - - if (showAddNewPoint) ...[ - TextField( - controller: newPointDeVenteController, - decoration: const InputDecoration( - labelText: 'Nom du nouveau point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.add_business), - filled: true, - fillColor: Colors.white, ), - ), - const SizedBox(height: 8), - Row( - children: [ - TextButton( - onPressed: () { - setDialogState(() { - showAddNewPoint = false; - newPointDeVenteController.clear(); - }); - }, - child: const Text('Annuler'), + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: () { + setDialogState(() { + showAddNewPoint = true; + newPointDeVenteController.clear(); + }); + }, + icon: const Icon(Icons.add, size: 16), + label: + const Text('Ajouter nouveau point'), + style: TextButton.styleFrom( + foregroundColor: Colors.teal.shade700, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: () => + loadPointsDeVente(setDialogState), + icon: + const Icon(Icons.refresh, size: 16), + label: const Text('Actualiser'), + ), + ], + ), + ], + if (showAddNewPoint) ...[ + TextField( + controller: newPointDeVenteController, + decoration: const InputDecoration( + labelText: + 'Nom du nouveau point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.add_business), + filled: true, + fillColor: Colors.white, ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: () async { - final nom = newPointDeVenteController.text.trim(); - if (nom.isNotEmpty) { - try { - final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); - if (id != null) { - setDialogState(() { - showAddNewPoint = false; - selectedPointDeVente = nom; - newPointDeVenteController.clear(); - }); - // Recharger la liste - await loadPointsDeVente(setDialogState); - Get.snackbar( - 'Succès', - 'Point de vente "$nom" créé avec succès', - backgroundColor: Colors.green, - colorText: Colors.white, - ); + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton( + onPressed: () { + setDialogState(() { + showAddNewPoint = false; + newPointDeVenteController.clear(); + }); + }, + child: const Text('Annuler'), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () async { + final nom = newPointDeVenteController + .text + .trim(); + if (nom.isNotEmpty) { + try { + final id = await _productDatabase + .getOrCreatePointDeVenteByNom( + nom); + if (id != null) { + setDialogState(() { + showAddNewPoint = false; + selectedPointDeVente = nom; + newPointDeVenteController + .clear(); + }); + // Recharger la liste + await loadPointsDeVente( + setDialogState); + Get.snackbar( + 'Succès', + 'Point de vente "$nom" créé avec succès', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } + } catch (e) { + Get.snackbar('Erreur', + 'Impossible de créer le point de vente: $e'); } - } catch (e) { - Get.snackbar('Erreur', 'Impossible de créer le point de vente: $e'); } - } - }, - icon: const Icon(Icons.save, size: 16), - label: const Text('Créer'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.teal, - foregroundColor: Colors.white, + }, + icon: const Icon(Icons.save, size: 16), + label: const Text('Créer'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, + foregroundColor: Colors.white, + ), ), - ), - ], - ), + ], + ), + ], ], - ], - ), - ], - ), - ), - 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 (MGA) *', - 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', - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.inventory), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), + ), + ], ), - ], - ), - const SizedBox(height: 16), - - // Catégorie avec gestion des valeurs non présentes - 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, - helperText: product.category != selectedCategory - ? 'Catégorie originale: ${product.category}' - : null, - ), - ), - 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 Référence (non modifiable) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.purple.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.purple.shade200), + 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(); + }); + }, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 16), + + // Prix et Stock sur la même ligne + Row( children: [ - Row( - children: [ - Icon(Icons.confirmation_number, color: Colors.purple.shade700), - const SizedBox(width: 8), - Text( - 'Référence du produit', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.purple.shade700, - ), + Expanded( + child: TextField( + controller: priceController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + decoration: InputDecoration( + labelText: 'Prix (MGA) *', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.attach_money), + filled: true, + fillColor: Colors.grey.shade50, ), - ], + ), ), - const SizedBox(height: 12), - TextField( - controller: referenceController, - decoration: const InputDecoration( - labelText: 'Référence', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.tag), - filled: true, - fillColor: Colors.white, - helperText: 'La référence peut être modifiée avec précaution', + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: stockController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Stock', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.inventory), + filled: true, + fillColor: Colors.grey.shade50, + ), ), - onChanged: (value) { - setDialogState(() { - updateQrPreview(); - }); - }, ), ], ), - ), - const SizedBox(height: 16), - - // Spécifications techniques - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.orange.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.orange.shade200), + const SizedBox(height: 16), + + // Catégorie avec gestion des valeurs non présentes + 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, + helperText: product.category != selectedCategory + ? 'Catégorie originale: ${product.category}' + : null, + ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.memory, color: Colors.orange.shade700), - const SizedBox(width: 8), - Text( - 'Spécifications techniques', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.orange.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - TextField( - controller: marqueController, - decoration: const InputDecoration( - labelText: 'Marque', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.branding_watermark), - filled: true, - fillColor: Colors.white, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: TextField( - controller: ramController, - decoration: const InputDecoration( - labelText: 'RAM', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.memory), - filled: true, - fillColor: Colors.white, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: memoireInterneController, - decoration: const InputDecoration( - labelText: 'Mémoire interne', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.storage), - filled: true, - fillColor: Colors.white, + 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 Référence (non modifiable) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.purple.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.purple.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.confirmation_number, + color: Colors.purple.shade700), + const SizedBox(width: 8), + Text( + 'Référence du produit', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.purple.shade700, ), ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: referenceController, + decoration: const InputDecoration( + labelText: 'Référence', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.tag), + filled: true, + fillColor: Colors.white, + helperText: + 'La référence peut être modifiée avec précaution', ), - ], - ), - const SizedBox(height: 8), - TextField( - controller: imeiController, - decoration: const InputDecoration( - labelText: 'IMEI (pour téléphones)', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.smartphone), - filled: true, - fillColor: Colors.white, + onChanged: (value) { + setDialogState(() { + updateQrPreview(); + }); + }, ), - ), - ], - ), - ), - 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', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.blue.shade700, + const SizedBox(height: 16), + + // Spécifications techniques + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.memory, color: Colors.orange.shade700), + const SizedBox(width: 8), + Text( + 'Spécifications techniques', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: marqueController, + decoration: const InputDecoration( + labelText: 'Marque', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.branding_watermark), + filled: true, + fillColor: Colors.white, ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: TextField( - controller: imageController, - decoration: const InputDecoration( - labelText: 'Chemin de l\'image', - border: OutlineInputBorder(), - isDense: true, + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: ramController, + decoration: const InputDecoration( + labelText: 'RAM', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.memory), + filled: true, + fillColor: Colors.white, + ), ), - 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(width: 12), + Expanded( + child: TextField( + controller: memoireInterneController, + decoration: const InputDecoration( + labelText: 'Mémoire interne', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.storage), + filled: true, + fillColor: Colors.white, + ), + ), ), - ), - ], - ), - const SizedBox(height: 12), - - // Aperçu de l'image - 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: pickedImage != null - ? Image.file(pickedImage!, fit: BoxFit.cover) - : (product.image != null && product.image!.isNotEmpty - ? Image.file( - File(product.image!), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.image, size: 50), - ) - : const Icon(Icons.image, size: 50)), + ], + ), + const SizedBox(height: 8), + TextField( + controller: imeiController, + decoration: const InputDecoration( + labelText: 'IMEI (pour téléphones)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.smartphone), + filled: true, + fillColor: Colors.white, ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 16), - - // Aperçu QR Code - if (qrPreviewData != null) + const SizedBox(height: 16), + + // Section Image Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.green.shade50, + color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.green.shade200), + border: Border.all(color: Colors.blue.shade200), ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(Icons.qr_code_2, color: Colors.green.shade700), + Icon(Icons.image, color: Colors.blue.shade700), const SizedBox(width: 8), Text( - 'Aperçu du QR Code', + 'Image du produit', style: TextStyle( fontWeight: FontWeight.w600, - color: Colors.green.shade700, + 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 Center( child: Container( - padding: const EdgeInsets.all(8), + height: 100, + width: 100, decoration: BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), ), - child: QrImageView( - data: qrPreviewData!, - version: QrVersions.auto, - size: 80, - backgroundColor: Colors.white, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: pickedImage != null + ? Image.file(pickedImage!, + fit: BoxFit.cover) + : (product.image != null && + product.image!.isNotEmpty + ? Image.file( + File(product.image!), + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + const Icon(Icons.image, + size: 50), + ) + : const Icon(Icons.image, size: 50)), ), ), ), - const SizedBox(height: 8), - Text( - 'Réf: ${referenceController.text.trim()}', - style: const TextStyle(fontSize: 10, color: Colors.grey), - ), ], ), ), - ], - ); - }, + 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: ${referenceController.text.trim()}', + 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; - final reference = referenceController.text.trim(); - - if (name.isEmpty || price <= 0) { - Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); - return; - } - - if (reference.isEmpty) { - Get.snackbar('Erreur', 'La référence est obligatoire'); - return; - } - - if (reference != product.reference) { - final existingProduct = await _productDatabase.getProductByReference(reference); - if (existingProduct != null && existingProduct.id != product.id) { - Get.snackbar('Erreur', 'Cette référence existe déjà pour un autre produit'); + 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; + final reference = referenceController.text.trim(); + + if (name.isEmpty || price <= 0) { + Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); return; } - } - final imei = imeiController.text.trim(); - if (imei.isNotEmpty && imei != product.imei) { - final existingProduct = await _productDatabase.getProductByIMEI(imei); - if (existingProduct != null && existingProduct.id != product.id) { - Get.snackbar('Erreur', 'Cet IMEI existe déjà pour un autre produit'); + if (reference.isEmpty) { + Get.snackbar('Erreur', 'La référence est obligatoire'); return; } - } - // Gérer le point de vente - int? pointDeVenteId; - String? finalPointDeVenteNom; - - if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { - finalPointDeVenteNom = newPointDeVenteController.text.trim(); - } else if (selectedPointDeVente != null && selectedPointDeVente != 'Aucun') { - finalPointDeVenteNom = selectedPointDeVente; - } - - if (finalPointDeVenteNom != null) { - pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(finalPointDeVenteNom); - } - // Si "Aucun" est sélectionné, pointDeVenteId reste null - - try { - final updatedProduct = Product( - id: product.id, - name: name, - price: price, - image: imageController.text.trim(), - category: selectedCategory, - description: descriptionController.text.trim(), - stock: stock, - qrCode: product.qrCode, - reference: reference, - marque: marqueController.text.trim().isNotEmpty ? marqueController.text.trim() : null, - ram: ramController.text.trim().isNotEmpty ? ramController.text.trim() : null, - memoireInterne: memoireInterneController.text.trim().isNotEmpty ? memoireInterneController.text.trim() : null, - imei: imei.isNotEmpty ? imei : null, - pointDeVenteId: pointDeVenteId, // Peut être null si "Aucun" - ); - - await _productDatabase.updateProduct(updatedProduct); - Get.back(); - Get.snackbar( - 'Succès', - 'Produit modifié avec succès!\nRéférence: $reference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 4), - icon: const Icon(Icons.check_circle, color: Colors.white), - ); - _loadProducts(); - _loadPointsDeVente(); - } catch (e) { - Get.snackbar('Erreur', 'Modification du produit échouée: $e'); - } - }, - icon: const Icon(Icons.save), - label: const Text('Sauvegarder les modifications'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + if (reference != product.reference) { + final existingProduct = + await _productDatabase.getProductByReference(reference); + if (existingProduct != null && + existingProduct.id != product.id) { + Get.snackbar('Erreur', + 'Cette référence existe déjà pour un autre produit'); + return; + } + } + + final imei = imeiController.text.trim(); + if (imei.isNotEmpty && imei != product.imei) { + final existingProduct = + await _productDatabase.getProductByIMEI(imei); + if (existingProduct != null && + existingProduct.id != product.id) { + Get.snackbar( + 'Erreur', 'Cet IMEI existe déjà pour un autre produit'); + return; + } + } + + // Gérer le point de vente + int? pointDeVenteId; + String? finalPointDeVenteNom; + + if (showAddNewPoint && + newPointDeVenteController.text.trim().isNotEmpty) { + finalPointDeVenteNom = newPointDeVenteController.text.trim(); + } else if (selectedPointDeVente != null && + selectedPointDeVente != 'Aucun') { + finalPointDeVenteNom = selectedPointDeVente; + } + + if (finalPointDeVenteNom != null) { + pointDeVenteId = await _productDatabase + .getOrCreatePointDeVenteByNom(finalPointDeVenteNom); + } + // Si "Aucun" est sélectionné, pointDeVenteId reste null + + try { + final updatedProduct = Product( + id: product.id, + name: name, + price: price, + image: imageController.text.trim(), + category: selectedCategory, + description: descriptionController.text.trim(), + stock: stock, + qrCode: product.qrCode, + reference: reference, + marque: marqueController.text.trim().isNotEmpty + ? marqueController.text.trim() + : null, + ram: ramController.text.trim().isNotEmpty + ? ramController.text.trim() + : null, + memoireInterne: + memoireInterneController.text.trim().isNotEmpty + ? memoireInterneController.text.trim() + : null, + imei: imei.isNotEmpty ? imei : null, + pointDeVenteId: pointDeVenteId, // Peut être null si "Aucun" + ); + + await _productDatabase.updateProduct(updatedProduct); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit modifié avec succès!\nRéférence: $reference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 4), + icon: const Icon(Icons.check_circle, color: Colors.white), + ); + _loadProducts(); + _loadPointsDeVente(); + } catch (e) { + Get.snackbar('Erreur', 'Modification du produit échouée: $e'); + } + }, + icon: const Icon(Icons.save), + label: const Text('Sauvegarder les modifications'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), ), - ), - ], - ), - ); -} + ], + ), + ); + } void _deleteProduct(Product product) { Get.dialog( @@ -3579,7 +3747,7 @@ Widget _buildImportProgressIndicator() { await _productDatabase.deleteProduct(product.id); Get.back(); Get.snackbar( - 'Succès', + 'Succès', 'Produit supprimé avec succès', backgroundColor: Colors.green, colorText: Colors.white, @@ -3590,438 +3758,462 @@ Widget _buildImportProgressIndicator() { 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)), + ), + ], + ), + ); + } + + Widget _buildProductCard(Product product) { + return FutureBuilder( + future: + _productDatabase.getPointDeVenteNomById(product.pointDeVenteId ?? 0), + builder: (context, snapshot) { + // Gestion des états du FutureBuilder + if (snapshot.connectionState == ConnectionState.waiting) { + return _buildProductCardContent(product, 'Chargement...'); + } + + if (snapshot.hasError) { + return _buildProductCardContent(product, 'Erreur de chargement'); + } + + final pointDeVente = snapshot.data ?? 'Non spécifié'; + return _buildProductCardContent(product, pointDeVente); + }, + ); + } + + void _showAddPointDeVenteDialog(Product product) { + final pointDeVenteController = TextEditingController(); + final _formKey = GlobalKey(); + + Get.dialog( + AlertDialog( + title: const Text('Ajouter un point de vente'), + content: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: pointDeVenteController, + decoration: const InputDecoration( + labelText: 'Nom du point de vente', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un nom'; + } + return null; + }, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + value: null, + hint: const Text('Ou sélectionner existant'), + items: _pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + pointDeVenteController.text = value; + } + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { + final nom = pointDeVenteController.text.trim(); + final id = + await _productDatabase.getOrCreatePointDeVenteByNom(nom); + + if (id != null) { + // Mettre à jour le produit avec le nouveau point de vente + final updatedProduct = Product( + id: product.id, + name: product.name, + price: product.price, + image: product.image, + category: product.category, + stock: product.stock, + description: product.description, + qrCode: product.qrCode, + reference: product.reference, + pointDeVenteId: id, + ); + + await _productDatabase.updateProduct(updatedProduct); + Get.back(); + Get.snackbar('Succès', 'Point de vente attribué', + backgroundColor: Colors.green); + _loadProducts(); // Rafraîchir la liste + } + } + }, + child: const Text('Enregistrer'), ), ], ), ); } - Widget _buildProductCard(Product product) { - return FutureBuilder( - future: _productDatabase.getPointDeVenteNomById(product.pointDeVenteId ?? 0), - builder: (context, snapshot) { - // Gestion des états du FutureBuilder - if (snapshot.connectionState == ConnectionState.waiting) { - return _buildProductCardContent(product, 'Chargement...'); - } - - if (snapshot.hasError) { - return _buildProductCardContent(product, 'Erreur de chargement'); - } - - final pointDeVente = snapshot.data ?? 'Non spécifié'; - return _buildProductCardContent(product, pointDeVente); - }, - ); -} - - -void _showAddPointDeVenteDialog(Product product) { - final pointDeVenteController = TextEditingController(); - final _formKey = GlobalKey(); - - Get.dialog( - AlertDialog( - title: const Text('Ajouter un point de vente'), - content: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: pointDeVenteController, - decoration: const InputDecoration( - labelText: 'Nom du point de vente', - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Veuillez entrer un nom'; - } - return null; - }, - ), - const SizedBox(height: 16), - DropdownButtonFormField( - value: null, - hint: const Text('Ou sélectionner existant'), - items: _pointsDeVente.map((point) { - return DropdownMenuItem( - value: point['nom'] as String, - child: Text(point['nom'] as String), - ); - }).toList(), - onChanged: (value) { - if (value != null) { - pointDeVenteController.text = value; - } - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton( - onPressed: () async { - if (_formKey.currentState!.validate()) { - final nom = pointDeVenteController.text.trim(); - final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); - - if (id != null) { - // Mettre à jour le produit avec le nouveau point de vente - final updatedProduct = Product( - id: product.id, - name: product.name, - price: product.price, - image: product.image, - category: product.category, - stock: product.stock, - description: product.description, - qrCode: product.qrCode, - reference: product.reference, - pointDeVenteId: id, - ); - - await _productDatabase.updateProduct(updatedProduct); - Get.back(); - Get.snackbar('Succès', 'Point de vente attribué', - backgroundColor: Colors.green); - _loadProducts(); // Rafraîchir la liste - } - } - }, - child: const Text('Enregistrer'), - ), - ], - ), - ); -} - void _showProductDetailsDialog(BuildContext context, Product product) { - Get.dialog( - Dialog( - insetPadding: const EdgeInsets.all(24), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.75, // Réduit de 0.9 à 0.75 - maxHeight: MediaQuery.of(context).size.height * 0.85, + Get.dialog( + Dialog( + insetPadding: const EdgeInsets.all(24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // En-tête moderne avec bouton fermer - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * + 0.75, // Réduit de 0.9 à 0.75 + maxHeight: MediaQuery.of(context).size.height * 0.85, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // En-tête moderne avec bouton fermer + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.shade100, - borderRadius: BorderRadius.circular(8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.shopping_bag, + color: Colors.blue.shade700, size: 20), ), - child: Icon(Icons.shopping_bag, - color: Colors.blue.shade700, size: 20), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - product.name, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey.shade800, + const SizedBox(width: 12), + Expanded( + child: Text( + product.name, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), ), ), - ), - IconButton( - onPressed: () => Get.back(), - icon: Icon(Icons.close, color: Colors.grey.shade600), - style: IconButton.styleFrom( - backgroundColor: Colors.white, - padding: const EdgeInsets.all(8), + IconButton( + onPressed: () => Get.back(), + icon: Icon(Icons.close, color: Colors.grey.shade600), + style: IconButton.styleFrom( + backgroundColor: Colors.white, + padding: const EdgeInsets.all(8), + ), ), - ), - ], + ], + ), ), - ), - // Contenu scrollable - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Image du produit avec ombre - Center( - child: Container( - width: 140, - height: 140, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: product.image != null && product.image!.isNotEmpty - ? Image.file( - File(product.image!), - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildPlaceholderImage(), - ) - : _buildPlaceholderImage(), + // Contenu scrollable + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image du produit avec ombre + Center( + child: Container( + width: 140, + height: 140, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: product.image != null && + product.image!.isNotEmpty + ? Image.file( + File(product.image!), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + _buildPlaceholderImage(), + ) + : _buildPlaceholderImage(), + ), ), ), - ), - const SizedBox(height: 24), - - // Informations principales avec design moderne - _buildModernInfoSection( - title: 'Informations générales', - icon: Icons.info_outline, - color: Colors.blue, - children: [ - _buildModernInfoRow('Prix', '${product.price} MGA', Icons.payments_outlined), - _buildModernInfoRow('Catégorie', product.category, Icons.category_outlined), - _buildModernInfoRow('Stock', '${product.stock}', Icons.inventory_2_outlined), - _buildModernInfoRow('Référence', product.reference ?? 'N/A', Icons.tag), - ], - ), - const SizedBox(height: 16), - - // Spécifications techniques - _buildModernInfoSection( - title: 'Spécifications techniques', - icon: Icons.settings_outlined, - color: Colors.purple, - children: [ - _buildModernInfoRow('Marque', product.marque ?? 'Non spécifiée', Icons.branding_watermark_outlined), - _buildModernInfoRow('RAM', product.ram ?? 'Non spécifiée', Icons.memory_outlined), - _buildModernInfoRow('Mémoire', product.memoireInterne ?? 'Non spécifiée', Icons.storage_outlined), - _buildModernInfoRow('IMEI', product.imei ?? 'Non spécifié', Icons.smartphone_outlined), - ], - ), + const SizedBox(height: 24), - // Description - if (product.description != null && product.description!.isNotEmpty) ...[ - const SizedBox(height: 16), + // Informations principales avec design moderne _buildModernInfoSection( - title: 'Description', - icon: Icons.description_outlined, - color: Colors.green, + title: 'Informations générales', + icon: Icons.info_outline, + color: Colors.blue, children: [ - Text( - product.description!, - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade700, - height: 1.4, - ), - ), + _buildModernInfoRow('Prix', '${product.price} MGA', + Icons.payments_outlined), + _buildModernInfoRow('Catégorie', product.category, + Icons.category_outlined), + _buildModernInfoRow('Stock', '${product.stock}', + Icons.inventory_2_outlined), + _buildModernInfoRow('Référence', + product.reference ?? 'N/A', Icons.tag), ], ), - ], - - // QR Code - if (product.qrCode != null && product.qrCode!.isNotEmpty) ...[ const SizedBox(height: 16), + + // Spécifications techniques _buildModernInfoSection( - title: 'QR Code', - icon: Icons.qr_code, - color: Colors.orange, + title: 'Spécifications techniques', + icon: Icons.settings_outlined, + color: Colors.purple, children: [ - Center( - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade200), + _buildModernInfoRow( + 'Marque', + product.marque ?? 'Non spécifiée', + Icons.branding_watermark_outlined), + _buildModernInfoRow( + 'RAM', + product.ram ?? 'Non spécifiée', + Icons.memory_outlined), + _buildModernInfoRow( + 'Mémoire', + product.memoireInterne ?? 'Non spécifiée', + Icons.storage_outlined), + _buildModernInfoRow( + 'IMEI', + product.imei ?? 'Non spécifié', + Icons.smartphone_outlined), + ], + ), + + // Description + if (product.description != null && + product.description!.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildModernInfoSection( + title: 'Description', + icon: Icons.description_outlined, + color: Colors.green, + children: [ + Text( + product.description!, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + height: 1.4, ), - child: QrImageView( - data: 'https://stock.guycom.mg/${product.reference}', - version: QrVersions.auto, - size: 80, + ), + ], + ), + ], + + // QR Code + if (product.qrCode != null && + product.qrCode!.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildModernInfoSection( + title: 'QR Code', + icon: Icons.qr_code, + color: Colors.orange, + children: [ + Center( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: Colors.grey.shade200), + ), + child: QrImageView( + data: + 'https://stock.guycom.mg/${product.reference}', + version: QrVersions.auto, + size: 80, + ), ), ), - ), - ], - ), + ], + ), + ], + const SizedBox(height: 8), ], - const SizedBox(height: 8), - ], + ), ), ), - ), - ], + ], + ), ), ), - ), - ); -} + ); + } -Widget _buildModernInfoSection({ - required String title, - required IconData icon, - required Color color, - required List children, -}) { - return Container( - width: double.infinity, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade200), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // En-tête de section - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), + Widget _buildModernInfoSection({ + required String title, + required IconData icon, + required Color color, + required List children, + }) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), ), - child: Row( - children: [ - Icon(icon, color: color, size: 18), - const SizedBox(width: 8), - Text( - title, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 15, - color: const Color.fromARGB(255, 8, 63, 108), - ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête de section + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), - ], + ), + child: Row( + children: [ + Icon(icon, color: color, size: 18), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + color: const Color.fromARGB(255, 8, 63, 108), + ), + ), + ], + ), ), - ), - // Contenu - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: children, + // Contenu + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), ), - ), - ], - ), - ); -} + ], + ), + ); + } -Widget _buildModernInfoRow(String label, String value, IconData icon) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(6), + Widget _buildModernInfoRow(String label, String value, IconData icon) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(6), + ), + child: Icon(icon, size: 16, color: Colors.grey.shade600), ), - child: Icon(icon, size: 16, color: Colors.grey.shade600), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade500, - fontWeight: FontWeight.w500, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + fontWeight: FontWeight.w500, + ), ), - ), - const SizedBox(height: 2), - Text( - value, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.grey.shade800, + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), ), - ), - ], - ), - ), - ], - ), - ); -} - -Widget _buildPlaceholderImage() { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Colors.grey.shade100, Colors.grey.shade200], - ), - borderRadius: BorderRadius.circular(16), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.image_outlined, size: 40, color: Colors.grey.shade400), - const SizedBox(height: 8), - Text( - 'Aucune image', - style: TextStyle( - color: Colors.grey.shade500, - fontSize: 12, - fontWeight: FontWeight.w500, + ], ), ), ], ), - ), - ); -} + ); + } + Widget _buildPlaceholderImage() { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.grey.shade100, Colors.grey.shade200], + ), + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.image_outlined, size: 40, color: Colors.grey.shade400), + const SizedBox(height: 8), + Text( + 'Aucune image', + style: TextStyle( + color: Colors.grey.shade500, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } // 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, @@ -4042,9 +4234,10 @@ Widget _buildPlaceholderImage() { 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 { @@ -4057,8 +4250,7 @@ Widget _buildPlaceholderImage() { return path; } - -@override + @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < 600; return Scaffold( @@ -4070,8 +4262,10 @@ Widget _buildPlaceholderImage() { // Nouveau bouton pour scanner et assigner FloatingActionButton( heroTag: 'assignBtn', - onPressed: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) - ? null + onPressed: (_isScanning || + _isAssigning || + _userController.pointDeVenteId <= 0) + ? null : _startPointDeVenteAssignmentScanning, mini: true, child: (_isScanning || _isAssigning) @@ -4084,12 +4278,14 @@ Widget _buildPlaceholderImage() { ), ) : Row( - children: [ - const Icon(Icons.qr_code), - ], - ), - backgroundColor: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) - ? Colors.grey + children: [ + const Icon(Icons.qr_code), + ], + ), + backgroundColor: (_isScanning || + _isAssigning || + _userController.pointDeVenteId <= 0) + ? Colors.grey : Colors.orange, foregroundColor: Colors.white, ), @@ -4106,7 +4302,6 @@ Widget _buildPlaceholderImage() { FloatingActionButton.extended( heroTag: 'addBtn', onPressed: _showAddProductDialog, - icon: const Icon(Icons.add), label: const Text('Ajouter'), backgroundColor: Colors.green, @@ -4124,9 +4319,9 @@ Widget _buildPlaceholderImage() { child: Column( children: [ // Card d'information sur l'attribution (desktop uniquement) - if (!isMobile && _userController.pointDeVenteId > 0) + if (!isMobile && _userController.pointDeVenteId > 0) _buildAssignmentScanCard(), - + // Barre de recherche Row( children: [ @@ -4154,8 +4349,10 @@ Widget _buildPlaceholderImage() { ), child: DropdownButton( value: _selectedCategory, - items: _categories.map((category) => - DropdownMenuItem(value: category, child: Text(category))).toList(), + items: _categories + .map((category) => DropdownMenuItem( + value: category, child: Text(category))) + .toList(), onChanged: (value) { setState(() { _selectedCategory = value!; @@ -4168,7 +4365,7 @@ Widget _buildPlaceholderImage() { ), ], ), - + // Boutons pour mobile if (isMobile) ...[ const SizedBox(height: 12), @@ -4187,10 +4384,16 @@ Widget _buildPlaceholderImage() { ), ) : const Icon(Icons.assignment), - label: Text((_isScanning || _isAssigning) ? 'Attribution...' : 'Assigner produits'), - onPressed: (_isScanning || _isAssigning) ? null : _startPointDeVenteAssignmentScanning, + label: Text((_isScanning || _isAssigning) + ? 'Attribution...' + : 'Assigner produits'), + onPressed: (_isScanning || _isAssigning) + ? null + : _startPointDeVenteAssignmentScanning, style: ElevatedButton.styleFrom( - backgroundColor: (_isScanning || _isAssigning) ? Colors.grey : Colors.orange.shade700, + backgroundColor: (_isScanning || _isAssigning) + ? Colors.grey + : Colors.orange.shade700, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( @@ -4202,9 +4405,9 @@ Widget _buildPlaceholderImage() { ], ), ], - + const SizedBox(height: 12), - + // Compteur de produits Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -4219,7 +4422,8 @@ Widget _buildPlaceholderImage() { ), if (_userController.pointDeVenteId > 0) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(12), @@ -4233,7 +4437,8 @@ Widget _buildPlaceholderImage() { ), ), ), - if (_searchController.text.isNotEmpty || _selectedCategory != 'Tous') + if (_searchController.text.isNotEmpty || + _selectedCategory != 'Tous') TextButton.icon( onPressed: () { setState(() { @@ -4270,9 +4475,9 @@ Widget _buildPlaceholderImage() { ), 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, @@ -4282,8 +4487,8 @@ Widget _buildPlaceholderImage() { 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, @@ -4307,9 +4512,4 @@ Widget _buildPlaceholderImage() { ), ); } - - } - - -