import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:file_picker/file_picker.dart'; import 'package:open_file/open_file.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:excel/excel.dart' hide Border; import 'package:youmazgestion/Services/qrService.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.dart'; import '../Components/appDrawer.dart'; import '../Components/app_bar.dart'; import '../Models/produit.dart'; class ProductManagementPage extends StatefulWidget { const ProductManagementPage({super.key}); @override _ProductManagementPageState createState() => _ProductManagementPageState(); } class _ProductManagementPageState extends State { final AppDatabase _productDatabase = AppDatabase.instance; final AppDatabase _appDatabase = AppDatabase.instance; final UserController _userController = Get.find(); final pdfService = PdfPrintService(); List _products = []; List _filteredProducts = []; final TextEditingController _searchController = TextEditingController(); String _selectedCategory = 'Tous'; List _categories = ['Tous']; bool _isLoading = true; List> _pointsDeVente = []; String? _selectedPointDeVente; // Variables pour le scanner QRViewController? _qrController; bool _isScanning = false; bool _isAssigning = false; final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); Future _loadAvailableCategories() async { try { final categories = await _productDatabase.getCategories(); setState(() { _availableCategories = ['Non catégorisé', ...categories]; }); } catch (e) { debugPrint('Erreur lors du chargement des catégories: $e'); // Garder la catégorie par défaut en cas d'erreur } } // Catégories prédéfinies pour l'ajout de produits List _availableCategories = ['Non catégorisé']; bool _isUserSuperAdmin() { return _userController.role == 'Super Admin'; } // Variables pour l'import Excel (conservées du code original) bool _isImporting = false; double _importProgress = 0.0; String _importStatusText = ''; @override void initState() { super.initState(); _loadProducts(); _loadPointsDeVente(); _loadAvailableCategories(); _searchController.addListener(_filterProducts); } @override void dispose() { _qrController?.dispose(); _searchController.dispose(); super.dispose(); } void _showAddProductDialog() { final nameController = TextEditingController(); final priceController = TextEditingController(); final stockController = TextEditingController(); final descriptionController = TextEditingController(); final imageController = TextEditingController(); final referenceController = TextEditingController(); final marqueController = TextEditingController(); final ramController = TextEditingController(); final memoireInterneController = TextEditingController(); final imeiController = TextEditingController(); final newPointDeVenteController = TextEditingController(); String? selectedPointDeVente; List> pointsDeVente = []; bool isLoadingPoints = true; String selectedCategory = _availableCategories.last; // 'Non catégorisé' par défaut File? pickedImage; String? qrPreviewData; bool autoGenerateReference = true; bool showAddNewPoint = false; // 🎨 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, ), ], ), ); } // 🎨 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', '${NumberFormat('#,##0.00', 'fr_FR').format(product.price)} 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, ), ), ), ], ), ], ), ), ), ], ), ), ), ), ); } // Fonction pour mettre à jour le QR preview void updateQrPreview() { if (nameController.text.isNotEmpty) { final reference = autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim(); if (reference.isNotEmpty) { qrPreviewData = 'https://stock.guycom.mg/$reference'; } else { qrPreviewData = null; } } else { qrPreviewData = null; } } final AppDatabase _productDatabase = AppDatabase.instance; Future loadPointsDeVente(StateSetter setDialogState) async { try { final result = await _productDatabase.getPointsDeVente(); setDialogState(() { // Ajouter l'option "Aucun" à la liste pointsDeVente = [ {'id': null, 'nom': 'Aucun'}, // Option pour pointDeVenteId null ...result ]; isLoadingPoints = false; if (selectedPointDeVente == null && result.isNotEmpty) { selectedPointDeVente = 'Aucun'; // Par défaut, sélectionner "Aucun" } }); } catch (e) { setDialogState(() { isLoadingPoints = false; }); Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); } } Get.dialog( AlertDialog( title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.add_shopping_cart, color: Colors.green.shade700), ), const SizedBox(width: 12), const Text('Ajouter un produit'), ], ), content: Container( width: 600, constraints: const BoxConstraints(maxHeight: 600), child: SingleChildScrollView( child: StatefulBuilder( builder: (context, setDialogState) { // Charger les points de vente une seule fois if (isLoadingPoints && pointsDeVente.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { loadPointsDeVente(setDialogState); }); } return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Champs obligatoires Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.teal.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.teal.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.store, color: Colors.teal.shade700), const SizedBox(width: 8), Text( 'Point de vente', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.teal.shade700, ), ), ], ), const SizedBox(height: 12), if (isLoadingPoints) const Center(child: CircularProgressIndicator()) else Column( children: [ if (!showAddNewPoint) ...[ DropdownButtonFormField( value: selectedPointDeVente, items: pointsDeVente.map((point) { return DropdownMenuItem( value: point['nom'] as String, child: Text(point['nom'] as String), ); }).toList(), onChanged: (value) { setDialogState( () => selectedPointDeVente = value); }, decoration: const InputDecoration( labelText: 'Sélectionner un point de vente', border: OutlineInputBorder(), prefixIcon: Icon(Icons.store), filled: true, fillColor: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ TextButton.icon( onPressed: () { setDialogState(() { showAddNewPoint = true; newPointDeVenteController.clear(); }); }, icon: const Icon(Icons.add, size: 16), label: const Text('Ajouter nouveau point'), style: TextButton.styleFrom( foregroundColor: Colors.teal.shade700, ), ), const Spacer(), TextButton.icon( onPressed: () => loadPointsDeVente(setDialogState), icon: const Icon(Icons.refresh, size: 16), label: const Text('Actualiser'), ), ], ), ], if (showAddNewPoint) ...[ DropdownButtonFormField( value: selectedPointDeVente, items: pointsDeVente.map((point) { return DropdownMenuItem( value: point['nom'] as String, child: Text(point['nom'] as String), ); }).toList(), onChanged: (value) { setDialogState( () => selectedPointDeVente = value); }, decoration: const InputDecoration( labelText: 'Sélectionner un point de vente', border: OutlineInputBorder(), prefixIcon: Icon(Icons.store), filled: true, fillColor: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ TextButton.icon( onPressed: () { setDialogState(() { showAddNewPoint = true; newPointDeVenteController.clear(); }); }, icon: const Icon(Icons.add, size: 16), label: const Text('Ajouter nouveau point'), style: TextButton.styleFrom( foregroundColor: Colors.teal.shade700, ), ), const Spacer(), TextButton.icon( onPressed: () => loadPointsDeVente(setDialogState), icon: const Icon(Icons.refresh, size: 16), label: const Text('Actualiser'), ), ], ), ], if (showAddNewPoint) ...[ TextField( controller: newPointDeVenteController, decoration: const InputDecoration( labelText: 'Nom du nouveau point de vente', border: OutlineInputBorder(), prefixIcon: Icon(Icons.add_business), filled: true, fillColor: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ TextButton( onPressed: () { setDialogState(() { showAddNewPoint = false; newPointDeVenteController.clear(); }); }, child: const Text('Annuler'), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: () async { final nom = newPointDeVenteController .text .trim(); if (nom.isNotEmpty) { try { final id = await _productDatabase .getOrCreatePointDeVenteByNom( nom); if (id != null) { setDialogState(() { showAddNewPoint = false; selectedPointDeVente = nom; newPointDeVenteController .clear(); }); // Recharger la liste await loadPointsDeVente( setDialogState); Get.snackbar( 'Succès', 'Point de vente "$nom" créé avec succès', backgroundColor: Colors.green, colorText: Colors.white, ); } } catch (e) { Get.snackbar('Erreur', 'Impossible de créer le point de vente: $e'); } } }, icon: const Icon(Icons.save, size: 16), label: const Text('Créer'), style: ElevatedButton.styleFrom( backgroundColor: Colors.teal, foregroundColor: Colors.white, ), ), ], ), ], ], ), ], ), ), const SizedBox(height: 16), // Nom du produit TextField( controller: nameController, decoration: InputDecoration( labelText: 'Nom du produit *', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.shopping_bag), filled: true, fillColor: Colors.grey.shade50, ), onChanged: (value) { setDialogState(() { updateQrPreview(); }); }, ), const SizedBox(height: 16), // Prix et Stock sur la même ligne Row( children: [ Expanded( child: TextField( controller: priceController, keyboardType: const TextInputType.numberWithOptions( decimal: true), decoration: InputDecoration( labelText: 'Prix (MGA) *', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.attach_money), filled: true, fillColor: Colors.grey.shade50, ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: stockController, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: 'Stock initial', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.inventory), filled: true, fillColor: Colors.grey.shade50, ), ), ), ], ), const SizedBox(height: 16), // Catégorie DropdownButtonFormField( value: selectedCategory, items: _availableCategories .map((category) => DropdownMenuItem( value: category, child: Text(category))) .toList(), onChanged: (value) { setDialogState(() => selectedCategory = value!); }, decoration: InputDecoration( labelText: 'Catégorie', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.category), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 16), // Description TextField( controller: descriptionController, maxLines: 3, decoration: InputDecoration( labelText: 'Description', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.description), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 16), // Section Référence Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.purple.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.purple.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.confirmation_number, color: Colors.purple.shade700), const SizedBox(width: 8), Text( 'Référence du produit', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.purple.shade700, ), ), ], ), const SizedBox(height: 12), Row( children: [ Checkbox( value: autoGenerateReference, onChanged: (value) { setDialogState(() { autoGenerateReference = value!; updateQrPreview(); }); }, ), const Text('Générer automatiquement'), ], ), const SizedBox(height: 8), if (!autoGenerateReference) TextField( controller: referenceController, decoration: const InputDecoration( labelText: 'Référence *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.tag), filled: true, fillColor: Colors.white, ), onChanged: (value) { setDialogState(() { updateQrPreview(); }); }, ), if (autoGenerateReference) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(4), ), child: Text( 'Référence générée automatiquement', style: TextStyle(color: Colors.grey.shade700), ), ), ], ), ), const SizedBox(height: 16), // Nouveaux champs (Marque, RAM, Mémoire interne, IMEI) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.memory, color: Colors.orange.shade700), const SizedBox(width: 8), Text( 'Spécifications techniques', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.orange.shade700, ), ), ], ), const SizedBox(height: 12), TextField( controller: marqueController, decoration: const InputDecoration( labelText: 'Marque', border: OutlineInputBorder(), prefixIcon: Icon(Icons.branding_watermark), filled: true, fillColor: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: ramController, decoration: const InputDecoration( labelText: 'RAM', border: OutlineInputBorder(), prefixIcon: Icon(Icons.memory), filled: true, fillColor: Colors.white, ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: memoireInterneController, decoration: const InputDecoration( labelText: 'Mémoire interne', border: OutlineInputBorder(), prefixIcon: Icon(Icons.storage), filled: true, fillColor: Colors.white, ), ), ), ], ), const SizedBox(height: 8), TextField( controller: imeiController, decoration: const InputDecoration( labelText: 'IMEI (pour téléphones)', border: OutlineInputBorder(), prefixIcon: Icon(Icons.smartphone), filled: true, fillColor: Colors.white, ), ), ], ), ), const SizedBox(height: 16), // Section Image Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.image, color: Colors.blue.shade700), const SizedBox(width: 8), Text( 'Image du produit (optionnel)', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.blue.shade700, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: imageController, decoration: const InputDecoration( labelText: 'Chemin de l\'image', border: OutlineInputBorder(), isDense: true, ), readOnly: true, ), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: () async { final result = await FilePicker.platform .pickFiles(type: FileType.image); if (result != null && result.files.single.path != null) { setDialogState(() { pickedImage = File(result.files.single.path!); imageController.text = pickedImage!.path; }); } }, icon: const Icon(Icons.folder_open, size: 16), label: const Text('Choisir'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.all(12), ), ), ], ), const SizedBox(height: 12), // Aperçu de l'image if (pickedImage != null) Center( child: Container( height: 100, width: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file(pickedImage!, fit: BoxFit.cover), ), ), ), ], ), ), const SizedBox(height: 16), // Aperçu QR Code if (qrPreviewData != null) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade200), ), child: Column( children: [ Row( children: [ Icon(Icons.qr_code_2, color: Colors.green.shade700), const SizedBox(width: 8), Text( 'Aperçu du QR Code', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.green.shade700, ), ), ], ), const SizedBox(height: 12), Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: QrImageView( data: qrPreviewData!, version: QrVersions.auto, size: 80, backgroundColor: Colors.white, ), ), ), const SizedBox(height: 8), Text( 'Réf: ${autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim()}', style: const TextStyle( fontSize: 10, color: Colors.grey), ), ], ), ), ], ); }, ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton.icon( onPressed: () async { final name = nameController.text.trim(); final price = double.tryParse(priceController.text.trim()) ?? 0.0; final stock = int.tryParse(stockController.text.trim()) ?? 0; if (name.isEmpty || price <= 0) { Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); return; } // Vérification de la référence String finalReference; if (autoGenerateReference) { finalReference = _generateUniqueReference(); } else { finalReference = referenceController.text.trim(); if (finalReference.isEmpty) { Get.snackbar('Erreur', 'La référence est obligatoire'); return; } final existingProduct = await _productDatabase .getProductByReference(finalReference); if (existingProduct != null) { Get.snackbar('Erreur', 'Cette référence existe déjà'); return; } } // Gérer le point de vente int? pointDeVenteId; String? finalPointDeVenteNom; if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { finalPointDeVenteNom = newPointDeVenteController.text.trim(); } else if (selectedPointDeVente != null && selectedPointDeVente != 'Aucun') { finalPointDeVenteNom = selectedPointDeVente; } if (finalPointDeVenteNom != null) { pointDeVenteId = await _productDatabase .getOrCreatePointDeVenteByNom(finalPointDeVenteNom); } // Si "Aucun" est sélectionné, pointDeVenteId reste null try { final qrPath = await _generateAndSaveQRCode(finalReference); final product = Product( name: name, price: price, image: imageController.text, category: selectedCategory, description: descriptionController.text.trim(), stock: stock, qrCode: qrPath, reference: finalReference, marque: marqueController.text.trim(), ram: ramController.text.trim(), memoireInterne: memoireInterneController.text.trim(), imei: imeiController.text.trim(), pointDeVenteId: pointDeVenteId, // Peut être null si "Aucun" ); await _productDatabase.createProduct(product); Get.back(); Get.snackbar( 'Succès', 'Produit ajouté avec succès!\nRéférence: $finalReference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 4), icon: const Icon(Icons.check_circle, color: Colors.white), ); _loadProducts(); _loadPointsDeVente(); } catch (e) { Get.snackbar('Erreur', 'Ajout du produit échoué: $e'); } }, icon: const Icon(Icons.save), label: const Text('Ajouter le produit'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), ), ], ), ); } // === FONCTIONS DE SCAN POUR ATTRIBUTION POINT DE VENTE === String? _getColumnValue( List row, Map mapping, String field) { if (!mapping.containsKey(field)) return null; int columnIndex = mapping[field]!; if (columnIndex >= row.length || row[columnIndex]?.value == null) return null; var cellValue = row[columnIndex]!.value; // 🔥 TRAITEMENT SPÉCIAL POUR IMEI if (field == 'imei') { print('🔍 IMEI brut depuis Excel: $cellValue (${cellValue.runtimeType})'); // Si c'est un nombre (notation scientifique) if (cellValue is num) { // Convertir directement en entier pour éviter les décimales String imeiStr = cellValue.toInt().toString(); print('🔄 IMEI converti depuis num: $imeiStr'); return imeiStr; } // Si c'est déjà un String String strValue = cellValue.toString().trim(); // Gérer la notation scientifique dans les strings if (strValue.contains('E') || strValue.contains('e')) { try { // Remplacer virgule par point et parser String normalized = strValue.replaceAll(',', '.'); double numValue = double.parse(normalized); String imeiStr = numValue.toInt().toString(); print('🔄 IMEI converti depuis notation scientifique: $strValue → $imeiStr'); return imeiStr; } catch (e) { print('❌ Erreur conversion IMEI: $e'); return null; } } return strValue; } // Pour les autres champs return cellValue.toString().trim(); } void _startPointDeVenteAssignmentScanning() { if (_isScanning) return; // Vérifier que l'utilisateur a un point de vente if (_userController.pointDeVenteId <= 0) { Get.snackbar( 'Erreur', 'Vous n\'avez pas de point de vente assigné. Contactez un administrateur.', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red.shade600, colorText: Colors.white, duration: const Duration(seconds: 4), icon: const Icon(Icons.error, color: Colors.white), ); return; } setState(() { _isScanning = true; }); Get.to(() => _buildAssignmentScannerPage())?.then((_) { setState(() { _isScanning = false; }); }); } Map _normalizeRowData( List row, Map mapping, int rowIndex) { final normalizedData = {}; // Fonction interne pour nettoyer et normaliser les valeurs String? _cleanValue(String? value) { if (value == null) return null; return value.toString().trim(); } // Fonction simple pour les nombres double? _normalizeNumber(String? value) { if (value == null || value.isEmpty) return null; final cleaned = value.replaceAll(',', '.').replaceAll(RegExp(r'\s+'), ''); final numericString = cleaned.replaceAll(RegExp(r'[^0-9.]'), ''); return double.tryParse(numericString); } // Normalisation du nom if (mapping.containsKey('name')) { final name = _cleanValue(_getColumnValue(row, mapping, 'name')); if (name != null && name.isNotEmpty) { normalizedData['name'] = name; } } // Normalisation du prix if (mapping.containsKey('price')) { final priceValue = _cleanValue(_getColumnValue(row, mapping, 'price')); final price = _normalizeNumber(priceValue); if (price != null && price > 0) { normalizedData['price'] = price; print('✅ Prix normalisé: $price'); } } // Normalisation de la référence if (mapping.containsKey('reference')) { final reference = _cleanValue(_getColumnValue(row, mapping, 'reference')); if (reference != null && reference.isNotEmpty) { normalizedData['reference'] = reference; } else { normalizedData['reference'] = _generateUniqueReference(); } } // Normalisation de la catégorie if (mapping.containsKey('category')) { final category = _cleanValue(_getColumnValue(row, mapping, 'category')); normalizedData['category'] = category ?? 'Non catégorisé'; } else { normalizedData['category'] = 'Non catégorisé'; } // Normalisation de la marque if (mapping.containsKey('marque')) { final marque = _cleanValue(_getColumnValue(row, mapping, 'marque')); if (marque != null && marque.isNotEmpty) { normalizedData['marque'] = marque; } } // Normalisation de la RAM if (mapping.containsKey('ram')) { final ram = _cleanValue(_getColumnValue(row, mapping, 'ram')); if (ram != null && ram.isNotEmpty) { final ramValue = ram.replaceAll('GB', 'Go').replaceAll('go', 'Go'); normalizedData['ram'] = ramValue; } } // Normalisation de la mémoire interne if (mapping.containsKey('memoire_interne')) { final memoire = _cleanValue(_getColumnValue(row, mapping, 'memoire_interne')); if (memoire != null && memoire.isNotEmpty) { final memoireValue = memoire.replaceAll('GB', 'Go').replaceAll('go', 'Go'); normalizedData['memoire_interne'] = memoireValue; } } // 🔥 IMPORTANT: Normaliser l'IMEI EN PREMIER avant de gérer le stock // 🔥 Normalisation de l'IMEI (simplifié car _getColumnValue le gère maintenant) String? imeiValue; if (mapping.containsKey('imei')) { final imei = _cleanValue(_getColumnValue(row, mapping, 'imei')); if (imei != null && imei.isNotEmpty) { // Nettoyer les espaces et tirets String cleanedImei = imei.replaceAll(RegExp(r'[\s-]'), ''); // Vérifier que c'est bien un IMEI valide (10-15 chiffres) if (cleanedImei.length >= 10 && cleanedImei.length <= 15 && RegExp(r'^\d+$').hasMatch(cleanedImei)) { imeiValue = cleanedImei.length > 15 ? cleanedImei.substring(0, 15) : cleanedImei; normalizedData['imei'] = imeiValue; print('✅ IMEI valide enregistré: $imeiValue'); } else { print('⚠️ IMEI invalide ignoré: "$cleanedImei" (longueur: ${cleanedImei.length})'); } } } // Le reste du code reste identique... // Normalisation du point de vente if (mapping.containsKey('point_de_vente')) { final pv = _cleanValue(_getColumnValue(row, mapping, 'point_de_vente')); if (pv != null && pv.isNotEmpty) { normalizedData['point_de_vente'] = pv.replaceAll(RegExp(r'\s+'), ' ').trim(); } } // Valeurs par défaut normalizedData['description'] = ''; // 🎯 LOGIQUE CRITIQUE: Gestion du stock selon la présence d'IMEI // On vérifie si normalizedData['imei'] existe ET n'est pas vide if (normalizedData.containsKey('imei') && normalizedData['imei'] != null && normalizedData['imei'].toString().isNotEmpty) { // Produit avec IMEI → stock forcé à 1 normalizedData['stock'] = 1; print('🔒 Stock forcé à 1 car IMEI présent: ${normalizedData['imei']}'); } else { // Produit sans IMEI → utiliser le stock du fichier if (mapping.containsKey('stock')) { final stockValue = _cleanValue(_getColumnValue(row, mapping, 'stock')); // Try parsing as int first int? stock = int.tryParse(stockValue ?? ''); // If parsing as int fails, try parsing as double and convert to int if (stock == null && stockValue != null && stockValue.isNotEmpty) { final doubleValue = double.tryParse(stockValue); if (doubleValue != null) { stock = doubleValue.toInt(); } } // Final fallback: ensure at least 1 stock ??= 1; // Never allow 0 or negative values normalizedData['stock'] = stock > 0 ? stock : 1; print('📦 Stock depuis Excel: $stock (pas d\'IMEI)'); } else { normalizedData['stock'] = 1; print('📦 Stock par défaut: 1 (pas d\'IMEI, pas de colonne stock)'); } } // Validation des données obligatoires if (normalizedData['name'] == null || normalizedData['price'] == null) { throw Exception( 'Ligne ${rowIndex + 1}: Données obligatoires manquantes (nom ou prix)'); } return normalizedData; } Map _mapHeaders(List headerRow) { Map columnMapping = {}; for (int i = 0; i < headerRow.length; i++) { if (headerRow[i]?.value == null) continue; String header = headerRow[i]!.value.toString().trim().toUpperCase(); print('📋 En-tête colonne $i: "$header"'); // Nom du produit if ((header.contains('NOM') && header.contains('PRODUIT')) || header == 'NOM') { columnMapping['name'] = i; print(' ✅ Mappé vers name'); } // Référence else if (header.contains('REFERENCE')) { columnMapping['reference'] = i; print(' ✅ Mappé vers reference'); } // Catégorie - VERSION TOLÉRANTE ⭐ else if (header.contains('CATEG') && header.contains('PRODUIT')) { columnMapping['category'] = i; print(' ✅ Mappé vers category'); } // Marque else if (header == 'MARQUE' || header == 'BRAND') { columnMapping['marque'] = i; print(' ✅ Mappé vers marque'); } // RAM else if (header == 'RAM' || header.contains('MEMOIRE RAM')) { columnMapping['ram'] = i; print(' ✅ Mappé vers ram'); } // Mémoire interne else if (header == 'INTERNE' || header.contains('MEMOIRE INTERNE') || header.contains('STOCKAGE')) { columnMapping['memoire_interne'] = i; print(' ✅ Mappé vers memoire_interne'); } // IMEI else if (header == 'IMEI' || header.contains('NUMERO IMEI')) { columnMapping['imei'] = i; print(' ✅ Mappé vers imei'); } // Prix else if (header == 'PRIX' || header == 'PRICE') { columnMapping['price'] = i; print(' ✅ Mappé vers price'); } // Stock else if (header == 'STOCK' || header == 'QUANTITY' || header == 'QTE') { columnMapping['stock'] = i; print(' ✅ Mappé vers stock'); } // Point de vente else if (header.contains('BOUTIQUE') || header.contains('POINT') || header == 'MAGASIN') { columnMapping['point_de_vente'] = i; print(' ✅ Mappé vers point_de_vente'); } else { print(' ⚠️ Non reconnu'); } } print('\n🎯 MAPPING FINAL: $columnMapping\n'); return columnMapping; } Widget _buildAssignmentScannerPage() { return Scaffold( appBar: AppBar( title: const Text('Scanner pour Attribution'), backgroundColor: Colors.orange.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: _onAssignmentQRViewCreated, overlay: QrScannerOverlayShape( borderColor: Colors.orange, 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: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.assignment, color: Colors.orange.shade300, size: 40), const SizedBox(height: 8), const Text( 'Scanner l\'IMEI pour assigner au point de vente', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), const SizedBox(height: 4), Text( 'Point de vente: ${_userController.pointDeVenteDesignation}', style: TextStyle( color: Colors.orange.shade300, fontSize: 14, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ], ), ), ), ], ), ); } void _onAssignmentQRViewCreated(QRViewController controller) { _qrController = controller; controller.scannedDataStream.listen((scanData) { if (scanData.code != null && scanData.code!.isNotEmpty) { // Pauser le scanner pour éviter les scans multiples controller.pauseCamera(); // Fermer la page du scanner Get.back(); // Traiter le résultat _assignProductToUserPointDeVente(scanData.code!); } }); } Future _assignProductToUserPointDeVente(String scannedImei) async { if (_isAssigning) return; setState(() { _isAssigning = true; }); try { // Montrer un indicateur de chargement Get.dialog( AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: Colors.orange.shade700), const SizedBox(height: 16), const Text('Recherche du produit...'), const SizedBox(height: 8), Text( 'IMEI: $scannedImei', 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)); // Chercher le produit avec l'IMEI scanné Product? foundProduct = await _productDatabase.getProductByIMEI(scannedImei); // Fermer l'indicateur de chargement Get.back(); if (foundProduct == null) { _showProductNotFoundDialog(scannedImei); return; } // Vérifier si le produit a déjà le bon point de vente if (foundProduct.pointDeVenteId == _userController.pointDeVenteId) { _showAlreadyAssignedDialog(foundProduct); return; } // Assigner le point de vente de l'utilisateur au produit // final updatedProduct = Product( // id: foundProduct.id, // name: foundProduct.name, // price: foundProduct.price, // image: foundProduct.image, // category: foundProduct.category, // description: foundProduct.description, // stock: foundProduct.stock, // qrCode: foundProduct.qrCode, // reference: foundProduct.reference, // marque: foundProduct.marque, // ram: foundProduct.ram, // memoireInterne: foundProduct.memoireInterne, // imei: foundProduct.imei, // pointDeVenteId: // _userController.pointDeVenteId, // Nouveau point de vente // ); await _appDatabase.createDemandeTransfert( produitId: foundProduct.id!, pointDeVenteSourceId: _userController.pointDeVenteId, pointDeVenteDestinationId: _userController.pointDeVenteId, demandeurId: _userController.userId, quantite: foundProduct.stock, notes: 'produit non assigner', ); // await _productDatabase.updateProduct(updatedProduct); // Recharger les produits pour refléter les changements _loadProducts(); // Afficher le dialogue de succès _showAssignmentSuccessDialog(foundProduct); } catch (e) { // Fermer l'indicateur de chargement si il est encore ouvert if (Get.isDialogOpen!) Get.back(); Get.snackbar( 'Erreur', 'Une erreur est survenue: ${e.toString()}', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red.shade600, colorText: Colors.white, duration: const Duration(seconds: 3), ); } finally { setState(() { _isAssigning = false; }); } } void _showAssignmentSuccessDialog(Product product) { Get.dialog( AlertDialog( title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.check_circle, color: Colors.green.shade700), ), const SizedBox(width: 12), const Expanded(child: Text( 'demande attribution réussie en attente de validation!')), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text('IMEI: ${product.imei}'), Text('Prix: ${NumberFormat('#,##0.00', 'fr_FR').format(product.price)} MGA'), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Produit assigné au point de vente:', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Text( _userController.pointDeVenteDesignation, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.green.shade700, ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Fermer'), ), ElevatedButton( onPressed: () { Get.back(); _startPointDeVenteAssignmentScanning(); // Scanner un autre produit }, style: ElevatedButton.styleFrom( backgroundColor: Colors.orange.shade700, foregroundColor: Colors.white, ), child: const Text('Scanner encore'), ), ], ), ); } void _showAlreadyAssignedDialog(Product product) { Get.dialog( AlertDialog( title: Row( children: [ Icon(Icons.info, color: Colors.blue.shade600), const SizedBox(width: 8), const Text('Déjà assigné'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text('IMEI: ${product.imei}'), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Ce produit est déjà assigné au point de vente:', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Text( _userController.pointDeVenteDesignation, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blue.shade700, ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Fermer'), ), ElevatedButton( onPressed: () { Get.back(); _startPointDeVenteAssignmentScanning(); // Scanner un autre produit }, style: ElevatedButton.styleFrom( backgroundColor: Colors.orange.shade700, foregroundColor: Colors.white, ), child: const Text('Scanner encore'), ), ], ), ); } void _showProductNotFoundDialog(String scannedImei) { 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 cet IMEI:'), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(4), ), child: Text( scannedImei, style: const TextStyle( fontFamily: 'monospace', fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 12), Text( 'Vérifiez que l\'IMEI est correct ou que le produit existe dans la base de données.', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Fermer'), ), ElevatedButton( onPressed: () { Get.back(); _startPointDeVenteAssignmentScanning(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.orange.shade700, foregroundColor: Colors.white, ), child: const Text('Scanner à nouveau'), ), ], ), ); } Widget _buildAssignmentScanCard() { final isMobile = MediaQuery.of(context).size.width < 600; 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.orange.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.assignment, color: Colors.orange.shade700, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Assigner produits à votre point de vente', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color.fromARGB(255, 9, 56, 95), ), ), const SizedBox(height: 4), Text( 'Point de vente: ${_userController.pointDeVenteDesignation}', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ), ), ElevatedButton.icon( onPressed: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) ? null : _startPointDeVenteAssignmentScanning, icon: (_isScanning || _isAssigning) ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.qr_code_scanner, size: 18), label: Text((_isScanning || _isAssigning) ? 'Scan...' : 'Assigner'), style: ElevatedButton.styleFrom( backgroundColor: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) ? Colors.grey : Colors.orange.shade700, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ], ), ), ); } // === FONCTIONS CONSERVÉES DU CODE ORIGINAL === // [Conservez toutes les autres méthodes du code original ici] // Réinitialisation de l'état d'import void _resetImportState() { setState(() { _isImporting = false; _importProgress = 0.0; _importStatusText = ''; }); } Future _loadPointsDeVente() async { try { final points = await _productDatabase.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'); } } Future _loadProducts() async { setState(() => _isLoading = true); try { await _productDatabase.initDatabase(); final products = await _productDatabase.getProducts(); final categories = await _productDatabase.getCategories(); setState(() { _products = products; _filteredProducts = products; _categories = ['Tous', ...categories]; _isLoading = false; }); } catch (e) { setState(() => _isLoading = false); Get.snackbar('Erreur', 'Impossible de charger les produits: $e'); } } void _filterProducts() { final query = _searchController.text.toLowerCase(); setState(() { _filteredProducts = _products.where((product) { final matchesSearch = product.name.toLowerCase().contains(query) || product.description!.toLowerCase().contains(query) || product.reference!.toLowerCase().contains(query); final matchesCategory = _selectedCategory == 'Tous' || product.category == _selectedCategory; return matchesSearch && matchesCategory; }).toList(); }); } // Méthode pour générer une référence unique String _generateUniqueReference() { final timestamp = DateTime.now().millisecondsSinceEpoch; final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0'); return 'PROD_${timestamp}${randomSuffix}'; } Future _downloadExcelTemplate() async { try { final excel = Excel.createExcel(); final sheet = excel['Sheet1']; // En-têtes modifiés sans DESCRIPTION et STOCK final headers = [ 'ID PRODUITS', // Sera ignoré lors de l'import 'NOM DU PRODUITS', // name 'REFERENCE PRODUITS', // reference 'CATEGORIES PRODUITS', // category 'MARQUE', // marque 'RAM', // ram 'INTERNE', // memoire_interne 'IMEI', // imei 'STOCK', 'PRIX', // price 'BOUTIQUE', // point_de_vente ]; // Ajouter les en-têtes avec style for (int i = 0; i < headers.length; i++) { final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0)); cell.value = headers[i]; cell.cellStyle = CellStyle( bold: true, backgroundColorHex: '#E8F4FD', horizontalAlign: HorizontalAlign.Center, ); } // Exemples modifiés sans DESCRIPTION et STOCK final examples = [ [ '1', // ID PRODUITS (sera ignoré) 'Smartphone Galaxy S24', // NOM DU PRODUITS 'SGS24-001', // REFERENCE PRODUITS 'Téléphone', // CATEGORIES PRODUITS 'Samsung', // MARQUE '8 Go', // RAM '256 Go', // INTERNE '123456789012345', // IMEI '1200.00', // PRIX '405A', // BOUTIQUE ], [ '2', // ID PRODUITS 'iPhone 15 Pro', // NOM DU PRODUITS 'IP15P-001', // REFERENCE PRODUITS 'Téléphone', // CATEGORIES PRODUITS 'Apple', // MARQUE '8 Go', // RAM '512 Go', // INTERNE '987654321098765', // IMEI '1599.00', // PRIX '405B', // BOUTIQUE ], [ '3', // ID PRODUITS 'MacBook Pro 14"', // NOM DU PRODUITS 'MBP14-001', // REFERENCE PRODUITS 'Informatique', // CATEGORIES PRODUITS 'Apple', // MARQUE '16 Go', // RAM '1 To', // INTERNE '', // IMEI (vide pour un ordinateur) '2499.00', // PRIX 'S405A', // BOUTIQUE ], [ '4', // ID PRODUITS 'iPad Air', // NOM DU PRODUITS 'IPA-001', // REFERENCE PRODUITS 'Tablette', // CATEGORIES PRODUITS 'Apple', // MARQUE '8 Go', // RAM '256 Go', // INTERNE '456789123456789', // IMEI '699.00', // PRIX '405A', // BOUTIQUE ], [ '5', // ID PRODUITS 'Gaming Laptop ROG', // NOM DU PRODUITS 'ROG-001', // REFERENCE PRODUITS 'Informatique', // CATEGORIES PRODUITS 'ASUS', // MARQUE '32 Go', // RAM '1 To', // INTERNE '', // IMEI (vide) '1899.00', // PRIX '405B', // BOUTIQUE ] ]; // Ajouter les exemples for (int row = 0; row < examples.length; row++) { for (int col = 0; col < examples[row].length; col++) { final cell = sheet.cell( CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row + 1)); cell.value = examples[row][col]; // Style pour les données (prix en gras) if (col == 8) { // Colonne PRIX cell.cellStyle = CellStyle( bold: true, ); } } } // Ajuster la largeur des colonnes (sans DESCRIPTION et STOCK) sheet.setColWidth(0, 12); // ID PRODUITS sheet.setColWidth(1, 25); // NOM DU PRODUITS sheet.setColWidth(2, 18); // REFERENCE PRODUITS sheet.setColWidth(3, 18); // CATEGORIES PRODUITS sheet.setColWidth(4, 15); // MARQUE sheet.setColWidth(5, 10); // RAM sheet.setColWidth(6, 12); // INTERNE sheet.setColWidth(7, 18); // IMEI sheet.setColWidth(8, 12); // PRIX sheet.setColWidth(9, 12); // BOUTIQUE // Ajouter une feuille d'instructions mise à jour final instructionSheet = excel['Instructions']; final instructions = [ ['INSTRUCTIONS D\'IMPORTATION'], [''], ['Format des colonnes:'], ['• ID PRODUITS: Numéro d\'identification (ignoré lors de l\'import)'], ['• NOM DU PRODUITS: Nom du produit (OBLIGATOIRE)'], ['• REFERENCE PRODUITS: Référence unique du produit'], ['• CATEGORIES PRODUITS: Catégorie du produit'], ['• MARQUE: Marque du produit'], ['• RAM: Mémoire RAM (ex: "8 Go", "16 Go")'], ['• INTERNE: Stockage interne (ex: "256 Go", "1 To")'], ['• IMEI: Numéro IMEI (pour les appareils mobiles)'], ['• PRIX: Prix du produit en euros (OBLIGATOIRE)'], ['• BOUTIQUE: Code du point de vente'], [''], ['Remarques importantes:'], ['• Les colonnes NOM DU PRODUITS et PRIX sont obligatoires'], ['• Si CATEGORIES PRODUITS est vide, "Non catégorisé" sera utilisé'], [ '• Si REFERENCE PRODUITS est vide, une référence sera générée automatiquement' ], ['• Le stock sera automatiquement initialisé à 1 pour chaque produit'], ['• La description sera automatiquement vide pour chaque produit'], ['• Les colonnes peuvent être dans n\'importe quel ordre'], ['• Vous pouvez supprimer les colonnes non utilisées'], [''], ['Formats acceptés:'], ['• PRIX: 1200.00 ou 1200,00 ou 1200'], ['• RAM/INTERNE: Texte libre (ex: "8 Go", "256 Go", "1 To")'], ]; for (int i = 0; i < instructions.length; i++) { final cell = instructionSheet .cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i)); cell.value = instructions[i][0]; if (i == 0) { // Titre cell.cellStyle = CellStyle( bold: true, fontSize: 16, backgroundColorHex: '#4CAF50', fontColorHex: '#FFFFFF', ); } else if (instructions[i][0].startsWith('•')) { // Points de liste cell.cellStyle = CellStyle( italic: true, ); } else if (instructions[i][0].endsWith(':')) { // Sous-titres cell.cellStyle = CellStyle( bold: true, backgroundColorHex: '#F5F5F5', ); } } // Ajuster la largeur de la colonne instructions instructionSheet.setColWidth(0, 80); final bytes = excel.save(); if (bytes == null) { Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); return; } final String? outputFile = await FilePicker.platform.saveFile( fileName: 'modele_import_produits_v3.xlsx', allowedExtensions: ['xlsx'], type: FileType.custom, ); if (outputFile != null) { try { await File(outputFile).writeAsBytes(bytes); Get.snackbar( 'Succès', 'Modèle téléchargé avec succès\n$outputFile\n\nConsultez l\'onglet "Instructions" pour plus d\'informations.', duration: const Duration(seconds: 6), backgroundColor: Colors.green, colorText: Colors.white, ); } catch (e) { Get.snackbar('Erreur', 'Impossible d\'écrire le fichier: $e'); } } } catch (e) { Get.snackbar('Erreur', 'Erreur lors de la création du modèle: $e'); debugPrint('Erreur création modèle Excel: $e'); } } // Détecter si une valeur semble être un format de date/temps suspect bool _isSuspiciousDateFormat(dynamic value) { if (value == null) return false; String valueStr = value.toString(); // Détecter les formats de date suspects qui devraient être des nombres if (valueStr.contains('-') && valueStr.contains(':')) { // Format DateTime détecté print('🔍 Format DateTime suspect: $valueStr'); return true; } // Détecter les très grands nombres (timestamps en millisecondes) if (valueStr.length > 10 && !valueStr.contains('.')) { double? numValue = double.tryParse(valueStr); if (numValue != null && numValue > 10000000000) { print('🔍 Grand nombre suspect: $valueStr'); return true; } } return false; } // Identifier les colonnes qui devraient contenir des nombres List _identifyNumberColumns(List headerRow) { List numberColumns = []; for (int i = 0; i < headerRow.length; i++) { if (headerRow[i]?.value == null) continue; String header = headerRow[i]!.value.toString().trim().toUpperCase(); // Identifier les en-têtes qui correspondent à des valeurs numériques if (_isNumericHeader(header)) { numberColumns.add(i); print('📊 Colonne numérique: "$header" (index $i)'); } } return numberColumns; } // Vérifier si un en-tête correspond à une colonne numérique bool _isNumericHeader(String header) { List numericHeaders = [ 'PRIX', 'PRICE', 'COST', 'COUT', 'MONTANT', 'AMOUNT', 'TOTAL', 'QUANTITE', 'QUANTITY', 'QTE', 'STOCK', 'NOMBRE', 'NUMBER', 'TAILLE', 'SIZE', 'POIDS', 'WEIGHT', 'RAM', 'MEMOIRE', 'STORAGE', 'STOCKAGE' ]; return numericHeaders.any((keyword) => header.contains(keyword)); } // Fonction de débogage pour analyser le fichier Excel void _debugExcelFile(Excel excel) { print('=== DEBUG EXCEL FILE ==='); print('Nombre de feuilles: ${excel.tables.length}'); for (var sheetName in excel.tables.keys) { print('Feuille: $sheetName'); var sheet = excel.tables[sheetName]!; print('Nombre de lignes: ${sheet.rows.length}'); if (sheet.rows.isNotEmpty) { print('En-têtes (première ligne):'); for (int i = 0; i < sheet.rows[0].length; i++) { var cellValue = sheet.rows[0][i]?.value; print(' Colonne $i: "$cellValue" (${cellValue.runtimeType})'); } if (sheet.rows.length > 1) { print('Première ligne de données:'); for (int i = 0; i < sheet.rows[1].length; i++) { var cellValue = sheet.rows[1][i]?.value; print(' Colonne $i: "$cellValue"'); } } } } print('=== FIN DEBUG ==='); } Excel _fixExcelNumberFormats(Excel excel) { print('🔧 Correction des formats de cellules Excel...'); for (var sheetName in excel.tables.keys) { print('📋 Traitement de la feuille: $sheetName'); var sheet = excel.tables[sheetName]!; if (sheet.rows.isEmpty) continue; // Analyser la première ligne pour identifier les colonnes de prix/nombres List numberColumns = _identifyNumberColumns(sheet.rows[0]); print('🔢 Colonnes numériques détectées: $numberColumns'); // Corriger chaque ligne de données (ignorer la ligne d'en-tête) for (int rowIndex = 1; rowIndex < sheet.rows.length; rowIndex++) { var row = sheet.rows[rowIndex]; for (int colIndex in numberColumns) { if (colIndex < row.length && row[colIndex] != null) { var cell = row[colIndex]!; var originalValue = cell.value; // Détecter si la cellule a un format de date/temps suspect if (_isSuspiciousDateFormat(originalValue)) { print( '⚠️ Cellule suspecte détectée en ($rowIndex, $colIndex): $originalValue'); // Convertir la valeur corrompue en nombre standard var correctedValue = _convertSuspiciousValue(originalValue); if (correctedValue != null) { print('✅ Correction: $originalValue → $correctedValue'); // Créer une nouvelle cellule avec la valeur corrigée excel.updateCell( sheetName, CellIndex.indexByColumnRow( columnIndex: colIndex, rowIndex: rowIndex), correctedValue); } } } } } } print('✅ Correction des formats terminée'); return excel; } double? _convertSuspiciousValue(dynamic suspiciousValue) { if (suspiciousValue == null) return null; String valueStr = suspiciousValue.toString(); // Cas 1: Format DateTime (ex: "3953-06-05T00:00:00.000") if (valueStr.contains('-') && valueStr.contains(':')) { return _convertDateTimeToNumber(valueStr); } // Cas 2: Grand nombre (ex: "39530605000000") if (valueStr.length > 10) { return _convertLargeNumberToPrice(valueStr); } return null; } // Convertir un format DateTime en nombre double? _convertDateTimeToNumber(String dateTimeStr) { try { print('🔄 Conversion DateTime: $dateTimeStr'); // Nettoyer la chaîne String cleanDateString = dateTimeStr.replaceAll('+', ''); final dateTime = DateTime.parse(cleanDateString); // Excel epoch: 1er janvier 1900 final excelEpoch = DateTime(1900, 1, 1); // Calculer le nombre de jours depuis l'epoch Excel final daysDifference = dateTime.difference(excelEpoch).inDays; // Appliquer la correction pour le bug Excel (+2) final correctedValue = daysDifference + 2; print('→ Jours calculés: $daysDifference → Corrigé: $correctedValue'); if (correctedValue > 0 && correctedValue < 100000000) { return correctedValue.toDouble(); } } catch (e) { print('❌ Erreur conversion DateTime: $e'); } return null; } // Convertir un grand nombre en prix double? _convertLargeNumberToPrice(String largeNumberStr) { try { print('🔄 Conversion grand nombre: $largeNumberStr'); double? numValue = double.tryParse(largeNumberStr); if (numValue == null) return null; // Si le nombre se termine par 000000 (microsecondes), les supprimer if (largeNumberStr.endsWith('000000') && largeNumberStr.length > 10) { String withoutMicros = largeNumberStr.substring(0, largeNumberStr.length - 6); double? daysSinceExcel = double.tryParse(withoutMicros); if (daysSinceExcel != null && daysSinceExcel > 1000 && daysSinceExcel < 10000000) { // Appliquer la correction du décalage Excel (+2) double correctedPrice = daysSinceExcel + 2; print( '→ Conversion: $largeNumberStr → $withoutMicros → $correctedPrice'); return correctedPrice; } } // Table de correspondance pour les cas connus Map knownConversions = { '39530605000000': 750000, '170950519000000': 5550000, }; if (knownConversions.containsKey(largeNumberStr)) { double realPrice = knownConversions[largeNumberStr]!; print('→ Conversion via table: $largeNumberStr → $realPrice'); return realPrice; } } catch (e) { print('❌ Erreur conversion grand nombre: $e'); } return null; } void _showExcelCompatibilityError() { Get.dialog( AlertDialog( title: const Text('Fichier Excel incompatible'), content: const Text( 'Ce fichier Excel contient des éléments qui ne sont pas compatibles avec notre système d\'importation.\n\n' 'Solutions recommandées :\n' '• Téléchargez notre modèle Excel et copiez-y vos données\n' '• Ou exportez votre fichier en format simple: Classeur Excel .xlsx depuis Excel\n' '• Ou créez un nouveau fichier Excel simple sans formatage complexe'), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), TextButton( onPressed: () { Get.back(); _downloadExcelTemplate(); }, child: const Text('Télécharger modèle'), style: TextButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, ), ), ], ), ); } Future _importFromExcel() async { try { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['xlsx', 'xls', 'csv'], allowMultiple: false, ); if (result == null || result.files.isEmpty) { Get.snackbar('Annulé', 'Aucun fichier sélectionné'); return; } setState(() { _isImporting = true; _importProgress = 0.0; _importStatusText = 'Lecture du fichier...'; }); final file = File(result.files.single.path!); if (!await file.exists()) { _resetImportState(); Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); return; } setState(() { _importProgress = 0.1; _importStatusText = 'Vérification du fichier...'; }); final bytes = await file.readAsBytes(); if (bytes.isEmpty) { _resetImportState(); Get.snackbar('Erreur', 'Le fichier Excel est vide'); return; } setState(() { _importProgress = 0.2; _importStatusText = 'Décodage du fichier Excel...'; }); Excel excel; try { excel = Excel.decodeBytes(bytes); _debugExcelFile(excel); } catch (e) { _resetImportState(); debugPrint('Erreur décodage Excel: $e'); if (e.toString().contains('styles') || e.toString().contains('Damaged')) { _showExcelCompatibilityError(); return; } else { Get.snackbar('Erreur', 'Impossible de lire le fichier Excel. Format non supporté.'); return; } } // ✨ NOUVELLE ÉTAPE: Corriger les formats de cellules setState(() { _importProgress = 0.25; _importStatusText = 'Correction des formats de cellules...'; }); excel = _fixExcelNumberFormats(excel); if (excel.tables.isEmpty) { _resetImportState(); Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille'); return; } setState(() { _importProgress = 0.3; _importStatusText = 'Analyse des données...'; }); final sheetName = excel.tables.keys.first; final sheet = excel.tables[sheetName]!; if (sheet.rows.isEmpty) { _resetImportState(); Get.snackbar('Erreur', 'La feuille Excel est vide'); return; } // Détection automatique des colonnes final headerRow = sheet.rows[0]; final columnMapping = _mapHeaders(headerRow); // Vérification des colonnes obligatoires if (!columnMapping.containsKey('name')) { _resetImportState(); Get.snackbar( 'Erreur', 'Colonne "Nom du produit" non trouvée dans le fichier'); return; } if (!columnMapping.containsKey('price')) { _resetImportState(); Get.snackbar('Erreur', 'Colonne "Prix" non trouvée dans le fichier'); return; } int successCount = 0; int errorCount = 0; List errorMessages = []; final totalRows = sheet.rows.length - 1; setState(() { _importStatusText = 'Importation en cours... (0/$totalRows)'; }); for (var i = 1; i < sheet.rows.length; i++) { try { final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); setState(() { _importProgress = currentProgress; _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; }); await Future.delayed(const Duration(milliseconds: 10)); final row = sheet.rows[i]; if (row.isEmpty) { errorCount++; errorMessages.add('Ligne ${i + 1}: Ligne vide'); continue; } // Normalisation des données (maintenant les prix sont corrects) final normalizedData = _normalizeRowData(row, columnMapping, i); // Vérification de la référence if (normalizedData['imei'] != null) { var existingProduct = await _productDatabase.getProductByIMEI(normalizedData['imei']); if (existingProduct != null) { errorCount++; errorMessages.add( 'Ligne ${i + 1}: imei déjà existante (${normalizedData['imei']})'); continue; } } // Création du point de vente si nécessaire int? pointDeVenteId; if (normalizedData['point_de_vente'] != null) { pointDeVenteId = await _productDatabase .getOrCreatePointDeVenteByNom(normalizedData['point_de_vente']); if (pointDeVenteId == null) { errorCount++; errorMessages.add( 'Ligne ${i + 1}: Impossible de créer le point de vente ${normalizedData['point_de_vente']}'); continue; } } setState(() { _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; }); // Création du produit avec les données normalisées final product = Product( name: normalizedData['name'], price: normalizedData['price'], image: '', category: normalizedData['category'], description: normalizedData['description'], stock: normalizedData['stock'], qrCode: '', reference: normalizedData['reference'], marque: normalizedData['marque'], ram: normalizedData['ram'], memoireInterne: normalizedData['memoire_interne'], imei: normalizedData['imei'], pointDeVenteId: pointDeVenteId, ); await _productDatabase.createProduct(product); successCount++; } catch (e) { errorCount++; errorMessages.add('Ligne ${i + 1}: ${e.toString()}'); debugPrint('Erreur ligne ${i + 1}: $e'); } } setState(() { _importProgress = 1.0; _importStatusText = 'Finalisation...'; }); await Future.delayed(const Duration(milliseconds: 500)); _resetImportState(); String message = '$successCount produits importés avec succès'; if (errorCount > 0) { message += ', $errorCount erreurs'; if (errorMessages.length <= 5) { message += ':\n${errorMessages.join('\n')}'; } } Get.snackbar( 'Importation terminée', message, duration: const Duration(seconds: 6), colorText: Colors.white, backgroundColor: successCount > 0 ? Colors.green : Colors.orange, ); // Recharger la liste des produits après importation _loadProducts(); print(errorMessages); } catch (e) { _resetImportState(); Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); debugPrint('Erreur générale import Excel: $e'); } } // Ajoutez ce widget dans votre méthode build, par exemple dans la partie supérieure Widget _buildImportProgressIndicator() { if (!_isImporting) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Importation en cours...', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blue.shade800, ), ), const SizedBox(height: 8), LinearProgressIndicator( value: _importProgress, backgroundColor: Colors.blue.shade100, valueColor: AlwaysStoppedAnimation(Colors.blue.shade600), ), const SizedBox(height: 8), Text( _importStatusText, style: TextStyle( fontSize: 14, color: Colors.blue.shade700, ), ), const SizedBox(height: 8), Text( '${(_importProgress * 100).round()}%', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.blue.shade600, ), ), ], ), ); } //============================================================================================================================= Widget _buildProductCardContent(Product product, String pointDeVenteText) { final isCurrentUserPointDeVente = product.pointDeVenteId == _userController.pointDeVenteId; return InkWell( onTap: () => _showProductDetailsDialog(context, product), child: Card( margin: const EdgeInsets.all(8), elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: isCurrentUserPointDeVente ? BorderSide(color: Colors.orange.shade300, width: 2) : BorderSide.none, ), child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Row( children: [ // Image du produit Container( width: 80, height: 80, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: product.image!.isNotEmpty ? Image.file( File(product.image!), fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => const Icon(Icons.image, size: 40), ) : const Icon(Icons.image, size: 40), ), ), const SizedBox(width: 16), // Informations du produit Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '${NumberFormat('#,##0').format(product.price)} MGA', style: const TextStyle( fontSize: 16, color: Colors.green, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(12), ), child: Text( product.category, style: TextStyle( fontSize: 12, color: Colors.blue.shade800, ), ), ), const SizedBox(width: 8), Text( 'Stock: ${product.stock}', style: TextStyle( fontSize: 12, color: product.stock! > 0 ? Colors.green : Colors.red, fontWeight: FontWeight.w500, ), ), ], ), // Afficher l'IMEI si disponible if (product.imei != null && product.imei!.isNotEmpty) ...[ const SizedBox(height: 4), Text( 'IMEI: ${product.imei}', style: TextStyle( fontSize: 10, color: Colors.grey.shade600, fontFamily: 'monospace', ), ), ], ], ), ), // Actions Column( children: [ // Bouton d'assignation rapide si l'utilisateur a un point de vente if (_userController.pointDeVenteId > 0 && !isCurrentUserPointDeVente && product.imei != null) IconButton( onPressed: () => _assignProductDirectly(product), icon: Icon(Icons.assignment, color: Colors.orange.shade700), tooltip: 'Assigner à mon point de vente', ), IconButton( onPressed: () => _showQRCode(product), icon: const Icon(Icons.qr_code_2, color: Colors.blue), tooltip: 'Voir QR Code', ), if(_isUserSuperAdmin()) IconButton( onPressed: () => _editProduct(product), icon: const Icon(Icons.edit, color: Colors.orange), tooltip: 'Modifier', ), if(_isUserSuperAdmin()) IconButton( onPressed: () => _deleteProduct(product), icon: const Icon(Icons.delete, color: Colors.red), tooltip: 'Supprimer', ), ], ), ], ), const SizedBox(height: 8), // Ligne du point de vente avec indication visuelle Row( children: [ Icon(Icons.store, size: 16, color: isCurrentUserPointDeVente ? Colors.orange.shade700 : Colors.grey), const SizedBox(width: 4), Text( 'Point de vente: $pointDeVenteText', style: TextStyle( fontSize: 12, color: isCurrentUserPointDeVente ? Colors.orange.shade700 : Colors.grey, fontWeight: isCurrentUserPointDeVente ? FontWeight.w600 : FontWeight.normal, ), ), const Spacer(), if (isCurrentUserPointDeVente) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( 'Mon PV', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.orange.shade700, ), ), ), if (pointDeVenteText == 'Non spécifié') TextButton( onPressed: () => _showAddPointDeVenteDialog(product), child: const Text('Ajouter', style: TextStyle(fontSize: 12)), ), ], ), ], ), ), ), ); } // Assignation directe d'un produit (via le bouton sur la carte) Future _assignProductDirectly(Product product) async { if (_isAssigning) return; // Confirmer l'action final confirm = await Get.dialog( AlertDialog( title: const Text('Confirmer l\'assignation'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Voulez-vous assigner ce produit à votre point de vente ?'), const SizedBox(height: 12), Text( product.name, style: const TextStyle(fontWeight: FontWeight.bold), ), Text('IMEI: ${product.imei}'), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), ), child: Text( 'Point de vente: ${_userController.pointDeVenteDesignation}', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.orange.shade700, ), ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(result: false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Get.back(result: true), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange.shade700, foregroundColor: Colors.white, ), child: const Text('Assigner'), ), ], ), ); if (confirm != true) return; setState(() { _isAssigning = true; }); try { // Assigner le point de vente de l'utilisateur au produit final updatedProduct = Product( id: product.id, name: product.name, price: product.price, image: product.image, category: product.category, description: product.description, stock: product.stock, qrCode: product.qrCode, reference: product.reference, marque: product.marque, ram: product.ram, memoireInterne: product.memoireInterne, imei: product.imei, pointDeVenteId: _userController.pointDeVenteId, ); await _productDatabase.updateProduct(updatedProduct); // Recharger les produits _loadProducts(); Get.snackbar( 'Succès', 'Produit "${product.name}" assigné au point de vente ${_userController.pointDeVenteDesignation}', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green.shade600, colorText: Colors.white, duration: const Duration(seconds: 3), icon: const Icon(Icons.check_circle, color: Colors.white), ); } catch (e) { Get.snackbar( 'Erreur', 'Impossible d\'assigner le produit: ${e.toString()}', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red.shade600, colorText: Colors.white, duration: const Duration(seconds: 3), ); } finally { setState(() { _isAssigning = false; }); } } void _showQRCode(Product product) { RxBool showFullUrl = false.obs; RxInt nombreAImprimer = (product.stock ?? 1).obs; // 🔹 Valeur modifiable Get.dialog( Obx(() { final qrData = showFullUrl.value ? 'https://stock.guycom.mg/${product.reference}' : product.reference!; return AlertDialog( title: Row( children: [ const Icon(Icons.qr_code_2, color: Colors.blue), const SizedBox(width: 8), Expanded( child: Text( 'QR Code - ${product.name}', style: const TextStyle(fontSize: 18), ), ), ], ), content: Container( width: 350, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Bouton bascule URL / Référence ElevatedButton.icon( onPressed: () { showFullUrl.value = !showFullUrl.value; }, icon: Icon( showFullUrl.value ? Icons.link : Icons.tag, size: 16, ), label: Text( showFullUrl.value ? 'URL Complète' : 'Référence Seulement', style: const TextStyle(fontSize: 14), ), style: ElevatedButton.styleFrom( backgroundColor: showFullUrl.value ? Colors.blue : Colors.green, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 40), ), ), const SizedBox(height: 16), // QR Code affiché Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: QrImageView( data: qrData, version: QrVersions.auto, size: 200, backgroundColor: Colors.white, errorCorrectionLevel: QrErrorCorrectLevel.M, ), ), const SizedBox(height: 16), // Informations du produit Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Row( children: [ Icon(Icons.inventory_2, size: 16, color: Colors.grey.shade600), const SizedBox(width: 8), Expanded( child: Text( product.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ), ], ), const SizedBox(height: 8), // 🔹 Nouveau champ : nombre à imprimer Row( children: [ const Icon(Icons.format_list_numbered, size: 16, color: Colors.deepPurple), const SizedBox(width: 8), Expanded( child: TextField( keyboardType: TextInputType.number, decoration: InputDecoration( labelText: 'Nombre d\'étiquettes à imprimer', border: const OutlineInputBorder(), isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), ), controller: TextEditingController(text: nombreAImprimer.value.toString()), onChanged: (val) { final parsed = int.tryParse(val); if (parsed != null && parsed > 0) { nombreAImprimer.value = parsed; } }, ), ), ], ), const SizedBox(height: 8), Row( children: [ Icon(Icons.label, size: 16, color: Colors.orange.shade600), const SizedBox(width: 8), Text( 'Format: Étiquette Niimbot B1 (50x15mm)', style: TextStyle( fontSize: 12, color: Colors.orange.shade700, fontWeight: FontWeight.w600, ), ), ], ), ], ), ), ], ), ), actions: [ // Copier TextButton.icon( onPressed: () { Clipboard.setData(ClipboardData(text: qrData)); Get.snackbar( 'Copié', '${showFullUrl.value ? "URL" : "Référence"} copiée', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), icon: const Icon(Icons.check_circle, color: Colors.white), ); }, icon: const Icon(Icons.copy, size: 18), label: const Text('Copier'), ), // Paramètres TextButton.icon( onPressed: () async { Get.back(); pdfService.showNiimbotSettingsDialog(); }, icon: const Icon(Icons.settings, size: 18), label: const Text('Paramètres'), style: TextButton.styleFrom(foregroundColor: Colors.blue.shade700), ), // 🔹 Imprimer selon le nombre choisi ElevatedButton.icon( onPressed: () async { Get.back(); final int n = nombreAImprimer.value; for (int i = 0; i < n; i++) { await pdfService.printQrNiimbotOptimized( qrData, productName: null, reference: product.reference ?? '', leftPadding: 1.0, topPadding: 0.5, qrSize: 12.0, fontSize: 5.0, labelSize: NiimbotLabelSize.small, ); await Future.delayed(const Duration(milliseconds: 100)); } Get.snackbar( 'Impression terminée', '$n étiquette${n > 1 ? "s" : ""} imprimée${n > 1 ? "s" : ""}', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 3), ); }, icon: const Icon(Icons.print, size: 18), label: const Text('Imprimer'), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange.shade600, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), // Fermer TextButton( onPressed: () => Get.back(), child: const Text('Fermer'), ), ], ); }), ); } Future _generatePDF(Product product, String qrUrl) async { final pdf = pw.Document(); pdf.addPage( pw.Page( build: (pw.Context context) { return pw.Center( child: pw.Column( children: [ // pw.Text('QR Code - ${product.name}', style: pw.TextStyle(fontSize: 20)), pw.SizedBox(height: 20), pw.BarcodeWidget( barcode: pw.Barcode.qrCode(), data: qrUrl, width: 200, height: 200, ), pw.SizedBox(height: 20), // pw.Text('URL/Référence: $qrUrl', style: pw.TextStyle(fontSize: 12)), pw.SizedBox(height: 10), pw.Text('Référence: ${product.reference}', style: pw.TextStyle(fontSize: 12)), ], ), ); }, ), ); final output = await getTemporaryDirectory(); final file = File("${output.path}/qrcode.pdf"); await file.writeAsBytes(await pdf.save()); OpenFile.open(file.path); } void _editProduct(Product product) { final nameController = TextEditingController(text: product.name); final priceController = TextEditingController(text: product.price.toString()); final stockController = TextEditingController(text: product.stock.toString()); final descriptionController = TextEditingController(text: product.description ?? ''); final imageController = TextEditingController(text: product.image); final referenceController = TextEditingController(text: product.reference ?? ''); final marqueController = TextEditingController(text: product.marque ?? ''); final ramController = TextEditingController(text: product.ram ?? ''); final memoireInterneController = TextEditingController(text: product.memoireInterne ?? ''); final imeiController = TextEditingController(text: product.imei ?? ''); final newPointDeVenteController = TextEditingController(); String? selectedPointDeVente; List> pointsDeVente = []; bool isLoadingPoints = true; // Initialiser la catégorie sélectionnée de manière sécurisée String selectedCategory = _availableCategories.contains(product.category) ? product.category : _availableCategories.last; // 'Non catégorisé' par défaut File? pickedImage; String? qrPreviewData; bool showAddNewPoint = false; // Fonction pour mettre à jour le QR preview void updateQrPreview() { if (nameController.text.isNotEmpty && referenceController.text.isNotEmpty) { qrPreviewData = 'https://stock.guycom.mg/${referenceController.text.trim()}'; } else { qrPreviewData = null; } } // Charger les points de vente Future loadPointsDeVente(StateSetter setDialogState) async { try { final result = await _productDatabase.getPointsDeVente(); setDialogState(() { // Ajouter l'option "Aucun" à la liste pointsDeVente = [ {'id': null, 'nom': 'Aucun'}, ...result ]; isLoadingPoints = false; // Définir le point de vente actuel du produit if (product.pointDeVenteId != null) { final currentPointDeVente = result.firstWhere( (point) => point['id'] == product.pointDeVenteId, orElse: () => {}, ); if (currentPointDeVente.isNotEmpty) { selectedPointDeVente = currentPointDeVente['nom'] as String; } } else { selectedPointDeVente = 'Aucun'; // Si aucun point de vente, sélectionner "Aucun" } }); } catch (e) { setDialogState(() { isLoadingPoints = false; }); Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); } } // Initialiser le QR preview updateQrPreview(); Get.dialog( AlertDialog( title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.edit, color: Colors.orange.shade700), ), const SizedBox(width: 12), const Text('Modifier le produit'), ], ), content: Container( width: 600, constraints: const BoxConstraints(maxHeight: 600), child: SingleChildScrollView( child: StatefulBuilder( builder: (context, setDialogState) { if (isLoadingPoints && pointsDeVente.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { loadPointsDeVente(setDialogState); }); } return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Champs obligatoires Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange.shade200), ), child: Row( children: [ Icon(Icons.info, color: Colors.orange.shade600, size: 16), const SizedBox(width: 8), const Text( 'Les champs marqués d\'un * sont obligatoires', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500), ), ], ), ), const SizedBox(height: 16), // Section Point de vente Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.teal.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.teal.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.store, color: Colors.teal.shade700), const SizedBox(width: 8), Text( 'Point de vente', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.teal.shade700, ), ), ], ), const SizedBox(height: 12), if (isLoadingPoints) const Center(child: CircularProgressIndicator()) else Column( children: [ if (!showAddNewPoint) ...[ DropdownButtonFormField( value: selectedPointDeVente, items: pointsDeVente.map((point) { return DropdownMenuItem( value: point['nom'] as String, child: Text(point['nom'] as String), ); }).toList(), onChanged: (value) { setDialogState( () => selectedPointDeVente = value); }, decoration: const InputDecoration( labelText: 'Sélectionner un point de vente', border: OutlineInputBorder(), prefixIcon: Icon(Icons.store), filled: true, fillColor: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ TextButton.icon( onPressed: () { setDialogState(() { showAddNewPoint = true; newPointDeVenteController.clear(); }); }, icon: const Icon(Icons.add, size: 16), label: const Text('Ajouter nouveau point'), style: TextButton.styleFrom( foregroundColor: Colors.teal.shade700, ), ), const Spacer(), TextButton.icon( onPressed: () => loadPointsDeVente(setDialogState), icon: const Icon(Icons.refresh, size: 16), label: const Text('Actualiser'), ), ], ), ], if (showAddNewPoint) ...[ DropdownButtonFormField( value: selectedPointDeVente, items: pointsDeVente.map((point) { return DropdownMenuItem( value: point['nom'] as String, child: Text(point['nom'] as String), ); }).toList(), onChanged: (value) { setDialogState( () => selectedPointDeVente = value); }, decoration: const InputDecoration( labelText: 'Sélectionner un point de vente', border: OutlineInputBorder(), prefixIcon: Icon(Icons.store), filled: true, fillColor: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ TextButton.icon( onPressed: () { setDialogState(() { showAddNewPoint = true; newPointDeVenteController.clear(); }); }, icon: const Icon(Icons.add, size: 16), label: const Text('Ajouter nouveau point'), style: TextButton.styleFrom( foregroundColor: Colors.teal.shade700, ), ), const Spacer(), TextButton.icon( onPressed: () => loadPointsDeVente(setDialogState), icon: const Icon(Icons.refresh, size: 16), label: const Text('Actualiser'), ), ], ), ], if (showAddNewPoint) ...[ TextField( controller: newPointDeVenteController, decoration: const InputDecoration( labelText: 'Nom du nouveau point de vente', border: OutlineInputBorder(), prefixIcon: Icon(Icons.add_business), filled: true, fillColor: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ TextButton( onPressed: () { setDialogState(() { showAddNewPoint = false; newPointDeVenteController.clear(); }); }, child: const Text('Annuler'), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: () async { final nom = newPointDeVenteController .text .trim(); if (nom.isNotEmpty) { try { final id = await _productDatabase .getOrCreatePointDeVenteByNom( nom); if (id != null) { setDialogState(() { showAddNewPoint = false; selectedPointDeVente = nom; newPointDeVenteController .clear(); }); // Recharger la liste await loadPointsDeVente( setDialogState); Get.snackbar( 'Succès', 'Point de vente "$nom" créé avec succès', backgroundColor: Colors.green, colorText: Colors.white, ); } } catch (e) { Get.snackbar('Erreur', 'Impossible de créer le point de vente: $e'); } } }, icon: const Icon(Icons.save, size: 16), label: const Text('Créer'), style: ElevatedButton.styleFrom( backgroundColor: Colors.teal, foregroundColor: Colors.white, ), ), ], ), ], ], ), ], ), ), const SizedBox(height: 16), // Nom du produit TextField( controller: nameController, decoration: InputDecoration( labelText: 'Nom du produit *', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.shopping_bag), filled: true, fillColor: Colors.grey.shade50, ), onChanged: (value) { setDialogState(() { updateQrPreview(); }); }, ), const SizedBox(height: 16), // Prix et Stock sur la même ligne Row( children: [ Expanded( child: TextField( controller: priceController, keyboardType: const TextInputType.numberWithOptions( decimal: true), decoration: InputDecoration( labelText: 'Prix (MGA) *', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.attach_money), filled: true, fillColor: Colors.grey.shade50, ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: stockController, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: 'Stock', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.inventory), filled: true, fillColor: Colors.grey.shade50, ), ), ), ], ), const SizedBox(height: 16), // Catégorie avec gestion des valeurs non présentes DropdownButtonFormField( value: selectedCategory, items: _availableCategories .map((category) => DropdownMenuItem( value: category, child: Text(category))) .toList(), onChanged: (value) { setDialogState(() => selectedCategory = value!); }, decoration: InputDecoration( labelText: 'Catégorie', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.category), filled: true, fillColor: Colors.grey.shade50, helperText: product.category != selectedCategory ? 'Catégorie originale: ${product.category}' : null, ), ), const SizedBox(height: 16), // Description TextField( controller: descriptionController, maxLines: 3, decoration: InputDecoration( labelText: 'Description', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.description), filled: true, fillColor: Colors.grey.shade50, ), ), const SizedBox(height: 16), // Section Référence (non modifiable) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.purple.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.purple.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.confirmation_number, color: Colors.purple.shade700), const SizedBox(width: 8), Text( 'Référence du produit', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.purple.shade700, ), ), ], ), const SizedBox(height: 12), TextField( controller: referenceController, decoration: const InputDecoration( labelText: 'Référence', border: OutlineInputBorder(), prefixIcon: Icon(Icons.tag), filled: true, fillColor: Colors.white, helperText: 'La référence peut être modifiée avec précaution', ), onChanged: (value) { setDialogState(() { updateQrPreview(); }); }, ), ], ), ), const SizedBox(height: 16), // Spécifications techniques Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.memory, color: Colors.orange.shade700), const SizedBox(width: 8), Text( 'Spécifications techniques', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.orange.shade700, ), ), ], ), const SizedBox(height: 12), TextField( controller: marqueController, decoration: const InputDecoration( labelText: 'Marque', border: OutlineInputBorder(), prefixIcon: Icon(Icons.branding_watermark), filled: true, fillColor: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: ramController, decoration: const InputDecoration( labelText: 'RAM', border: OutlineInputBorder(), prefixIcon: Icon(Icons.memory), filled: true, fillColor: Colors.white, ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: memoireInterneController, decoration: const InputDecoration( labelText: 'Mémoire interne', border: OutlineInputBorder(), prefixIcon: Icon(Icons.storage), filled: true, fillColor: Colors.white, ), ), ), ], ), const SizedBox(height: 8), TextField( controller: imeiController, decoration: const InputDecoration( labelText: 'IMEI (pour téléphones)', border: OutlineInputBorder(), prefixIcon: Icon(Icons.smartphone), filled: true, fillColor: Colors.white, ), ), ], ), ), const SizedBox(height: 16), // Section Image Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.image, color: Colors.blue.shade700), const SizedBox(width: 8), Text( 'Image du produit', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.blue.shade700, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: imageController, decoration: const InputDecoration( labelText: 'Chemin de l\'image', border: OutlineInputBorder(), isDense: true, ), readOnly: true, ), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: () async { final result = await FilePicker.platform .pickFiles(type: FileType.image); if (result != null && result.files.single.path != null) { setDialogState(() { pickedImage = File(result.files.single.path!); imageController.text = pickedImage!.path; }); } }, icon: const Icon(Icons.folder_open, size: 16), label: const Text('Choisir'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.all(12), ), ), ], ), const SizedBox(height: 12), // Aperçu de l'image Center( child: Container( height: 100, width: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: pickedImage != null ? Image.file(pickedImage!, fit: BoxFit.cover) : (product.image != null && product.image!.isNotEmpty ? Image.file( File(product.image!), fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => const Icon(Icons.image, size: 50), ) : const Icon(Icons.image, size: 50)), ), ), ), ], ), ), const SizedBox(height: 16), // Aperçu QR Code if (qrPreviewData != null) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade200), ), child: Column( children: [ Row( children: [ Icon(Icons.qr_code_2, color: Colors.green.shade700), const SizedBox(width: 8), Text( 'Aperçu du QR Code', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.green.shade700, ), ), ], ), const SizedBox(height: 12), Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: QrImageView( data: qrPreviewData!, version: QrVersions.auto, size: 80, backgroundColor: Colors.white, ), ), ), const SizedBox(height: 8), Text( 'Réf: ${referenceController.text.trim()}', style: const TextStyle( fontSize: 10, color: Colors.grey), ), ], ), ), ], ); }, ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton.icon( onPressed: () async { final name = nameController.text.trim(); final price = double.tryParse(priceController.text.trim()) ?? 0.0; final stock = int.tryParse(stockController.text.trim()) ?? 0; final reference = referenceController.text.trim(); if (name.isEmpty || price <= 0) { Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); return; } if (reference.isEmpty) { Get.snackbar('Erreur', 'La référence est obligatoire'); return; } if (reference != product.reference) { final existingProduct = await _productDatabase.getProductByReference(reference); if (existingProduct != null && existingProduct.id != product.id) { Get.snackbar('Erreur', 'Cette référence existe déjà pour un autre produit'); return; } } final imei = imeiController.text.trim(); if (imei.isNotEmpty && imei != product.imei) { final existingProduct = await _productDatabase.getProductByIMEI(imei); if (existingProduct != null && existingProduct.id != product.id) { Get.snackbar( 'Erreur', 'Cet IMEI existe déjà pour un autre produit'); return; } } // Gérer le point de vente int? pointDeVenteId; String? finalPointDeVenteNom; if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { finalPointDeVenteNom = newPointDeVenteController.text.trim(); } else if (selectedPointDeVente != null && selectedPointDeVente != 'Aucun') { finalPointDeVenteNom = selectedPointDeVente; } if (finalPointDeVenteNom != null) { pointDeVenteId = await _productDatabase .getOrCreatePointDeVenteByNom(finalPointDeVenteNom); } // Si "Aucun" est sélectionné, pointDeVenteId reste null try { final updatedProduct = Product( id: product.id, name: name, price: price, image: imageController.text.trim(), category: selectedCategory, description: descriptionController.text.trim(), stock: stock, qrCode: product.qrCode, reference: reference, marque: marqueController.text.trim().isNotEmpty ? marqueController.text.trim() : null, ram: ramController.text.trim().isNotEmpty ? ramController.text.trim() : null, memoireInterne: memoireInterneController.text.trim().isNotEmpty ? memoireInterneController.text.trim() : null, imei: imei.isNotEmpty ? imei : null, pointDeVenteId: pointDeVenteId, // Peut être null si "Aucun" ); await _productDatabase.updateProduct(updatedProduct); Get.back(); Get.snackbar( 'Succès', 'Produit modifié avec succès!\nRéférence: $reference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 4), icon: const Icon(Icons.check_circle, color: Colors.white), ); _loadProducts(); _loadPointsDeVente(); } catch (e) { Get.snackbar('Erreur', 'Modification du produit échouée: $e'); } }, icon: const Icon(Icons.save), label: const Text('Sauvegarder les modifications'), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), ), ], ), ); } void _deleteProduct(Product product) { Get.dialog( AlertDialog( title: const Text('Confirmer la suppression'), content: Text('Êtes-vous sûr de vouloir supprimer "${product.name}" ?'), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () async { try { await _productDatabase.deleteProduct(product.id); Get.back(); Get.snackbar( 'Succès', 'Produit supprimé avec succès', backgroundColor: Colors.green, colorText: Colors.white, ); _loadProducts(); } catch (e) { Get.back(); Get.snackbar('Erreur', 'Suppression échouée: $e'); } }, child: const Text('Supprimer', style: TextStyle(color: Colors.white)), ), ], ), ); } Widget _buildProductCard(Product product) { return FutureBuilder( future: _productDatabase.getPointDeVenteNomById(product.pointDeVenteId ?? 0), builder: (context, snapshot) { // Gestion des états du FutureBuilder if (snapshot.connectionState == ConnectionState.waiting) { return _buildProductCardContent(product, 'Chargement...'); } if (snapshot.hasError) { return _buildProductCardContent(product, 'Erreur de chargement'); } final pointDeVente = snapshot.data ?? 'Non spécifié'; return _buildProductCardContent(product, pointDeVente); }, ); } void _showAddPointDeVenteDialog(Product product) { final pointDeVenteController = TextEditingController(); final _formKey = GlobalKey(); Get.dialog( AlertDialog( title: const Text('Ajouter un point de vente'), content: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: pointDeVenteController, decoration: const InputDecoration( labelText: 'Nom du point de vente', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer un nom'; } return null; }, ), const SizedBox(height: 16), DropdownButtonFormField( value: null, hint: const Text('Ou sélectionner existant'), items: _pointsDeVente.map((point) { return DropdownMenuItem( value: point['nom'] as String, child: Text(point['nom'] as String), ); }).toList(), onChanged: (value) { if (value != null) { pointDeVenteController.text = value; } }, ), ], ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ElevatedButton( onPressed: () async { if (_formKey.currentState!.validate()) { final nom = pointDeVenteController.text.trim(); final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); if (id != null) { // Mettre à jour le produit avec le nouveau point de vente final updatedProduct = Product( id: product.id, name: product.name, price: product.price, image: product.image, category: product.category, stock: product.stock, description: product.description, qrCode: product.qrCode, reference: product.reference, pointDeVenteId: id, ); await _productDatabase.updateProduct(updatedProduct); Get.back(); Get.snackbar('Succès', 'Point de vente attribué', backgroundColor: Colors.green); _loadProducts(); // Rafraîchir la liste } } }, child: const Text('Enregistrer'), ), ], ), ); } void _showProductDetailsDialog(BuildContext context, Product product) { Get.dialog( Dialog( insetPadding: const EdgeInsets.all(24), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: ConstrainedBox( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75, // Réduit de 0.9 à 0.75 maxHeight: MediaQuery.of(context).size.height * 0.85, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // En-tête moderne avec bouton fermer Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.shopping_bag, color: Colors.blue.shade700, size: 20), ), const SizedBox(width: 12), Expanded( child: Text( product.name, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey.shade800, ), ), ), IconButton( onPressed: () => Get.back(), icon: Icon(Icons.close, color: Colors.grey.shade600), style: IconButton.styleFrom( backgroundColor: Colors.white, padding: const EdgeInsets.all(8), ), ), ], ), ), // Contenu scrollable Flexible( child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image du produit avec ombre Center( child: Container( width: 140, height: 140, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: product.image != null && product.image!.isNotEmpty ? Image.file( File(product.image!), fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildPlaceholderImage(), ) : _buildPlaceholderImage(), ), ), ), const SizedBox(height: 24), // Informations principales avec design moderne _buildModernInfoSection( title: 'Informations générales', icon: Icons.info_outline, color: Colors.blue, children: [ _buildModernInfoRow('Prix', '${NumberFormat('#,##0.00', 'fr_FR').format(product.price)} MGA', Icons.payments_outlined), _buildModernInfoRow('Catégorie', product.category, Icons.category_outlined), _buildModernInfoRow('Stock', '${product.stock}', Icons.inventory_2_outlined), _buildModernInfoRow('Référence', product.reference ?? 'N/A', Icons.tag), ], ), const SizedBox(height: 16), // Spécifications techniques _buildModernInfoSection( title: 'Spécifications techniques', icon: Icons.settings_outlined, color: Colors.purple, children: [ _buildModernInfoRow( 'Marque', product.marque ?? 'Non spécifiée', Icons.branding_watermark_outlined), _buildModernInfoRow( 'RAM', product.ram ?? 'Non spécifiée', Icons.memory_outlined), _buildModernInfoRow( 'Mémoire', product.memoireInterne ?? 'Non spécifiée', Icons.storage_outlined), _buildModernInfoRow( 'IMEI', product.imei ?? 'Non spécifié', Icons.smartphone_outlined), ], ), // Description if (product.description != null && product.description!.isNotEmpty) ...[ const SizedBox(height: 16), _buildModernInfoSection( title: 'Description', icon: Icons.description_outlined, color: Colors.green, children: [ Text( product.description!, style: TextStyle( fontSize: 14, color: Colors.grey.shade700, height: 1.4, ), ), ], ), ], // QR Code if (product.qrCode != null && product.qrCode!.isNotEmpty) ...[ const SizedBox(height: 16), _buildModernInfoSection( title: 'QR Code', icon: Icons.qr_code, color: Colors.orange, children: [ Center( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), ), child: QrImageView( data: 'https://stock.guycom.mg/${product.reference}', version: QrVersions.auto, size: 80, ), ), ), ], ), ], const SizedBox(height: 8), ], ), ), ), ], ), ), ), ); } Widget _buildModernInfoSection({ required String title, required IconData icon, required Color color, required List children, }) { return Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête de section Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), child: Row( children: [ Icon(icon, color: color, size: 18), const SizedBox(width: 8), Text( title, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 15, color: const Color.fromARGB(255, 8, 63, 108), ), ), ], ), ), // Contenu Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: children, ), ), ], ), ); } Widget _buildModernInfoRow(String label, String value, IconData icon) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(6), ), child: Icon(icon, size: 16, color: Colors.grey.shade600), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 12, color: Colors.grey.shade500, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( value, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey.shade800, ), ), ], ), ), ], ), ); } Widget _buildPlaceholderImage() { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.grey.shade100, Colors.grey.shade200], ), borderRadius: BorderRadius.circular(16), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.image_outlined, size: 40, color: Colors.grey.shade400), const SizedBox(height: 8), Text( 'Aucune image', style: TextStyle( color: Colors.grey.shade500, fontSize: 12, fontWeight: FontWeight.w500, ), ), ], ), ), ); } // Méthode pour générer et sauvegarder le QR Code Future _generateAndSaveQRCode(String reference) async { final qrUrl = 'https://stock.guycom.mg/$reference'; final validation = QrValidator.validate( data: qrUrl, version: QrVersions.auto, errorCorrectionLevel: QrErrorCorrectLevel.L, ); if (validation.status != QrValidationStatus.valid) { throw Exception('Données QR invalides: ${validation.error}'); } final qrCode = validation.qrCode!; final painter = QrPainter.withQr( qr: qrCode, color: Colors.black, emptyColor: Colors.white, gapless: true, ); final directory = await getApplicationDocumentsDirectory(); final path = '${directory.path}/$reference.png'; try { final picData = await painter.toImageData(2048, format: ImageByteFormat.png); if (picData != null) { await File(path).writeAsBytes(picData.buffer.asUint8List()); } else { throw Exception('Impossible de générer l\'image QR'); } } catch (e) { throw Exception('Erreur lors de la génération du QR code: $e'); } return path; } @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < 600; return Scaffold( appBar: CustomAppBar(title: 'Gestion des produits'), drawer: CustomDrawer(), floatingActionButton: Column( mainAxisSize: MainAxisSize.min, children: [ // Nouveau bouton pour scanner et assigner FloatingActionButton( heroTag: 'assignBtn', onPressed: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) ? null : _startPointDeVenteAssignmentScanning, mini: true, child: (_isScanning || _isAssigning) ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Row( children: [ const Icon(Icons.qr_code), ], ), backgroundColor: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) ? Colors.grey : Colors.orange, foregroundColor: Colors.white, ), const SizedBox(height: 8), if (_userController.username == 'superadmin'|| _userController.username == 'admin') ...[ FloatingActionButton( heroTag: 'importBtn', onPressed: _isImporting ? null : _importFromExcel, mini: true, child: const Icon(Icons.upload), backgroundColor: Colors.blue, foregroundColor: Colors.white, ), ], const SizedBox(height: 8), FloatingActionButton.extended( heroTag: 'addBtn', onPressed: _showAddProductDialog, icon: const Icon(Icons.add), label: const Text('Ajouter'), backgroundColor: Colors.green, foregroundColor: Colors.white, ), ], ), body: Column( children: [ // Barre de recherche et filtres Container( padding: const EdgeInsets.all(16), color: Colors.grey.shade100, child: Column( children: [ // Card d'information sur l'attribution (desktop uniquement) if (!isMobile && _userController.pointDeVenteId > 0) _buildAssignmentScanCard(), // Barre de recherche Row( children: [ Expanded( child: TextField( controller: _searchController, decoration: InputDecoration( labelText: 'Rechercher...', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.white, ), ), ), const SizedBox(width: 16), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: DropdownButton( value: _selectedCategory, items: _categories .map((category) => DropdownMenuItem( value: category, child: Text(category))) .toList(), onChanged: (value) { setState(() { _selectedCategory = value!; _filterProducts(); }); }, underline: const SizedBox(), hint: const Text('Catégorie'), ), ), ], ), // Boutons pour mobile if (isMobile) ...[ const SizedBox(height: 12), if (_userController.pointDeVenteId > 0) Row( children: [ Expanded( child: ElevatedButton.icon( icon: (_isScanning || _isAssigning) ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.assignment), label: Text((_isScanning || _isAssigning) ? 'Attribution...' : 'Assigner produits'), onPressed: (_isScanning || _isAssigning) ? null : _startPointDeVenteAssignmentScanning, style: ElevatedButton.styleFrom( backgroundColor: (_isScanning || _isAssigning) ? Colors.grey : Colors.orange.shade700, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), ], const SizedBox(height: 12), // Compteur de produits Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${_filteredProducts.length} produit(s) trouvé(s)', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey, ), ), if (_userController.pointDeVenteId > 0) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(12), ), child: Text( 'PV: ${_userController.pointDeVenteDesignation}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.orange.shade700, ), ), ), if (_searchController.text.isNotEmpty || _selectedCategory != 'Tous') TextButton.icon( onPressed: () { setState(() { _searchController.clear(); _selectedCategory = 'Tous'; _filterProducts(); }); }, icon: const Icon(Icons.clear, size: 16), label: const Text('Réinitialiser'), style: TextButton.styleFrom( foregroundColor: Colors.orange, ), ), ], ), ], ), ), _buildImportProgressIndicator(), // Liste des produits Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : _filteredProducts.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.inventory_2_outlined, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( _products.isEmpty ? 'Aucun produit enregistré' : 'Aucun produit trouvé pour cette recherche', style: TextStyle( fontSize: 18, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Text( _products.isEmpty ? 'Commencez par ajouter votre premier produit' : 'Essayez de modifier vos critères de recherche', style: TextStyle( fontSize: 14, color: Colors.grey.shade500, ), ), ], ), ) : RefreshIndicator( onRefresh: _loadProducts, child: ListView.builder( itemCount: _filteredProducts.length, itemBuilder: (context, index) { final product = _filteredProducts[index]; return _buildProductCard(product); }, ), ), ), ], ), ); } }