import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/produit.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; class NouvelleCommandePage extends StatefulWidget { const NouvelleCommandePage({super.key}); @override _NouvelleCommandePageState createState() => _NouvelleCommandePageState(); } class _NouvelleCommandePageState extends State { final AppDatabase _appDatabase = AppDatabase.instance; final _formKey = GlobalKey(); bool _isLoading = false; // Contrôleurs client final TextEditingController _nomController = TextEditingController(); final TextEditingController _prenomController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _telephoneController = TextEditingController(); final TextEditingController _adresseController = TextEditingController(); // Contrôleurs pour les filtres final TextEditingController _searchNameController = TextEditingController(); final TextEditingController _searchImeiController = TextEditingController(); final TextEditingController _searchReferenceController = TextEditingController(); // Panier final List _products = []; final List _filteredProducts = []; final Map _quantites = {}; // Variables de filtre bool _showOnlyInStock = false; // Utilisateurs commerciaux List _commercialUsers = []; Users? _selectedCommercialUser; // Variables pour les suggestions clients List _clientSuggestions = []; bool _showNomSuggestions = false; bool _showTelephoneSuggestions = false; GlobalKey _nomFieldKey = GlobalKey(); GlobalKey _telephoneFieldKey = GlobalKey(); @override void initState() { super.initState(); _loadProducts(); _loadCommercialUsers(); // Listeners pour les filtres _searchNameController.addListener(_filterProducts); _searchImeiController.addListener(_filterProducts); _searchReferenceController.addListener(_filterProducts); // Listeners pour l'autocomplétion client _nomController.addListener(() { if (_nomController.text.length >= 3) { _showClientSuggestions(_nomController.text, isNom: true); } else { _hideNomSuggestions(); } }); _telephoneController.addListener(() { if (_telephoneController.text.length >= 3) { _showClientSuggestions(_telephoneController.text, isNom: false); } else { _hideTelephoneSuggestions(); } }); } // Méthode pour vider complètement le formulaire et le panier void _clearFormAndCart() { setState(() { // Vider les contrôleurs client _nomController.clear(); _prenomController.clear(); _emailController.clear(); _telephoneController.clear(); _adresseController.clear(); // Vider le panier _quantites.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; } final suggestions = await _appDatabase.suggestClients(query); setState(() { _clientSuggestions = suggestions; if (isNom) { _showNomSuggestions = true; _showTelephoneSuggestions = false; } else { _showTelephoneSuggestions = true; _showNomSuggestions = false; } }); } void _showOverlay({required bool isNom}) { // Utiliser une approche plus simple avec setState setState(() { _clientSuggestions = _clientSuggestions; if (isNom) { _showNomSuggestions = true; _showTelephoneSuggestions = false; } else { _showTelephoneSuggestions = true; _showNomSuggestions = false; } }); } void _fillClientForm(Client client) { setState(() { _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), ); } void _hideNomSuggestions() { if (mounted && _showNomSuggestions) { setState(() { _showNomSuggestions = false; }); } } void _hideTelephoneSuggestions() { if (mounted && _showTelephoneSuggestions){ setState(() { _showTelephoneSuggestions = false; }); } } void _hideAllSuggestions() { _hideNomSuggestions(); _hideTelephoneSuggestions(); } Future _loadProducts() async { final products = await _appDatabase.getProducts(); setState(() { _products.clear(); _products.addAll(products); _filteredProducts.clear(); _filteredProducts.addAll(products); }); } Future _loadCommercialUsers() async { final commercialUsers = await _appDatabase.getCommercialUsers(); setState(() { _commercialUsers = commercialUsers; if (_commercialUsers.isNotEmpty) { _selectedCommercialUser = _commercialUsers.first; } }); } void _filterProducts() { final nameQuery = _searchNameController.text.toLowerCase(); final imeiQuery = _searchImeiController.text.toLowerCase(); final referenceQuery = _searchReferenceController.text.toLowerCase(); setState(() { _filteredProducts.clear(); for (var product in _products) { bool matchesName = nameQuery.isEmpty || product.name.toLowerCase().contains(nameQuery); bool matchesImei = imeiQuery.isEmpty || (product.imei?.toLowerCase().contains(imeiQuery) ?? false); bool matchesReference = referenceQuery.isEmpty || (product.reference?.toLowerCase().contains(referenceQuery) ?? false); bool matchesStock = !_showOnlyInStock || (product.stock != null && product.stock! > 0); if (matchesName && matchesImei && matchesReference && matchesStock) { _filteredProducts.add(product); } } }); } void _toggleStockFilter() { setState(() { _showOnlyInStock = !_showOnlyInStock; }); _filterProducts(); } void _clearFilters() { setState(() { _searchNameController.clear(); _searchImeiController.clear(); _searchReferenceController.clear(); _showOnlyInStock = false; }); _filterProducts(); } // Section des filtres adaptée pour mobile Widget _buildFilterSection() { final isMobile = MediaQuery.of(context).size.width < 600; return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.filter_list, color: Colors.blue.shade700), const SizedBox(width: 8), const Text( 'Filtres de recherche', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 9, 56, 95), ), ), const Spacer(), TextButton.icon( onPressed: _clearFilters, icon: const Icon(Icons.clear, size: 18), label: isMobile ? const SizedBox() : const Text('Réinitialiser'), style: TextButton.styleFrom( foregroundColor: Colors.grey.shade600, ), ), ], ), const SizedBox(height: 16), // Champ de recherche par nom TextField( controller: _searchNameController, decoration: InputDecoration( labelText: 'Rechercher par nom', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 12), if (!isMobile) ...[ // Version desktop - champs sur la même ligne Row( children: [ Expanded( child: TextField( controller: _searchImeiController, decoration: InputDecoration( labelText: 'IMEI', prefixIcon: const Icon(Icons.phone_android), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: _searchReferenceController, decoration: InputDecoration( labelText: 'Référence', prefixIcon: const Icon(Icons.qr_code), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), ), ], ), ] else ...[ // Version mobile - champs empilés TextField( controller: _searchImeiController, decoration: InputDecoration( labelText: 'IMEI', prefixIcon: const Icon(Icons.phone_android), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 12), TextField( controller: _searchReferenceController, decoration: InputDecoration( labelText: 'Référence', prefixIcon: const Icon(Icons.qr_code), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.grey.shade50, ), ), ], const SizedBox(height: 16), // Boutons de filtre adaptés pour mobile Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton.icon( onPressed: _toggleStockFilter, icon: Icon( _showOnlyInStock ? Icons.inventory : Icons.inventory_2, size: 20, ), label: Text(_showOnlyInStock ? isMobile ? 'Tous' : 'Afficher tous' : isMobile ? 'En stock' : 'Stock disponible'), style: ElevatedButton.styleFrom( backgroundColor: _showOnlyInStock ? Colors.green.shade600 : Colors.blue.shade600, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( horizontal: isMobile ? 12 : 16, vertical: 8 ), ), ), ], ), const SizedBox(height: 8), // Compteur de résultats Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8 ), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(20), ), child: Text( '${_filteredProducts.length} produit(s)', style: TextStyle( color: Colors.blue.shade700, fontWeight: FontWeight.w600, fontSize: isMobile ? 12 : 14, ), ), ), ], ), ), ); } @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < 600; return Scaffold( floatingActionButton: _buildFloatingCartButton(), drawer: isMobile ? CustomDrawer() : null, body: GestureDetector( onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs child: Column( children: [ // Section des filtres - adaptée comme dans HistoriquePage if (!isMobile) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildFilterSection(), ), // Sur mobile, bouton pour afficher les filtres dans un modal if (isMobile) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: SizedBox( width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Icons.filter_alt), label: const Text('Filtres produits'), onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: _buildFilterSection(), ), ); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade700, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ), // Compteur de résultats visible en haut sur mobile Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(20), ), child: Text( '${_filteredProducts.length} produit(s)', style: TextStyle( color: Colors.blue.shade700, fontWeight: FontWeight.w600, ), ), ), ), ], // Liste des produits Expanded( child: _buildProductList(), ), ], ), ), ); } Widget _buildSuggestionsList({required bool isNom}) { if (_clientSuggestions.isEmpty) return const SizedBox(); return Container( margin: const EdgeInsets.only(top: 4), constraints: const BoxConstraints(maxHeight: 150), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, itemCount: _clientSuggestions.length, itemBuilder: (context, index) { final client = _clientSuggestions[index]; return ListTile( dense: true, 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: () { _fillClientForm(client); _hideAllSuggestions(); }, ); }, ), ); } Widget _buildFloatingCartButton() { final isMobile = MediaQuery.of(context).size.width < 600; final cartItemCount = _quantites.values.where((q) => q > 0).length; return FloatingActionButton.extended( onPressed: () { _showCartBottomSheet(); }, icon: const Icon(Icons.shopping_cart), label: Text( isMobile ? 'Panier ($cartItemCount)' : 'Panier ($cartItemCount)', style: TextStyle(fontSize: isMobile ? 12 : 14), ), backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, ); } void _showClientFormDialog() { final isMobile = MediaQuery.of(context).size.width < 600; // Variables locales pour les suggestions dans le dialog bool showNomSuggestions = false; bool showPrenomSuggestions = false; bool showEmailSuggestions = false; bool showTelephoneSuggestions = false; List localClientSuggestions = []; // GlobalKeys pour positionner les overlays final GlobalKey nomFieldKey = GlobalKey(); final GlobalKey prenomFieldKey = GlobalKey(); final GlobalKey emailFieldKey = GlobalKey(); final GlobalKey telephoneFieldKey = GlobalKey(); Get.dialog( StatefulBuilder( builder: (context, setDialogState) { return Stack( children: [ AlertDialog( title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.person_add, color: Colors.blue.shade700), ), const SizedBox(width: 12), Expanded( child: Text( isMobile ? 'Client' : 'Informations Client', style: TextStyle(fontSize: isMobile ? 16 : 18), ), ), ], ), content: Container( width: isMobile ? double.maxFinite : 600, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), child: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Champ Nom avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: nomFieldKey, controller: _nomController, label: 'Nom', validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null, onChanged: (value) async { if (value.length >= 2) { final suggestions = await _appDatabase.suggestClients(value); setDialogState(() { localClientSuggestions = suggestions; showNomSuggestions = suggestions.isNotEmpty; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; }); } else { setDialogState(() { showNomSuggestions = false; localClientSuggestions = []; }); } }, ), const SizedBox(height: 12), // Champ Prénom avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: prenomFieldKey, controller: _prenomController, label: 'Prénom', validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null, onChanged: (value) async { if (value.length >= 2) { final suggestions = await _appDatabase.suggestClients(value); setDialogState(() { localClientSuggestions = suggestions; showPrenomSuggestions = suggestions.isNotEmpty; showNomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; }); } else { setDialogState(() { showPrenomSuggestions = false; localClientSuggestions = []; }); } }, ), const SizedBox(height: 12), // Champ Email avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: emailFieldKey, controller: _emailController, label: 'Email', keyboardType: TextInputType.emailAddress, validator: (value) { if (value?.isEmpty ?? true) return 'Veuillez entrer un email'; if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) { return 'Email invalide'; } return null; }, onChanged: (value) async { if (value.length >= 3) { final suggestions = await _appDatabase.suggestClients(value); setDialogState(() { localClientSuggestions = suggestions; showEmailSuggestions = suggestions.isNotEmpty; showNomSuggestions = false; showPrenomSuggestions = false; showTelephoneSuggestions = false; }); } else { setDialogState(() { showEmailSuggestions = false; localClientSuggestions = []; }); } }, ), const SizedBox(height: 12), // Champ Téléphone avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: telephoneFieldKey, controller: _telephoneController, label: 'Téléphone', keyboardType: TextInputType.phone, validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null, onChanged: (value) async { if (value.length >= 3) { final suggestions = await _appDatabase.suggestClients(value); setDialogState(() { localClientSuggestions = suggestions; showTelephoneSuggestions = suggestions.isNotEmpty; showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; }); } else { setDialogState(() { showTelephoneSuggestions = false; localClientSuggestions = []; }); } }, ), const SizedBox(height: 12), _buildTextFormField( controller: _adresseController, label: 'Adresse', maxLines: 2, validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null, ), const SizedBox(height: 12), _buildCommercialDropdown(), ], ), ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( horizontal: isMobile ? 16 : 20, vertical: isMobile ? 10 : 12 ), ), onPressed: () { if (_formKey.currentState!.validate()) { // Fermer toutes les suggestions avant de soumettre setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); Get.back(); _submitOrder(); } }, child: Text( isMobile ? 'Valider' : 'Valider la commande', style: TextStyle(fontSize: isMobile ? 12 : 14), ), ), ], ), // Overlay pour les suggestions du nom if (showNomSuggestions) _buildSuggestionOverlay( fieldKey: nomFieldKey, suggestions: localClientSuggestions, onClientSelected: (client) { _fillFormWithClient(client); setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); }, onDismiss: () { setDialogState(() { showNomSuggestions = false; localClientSuggestions = []; }); }, ), // Overlay pour les suggestions du prénom if (showPrenomSuggestions) _buildSuggestionOverlay( fieldKey: prenomFieldKey, suggestions: localClientSuggestions, onClientSelected: (client) { _fillFormWithClient(client); setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); }, onDismiss: () { setDialogState(() { showPrenomSuggestions = false; localClientSuggestions = []; }); }, ), // Overlay pour les suggestions de l'email if (showEmailSuggestions) _buildSuggestionOverlay( fieldKey: emailFieldKey, suggestions: localClientSuggestions, onClientSelected: (client) { _fillFormWithClient(client); setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); }, onDismiss: () { setDialogState(() { showEmailSuggestions = false; localClientSuggestions = []; }); }, ), // Overlay pour les suggestions du téléphone if (showTelephoneSuggestions) _buildSuggestionOverlay( fieldKey: telephoneFieldKey, suggestions: localClientSuggestions, onClientSelected: (client) { _fillFormWithClient(client); setDialogState(() { showNomSuggestions = false; showPrenomSuggestions = false; showEmailSuggestions = false; showTelephoneSuggestions = false; localClientSuggestions = []; }); }, onDismiss: () { setDialogState(() { showTelephoneSuggestions = false; localClientSuggestions = []; }); }, ), ], ); }, ), ); } // Widget pour créer un TextFormField avec une clé Widget _buildTextFormFieldWithKey({ required GlobalKey key, required TextEditingController controller, required String label, TextInputType? keyboardType, int maxLines = 1, String? Function(String?)? validator, void Function(String)? onChanged, }) { return Container( key: key, child: _buildTextFormField( controller: controller, label: label, keyboardType: keyboardType, maxLines: maxLines, validator: validator, onChanged: onChanged, ), ); } // Widget pour l'overlay des suggestions Widget _buildSuggestionOverlay({ required GlobalKey fieldKey, required List suggestions, required Function(Client) onClientSelected, required VoidCallback onDismiss, }) { return Positioned.fill( child: GestureDetector( onTap: onDismiss, child: Material( color: Colors.transparent, child: Builder( builder: (context) { // Obtenir la position du champ final RenderBox? renderBox = fieldKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) return const SizedBox(); final position = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; return Stack( children: [ Positioned( left: position.dx, top: position.dy + size.height + 4, width: size.width, child: GestureDetector( onTap: () {}, // Empêcher la fermeture au tap sur la liste child: Container( constraints: const BoxConstraints( maxHeight: 200, // Hauteur maximum pour la scrollabilité ), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Scrollbar( thumbVisibility: suggestions.length > 3, child: ListView.separated( padding: EdgeInsets.zero, shrinkWrap: true, itemCount: suggestions.length, separatorBuilder: (context, index) => Divider( height: 1, color: Colors.grey.shade200, ), itemBuilder: (context, index) { final client = suggestions[index]; return ListTile( dense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), leading: CircleAvatar( radius: 16, backgroundColor: Colors.blue.shade100, child: Icon( Icons.person, size: 16, color: Colors.blue.shade700, ), ), title: Text( '${client.nom} ${client.prenom}', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), subtitle: Text( '${client.telephone} • ${client.email}', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), onTap: () => onClientSelected(client), hoverColor: Colors.blue.shade50, ); }, ), ), ), ), ), ), ], ); }, ), ), ), ); } // Méthode pour remplir le formulaire avec les données du client void _fillFormWithClient(Client client) { _nomController.text = client.nom; _prenomController.text = client.prenom; _emailController.text = client.email; _telephoneController.text = client.telephone; _adresseController.text = client.adresse ?? ''; Get.snackbar( 'Client trouvé', 'Les informations ont été remplies automatiquement', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), ); } Widget _buildTextFormField({ required TextEditingController controller, required String label, TextInputType? keyboardType, String? Function(String?)? validator, int? maxLines, void Function(String)? onChanged, }) { return TextFormField( controller: controller, decoration: InputDecoration( labelText: label, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.white, ), keyboardType: keyboardType, validator: validator, maxLines: maxLines, onChanged: onChanged, ); } Widget _buildCommercialDropdown() { return DropdownButtonFormField( value: _selectedCommercialUser, decoration: InputDecoration( labelText: 'Commercial', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.white, ), items: _commercialUsers.map((Users user) { return DropdownMenuItem( value: user, child: Text('${user.name} ${user.lastName}'), ); }).toList(), onChanged: (Users? newValue) { setState(() { _selectedCommercialUser = newValue; }); }, validator: (value) => value == null ? 'Veuillez sélectionner un commercial' : null, ); } Widget _buildProductList() { final isMobile = MediaQuery.of(context).size.width < 600; return _filteredProducts.isEmpty ? _buildEmptyState() : ListView.builder( padding: const EdgeInsets.all(16.0), itemCount: _filteredProducts.length, itemBuilder: (context, index) { final product = _filteredProducts[index]; final quantity = _quantites[product.id] ?? 0; return _buildProductListItem(product, quantity, isMobile); }, ); } Widget _buildEmptyState() { return Center( child: Padding( padding: const EdgeInsets.all(32.0), child: Column( children: [ Icon( Icons.search_off, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( 'Aucun produit trouvé', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey.shade600, ), ), const SizedBox(height: 8), Text( 'Modifiez vos critères de recherche', style: TextStyle( fontSize: 14, color: Colors.grey.shade500, ), ), ], ), ), ); } Widget _buildProductListItem(Product product, int quantity, bool isMobile) { final bool isOutOfStock = product.stock != null && product.stock! <= 0; return Card( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: isOutOfStock ? Border.all(color: Colors.red.shade200, width: 1.5) : null, ), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ Container( width: isMobile ? 40 : 50, height: isMobile ? 40 : 50, decoration: BoxDecoration( color: isOutOfStock ? Colors.red.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.shopping_bag, size: isMobile ? 20 : 24, color: isOutOfStock ? Colors.red : Colors.blue, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: TextStyle( fontWeight: FontWeight.bold, fontSize: isMobile ? 14 : 16, color: isOutOfStock ? Colors.red.shade700 : null, ), ), const SizedBox(height: 4), Text( '${product.price.toStringAsFixed(2)} MGA', style: TextStyle( color: Colors.green.shade700, fontWeight: FontWeight.w600, fontSize: isMobile ? 12 : 14, ), ), 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 - plus compact sur mobile 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, ), ), ], ), ), Container( decoration: BoxDecoration( color: isOutOfStock ? Colors.grey.shade100 : 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 (quantity > 0) { setState(() { _quantites[product.id!] = quantity - 1; }); } }, ), Text( quantity.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 || quantity < product.stock!) { setState(() { _quantites[product.id!] = quantity + 1; }); } else { Get.snackbar( 'Stock insuffisant', 'Quantité demandée non disponible', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); } }, ), ], ), ), ], ), ), ), ); } void _showCartBottomSheet() { final isMobile = MediaQuery.of(context).size.width < 600; Get.bottomSheet( Container( height: MediaQuery.of(context).size.height * (isMobile ? 0.85 : 0.7), padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Votre Panier', style: TextStyle( fontSize: isMobile ? 18 : 20, fontWeight: FontWeight.bold ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Get.back(), ), ], ), const Divider(), Expanded(child: _buildCartItemsList()), const Divider(), _buildCartTotalSection(), const SizedBox(height: 16), _buildSubmitButton(), ], ), ), isScrollControlled: true, ); } Widget _buildCartItemsList() { final itemsInCart = _quantites.entries.where((e) => e.value > 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 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(() { _quantites.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), ), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), leading: Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.shopping_bag, size: 20), ), title: Text(product.name), subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} MGA'), trailing: Text( '${(entry.value * product.price).toStringAsFixed(2)} MGA', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.blue.shade800, ), ), ), ), ); }, ); } Widget _buildCartTotalSection() { double total = 0; _quantites.forEach((productId, quantity) { final product = _products.firstWhere((p) => p.id == productId); total += quantity * product.price; }); return Column( children: [ 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), Text( '${_quantites.values.where((q) => q > 0).length} article(s)', style: TextStyle(color: Colors.grey.shade600), ), ], ); } 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 ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( isMobile ? 'Valider' : 'Valider la Commande', style: TextStyle(fontSize: isMobile ? 14 : 16), ), ), ); } Future _submitOrder() async { // Vérifier d'abord si le panier est vide final itemsInCart = _quantites.entries.where((e) => e.value > 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(); // Ouvrir le panier pour montrer qu'il est vide return; } // Ensuite vérifier les informations client if (_nomController.text.isEmpty || _prenomController.text.isEmpty || _emailController.text.isEmpty || _telephoneController.text.isEmpty || _adresseController.text.isEmpty) { Get.snackbar( 'Informations manquantes', 'Veuillez remplir les informations client', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); _showClientFormDialog(); return; } setState(() { _isLoading = true; }); // Créer le client final client = Client( nom: _nomController.text, prenom: _prenomController.text, email: _emailController.text, telephone: _telephoneController.text, adresse: _adresseController.text, dateCreation: DateTime.now(), ); // Calculer le total et préparer les détails double total = 0; final details = []; for (final entry in itemsInCart) { final product = _products.firstWhere((p) => p.id == entry.key); total += entry.value * product.price; details.add(DetailCommande( commandeId: 0, produitId: product.id!, quantite: entry.value, prixUnitaire: product.price, sousTotal: entry.value * product.price, )); } // Créer la commande 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 - adapté pour mobile final isMobile = MediaQuery.of(context).size.width < 600; await showDialog( context: context, barrierDismissible: false, // Empêcher la fermeture accidentelle 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); // Vider complètement le formulaire et le panier _clearFormAndCart(); // Recharger les produits pour mettre à jour le stock _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, ); } } @override void dispose() { // Nettoyer les suggestions _hideAllSuggestions(); // Disposer les contrôleurs _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); _telephoneController.dispose(); _adresseController.dispose(); // Disposal des contrôleurs de filtre _searchNameController.dispose(); _searchImeiController.dispose(); _searchReferenceController.dispose(); super.dispose(); }}