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'; import 'package:youmazgestion/controller/userController.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(); List> _pointsDeVente = []; String? _selectedPointDeVente; final UserController _userController = Get.find(); // 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(); _loadPointsDeVenteWithDefault(); // Charger les points de vente _searchNameController.addListener(_filterProducts); _searchImeiController.addListener(_filterProducts); _searchReferenceController.addListener(_filterProducts); } Future _loadPointsDeVenteWithDefault() async { try { final points = await _appDatabase.getPointsDeVente(); setState(() { _pointsDeVente = points; if (points.isNotEmpty) { if (_userController.pointDeVenteId > 0) { final userPointDeVente = points.firstWhere( (point) => point['id'] == _userController.pointDeVenteId, orElse: () => {}, ); if (userPointDeVente.isNotEmpty) { _selectedPointDeVente = userPointDeVente['nom'] as String; } else { _selectedPointDeVente = points[0]['nom'] as String; } } else { _selectedPointDeVente = points[0]['nom'] as String; } } }); _filterProducts(); // Appliquer le filtre dès le chargement } catch (e) { Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); print('❌ Erreur chargement points de vente: $e'); } } bool _isUserSuperAdmin() { return _userController.role == 'Super Admin'; } bool _isProduitCommandable(Product product) { if (_isUserSuperAdmin()) { return true; // Les superadmins peuvent tout commander } // Les autres utilisateurs ne peuvent commander que les produits de leur PV return product.pointDeVenteId == _userController.pointDeVenteId; } // 🎯 MÉTHODE UTILITAIRE: Obtenir l'ID du point de vente sélectionné int? _getSelectedPointDeVenteId() { if (_selectedPointDeVente == null) return null; final pointDeVente = _pointsDeVente.firstWhere( (point) => point['nom'] == _selectedPointDeVente, orElse: () => {}, ); return pointDeVente.isNotEmpty ? pointDeVente['id'] as int : null; } // 2. Ajoutez cette méthode pour charger les points de vente // 2. Ajoutez cette méthode pour charger les points de vente Future _loadPointsDeVente() async { try { final points = await _appDatabase.getPointsDeVente(); setState(() { _pointsDeVente = points; if (points.isNotEmpty) { _selectedPointDeVente = points.first['nom'] as String; } }); } catch (e) { Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); } } // ==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 // 🎯 MODIFIÉ: Validation avant ajout au panier // 🎯 MODIFIÉ: Validation avant ajout au panier (inchangée) void _ajouterAuPanier(Product product, int quantite) { // 🔒 VÉRIFICATION SÉCURITÉ: Non-superadmin ne peut commander que ses produits if (!_isProduitCommandable(product)) { Get.snackbar( 'Produit non commandable', 'Ce produit appartient à un autre point de vente. Seuls les produits de votre point de vente "${_userController.pointDeVenteDesignation}" sont commandables.', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange.shade600, colorText: Colors.white, icon: const Icon(Icons.info, color: Colors.white), duration: const Duration(seconds: 5), ); return; } // 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, produitId: product.id!, quantite: quantite, prixUnitaire: product.price, produitNom: product.name, produitReference: product.reference, ); _panierDetails[product.id!] = detail; }); } // 🎯 MODIFIÉ: Validation lors de la modification de quantité void _modifierQuantite(int productId, int nouvelleQuantite) { final detailExistant = _panierDetails[productId]; if (detailExistant == null) return; final product = _products.firstWhere((p) => p.id == productId); // 🔒 VÉRIFICATION SÉCURITÉ supplémentaire if (!_isProduitCommandable(product)) { Get.snackbar( 'Modification impossible', 'Vous ne pouvez modifier que les produits de votre point de vente', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange.shade600, colorText: Colors.white, ); return; } if (nouvelleQuantite <= 0) { setState(() { _panierDetails.remove(productId); }); return; } // ... reste du code existant pour la modification 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, 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) { final isProduitCommandable = _isProduitCommandable(product); final canRequestTransfer = product.stock != null && product.stock! >= 1; Get.dialog( Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header avec icône de succès Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.green.shade200), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade100, shape: BoxShape.circle, ), child: Icon( Icons.check_circle, color: Colors.green.shade700, size: 24, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Produit identifié !', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87, ), ), Text( 'Ajouté au panier avec succès', style: TextStyle( fontSize: 12, color: Colors.green.shade700, fontWeight: FontWeight.w500, ), ), ], ), ), ], ), ), const SizedBox(height: 20), // Informations du produit Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), const SizedBox(height: 12), // Détails du produit en grille _buildProductDetailRow('Prix', '${product.price.toStringAsFixed(2)} MGA'), _buildProductDetailRow('Quantité ajoutée', '$newQuantity'), if (product.imei != null && product.imei!.isNotEmpty) _buildProductDetailRow('IMEI', product.imei!), if (product.reference != null && product.reference!.isNotEmpty) _buildProductDetailRow('Référence', product.reference!), if (product.stock != null) _buildProductDetailRow('Stock restant', '${product.stock! - newQuantity}'), ], ), ), const SizedBox(height: 20), // Badge identification automatique Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.blue.shade200), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.auto_awesome, color: Colors.blue.shade700, size: 16), const SizedBox(width: 6), Text( 'Identifié automatiquement', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.blue.shade700, ), ), ], ), ), const SizedBox(height: 24), // Boutons d'action redessinés Column( children: [ // Bouton principal selon les permissions SizedBox( width: double.infinity, height: 48, child: (!isProduitCommandable && !_isUserSuperAdmin()) ? ElevatedButton.icon( onPressed: canRequestTransfer ? () { Get.back(); _showDemandeTransfertDialog(product); } : () { Get.snackbar( 'Stock insuffisant', 'Impossible de demander un transfert : produit en rupture de stock', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange.shade600, colorText: Colors.white, margin: const EdgeInsets.all(16), ); }, icon: const Icon(Icons.swap_horiz, size: 20), label: const Text( 'Demander un transfert', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), style: ElevatedButton.styleFrom( backgroundColor: canRequestTransfer ? Colors.orange.shade600 : Colors.grey.shade400, foregroundColor: Colors.white, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ) : ElevatedButton.icon( onPressed: () { _ajouterAuPanier(product, 1); Get.back(); _showCartBottomSheet(); }, icon: const Icon(Icons.shopping_cart, size: 20), label: const Text( 'Voir le panier', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.green.shade600, foregroundColor: Colors.white, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(height: 12), // Boutons secondaires Row( children: [ // Continuer Expanded( child: SizedBox( height: 44, child: OutlinedButton.icon( onPressed: () => Get.back(), icon: const Icon(Icons.close, size: 18), label: const Text('Continuer'), style: OutlinedButton.styleFrom( foregroundColor: Colors.grey.shade700, side: BorderSide(color: Colors.grey.shade300), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ), const SizedBox(width: 12), // Scanner encore Expanded( child: SizedBox( height: 44, child: ElevatedButton.icon( onPressed: () { Get.back(); _startAutomaticScanning(); }, icon: const Icon(Icons.qr_code_scanner, size: 18), label: const Text('Scanner'), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade600, foregroundColor: Colors.white, elevation: 1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ), ], ), ], ), ], ), ), ), ); } // Widget helper pour les détails du produit Widget _buildProductDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 100, child: Text( '$label:', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey.shade600, ), ), ), Expanded( child: Text( value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ), ], ), ); } 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(); } // 🎯 MODIFIÉ: Chargement de TOUS les produits (visibilité totale) Future _loadProducts() async { final products = await _appDatabase.getProducts(); setState(() { _products.clear(); // ✅ TOUS les utilisateurs voient TOUS les produits _products.addAll(products); print("✅ Produits chargés: ${products.length} (tous visibles)"); _filteredProducts.clear(); _filteredProducts.addAll(_products); }); } Future _loadCommercialUsers() async { final commercialUsers = await _appDatabase.getCommercialUsers(); setState(() { _commercialUsers = commercialUsers; if (_commercialUsers.isNotEmpty) { _selectedCommercialUser = _commercialUsers.first; } }); } // 🎯 MODIFIÉ: Filtrage avec visibilité totale mais indication des restrictions void _filterProducts() { final nameQuery = _searchNameController.text.toLowerCase(); final imeiQuery = _searchImeiController.text.toLowerCase(); final referenceQuery = _searchReferenceController.text.toLowerCase(); final selectedPointDeVenteId = _getSelectedPointDeVenteId(); 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); // Appliquer le filtre par point de vente uniquement si un point est sélectionné bool matchesPointDeVente = true; if (selectedPointDeVenteId != null) { matchesPointDeVente = product.pointDeVenteId == selectedPointDeVenteId; } if (matchesName && matchesImei && matchesReference && matchesStock && matchesPointDeVente) { _filteredProducts.add(product); } } }); print("🔍 Filtrage: ${_filteredProducts.length} produits visibles"); } void _toggleStockFilter() { setState(() { _showOnlyInStock = !_showOnlyInStock; }); _filterProducts(); } // 🎯 MÉTHODE UTILITAIRE: Reset des filtres avec point de vente utilisateur void _clearFilters() { setState(() { _searchNameController.clear(); _searchImeiController.clear(); _searchReferenceController.clear(); _showOnlyInStock = false; // Réinitialiser au point de vente de l'utilisateur connecté if (_userController.pointDeVenteId > 0) { final userPointDeVente = _pointsDeVente.firstWhere( (point) => point['id'] == _userController.pointDeVenteId, orElse: () => {}, ); if (userPointDeVente.isNotEmpty) { _selectedPointDeVente = userPointDeVente['nom'] as String; } else { _selectedPointDeVente = null; // Fallback si le point de vente n'existe plus } } else { _selectedPointDeVente = null; } }); _filterProducts(); print("🔄 Filtres réinitialisés - Point de vente: $_selectedPointDeVente"); } // 11. Modifiez la section des filtres pour inclure le bouton de réinitialisation 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 avec indicateurs de filtres actifs Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8 ), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(20), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${_filteredProducts.length} produit(s)', style: TextStyle( color: Colors.blue.shade700, fontWeight: FontWeight.w600, fontSize: isMobile ? 12 : 14, ), ), // Indicateurs de filtres actifs if (_selectedPointDeVente != null || _showOnlyInStock || _searchNameController.text.isNotEmpty || _searchImeiController.text.isNotEmpty || _searchReferenceController.text.isNotEmpty) ...[ const SizedBox(height: 4), Wrap( spacing: 4, children: [ if (_selectedPointDeVente != null) _buildFilterChip('PV: $_selectedPointDeVente'), if (_showOnlyInStock) _buildFilterChip('En stock'), if (_searchNameController.text.isNotEmpty) _buildFilterChip('Nom: ${_searchNameController.text}'), if (_searchImeiController.text.isNotEmpty) _buildFilterChip('IMEI: ${_searchImeiController.text}'), if (_searchReferenceController.text.isNotEmpty) _buildFilterChip('Réf: ${_searchReferenceController.text}'), ], ), ], ], ), ), ], ), ), ); } Widget _buildFilterChip(String label) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.orange.shade300), ), child: Text( label, style: TextStyle( fontSize: 10, color: Colors.orange.shade700, fontWeight: FontWeight.w500, ), ), ); } Widget _buildFloatingCartButton() { final isMobile = MediaQuery.of(context).size.width < 600; final cartItemCount = _panierDetails.values.where((d) => d.quantite > 0).length; if (isMobile) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( heroTag: "scan_btn", onPressed: _isScanning ? null : _startAutomaticScanning, backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, foregroundColor: Colors.white, mini: true, child: _isScanning ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.qr_code_scanner), ), const SizedBox(width: 8), FloatingActionButton.extended( onPressed: _showCartBottomSheet, icon: const Icon(Icons.shopping_cart), label: Text('$cartItemCount'), backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, ), ], ); } else { return FloatingActionButton.extended( onPressed: _showCartBottomSheet, icon: const Icon(Icons.shopping_cart), label: Text('Panier ($cartItemCount)'), backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, ); } } // Nouvelle méthode pour afficher les filtres sur mobile void _showMobileFilters(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Column( children: [ _buildPointDeVenteFilter(), _buildFilterSection(), ], ), ), ); } 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 _buildUserPointDeVenteInfo() { if (_userController.pointDeVenteId <= 0) { return const SizedBox.shrink(); } 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.blue.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.store, color: Colors.blue.shade700, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Votre point de vente', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color.fromARGB(255, 9, 56, 95), ), ), const SizedBox(height: 4), Text( _userController.pointDeVenteDesignation, style: TextStyle( fontSize: 12, color: Colors.blue.shade700, fontWeight: FontWeight.w500, ), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: Text( 'ID: ${_userController.pointDeVenteId}', style: TextStyle( fontSize: 10, color: Colors.blue.shade600, fontWeight: FontWeight.w500, ), ), ), ], ), ), ); } // 6. Ajoutez cette méthode pour filtrer les produits par point de vente // 🎯 MODIFIÉ: Dropdown avec gestion améliorée Widget _buildPointDeVenteFilter() { // if (!_isUserSuperAdmin()) { // return const SizedBox.shrink(); // Cacher pour les non-admins // } 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: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.filter_list, color: Colors.green.shade700), const SizedBox(width: 8), const Text('Filtrer par point de vente (Admin)', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 8), DropdownButtonFormField( value: _selectedPointDeVente, decoration: InputDecoration(labelText: 'Point de vente'), items: [ const DropdownMenuItem( value: null, child: Text('Tous les points de vente')), ..._pointsDeVente.map((point) { return DropdownMenuItem( value: point['nom'] as String, child: Text(point['nom'] as String), ); }).toList(), ], onChanged: (value) { setState(() { _selectedPointDeVente = value; _filterProducts(); }); }, ), ], ), ), ); } // 🎯 MODIFIÉ: Interface utilisateur adaptée selon le rôle // 🎯 NOUVEAU: Header d'information adapté Widget _buildRoleBasedHeader() { final commandableCount = _products.where((p) => _isProduitCommandable(p)).length; final totalCount = _products.length; 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: Column( children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _isUserSuperAdmin() ? Colors.purple.shade100 : Colors.blue.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon( _isUserSuperAdmin() ? Icons.admin_panel_settings : Icons.visibility, color: _isUserSuperAdmin() ? Colors.purple.shade700 : Colors.blue.shade700, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _isUserSuperAdmin() ? 'Mode Administrateur' : 'Mode Consultation étendue', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _isUserSuperAdmin() ? Colors.purple.shade700 : Colors.blue.shade700, ), ), const SizedBox(height: 4), Text( _isUserSuperAdmin() ? 'Tous les produits sont visibles et commandables' : 'Tous les produits sont visibles • Commandes limitées à votre PV', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _isUserSuperAdmin() ? Colors.purple.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all( color: _isUserSuperAdmin() ? Colors.purple.shade200 : Colors.blue.shade200 ), ), child: Text( _userController.role.toUpperCase(), style: TextStyle( fontSize: 10, color: _isUserSuperAdmin() ? Colors.purple.shade600 : Colors.blue.shade600, fontWeight: FontWeight.w600, ), ), ), ], ), // Statistiques de produits const SizedBox(height: 12), Row( children: [ // Produits visibles Expanded( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: Row( children: [ Icon(Icons.visibility, size: 16, color: Colors.blue.shade600), const SizedBox(width: 8), Expanded( child: Text( '$totalCount produit(s) visibles', style: TextStyle( fontSize: 12, color: Colors.blue.shade600, fontWeight: FontWeight.w500, ), ), ), ], ), ), ), if (!_isUserSuperAdmin()) ...[ const SizedBox(width: 8), // Produits commandables Expanded( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade200), ), child: Row( children: [ Icon(Icons.shopping_cart, size: 16, color: Colors.green.shade600), const SizedBox(width: 8), Expanded( child: Text( '$commandableCount commandables', style: TextStyle( fontSize: 12, color: Colors.green.shade600, fontWeight: FontWeight.w500, ), ), ), ], ), ), ), ], ], ), ], ), ), ); } 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, ), ), ], ), ), ); } // 🎯 MODIFIÉ: Interface produit avec indication visuelle de la commandabilité 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; final isCurrentUserPointDeVente = product.pointDeVenteId == _userController.pointDeVenteId; final isProduitCommandable = _isProduitCommandable(product); return FutureBuilder( future: _appDatabase.getPointDeVenteNomById(product.pointDeVenteId ?? 0), builder: (context, snapshot) { String pointDeVenteText = 'Chargement...'; if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { pointDeVenteText = 'Erreur de chargement'; } else { pointDeVenteText = snapshot.data ?? 'Non spécifié'; } } return Card( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: isCurrentUserPointDeVente ? BorderSide(color: Colors.orange.shade300, width: 2) : !isProduitCommandable ? BorderSide(color: Colors.grey.shade300, width: 1.5) : BorderSide.none, ), child: Opacity( opacity: isProduitCommandable ? 1.0 : 0.7, 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) : isCurrentUserPointDeVente ? Border.all(color: Colors.orange.shade300, width: 2) : !isProduitCommandable ? Border.all(color: Colors.grey.shade200, width: 1) : null, ), child: Padding( padding: const EdgeInsets.all(12.0), child: Column( children: [ Row( children: [ Container( width: isMobile ? 40 : 50, height: isMobile ? 40 : 50, decoration: BoxDecoration( color: !isProduitCommandable ? Colors.grey.shade100 : isOutOfStock ? Colors.red.shade50 : detailPanier?.estCadeau == true ? Colors.green.shade50 : detailPanier?.aRemise == true ? Colors.orange.shade50 : isCurrentUserPointDeVente ? Colors.orange.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Icon( !isProduitCommandable ? Icons.lock_outline : detailPanier?.estCadeau == true ? Icons.card_giftcard : detailPanier?.aRemise == true ? Icons.discount : isCurrentUserPointDeVente ? Icons.store : Icons.shopping_bag, size: isMobile ? 20 : 24, color: !isProduitCommandable ? Colors.grey.shade500 : isOutOfStock ? Colors.red : detailPanier?.estCadeau == true ? Colors.green.shade700 : detailPanier?.aRemise == true ? Colors.orange.shade700 : isCurrentUserPointDeVente ? 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: !isProduitCommandable ? Colors.grey.shade600 : isOutOfStock ? Colors.red.shade700 : null, ), ), ), // Indicateurs de statut if (!isProduitCommandable && !_isUserSuperAdmin()) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.lock_outline, size: 10, color: Colors.grey.shade600), const SizedBox(width: 2), Text( 'AUTRE PV', style: TextStyle( fontSize: 9, fontWeight: FontWeight.bold, color: Colors.grey.shade600, ), ), ], ), ), 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, ), ), ), if (isCurrentUserPointDeVente && detailPanier?.estCadeau != true && isProduitCommandable) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(10), ), child: Text( 'MON PV', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.orange.shade700, ), ), ), ], ), const SizedBox(height: 4), // ===== PRIX AVEC GESTION CADEAUX/REMISES ===== 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, ), ), ], ], ], ), // Affichage remise if (detailPanier?.aRemise == true && !detailPanier!.estCadeau) Text( 'Remise: ${detailPanier!.remiseDescription}', style: TextStyle( fontSize: isMobile ? 10 : 12, color: Colors.orange.shade600, fontWeight: FontWeight.w500, ), ), // Stock 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, ), ), // Point de vente const SizedBox(height: 4), Row( children: [ Icon( Icons.store, size: 12, color: isCurrentUserPointDeVente ? Colors.orange.shade700 : !isProduitCommandable ? Colors.grey.shade500 : Colors.grey.shade600 ), const SizedBox(width: 4), Expanded( child: Text( 'PV: $pointDeVenteText', style: TextStyle( fontSize: isMobile ? 9 : 11, color: isCurrentUserPointDeVente ? Colors.orange.shade700 : !isProduitCommandable ? Colors.grey.shade500 : Colors.grey.shade600, fontWeight: isCurrentUserPointDeVente ? FontWeight.w600 : FontWeight.normal, ), ), ), if (!isProduitCommandable && !_isUserSuperAdmin()) Icon( Icons.lock_outline, size: 12, color: Colors.grey.shade500, ), ], ), ], ), ), // ===== CONTRÔLES QUANTITÉ ET ACTIONS ===== Column( children: [ // Boutons d'actions (seulement si commandable ET dans le panier) if (isProduitCommandable && 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é (seulement si commandable) if (isProduitCommandable) Container( decoration: BoxDecoration( color: isOutOfStock ? Colors.grey.shade100 : detailPanier?.estCadeau == true ? Colors.green.shade50 : isCurrentUserPointDeVente ? Colors.orange.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, ); } }, ), ], ), ) else // Message informatif pour produits non-commandables Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.grey.shade300), ), child: Column( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_outline, size: 14, color: Colors.grey.shade600), const SizedBox(width: 4), Text( 'Consultation', style: TextStyle( fontSize: 11, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 4), ElevatedButton.icon( icon: const Icon(Icons.swap_horiz, size: 14), label:!isMobile ? const Text('Demander transfertt'):const SizedBox.shrink(), style: ElevatedButton.styleFrom( backgroundColor: (product.stock != null && product.stock! >= 1) ? Colors.blue.shade700 : Colors.grey.shade400, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), ), onPressed: (product.stock != null && product.stock! >= 1) ? () => _showDemandeTransfertDialog(product) : () { Get.snackbar( 'Stock insuffisant', 'Impossible de demander un transfert : produit en rupture de stock', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange.shade600, colorText: Colors.white, ); }, ), ], ), ), ], ), ], ), ], ), ), ), ), ); }, ); } // 🎨 INTERFACE AMÉLIORÉE: Dialog moderne pour demande de transfert Future _showDemandeTransfertDialog(Product product) async { final quantiteController = TextEditingController(text: '1'); final notesController = TextEditingController(); final _formKey = GlobalKey(); // Récupérer les infos du point de vente source final pointDeVenteSource = await _appDatabase.getPointDeVenteNomById(product.pointDeVenteId ?? 0); final pointDeVenteDestination = await _appDatabase.getPointDeVenteNomById(_userController.pointDeVenteId); await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), contentPadding: EdgeInsets.zero, content: Container( width: 400, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ // En-tête avec design moderne Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue.shade600, Colors.blue.shade700], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), ), child: Column( children: [ Icon( Icons.swap_horizontal_circle, size: 48, color: Colors.white, ), const SizedBox(height: 8), Text( 'Demande de transfert', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( 'Transférer un produit entre points de vente', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.9), ), ), ], ), ), // Contenu principal Padding( padding: const EdgeInsets.all(20), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Informations du produit Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.inventory_2, color: Colors.blue.shade700, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Produit à transférer', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), Text( product.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildInfoCard( 'Prix unitaire', '${product.price.toStringAsFixed(2)} MGA', Icons.attach_money, Colors.green, ), ), const SizedBox(width: 8), Expanded( child: _buildInfoCard( 'Stock disponible', '${product.stock ?? 0}', Icons.inventory, product.stock != null && product.stock! > 0 ? Colors.green : Colors.red, ), ), ], ), if (product.reference != null && product.reference!.isNotEmpty) ...[ const SizedBox(height: 8), Text( 'Référence: ${product.reference}', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, fontFamily: 'monospace', ), ), ], ], ), ), const SizedBox(height: 20), // Informations de transfert Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.orange.shade200), ), child: Column( children: [ Row( children: [ Icon(Icons.arrow_forward, color: Colors.orange.shade700), const SizedBox(width: 8), Text( 'Informations de transfert', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.orange.shade700, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildTransferStep( 'DE', pointDeVenteSource ?? 'Chargement...', Icons.store_outlined, Colors.red.shade600, ), ), Container( margin: const EdgeInsets.symmetric(horizontal: 8), child: Icon( Icons.arrow_forward, color: Colors.orange.shade700, size: 24, ), ), Expanded( child: _buildTransferStep( 'VERS', pointDeVenteDestination ?? 'Chargement...', Icons.store, Colors.green.shade600, ), ), ], ), ], ), ), const SizedBox(height: 20), // Champ quantité avec design amélioré Text( 'Quantité à transférer', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey.shade700, ), ), const SizedBox(height: 8), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300), ), child: Row( children: [ IconButton( onPressed: () { int currentQty = int.tryParse(quantiteController.text) ?? 1; if (currentQty > 1) { quantiteController.text = (currentQty - 1).toString(); } }, icon: Icon(Icons.remove, color: Colors.grey.shade600), ), Expanded( child: TextFormField( controller: quantiteController, decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 16), hintText: 'Quantité', ), textAlign: TextAlign.center, keyboardType: TextInputType.number, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer une quantité'; } final qty = int.tryParse(value) ?? 0; if (qty <= 0) { return 'Quantité invalide'; } if (product.stock != null && qty > product.stock!) { return 'Quantité supérieure au stock disponible'; } return null; }, ), ), IconButton( onPressed: () { int currentQty = int.tryParse(quantiteController.text) ?? 1; int maxStock = product.stock ?? 999; if (currentQty < maxStock) { quantiteController.text = (currentQty + 1).toString(); } }, icon: Icon(Icons.add, color: Colors.grey.shade600), ), ], ), ), // Boutons d'action avec design moderne Row( children: [ Expanded( child: TextButton( onPressed: () => Navigator.pop(context), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300), ), ), child: Text( 'Annuler', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey.shade700, ), ), ), ), const SizedBox(width: 12), Expanded( flex: 2, child: ElevatedButton.icon( onPressed: () async { if (!_formKey.currentState!.validate()) return; final qty = int.tryParse(quantiteController.text) ?? 0; if (qty <= 0) { Get.snackbar( 'Erreur', 'Quantité invalide', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); return; } try { setState(() => _isLoading = true); Navigator.pop(context); await _appDatabase.createDemandeTransfert( produitId: product.id!, pointDeVenteSourceId: product.pointDeVenteId!, pointDeVenteDestinationId: _userController.pointDeVenteId, demandeurId: _userController.userId, quantite: qty, notes: notesController.text.isNotEmpty ? notesController.text : 'Demande de transfert depuis l\'application mobile', ); Get.snackbar( 'Demande envoyée ✅', 'Votre demande de transfert de $qty unité(s) a été enregistrée et sera traitée prochainement.', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 4), icon: const Icon(Icons.check_circle, color: Colors.white), ); } catch (e) { Get.snackbar( 'Erreur', 'Impossible d\'envoyer la demande: ${e.toString()}', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 4), ); } finally { setState(() => _isLoading = false); } }, icon: const Icon(Icons.send, color: Colors.white), label: const Text( 'Envoyer la demande', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade600, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 2, ), ), ), ], ), ], ), ), ), ], ), ), ), ), ); } // 🎨 Widget pour les cartes d'information Widget _buildInfoCard(String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.3)), ), child: Column( children: [ Icon(icon, color: color, size: 20), const SizedBox(height: 4), Text( label, style: TextStyle( fontSize: 10, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), Text( value, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: color, ), textAlign: TextAlign.center, ), ], ), ); } // 🎨 Widget pour les étapes de transfert Widget _buildTransferStep(String label, String pointDeVente, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.3)), ), child: Column( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Icon(icon, color: color, size: 16), ), const SizedBox(height: 6), Text( label, style: TextStyle( fontSize: 10, color: Colors.grey.shade600, fontWeight: FontWeight.bold, ), ), Text( pointDeVente, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: color, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ); } // 🎨 BOUTON AMÉLIORÉ dans le widget principal // Remplacez le bouton "Demander transfert" existant par celui-ci : 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), ), ), ); } // 🎯 MODIFIÉ: Validation finale avant soumission Future _submitOrder() async { // Vérification panier 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; } // 🔒 VALIDATION SÉCURITÉ FINALE: Vérifier tous les produits du panier if (!_isUserSuperAdmin()) { final produitsNonAutorises = []; for (final entry in itemsInCart) { final product = _products.firstWhere((p) => p.id == entry.key); if (product.pointDeVenteId != _userController.pointDeVenteId) { produitsNonAutorises.add(product.name); } } if (produitsNonAutorises.isNotEmpty) { Get.dialog( AlertDialog( title: Row( children: [ Icon(Icons.security, color: Colors.red.shade600), const SizedBox(width: 8), const Text('Commande non autorisée'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Les produits suivants ne sont pas autorisés pour votre point de vente:'), const SizedBox(height: 8), ...produitsNonAutorises.map((nom) => Padding( padding: const EdgeInsets.only(left: 16, top: 4), child: Text('• $nom', style: TextStyle(color: Colors.red.shade700)), )), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.info_outline, color: Colors.orange.shade700, size: 16), const SizedBox(width: 8), Expanded( child: Text( 'Contactez un administrateur pour commander ces produits.', style: TextStyle( fontSize: 12, color: Colors.orange.shade700, ), ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Compris'), ), ], ), ); return; } } // Vérification 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 // 8. Modifiez votre méthode build pour inclure les nouvelles cartes d'information // VERSION OPTIMISÉE DE VOTRE INTERFACE EN-TÊTES ET RECHERCHE @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < 600; return Scaffold( floatingActionButton: _buildFloatingCartButton(), appBar: CustomAppBar(title: 'Nouvelle commande'), drawer: CustomDrawer(), body: GestureDetector( onTap: _hideAllSuggestions, child: Column( children: [ // 🎯 EN-TÊTE OPTIMISÉ Container( decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Column( children: [ // Info utilisateur (toujours visible mais compacte) _buildCompactUserInfo(), // Zone de recherche principale _buildMainSearchSection(isMobile), // Filtres rapides (toujours visibles) _buildQuickFilters(isMobile), ], ), ), // Liste des produits avec indicateur de résultats Expanded( child: Column( children: [ _buildResultsHeader(), Expanded(child: _buildProductList()), ], ), ), ], ), ), ); } // 🎯 INFORMATION UTILISATEUR COMPACTE Widget _buildCompactUserInfo() { if (_userController.pointDeVenteId <= 0) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.blue.shade50, border: Border(bottom: BorderSide(color: Colors.blue.shade100)), ), child: Row( children: [ Icon(Icons.store, size: 16, color: Colors.blue.shade700), const SizedBox(width: 8), Text( _userController.pointDeVenteDesignation, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.blue.shade700, ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( 'ID: ${_userController.pointDeVenteId}', style: TextStyle( fontSize: 10, color: Colors.blue.shade600, ), ), ), ], ), ); } // 🎯 ZONE DE RECHERCHE PRINCIPALE OPTIMISÉE Widget _buildMainSearchSection(bool isMobile) { return Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // Recherche principale avec actions intégrées Row( children: [ Expanded( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: TextField( controller: _searchNameController, decoration: InputDecoration( hintText: 'Rechercher par nom, IMEI, référence...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchNameController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchNameController.clear(); _filterProducts(); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12 ), ), ), ), ), const SizedBox(width: 8), // Bouton Scanner toujours visible Container( decoration: BoxDecoration( color: Colors.green.shade700, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.green.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: IconButton( icon: _isScanning ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.qr_code_scanner), color: Colors.white, onPressed: _isScanning ? null : _startAutomaticScanning, tooltip: 'Scanner un produit', ), ), if (!isMobile) ...[ const SizedBox(width: 8), // Bouton filtres avancés Container( decoration: BoxDecoration( color: Colors.blue.shade700, borderRadius: BorderRadius.circular(12), ), child: IconButton( icon: const Icon(Icons.tune), color: Colors.white, onPressed: () => _showAdvancedFiltersDialog(), tooltip: 'Filtres avancés', ), ), ], ], ), // Recherche multicritères (desktop uniquement) if (!isMobile) ...[ const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: _searchImeiController, decoration: InputDecoration( hintText: 'IMEI', prefixIcon: const Icon(Icons.phone_android, size: 20), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), isDense: true, filled: true, fillColor: Colors.grey.shade50, ), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _searchReferenceController, decoration: InputDecoration( hintText: 'Référence', prefixIcon: const Icon(Icons.qr_code, size: 20), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), isDense: true, filled: true, fillColor: Colors.grey.shade50, ), ), ), const SizedBox(width: 8), Expanded( child: DropdownButtonFormField( value: _selectedPointDeVente, decoration: InputDecoration( hintText: 'Point de vente', prefixIcon: const Icon(Icons.store, size: 20), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), isDense: true, filled: true, fillColor: Colors.grey.shade50, ), items: [ const DropdownMenuItem( value: null, child: Text('Tous les PV'), ), ..._pointsDeVente.map((point) { return DropdownMenuItem( value: point['nom'] as String, child: Text(point['nom'] as String), ); }).toList(), ], onChanged: (value) { setState(() { _selectedPointDeVente = value; _filterProducts(); }); }, ), ), ], ), ], ], ), ); } // 🎯 FILTRES RAPIDES OPTIMISÉS Widget _buildQuickFilters(bool isMobile) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.grey.shade50, border: Border(top: BorderSide(color: Colors.grey.shade200)), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ // Filtre stock FilterChip( label: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _showOnlyInStock ? Icons.inventory : Icons.inventory_2, size: 16, ), const SizedBox(width: 4), Text(_showOnlyInStock ? 'En stock' : 'Tous'), ], ), selected: _showOnlyInStock, onSelected: (selected) => _toggleStockFilter(), selectedColor: Colors.green.shade100, checkmarkColor: Colors.green.shade700, ), const SizedBox(width: 8), // Filtre mobile pour ouvrir les filtres avancés if (isMobile) ActionChip( label: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.tune, size: 16), SizedBox(width: 4), Text('Filtres'), ], ), onPressed: () => _showMobileFilters(context), ), const SizedBox(width: 8), // Bouton reset si filtres actifs if (_hasActiveFilters()) ActionChip( label: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.clear, size: 16), SizedBox(width: 4), Text('Reset'), ], ), onPressed: _clearFilters, backgroundColor: Colors.orange.shade100, ), ], ), ), ); } // 🎯 EN-TÊTE DES RÉSULTATS Widget _buildResultsHeader() { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.white, border: Border(bottom: BorderSide(color: Colors.grey.shade200)), ), child: Row( children: [ Text( '${_filteredProducts.length} produit(s)', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.grey.shade700, ), ), const Spacer(), // Indicateurs de filtres actifs if (_hasActiveFilters()) ...[ Wrap( spacing: 4, children: _getActiveFilterChips(), ), ], ], ), ); } // 🎯 MÉTHODES UTILITAIRES bool _hasActiveFilters() { return _selectedPointDeVente != null || _showOnlyInStock || _searchNameController.text.isNotEmpty || _searchImeiController.text.isNotEmpty || _searchReferenceController.text.isNotEmpty; } List _getActiveFilterChips() { List chips = []; if (_selectedPointDeVente != null) { chips.add(_buildMiniFilterChip('PV: $_selectedPointDeVente')); } if (_showOnlyInStock) { chips.add(_buildMiniFilterChip('En stock')); } if (_searchNameController.text.isNotEmpty) { chips.add(_buildMiniFilterChip('Nom: ${_searchNameController.text}')); } if (_searchImeiController.text.isNotEmpty) { chips.add(_buildMiniFilterChip('IMEI: ${_searchImeiController.text}')); } if (_searchReferenceController.text.isNotEmpty) { chips.add(_buildMiniFilterChip('Réf: ${_searchReferenceController.text}')); } return chips; } Widget _buildMiniFilterChip(String label) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade300), ), child: Text( label, style: TextStyle( fontSize: 10, color: Colors.blue.shade700, fontWeight: FontWeight.w500, ), ), ); } // 🎯 DIALOGUE FILTRES AVANCÉS void _showAdvancedFiltersDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Filtres avancés'), content: SizedBox( width: 400, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Tous vos filtres existants ici _buildPointDeVenteFilter(), const SizedBox(height: 16), // Autres filtres selon vos besoins ], ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Fermer'), ), ElevatedButton( onPressed: () { _filterProducts(); Get.back(); }, child: const Text('Appliquer'), ), ], ), ); } }