From 55e896775d07cd77f77f8c61af7be5a8becf5a20 Mon Sep 17 00:00:00 2001 From: "b.razafimandimbihery" Date: Sat, 28 Jun 2025 23:28:18 +0300 Subject: [PATCH] changement au nivaue de scan --- lib/Services/stock_managementDatabase.dart | 18 +- lib/Views/DemandeTransfert.dart | 6 +- lib/Views/HandleProduct.dart | 536 +++++++++++++- lib/Views/commandManagement.dart | 779 +++++++++++---------- lib/Views/newCommand.dart | 338 +++++++-- lib/config/DatabaseConfig.dart | 4 +- 6 files changed, 1203 insertions(+), 478 deletions(-) diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index b0821a9..6c4cff0 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -2579,6 +2579,22 @@ Future 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 1663326..9949c66 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -251,6 +251,7 @@ Future buildIconGift() async { } // Bon de livraison============================================== + Future _generateBonLivraison(Commande commande) async { final details = await _database.getDetailsCommande(commande.id!); final client = await _database.getClientById(commande.clientId); @@ -297,23 +298,25 @@ Future _generateBonLivraison(Commande commande) async { 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); + // 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 - pw.Widget buildExemplaire(String typeExemplaire, {bool isSecond = false}) { + // 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), + border: pw.Border.all(color: PdfColors.black, width: 1.5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, @@ -321,7 +324,7 @@ Future _generateBonLivraison(Commande commande) async { // En-tête avec indication de l'exemplaire pw.Container( width: double.infinity, - padding: const pw.EdgeInsets.all(3), + padding: const pw.EdgeInsets.all(5), decoration: pw.BoxDecoration( color: typeExemplaire == "CLIENT" ? PdfColors.blue100 : PdfColors.green100, ), @@ -329,7 +332,7 @@ Future _generateBonLivraison(Commande commande) async { child: pw.Text( 'BON DE LIVRAISON - EXEMPLAIRE $typeExemplaire', style: pw.TextStyle( - fontSize: 8, + fontSize: 14, fontWeight: pw.FontWeight.bold, color: typeExemplaire == "CLIENT" ? PdfColors.blue800 : PdfColors.green800, ), @@ -337,383 +340,395 @@ Future _generateBonLivraison(Commande commande) async { ), ), - pw.Padding( - padding: const pw.EdgeInsets.all(6), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // En-tête principal - pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - // Logo et infos entreprise - très compact - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Container( - width: 45, - height: 45, - child: pw.Image(image), - ), - pw.Text('NOTRE COMPETENCE, A VOTRE SERVICE', style: italicLogoStyle), - pw.SizedBox(height: 3), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text('📍 REMAX Andravoangy', style: tinyTextStyle), - pw.Text('📍 SUPREME CENTER Behoririka', 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: 1026/GC78-20-02-22', style: pw.TextStyle(fontSize: 5, 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.Container( - padding: const pw.EdgeInsets.all(3), - decoration: pw.BoxDecoration( - border: pw.Border.all(color: PdfColors.black), + pw.Expanded( + child: pw.Padding( + padding: const pw.EdgeInsets.all(8), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête principal + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + // Logo et infos entreprise + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Container( + width: 100, + height: 100, + child: pw.Image(image), ), - child: pw.Column( + 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('Boutique:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTextStyle), - pw.SizedBox(height: 1), - pw.Text('Bon N°:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTextStyle), + 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: 2), + pw.Text('NIF: 1026/GC78-20-02-22', + style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), ], ), - ), - ], - ), - - // Informations client - compact - pw.Container( - width: 100, - decoration: pw.BoxDecoration( - border: pw.Border.all(color: PdfColors.black, width: 1), - ), - padding: const pw.EdgeInsets.all(4), - 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: 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, + // Informations centrales + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, 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)), - ), - ], - ), - 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(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, - ), - textAlign: pw.TextAlign.center, + 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(6), + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black), ), - ), - 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.Text('Boutique:', style: frameTextStyle), + 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), ], ), ), ], - ); - }).toList(), - ], - ), - - pw.SizedBox(height: 4), - - // Section finale - très compacte - pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // Totaux - pw.Expanded( - flex: 2, - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - if (totalRemises > 0 || totalCadeaux > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Text('SOUS-TOTAL:', style: smallTextStyle), - pw.SizedBox(width: 8), - pw.Text('${sousTotal.toStringAsFixed(0)}', style: smallTextStyle), - ], - ), - pw.SizedBox(height: 1), + ), + + // Informations client + pw.Container( + width: 120, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1), + ), + padding: const pw.EdgeInsets.all(6), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text('CLIENT', style: frameTextStyle), + 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), ], - - 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.SizedBox(height: 1), + ), + ), + ], + ), + + 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(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']; - if (totalCadeaux > 0) ...[ + 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 (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(3), + child: pw.Text('${detail.quantite}', style: normalTextStyle, 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.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), + ], + ), + ), + // 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(), + ], + ), + ), + + pw.SizedBox(height: 8), + + // Section finale (ajustée pour le mode paysage) + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Totaux + pw.Expanded( + flex: 2, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('SOUS-TOTAL:', style: smallTextStyle), + pw.SizedBox(width: 10), + pw.Text('${sousTotal.toStringAsFixed(0)}', style: smallTextStyle), + ], + ), + pw.SizedBox(height: 2), + ], + + if (totalRemises > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + 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: 2), + ], + + if (totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + 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: 2), + ], + + 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('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('TOTAL:', style: boldTextStyle), + pw.SizedBox(width: 10), + pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', style: boldTextStyle), ], ), - pw.SizedBox(height: 1), - ], - - pw.Container(width: 100, height: 1, color: PdfColors.black, margin: const pw.EdgeInsets.symmetric(vertical: 1)), - - 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), + + if (totalCadeaux > 0) ...[ + pw.SizedBox(height: 3), + pw.Container( + padding: const pw.EdgeInsets.all(3), + decoration: pw.BoxDecoration( + color: PdfColors.green50, + borderRadius: pw.BorderRadius.circular(3), + ), + child: pw.Text( + '🎁 $nombreCadeaux cadeau(s) offert(s) (${totalCadeaux.toStringAsFixed(0)} MGA)', + style: pw.TextStyle(fontSize: 9, color: PdfColors.green700), + ), + ), ], - ), - - if (totalCadeaux > 0) ...[ - pw.SizedBox(height: 3), + ], + ), + ), + + pw.SizedBox(width: 15), + + // Informations vendeurs et signatures + pw.Expanded( + flex: 3, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Vendeurs pw.Container( - padding: const pw.EdgeInsets.all(3), + padding: const pw.EdgeInsets.all(4), decoration: pw.BoxDecoration( - color: PdfColors.green50, + color: PdfColors.grey100, borderRadius: pw.BorderRadius.circular(3), ), - child: pw.Text( - '🎁 $nombreCadeaux cadeau(s) offert(s) (${totalCadeaux.toStringAsFixed(0)} MGA)', - style: pw.TextStyle(fontSize: 5, color: PdfColors.green700), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + 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, + children: [ + pw.Text('Initiateur:', style: tinyTextStyle), + pw.Text( + commandeur != null ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() : 'N/A', + style: pw.TextStyle(fontSize: 9), + ), + ], + ), + ), + pw.Expanded( + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('Validateur:', style: tinyTextStyle), + pw.Text( + validateur != null ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() : 'N/A', + style: pw.TextStyle(fontSize: 9), + ), + ], + ), + ), + ], + ), + ], ), ), - ], - ], - ), - ), - - pw.SizedBox(width: 10), - - // Informations vendeurs et signatures - pw.Expanded( - flex: 3, - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // Vendeurs - pw.Container( - padding: const pw.EdgeInsets.all(3), - decoration: pw.BoxDecoration( - color: PdfColors.grey100, - borderRadius: pw.BorderRadius.circular(3), - ), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, + + pw.SizedBox(height: 8), + + // Signatures + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - pw.Text('VENDEURS', style: pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold)), - pw.SizedBox(height: 1), - pw.Row( + pw.Column( children: [ - pw.Expanded( - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text('Initiateur:', style: tinyTextStyle), - pw.Text( - commandeur != null ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() : 'N/A', - style: pw.TextStyle(fontSize: 5), - ), - ], - ), - ), - pw.Expanded( - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text('Validateur:', style: tinyTextStyle), - pw.Text( - validateur != null ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() : 'N/A', - style: pw.TextStyle(fontSize: 5), - ), - ], - ), - ), + 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: 9, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 15), + pw.Container(width: 70, height: 1, color: PdfColors.black), ], ), ], ), - ), - - pw.SizedBox(height: 6), - - // Signatures - pw.Row( - 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.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.SizedBox(height: 3), - - // Note finale - pw.Text( - 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', - style: italicTextStyle, - ), - ], + ], + ), + + pw.SizedBox(height: 4), + + // Note finale + pw.Text( + 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', + style: italicTextStyle, + ), + ], + ), ), ), ], @@ -721,42 +736,45 @@ Future _generateBonLivraison(Commande commande) async { ); } + // PAGE EN MODE PAYSAGE : Les deux exemplaires sur une seule page pdf.addPage( pw.Page( - pageFormat: PdfPageFormat.a4.landscape, - margin: const pw.EdgeInsets.all(10), + pageFormat: PdfPageFormat.a4.landscape, // Mode paysage + margin: const pw.EdgeInsets.all(12), build: (pw.Context context) { - return pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, + return pw.Row( // Utilisation de Row au lieu de Column pour placer côte à côte children: [ - // Exemplaire CLIENT (à gauche) + // Premier exemplaire (CLIENT) pw.Expanded( child: buildExemplaire("CLIENT"), ), - pw.SizedBox(width: 10), + pw.SizedBox(width: 15), - // Ligne de séparation verticale avec ciseaux - pw.Column( - mainAxisAlignment: pw.MainAxisAlignment.center, - children: [ - 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, - ), - ], + // 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: 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), + pw.SizedBox(width: 15), - // Exemplaire MAGASIN (à droite) + // Deuxième exemplaire (MAGASIN) pw.Expanded( - child: buildExemplaire("MAGASIN", isSecond: true), + child: buildExemplaire("MAGASIN"), ), ], ); @@ -851,7 +869,7 @@ Future _generateInvoice(Commande commande) async { crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Container( - width: 120, + width: 200, height: 120, child: pw.Image(image), ), @@ -874,6 +892,7 @@ Future _generateInvoice(Commande commande) async { 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), ], ), @@ -946,7 +965,7 @@ Future _generateInvoice(Commande commande) async { 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.Text('${client?.nom} \n ${client?.prenom}', style: boldTextStyle), pw.SizedBox(height: 4), pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle), ], 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..c7ba0c0 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;