diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e9bdd1a..7a00b33 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ validerTransfert(int demandeId, int validateurId) async { final sourceId = fields['point_de_vente_source_id'] as int; final destinationId = fields['point_de_vente_destination_id'] as int; + + final getpointDeventeSource = await db.query( + 'Select point_de_vente_source_id FROM demandes_transfert WHERE id=?',[demandeId] + ); + final getpointDeventeDest = await db.query( + 'Select point_de_vente_destination_id FROM demandes_transfert WHERE id=?',[demandeId] + ); + final getpointDeventeSourceValue = getpointDeventeSource.first.fields['point_de_vente_source_id']; + final getpointDeventedestValue = getpointDeventeDest.first.fields['point_de_vente_destination_id']; + if(getpointDeventeSourceValue==getpointDeventedestValue){ + await db.query('update products set point_de_vente_id=? where id = ?',[getpointDeventedestValue,produitId]); + }else{ + + + + // 2. Vérifier le stock source final stockSource = await db.query( 'SELECT stock FROM products WHERE id = ? AND point_de_vente_id = ? FOR UPDATE', @@ -2646,7 +2662,7 @@ Future validerTransfert(int demandeId, int validateurId) async { null, // IMEI doit être unique donc on ne le copie pas ]); } - + } // 5. Mettre à jour le statut de la demande await db.query(''' UPDATE demandes_transfert diff --git a/lib/Views/DemandeTransfert.dart b/lib/Views/DemandeTransfert.dart index c3a8a7d..6b7e784 100644 --- a/lib/Views/DemandeTransfert.dart +++ b/lib/Views/DemandeTransfert.dart @@ -205,7 +205,7 @@ class _GestionTransfertsPageState extends State with Tick ), Text('Référence: ${demande['produit_reference']}'), Text('Quantité: ${demande['quantite']}'), - Text('De: ${demande['point_vente_source']}'), + Text(demande['point_vente_source'] == demande['point_vente_destination']?'De: Non specifier' : 'De: ${demande['point_vente_source']}'), Text('Vers: ${demande['point_vente_destination']}'), Text( 'Stock disponible: $stockDisponible', @@ -602,7 +602,7 @@ class _GestionTransfertsPageState extends State with Tick statutIcon = Icons.check_circle; statutText = 'Validée'; break; - case 'rejetee': + case 'refusee': statutColor = Colors.red; statutIcon = Icons.cancel; statutText = 'Rejetée'; @@ -695,7 +695,7 @@ class _GestionTransfertsPageState extends State with Tick const SizedBox(width: 8), Expanded( child: Text( - '${demande['point_vente_source'] ?? 'N/A'}', + demande['point_vente_source']==demande['point_vente_destination']?"Non specifier" : '${demande['point_vente_source'] ?? 'N/A'}', style: const TextStyle(fontWeight: FontWeight.w500), ), ), diff --git a/lib/Views/HandleProduct.dart b/lib/Views/HandleProduct.dart index f0d6dd5..dd15d6e 100644 --- a/lib/Views/HandleProduct.dart +++ b/lib/Views/HandleProduct.dart @@ -26,6 +26,7 @@ class ProductManagementPage extends StatefulWidget { class _ProductManagementPageState extends State { final AppDatabase _productDatabase = AppDatabase.instance; + final AppDatabase _appDatabase = AppDatabase.instance; final UserController _userController = Get.find(); List _products = []; @@ -99,6 +100,494 @@ bool _isUserSuperAdmin() { 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', + '${product.price.toStringAsFixed(2)} MGA', + Icons.attach_money, + Colors.green, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildInfoCard( + 'Stock disponible', + '${product.stock ?? 0}', + Icons.inventory, + product.stock != null && product.stock! > 0 + ? Colors.green + : Colors.red, + ), + ), + ], + ), + if (product.reference != null && product.reference!.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + 'Référence: ${product.reference}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontFamily: 'monospace', + ), + ), + ], + ], + ), + ), + + const SizedBox(height: 20), + + // Informations de transfert + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange.shade200), + ), + child: Column( + children: [ + Row( + children: [ + Icon(Icons.arrow_forward, color: Colors.orange.shade700), + const SizedBox(width: 8), + Text( + 'Informations de transfert', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.orange.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildTransferStep( + 'DE', + pointDeVenteSource ?? 'Chargement...', + Icons.store_outlined, + Colors.red.shade600, + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: Icon( + Icons.arrow_forward, + color: Colors.orange.shade700, + size: 24, + ), + ), + Expanded( + child: _buildTransferStep( + 'VERS', + pointDeVenteDestination ?? 'Chargement...', + Icons.store, + Colors.green.shade600, + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Champ quantité avec design amélioré + Text( + 'Quantité à transférer', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: [ + IconButton( + onPressed: () { + int currentQty = int.tryParse(quantiteController.text) ?? 1; + if (currentQty > 1) { + quantiteController.text = (currentQty - 1).toString(); + } + }, + icon: Icon(Icons.remove, color: Colors.grey.shade600), + ), + Expanded( + child: TextFormField( + controller: quantiteController, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 16), + hintText: 'Quantité', + ), + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer une quantité'; + } + final qty = int.tryParse(value) ?? 0; + if (qty <= 0) { + return 'Quantité invalide'; + } + if (product.stock != null && qty > product.stock!) { + return 'Quantité supérieure au stock disponible'; + } + return null; + }, + ), + ), + IconButton( + onPressed: () { + int currentQty = int.tryParse(quantiteController.text) ?? 1; + int maxStock = product.stock ?? 999; + if (currentQty < maxStock) { + quantiteController.text = (currentQty + 1).toString(); + } + }, + icon: Icon(Icons.add, color: Colors.grey.shade600), + ), + ], + ), + ), + + + + // Boutons d'action avec design moderne + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.grey.shade300), + ), + ), + child: Text( + 'Annuler', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: ElevatedButton.icon( + onPressed: () async { + if (!_formKey.currentState!.validate()) return; + + final qty = int.tryParse(quantiteController.text) ?? 0; + if (qty <= 0) { + Get.snackbar( + 'Erreur', + 'Quantité invalide', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + + try { + setState(() => _isLoading = true); + Navigator.pop(context); + + await _appDatabase.createDemandeTransfert( + produitId: product.id!, + pointDeVenteSourceId: product.pointDeVenteId!, + pointDeVenteDestinationId: _userController.pointDeVenteId, + demandeurId: _userController.userId, + quantite: qty, + notes: notesController.text.isNotEmpty + ? notesController.text + : 'Demande de transfert depuis l\'application mobile', + ); + + Get.snackbar( + 'Demande envoyée ✅', + 'Votre demande de transfert de $qty unité(s) a été enregistrée et sera traitée prochainement.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 4), + icon: const Icon(Icons.check_circle, color: Colors.white), + ); + } catch (e) { + Get.snackbar( + 'Erreur', + 'Impossible d\'envoyer la demande: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 4), + ); + } finally { + setState(() => _isLoading = false); + } + }, + icon: const Icon(Icons.send, color: Colors.white), + label: const Text( + 'Envoyer la demande', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); +} + + + + // Fonction pour mettre à jour le QR preview void updateQrPreview() { if (nameController.text.isNotEmpty) { @@ -1253,25 +1742,32 @@ bool _isUserSuperAdmin() { } // 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 _productDatabase.updateProduct(updatedProduct); + // 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(); @@ -1311,7 +1807,7 @@ bool _isUserSuperAdmin() { child: Icon(Icons.check_circle, color: Colors.green.shade700), ), const SizedBox(width: 12), - const Expanded(child: Text('Attribution réussie !')), + const Expanded(child: Text( 'demande attribution réussie en attente de validation!')), ], ), content: Column( diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart index 2cc02cd..ef3ebf1 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -39,13 +39,8 @@ class _GestionCommandesPageState extends State { DateTime? _selectedDate; final TextEditingController _searchController = TextEditingController(); bool _showCancelledOrders = false; - // final userController = Get.find(); final userController = Get.find(); - bool verifAdmin() { - return userController.role == 'Super Admin'; - } - @override void initState() { super.initState(); @@ -78,22 +73,17 @@ class _GestionCommandesPageState extends State { final matchesDate = _selectedDate == null || DateFormat('yyyy-MM-dd').format(commande.dateCommande) == DateFormat('yyyy-MM-dd').format(_selectedDate!); - - final shouldShowCancelled = - _showCancelledOrders || commande.statut != StatutCommande.annulee; - - return matchesSearch && - matchesStatut && - matchesDate && - shouldShowCancelled; + + final shouldShowCancelled = _showCancelledOrders || commande.statut != StatutCommande.annulee; + + return matchesSearch && matchesStatut && matchesDate && shouldShowCancelled; }).toList(); }); } - Future _updateStatut(int commandeId, StatutCommande newStatut, - {int? validateurId}) async { + Future _updateStatut(int commandeId, StatutCommande newStatut, {int? validateurId}) async { final commandeExistante = await _database.getCommandeById(commandeId); - + if (commandeExistante == null) { Get.snackbar( 'Erreur', @@ -123,12 +113,12 @@ class _GestionCommandesPageState extends State { } else { await _database.updateStatutCommande(commandeId, newStatut); } - + await _loadCommandes(); - + String message = 'Statut de la commande mis à jour'; Color backgroundColor = Colors.green; - + switch (newStatut) { case StatutCommande.annulee: message = 'Commande annulée avec succès'; @@ -141,7 +131,7 @@ class _GestionCommandesPageState extends State { default: break; } - + Get.snackbar( 'Succès', message, @@ -162,19 +152,18 @@ class _GestionCommandesPageState extends State { if (selectedPayment.type == PaymentType.cash) { await _showCashPaymentDialog(commande, selectedPayment.amountGiven); } - + await _updateStatut( - commande.id!, + commande.id!, StatutCommande.confirmee, validateurId: userController.userId, ); - + await _generateReceipt(commande, selectedPayment); } } - Future _showCashPaymentDialog( - Commande commande, double amountGiven) async { + Future _showCashPaymentDialog(Commande commande, double amountGiven) async { final amountController = TextEditingController( text: amountGiven.toStringAsFixed(2), ); @@ -245,127 +234,117 @@ class _GestionCommandesPageState extends State { Future buildIconPhoneText() async { final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); - return pw.Text(String.fromCharCode(0xf095), - style: pw.TextStyle(font: font)); - } - - Future buildIconGift() async { - final font = - pw.Font.ttf(await rootBundle.load('assets/NotoEmoji-Regular.ttf')); - return pw.Text('🎁', style: pw.TextStyle(font: font, fontSize: 16)); + return pw.Text(String.fromCharCode(0xf095), style: pw.TextStyle(font: font)); } - +Future buildIconGift() async { + final font = pw.Font.ttf(await rootBundle.load('assets/NotoEmoji-Regular.ttf')); + return pw.Text('🎁', style: pw.TextStyle(font: font, fontSize: 16)); +} Future buildIconCheckedText() async { final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); - return pw.Text(String.fromCharCode(0xf14a), - style: pw.TextStyle(font: font)); + return pw.Text(String.fromCharCode(0xf14a), style: pw.TextStyle(font: font)); } Future buildIconGlobeText() async { final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); - return pw.Text(String.fromCharCode(0xf0ac), - style: pw.TextStyle(font: font)); + return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font)); } // Bon de livraison============================================== - Future _generateBonLivraison(Commande commande) async { - final details = await _database.getDetailsCommande(commande.id!); - final client = await _database.getClientById(commande.clientId); - final pointDeVente = await _database.getPointDeVenteById(1); - - // Récupérer les informations des vendeurs - final commandeur = commande.commandeurId != null - ? await _database.getUserById(commande.commandeurId!) - : null; - final validateur = commande.validateurId != null - ? await _database.getUserById(commande.validateurId!) - : null; - - final iconPhone = await buildIconPhoneText(); - final iconChecked = await buildIconCheckedText(); - final iconGlobe = await buildIconGlobeText(); - - double sousTotal = 0; - double totalRemises = 0; - double totalCadeaux = 0; - int nombreCadeaux = 0; - - for (final detail in details) { - sousTotal += detail.sousTotal; - if (detail.estCadeau) { - totalCadeaux += detail.sousTotal; - nombreCadeaux += detail.quantite; - } else { - totalRemises += detail.montantRemise; - } - } - - final List> detailsAvecProduits = []; - for (final detail in details) { - final produit = await _database.getProductById(detail.produitId); - detailsAvecProduits.add({ - 'detail': detail, - 'produit': produit, - }); + bool verifAdmin() { + return userController.role == 'Super Admin'; + } +Future _generateBonLivraison(Commande commande) async { + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + final pointDeVente = await _database.getPointDeVenteById(1); + + // Récupérer les informations des vendeurs + final commandeur = commande.commandeurId != null + ? await _database.getUserById(commande.commandeurId!) + : null; + final validateur = commande.validateurId != null + ? await _database.getUserById(commande.validateurId!) + : null; + + final iconPhone = await buildIconPhoneText(); + final iconChecked = await buildIconCheckedText(); + final iconGlobe = await buildIconGlobeText(); + + double sousTotal = 0; + double totalRemises = 0; + double totalCadeaux = 0; + int nombreCadeaux = 0; + + for (final detail in details) { + sousTotal += detail.sousTotal; + if (detail.estCadeau) { + totalCadeaux += detail.sousTotal; + nombreCadeaux += detail.quantite; + } else { + totalRemises += detail.montantRemise; } + } - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - final italicFont = - pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); - - // Tailles de texte adaptées pour côte à côte - final tinyTextStyle = pw.TextStyle(fontSize: 5); - final smallTextStyle = pw.TextStyle(fontSize: 6); - final normalTextStyle = pw.TextStyle(fontSize: 7); - final boldTextStyle = - pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold); - final boldClientStyle = - pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold); - final frameTextStyle = pw.TextStyle(fontSize: 6); - final italicTextStyle = pw.TextStyle( - fontSize: 5, fontWeight: pw.FontWeight.bold, font: italicFont); - final italicLogoStyle = pw.TextStyle( - fontSize: 4, fontWeight: pw.FontWeight.bold, font: italicFont); - final titleStyle = - pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold); - - // Fonction pour créer un exemplaire - pw.Widget buildExemplaire(String typeExemplaire, {bool isSecond = false}) { - return pw.Container( - width: double.infinity, - decoration: pw.BoxDecoration( - border: pw.Border.all(color: PdfColors.black, width: 1), - ), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // En-tête avec indication de l'exemplaire - pw.Container( - width: double.infinity, - padding: const pw.EdgeInsets.all(3), - decoration: pw.BoxDecoration( - color: typeExemplaire == "CLIENT" - ? PdfColors.blue100 - : PdfColors.green100, - ), - child: pw.Center( - child: pw.Text( - 'BON DE LIVRAISON - EXEMPLAIRE $typeExemplaire', - style: pw.TextStyle( - fontSize: 8, - fontWeight: pw.FontWeight.bold, - color: typeExemplaire == "CLIENT" - ? PdfColors.blue800 - : PdfColors.green800, - ), + final List> detailsAvecProduits = []; + for (final detail in details) { + final produit = await _database.getProductById(detail.produitId); + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + } + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); + + // Tailles de texte agrandies pour une meilleure lisibilité + final tinyTextStyle = pw.TextStyle(fontSize: 9); + final smallTextStyle = pw.TextStyle(fontSize: 10); + final normalTextStyle = pw.TextStyle(fontSize: 11); + final boldTextStyle = pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold); + final boldClientStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold); + final frameTextStyle = pw.TextStyle(fontSize: 10); + final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont); + final italicLogoStyle = pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold, font: italicFont); + final titleStyle = pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold); + final headerStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold); + + // Fonction pour créer un exemplaire en mode paysage + pw.Widget buildExemplaire(String typeExemplaire) { + return pw.Container( + height: 380, // Hauteur ajustée pour le mode paysage + width: double.infinity, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1.5), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête avec indication de l'exemplaire + pw.Container( + width: double.infinity, + padding: const pw.EdgeInsets.all(5), + decoration: pw.BoxDecoration( + color: typeExemplaire == "CLIENT" ? PdfColors.blue100 : PdfColors.green100, + ), + child: pw.Center( + child: pw.Text( + 'BON DE LIVRAISON - EXEMPLAIRE $typeExemplaire', + style: pw.TextStyle( + fontSize: 14, + fontWeight: pw.FontWeight.bold, + color: typeExemplaire == "CLIENT" ? PdfColors.blue800 : PdfColors.green800, ), ), ), - - pw.Padding( - padding: const pw.EdgeInsets.all(6), + ), + + pw.Expanded( + child: pw.Padding( + padding: const pw.EdgeInsets.all(8), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ @@ -374,317 +353,221 @@ class _GestionCommandesPageState extends State { crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - // Logo et infos entreprise - très compact + // Logo et infos entreprise pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Container( - width: 45, - height: 45, + width: 100, + height: 100, child: pw.Image(image), ), - pw.Text('NOTRE COMPETENCE, A VOTRE SERVICE', - style: italicLogoStyle), pw.SizedBox(height: 3), + pw.Text('NOTRE COMPETENCE, A VOTRE SERVICE', style: italicLogoStyle), + pw.SizedBox(height: 4), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('📍 REMAX Andravoangy', - style: tinyTextStyle), - pw.Text('📍 SUPREME CENTER Behoririka', - style: tinyTextStyle), + pw.Text('📍 REMAX Andravoangy', style: tinyTextStyle), + pw.Text('📍 SUPREME CENTER Behoririka \n BOX 405 | 416 | 119', style: tinyTextStyle), + pw.Text('📍 Tripolisa analankely BOX 7', style: tinyTextStyle), pw.Text('📞 033 37 808 18', style: tinyTextStyle), pw.Text('🌐 www.guycom.mg', style: tinyTextStyle), - pw.SizedBox(height: 1), - // Ajout du NIF - pw.Text( - 'NIF: 4000106673 - STAT 95210 11 2017 1 003651', - style: pw.TextStyle( - fontSize: 5, - fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 2), + pw.Text('NIF: 4000106673 - STAT 95210 11 2017 1 003651', + style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), ], ), ], ), - + // Informations centrales pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ - pw.Text( - 'Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', - style: boldClientStyle), - pw.SizedBox(height: 3), - pw.Container( - width: 80, height: 1, color: PdfColors.black), - pw.SizedBox(height: 3), + pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldClientStyle), + pw.SizedBox(height: 4), + pw.Container(width: 100, height: 2, color: PdfColors.black), + pw.SizedBox(height: 4), pw.Container( - padding: const pw.EdgeInsets.all(3), + padding: const pw.EdgeInsets.all(6), decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.black), ), child: pw.Column( children: [ pw.Text('Boutique:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', - style: boldTextStyle), - pw.SizedBox(height: 1), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTextStyle), + pw.SizedBox(height: 2), pw.Text('Bon N°:', style: frameTextStyle), - pw.Text( - '${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', - style: boldTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTextStyle), ], ), ), ], ), - - // Informations client - compact + + // Informations client pw.Container( - width: 100, + width: 120, decoration: pw.BoxDecoration( - border: - pw.Border.all(color: PdfColors.black, width: 1), + border: pw.Border.all(color: PdfColors.black, width: 1), ), - padding: const pw.EdgeInsets.all(4), + padding: const pw.EdgeInsets.all(6), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Text('CLIENT', style: frameTextStyle), - pw.SizedBox(height: 1), - pw.Text( - 'ID: ${pointDeVente?['nom'] ?? 'S405A'}-${client?.id ?? 'Non spécifié'}', - style: smallTextStyle), - pw.Container( - width: 80, - height: 1, - color: PdfColors.black, - margin: - const pw.EdgeInsets.symmetric(vertical: 1)), - pw.Text(client?.nom ?? 'Non spécifié', - style: boldTextStyle), - pw.SizedBox(height: 1), - pw.Text(client?.telephone ?? 'Non spécifié', - style: tinyTextStyle), + pw.SizedBox(height: 2), + pw.Text('ID: ${pointDeVente?['nom'] ?? 'S405A'}-${client?.id ?? 'Non spécifié'}', style: smallTextStyle), + pw.Container(width: 100, height: 1, color: PdfColors.black, margin: const pw.EdgeInsets.symmetric(vertical: 2)), + pw.Text('${client?.nom} \n ${client?.prenom}', style: boldTextStyle), + pw.SizedBox(height: 2), + pw.Text(client?.telephone ?? 'Non spécifié', style: tinyTextStyle), ], ), ), ], ), - - pw.SizedBox(height: 4), - - // Tableau des produits - très compact - pw.Table( - border: pw.TableBorder.all(width: 0.5), - columnWidths: { - 0: const pw.FlexColumnWidth(3.5), - 1: const pw.FlexColumnWidth(0.8), - 2: const pw.FlexColumnWidth(1.2), - 3: const pw.FlexColumnWidth(1.5), - 4: const pw.FlexColumnWidth(1.2), - }, - children: [ - pw.TableRow( - decoration: - const pw.BoxDecoration(color: PdfColors.grey200), - children: [ - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Text('Désignations', - style: boldTextStyle)), - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Text('Qté', - style: boldTextStyle, - textAlign: pw.TextAlign.center)), - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Text('P.U.', - style: boldTextStyle, - textAlign: pw.TextAlign.right)), - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Text('Remise/Cadeau', - style: boldTextStyle, - textAlign: pw.TextAlign.center)), - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Text('Montant', - style: boldTextStyle, - textAlign: pw.TextAlign.right)), - ], - ), - ...detailsAvecProduits.map((item) { - final detail = item['detail'] as DetailCommande; - final produit = item['produit']; - - return pw.TableRow( - decoration: detail.estCadeau - ? const pw.BoxDecoration(color: PdfColors.green50) - : detail.aRemise - ? const pw.BoxDecoration( - color: PdfColors.orange50) - : null, + + pw.SizedBox(height: 8), + + // Tableau des produits (ajusté pour le mode paysage) + pw.Expanded( + child: pw.Table( + border: pw.TableBorder.all(width: 1), + columnWidths: { + 0: const pw.FlexColumnWidth(5), + 1: const pw.FlexColumnWidth(1.2), + 2: const pw.FlexColumnWidth(1.5), + 3: const pw.FlexColumnWidth(1.5), + 4: const pw.FlexColumnWidth(1.5), + }, + children: [ + pw.TableRow( + decoration: const pw.BoxDecoration(color: PdfColors.grey200), children: [ - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Row( - children: [ - pw.Expanded( - child: pw.Text( - detail.produitNom ?? - 'Produit inconnu', - style: pw.TextStyle( - fontSize: 6, - fontWeight: - pw.FontWeight.bold)), - ), - if (detail.estCadeau) - pw.Container( - padding: - const pw.EdgeInsets.symmetric( - horizontal: 1, vertical: 0.5), - decoration: pw.BoxDecoration( - color: PdfColors.green, - borderRadius: - pw.BorderRadius.circular(2), - ), - child: pw.Text('🎁', - style: pw.TextStyle( - fontSize: 4, - color: PdfColors.white)), + pw.Padding(padding: const pw.EdgeInsets.all(3), + child: pw.Text('Désignations', style: boldTextStyle)), + pw.Padding(padding: const pw.EdgeInsets.all(3), + child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)), + pw.Padding(padding: const pw.EdgeInsets.all(3), + child: pw.Text('P.U.', style: boldTextStyle, textAlign: pw.TextAlign.right)), + // pw.Padding(padding: const pw.EdgeInsets.all(3), + // child: pw.Text('Remise/Cadeau', style: boldTextStyle, textAlign: pw.TextAlign.center)), + pw.Padding(padding: const pw.EdgeInsets.all(3), + child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)), + ], + ), + + ...detailsAvecProduits.map((item) { + final detail = item['detail'] as DetailCommande; + final produit = item['produit']; + + return pw.TableRow( + decoration: detail.estCadeau + ? const pw.BoxDecoration(color: PdfColors.green50) + : detail.aRemise + ? const pw.BoxDecoration(color: PdfColors.orange50) + : null, + children: [ + pw.Padding( + padding: const pw.EdgeInsets.all(3), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Row( + children: [ + pw.Expanded( + child: pw.Text(detail.produitNom ?? 'Produit inconnu', + style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)), ), - ], - ), - if (produit?.category != null && - produit!.category.isNotEmpty) - pw.Text( - '${produit.category}${produit?.marque != null && produit!.marque.isNotEmpty ? ' - ${produit.marque}' : ''}', - style: tinyTextStyle), - if (produit?.imei != null && - produit!.imei!.isNotEmpty) - pw.Text('IMEI: ${produit.imei}', + if (detail.estCadeau) + pw.Container( + padding: const pw.EdgeInsets.symmetric(horizontal: 2, vertical: 1), + decoration: pw.BoxDecoration( + color: PdfColors.green, + borderRadius: pw.BorderRadius.circular(2), + ), + child: pw.Text('🎁', style: pw.TextStyle(fontSize: 5, color: PdfColors.white)), + ), + ], + ), + if (produit?.category != null && produit!.category.isNotEmpty) + pw.Text('${produit.category}${produit?.marque != null && produit!.marque.isNotEmpty ? ' - ${produit.marque}' : ''}', style: tinyTextStyle), - ], + if (produit?.imei != null && produit!.imei!.isNotEmpty) + pw.Text('IMEI: ${produit.imei}', style: tinyTextStyle), + ], + ), ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Text('${detail.quantite}', - style: normalTextStyle, - textAlign: pw.TextAlign.center), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - if (detail.estCadeau) ...[ - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 4, - decoration: - pw.TextDecoration.lineThrough, - color: PdfColors.grey600)), - pw.Text('GRATUIT', - style: pw.TextStyle( - fontSize: 5, - color: PdfColors.green700, - fontWeight: pw.FontWeight.bold)), - ] else if (detail.aRemise) ...[ - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 4, - decoration: - pw.TextDecoration.lineThrough, - color: PdfColors.grey600)), - pw.Text( - '${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 6, - color: PdfColors.orange)), - ] else - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: smallTextStyle), - ], + pw.Padding( + padding: const pw.EdgeInsets.all(3), + child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Text( - detail.estCadeau - ? 'CADEAU' - : detail.aRemise - ? 'REMISE' - : '-', - style: pw.TextStyle( - fontSize: 5, - color: detail.estCadeau - ? PdfColors.green700 - : detail.aRemise - ? PdfColors.orange - : PdfColors.grey600, - fontWeight: detail.estCadeau - ? pw.FontWeight.bold - : pw.FontWeight.normal, + pw.Padding( + padding: const pw.EdgeInsets.all(3), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (detail.estCadeau) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 8, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), + pw.Text('GRATUIT', style: pw.TextStyle(fontSize: 9, color: PdfColors.green700, fontWeight: pw.FontWeight.bold)), + ] else if (detail.aRemise) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 8, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), + pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 9, color: PdfColors.orange)), + ] else + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: smallTextStyle), + ], ), - textAlign: pw.TextAlign.center, ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(1), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - if (detail.estCadeau) ...[ - pw.Text( - '${detail.sousTotal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 4, - decoration: - pw.TextDecoration.lineThrough, - color: PdfColors.grey600)), - pw.Text('GRATUIT', - style: pw.TextStyle( - fontSize: 5, - fontWeight: pw.FontWeight.bold, - color: PdfColors.green700)), - ] else if (detail.aRemise) ...[ - pw.Text( - '${detail.sousTotal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 4, - decoration: - pw.TextDecoration.lineThrough, - color: PdfColors.grey600)), - pw.Text( - '${detail.prixFinal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 6, - fontWeight: pw.FontWeight.bold)), - ] else - pw.Text( - '${detail.prixFinal.toStringAsFixed(0)}', - style: smallTextStyle), - ], + // pw.Padding( + // padding: const pw.EdgeInsets.all(3), + // child: pw.Text( + // detail.estCadeau + // ? 'CADEAU' + // : detail.aRemise + // ? 'REMISE' + // : '-', + // style: pw.TextStyle( + // fontSize: 9, + // color: detail.estCadeau ? PdfColors.green700 : detail.aRemise ? PdfColors.orange : PdfColors.grey600, + // fontWeight: detail.estCadeau ? pw.FontWeight.bold : pw.FontWeight.normal, + // ), + // textAlign: pw.TextAlign.center, + // ), + // ), + pw.Padding( + padding: const pw.EdgeInsets.all(3), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (detail.estCadeau) ...[ + pw.Text('${detail.sousTotal.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 8, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), + pw.Text('GRATUIT', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, color: PdfColors.green700)), + ] else if (detail.aRemise) ...[ + pw.Text('${detail.sousTotal.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 8, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), + pw.Text('${detail.prixFinal.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + ] else + pw.Text('${detail.prixFinal.toStringAsFixed(0)}', style: smallTextStyle), + ], + ), ), - ), - ], - ); - }).toList(), - ], + ], + ); + }).toList(), + ], + ), ), - pw.SizedBox(height: 4), - - // Section finale - très compacte + pw.SizedBox(height: 8), + + // Section finale (ajustée pour le mode paysage) pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ @@ -699,63 +582,48 @@ class _GestionCommandesPageState extends State { mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('SOUS-TOTAL:', style: smallTextStyle), - pw.SizedBox(width: 8), - pw.Text('${sousTotal.toStringAsFixed(0)}', - style: smallTextStyle), + pw.SizedBox(width: 10), + pw.Text('${sousTotal.toStringAsFixed(0)}', style: smallTextStyle), ], ), - pw.SizedBox(height: 1), + pw.SizedBox(height: 2), ], + if (totalRemises > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ - pw.Text('REMISES:', - style: pw.TextStyle( - color: PdfColors.orange, - fontSize: 6)), - pw.SizedBox(width: 8), - pw.Text('-${totalRemises.toStringAsFixed(0)}', - style: pw.TextStyle( - color: PdfColors.orange, - fontSize: 6)), + pw.Text('REMISES:', style: pw.TextStyle(color: PdfColors.orange, fontSize: 10)), + pw.SizedBox(width: 10), + pw.Text('-${totalRemises.toStringAsFixed(0)}', style: pw.TextStyle(color: PdfColors.orange, fontSize: 10)), ], ), - pw.SizedBox(height: 1), + pw.SizedBox(height: 2), ], + if (totalCadeaux > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ - pw.Text('CADEAUX ($nombreCadeaux):', - style: pw.TextStyle( - color: PdfColors.green700, - fontSize: 6)), - pw.SizedBox(width: 8), - pw.Text('-${totalCadeaux.toStringAsFixed(0)}', - style: pw.TextStyle( - color: PdfColors.green700, - fontSize: 6)), + pw.Text('CADEAUX ($nombreCadeaux):', style: pw.TextStyle(color: PdfColors.green700, fontSize: 10)), + pw.SizedBox(width: 10), + pw.Text('-${totalCadeaux.toStringAsFixed(0)}', style: pw.TextStyle(color: PdfColors.green700, fontSize: 10)), ], ), - pw.SizedBox(height: 1), + pw.SizedBox(height: 2), ], - pw.Container( - width: 100, - height: 1, - color: PdfColors.black, - margin: - const pw.EdgeInsets.symmetric(vertical: 1)), + + pw.Container(width: 120, height: 1.5, color: PdfColors.black, margin: const pw.EdgeInsets.symmetric(vertical: 2)), + pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('TOTAL:', style: boldTextStyle), - pw.SizedBox(width: 8), - pw.Text( - '${commande.montantTotal.toStringAsFixed(0)} MGA', - style: boldTextStyle), + pw.SizedBox(width: 10), + pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', style: boldTextStyle), ], ), + if (totalCadeaux > 0) ...[ pw.SizedBox(height: 3), pw.Container( @@ -766,17 +634,16 @@ class _GestionCommandesPageState extends State { ), child: pw.Text( '🎁 $nombreCadeaux cadeau(s) offert(s) (${totalCadeaux.toStringAsFixed(0)} MGA)', - style: pw.TextStyle( - fontSize: 5, color: PdfColors.green700), + style: pw.TextStyle(fontSize: 9, color: PdfColors.green700), ), ), ], ], ), ), - - pw.SizedBox(width: 10), - + + pw.SizedBox(width: 15), + // Informations vendeurs et signatures pw.Expanded( flex: 3, @@ -785,7 +652,7 @@ class _GestionCommandesPageState extends State { children: [ // Vendeurs pw.Container( - padding: const pw.EdgeInsets.all(3), + padding: const pw.EdgeInsets.all(4), decoration: pw.BoxDecoration( color: PdfColors.grey100, borderRadius: pw.BorderRadius.circular(3), @@ -793,43 +660,30 @@ class _GestionCommandesPageState extends State { child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('VENDEURS', - style: pw.TextStyle( - fontSize: 6, - fontWeight: pw.FontWeight.bold)), - pw.SizedBox(height: 1), + pw.Text('VENDEURS', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 3), pw.Row( children: [ pw.Expanded( child: pw.Column( - crossAxisAlignment: - pw.CrossAxisAlignment.start, + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('Initiateur:', - style: tinyTextStyle), + pw.Text('Initiateur:', style: tinyTextStyle), pw.Text( - commandeur != null - ? '${commandeur.name} ${commandeur.lastName ?? ''}' - .trim() - : 'N/A', - style: pw.TextStyle(fontSize: 5), + commandeur != null ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() : 'N/A', + style: pw.TextStyle(fontSize: 9), ), ], ), ), pw.Expanded( child: pw.Column( - crossAxisAlignment: - pw.CrossAxisAlignment.start, + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('Validateur:', - style: tinyTextStyle), + pw.Text('Validateur:', style: tinyTextStyle), pw.Text( - validateur != null - ? '${validateur.name} ${validateur.lastName ?? ''}' - .trim() - : 'N/A', - style: pw.TextStyle(fontSize: 5), + validateur != null ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() : 'N/A', + style: pw.TextStyle(fontSize: 9), ), ], ), @@ -839,38 +693,25 @@ class _GestionCommandesPageState extends State { ], ), ), - - pw.SizedBox(height: 6), - + + pw.SizedBox(height: 8), + // Signatures pw.Row( - mainAxisAlignment: - pw.MainAxisAlignment.spaceBetween, + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Column( children: [ - pw.Text('Vendeur', - style: pw.TextStyle( - fontSize: 5, - fontWeight: pw.FontWeight.bold)), - pw.SizedBox(height: 8), - pw.Container( - width: 50, - height: 1, - color: PdfColors.black), + pw.Text('Vendeur', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 15), + pw.Container(width: 70, height: 1, color: PdfColors.black), ], ), pw.Column( children: [ - pw.Text('Client', - style: pw.TextStyle( - fontSize: 5, - fontWeight: pw.FontWeight.bold)), - pw.SizedBox(height: 8), - pw.Container( - width: 50, - height: 1, - color: PdfColors.black), + pw.Text('Client', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 15), + pw.Container(width: 70, height: 1, color: PdfColors.black), ], ), ], @@ -880,9 +721,9 @@ class _GestionCommandesPageState extends State { ), ], ), - - pw.SizedBox(height: 3), - + + pw.SizedBox(height: 4), + // Note finale pw.Text( 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', @@ -891,1387 +732,1237 @@ class _GestionCommandesPageState extends State { ], ), ), - ], - ), - ); - } - - pdf.addPage( - pw.Page( - pageFormat: PdfPageFormat.a4.landscape, - margin: const pw.EdgeInsets.all(10), - build: (pw.Context context) { - return pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // Exemplaire CLIENT (à gauche) - pw.Expanded( - child: buildExemplaire("CLIENT"), - ), - - pw.SizedBox(width: 10), - - // Ligne de séparation verticale avec ciseaux - pw.Column( + ), + ], + ), + ); + } + + // PAGE EN MODE PAYSAGE : Les deux exemplaires sur une seule page + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4.landscape, // Mode paysage + margin: const pw.EdgeInsets.all(12), + build: (pw.Context context) { + return pw.Row( // Utilisation de Row au lieu de Column pour placer côte à côte + children: [ + // Premier exemplaire (CLIENT) + pw.Expanded( + child: buildExemplaire("CLIENT"), + ), + + pw.SizedBox(width: 15), + + // Trait de séparation vertical + pw.Container( + width: 2, + height: double.infinity, + child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, children: [ + pw.Text('✂️', style: pw.TextStyle(fontSize: 14)), + pw.SizedBox(height: 10), pw.Transform.rotate( - angle: 3.14159 / 2, // 90 degrés en radians - child: pw.Text('✂️ DÉCOUPER ICI ✂️', - style: pw.TextStyle( - fontSize: 8, color: PdfColors.grey600)), - ), - pw.Container( - width: 1, - height: 200, - color: PdfColors.grey400, + angle: 1.5708, // 90 degrés en radians (π/2) + child: pw.Text('DÉCOUPER ICI', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)), ), + pw.SizedBox(height: 10), + pw.Text('✂️', style: pw.TextStyle(fontSize: 14)), ], ), - - pw.SizedBox(width: 10), - - // Exemplaire MAGASIN (à droite) - pw.Expanded( - child: buildExemplaire("MAGASIN", isSecond: true), - ), - ], - ); - }, - ), - ); - - // Sauvegarder le PDF - final output = await getTemporaryDirectory(); - final file = File("${output.path}/bon_livraison_${commande.id}.pdf"); - await file.writeAsBytes(await pdf.save()); - - // Partager ou ouvrir le fichier - await OpenFile.open(file.path); - } + ), + + pw.SizedBox(width: 15), + + // Deuxième exemplaire (MAGASIN) + pw.Expanded( + child: buildExemplaire("MAGASIN"), + ), + ], + ); + }, + ), + ); + + // Sauvegarder le PDF + final output = await getTemporaryDirectory(); + final file = File("${output.path}/bon_livraison_${commande.id}.pdf"); + await file.writeAsBytes(await pdf.save()); + + // Partager ou ouvrir le fichier + await OpenFile.open(file.path); +} //============================================================== - // Modifiez la méthode _generateInvoice dans GestionCommandesPage - Future _generateInvoice(Commande commande) async { - final details = await _database.getDetailsCommande(commande.id!); - final client = await _database.getClientById(commande.clientId); - final pointDeVente = await _database.getPointDeVenteById(1); - - // Récupérer les informations des vendeurs - final commandeur = commande.commandeurId != null - ? await _database.getUserById(commande.commandeurId!) - : null; - final validateur = commande.validateurId != null - ? await _database.getUserById(commande.validateurId!) - : null; - - final iconPhone = await buildIconPhoneText(); - final iconChecked = await buildIconCheckedText(); - final iconGlobe = await buildIconGlobeText(); - - double sousTotal = 0; - double totalRemises = 0; - double totalCadeaux = 0; - int nombreCadeaux = 0; - - for (final detail in details) { - sousTotal += detail.sousTotal; - if (detail.estCadeau) { - totalCadeaux += detail.sousTotal; - nombreCadeaux += detail.quantite; - } else { - totalRemises += detail.montantRemise; - } - } - final List> detailsAvecProduits = []; - for (final detail in details) { - final produit = await _database.getProductById(detail.produitId); - detailsAvecProduits.add({ - 'detail': detail, - 'produit': produit, - }); + // Modifiez la méthode _generateInvoice dans GestionCommandesPage +Future _generateInvoice(Commande commande) async { + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + final pointDeVente = await _database.getPointDeVenteById(1); + + // Récupérer les informations des vendeurs + final commandeur = commande.commandeurId != null + ? await _database.getUserById(commande.commandeurId!) + : null; + final validateur = commande.validateurId != null + ? await _database.getUserById(commande.validateurId!) + : null; + + final iconPhone = await buildIconPhoneText(); + final iconChecked = await buildIconCheckedText(); + final iconGlobe = await buildIconGlobeText(); + + double sousTotal = 0; + double totalRemises = 0; + double totalCadeaux = 0; + int nombreCadeaux = 0; + + for (final detail in details) { + sousTotal += detail.sousTotal; + if (detail.estCadeau) { + totalCadeaux += detail.sousTotal; + nombreCadeaux += detail.quantite; + } else { + totalRemises += detail.montantRemise; } + } - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - final italicFont = - pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); - - // Tailles de texte adaptées pour le mode portrait - final smallTextStyle = pw.TextStyle(fontSize: 8); - final normalTextStyle = pw.TextStyle(fontSize: 9); - final boldTextStyle = - pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold); - final boldClientTextStyle = - pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold); - final frameTextStyle = pw.TextStyle(fontSize: 9); - final italicTextStyle = pw.TextStyle( - fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont); - final italicTextStyleLogo = pw.TextStyle( - fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont); - final emojiSuportFont = - pw.Font.ttf(await rootBundle.load('assets/NotoEmoji-Regular.ttf')); - final emojifont = pw.TextStyle( - fontSize: 8, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); - - pdf.addPage( - pw.Page( - pageFormat: PdfPageFormat.a4, // Mode portrait - margin: const pw.EdgeInsets.all(20), // Marges normales - build: (pw.Context context) { - return pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // En-tête avec logo et informations - optimisé pour portrait - pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - // Section logo et adresses - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Container( - width: 120, - height: 120, - child: pw.Image(image), + final List> detailsAvecProduits = []; + for (final detail in details) { + final produit = await _database.getProductById(detail.produitId); + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + } + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); + + // Tailles de texte adaptées pour le mode portrait + final smallTextStyle = pw.TextStyle(fontSize: 8); + final normalTextStyle = pw.TextStyle(fontSize: 9); + final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold); + final boldClientTextStyle = pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold); + final frameTextStyle = pw.TextStyle(fontSize: 9); + final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont); + final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont); + final emojiSuportFont = pw.Font.ttf(await rootBundle.load('assets/NotoEmoji-Regular.ttf')); + final emojifont = pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); + + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4, // Mode portrait + margin: const pw.EdgeInsets.all(20), // Marges normales + build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête avec logo et informations - optimisé pour portrait + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + // Section logo et adresses + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Container( + width: 200, + height: 120, + child: pw.Image(image), + ), + pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo), + pw.SizedBox(height: 10), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]), + pw.SizedBox(height: 2), + pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]), + pw.SizedBox(height: 2), + pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]), + pw.SizedBox(height: 2), + pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]), + pw.SizedBox(height: 2), + pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]), + ], + ), + pw.SizedBox(height: 8), + pw.Row(children: [iconPhone, pw.SizedBox(width: 4), pw.Text('033 37 808 18', style: smallTextStyle)]), + pw.Row(children: [iconGlobe, pw.SizedBox(width: 4), pw.Text('www.guycom.mg', style: smallTextStyle)]), + pw.Row(children: [iconGlobe, pw.SizedBox(width: 4), pw.Text('NIF: 1026/GC78-20-02-22', style: smallTextStyle)]), + pw.Text('Facebook: GuyCom', style: smallTextStyle), + ], + ), + + // Section droite - informations commande et client + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldClientTextStyle), + pw.SizedBox(height: 8), + pw.Container(width: 200, height: 1, color: PdfColors.black), + pw.SizedBox(height: 10), + + // Informations boutique et facture + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Container( + width: 100, + height: 45, + padding: const pw.EdgeInsets.all(6), + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1), + ), + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + pw.Text('Boutique:', style: frameTextStyle), + pw.SizedBox(height: 2), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldClientTextStyle), + ] + ) + ), + pw.SizedBox(width: 10), + pw.Container( + width: 100, + height: 45, + padding: const pw.EdgeInsets.all(6), + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1), + ), + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + pw.Text('Facture N°:', style: frameTextStyle), + pw.SizedBox(height: 2), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldClientTextStyle), + ] + ) + ), + ], + ), + + pw.SizedBox(height: 15), + + // Section client + pw.Container( + width: 220, + height: 100, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1), ), - pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', - style: italicTextStyleLogo), - pw.SizedBox(height: 10), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, + padding: const pw.EdgeInsets.all(10), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ - pw.Row(children: [ - iconChecked, - pw.SizedBox(width: 4), - pw.Text('REMAX by GUYCOM Andravoangy', - style: smallTextStyle) - ]), - pw.SizedBox(height: 2), - pw.Row(children: [ - iconChecked, - pw.SizedBox(width: 4), - pw.Text('SUPREME CENTER Behoririka box 405', - style: smallTextStyle) - ]), - pw.SizedBox(height: 2), - pw.Row(children: [ - iconChecked, - pw.SizedBox(width: 4), - pw.Text('SUPREME CENTER Behoririka box 416', - style: smallTextStyle) - ]), - pw.SizedBox(height: 2), - pw.Row(children: [ - iconChecked, - pw.SizedBox(width: 4), - pw.Text('SUPREME CENTER Behoririka box 119', - style: smallTextStyle) - ]), - pw.SizedBox(height: 2), - pw.Row(children: [ - iconChecked, - pw.SizedBox(width: 4), - pw.Text('TRIPOLITSA Analakely BOX 7', - style: smallTextStyle) - ]), + pw.Text('ID Client: ', style: frameTextStyle), + pw.SizedBox(height: 4), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldClientTextStyle), + pw.SizedBox(height: 6), + pw.Container(width: 180, height: 1, color: PdfColors.black), + pw.SizedBox(height: 4), + pw.Text('${client?.nom} \n ${client?.prenom}', style: boldTextStyle), + pw.SizedBox(height: 4), + pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle), ], ), - pw.SizedBox(height: 8), - pw.Row(children: [ - iconPhone, - pw.SizedBox(width: 4), - pw.Text('033 37 808 18', style: smallTextStyle) - ]), - pw.Row(children: [ - iconGlobe, - pw.SizedBox(width: 4), - pw.Text('www.guycom.mg', style: smallTextStyle) - ]), - pw.Text('Facebook: GuyCom', style: smallTextStyle), - ], - ), - - // Section droite - informations commande et client - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - pw.Text( - 'Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', - style: boldClientTextStyle), - pw.SizedBox(height: 8), - pw.Container( - width: 200, height: 1, color: PdfColors.black), - pw.SizedBox(height: 10), - - // Informations boutique et facture - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Container( - width: 100, - height: 45, - padding: const pw.EdgeInsets.all(6), - decoration: pw.BoxDecoration( - border: pw.Border.all( - color: PdfColors.black, width: 1), - ), - child: pw.Column( - mainAxisAlignment: - pw.MainAxisAlignment.center, - children: [ - pw.Text('Boutique:', style: frameTextStyle), - pw.SizedBox(height: 2), - pw.Text( - '${pointDeVente?['nom'] ?? 'S405A'}', - style: boldClientTextStyle), - ])), - pw.SizedBox(width: 10), - pw.Container( - width: 100, - height: 45, - padding: const pw.EdgeInsets.all(6), - decoration: pw.BoxDecoration( - border: pw.Border.all( - color: PdfColors.black, width: 1), - ), - child: pw.Column( - mainAxisAlignment: - pw.MainAxisAlignment.center, - children: [ - pw.Text('Facture N°:', - style: frameTextStyle), - pw.SizedBox(height: 2), - pw.Text( - '${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', - style: boldClientTextStyle), - ])), - ], - ), - - pw.SizedBox(height: 15), - - // Section client - pw.Container( - width: 220, - height: 100, - decoration: pw.BoxDecoration( - border: - pw.Border.all(color: PdfColors.black, width: 1), - ), - padding: const pw.EdgeInsets.all(10), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text('ID Client: ', style: frameTextStyle), - pw.SizedBox(height: 4), - pw.Text( - '${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', - style: boldClientTextStyle), - pw.SizedBox(height: 6), - pw.Container( - width: 180, height: 1, color: PdfColors.black), - pw.SizedBox(height: 4), - pw.Text(client?.nom ?? 'Non spécifié', - style: boldClientTextStyle), - pw.SizedBox(height: 4), - pw.Text(client?.telephone ?? 'Non spécifié', - style: frameTextStyle), - ], - ), - ), - ], - ), - ], - ), - - pw.SizedBox(height: 15), - - // Tableau des produits avec cadeaux - optimisé pour portrait - pw.Table( - border: pw.TableBorder.all(width: 0.5), - columnWidths: { - 0: const pw.FlexColumnWidth(3), // Désignations - 1: const pw.FlexColumnWidth(0.8), // Quantité - 2: const pw.FlexColumnWidth(1.2), // Prix unitaire - 3: const pw.FlexColumnWidth(1.5), // Remise/cadeau - 4: const pw.FlexColumnWidth(1.2), // Montant - }, - children: [ - pw.TableRow( - decoration: - const pw.BoxDecoration(color: PdfColors.grey200), + ), + ], + ), + ], + ), + + pw.SizedBox(height: 15), + + // Tableau des produits avec cadeaux - optimisé pour portrait + pw.Table( + border: pw.TableBorder.all(width: 0.5), + columnWidths: { + 0: const pw.FlexColumnWidth(3), // Désignations + 1: const pw.FlexColumnWidth(0.8), // Quantité + 2: const pw.FlexColumnWidth(1.2), // Prix unitaire + 3: const pw.FlexColumnWidth(1.5), // Remise/cadeau + 4: const pw.FlexColumnWidth(1.2), // Montant + }, + children: [ + pw.TableRow( + decoration: const pw.BoxDecoration(color: PdfColors.grey200), + children: [ + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text('Désignations', style: boldTextStyle) + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center) + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right) + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text('Remise/Cadeau', style: boldTextStyle, textAlign: pw.TextAlign.center) + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right) + ), + ], + ), + + ...detailsAvecProduits.map((item) { + final detail = item['detail'] as DetailCommande; + final produit = item['produit']; + + return pw.TableRow( + decoration: detail.estCadeau + ? const pw.BoxDecoration( + color: PdfColors.green50, + border: pw.Border( + left: pw.BorderSide( + color: PdfColors.green300, + width: 3, + ), + ), + ) + : detail.aRemise + ? const pw.BoxDecoration( + color: PdfColors.orange50, + border: pw.Border( + left: pw.BorderSide( + color: PdfColors.orange300, + width: 3, + ), + ), + ) + : null, children: [ pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('Désignations', style: boldTextStyle)), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('Qté', - style: boldTextStyle, - textAlign: pw.TextAlign.center)), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('Prix unitaire', - style: boldTextStyle, - textAlign: pw.TextAlign.right)), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('Remise/Cadeau', - style: boldTextStyle, - textAlign: pw.TextAlign.center)), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('Montant', - style: boldTextStyle, - textAlign: pw.TextAlign.right)), - ], - ), - ...detailsAvecProduits.map((item) { - final detail = item['detail'] as DetailCommande; - final produit = item['produit']; - - return pw.TableRow( - decoration: detail.estCadeau - ? const pw.BoxDecoration( - color: PdfColors.green50, - border: pw.Border( - left: pw.BorderSide( - color: PdfColors.green300, - width: 3, + padding: const pw.EdgeInsets.all(4), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Row( + children: [ + pw.Expanded( + child: pw.Text(detail.produitNom ?? 'Produit inconnu', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), ), - ), - ) - : detail.aRemise - ? const pw.BoxDecoration( - color: PdfColors.orange50, - border: pw.Border( - left: pw.BorderSide( - color: PdfColors.orange300, - width: 3, + if (detail.estCadeau) + pw.Container( + padding: const pw.EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: pw.BoxDecoration( + color: PdfColors.green100, + borderRadius: pw.BorderRadius.circular(4), ), - ), - ) - : null, - children: [ - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Row( - children: [ - pw.Expanded( child: pw.Text( - detail.produitNom ?? 'Produit inconnu', - style: pw.TextStyle( - fontSize: 9, - fontWeight: pw.FontWeight.bold)), - ), - if (detail.estCadeau) - pw.Container( - padding: const pw.EdgeInsets.symmetric( - horizontal: 4, vertical: 2), - decoration: pw.BoxDecoration( - color: PdfColors.green100, - borderRadius: - pw.BorderRadius.circular(4), - ), - child: pw.Text( - 'CADEAU', - style: pw.TextStyle( - fontSize: 7, - fontWeight: pw.FontWeight.bold, - color: PdfColors.green700, - ), + 'CADEAU', + style: pw.TextStyle( + fontSize: 7, + fontWeight: pw.FontWeight.bold, + color: PdfColors.green700, ), ), + ), + ], + ), + pw.SizedBox(height: 2), + if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty) + pw.Text('${produit.category} - ${produit.marque}', style: smallTextStyle), + if (produit?.imei != null && produit!.imei!.isNotEmpty) + pw.Text('IMEI: ${produit.imei}', style: smallTextStyle), + if (produit?.reference != null && produit!.reference!.isNotEmpty) + pw.Row( + children: [ + if (produit?.ram != null && produit!.ram!.isNotEmpty) + pw.Text('${produit.ram}', style: smallTextStyle), + if (produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty) + pw.Text(' | ${produit.memoireInterne}', style: smallTextStyle), + pw.Text(' | ${produit.reference}', style: smallTextStyle), ], ), - pw.SizedBox(height: 2), - if (produit?.category != null && - produit!.category.isNotEmpty && - produit?.marque != null && - produit!.marque.isNotEmpty) - pw.Text( - '${produit.category} - ${produit.marque}', - style: smallTextStyle), - if (produit?.imei != null && - produit!.imei!.isNotEmpty) - pw.Text('IMEI: ${produit.imei}', - style: smallTextStyle), - if (produit?.reference != null && - produit!.reference!.isNotEmpty) - pw.Row( - children: [ - if (produit?.ram != null && - produit!.ram!.isNotEmpty) - pw.Text('${produit.ram}', - style: smallTextStyle), - if (produit?.memoireInterne != null && - produit!.memoireInterne!.isNotEmpty) - pw.Text(' | ${produit.memoireInterne}', - style: smallTextStyle), - pw.Text(' | ${produit.reference}', - style: smallTextStyle), - ], - ), - ], - ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('${detail.quantite}', - style: normalTextStyle, - textAlign: pw.TextAlign.center), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - if (detail.estCadeau) ...[ - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 7, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - )), - pw.Text('GRATUIT', - style: pw.TextStyle( - fontSize: 8, - color: PdfColors.green700, - fontWeight: pw.FontWeight.bold, - )), - ] else if (detail.aRemise && - detail.prixUnitaire != - detail.sousTotal / detail.quantite) ...[ - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 7, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - )), - pw.Text( - '${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 9, color: PdfColors.orange)), - ] else - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: normalTextStyle), - ], - ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text( - detail.estCadeau - ? 'CADEAU\nOFFERT' - : detail.aRemise - ? detail.remiseDescription - : '-', - style: pw.TextStyle( - fontSize: 7, - color: detail.estCadeau - ? PdfColors.green700 - : detail.aRemise - ? PdfColors.orange - : PdfColors.grey600, - fontWeight: detail.estCadeau - ? pw.FontWeight.bold - : pw.FontWeight.normal, - ), - textAlign: pw.TextAlign.center, - ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - if (detail.estCadeau) ...[ - pw.Text( - '${detail.sousTotal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 7, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - )), - pw.Text('GRATUIT', - style: pw.TextStyle( - fontSize: 8, - fontWeight: pw.FontWeight.bold, - color: PdfColors.green700, - )), - ] else if (detail.aRemise && - detail.sousTotal != detail.prixFinal) ...[ - pw.Text( - '${detail.sousTotal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 7, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - )), - pw.Text( - '${detail.prixFinal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 9, - fontWeight: pw.FontWeight.bold)), - ] else - pw.Text( - '${detail.prixFinal.toStringAsFixed(0)}', - style: normalTextStyle), - ], - ), - ), - ], - ); - }).toList(), - ], - ), - - pw.SizedBox(height: 12), - - // Section totaux - alignée à droite - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - if (totalRemises > 0 || totalCadeaux > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Text('SOUS-TOTAL', style: normalTextStyle), - pw.SizedBox(width: 20), - pw.Container( - width: 80, - child: pw.Text('${sousTotal.toStringAsFixed(0)}', - style: normalTextStyle, - textAlign: pw.TextAlign.right), - ), ], ), - pw.SizedBox(height: 4), - ], - if (totalRemises > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ - pw.Text('REMISES TOTALES', + if (detail.estCadeau) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: pw.TextStyle( - color: PdfColors.orange, fontSize: 9)), - pw.SizedBox(width: 20), - pw.Container( - width: 80, - child: pw.Text( - '-${totalRemises.toStringAsFixed(0)}', - style: pw.TextStyle( - color: PdfColors.orange, - fontWeight: pw.FontWeight.bold, - fontSize: 9), - textAlign: pw.TextAlign.right), - ), + fontSize: 7, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('GRATUIT', + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.green700, + fontWeight: pw.FontWeight.bold, + )), + ] else if (detail.aRemise && detail.prixUnitaire != detail.sousTotal / detail.quantite) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 7, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 9, color: PdfColors.orange)), + ] else + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: normalTextStyle), ], ), - pw.SizedBox(height: 4), - ], - if (totalCadeaux > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text( + detail.estCadeau + ? 'CADEAU\nOFFERT' + : detail.aRemise + ? detail.remiseDescription + : '-', + style: pw.TextStyle( + fontSize: 7, + color: detail.estCadeau + ? PdfColors.green700 + : detail.aRemise + ? PdfColors.orange + : PdfColors.grey600, + fontWeight: detail.estCadeau ? pw.FontWeight.bold : pw.FontWeight.normal, + ), + textAlign: pw.TextAlign.center, + ), + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ - pw.Text('CADEAUX OFFERTS ($nombreCadeaux)', + if (detail.estCadeau) ...[ + pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: pw.TextStyle( - color: PdfColors.green700, fontSize: 9)), - pw.SizedBox(width: 20), - pw.Container( - width: 80, - child: pw.Text( - '-${totalCadeaux.toStringAsFixed(0)}', - style: pw.TextStyle( - color: PdfColors.green700, - fontWeight: pw.FontWeight.bold, - fontSize: 9), - textAlign: pw.TextAlign.right), - ), + fontSize: 7, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('GRATUIT', + style: pw.TextStyle( + fontSize: 8, + fontWeight: pw.FontWeight.bold, + color: PdfColors.green700, + )), + ] else if (detail.aRemise && detail.sousTotal != detail.prixFinal) ...[ + pw.Text('${detail.sousTotal.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 7, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('${detail.prixFinal.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + ] else + pw.Text('${detail.prixFinal.toStringAsFixed(0)}', + style: normalTextStyle), ], ), - pw.SizedBox(height: 4), - ], - if (totalRemises > 0 || totalCadeaux > 0) ...[ - pw.Container( - width: 200, - height: 1, - color: PdfColors.black, - margin: const pw.EdgeInsets.symmetric(vertical: 4), - ), - ], + ), + ], + ); + }).toList(), + ], + ), + + pw.SizedBox(height: 12), + + // Section totaux - alignée à droite + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (totalRemises > 0 || totalCadeaux > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ - pw.Text('TOTAL', style: boldTextStyle), + pw.Text('SOUS-TOTAL', style: normalTextStyle), pw.SizedBox(width: 20), pw.Container( width: 80, - child: pw.Text( - '${commande.montantTotal.toStringAsFixed(0)}', - style: boldTextStyle, - textAlign: pw.TextAlign.right), + child: pw.Text('${sousTotal.toStringAsFixed(0)}', + style: normalTextStyle, textAlign: pw.TextAlign.right), ), ], ), - if (totalRemises > 0 || totalCadeaux > 0) ...[ - pw.SizedBox(height: 4), - pw.Text( - 'Économies réalisées: ${(totalRemises + totalCadeaux).toStringAsFixed(0)} MGA', - style: pw.TextStyle( - fontSize: 8, - color: PdfColors.green, - fontStyle: pw.FontStyle.italic, + pw.SizedBox(height: 4), + ], + + if (totalRemises > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('REMISES TOTALES', style: pw.TextStyle(color: PdfColors.orange, fontSize: 9)), + pw.SizedBox(width: 20), + pw.Container( + width: 80, + child: pw.Text('-${totalRemises.toStringAsFixed(0)}', + style: pw.TextStyle(color: PdfColors.orange, fontWeight: pw.FontWeight.bold, fontSize: 9), + textAlign: pw.TextAlign.right), ), - ), - ], + ], + ), + pw.SizedBox(height: 4), ], - ), - ], - ), - - pw.SizedBox(height: 15), - - // Montant en lettres - pw.Text( - 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', - style: italicTextStyle), - - pw.SizedBox(height: 15), - - // Informations vendeurs - Section dédiée - pw.Container( - width: double.infinity, - padding: const pw.EdgeInsets.all(12), - decoration: pw.BoxDecoration( - color: PdfColors.grey100, - borderRadius: pw.BorderRadius.circular(8), - border: pw.Border.all(color: PdfColors.grey300), - ), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text( - 'INFORMATIONS VENDEURS', - style: pw.TextStyle( - fontSize: 11, - fontWeight: pw.FontWeight.bold, - color: PdfColors.blue700, + + if (totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('CADEAUX OFFERTS ($nombreCadeaux)', + style: pw.TextStyle(color: PdfColors.green700, fontSize: 9)), + pw.SizedBox(width: 20), + pw.Container( + width: 80, + child: pw.Text('-${totalCadeaux.toStringAsFixed(0)}', + style: pw.TextStyle(color: PdfColors.green700, fontWeight: pw.FontWeight.bold, fontSize: 9), + textAlign: pw.TextAlign.right), + ), + ], ), - ), - pw.SizedBox(height: 8), + pw.SizedBox(height: 4), + ], + + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.Container( + width: 200, + height: 1, + color: PdfColors.black, + margin: const pw.EdgeInsets.symmetric(vertical: 4), + ), + ], + pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + mainAxisAlignment: pw.MainAxisAlignment.end, children: [ - pw.Expanded( - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text( - 'Vendeur initiateur:', - style: pw.TextStyle( - fontSize: 9, - fontWeight: pw.FontWeight.bold, - color: PdfColors.grey700, - ), - ), - pw.SizedBox(height: 3), - pw.Text( - commandeur != null - ? '${commandeur.name} ${commandeur.lastName ?? ''}' - .trim() - : 'Non spécifié', - style: pw.TextStyle( - fontSize: 10, - color: PdfColors.black, - ), - ), - pw.SizedBox(height: 3), - pw.Text( - 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', - style: pw.TextStyle( - fontSize: 8, - color: PdfColors.grey600, - ), - ), - ], - ), - ), - pw.Container( - width: 1, - height: 40, - color: PdfColors.grey400, - ), + pw.Text('TOTAL', style: boldTextStyle), pw.SizedBox(width: 20), - pw.Expanded( - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text( - 'Vendeur validateur:', - style: pw.TextStyle( - fontSize: 9, - fontWeight: pw.FontWeight.bold, - color: PdfColors.grey700, - ), - ), - pw.SizedBox(height: 3), - pw.Text( - validateur != null - ? '${validateur.name} ${validateur.lastName ?? ''}' - .trim() - : 'Non spécifié', - style: pw.TextStyle( - fontSize: 10, - color: PdfColors.black, - ), - ), - pw.SizedBox(height: 3), - pw.Text( - 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now())}', - style: pw.TextStyle( - fontSize: 8, - color: PdfColors.grey600, - ), - ), - ], - ), + pw.Container( + width: 80, + child: pw.Text('${commande.montantTotal.toStringAsFixed(0)}', + style: boldTextStyle, textAlign: pw.TextAlign.right), ), ], ), + + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.SizedBox(height: 4), + pw.Text( + 'Économies réalisées: ${(totalRemises + totalCadeaux).toStringAsFixed(0)} MGA', + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.green, + fontStyle: pw.FontStyle.italic, + ), + ), + ], ], ), + ], + ), + + pw.SizedBox(height: 15), + + // Montant en lettres + pw.Text('Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', + style: italicTextStyle), + + pw.SizedBox(height: 15), + + // Informations vendeurs - Section dédiée + pw.Container( + width: double.infinity, + padding: const pw.EdgeInsets.all(12), + decoration: pw.BoxDecoration( + color: PdfColors.grey100, + borderRadius: pw.BorderRadius.circular(8), + border: pw.Border.all(color: PdfColors.grey300), ), - - pw.SizedBox(height: 12), - - // Note de remerciement pour les cadeaux - if (totalCadeaux > 0) ...[ - pw.Container( - width: double.infinity, - padding: const pw.EdgeInsets.all(10), - decoration: pw.BoxDecoration( - color: PdfColors.blue50, - borderRadius: pw.BorderRadius.circular(6), - border: pw.Border.all(color: PdfColors.blue200), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'INFORMATIONS VENDEURS', + style: pw.TextStyle( + fontSize: 11, + fontWeight: pw.FontWeight.bold, + color: PdfColors.blue700, + ), ), - child: pw.Row( + pw.SizedBox(height: 8), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - pw.Text('🎁 ', style: emojifont), pw.Expanded( - child: pw.Text( - 'Merci de votre confiance ! Nous espérons que nos cadeaux vous feront plaisir. ($nombreCadeaux article(s) offert(s) - Valeur: ${totalCadeaux.toStringAsFixed(0)} MGA)', - style: pw.TextStyle( - fontSize: 9, - fontStyle: pw.FontStyle.italic, - color: PdfColors.blue700, - ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Vendeur initiateur:', + style: pw.TextStyle( + fontSize: 9, + fontWeight: pw.FontWeight.bold, + color: PdfColors.grey700, + ), + ), + pw.SizedBox(height: 3), + pw.Text( + commandeur != null + ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() + : 'Non spécifié', + style: pw.TextStyle( + fontSize: 10, + color: PdfColors.black, + ), + ), + pw.SizedBox(height: 3), + pw.Text( + 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.grey600, + ), + ), + ], ), ), - ], - ), - ), - pw.SizedBox(height: 12), - ], - - // Signatures - espacées sur toute la largeur - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text( - 'Signature vendeur initiateur', - style: pw.TextStyle( - fontSize: 9, fontWeight: pw.FontWeight.bold), - ), - pw.SizedBox(height: 2), - pw.Text( - commandeur != null - ? '${commandeur.name} ${commandeur.lastName ?? ''}' - .trim() - : 'Non spécifié', - style: - pw.TextStyle(fontSize: 8, color: PdfColors.grey600), - ), - pw.SizedBox(height: 20), pw.Container( - width: 120, height: 1, color: PdfColors.black), - ], - ), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text( - 'Signature vendeur validateur', - style: pw.TextStyle( - fontSize: 9, fontWeight: pw.FontWeight.bold), + width: 1, + height: 40, + color: PdfColors.grey400, ), - pw.SizedBox(height: 2), - pw.Text( - validateur != null - ? '${validateur.name} ${validateur.lastName ?? ''}' - .trim() - : 'Non spécifié', - style: - pw.TextStyle(fontSize: 8, color: PdfColors.grey600), + pw.SizedBox(width: 20), + pw.Expanded( + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Vendeur validateur:', + style: pw.TextStyle( + fontSize: 9, + fontWeight: pw.FontWeight.bold, + color: PdfColors.grey700, + ), + ), + pw.SizedBox(height: 3), + pw.Text( + validateur != null + ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() + : 'Non spécifié', + style: pw.TextStyle( + fontSize: 10, + color: PdfColors.black, + ), + ), + pw.SizedBox(height: 3), + pw.Text( + 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now())}', + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.grey600, + ), + ), + ], + ), ), - pw.SizedBox(height: 20), - pw.Container( - width: 120, height: 1, color: PdfColors.black), ], ), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text( - 'Signature du client', + ], + ), + ), + + pw.SizedBox(height: 12), + + // Note de remerciement pour les cadeaux + if (totalCadeaux > 0) ...[ + pw.Container( + width: double.infinity, + padding: const pw.EdgeInsets.all(10), + decoration: pw.BoxDecoration( + color: PdfColors.blue50, + borderRadius: pw.BorderRadius.circular(6), + border: pw.Border.all(color: PdfColors.blue200), + ), + child: pw.Row( + children: [ + pw.Text('🎁 ', style: emojifont), + pw.Expanded( + child: pw.Text( + 'Merci de votre confiance ! Nous espérons que nos cadeaux vous feront plaisir. ($nombreCadeaux article(s) offert(s) - Valeur: ${totalCadeaux.toStringAsFixed(0)} MGA)', style: pw.TextStyle( - fontSize: 9, fontWeight: pw.FontWeight.bold), - ), - pw.SizedBox(height: 2), - pw.Text( - client?.nomComplet ?? 'Non spécifié', - style: - pw.TextStyle(fontSize: 8, color: PdfColors.grey600), + fontSize: 9, + fontStyle: pw.FontStyle.italic, + color: PdfColors.blue700, + ), ), - pw.SizedBox(height: 20), - pw.Container( - width: 120, height: 1, color: PdfColors.black), - ], - ), - ], + ), + ], + ), ), + pw.SizedBox(height: 12), ], - ); - }, - ), - ); + + // Signatures - espacées sur toute la largeur + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text( + 'Signature vendeur initiateur', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold), + ), + pw.SizedBox(height: 2), + pw.Text( + commandeur != null + ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() + : 'Non spécifié', + style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600), + ), + pw.SizedBox(height: 20), + pw.Container(width: 120, height: 1, color: PdfColors.black), + ], + ), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text( + 'Signature vendeur validateur', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold), + ), + pw.SizedBox(height: 2), + pw.Text( + validateur != null + ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() + : 'Non spécifié', + style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600), + ), + pw.SizedBox(height: 20), + pw.Container(width: 120, height: 1, color: PdfColors.black), + ], + ), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text( + 'Signature du client', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold), + ), + pw.SizedBox(height: 2), + pw.Text( + client?.nomComplet ?? 'Non spécifié', + style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600), + ), + pw.SizedBox(height: 20), + pw.Container(width: 120, height: 1, color: PdfColors.black), + ], + ), + ], + ), + ], + ); + }, + ), + ); - final output = await getTemporaryDirectory(); - final file = File('${output.path}/facture_${commande.id}.pdf'); - await file.writeAsBytes(await pdf.save()); - await OpenFile.open(file.path); - } + final output = await getTemporaryDirectory(); + final file = File('${output.path}/facture_${commande.id}.pdf'); + await file.writeAsBytes(await pdf.save()); + await OpenFile.open(file.path); +} String _numberToWords(int number) { NumbersToLetters.toLetters('fr', number); return NumbersToLetters.toLetters('fr', number); } - Future _generateInvoiceWithPasswordVerification( - Commande commande) async { - await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return PasswordVerificationDialog( - title: 'Génération de facture', - message: - 'Pour générer la facture de la commande #${commande.id}, veuillez confirmer votre identité en saisissant votre mot de passe.', - onPasswordVerified: (String password) async { - // Afficher un indicateur de chargement - Get.dialog( - const Center( - child: CircularProgressIndicator(), - ), - barrierDismissible: false, - ); - - try { - await _generateInvoice(commande); - Get.back(); // Fermer l'indicateur de chargement - - Get.snackbar( - 'Succès', - 'Facture générée avec succès', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } catch (e) { - Get.back(); // Fermer l'indicateur de chargement - Get.snackbar( - 'Erreur', - 'Erreur lors de la génération de la facture: $e', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - duration: const Duration(seconds: 3), - ); - } - }, - ); - }, - ); - } - Future _generateBon_lifraisonWithPasswordVerification( - Commande commande) async { - await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return PasswordVerificationDialog( - title: 'Génération de Bon de livraison', - message: - 'Pour générer de Bon de livraison de la commande #${commande.id}, veuillez confirmer votre identité en saisissant votre mot de passe.', - onPasswordVerified: (String password) async { - // Afficher un indicateur de chargement - Get.dialog( - const Center( - child: CircularProgressIndicator(), - ), - barrierDismissible: false, +Future _generateInvoiceWithPasswordVerification(Commande commande) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return PasswordVerificationDialog( + title: 'Génération de facture', + message: 'Pour générer la facture de la commande #${commande.id}, veuillez confirmer votre identité en saisissant votre mot de passe.', + onPasswordVerified: (String password) async { + // Afficher un indicateur de chargement + Get.dialog( + const Center( + child: CircularProgressIndicator(), + ), + barrierDismissible: false, + ); + + try { + await _generateInvoice(commande); + Get.back(); // Fermer l'indicateur de chargement + + Get.snackbar( + 'Succès', + 'Facture générée avec succès', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), ); + } catch (e) { + Get.back(); // Fermer l'indicateur de chargement + Get.snackbar( + 'Erreur', + 'Erreur lors de la génération de la facture: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + }, + ); + }, + ); +} +Future _generateBon_lifraisonWithPasswordVerification(Commande commande) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return PasswordVerificationDialog( + title: 'Génération de Bon de livraison', + message: 'Pour générer de Bon de livraison de la commande #${commande.id}, veuillez confirmer votre identité en saisissant votre mot de passe.', + onPasswordVerified: (String password) async { + // Afficher un indicateur de chargement + Get.dialog( + const Center( + child: CircularProgressIndicator(), + ), + barrierDismissible: false, + ); + + try { + await _generateBonLivraison(commande); + Get.back(); // Fermer l'indicateur de chargement + + Get.snackbar( + 'Succès', + 'Facture générée avec succès', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } catch (e) { + Get.back(); // Fermer l'indicateur de chargement + Get.snackbar( + 'Erreur', + 'Erreur lors de la génération de la facture: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + }, + ); + }, + ); +} - try { - await _generateBonLivraison(commande); - Get.back(); // Fermer l'indicateur de chargement - - Get.snackbar( - 'Succès', - 'Facture générée avec succès', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } catch (e) { - Get.back(); // Fermer l'indicateur de chargement - Get.snackbar( - 'Erreur', - 'Erreur lors de la génération de la facture: $e', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - duration: const Duration(seconds: 3), - ); - } - }, - ); - }, - ); - } - String _getPaymentMethodLabel(PaymentMethod payment) { - switch (payment.type) { - case PaymentType.cash: - return 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)'; - case PaymentType.card: - return 'CARTE BANCAIRE'; - case PaymentType.mvola: - return 'MVOLA'; - case PaymentType.orange: - return 'ORANGE MONEY'; - case PaymentType.airtel: - return 'AIRTEL MONEY'; - default: - return 'MÉTHODE INCONNUE (${payment.type.toString()})'; // Debug info - } +String _getPaymentMethodLabel(PaymentMethod payment) { + switch (payment.type) { + case PaymentType.cash: + return 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)'; + case PaymentType.card: + return 'CARTE BANCAIRE'; + case PaymentType.mvola: + return 'MVOLA'; + case PaymentType.orange: + return 'ORANGE MONEY'; + case PaymentType.airtel: + return 'AIRTEL MONEY'; + default: + return 'MÉTHODE INCONNUE (${payment.type.toString()})'; // Debug info } - - Future _generateReceipt( - Commande commande, PaymentMethod payment) async { +} + Future _generateReceipt(Commande commande, PaymentMethod payment) async { final details = await _database.getDetailsCommande(commande.id!); - final client = await _database.getClientById(commande.clientId); - final commandeur = commande.commandeurId != null - ? await _database.getUserById(commande.commandeurId!) - : null; - final validateur = commande.validateurId != null - ? await _database.getUserById(commande.validateurId!) - : null; - final pointDeVente = commandeur?.pointDeVenteId != null - ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) - : null; - final emojiSuportFont = - pw.Font.ttf(await rootBundle.load('assets/NotoEmoji-Regular.ttf')); - final emojifont = pw.TextStyle( - fontSize: 7, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); - final List> detailsAvecProduits = []; - for (final detail in details) { - final produit = await _database.getProductById(detail.produitId); - detailsAvecProduits.add({ - 'detail': detail, - 'produit': produit, - }); - } - - double sousTotal = 0; - double totalRemises = 0; - double totalCadeaux = 0; - int nombreCadeaux = 0; - - for (final detail in details) { - sousTotal += detail.sousTotal; - if (detail.estCadeau) { - totalCadeaux += detail.sousTotal; - nombreCadeaux += detail.quantite; - } else { - totalRemises += detail.montantRemise; - } + final client = await _database.getClientById(commande.clientId); + final commandeur = commande.commandeurId != null + ? await _database.getUserById(commande.commandeurId!) + : null; + final validateur = commande.validateurId != null + ? await _database.getUserById(commande.validateurId!) + : null; + final pointDeVente = commandeur?.pointDeVenteId != null + ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) + : null; +final emojiSuportFont = pw.Font.ttf( await rootBundle.load('assets/NotoEmoji-Regular.ttf')); + final emojifont = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); + final List> detailsAvecProduits = []; + for (final detail in details) { + final produit = await _database.getProductById(detail.produitId); + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + } + + double sousTotal = 0; + double totalRemises = 0; + double totalCadeaux = 0; + int nombreCadeaux = 0; + + for (final detail in details) { + sousTotal += detail.sousTotal; + if (detail.estCadeau) { + totalCadeaux += detail.sousTotal; + nombreCadeaux += detail.quantite; + } else { + totalRemises += detail.montantRemise; } - - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - // DEBUG: Affichage des informations de paiement - print('=== DEBUG PAYMENT METHOD ==='); - print('Payment type: ${payment.type}'); - print('Payment type toString: ${payment.type.toString()}'); - print('Payment type runtimeType: ${payment.type.runtimeType}'); - print('Payment type index: ${payment.type.index}'); - print('Amount given: ${payment.amountGiven}'); - print('PaymentType.airtel: ${PaymentType.airtel}'); - print( - 'payment.type == PaymentType.airtel: ${payment.type == PaymentType.airtel}'); - print('=== END DEBUG ==='); - pdf.addPage( - pw.Page( - pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity), - margin: const pw.EdgeInsets.all(4), - build: (pw.Context context) { - return pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Center( - child: pw.Container( - width: 40, - height: 40, - child: pw.Image(image), - ), + } + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + // DEBUG: Affichage des informations de paiement + print('=== DEBUG PAYMENT METHOD ==='); + print('Payment type: ${payment.type}'); + print('Payment type toString: ${payment.type.toString()}'); + print('Payment type runtimeType: ${payment.type.runtimeType}'); + print('Payment type index: ${payment.type.index}'); + print('Amount given: ${payment.amountGiven}'); + print('PaymentType.airtel: ${PaymentType.airtel}'); + print('payment.type == PaymentType.airtel: ${payment.type == PaymentType.airtel}'); + print('=== END DEBUG ==='); + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity), + margin: const pw.EdgeInsets.all(4), + build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Center( + child: pw.Container( + width: 40, + height: 40, + child: pw.Image(image), ), - pw.SizedBox(height: 4), - - pw.Text('GUYCOM MADAGASCAR', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - )), - pw.Text('Tél: 033 37 808 18', - style: const pw.TextStyle(fontSize: 7)), - pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)), - - pw.SizedBox(height: 6), - - pw.Text('TICKET DE CAISSE', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - decoration: pw.TextDecoration.underline, - )), - pw.Text( - 'N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}', - style: const pw.TextStyle(fontSize: 8)), - pw.Text( - 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', - style: const pw.TextStyle(fontSize: 8)), - - if (pointDeVente != null) - pw.Text('Point de vente: ${pointDeVente['designation']}', - style: const pw.TextStyle(fontSize: 8)), - - pw.Divider(thickness: 0.5), - - pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', - style: pw.TextStyle( - fontSize: 8, fontWeight: pw.FontWeight.bold)), - if (client?.telephone != null) - pw.Text('Tél: ${client!.telephone}', - style: const pw.TextStyle(fontSize: 7)), - - if (commandeur != null || validateur != null) - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Divider(thickness: 0.5), - if (commandeur != null) - pw.Text('Vendeur: ${commandeur.name}', - style: const pw.TextStyle(fontSize: 7)), - if (validateur != null) - pw.Text('Validateur: ${validateur.name}', - style: const pw.TextStyle(fontSize: 7)), - ], - ), - - pw.Divider(thickness: 0.5), - - // Tableau des produits avec cadeaux - pw.Table( - columnWidths: { - 0: const pw.FlexColumnWidth(3.5), - 1: const pw.FlexColumnWidth(1), - 2: const pw.FlexColumnWidth(1.5), - }, + ), + pw.SizedBox(height: 4), + + pw.Text('GUYCOM MADAGASCAR', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + )), + pw.Text('Tél: 033 37 808 18', style: const pw.TextStyle(fontSize: 7)), + pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)), + + pw.SizedBox(height: 6), + + pw.Text('TICKET DE CAISSE', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + decoration: pw.TextDecoration.underline, + )), + pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}', + style: const pw.TextStyle(fontSize: 8)), + pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', + style: const pw.TextStyle(fontSize: 8)), + + if (pointDeVente != null) + pw.Text('Point de vente: ${pointDeVente['designation']}', + style: const pw.TextStyle(fontSize: 8)), + + pw.Divider(thickness: 0.5), + + pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', + style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), + if (client?.telephone != null) + pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)), + + if (commandeur != null || validateur != null) + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.TableRow( - children: [ - pw.Text('Désignation', - style: pw.TextStyle( - fontSize: 7, fontWeight: pw.FontWeight.bold)), - pw.Text('Qté', - style: pw.TextStyle( - fontSize: 7, fontWeight: pw.FontWeight.bold)), - pw.Text('P.U', - style: pw.TextStyle( - fontSize: 7, fontWeight: pw.FontWeight.bold)), - ], - decoration: const pw.BoxDecoration( - border: pw.Border(bottom: pw.BorderSide(width: 0.5)), - ), - ), - ...detailsAvecProduits.map((item) { - final detail = item['detail'] as DetailCommande; - final produit = item['produit']; - - return pw.TableRow( - decoration: const pw.BoxDecoration( - border: pw.Border(bottom: pw.BorderSide(width: 0.2))), - children: [ - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Row( - children: [ - pw.Expanded( - child: pw.Text(detail.produitNom ?? 'Produit', - style: const pw.TextStyle(fontSize: 7)), - ), - if (detail.estCadeau) - pw.Text('🎁', style: emojifont), - ], - ), - if (produit?.reference != null) - pw.Text('Ref: ${produit!.reference}', - style: const pw.TextStyle(fontSize: 6)), - if (produit?.imei != null) - pw.Text('IMEI: ${produit!.imei}', - style: const pw.TextStyle(fontSize: 6)), - if (detail.estCadeau) - pw.Text('CADEAU OFFERT', - style: pw.TextStyle( - fontSize: 6, - color: PdfColors.green700, - fontWeight: pw.FontWeight.bold, - )), - if (detail.aRemise && !detail.estCadeau) - pw.Text('Remise: ${detail.remiseDescription}', - style: pw.TextStyle( - fontSize: 6, color: PdfColors.orange)), - ], - ), - pw.Text(detail.quantite.toString(), - style: const pw.TextStyle(fontSize: 7)), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - if (detail.estCadeau) ...[ - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 6, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - )), - pw.Text('GRATUIT', - style: pw.TextStyle( - fontSize: 7, - color: PdfColors.green700, - fontWeight: pw.FontWeight.bold, - )), - ] else if (detail.aRemise && - detail.prixUnitaire != - detail.prixFinal / detail.quantite) ...[ - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 6, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - )), - pw.Text( - '${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', - style: const pw.TextStyle(fontSize: 7)), - ] else - pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: const pw.TextStyle(fontSize: 7)), - ], - ), - ], - ); - }), + pw.Divider(thickness: 0.5), + if (commandeur != null) + pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)), + if (validateur != null) + pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)), ], ), - - pw.Divider(thickness: 0.5), - - // Totaux avec remises et cadeaux pour le ticket - if (totalRemises > 0 || totalCadeaux > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + + pw.Divider(thickness: 0.5), + + // Tableau des produits avec cadeaux + pw.Table( + columnWidths: { + 0: const pw.FlexColumnWidth(3.5), + 1: const pw.FlexColumnWidth(1), + 2: const pw.FlexColumnWidth(1.5), + }, + children: [ + pw.TableRow( children: [ - pw.Text('SOUS-TOTAL:', - style: const pw.TextStyle(fontSize: 8)), - pw.Text('${sousTotal.toStringAsFixed(0)} MGA', - style: const pw.TextStyle(fontSize: 8)), + pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), ], - ), - if (totalRemises > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text('REMISES:', - style: pw.TextStyle( - fontSize: 8, color: PdfColors.orange)), - pw.Text('-${totalRemises.toStringAsFixed(0)} MGA', - style: pw.TextStyle( - fontSize: 8, color: PdfColors.orange)), - ], - ), - ], - if (totalCadeaux > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text('CADEAUX ($nombreCadeaux):', - style: pw.TextStyle( - fontSize: 8, color: PdfColors.green700)), - pw.Text('-${totalCadeaux.toStringAsFixed(0)} MGA', - style: pw.TextStyle( - fontSize: 8, color: PdfColors.green700)), - ], + decoration: const pw.BoxDecoration( + border: pw.Border(bottom: pw.BorderSide(width: 0.5)), ), - ], - pw.Divider(thickness: 0.3), - ], - - // Total final - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text('TOTAL:', - style: pw.TextStyle( - fontSize: 9, fontWeight: pw.FontWeight.bold)), - pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', - style: pw.TextStyle( - fontSize: 9, fontWeight: pw.FontWeight.bold)), - ], - ), - - if (totalRemises > 0 || totalCadeaux > 0) ...[ - pw.SizedBox(height: 4), - pw.Text( - 'Économies: ${(totalRemises + totalCadeaux).toStringAsFixed(0)} MGA !', - style: pw.TextStyle( - fontSize: 7, - color: PdfColors.green, - fontStyle: pw.FontStyle.italic, - ), - textAlign: pw.TextAlign.center, ), - ], - - pw.Divider(thickness: 0.5), - - // Détails du paiement - pw.Text('MODE DE PAIEMENT:', - style: const pw.TextStyle(fontSize: 8)), - pw.Text( - _getPaymentMethodLabel(payment), - style: - pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), - ), - - if (payment.type == PaymentType.cash && - payment.amountGiven > commande.montantTotal) - pw.Text( - 'Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(0)} MGA', - style: const pw.TextStyle(fontSize: 8)), - - pw.SizedBox(height: 8), - - // Messages de fin avec cadeaux - if (totalCadeaux > 0) ...[ - pw.Container( - padding: const pw.EdgeInsets.all(4), - decoration: pw.BoxDecoration( - color: PdfColors.green50, - borderRadius: pw.BorderRadius.circular(4), - ), - child: pw.Column( + + ...detailsAvecProduits.map((item) { + final detail = item['detail'] as DetailCommande; + final produit = item['produit']; + + return pw.TableRow( + decoration: const pw.BoxDecoration( + border: pw.Border(bottom: pw.BorderSide(width: 0.2))), children: [ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.center, - children: [ - pw.Text( - '🎁', - style: emojifont, - textAlign: pw.TextAlign.center, - ), - pw.Text( - 'Profitez de vos cadeaux !', + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Row( + children: [ + pw.Expanded( + child: pw.Text(detail.produitNom ?? 'Produit', + style: const pw.TextStyle(fontSize: 7)), + ), + if (detail.estCadeau) + pw.Text('🎁', style: emojifont), + ], + ), + if (produit?.reference != null) + pw.Text('Ref: ${produit!.reference}', + style: const pw.TextStyle(fontSize: 6)), + if (produit?.imei != null) + pw.Text('IMEI: ${produit!.imei}', + style: const pw.TextStyle(fontSize: 6)), + if (detail.estCadeau) + pw.Text('CADEAU OFFERT', style: pw.TextStyle( - fontSize: 7, + fontSize: 6, + color: PdfColors.green700, fontWeight: pw.FontWeight.bold, + )), + if (detail.aRemise && !detail.estCadeau) + pw.Text('Remise: ${detail.remiseDescription}', + style: pw.TextStyle(fontSize: 6, color: PdfColors.orange)), + ], + ), + pw.Text(detail.quantite.toString(), + style: const pw.TextStyle(fontSize: 7)), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (detail.estCadeau) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 6, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('GRATUIT', + style: pw.TextStyle( + fontSize: 7, color: PdfColors.green700, - ), - textAlign: pw.TextAlign.center, - ), - pw.Text( - '🎁', - style: emojifont, - textAlign: pw.TextAlign.center, - ), - ]), - pw.Text( - '$nombreCadeaux article(s) offert(s)', - style: pw.TextStyle( - fontSize: 6, - color: PdfColors.green600, - ), - textAlign: pw.TextAlign.center, + fontWeight: pw.FontWeight.bold, + )), + ] else if (detail.aRemise && detail.prixUnitaire != detail.prixFinal / detail.quantite) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 6, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', + style: const pw.TextStyle(fontSize: 7)), + ] else + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: const pw.TextStyle(fontSize: 7)), + ], ), ], - ), + ); + }), + ], + ), + + pw.Divider(thickness: 0.5), + + // Totaux avec remises et cadeaux pour le ticket + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('SOUS-TOTAL:', + style: const pw.TextStyle(fontSize: 8)), + pw.Text('${sousTotal.toStringAsFixed(0)} MGA', + style: const pw.TextStyle(fontSize: 8)), + ], + ), + + if (totalRemises > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('REMISES:', + style: pw.TextStyle(fontSize: 8, color: PdfColors.orange)), + pw.Text('-${totalRemises.toStringAsFixed(0)} MGA', + style: pw.TextStyle(fontSize: 8, color: PdfColors.orange)), + ], ), - pw.SizedBox(height: 6), ], - - pw.Text('Article non échangeable - Garantie selon conditions', - style: const pw.TextStyle(fontSize: 6)), - pw.Text('Ticket à conserver comme justificatif', - style: const pw.TextStyle(fontSize: 6)), - pw.SizedBox(height: 8), - pw.Text('Merci pour votre confiance !', - style: pw.TextStyle( - fontSize: 8, fontStyle: pw.FontStyle.italic)), + + if (totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('CADEAUX ($nombreCadeaux):', + style: pw.TextStyle(fontSize: 8, color: PdfColors.green700)), + pw.Text('-${totalCadeaux.toStringAsFixed(0)} MGA', + style: pw.TextStyle(fontSize: 8, color: PdfColors.green700)), + ], + ), + ], + + pw.Divider(thickness: 0.3), ], - ); - }, - ), - ); + + // Total final + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('TOTAL:', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + ], + ), + + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.SizedBox(height: 4), + pw.Text('Économies: ${(totalRemises + totalCadeaux).toStringAsFixed(0)} MGA !', + style: pw.TextStyle( + fontSize: 7, + color: PdfColors.green, + fontStyle: pw.FontStyle.italic, + ), + textAlign: pw.TextAlign.center, + ), + ], + + pw.Divider(thickness: 0.5), + + // Détails du paiement + pw.Text('MODE DE PAIEMENT:', + style: const pw.TextStyle(fontSize: 8)), + pw.Text( + _getPaymentMethodLabel(payment), + style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), +), + + if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal) + pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(0)} MGA', + style: const pw.TextStyle(fontSize: 8)), + + pw.SizedBox(height: 8), + + // Messages de fin avec cadeaux + if (totalCadeaux > 0) ...[ + pw.Container( + padding: const pw.EdgeInsets.all(4), + decoration: pw.BoxDecoration( + color: PdfColors.green50, + borderRadius: pw.BorderRadius.circular(4), + ), + child: pw.Column( + children: [ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + + pw.Text('🎁', + style: emojifont, + textAlign: pw.TextAlign.center, + ), + pw.Text('Profitez de vos cadeaux !', + style: pw.TextStyle( + fontSize: 7, + fontWeight: pw.FontWeight.bold, + color: PdfColors.green700, + ), + textAlign: pw.TextAlign.center, + ), + pw.Text('🎁', + style: emojifont, + textAlign: pw.TextAlign.center, + ), + ] + ), + pw.Text('$nombreCadeaux article(s) offert(s)', + style: pw.TextStyle( + fontSize: 6, + color: PdfColors.green600, + ), + textAlign: pw.TextAlign.center, + ), + ], + ), + ), + pw.SizedBox(height: 6), + ], + + pw.Text('Article non échangeable - Garantie selon conditions', + style: const pw.TextStyle(fontSize: 6)), + pw.Text('Ticket à conserver comme justificatif', + style: const pw.TextStyle(fontSize: 6)), + pw.SizedBox(height: 8), + pw.Text('Merci pour votre confiance !', + style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)), + ], + ); + }, + ), + ); - final output = await getTemporaryDirectory(); - final file = File('${output.path}/ticket_${commande.id}.pdf'); - await file.writeAsBytes(await pdf.save()); - await OpenFile.open(file.path); - } + final output = await getTemporaryDirectory(); + final file = File('${output.path}/ticket_${commande.id}.pdf'); + await file.writeAsBytes(await pdf.save()); + await OpenFile.open(file.path); +} Color _getStatutColor(StatutCommande statut) { switch (statut) { @@ -2637,317 +2328,297 @@ class _GestionCommandesPageState extends State { // Liste des commandes Expanded( - child: _filteredCommandes.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inbox, - size: 64, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'Aucune commande trouvée', - style: TextStyle( - fontSize: 18, - color: Colors.grey.shade600, - fontWeight: FontWeight.w500, - ), + child: _filteredCommandes.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Aucune commande trouvée', + style: TextStyle( + fontSize: 18, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 8), - Text( - 'Essayez de modifier vos filtres', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade500, - ), + ), + const SizedBox(height: 8), + Text( + 'Essayez de modifier vos filtres', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, ), - ], + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _filteredCommandes.length, + itemBuilder: (context, index) { + final commande = _filteredCommandes[index]; + + return FutureBuilder>( + future: _database.getDetailsCommande(commande.id!), + builder: (context, snapshot) { + double totalRemises = 0; + bool aDesRemises = false; + + if (snapshot.hasData) { + for (final detail in snapshot.data!) { + totalRemises += detail.montantRemise; + if (detail.aRemise) aDesRemises = true; + } + } + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: _getStatutColor(commande.statut), + borderRadius: BorderRadius.circular(12), + border: aDesRemises + ? Border.all(color: Colors.orange.shade300, width: 2) + : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + leading: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + border: aDesRemises + ? Border.all(color: Colors.orange.shade300, width: 2) + : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + aDesRemises ? Icons.discount : _getStatutIcon(commande.statut), + size: 20, + color: aDesRemises + ? Colors.teal.shade700 + : commande.statut == StatutCommande.annulee + ? Colors.red + : Colors.blue.shade600, + ), + Text( + '#${commande.id}', + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + title: Text( + commande.clientNomComplet, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + DateFormat('dd/MM/yyyy').format(commande.dateCommande), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: _filteredCommandes.length, - itemBuilder: (context, index) { - final commande = _filteredCommandes[index]; - - return FutureBuilder>( - future: _database.getDetailsCommande(commande.id!), - builder: (context, snapshot) { - double totalRemises = 0; - bool aDesRemises = false; - - if (snapshot.hasData) { - for (final detail in snapshot.data!) { - totalRemises += detail.montantRemise; - if (detail.aRemise) aDesRemises = true; - } - } - - return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: _getStatutColor(commande.statut), - borderRadius: BorderRadius.circular(12), - border: aDesRemises - ? Border.all( - color: Colors.orange.shade300, width: 2) - : null, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: ExpansionTile( - tilePadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - leading: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(25), - border: aDesRemises - ? Border.all( - color: Colors.orange.shade300, - width: 2) - : null, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - aDesRemises - ? Icons.discount - : _getStatutIcon(commande.statut), - size: 20, - color: aDesRemises - ? Colors.teal.shade700 - : commande.statut == - StatutCommande.annulee - ? Colors.red - : Colors.blue.shade600, - ), - Text( - '#${commande.id}', - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - title: Text( - commande.clientNomComplet, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.calendar_today, - size: 14, - color: Colors.grey.shade600, - ), - const SizedBox(width: 4), - Text( - DateFormat('dd/MM/yyyy') - .format(commande.dateCommande), - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - const SizedBox(width: 16), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(12), - ), - child: Text( - commande.statutLibelle, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: commande.statut == - StatutCommande.annulee - ? Colors.red - : Colors.blue.shade700, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.attach_money, - size: 14, - color: Colors.green.shade600, - ), - const SizedBox(width: 4), - Text( - '${commande.montantTotal.toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.green.shade700, - ), - ), - // Affichage des remises si elles existent - if (totalRemises > 0) ...[ - const SizedBox(width: 12), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.orange.shade100, - borderRadius: - BorderRadius.circular(10), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.discount, - size: 12, - color: Colors.teal.shade700, - ), - const SizedBox(width: 2), - Text( - '-${totalRemises.toStringAsFixed(0)}', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.teal.shade700, - ), - ), - ], - ), - ), - ], - ], - ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: - Colors.black.withOpacity(0.1), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: IconButton( - icon: Icon( - Icons.receipt_outlined, - color: Colors.blue.shade600, - ), - onPressed: () => - _generateBon_lifraisonWithPasswordVerification( - commande), - tooltip: 'Générer le Bon de livraison', - ), - ), - if (verifAdmin()) ...[ - const SizedBox(width: 10), - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: - Colors.black.withOpacity(0.1), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: IconButton( - icon: Icon( - Icons.receipt_long, - color: Colors.blue.shade600, - ), - onPressed: () => - _generateInvoiceWithPasswordVerification( - commande), - tooltip: 'Générer la facture', - ), - ), - ], - ], - ), - children: [ - Container( - padding: const EdgeInsets.all(16.0), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(12), - bottomRight: Radius.circular(12), - ), - ), - child: Column( - children: [ - CommandeDetails(commande: commande), - const SizedBox(height: 16), - if (commande.statut != - StatutCommande.annulee) - CommandeActions( - commande: commande, - onStatutChanged: _updateStatut, - onPaymentSelected: - _showPaymentOptions, - ), - ], - ), - ), - ], + ), + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + commande.statutLibelle, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: commande.statut == StatutCommande.annulee + ? Colors.red + : Colors.blue.shade700, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.attach_money, + size: 14, + color: Colors.green.shade600, + ), + const SizedBox(width: 4), + Text( + '${commande.montantTotal.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + // Affichage des remises si elles existent + if (totalRemises > 0) ...[ + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.discount, + size: 12, + color: Colors.teal.shade700, + ), + const SizedBox(width: 2), + Text( + '-${totalRemises.toStringAsFixed(0)}', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.teal.shade700, ), - ); - }, - ); - }, - )), + ), + ], + ), + ), + ], + ], + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: IconButton( + icon: Icon( + Icons.receipt_outlined, + color: Colors.blue.shade600, + ), + onPressed: () => _generateBon_lifraisonWithPasswordVerification(commande), + tooltip: 'Générer le Bon de livraison', + ), + ), + const SizedBox(width: 10,), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: IconButton( + icon: Icon( + Icons.receipt_long, + color: Colors.blue.shade600, + ), + onPressed: () => _generateInvoiceWithPasswordVerification(commande), + tooltip: 'Générer la facture', + ), + ), + ], + ), + children: [ + Container( + padding: const EdgeInsets.all(16.0), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Column( + children: [ + CommandeDetails(commande: commande), + const SizedBox(height: 16), + if (commande.statut != StatutCommande.annulee) + CommandeActions( + commande: commande, + onStatutChanged: _updateStatut, + onPaymentSelected: _showPaymentOptions, + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, +) + ), ], ), ); diff --git a/lib/Views/newCommand.dart b/lib/Views/newCommand.dart index db5429c..83e1e34 100644 --- a/lib/Views/newCommand.dart +++ b/lib/Views/newCommand.dart @@ -608,99 +608,291 @@ void _modifierQuantite(int productId, int nouvelleQuantite) { } void _showProductFoundAndAddedDialog(Product product, int newQuantity) { - Get.dialog( - AlertDialog( - title: Row( + final isProduitCommandable = _isProduitCommandable(product); + final canRequestTransfer = product.stock != null && product.stock! >= 1; + + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ + // Header avec icône de succès Container( - padding: const EdgeInsets.all(8), + width: double.infinity, + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.green.shade200), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.check_circle, + color: Colors.green.shade700, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Produit identifié !', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + 'Ajouté au panier avec succès', + style: TextStyle( + fontSize: 12, + color: Colors.green.shade700, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], ), - child: Icon(Icons.check_circle, color: Colors.green.shade700), ), - const SizedBox(width: 12), - const Expanded(child: Text('Produit identifié et ajouté !')), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + + const SizedBox(height: 20), + + // Informations du produit + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 12), + + // Détails du produit en grille + _buildProductDetailRow('Prix', '${product.price.toStringAsFixed(2)} MGA'), + _buildProductDetailRow('Quantité ajoutée', '$newQuantity'), + + if (product.imei != null && product.imei!.isNotEmpty) + _buildProductDetailRow('IMEI', product.imei!), + + if (product.reference != null && product.reference!.isNotEmpty) + _buildProductDetailRow('Référence', product.reference!), + + if (product.stock != null) + _buildProductDetailRow('Stock restant', '${product.stock! - newQuantity}'), + ], ), ), - const SizedBox(height: 8), - if (product.imei != null && product.imei!.isNotEmpty) - Text('IMEI: ${product.imei}'), - if (product.reference != null && product.reference!.isNotEmpty) - Text('Référence: ${product.reference}'), - Text('Prix: ${product.price.toStringAsFixed(2)} MGA'), - Text('Quantité dans le panier: $newQuantity'), - if (product.stock != null) - Text('Stock restant: ${product.stock! - newQuantity}'), - const SizedBox(height: 12), + + const SizedBox(height: 20), + + // Badge identification automatique Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Colors.green.shade50, - borderRadius: BorderRadius.circular(8), + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.blue.shade200), ), child: Row( + mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.auto_awesome, - color: Colors.green.shade700, size: 16), - const SizedBox(width: 8), - const Expanded( - child: Text( - 'Produit identifié automatiquement', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - ), + color: Colors.blue.shade700, size: 16), + const SizedBox(width: 6), + Text( + 'Identifié automatiquement', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, ), ), ], ), ), + + const SizedBox(height: 24), + + // Boutons d'action redessinés + Column( + children: [ + // Bouton principal selon les permissions + SizedBox( + width: double.infinity, + height: 48, + child: (!isProduitCommandable && !_isUserSuperAdmin()) + ? ElevatedButton.icon( + onPressed: canRequestTransfer + ? () { + Get.back(); + _showDemandeTransfertDialog(product); + } + : () { + Get.snackbar( + 'Stock insuffisant', + 'Impossible de demander un transfert : produit en rupture de stock', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + margin: const EdgeInsets.all(16), + ); + }, + icon: const Icon(Icons.swap_horiz, size: 20), + label: const Text( + 'Demander un transfert', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + style: ElevatedButton.styleFrom( + backgroundColor: canRequestTransfer + ? Colors.orange.shade600 + : Colors.grey.shade400, + foregroundColor: Colors.white, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ) + : ElevatedButton.icon( + onPressed: () { + _ajouterAuPanier(product, 1); + Get.back(); + _showCartBottomSheet(); + }, + icon: const Icon(Icons.shopping_cart, size: 20), + label: const Text( + 'Voir le panier', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + + const SizedBox(height: 12), + + // Boutons secondaires + Row( + children: [ + // Continuer + Expanded( + child: SizedBox( + height: 44, + child: OutlinedButton.icon( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, size: 18), + label: const Text('Continuer'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.grey.shade700, + side: BorderSide(color: Colors.grey.shade300), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ), + + const SizedBox(width: 12), + + // Scanner encore + Expanded( + child: SizedBox( + height: 44, + child: ElevatedButton.icon( + onPressed: () { + Get.back(); + _startAutomaticScanning(); + }, + icon: const Icon(Icons.qr_code_scanner, size: 18), + label: const Text('Scanner'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ), + ], + ), + ], + ), ], ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Continuer'), - ), - ElevatedButton( - onPressed: () { - Get.back(); - _showCartBottomSheet(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, + ), + ), + ); +} + +// Widget helper pour les détails du produit +Widget _buildProductDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 100, + child: Text( + '$label:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, ), - child: const Text('Voir le panier'), ), - ElevatedButton( - onPressed: () { - Get.back(); - _startAutomaticScanning(); // Scanner un autre produit - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, + ), + Expanded( + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black87, ), - child: const Text('Scanner encore'), ), - ], - ), - ); - } + ), + ], + ), + ); +} void _showProductNotFoundDialog(String scannedData) { Get.dialog( @@ -1899,9 +2091,9 @@ Widget _buildUserPointDeVenteInfo() { // 6. Ajoutez cette méthode pour filtrer les produits par point de vente // 🎯 MODIFIÉ: Dropdown avec gestion améliorée Widget _buildPointDeVenteFilter() { - if (!_isUserSuperAdmin()) { - return const SizedBox.shrink(); // Cacher pour les non-admins - } + // if (!_isUserSuperAdmin()) { + // return const SizedBox.shrink(); // Cacher pour les non-admins + // } return Card( elevation: 2, @@ -2630,7 +2822,7 @@ Widget _buildProductListItem(Product product, int quantity, bool isMobile) { const SizedBox(height: 4), ElevatedButton.icon( icon: const Icon(Icons.swap_horiz, size: 14), - label: const Text('Demander transfert'), + label:!isMobile ? const Text('Demander transfertt'):const SizedBox.shrink(), style: ElevatedButton.styleFrom( backgroundColor: (product.stock != null && product.stock! >= 1) ? Colors.blue.shade700 diff --git a/lib/config/DatabaseConfig.dart b/lib/config/DatabaseConfig.dart index f0b7124..2f53f82 100644 --- a/lib/config/DatabaseConfig.dart +++ b/lib/config/DatabaseConfig.dart @@ -1,6 +1,8 @@ // Config/database_config.dart - Version améliorée class DatabaseConfig { - static const String host = 'localhost'; +// static const String host = '10.0.2.2'; + //static const String host = '172.20.10.5'; + static const String host = 'localhost'; static const int port = 3306; static const String username = 'root'; static const String? password = null; @@ -17,7 +19,7 @@ class DatabaseConfig { static const int maxConnections = 10; static const int minConnections = 2; - static bool get isDevelopment => true; + static bool get isDevelopment => false; static Map getConfig() { if (isDevelopment) {