import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Components/newCommandComponents/CadeauDialog.dart'; import 'package:youmazgestion/Components/newCommandComponents/RemiseDialog.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/produit.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; class NouvelleCommandePage extends StatefulWidget { const NouvelleCommandePage({super.key}); @override _NouvelleCommandePageState createState() => _NouvelleCommandePageState(); } class _NouvelleCommandePageState extends State { final AppDatabase _appDatabase = AppDatabase.instance; final _formKey = GlobalKey(); bool _isLoading = false; // Contrôleurs client final TextEditingController _nomController = TextEditingController(); final TextEditingController _prenomController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _telephoneController = TextEditingController(); final TextEditingController _adresseController = TextEditingController(); // Contrôleurs pour les filtres final TextEditingController _searchNameController = TextEditingController(); final TextEditingController _searchImeiController = TextEditingController(); final TextEditingController _searchReferenceController = TextEditingController(); // Panier final List _products = []; final List _filteredProducts = []; final Map _quantites = {}; final Map _panierDetails = {}; // Variables de filtre bool _showOnlyInStock = false; // Utilisateurs commerciaux List _commercialUsers = []; Users? _selectedCommercialUser; // Variables pour les suggestions clients bool _showNomSuggestions = false; bool _showTelephoneSuggestions = false; // Variables pour le scanner (identiques à ProductManagementPage) QRViewController? _qrController; bool _isScanning = false; final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); @override void initState() { super.initState(); _loadProducts(); _loadCommercialUsers(); // Listeners pour les filtres _searchNameController.addListener(_filterProducts); _searchImeiController.addListener(_filterProducts); _searchReferenceController.addListener(_filterProducts); // Listeners pour l'autocomplétion client _nomController.addListener(() { if (_nomController.text.length >= 3) { _showClientSuggestions(_nomController.text, isNom: true); } else { _hideNomSuggestions(); } }); _telephoneController.addListener(() { if (_telephoneController.text.length >= 3) { _showClientSuggestions(_telephoneController.text, isNom: false); } else { _hideTelephoneSuggestions(); } }); } // ==Gestion des remise // 3. Ajouter ces méthodes pour gérer les remises Future _showRemiseDialog(Product product) async { final detailExistant = _panierDetails[product.id!]; final result = await showDialog( context: context, builder: (context) => RemiseDialog( product: product, quantite: detailExistant?.quantite ?? 1, prixUnitaire: product.price, detailExistant: detailExistant, ), ); if (result != null) { if (result == 'supprimer') { _supprimerRemise(product.id!); } else if (result is Map) { _appliquerRemise(product.id!, result); } } } void _appliquerRemise(int productId, Map remiseData) { final detailExistant = _panierDetails[productId]; if (detailExistant == null) return; final detailAvecRemise = detailExistant.appliquerRemise( type: remiseData['type'] as RemiseType, valeur: remiseData['valeur'] as double, ); setState(() { _panierDetails[productId] = detailAvecRemise; }); Get.snackbar( 'Remise appliquée', 'Remise de ${detailAvecRemise.remiseDescription} appliquée', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange.shade600, colorText: Colors.white, duration: const Duration(seconds: 2), ); } void _supprimerRemise(int productId) { final detailExistant = _panierDetails[productId]; if (detailExistant == null) return; setState(() { _panierDetails[productId] = detailExistant.supprimerRemise(); }); Get.snackbar( 'Remise supprimée', 'La remise a été supprimée', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.blue.shade600, colorText: Colors.white, duration: const Duration(seconds: 2), ); } // Ajout des produits au pannier // 4. Modifier la méthode pour ajouter des produits au panier void _ajouterAuPanier(Product product, int quantite) { // Vérifier le stock disponible if (product.stock != null && quantite > product.stock!) { Get.snackbar( 'Stock insuffisant', 'Quantité demandée non disponible', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); return; } setState(() { final detail = DetailCommande.sansRemise( commandeId: 0, // Sera défini lors de la création produitId: product.id!, quantite: quantite, prixUnitaire: product.price, produitNom: product.name, produitReference: product.reference, ); _panierDetails[product.id!] = detail; }); } // Modification de la méthode _modifierQuantite pour gérer les cadeaux void _modifierQuantite(int productId, int nouvelleQuantite) { final detailExistant = _panierDetails[productId]; if (detailExistant == null) return; if (nouvelleQuantite <= 0) { setState(() { _panierDetails.remove(productId); }); return; } final product = _products.firstWhere((p) => p.id == productId); if (product.stock != null && nouvelleQuantite > product.stock!) { Get.snackbar( 'Stock insuffisant', 'Quantité maximum: ${product.stock}', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange, colorText: Colors.white, ); return; } final nouveauSousTotal = nouvelleQuantite * detailExistant.prixUnitaire; setState(() { if (detailExistant.estCadeau) { // Pour un cadeau, le prix final reste à 0 _panierDetails[productId] = DetailCommande( id: detailExistant.id, commandeId: detailExistant.commandeId, produitId: detailExistant.produitId, quantite: nouvelleQuantite, prixUnitaire: detailExistant.prixUnitaire, sousTotal: nouveauSousTotal, prixFinal: 0.0, // Prix final à 0 pour un cadeau estCadeau: true, produitNom: detailExistant.produitNom, produitReference: detailExistant.produitReference, ); } else if (detailExistant.aRemise) { // Recalculer la remise si elle existe final detail = DetailCommande( id: detailExistant.id, commandeId: detailExistant.commandeId, produitId: detailExistant.produitId, quantite: nouvelleQuantite, prixUnitaire: detailExistant.prixUnitaire, sousTotal: nouveauSousTotal, prixFinal: nouveauSousTotal, produitNom: detailExistant.produitNom, produitReference: detailExistant.produitReference, ).appliquerRemise( type: detailExistant.remiseType!, valeur: detailExistant.remiseValeur, ); _panierDetails[productId] = detail; } else { // Article normal sans remise _panierDetails[productId] = DetailCommande( id: detailExistant.id, commandeId: detailExistant.commandeId, produitId: detailExistant.produitId, quantite: nouvelleQuantite, prixUnitaire: detailExistant.prixUnitaire, sousTotal: nouveauSousTotal, prixFinal: nouveauSousTotal, produitNom: detailExistant.produitNom, produitReference: detailExistant.produitReference, ); } }); } // === NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE (identiques à ProductManagementPage) === void _startAutomaticScanning() { if (_isScanning) return; setState(() { _isScanning = true; }); Get.to(() => _buildAutomaticScannerPage())?.then((_) { setState(() { _isScanning = false; }); }); } Widget _buildAutomaticScannerPage() { return Scaffold( appBar: AppBar( title: const Text('Scanner Produit'), backgroundColor: Colors.green.shade700, foregroundColor: Colors.white, leading: IconButton( icon: const Icon(Icons.close), onPressed: () { _qrController?.dispose(); Get.back(); }, ), actions: [ IconButton( icon: const Icon(Icons.flash_on), onPressed: () async { await _qrController?.toggleFlash(); }, ), IconButton( icon: const Icon(Icons.flip_camera_ios), onPressed: () async { await _qrController?.flipCamera(); }, ), ], ), body: Stack( children: [ // Scanner view QRView( key: _qrKey, onQRViewCreated: _onAutomaticQRViewCreated, overlay: QrScannerOverlayShape( borderColor: Colors.green, borderRadius: 10, borderLength: 30, borderWidth: 10, cutOutSize: 250, ), ), // Instructions overlay Positioned( bottom: 100, left: 20, right: 20, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(12), ), child: const Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.qr_code_scanner, color: Colors.white, size: 40), SizedBox(height: 8), Text( 'Scanner automatiquement un produit', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), SizedBox(height: 4), Text( 'Pointez vers QR Code, IMEI ou code-barres', style: TextStyle( color: Colors.white70, fontSize: 14, ), textAlign: TextAlign.center, ), ], ), ), ), ], ), ); } void _onAutomaticQRViewCreated(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 avec identification automatique _processScannedData(scanData.code!); } }); } Future _processScannedData(String scannedData) async { try { // Montrer un indicateur de chargement Get.dialog( AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: Colors.green.shade700), const SizedBox(height: 16), const Text('Identification du produit...'), const SizedBox(height: 8), Text( 'Code: $scannedData', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, fontFamily: 'monospace', ), ), ], ), ), barrierDismissible: false, ); // Attendre un court instant pour l'effet visuel await Future.delayed(const Duration(milliseconds: 300)); // Recherche automatique du produit par différents critères Product? foundProduct = await _findProductAutomatically(scannedData); // Fermer l'indicateur de chargement Get.back(); if (foundProduct == null) { _showProductNotFoundDialog(scannedData); return; } // Vérifier le stock if (foundProduct.stock != null && foundProduct.stock! <= 0) { Get.snackbar( 'Stock insuffisant', 'Le produit "${foundProduct.name}" n\'est plus en stock', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange.shade600, colorText: Colors.white, duration: const Duration(seconds: 3), icon: const Icon(Icons.warning_amber, color: Colors.white), ); return; } final detailExistant = _panierDetails[foundProduct!.id!]; // Vérifier si le produit peut être ajouté (stock disponible) final currentQuantity = _quantites[foundProduct.id] ?? 0; if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) { Get.snackbar( 'Stock limite atteint', 'Quantité maximum atteinte pour "${foundProduct.name}"', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange.shade600, colorText: Colors.white, duration: const Duration(seconds: 3), icon: const Icon(Icons.warning_amber, color: Colors.white), ); return; } // Ajouter le produit au panier _modifierQuantite(foundProduct.id!, currentQuantity + 1); // Afficher le dialogue de succès _showProductFoundAndAddedDialog(foundProduct, currentQuantity + 1); } 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()}', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red.shade600, colorText: Colors.white, duration: const Duration(seconds: 3), ); } } Future _findProductAutomatically(String scannedData) async { // Nettoyer les données scannées final cleanedData = scannedData.trim(); // 1. Essayer de trouver par IMEI exact for (var product in _products) { if (product.imei?.toLowerCase().trim() == cleanedData.toLowerCase()) { return product; } } // 2. Essayer de trouver par référence exacte for (var product in _products) { if (product.reference?.toLowerCase().trim() == cleanedData.toLowerCase()) { return product; } } // 3. Si c'est une URL QR code, extraire la référence if (cleanedData.contains('stock.guycom.mg/')) { final reference = cleanedData.split('/').last; for (var product in _products) { if (product.reference?.toLowerCase().trim() == reference.toLowerCase()) { return product; } } } // 4. Recherche par correspondance partielle dans le nom for (var product in _products) { if (product.name.toLowerCase().contains(cleanedData.toLowerCase()) && cleanedData.length >= 3) { return product; } } // 5. Utiliser la base de données pour une recherche plus approfondie try { // Recherche par IMEI dans la base final productByImei = await _appDatabase.getProductByIMEI(cleanedData); if (productByImei != null) { return productByImei; } // Recherche par référence dans la base final productByRef = await _appDatabase.getProductByReference(cleanedData); if (productByRef != null) { return productByRef; } } catch (e) { print('Erreur recherche base de données: $e'); } return null; } void _showProductFoundAndAddedDialog(Product product, int newQuantity) { 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.check_circle, color: Colors.green.shade700), ), const SizedBox(width: 12), const Expanded(child: Text('Produit identifié et ajouté !')), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), if (product.imei != null && product.imei!.isNotEmpty) Text('IMEI: ${product.imei}'), if (product.reference != null && product.reference!.isNotEmpty) Text('Référence: ${product.reference}'), Text('Prix: ${product.price.toStringAsFixed(2)} MGA'), Text('Quantité dans le panier: $newQuantity'), if (product.stock != null) Text('Stock restant: ${product.stock! - newQuantity}'), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.auto_awesome, color: Colors.green.shade700, size: 16), const SizedBox(width: 8), const Expanded( child: Text( 'Produit identifié automatiquement', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Continuer'), ), ElevatedButton( onPressed: () { Get.back(); _showCartBottomSheet(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.green.shade700, foregroundColor: Colors.white, ), child: const Text('Voir le panier'), ), ElevatedButton( onPressed: () { Get.back(); _startAutomaticScanning(); // Scanner un autre produit }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade700, foregroundColor: Colors.white, ), child: const Text('Scanner encore'), ), ], ), ); } void _showProductNotFoundDialog(String scannedData) { Get.dialog( AlertDialog( title: Row( children: [ Icon(Icons.search_off, color: Colors.red.shade600), const SizedBox(width: 8), const Text('Produit non trouvé'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Aucun produit trouvé avec ce code:'), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(4), ), child: Text( scannedData, style: const TextStyle( fontFamily: 'monospace', fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 12), Text( 'Vérifiez que le code est correct ou que le produit existe dans la base de données.', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Types de codes supportés:', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.blue.shade700, ), ), const SizedBox(height: 4), Text( '• QR Code produit\n• IMEI (téléphones)\n• Référence produit\n• Code-barres', style: TextStyle( fontSize: 11, color: Colors.blue.shade600, ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Fermer'), ), ElevatedButton( onPressed: () { Get.back(); _startAutomaticScanning(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.green.shade700, foregroundColor: Colors.white, ), child: const Text('Scanner à nouveau'), ), ], ), ); } Widget _buildAutoScanInfoCard() { return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.auto_awesome, color: Colors.green.shade700, size: 20, ), ), const SizedBox(width: 12), const Expanded( child: Text( 'Scanner automatiquement: QR Code, IMEI, Référence ou code-barres', style: TextStyle( fontSize: 14, color: Color.fromARGB(255, 9, 56, 95), ), ), ), ElevatedButton.icon( onPressed: _isScanning ? null : _startAutomaticScanning, icon: _isScanning ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.qr_code_scanner, size: 18), label: Text(_isScanning ? 'Scan...' : 'Scanner'), style: ElevatedButton.styleFrom( backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ], ), ), ); } // === FIN DES NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE === // 8. Modifier _clearFormAndCart pour vider le nouveau panier void _clearFormAndCart() { setState(() { // Vider les contrôleurs client _nomController.clear(); _prenomController.clear(); _emailController.clear(); _telephoneController.clear(); _adresseController.clear(); // Vider le nouveau panier _panierDetails.clear(); // Réinitialiser le commercial au premier de la liste if (_commercialUsers.isNotEmpty) { _selectedCommercialUser = _commercialUsers.first; } // Masquer toutes les suggestions _hideAllSuggestions(); // Réinitialiser l'état de chargement _isLoading = false; }); } Future _showClientSuggestions(String query, {required bool isNom}) async { if (query.length < 3) { _hideAllSuggestions(); return; } setState(() { if (isNom) { _showNomSuggestions = true; _showTelephoneSuggestions = false; } else { _showTelephoneSuggestions = true; _showNomSuggestions = false; } }); } void _hideNomSuggestions() { if (mounted && _showNomSuggestions) { setState(() { _showNomSuggestions = false; }); } } void _hideTelephoneSuggestions() { if (mounted && _showTelephoneSuggestions){ setState(() { _showTelephoneSuggestions = false; }); } } void _hideAllSuggestions() { _hideNomSuggestions(); _hideTelephoneSuggestions(); } Future _loadProducts() async { final products = await _appDatabase.getProducts(); setState(() { _products.clear(); _products.addAll(products); _filteredProducts.clear(); _filteredProducts.addAll(products); }); } Future _loadCommercialUsers() async { final commercialUsers = await _appDatabase.getCommercialUsers(); setState(() { _commercialUsers = commercialUsers; if (_commercialUsers.isNotEmpty) { _selectedCommercialUser = _commercialUsers.first; } }); } void _filterProducts() { final nameQuery = _searchNameController.text.toLowerCase(); final imeiQuery = _searchImeiController.text.toLowerCase(); final referenceQuery = _searchReferenceController.text.toLowerCase(); setState(() { _filteredProducts.clear(); for (var product in _products) { bool matchesName = nameQuery.isEmpty || product.name.toLowerCase().contains(nameQuery); bool matchesImei = imeiQuery.isEmpty || (product.imei?.toLowerCase().contains(imeiQuery) ?? false); bool matchesReference = referenceQuery.isEmpty || (product.reference?.toLowerCase().contains(referenceQuery) ?? false); bool matchesStock = !_showOnlyInStock || (product.stock != null && product.stock! > 0); if (matchesName && matchesImei && matchesReference && matchesStock) { _filteredProducts.add(product); } } }); } void _toggleStockFilter() { setState(() { _showOnlyInStock = !_showOnlyInStock; }); _filterProducts(); } void _clearFilters() { setState(() { _searchNameController.clear(); _searchImeiController.clear(); _searchReferenceController.clear(); _showOnlyInStock = false; }); _filterProducts(); } // Section des filtres adaptée pour mobile Widget _buildFilterSection() { final isMobile = MediaQuery.of(context).size.width < 600; return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.filter_list, color: Colors.blue.shade700), const SizedBox(width: 8), const Text( 'Filtres de recherche', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 9, 56, 95), ), ), const Spacer(), TextButton.icon( onPressed: _clearFilters, icon: const Icon(Icons.clear, size: 18), label: isMobile ? const SizedBox() : const Text('Réinitialiser'), style: TextButton.styleFrom( foregroundColor: Colors.grey.shade600, ), ), ], ), const SizedBox(height: 16), // Champ de recherche par nom TextField( controller: _searchNameController, decoration: InputDecoration( labelText: 'Rechercher par nom', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 12), if (!isMobile) ...[ // Version desktop - champs sur la même ligne Row( children: [ Expanded( child: TextField( controller: _searchImeiController, decoration: InputDecoration( labelText: 'IMEI', prefixIcon: const Icon(Icons.phone_android), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: _searchReferenceController, decoration: InputDecoration( labelText: 'Référence', prefixIcon: const Icon(Icons.qr_code), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), ), ], ), ] else ...[ // Version mobile - champs empilés TextField( controller: _searchImeiController, decoration: InputDecoration( labelText: 'IMEI', prefixIcon: const Icon(Icons.phone_android), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 12), TextField( controller: _searchReferenceController, decoration: InputDecoration( labelText: 'Référence', prefixIcon: const Icon(Icons.qr_code), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), ], const SizedBox(height: 16), // Boutons de filtre adaptés pour mobile Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton.icon( onPressed: _toggleStockFilter, icon: Icon( _showOnlyInStock ? Icons.inventory : Icons.inventory_2, size: 20, ), label: Text(_showOnlyInStock ? isMobile ? 'Tous' : 'Afficher tous' : isMobile ? 'En stock' : 'Stock disponible'), style: ElevatedButton.styleFrom( backgroundColor: _showOnlyInStock ? Colors.green.shade600 : Colors.blue.shade600, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( horizontal: isMobile ? 12 : 16, vertical: 8 ), ), ), ], ), const SizedBox(height: 8), // Compteur de résultats Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8 ), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(20), ), child: Text( '${_filteredProducts.length} produit(s)', style: TextStyle( color: Colors.blue.shade700, fontWeight: FontWeight.w600, fontSize: isMobile ? 12 : 14, ), ), ), ], ), ), ); } Widget _buildFloatingCartButton() { final isMobile = MediaQuery.of(context).size.width < 600; final cartItemCount = _quantites.values.where((q) => q > 0).length; return FloatingActionButton.extended( onPressed: () { _showCartBottomSheet(); }, icon: const Icon(Icons.shopping_cart), label: Text( isMobile ? 'Panier ($cartItemCount)' : 'Panier ($cartItemCount)', style: TextStyle(fontSize: isMobile ? 12 : 14), ), backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, ); } void _showClientFormDialog() { final isMobile = MediaQuery.of(context).size.width < 600; // Variables locales pour les suggestions dans le dialog bool showNomSuggestions = false; bool showPrenomSuggestions = false; bool showEmailSuggestions = false; bool showTelephoneSuggestions = false; List localClientSuggestions = []; // GlobalKeys pour positionner les overlays final GlobalKey nomFieldKey = GlobalKey(); final GlobalKey prenomFieldKey = GlobalKey(); final GlobalKey emailFieldKey = GlobalKey(); final GlobalKey telephoneFieldKey = GlobalKey(); Get.dialog( StatefulBuilder( builder: (context, setDialogState) { return Stack( children: [ AlertDialog( title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.person_add, color: Colors.blue.shade700), ), const SizedBox(width: 12), Expanded( child: Text( isMobile ? 'Client' : 'Informations Client', style: TextStyle(fontSize: isMobile ? 16 : 18), ), ), ], ), content: Container( width: isMobile ? double.maxFinite : 600, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), child: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Champ Nom avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: nomFieldKey, controller: _nomController, label: 'Nom', validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null, onChanged: (value) async { if (value.length >= 2) { final suggestions = await _appDatabase.suggestClients(value); setDialogState(() { localClientSuggestions = suggestions; showNomSuggestions = suggestions.isNotEmpty; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; }); } else { setDialogState(() { showNomSuggestions = false; localClientSuggestions = []; }); } }, ), const SizedBox(height: 12), // Champ Prénom avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: prenomFieldKey, controller: _prenomController, label: 'Prénom', validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null, onChanged: (value) async { if (value.length >= 2) { final suggestions = await _appDatabase.suggestClients(value); setDialogState(() { localClientSuggestions = suggestions; showPrenomSuggestions = suggestions.isNotEmpty; showNomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; }); } else { setDialogState(() { showPrenomSuggestions = false; localClientSuggestions = []; }); } }, ), const SizedBox(height: 12), // Champ Email avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: emailFieldKey, controller: _emailController, label: 'Email', keyboardType: TextInputType.emailAddress, validator: (value) { if (value?.isEmpty ?? true) return 'Veuillez entrer un email'; if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) { return 'Email invalide'; } return null; }, onChanged: (value) async { if (value.length >= 3) { final suggestions = await _appDatabase.suggestClients(value); setDialogState(() { localClientSuggestions = suggestions; showEmailSuggestions = suggestions.isNotEmpty; showNomSuggestions = false; showPrenomSuggestions = false; showTelephoneSuggestions = false; }); } else { setDialogState(() { showEmailSuggestions = false; localClientSuggestions = []; }); } }, ), const SizedBox(height: 12), // Champ Téléphone avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: telephoneFieldKey, controller: _telephoneController, label: 'Téléphone', keyboardType: TextInputType.phone, validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null, onChanged: (value) async { if (value.length >= 3) { final suggestions = await _appDatabase.suggestClients(value); setDialogState(() { localClientSuggestions = suggestions; showTelephoneSuggestions = suggestions.isNotEmpty; showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; }); } else { setDialogState(() { showTelephoneSuggestions = false; localClientSuggestions = []; }); } }, ), const SizedBox(height: 12), _buildTextFormField( controller: _adresseController, label: 'Adresse', maxLines: 2, validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null, ), const SizedBox(height: 12), _buildCommercialDropdown(), ], ), ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( horizontal: isMobile ? 16 : 20, vertical: isMobile ? 10 : 12 ), ), onPressed: () { if (_formKey.currentState!.validate()) { // Fermer toutes les suggestions avant de soumettre setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); Get.back(); _submitOrder(); } }, child: Text( isMobile ? 'Valider' : 'Valider la commande', style: TextStyle(fontSize: isMobile ? 12 : 14), ), ), ], ), // Overlay pour les suggestions du nom if (showNomSuggestions) _buildSuggestionOverlay( fieldKey: nomFieldKey, suggestions: localClientSuggestions, onClientSelected: (client) { _fillFormWithClient(client); setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); }, onDismiss: () { setDialogState(() { showNomSuggestions = false; localClientSuggestions = []; }); }, ), // Overlay pour les suggestions du prénom if (showPrenomSuggestions) _buildSuggestionOverlay( fieldKey: prenomFieldKey, suggestions: localClientSuggestions, onClientSelected: (client) { _fillFormWithClient(client); setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); }, onDismiss: () { setDialogState(() { showPrenomSuggestions = false; localClientSuggestions = []; }); }, ), // Overlay pour les suggestions de l'email if (showEmailSuggestions) _buildSuggestionOverlay( fieldKey: emailFieldKey, suggestions: localClientSuggestions, onClientSelected: (client) { _fillFormWithClient(client); setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); }, onDismiss: () { setDialogState(() { showEmailSuggestions = false; localClientSuggestions = []; }); }, ), // Overlay pour les suggestions du téléphone if (showTelephoneSuggestions) _buildSuggestionOverlay( fieldKey: telephoneFieldKey, suggestions: localClientSuggestions, onClientSelected: (client) { _fillFormWithClient(client); setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); }, onDismiss: () { setDialogState(() { showTelephoneSuggestions = false; localClientSuggestions = []; }); }, ), ], ); }, ), ); } // Widget pour créer un TextFormField avec une clé Widget _buildTextFormFieldWithKey({ required GlobalKey key, required TextEditingController controller, required String label, TextInputType? keyboardType, int maxLines = 1, String? Function(String?)? validator, void Function(String)? onChanged, }) { return Container( key: key, child: _buildTextFormField( controller: controller, label: label, keyboardType: keyboardType, maxLines: maxLines, validator: validator, onChanged: onChanged, ), ); } // Widget pour l'overlay des suggestions // Widget pour l'overlay des suggestions Widget _buildSuggestionOverlay({ required GlobalKey fieldKey, required List suggestions, required Function(Client) onClientSelected, required VoidCallback onDismiss, }) { return Positioned.fill( child: GestureDetector( onTap: onDismiss, child: Material( color: Colors.transparent, child: Builder( builder: (context) { // Obtenir la position du champ final RenderBox? renderBox = fieldKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) return const SizedBox(); final position = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; return Stack( children: [ Positioned( left: position.dx, top: position.dy + size.height + 4, width: size.width, child: GestureDetector( onTap: () {}, // Empêcher la fermeture au tap sur la liste child: Container( constraints: const BoxConstraints( maxHeight: 200, // Hauteur maximum pour la scrollabilité ), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Scrollbar( thumbVisibility: suggestions.length > 3, child: ListView.separated( padding: EdgeInsets.zero, shrinkWrap: true, itemCount: suggestions.length, separatorBuilder: (context, index) => Divider( height: 1, color: Colors.grey.shade200, ), itemBuilder: (context, index) { final client = suggestions[index]; return ListTile( dense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), leading: CircleAvatar( radius: 16, backgroundColor: Colors.blue.shade100, child: Icon( Icons.person, size: 16, color: Colors.blue.shade700, ), ), title: Text( '${client.nom} ${client.prenom}', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), subtitle: Text( '${client.telephone} • ${client.email}', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), onTap: () => onClientSelected(client), hoverColor: Colors.blue.shade50, ); }, ), ), ), ), ), ), ], ); }, ), ), ), ); } // Méthode pour remplir le formulaire avec les données du client void _fillFormWithClient(Client client) { _nomController.text = client.nom; _prenomController.text = client.prenom; _emailController.text = client.email; _telephoneController.text = client.telephone; _adresseController.text = client.adresse ?? ''; Get.snackbar( 'Client trouvé', 'Les informations ont été remplies automatiquement', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), ); } Widget _buildTextFormField({ required TextEditingController controller, required String label, TextInputType? keyboardType, String? Function(String?)? validator, int? maxLines, void Function(String)? onChanged, }) { return TextFormField( controller: controller, decoration: InputDecoration( labelText: label, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.white, ), keyboardType: keyboardType, validator: validator, maxLines: maxLines, onChanged: onChanged, ); } Widget _buildCommercialDropdown() { return DropdownButtonFormField( value: _selectedCommercialUser, decoration: InputDecoration( labelText: 'Commercial', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.white, ), items: _commercialUsers.map((Users user) { return DropdownMenuItem( value: user, child: Text('${user.name} ${user.lastName}'), ); }).toList(), onChanged: (Users? newValue) { setState(() { _selectedCommercialUser = newValue; }); }, validator: (value) => value == null ? 'Veuillez sélectionner un commercial' : null, ); } Widget _buildProductList() { final isMobile = MediaQuery.of(context).size.width < 600; return _filteredProducts.isEmpty ? _buildEmptyState() : ListView.builder( padding: const EdgeInsets.all(16.0), itemCount: _filteredProducts.length, itemBuilder: (context, index) { final product = _filteredProducts[index]; final quantity = _quantites[product.id] ?? 0; return _buildProductListItem(product, quantity, isMobile); }, ); } Widget _buildEmptyState() { return Center( child: Padding( padding: const EdgeInsets.all(32.0), child: Column( children: [ Icon( Icons.search_off, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( 'Aucun produit trouvé', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey.shade600, ), ), const SizedBox(height: 8), Text( 'Modifiez vos critères de recherche', style: TextStyle( fontSize: 14, color: Colors.grey.shade500, ), ), ], ), ), ); } // Modification de la méthode _buildProductListItem pour inclure le bouton cadeau Widget _buildProductListItem(Product product, int quantity, bool isMobile) { final bool isOutOfStock = product.stock != null && product.stock! <= 0; final detailPanier = _panierDetails[product.id!]; final int currentQuantity = detailPanier?.quantite ?? 0; return Card( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: isOutOfStock ? Border.all(color: Colors.red.shade200, width: 1.5) : detailPanier?.estCadeau == true ? Border.all(color: Colors.green.shade300, width: 2) : detailPanier?.aRemise == true ? Border.all(color: Colors.orange.shade300, width: 2) : null, ), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ Container( width: isMobile ? 40 : 50, height: isMobile ? 40 : 50, decoration: BoxDecoration( color: isOutOfStock ? Colors.red.shade50 : detailPanier?.estCadeau == true ? Colors.green.shade50 : detailPanier?.aRemise == true ? Colors.orange.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Icon( detailPanier?.estCadeau == true ? Icons.card_giftcard : detailPanier?.aRemise == true ? Icons.discount : Icons.shopping_bag, size: isMobile ? 20 : 24, color: isOutOfStock ? Colors.red : detailPanier?.estCadeau == true ? Colors.green.shade700 : detailPanier?.aRemise == true ? Colors.orange.shade700 : Colors.blue, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( product.name, style: TextStyle( fontWeight: FontWeight.bold, fontSize: isMobile ? 14 : 16, color: isOutOfStock ? Colors.red.shade700 : null, ), ), ), if (detailPanier?.estCadeau == true) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(10), ), child: Text( 'CADEAU', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.green.shade700, ), ), ), ], ), const SizedBox(height: 4), Row( children: [ if (detailPanier?.estCadeau == true) ...[ Text( 'Gratuit', style: TextStyle( color: Colors.green.shade700, fontWeight: FontWeight.bold, fontSize: isMobile ? 12 : 14, ), ), const SizedBox(width: 8), Text( '${product.price.toStringAsFixed(2)} MGA', style: TextStyle( color: Colors.grey.shade500, fontWeight: FontWeight.w600, fontSize: isMobile ? 11 : 13, decoration: TextDecoration.lineThrough, ), ), ] else ...[ Text( '${product.price.toStringAsFixed(2)} MGA', style: TextStyle( color: Colors.green.shade700, fontWeight: FontWeight.w600, fontSize: isMobile ? 12 : 14, decoration: detailPanier?.aRemise == true ? TextDecoration.lineThrough : null, ), ), if (detailPanier?.aRemise == true) ...[ const SizedBox(width: 8), Text( '${(detailPanier!.prixFinal / detailPanier.quantite).toStringAsFixed(2)} MGA', style: TextStyle( color: Colors.orange.shade700, fontWeight: FontWeight.bold, fontSize: isMobile ? 12 : 14, ), ), ], ], ], ), if (detailPanier?.aRemise == true && !detailPanier!.estCadeau) Text( 'Remise: ${detailPanier!.remiseDescription}', style: TextStyle( fontSize: isMobile ? 10 : 12, color: Colors.orange.shade600, fontWeight: FontWeight.w500, ), ), if (product.stock != null) Text( 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}', style: TextStyle( fontSize: isMobile ? 10 : 12, color: isOutOfStock ? Colors.red.shade600 : Colors.grey.shade600, fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal, ), ), // Affichage IMEI et Référence if (product.imei != null && product.imei!.isNotEmpty) Text( 'IMEI: ${product.imei}', style: TextStyle( fontSize: isMobile ? 9 : 11, color: Colors.grey.shade600, fontFamily: 'monospace', ), ), if (product.reference != null && product.reference!.isNotEmpty) Text( 'Réf: ${product.reference}', style: TextStyle( fontSize: isMobile ? 9 : 11, color: Colors.grey.shade600, ), ), ], ), ), // Actions (quantité, remise et cadeau) Column( children: [ // Boutons d'actions (seulement si le produit est dans le panier) if (currentQuantity > 0) ...[ Row( mainAxisSize: MainAxisSize.min, children: [ // Bouton cadeau Container( margin: const EdgeInsets.only(right: 4), child: IconButton( icon: Icon( detailPanier?.estCadeau == true ? Icons.card_giftcard : Icons.card_giftcard_outlined, size: isMobile ? 16 : 18, color: detailPanier?.estCadeau == true ? Colors.green.shade700 : Colors.grey.shade600, ), onPressed: isOutOfStock ? null : () => _basculerStatutCadeau(product.id!), tooltip: detailPanier?.estCadeau == true ? 'Retirer le statut cadeau' : 'Marquer comme cadeau', style: IconButton.styleFrom( backgroundColor: detailPanier?.estCadeau == true ? Colors.green.shade100 : Colors.grey.shade100, minimumSize: Size(isMobile ? 32 : 36, isMobile ? 32 : 36), ), ), ), // Bouton remise (seulement pour les articles non-cadeaux) if (!detailPanier!.estCadeau) Container( margin: const EdgeInsets.only(right: 4), child: IconButton( icon: Icon( detailPanier.aRemise ? Icons.discount : Icons.local_offer, size: isMobile ? 16 : 18, color: detailPanier.aRemise ? Colors.orange.shade700 : Colors.grey.shade600, ), onPressed: isOutOfStock ? null : () => _showRemiseDialog(product), tooltip: detailPanier.aRemise ? 'Modifier la remise' : 'Ajouter une remise', style: IconButton.styleFrom( backgroundColor: detailPanier.aRemise ? Colors.orange.shade100 : Colors.grey.shade100, minimumSize: Size(isMobile ? 32 : 36, isMobile ? 32 : 36), ), ), ), // Bouton pour ajouter un cadeau à un autre produit Container( margin: const EdgeInsets.only(left: 4), child: IconButton( icon: Icon( Icons.add_circle_outline, size: isMobile ? 16 : 18, color: Colors.green.shade600, ), onPressed: isOutOfStock ? null : () => _showCadeauDialog(product), tooltip: 'Ajouter un cadeau', style: IconButton.styleFrom( backgroundColor: Colors.green.shade50, minimumSize: Size(isMobile ? 32 : 36, isMobile ? 32 : 36), ), ), ), ], ), const SizedBox(height: 8), ], // Contrôles de quantité Container( decoration: BoxDecoration( color: isOutOfStock ? Colors.grey.shade100 : detailPanier?.estCadeau == true ? Colors.green.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Icon( Icons.remove, size: isMobile ? 16 : 18 ), onPressed: isOutOfStock ? null : () { if (currentQuantity > 0) { _modifierQuantite(product.id!, currentQuantity - 1); } }, ), Text( currentQuantity.toString(), style: TextStyle( fontWeight: FontWeight.bold, fontSize: isMobile ? 12 : 14, ), ), IconButton( icon: Icon( Icons.add, size: isMobile ? 16 : 18 ), onPressed: isOutOfStock ? null : () { if (product.stock == null || currentQuantity < product.stock!) { if (currentQuantity == 0) { _ajouterAuPanier(product, 1); } else { _modifierQuantite(product.id!, currentQuantity + 1); } } else { Get.snackbar( 'Stock insuffisant', 'Quantité demandée non disponible', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); } }, ), ], ), ), ], ), ], ), ), ), ); } void _showCartBottomSheet() { final isMobile = MediaQuery.of(context).size.width < 600; Get.bottomSheet( Container( height: MediaQuery.of(context).size.height * (isMobile ? 0.85 : 0.7), padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Votre Panier', style: TextStyle( fontSize: isMobile ? 18 : 20, fontWeight: FontWeight.bold ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Get.back(), ), ], ), const Divider(), Expanded(child: _buildCartItemsList()), const Divider(), _buildCartTotalSection(), const SizedBox(height: 16), _buildSubmitButton(), ], ), ), isScrollControlled: true, ); } // 6. Modifier _buildCartItemsList pour afficher les remises Widget _buildCartItemsList() { final itemsInCart = _panierDetails.entries.where((e) => e.value.quantite > 0).toList(); if (itemsInCart.isEmpty) { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.shopping_cart_outlined, size: 60, color: Colors.grey), SizedBox(height: 16), Text( 'Votre panier est vide', style: TextStyle(fontSize: 16, color: Colors.grey), ), ], ), ); } return ListView.builder( itemCount: itemsInCart.length, itemBuilder: (context, index) { final entry = itemsInCart[index]; final detail = entry.value; final product = _products.firstWhere((p) => p.id == entry.key); return Dismissible( key: Key(entry.key.toString()), background: Container( color: Colors.red.shade100, alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), child: const Icon(Icons.delete, color: Colors.red), ), direction: DismissDirection.endToStart, onDismissed: (direction) { setState(() { _panierDetails.remove(entry.key); }); Get.snackbar( 'Produit retiré', '${product.name} a été retiré du panier', snackPosition: SnackPosition.BOTTOM, ); }, child: Card( margin: const EdgeInsets.only(bottom: 8), elevation: 1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: detail.estCadeau ? BorderSide(color: Colors.green.shade300, width: 1.5) : detail.aRemise ? BorderSide(color: Colors.orange.shade300, width: 1.5) : BorderSide.none, ), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), leading: Container( width: 40, height: 40, decoration: BoxDecoration( color: detail.estCadeau ? Colors.green.shade50 : detail.aRemise ? Colors.orange.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Icon( detail.estCadeau ? Icons.card_giftcard : detail.aRemise ? Icons.discount : Icons.shopping_bag, size: 20, color: detail.estCadeau ? Colors.green.shade700 : detail.aRemise ? Colors.orange.shade700 : Colors.blue.shade700, ), ), title: Row( children: [ Expanded(child: Text(product.name)), if (detail.estCadeau) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(10), ), child: Text( 'CADEAU', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.green.shade700, ), ), ), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text('${detail.quantite} x '), if (detail.estCadeau) ...[ Text( 'GRATUIT', style: TextStyle( color: Colors.green.shade700, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 8), Text( '(${detail.prixUnitaire.toStringAsFixed(2)} MGA)', style: TextStyle( fontSize: 11, color: Colors.grey.shade500, decoration: TextDecoration.lineThrough, ), ), ] else if (detail.aRemise) ...[ Text( '${detail.prixUnitaire.toStringAsFixed(2)}', style: const TextStyle( decoration: TextDecoration.lineThrough, color: Colors.grey, ), ), const Text(' → '), Text( '${(detail.prixFinal / detail.quantite).toStringAsFixed(2)} MGA', style: TextStyle( color: Colors.orange.shade700, fontWeight: FontWeight.bold, ), ), ] else Text('${detail.prixUnitaire.toStringAsFixed(2)} MGA'), ], ), if (detail.aRemise && !detail.estCadeau) Text( 'Remise: ${detail.remiseDescription} (-${detail.montantRemise.toStringAsFixed(2)} MGA)', style: TextStyle( fontSize: 11, color: Colors.orange.shade600, fontStyle: FontStyle.italic, ), ), if (detail.estCadeau) Row( children: [ Icon( Icons.card_giftcard, size: 12, color: Colors.green.shade600, ), const SizedBox(width: 4), Text( 'Article offert gracieusement', style: TextStyle( fontSize: 11, color: Colors.green.shade600, fontStyle: FontStyle.italic, ), ), ], ), ], ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (detail.estCadeau) ...[ Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.card_giftcard, size: 16, color: Colors.green.shade700, ), const SizedBox(width: 4), Text( 'GRATUIT', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.green.shade700, fontSize: 14, ), ), ], ), Text( 'Valeur: ${detail.sousTotal.toStringAsFixed(2)} MGA', style: TextStyle( fontSize: 10, color: Colors.grey.shade500, fontStyle: FontStyle.italic, ), ), ] else if (detail.aRemise && detail.sousTotal != detail.prixFinal) ...[ Text( '${detail.sousTotal.toStringAsFixed(2)} MGA', style: const TextStyle( fontSize: 11, decoration: TextDecoration.lineThrough, color: Colors.grey, ), ), Text( '${detail.prixFinal.toStringAsFixed(2)} MGA', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.orange.shade700, fontSize: 14, ), ), ] else Text( '${detail.prixFinal.toStringAsFixed(2)} MGA', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.blue.shade800, fontSize: 14, ), ), ], ), onTap: () { if (detail.estCadeau) { _basculerStatutCadeau(product.id!); } else { _showRemiseDialog(product); } }, ), ), ); }, ); } // 7. Modifier _buildCartTotalSection pour afficher les totaux avec remises Widget _buildCartTotalSection() { double sousTotal = 0; double totalRemises = 0; double totalCadeaux = 0; double total = 0; int nombreCadeaux = 0; _panierDetails.forEach((productId, detail) { sousTotal += detail.sousTotal; if (detail.estCadeau) { totalCadeaux += detail.sousTotal; nombreCadeaux += detail.quantite; } else { totalRemises += detail.montantRemise; } total += detail.prixFinal; }); return Column( children: [ // Sous-total if (totalRemises > 0 || totalCadeaux > 0) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Sous-total:', style: TextStyle(fontSize: 14)), Text( '${sousTotal.toStringAsFixed(2)} MGA', style: const TextStyle(fontSize: 14), ), ], ), const SizedBox(height: 4), ], // Remises totales if (totalRemises > 0) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Remises totales:', style: TextStyle( fontSize: 14, color: Colors.orange.shade700, ), ), Text( '-${totalRemises.toStringAsFixed(2)} MGA', style: TextStyle( fontSize: 14, color: Colors.orange.shade700, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 4), ], // Cadeaux offerts if (totalCadeaux > 0) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( Icons.card_giftcard, size: 16, color: Colors.green.shade700, ), const SizedBox(width: 4), Text( 'Cadeaux offerts ($nombreCadeaux):', style: TextStyle( fontSize: 14, color: Colors.green.shade700, ), ), ], ), Text( '-${totalCadeaux.toStringAsFixed(2)} MGA', style: TextStyle( fontSize: 14, color: Colors.green.shade700, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 4), ], if (totalRemises > 0 || totalCadeaux > 0) const Divider(height: 16), // Total final Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Total:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), Text( '${total.toStringAsFixed(2)} MGA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green, ), ), ], ), const SizedBox(height: 8), // Résumé Text( '${_panierDetails.values.where((d) => d.quantite > 0).length} article(s)', style: TextStyle(color: Colors.grey.shade600), ), // Économies totales if (totalRemises > 0 || totalCadeaux > 0) ...[ const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.green.shade200), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.savings, size: 16, color: Colors.green.shade700, ), const SizedBox(width: 4), Text( 'Économies totales: ${(totalRemises + totalCadeaux).toStringAsFixed(2)} MGA', style: TextStyle( color: Colors.green.shade700, fontWeight: FontWeight.bold, fontSize: 12, ), ), ], ), ), ], // Détail des économies if (totalRemises > 0 && totalCadeaux > 0) ...[ const SizedBox(height: 4), Text( 'Remises: ${totalRemises.toStringAsFixed(2)} MGA • Cadeaux: ${totalCadeaux.toStringAsFixed(2)} MGA', style: TextStyle( color: Colors.grey.shade600, fontSize: 11, fontStyle: FontStyle.italic, ), ), ], ], ); } Widget _buildSubmitButton() { final isMobile = MediaQuery.of(context).size.width < 600; return SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( padding: EdgeInsets.symmetric( vertical: isMobile ? 12 : 16 ), backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 4, ), onPressed: _submitOrder, child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( isMobile ? 'Valider' : 'Valider la Commande', style: TextStyle(fontSize: isMobile ? 14 : 16), ), ), ); } Future _submitOrder() async { // Vérifier d'abord si le panier est vide final itemsInCart = _panierDetails.entries.where((e) => e.value.quantite > 0).toList(); if (itemsInCart.isEmpty) { Get.snackbar( 'Panier vide', 'Veuillez ajouter des produits à votre commande', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); _showCartBottomSheet(); return; } // Vérifier les informations client if (_nomController.text.isEmpty || _prenomController.text.isEmpty || _emailController.text.isEmpty || _telephoneController.text.isEmpty || _adresseController.text.isEmpty) { Get.snackbar( 'Informations manquantes', 'Veuillez remplir les informations client', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); _showClientFormDialog(); return; } setState(() { _isLoading = true; }); // Créer le client final client = Client( nom: _nomController.text, prenom: _prenomController.text, email: _emailController.text, telephone: _telephoneController.text, adresse: _adresseController.text, dateCreation: DateTime.now(), ); // Calculer le total final et préparer les détails double total = 0; final details = []; for (final entry in itemsInCart) { final detail = entry.value; total += detail.prixFinal; details.add(detail); } // Créer la commande avec le total final (après remises) final commande = Commande( clientId: 0, dateCommande: DateTime.now(), statut: StatutCommande.enAttente, montantTotal: total, notes: 'Commande passée via l\'application', commandeurId: _selectedCommercialUser?.id, ); try { await _appDatabase.createCommandeComplete(client, commande, details); // Fermer le panier avant d'afficher la confirmation Get.back(); // Afficher le dialogue de confirmation final isMobile = MediaQuery.of(context).size.width < 600; await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.check_circle, color: Colors.green.shade700), ), const SizedBox(width: 12), Expanded( child: Text( 'Commande Validée', style: TextStyle(fontSize: isMobile ? 16 : 18), ), ), ], ), content: Text( 'Votre commande a été enregistrée et expédiée avec succès.', style: TextStyle(fontSize: isMobile ? 14 : 16), ), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.green.shade700, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( vertical: isMobile ? 12 : 16 ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), onPressed: () { Navigator.pop(context); _clearFormAndCart(); _loadProducts(); }, child: Text( 'OK', style: TextStyle(fontSize: isMobile ? 14 : 16), ), ), ), ], ), ); } catch (e) { setState(() { _isLoading = false; }); Get.snackbar( 'Erreur', 'Une erreur est survenue: ${e.toString()}', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); } } Future _showCadeauDialog(Product product) async { final detailExistant = _panierDetails[product.id!]; if (detailExistant == null || detailExistant.quantite == 0) { Get.snackbar( 'Produit requis', 'Vous devez d\'abord ajouter ce produit au panier', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange, colorText: Colors.white, ); return; } final result = await showDialog>( context: context, builder: (context) => CadeauDialog( product: product, quantite: detailExistant.quantite, detailExistant: detailExistant, ), ); if (result != null) { _ajouterCadeauAuPanier( result['produit'] as Product, result['quantite'] as int, ); } } void _ajouterCadeauAuPanier(Product produitCadeau, int quantite) { // Vérifier le stock disponible if (produitCadeau.stock != null && quantite > produitCadeau.stock!) { Get.snackbar( 'Stock insuffisant', 'Quantité de cadeau demandée non disponible', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); return; } setState(() { final detailCadeau = DetailCommande.cadeau( commandeId: 0, // Sera défini lors de la création produitId: produitCadeau.id!, quantite: quantite, prixUnitaire: produitCadeau.price, produitNom: produitCadeau.name, produitReference: produitCadeau.reference, ); _panierDetails[produitCadeau.id!] = detailCadeau; }); Get.snackbar( 'Cadeau ajouté', '${produitCadeau.name} a été ajouté comme cadeau', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green.shade600, colorText: Colors.white, duration: const Duration(seconds: 3), icon: const Icon(Icons.card_giftcard, color: Colors.white), ); } void _basculerStatutCadeau(int productId) { final detailExistant = _panierDetails[productId]; if (detailExistant == null) return; setState(() { if (detailExistant.estCadeau) { // Convertir en article normal _panierDetails[productId] = detailExistant.convertirEnArticleNormal(); Get.snackbar( 'Statut modifié', 'L\'article n\'est plus un cadeau', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.blue.shade600, colorText: Colors.white, duration: const Duration(seconds: 2), ); } else { // Convertir en cadeau _panierDetails[productId] = detailExistant.convertirEnCadeau(); Get.snackbar( 'Cadeau offert', 'L\'article est maintenant un cadeau', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green.shade600, colorText: Colors.white, duration: const Duration(seconds: 2), icon: const Icon(Icons.card_giftcard, color: Colors.white), ); } }); } @override void dispose() { _qrController?.dispose(); // Vos disposals existants... _hideAllSuggestions(); _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); _telephoneController.dispose(); _adresseController.dispose(); _searchNameController.dispose(); _searchImeiController.dispose(); _searchReferenceController.dispose(); super.dispose(); } // 10. Modifier le Widget build pour utiliser le nouveau scan automatique @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < 600; return Scaffold( floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ // Bouton de scan automatique (remplace l'ancien scan IMEI) FloatingActionButton( heroTag: "auto_scan", onPressed: _isScanning ? null : _startAutomaticScanning, backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, foregroundColor: Colors.white, child: _isScanning ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.auto_awesome), ), const SizedBox(height: 10), // Bouton panier existant _buildFloatingCartButton(), ], ), appBar: CustomAppBar(title: 'Nouvelle commande'), drawer: CustomDrawer(), body: GestureDetector( onTap: _hideAllSuggestions, child: Column( children: [ // Section d'information sur le scan automatique (desktop) if (!isMobile) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildAutoScanInfoCard(), ), // Section des filtres if (!isMobile) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildFilterSection(), ), // Boutons pour mobile if (isMobile) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( children: [ Expanded( flex: 2, child: ElevatedButton.icon( icon: const Icon(Icons.filter_alt), label: const Text('Filtres'), onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: _buildFilterSection(), ), ); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade700, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(width: 8), Expanded( flex: 1, child: ElevatedButton.icon( icon: _isScanning ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.auto_awesome), label: Text(_isScanning ? 'Scan...' : 'Auto-scan'), onPressed: _isScanning ? null : _startAutomaticScanning, style: ElevatedButton.styleFrom( backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), ), // Compteur de résultats Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(20), ), child: Text( '${_filteredProducts.length} produit(s)', style: TextStyle( color: Colors.blue.shade700, fontWeight: FontWeight.w600, ), ), ), ), ], // Liste des produits Expanded( child: _buildProductList(), ), ], ), ), ); } }