diff --git a/lib/Components/commandManagementComponents/CommandDetails.dart b/lib/Components/commandManagementComponents/CommandDetails.dart index 6633fd2..55afedf 100644 --- a/lib/Components/commandManagementComponents/CommandDetails.dart +++ b/lib/Components/commandManagementComponents/CommandDetails.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; +import 'package:intl/intl.dart'; class CommandeDetails extends StatelessWidget { final Commande commande; @@ -54,7 +55,7 @@ class CommandeDetails extends StatelessWidget { ), const SizedBox(height: 2), Text( - '${(detail.prixFinal / detail.quantite).toStringAsFixed(2)} MGA', + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal / detail.quantite)} MGA', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, @@ -65,7 +66,7 @@ class CommandeDetails extends StatelessWidget { ), ); } else { - return _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA', isAmount: true); + return _buildTableCell('${NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire)} MGA', isAmount: true); } } @@ -85,7 +86,7 @@ class CommandeDetails extends StatelessWidget { textAlign: TextAlign.center, ), Text( - '-${detail.montantRemise.toStringAsFixed(0)} MGA', + '-${NumberFormat('#,##0', 'fr_FR').format(detail.montantRemise)} MGA', style: TextStyle( fontSize: 10, color: Colors.teal.shade700, @@ -119,7 +120,7 @@ class CommandeDetails extends StatelessWidget { ), const SizedBox(height: 2), Text( - '${detail.prixFinal.toStringAsFixed(2)} MGA', + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)} MGA', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, @@ -129,7 +130,7 @@ class CommandeDetails extends StatelessWidget { ), ); } else { - return _buildTableCell('${detail.prixFinal.toStringAsFixed(2)} MGA', isAmount: true); + return _buildTableCell('${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)} MGA', isAmount: true); } } @@ -197,7 +198,7 @@ class CommandeDetails extends StatelessWidget { borderRadius: BorderRadius.circular(12), ), child: Text( - 'Économies: ${totalRemises.toStringAsFixed(0)} MGA', + 'Économies: ${NumberFormat('#,##0', 'fr_FR').format(totalRemises)} MGA', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, @@ -312,7 +313,7 @@ class CommandeDetails extends StatelessWidget { ), ), Text( - '${sousTotal.toStringAsFixed(2)} MGA', + '${NumberFormat('#,##0', 'fr_FR').format(sousTotal)} MGA', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -343,7 +344,7 @@ class CommandeDetails extends StatelessWidget { ], ), Text( - '-${totalRemises.toStringAsFixed(2)} MGA', + '-${NumberFormat('#,##0', 'fr_FR').format(totalRemises)} MGA', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -367,7 +368,7 @@ class CommandeDetails extends StatelessWidget { ), ), Text( - '${commande.montantTotal.toStringAsFixed(2)} MGA', + '${NumberFormat('#,##0', 'fr_FR').format(commande.montantTotal)} MGA', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18, diff --git a/lib/Components/commandManagementComponents/CommandeActions.dart b/lib/Components/commandManagementComponents/CommandeActions.dart index 04241c1..47566b2 100644 --- a/lib/Components/commandManagementComponents/CommandeActions.dart +++ b/lib/Components/commandManagementComponents/CommandeActions.dart @@ -7,13 +7,13 @@ import 'package:youmazgestion/Models/client.dart'; class CommandeActions extends StatelessWidget { final Commande commande; final Function(int, StatutCommande) onStatutChanged; - final Function(Commande) onPaymentSelected; + final Function(Commande) onGenerateBonLivraison; const CommandeActions({ required this.commande, required this.onStatutChanged, - required this.onPaymentSelected, + required this.onGenerateBonLivraison, }); @@ -30,7 +30,7 @@ class CommandeActions extends StatelessWidget { label: 'Confirmer', icon: Icons.check_circle, color: Colors.blue, - onPressed: () => onPaymentSelected(commande), + onPressed: () => onGenerateBonLivraison(commande), ), _buildActionButton( label: 'Annuler', diff --git a/lib/Components/commandManagementComponents/DiscountDialog.dart b/lib/Components/commandManagementComponents/DiscountDialog.dart index 12b4b1e..9974f8a 100644 --- a/lib/Components/commandManagementComponents/DiscountDialog.dart +++ b/lib/Components/commandManagementComponents/DiscountDialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:youmazgestion/Models/client.dart'; @@ -48,7 +49,7 @@ class _DiscountDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Montant original: ${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), + Text('Montant original: ${NumberFormat('#,##0', 'fr_FR').format(widget.commande.montantTotal)} MGA'), const SizedBox(height: 16), // Choix du type de remise @@ -124,7 +125,7 @@ class _DiscountDialogState extends State { children: [ const Text('Montant final:'), Text( - '${_montantFinal.toStringAsFixed(2)} MGA', + '${NumberFormat('#,##0', 'fr_FR').format(_montantFinal)} MGA', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -138,7 +139,7 @@ class _DiscountDialogState extends State { children: [ const Text('Économie:'), Text( - '${(widget.commande.montantTotal - _montantFinal).toStringAsFixed(2)} MGA', + '${NumberFormat('#,##0', 'fr_FR').format(widget.commande.montantTotal - _montantFinal)} MGA', style: const TextStyle( color: Colors.green, fontWeight: FontWeight.bold, diff --git a/lib/Components/commandManagementComponents/PaymentMethodDialog.dart b/lib/Components/commandManagementComponents/PaymentMethodDialog.dart index 875ad01..0887f5b 100644 --- a/lib/Components/commandManagementComponents/PaymentMethodDialog.dart +++ b/lib/Components/commandManagementComponents/PaymentMethodDialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/src/snackbar/snackbar.dart'; +import 'package:intl/intl.dart'; import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart'; import 'package:youmazgestion/Components/paymentType.dart'; import 'package:youmazgestion/Models/client.dart'; @@ -81,7 +82,7 @@ class _PaymentMethodDialogState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)), - Text('${montantFinal.toStringAsFixed(2)} MGA', + Text('${NumberFormat('#,##0', 'fr_FR').format(montantFinal)} MGA', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ], ), @@ -162,7 +163,7 @@ class _PaymentMethodDialogState extends State { ), const SizedBox(height: 8), Text( - 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', + 'Monnaie à rendre: ${NumberFormat('#,##0', 'fr_FR').format(change)} MGA', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, diff --git a/lib/Components/newCommandComponents/CadeauDialog.dart b/lib/Components/newCommandComponents/CadeauDialog.dart index 8c2d2f7..400c82c 100644 --- a/lib/Components/newCommandComponents/CadeauDialog.dart +++ b/lib/Components/newCommandComponents/CadeauDialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Models/produit.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; @@ -149,7 +150,7 @@ class _CadeauDialogState extends State { ), ), Text( - 'Prix: ${widget.product.price.toStringAsFixed(2)} MGA', + 'Prix: ${NumberFormat('#,##0', 'fr_FR').format(widget.product.price)} MGA', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, @@ -255,7 +256,7 @@ class _CadeauDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Prix normal: ${produit.price.toStringAsFixed(2)} MGA', + 'Prix normal: ${NumberFormat('#,##0', 'fr_FR').format(produit.price)} MGA', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, diff --git a/lib/Components/newCommandComponents/RemiseDialog.dart b/lib/Components/newCommandComponents/RemiseDialog.dart index e5f81a7..47acef6 100644 --- a/lib/Components/newCommandComponents/RemiseDialog.dart +++ b/lib/Components/newCommandComponents/RemiseDialog.dart @@ -1,6 +1,7 @@ // Components/RemiseDialog.dart import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Models/produit.dart'; @@ -115,11 +116,11 @@ class _RemiseDialogState extends State { style: const TextStyle(fontSize: 12), ), Text( - 'Prix unitaire: ${widget.prixUnitaire.toStringAsFixed(2)} MGA', + 'Prix unitaire: ${NumberFormat('#,##0', 'fr_FR').format(widget.prixUnitaire)} MGA', style: const TextStyle(fontSize: 12), ), Text( - 'Sous-total: ${_sousTotal.toStringAsFixed(2)} MGA', + 'Sous-total: ${NumberFormat('#,##0', 'fr_FR').format(_sousTotal)} MGA', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, @@ -234,7 +235,7 @@ class _RemiseDialogState extends State { children: [ const Text('Sous-total:', style: TextStyle(fontSize: 12)), Text( - '${_sousTotal.toStringAsFixed(2)} MGA', + '${NumberFormat('#,##0', 'fr_FR').format(_sousTotal)} MGA', style: const TextStyle(fontSize: 12), ), ], @@ -252,7 +253,7 @@ class _RemiseDialogState extends State { ), ), Text( - '-${_montantRemise.toStringAsFixed(2)} MGA', + '-${NumberFormat('#,##0', 'fr_FR').format(_montantRemise)} MGA', style: TextStyle( fontSize: 12, color: Colors.orange.shade700, @@ -274,7 +275,7 @@ class _RemiseDialogState extends State { ), ), Text( - '${_prixFinal.toStringAsFixed(2)} MGA', + '${NumberFormat('#,##0', 'fr_FR').format(_prixFinal)} MGA', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index 998cea1..8181810 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:math' as console; +import 'dart:typed_data'; import 'package:get/get.dart'; import 'package:mysql1/mysql1.dart'; import 'package:youmazgestion/controller/userController.dart'; @@ -596,6 +598,7 @@ String _formatDate(DateTime date) { await insertDefaultPointsDeVente(); final newResult = await db.query('SELECT * FROM points_de_vente ORDER BY nom ASC'); + print(newResult); return newResult.map((row) => row.fields).toList(); } @@ -1023,7 +1026,7 @@ Future>> getCommandesParPointDeVente( Future>> getPointsDeVentes() async { final db = await database; - final result = await db.query('SELECT DISTINCT id, nom FROM pointsdevente ORDER BY nom ASC'); + final result = await db.query('SELECT DISTINCT * FROM pointsdevente ORDER BY nom ASC'); return result.map((row) => row.fields).toList(); } @@ -1237,6 +1240,7 @@ Future>> getCommandesParPointDeVente( [newDesignation, id]); return result.affectedRows!; } + // Future deletePointDeVente(int id) async { // final db = await database; @@ -1425,12 +1429,53 @@ Future>> getCommandesParPointDeVente( } } - Future?> getPointDeVenteById(int id) async { - final db = await database; - final result = - await db.query('SELECT * FROM points_de_vente WHERE id = ?', [id]); - return result.isNotEmpty ? result.first.fields : null; + Future getPointDeVenteById(int id) async { + final db = await database; + final result = + await db.query('SELECT * FROM points_de_vente WHERE id = ?', [id]); + return result.isNotEmpty ? result.first : null; +} +List parseHeaderInfo(dynamic blobData) { + if (blobData == null) return []; + + try { + String content = ''; + + print("=== TYPE DE DONNÉES BLOB === ${blobData.runtimeType}"); + + if (blobData is String) { + content = blobData; + } else if (blobData is Uint8List || blobData is List) { + try { + content = utf8.decode(blobData); + } catch (eUtf8) { + print('❌ utf8.decode failed: $eUtf8'); + try { + content = latin1.decode(blobData); + } catch (eLatin1) { + print('❌ latin1.decode failed: $eLatin1'); + content = String.fromCharCodes(blobData); + } + } + } else { + content = blobData.toString(); + } + + print('=== LIVRAISON BRUTE ===\n$content\n=== FIN ==='); + + return content + .split('\n') + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .toList(); + } catch (e) { + print('❌ Erreur lors du parsing des données d\'en-tête: $e'); + return []; } +} + + + Future getOrCreatePointDeVenteByNom(String nom) async { final db = await database; @@ -3378,6 +3423,47 @@ Future>> getHistoriqueSortiesPersonnelles({ return []; } } +Future updatePointDeVentes( + int id, + String nom, + String code, { + String? content, + String? livraison, + String? facture, + Uint8List? imagePath, +}) async { + final db = await database; + + try { + await db.query( + ''' + UPDATE points_de_vente + SET nom = ?, + content = ?, + livraison = ?, + facture = ?, + logo = ? + WHERE id = ? + ''', + [ + nom, + (content?.isEmpty ?? true) ? null : content, + (livraison?.isEmpty ?? true) ? null : livraison, + (facture?.isEmpty ?? true) ? null : facture, + imagePath, + id, + ], + ); + } catch (e, stacktrace) { + print('Erreur lors de la mise à jour du point de vente : $e'); + print('Stacktrace : $stacktrace'); + rethrow; // si tu veux faire remonter l’erreur plus haut + } +} + + + + Future> getStatistiquesSortiesPersonnelles() async { final db = await database; diff --git a/lib/Views/Dashboard.dart b/lib/Views/Dashboard.dart index 39747c8..d8b0ed7 100644 --- a/lib/Views/Dashboard.dart +++ b/lib/Views/Dashboard.dart @@ -1422,12 +1422,12 @@ Widget _buildTableauVentesPointDeVente(List> ventesData) { ), Expanded( flex: 2, - child: Text( + child:Text( NumberFormat('#,##0.00', 'fr_FR').format( - (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0, - ), - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), - ), + (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0, + ), + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ) ), Expanded( flex: 1, diff --git a/lib/Views/HandleProduct.dart b/lib/Views/HandleProduct.dart index 60f98e4..82cb1a3 100644 --- a/lib/Views/HandleProduct.dart +++ b/lib/Views/HandleProduct.dart @@ -4800,6 +4800,7 @@ Future _showDemandeTransfertDialog(Product product) async { foregroundColor: Colors.white, ), const SizedBox(height: 8), + if (_userController.username == 'superadmin'|| _userController.username == 'admin') ...[ FloatingActionButton( heroTag: 'importBtn', onPressed: _isImporting ? null : _importFromExcel, @@ -4808,6 +4809,7 @@ Future _showDemandeTransfertDialog(Product product) async { backgroundColor: Colors.blue, foregroundColor: Colors.white, ), + ], const SizedBox(height: 8), FloatingActionButton.extended( heroTag: 'addBtn', diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart index 5357ade..ad1776b 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; +import 'package:mysql1/mysql1.dart'; import 'package:numbers_to_letters/numbers_to_letters.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; @@ -276,10 +277,22 @@ class _GestionCommandesPageState extends State { /// Le PDF est sauvegardé dans un fichier temporaire, qui est partagé /// via le mécanisme de partage de fichiers du système. /// - Future _generateBonLivraison(Commande commande) async { +// Dans GestionCommandesPage - Remplacez la méthode _generateBonLivraison complète + +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); + final pointDeVenteId = commande.pointDeVenteId; + ResultRow? pointDeVenteComplet; + + // ✅ MODIFICATION: Récupération complète des données du point de vente + if (pointDeVenteId != null) { + pointDeVenteComplet = await _database.getPointDeVenteById(pointDeVenteId); +} else { + print("ce point de vente n'existe pas"); +} + final pointDeVente = pointDeVenteComplet; + // Récupérer les informations des vendeurs final commandeur = commande.commandeurId != null @@ -289,6 +302,32 @@ class _GestionCommandesPageState extends State { ? await _database.getUserById(commande.validateurId!) : null; + // ✅ NOUVELLE FONCTIONNALITÉ: Parser les informations d'en-tête pour livraison + List infosLivraison = []; + final livraisonBrute = pointDeVenteComplet?['livraison']; + print('=== LIVRAISON BRUTE ==='); + print(livraisonBrute); + print('=== FIN ==='); + + if (livraisonBrute != null) { + infosLivraison = _database.parseHeaderInfo(livraisonBrute); + print('=== INFOS LIVRAISON PARSÉES ==='); + for (int i = 0; i < infosLivraison.length; i++) { + print('Ligne $i: ${infosLivraison[i]}'); + } + print('==============================='); + } + + + // Infos par défaut si aucune info personnalisée + final infosLivraisonDefaut = [ + 'REMAX Andravoangy', + 'SUPREME CENTER Behoririka \n BOX 405 | 416 | 119', + 'Tripolisa analankely BOX 7', + '033 37 808 18', + 'www.guycom.mg', + ]; + // ✅ DEBUG: Vérifiez combien de détails vous avez print('=== DEBUG BON DE LIVRAISON ==='); print('Nombre de détails récupérés: ${details.length}'); @@ -329,16 +368,14 @@ class _GestionCommandesPageState extends State { }); print(' ✅ Produit trouvé: ${produit.name}'); } else { - // ✅ Même si le produit est null, on l'ajoute quand même avec les infos du détail detailsAvecProduits.add({ 'detail': detail, - 'produit': null, // On garde null mais on utilisera les infos du détail + 'produit': null, }); print(' ⚠️ Produit non trouvé, utilisation des données du détail'); } } catch (e) { print(' ❌ Erreur lors de la récupération du produit: $e'); - // En cas d'erreur, on ajoute quand même le détail detailsAvecProduits.add({ 'detail': detail, 'produit': null, @@ -361,10 +398,9 @@ class _GestionCommandesPageState extends State { regularFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Regular.ttf')); } catch (e) { print('⚠️ Impossible de charger les polices personnalisées: $e'); - // Utiliser les polices par défaut } - // ✅ DÉFINITION DES STYLES DE TEXTE - Variables globales dans la fonction + // ✅ DÉFINITION DES STYLES DE TEXTE final tinyTextStyle = pw.TextStyle(fontSize: 9, font: regularFont); final smallTextStyle = pw.TextStyle(fontSize: 10, font: regularFont); final normalTextStyle = pw.TextStyle(fontSize: 11, font: regularFont); @@ -373,11 +409,54 @@ class _GestionCommandesPageState extends State { final frameTextStyle = pw.TextStyle(fontSize: 10, font: regularFont); final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont ?? regularFont); final italicLogoStyle = pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold, font: italicFont ?? regularFont); + Future buildLogoWidget() async { + final logoRaw = pointDeVenteComplet?['logo']; + + if (logoRaw != null) { + try { + Uint8List bytes; + if (logoRaw is Uint8List) { + bytes = logoRaw; + } else if (logoRaw is List) { + bytes = Uint8List.fromList(logoRaw); + } else if (logoRaw.runtimeType.toString() == 'Blob') { + // Cast dynamique pour appeler toBytes() + dynamic blobDynamic = logoRaw; + bytes = blobDynamic.toBytes(); + } else { + throw Exception("Format de logo non supporté: ${logoRaw.runtimeType}"); + } + + final imageLogo = pw.MemoryImage(bytes); + return pw.Image(imageLogo, width: 100, height: 100); + } catch (e) { + print('Erreur chargement logo BDD: $e'); + } + } + return pw.Image(image, width: 100, height: 100); +} + + +final logoWidget = await buildLogoWidget(); + + // ✅ FONCTION POUR CONSTRUIRE L'EN-TÊTE DYNAMIQUE + pw.Widget buildEnteteInfos() { + final infosAUtiliser = infosLivraison.isNotEmpty ? infosLivraison : infosLivraisonDefaut; + + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: infosAUtiliser.map((info) { + return pw.Padding( + padding: const pw.EdgeInsets.only(bottom: 1), + child: pw.Text(info, style: tinyTextStyle), + ); + }).toList(), + ); + } - // ✅ Fonction pour créer un exemplaire - CORRIGÉE + // ✅ Fonction pour créer un exemplaire - AVEC EN-TÊTE DYNAMIQUE pw.Widget buildExemplaire(String typeExemplaire) { return pw.Container( - // ✅ PAS DE HAUTEUR FIXE - Elle s'adapte au contenu width: double.infinity, decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.black, width: 1.5), @@ -415,28 +494,15 @@ class _GestionCommandesPageState extends State { crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - // Logo et infos entreprise + // Logo et infos entreprise - ✅ AVEC INFOS DYNAMIQUES pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Container( - width: 100, - height: 100, - child: pw.Image(image), - ), + logoWidget, 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 \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), - ], - ), + buildEnteteInfos(), // ✅ EN-TÊTE DYNAMIQUE ICI ], ), @@ -964,14 +1030,22 @@ class _GestionCommandesPageState extends State { // 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); + final pointDeVenteId = commande.pointDeVenteId; + ResultRow? pointDeVenteComplet; + + if (pointDeVenteId != null) { + pointDeVenteComplet = await _database.getPointDeVenteById(pointDeVenteId); + } else { + print("ce point de vente n'existe pas"); + } + final pointDeVente = pointDeVenteComplet; // Récupérer les informations des vendeurs final commandeur = commande.commandeurId != null @@ -981,6 +1055,34 @@ class _GestionCommandesPageState extends State { ? await _database.getUserById(commande.validateurId!) : null; + List infosFacture = []; + final factureBrute = pointDeVenteComplet?['facture']; + print('=== FACTURE BRUTE ==='); + print(factureBrute); + print('=== FIN ==='); + + if (factureBrute != null) { + infosFacture = _database.parseHeaderInfo(factureBrute); + print('=== INFOS FACTURE PARSÉES ==='); + for (int i = 0; i < infosFacture.length; i++) { + print('Ligne $i: ${infosFacture[i]}'); + } + print('==============================='); + } + + // Infos par défaut si aucune info personnalisée + final infosFactureDefaut = [ + 'REMAX by GUYCOM Andravoangy', + 'SUPREME CENTER Behoririka box 405', + 'SUPREME CENTER Behoririka box 416', + 'SUPREME CENTER Behoririka box 119', + 'TRIPOLITSA Analakely BOX 7', + '033 37 808 18', + 'www.guycom.mg', + 'NIF: 4000106673 - STAT 95210 11 2017 1 003651', + 'Facebook: GuyCom', + ]; + final iconPhone = await buildIconPhoneText(); final iconChecked = await buildIconCheckedText(); final iconGlobe = await buildIconGlobeText(); @@ -1032,6 +1134,55 @@ class _GestionCommandesPageState extends State { final emojifont = pw.TextStyle( fontSize: 8, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); + Future buildLogoWidget() async { + final logoRaw = pointDeVenteComplet?['logo']; + + if (logoRaw != null) { + try { + Uint8List bytes; + if (logoRaw is Uint8List) { + bytes = logoRaw; + } else if (logoRaw is List) { + bytes = Uint8List.fromList(logoRaw); + } else if (logoRaw.runtimeType.toString() == 'Blob') { + // Cast dynamique pour appeler toBytes() + dynamic blobDynamic = logoRaw; + bytes = blobDynamic.toBytes(); + } else { + throw Exception("Format de logo non supporté: ${logoRaw.runtimeType}"); + } + + final imageLogo = pw.MemoryImage(bytes); + return pw.Container(width: 200, height: 120, child: pw.Image(imageLogo)); + } catch (e) { + print('Erreur chargement logo BDD: $e'); + } + } + return pw.Container(width: 200, height: 100, child: pw.Image(image)); + } + + final logoWidget = await buildLogoWidget(); +pw.Widget buildEnteteFactureInfos() { + final infosAUtiliser = infosFacture.isNotEmpty ? infosFacture : infosFactureDefaut; + + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + ...infosAUtiliser.map((info) { + return pw.Row( + children: [ + iconChecked, + pw.SizedBox(width: 4), + pw.Text(info, style: smallTextStyle), + ], + ); + }), + pw.SizedBox(height: 2), // ajouté en fin de liste + ], + ); +} + + pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, // Mode portrait @@ -1049,71 +1200,11 @@ class _GestionCommandesPageState extends State { pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Container( - width: 200, - height: 120, - child: pw.Image(image), - ), + logoWidget, 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: 4000106673 - STAT 95210 11 2017 1 003651', - style: smallTextStyle) - ]), - pw.Text('Facebook: GuyCom', style: smallTextStyle), + buildEnteteFactureInfos(), ], ), @@ -1906,9 +1997,11 @@ class _GestionCommandesPageState extends State { return 'MÉTHODE INCONNUE (${payment.type.toString()})'; // Debug info } } +// Dans GestionCommandesPage - Remplacez la méthode _generateReceipt complète 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 @@ -2875,6 +2968,30 @@ class _GestionCommandesPageState extends State { 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.payment, + color: Colors.green.shade600, + ), + onPressed: () => _showPaymentOptions(commande), + tooltip: 'Générer le ticket de la commande', + ), + ), + const SizedBox( + width: 10, + ), Container( decoration: BoxDecoration( color: Colors.white, @@ -2950,8 +3067,7 @@ class _GestionCommandesPageState extends State { CommandeActions( commande: commande, onStatutChanged: _updateStatut, - onPaymentSelected: - _showPaymentOptions, + onGenerateBonLivraison:_generateBon_lifraisonWithPasswordVerification ), ], ), diff --git a/lib/Views/gestion_point_de_vente.dart b/lib/Views/gestion_point_de_vente.dart index f69f6ad..08baf7c 100644 --- a/lib/Views/gestion_point_de_vente.dart +++ b/lib/Views/gestion_point_de_vente.dart @@ -1,5 +1,9 @@ +import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; @@ -22,7 +26,9 @@ class _AjoutPointDeVentePageState extends State { // Liste des points de vente List> _pointsDeVente = []; + List> _filteredPointsDeVente = []; final TextEditingController _searchController = TextEditingController(); + @override void initState() { @@ -31,52 +37,61 @@ class _AjoutPointDeVentePageState extends State { _searchController.addListener(_filterPointsDeVente); } -Future _loadPointsDeVente() async { - setState(() { - _isLoading = true; - }); - - try { - final points = await _appDatabase.getPointsDeVente(); - - // Enrichir chaque point de vente avec les informations de contraintes - for (var point in points) { - final verification = await _appDatabase.checkCanDeletePointDeVente(point['id']); - point['canDelete'] = verification['canDelete']; - point['constraintCount'] = (verification['reasons'] as List).length; + Future _loadPointsDeVente() async { + if (mounted) { + setState(() { + _isLoading = true; + }); } - setState(() { - _pointsDeVente = points; - _isLoading = false; - }); - } catch (e) { - setState(() { - _isLoading = false; - }); - Get.snackbar( - 'Erreur', - 'Impossible de charger les points de vente: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); + try { + final points = await _appDatabase.getPointsDeVente(); + + // Enrichir chaque point de vente avec les informations de contraintes + for (var point in points) { + final verification = await _appDatabase.checkCanDeletePointDeVente(point['id']); + point['canDelete'] = verification['canDelete']; + point['constraintCount'] = (verification['reasons'] as List).length; + } + + if (mounted) { + setState(() { + _pointsDeVente = points; + _filteredPointsDeVente = List.from(points); + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + }); + Get.snackbar( + 'Erreur', + 'Impossible de charger les points de vente: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } } -} void _filterPointsDeVente() { - final query = _searchController.text.toLowerCase(); - if (query.isEmpty) { - _loadPointsDeVente(); - return; + final query = _searchController.text.toLowerCase().trim(); + if (mounted) { + setState(() { + if (query.isEmpty) { + _filteredPointsDeVente = List.from(_pointsDeVente); + } else { + _filteredPointsDeVente = _pointsDeVente.where((point) { + final nom = point['nom']?.toString().toLowerCase() ?? ''; + final code = point['code']?.toString().toLowerCase() ?? ''; + return nom.contains(query) || code.contains(query); + }).toList(); + } + }); } - - setState(() { - _pointsDeVente = _pointsDeVente.where((point) { - final nom = point['nom']?.toString().toLowerCase() ?? ''; - return nom.contains(query); - }).toList(); - }); } Future _submitForm() async { @@ -114,244 +129,368 @@ Future _loadPointsDeVente() async { colorText: Colors.white, ); } finally { - setState(() { - _isLoading = false; - }); + if (mounted) { + setState(() { + _isLoading = false; + }); + } } } } -Future _showConstraintDialog(int id, Map verificationResult) async { - final reasons = verificationResult['reasons'] as List; - final suggestions = verificationResult['suggestions'] as List; - - await Get.dialog( - AlertDialog( - title: Row( - children: const [ - Icon(Icons.warning, color: Colors.orange), - SizedBox(width: 8), - Text('Suppression impossible'), - ], - ), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Ce point de vente ne peut pas être supprimé pour les raisons suivantes :', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 12), - ...reasons.map((reason) => Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('• ', style: TextStyle(color: Colors.red)), - Expanded(child: Text(reason)), - ], - ), - )), - if (suggestions.isNotEmpty) ...[ - const SizedBox(height: 16), - const Text( - 'Solutions possibles :', - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue), - ), - const SizedBox(height: 8), - ...suggestions.map((suggestion) => Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('💡 ', style: TextStyle(fontSize: 12)), - Expanded(child: Text(suggestion, style: const TextStyle(fontSize: 13))), - ], - ), - )), - ], - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Fermer'), - ), - if (reasons.any((r) => r.contains('produit'))) - ElevatedButton( - onPressed: () { - Get.back(); - _showTransferDialog(id); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - child: const Text('Transférer les produits'), - ), - ], - ), +Future editPointDeVente(BuildContext context, Map pointDeVente) async { + print('=== DIAGNOSTIC DES DONNÉES ==='); + print('ID: ${pointDeVente['id']}'); + print('Nom: ${pointDeVente['nom']}'); + print('Code: ${pointDeVente['code']}'); + print('Content (ticket): "${pointDeVente['content']}" (Type: ${pointDeVente['content'].runtimeType})'); + print('Livraison: "${pointDeVente['livraison']}" (Type: ${pointDeVente['livraison'].runtimeType})'); + print('Facture: "${pointDeVente['facture']}" (Type: ${pointDeVente['facture'].runtimeType})'); + print('Logo: ${pointDeVente['logo']?.runtimeType}'); + print('Toutes les clés: ${pointDeVente.keys.toList()}'); + print('==============================='); + final _editFormKey = GlobalKey(); + final ImagePicker _picker = ImagePicker(); + + // Fonction helper pour convertir les valeurs en String de manière sûre + String safeStringConversion(dynamic value) { + if (value == null) return ''; + if (value is String) return value; + // Vérifier si c'est un type binaire (Uint8List, List, etc.) + if (value is Uint8List || value is List || value is List) { + // Si c'est des données binaires, on retourne une chaîne vide + // car on ne peut pas les convertir en texte lisible + return ''; + } + // Pour tous les autres types, essayer la conversion toString() + try { + return value.toString(); + } catch (e) { + print('Erreur conversion vers String: $e, type: ${value.runtimeType}'); + return ''; + } + } + + // Initialisation dynamique des contrôleurs avec conversion sécurisée + final _editNomController = TextEditingController( + text: safeStringConversion(pointDeVente['nom']) ); -} -Future _showTransferDialog(int sourcePointDeVenteId) async { - final pointsDeVente = await _appDatabase.getPointsDeVenteForTransfer(sourcePointDeVenteId); - - if (pointsDeVente.isEmpty) { - Get.snackbar( - 'Erreur', - 'Aucun autre point de vente disponible pour le transfert', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - return; + final _editCodeController = TextEditingController( + text: safeStringConversion(pointDeVente['code']) + ); + final _editTicketController = TextEditingController( + text: safeStringConversion(pointDeVente['content']) + ); + final _editLivraisonController = TextEditingController( + text: safeStringConversion(pointDeVente['livraison']) + ); + final _editFactureController = TextEditingController( + text: safeStringConversion(pointDeVente['facture']) + ); + + File? _selectedImage; + Uint8List? _currentImageBlob; + + // Gérer la conversion du logo de manière sécurisée + final logoData = pointDeVente['logo']; + if (logoData != null) { + try { + if (logoData is Uint8List) { + _currentImageBlob = logoData; + } else if (logoData is List) { + _currentImageBlob = Uint8List.fromList(logoData); + } else if (logoData is List) { + // Cas où c'est une List contenant des int + _currentImageBlob = Uint8List.fromList(logoData.cast()); + } else { + // Type non supporté (comme Blob), laisser null et logger + print('Type de logo non supporté: ${logoData.runtimeType}'); + _currentImageBlob = null; + } + } catch (e) { + print('Erreur lors de la conversion du logo: $e'); + _currentImageBlob = null; + } } - - int? selectedPointDeVenteId; - + + bool _isEditLoading = false; + + Future pickImage(ImageSource source) async { + try { + final XFile? image = await _picker.pickImage(source: source); + if (image != null) { + _selectedImage = File(image.path); + _currentImageBlob = await _selectedImage!.readAsBytes(); + } + } catch (e) { + print('Erreur lors de la sélection de l\'image : $e'); + } + } + + Future saveChanges() async { + if (_editFormKey.currentState?.validate() ?? false) { + _isEditLoading = true; + + try { + await _appDatabase.updatePointDeVentes( + pointDeVente['id'], + _editNomController.text.trim(), + _editCodeController.text.trim(), + content: _editTicketController.text.trim(), + livraison: _editLivraisonController.text.trim(), + facture: _editFactureController.text.trim(), + imagePath: _currentImageBlob, + ); + + Get.back(); // Fermer le dialog + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Modification enregistrée avec succès')), + ); + + await _loadPointsDeVente(); // Rafraîchir la liste + } catch (e) { + print('Erreur lors de la sauvegarde : $e'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Erreur lors de la modification')), + ); + } finally { + _isEditLoading = false; + } + } + } + + // Affichage de la boîte de dialogue await Get.dialog( - AlertDialog( - title: const Text('Transférer les produits'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Sélectionnez le point de vente de destination pour les produits :'), - const SizedBox(height: 16), - SizedBox( - width: double.maxFinite, - child: DropdownButtonFormField( - value: selectedPointDeVenteId, - decoration: const InputDecoration( - labelText: 'Point de vente de destination', - border: OutlineInputBorder(), + Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 700), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _editFormKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Modifier le point de vente', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + TextFormField( + controller: _editNomController, + decoration: const InputDecoration(labelText: 'Nom du point de vente'), + validator: (value) => value == null || value.isEmpty ? 'Le nom est requis' : null, + ), + const SizedBox(height: 8), + TextFormField( + controller: _editCodeController, + decoration: const InputDecoration(labelText: 'Code du point de vente'), + ), + const SizedBox(height: 8), + TextFormField( + controller: _editTicketController, + decoration: const InputDecoration(labelText: 'Info ticket'), + maxLines: 3, + ), + const SizedBox(height: 8), + TextFormField( + controller: _editLivraisonController, + decoration: const InputDecoration(labelText: 'Info bon de livraison'), + maxLines: 3, + ), + const SizedBox(height: 8), + TextFormField( + controller: _editFactureController, + decoration: const InputDecoration(labelText: 'Info facture'), + maxLines: 3, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => pickImage(ImageSource.gallery), + icon: const Icon(Icons.image), + label: const Text('Galerie'), + ), + ), + const SizedBox(width: 10), + Expanded( + child: ElevatedButton.icon( + onPressed: () => pickImage(ImageSource.camera), + icon: const Icon(Icons.camera_alt), + label: const Text('Caméra'), + ), + ), + ], + ), + const SizedBox(height: 10), + if (_currentImageBlob != null) + Container( + height: 100, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory( + _currentImageBlob!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Center(child: Text('Erreur image')), + ), + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _isEditLoading ? null : saveChanges, + icon: const Icon(Icons.save), + label: Text(_isEditLoading ? 'Sauvegarde...' : 'Enregistrer'), + ), + ), + ], ), - items: pointsDeVente.map((pv) => DropdownMenuItem( - value: pv['id'] as int, - child: Text(pv['nom'] as String), - )).toList(), - onChanged: (value) { - selectedPointDeVenteId = value; - }, ), ), - ], - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), ), - ElevatedButton( - onPressed: () async { - if (selectedPointDeVenteId != null) { - Get.back(); - await _performTransferAndDelete(sourcePointDeVenteId, selectedPointDeVenteId!); - } else { - Get.snackbar( - 'Erreur', - 'Veuillez sélectionner un point de vente de destination', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - child: const Text('Transférer et supprimer'), - ), - ], + ), ), ); + + // Nettoyage + await Future.delayed(const Duration(milliseconds: 200)); + _editNomController.dispose(); + _editCodeController.dispose(); + _editTicketController.dispose(); + _editLivraisonController.dispose(); + _editFactureController.dispose(); } -// Nouvelle méthode pour effectuer le transfert et la suppression -Future _performTransferAndDelete(int sourceId, int targetId) async { - setState(() { - _isLoading = true; - }); - - try { - // Afficher un dialog de confirmation final - final confirmed = await Get.dialog( + + + Future _showConstraintDialog(int id, Map verificationResult) async { + final reasons = verificationResult['reasons'] as List; + final suggestions = verificationResult['suggestions'] as List; + + await Get.dialog( AlertDialog( - title: const Text('Confirmation finale'), - content: const Text( - 'Cette action va transférer tous les produits vers le point de vente sélectionné ' - 'puis supprimer définitivement le point de vente original. ' - 'Cette action est irréversible. Continuer ?' + title: const Row( + children: [ + Icon(Icons.warning, color: Colors.orange), + SizedBox(width: 8), + Expanded(child: Text('Suppression impossible')), + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 400), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Ce point de vente ne peut pas être supprimé pour les raisons suivantes :', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + ...reasons.map((reason) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('• ', style: TextStyle(color: Colors.red)), + Expanded(child: Text(reason)), + ], + ), + )), + if (suggestions.isNotEmpty) ...[ + const SizedBox(height: 16), + const Text( + 'Solutions possibles :', + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue), + ), + const SizedBox(height: 8), + ...suggestions.map((suggestion) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('💡 ', style: TextStyle(fontSize: 12)), + Expanded(child: Text(suggestion, style: const TextStyle(fontSize: 13))), + ], + ), + )), + ], + ], + ), + ), ), actions: [ TextButton( - onPressed: () => Get.back(result: false), - child: const Text('Annuler'), + onPressed: () => Get.back(), + child: const Text('Fermer'), ), - ElevatedButton( - onPressed: () => Get.back(result: true), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, + if (reasons.any((r) => r.contains('produit'))) + ElevatedButton( + onPressed: () { + Get.back(); + _showTransferDialog(id); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + child: const Text('Transférer'), ), - child: const Text('Confirmer'), - ), ], ), ); + } - if (confirmed == true) { - await _appDatabase.deletePointDeVenteWithTransfer(sourceId, targetId); - await _loadPointsDeVente(); - + Future _showTransferDialog(int sourcePointDeVenteId) async { + final pointsDeVente = await _appDatabase.getPointsDeVenteForTransfer(sourcePointDeVenteId); + + if (pointsDeVente.isEmpty) { Get.snackbar( - 'Succès', - 'Produits transférés et point de vente supprimé avec succès', + 'Erreur', + 'Aucun autre point de vente disponible pour le transfert', snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, + backgroundColor: Colors.red, colorText: Colors.white, - duration: const Duration(seconds: 4), ); + return; } - } catch (e) { - Get.snackbar( - 'Erreur', - 'Erreur lors du transfert: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } finally { - setState(() { - _isLoading = false; - }); - } -} -// Vous pouvez aussi ajouter une méthode pour voir les détails d'un point de vente -Future _showPointDeVenteDetails(Map pointDeVente) async { - final id = pointDeVente['id'] as int; - - try { - // Récupérer les statistiques - final stats = await _getPointDeVenteStats(id); + + int? selectedPointDeVenteId; await Get.dialog( AlertDialog( - title: Text('Détails: ${pointDeVente['nom']}'), - content: SingleChildScrollView( + title: const Text('Transférer les produits'), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildStatRow('Produits associés', '${stats['produits']}'), - _buildStatRow('Utilisateurs associés', '${stats['utilisateurs']}'), - _buildStatRow('Demandes de transfert', '${stats['transferts']}'), - const SizedBox(height: 8), - Text( - 'Code: ${pointDeVente['code'] ?? 'N/A'}', - style: const TextStyle(fontSize: 12, color: Colors.grey), + const Text('Sélectionnez le point de vente de destination pour les produits :'), + const SizedBox(height: 16), + DropdownButtonFormField( + value: selectedPointDeVenteId, + isExpanded: true, + decoration: const InputDecoration( + labelText: 'Point de vente de destination', + border: OutlineInputBorder(), + ), + items: pointsDeVente.map((pv) => DropdownMenuItem( + value: pv['id'] as int, + child: Text( + pv['nom'] as String, + overflow: TextOverflow.ellipsis, + ), + )).toList(), + onChanged: (value) { + selectedPointDeVenteId = value; + }, ), ], ), @@ -359,124 +498,309 @@ Future _showPointDeVenteDetails(Map pointDeVente) async { actions: [ TextButton( onPressed: () => Get.back(), - child: const Text('Fermer'), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () async { + if (selectedPointDeVenteId != null) { + Get.back(); + await _performTransferAndDelete(sourcePointDeVenteId, selectedPointDeVenteId!); + } else { + Get.snackbar( + 'Erreur', + 'Veuillez sélectionner un point de vente de destination', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + child: const Text('Transférer'), ), ], ), ); - } catch (e) { - Get.snackbar( - 'Erreur', - 'Impossible de récupérer les détails: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); } -} - -Widget _buildStatRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label), - Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), - ], - ), - ); -} -// Méthode helper pour récupérer les stats -Future> _getPointDeVenteStats(int id) async { - final verification = await _appDatabase.checkCanDeletePointDeVente(id); - - // Parser les raisons pour extraire les nombres - int produits = 0, utilisateurs = 0, transferts = 0; - - for (String reason in verification['reasons']) { - if (reason.contains('produit')) { - produits = int.tryParse(reason.split(' ')[0]) ?? 0; - } else if (reason.contains('utilisateur')) { - utilisateurs = int.tryParse(reason.split(' ')[0]) ?? 0; - } else if (reason.contains('transfert')) { - transferts = int.tryParse(reason.split(' ')[0]) ?? 0; + Future _performTransferAndDelete(int sourceId, int targetId) async { + if (mounted) { + setState(() { + _isLoading = true; + }); } - } - - return { - 'produits': produits, - 'utilisateurs': utilisateurs, - 'transferts': transferts, - }; -} - Future _deletePointDeVente(int id) async { - // 1. D'abord vérifier si la suppression est possible - final verificationResult = await _appDatabase.checkCanDeletePointDeVente(id); - - if (!verificationResult['canDelete']) { - // Afficher un dialog avec les détails des contraintes - await _showConstraintDialog(id, verificationResult); - return; - } - - // 2. Si pas de contraintes, procéder normalement - final confirmed = await Get.dialog( - AlertDialog( - title: const Text('Confirmer la suppression'), - content: const Text('Voulez-vous vraiment supprimer ce point de vente ?'), - actions: [ - TextButton( - onPressed: () => Get.back(result: false), - child: const Text('Annuler'), - ), - TextButton( - onPressed: () => Get.back(result: true), - child: const Text('Supprimer', style: TextStyle(color: Colors.red)), + try { + final confirmed = await Get.dialog( + AlertDialog( + title: const Text('Confirmation finale'), + content: const Text( + 'Cette action va transférer tous les produits vers le point de vente sélectionné ' + 'puis supprimer définitivement le point de vente original. ' + 'Cette action est irréversible. Continuer ?' + ), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () => Get.back(result: true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Confirmer'), + ), + ], ), - ], - ), - ); - - if (confirmed == true) { - setState(() { - _isLoading = true; - }); + ); - try { - await _appDatabase.deletePointDeVente(id); - await _loadPointsDeVente(); - + if (confirmed == true) { + await _appDatabase.deletePointDeVenteWithTransfer(sourceId, targetId); + await _loadPointsDeVente(); + + Get.snackbar( + 'Succès', + 'Produits transférés et point de vente supprimé avec succès', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 4), + ); + } + } catch (e) { Get.snackbar( - 'Succès', - 'Point de vente supprimé avec succès', + 'Erreur', + 'Erreur lors du transfert: ${e.toString()}', snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, + backgroundColor: Colors.red, colorText: Colors.white, ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future _showPointDeVenteDetails(Map pointDeVente) async { + final id = pointDeVente['id'] as int; + + try { + final stats = await _getPointDeVenteStats(id); + + await Get.dialog( + AlertDialog( + title: Text( + 'Détails: ${pointDeVente['nom']}', + style: const TextStyle(fontSize: 16), + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400, + maxHeight: 500, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (pointDeVente['logo'] != null) + Container( + height: 150, + width: double.infinity, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory( + pointDeVente['logo'] as Uint8List, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey.shade200, + child: const Center( + child: Text('Image non disponible'), + ), + ); + }, + ), + ), + ), + + _buildStatRow('Produits associés', '${stats['produits']}'), + _buildStatRow('Utilisateurs associés', '${stats['utilisateurs']}'), + _buildStatRow('Demandes de transfert', '${stats['transferts']}'), + const SizedBox(height: 8), + if (pointDeVente['content'] != null && + pointDeVente['content'].toString().isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Ticket: ${pointDeVente['content']}', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ), + if (pointDeVente['livraison'] != null && + pointDeVente['livraison'].toString().isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Bon de livraison: ${pointDeVente['livraison']}', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ), + if (pointDeVente['facture'] != null && + pointDeVente['facture'].toString().isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Facture: ${pointDeVente['facture']}', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ], + ), + ); } catch (e) { Get.snackbar( 'Erreur', - 'Impossible de supprimer le point de vente: ${e.toString()}', + 'Impossible de récupérer les détails: ${e.toString()}', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); - } finally { - setState(() { - _isLoading = false; - }); } } -} + + Widget _buildStatRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + label, + style: const TextStyle(fontSize: 13), + ), + ), + Text( + value, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), + ), + ], + ), + ); + } + + Future> _getPointDeVenteStats(int id) async { + final verification = await _appDatabase.checkCanDeletePointDeVente(id); + + // Parser les raisons pour extraire les nombres + int produits = 0, utilisateurs = 0, transferts = 0; + + for (String reason in verification['reasons']) { + if (reason.contains('produit')) { + produits = int.tryParse(reason.split(' ')[0]) ?? 0; + } else if (reason.contains('utilisateur')) { + utilisateurs = int.tryParse(reason.split(' ')[0]) ?? 0; + } else if (reason.contains('transfert')) { + transferts = int.tryParse(reason.split(' ')[0]) ?? 0; + } + } + + return { + 'produits': produits, + 'utilisateurs': utilisateurs, + 'transferts': transferts, + }; + } + + Future _deletePointDeVente(int id) async { + final verificationResult = await _appDatabase.checkCanDeletePointDeVente(id); + + if (!verificationResult['canDelete']) { + await _showConstraintDialog(id, verificationResult); + return; + } + + final confirmed = await Get.dialog( + AlertDialog( + title: const Text('Confirmer la suppression'), + content: const Text('Voulez-vous vraiment supprimer ce point de vente ?'), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text('Supprimer', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + + if (confirmed == true) { + if (mounted) { + setState(() { + _isLoading = true; + }); + } + + try { + await _appDatabase.deletePointDeVente(id); + await _loadPointsDeVente(); + + Get.snackbar( + 'Succès', + 'Point de vente supprimé avec succès', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } catch (e) { + Get.snackbar( + 'Erreur', + 'Impossible de supprimer le point de vente: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + } @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar(title: 'Gestion des points de vente'), - drawer: CustomDrawer(), + drawer: CustomDrawer(), body: Column( children: [ // Formulaire d'ajout @@ -503,7 +827,6 @@ Future> _getPointDeVenteStats(int id) async { ), const SizedBox(height: 16), - // Champ Nom TextFormField( controller: _nomController, decoration: InputDecoration( @@ -524,7 +847,6 @@ Future> _getPointDeVenteStats(int id) async { ), const SizedBox(height: 12), - // Champ Code TextFormField( controller: _codeController, decoration: InputDecoration( @@ -539,10 +861,8 @@ Future> _getPointDeVenteStats(int id) async { ), const SizedBox(height: 16), - // Bouton de soumission ElevatedButton( - onPressed: _isLoading ? null : _submitForm, - + onPressed: _isLoading ? null : _submitForm, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: Colors.blue.shade800, @@ -550,8 +870,8 @@ Future> _getPointDeVenteStats(int id) async { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - ), - child: _isLoading + ), + child: _isLoading ? const SizedBox( width: 20, height: 20, @@ -579,7 +899,7 @@ Future> _getPointDeVenteStats(int id) async { children: [ // Barre de recherche Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.only(bottom: 16), child: TextField( controller: _searchController, decoration: InputDecoration( @@ -595,7 +915,7 @@ Future> _getPointDeVenteStats(int id) async { icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); - _loadPointsDeVente(); + _filterPointsDeVente(); }, ) : null, @@ -603,49 +923,11 @@ Future> _getPointDeVenteStats(int id) async { ), ), - // En-tête de liste - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - const Expanded( - flex: 2, - child: Text( - 'Nom', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 9, 56, 95), - ), - ), - ), - const Expanded( - child: Text( - 'Code', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 9, 56, 95), - ), - ), - ), - SizedBox( - width: 40, - child: Text( - 'Actions', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 9, 56, 95), - ), - ), - ), - ], - ), - ), - // Liste Expanded( - child: _isLoading && _pointsDeVente.isEmpty + child: _isLoading && _filteredPointsDeVente.isEmpty ? const Center(child: CircularProgressIndicator()) - : _pointsDeVente.isEmpty + : _filteredPointsDeVente.isEmpty ? const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -663,154 +945,162 @@ Future> _getPointDeVenteStats(int id) async { ), ) : ListView.builder( - itemCount: _pointsDeVente.length, - itemBuilder: (context, index) { - final point = _pointsDeVente[index]; - final canDelete = point['canDelete'] ?? true; - final constraintCount = point['constraintCount'] ?? 0; - - return Card( - margin: const EdgeInsets.only(bottom: 8), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: !canDelete ? Border.all( - color: Colors.orange.shade300, - width: 1, - ) : null, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - // Icône avec indicateur de statut - Stack( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: canDelete ? Colors.blue.shade50 : Colors.orange.shade50, - borderRadius: BorderRadius.circular(6), - ), - child: Icon( - Icons.store, - color: canDelete ? Colors.blue.shade700 : Colors.orange.shade700, - size: 20, - ), - ), - if (!canDelete) - Positioned( - right: 0, - top: 0, - child: Container( - padding: const EdgeInsets.all(2), - decoration: const BoxDecoration( - color: Colors.orange, - shape: BoxShape.circle, - ), - child: Text( - '$constraintCount', - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - const SizedBox(width: 12), - - // Informations du point de vente - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - point['nom'] ?? 'N/A', - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 15, - ), - ), - ), - if (!canDelete) - Icon( - Icons.link, - size: 16, - color: Colors.orange.shade600, - ), - ], - ), - Row( - children: [ - if (point['code'] != null && point['code'].toString().isNotEmpty) - Text( - 'Code: ${point['code']}', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 12, - ), - ), - if (!canDelete) ...[ - const SizedBox(width: 8), - Text( - '$constraintCount contrainte(s)', - style: TextStyle( - color: Colors.orange.shade600, - fontSize: 11, - fontWeight: FontWeight.w500, - ), - ), - ], - ], - ), - ], - ), - ), - - // Boutons d'actions - Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Bouton détails - IconButton( - icon: Icon( - Icons.info_outline, - size: 20, - color: Colors.blue.shade600, - ), - onPressed: () => _showPointDeVenteDetails(point), - tooltip: 'Voir les détails', - ), - - // Bouton suppression avec indication visuelle - IconButton( - icon: Icon( - canDelete ? Icons.delete_outline : Icons.delete_forever_outlined, - size: 20, - color: canDelete ? Colors.red : Colors.orange.shade700, - ), - onPressed: () => _deletePointDeVente(point['id']), - tooltip: canDelete ? 'Supprimer' : 'Supprimer (avec contraintes)', - ), - ], - ), - ], - ), - ), - ), - ); - }, -) + itemCount: _filteredPointsDeVente.length, + itemBuilder: (context, index) { + final point = _filteredPointsDeVente[index]; + final canDelete = point['canDelete'] ?? true; + final constraintCount = point['constraintCount'] ?? 0; + return Card( + margin: const EdgeInsets.only(bottom: 8), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: !canDelete ? Border.all( + color: Colors.orange.shade300, + width: 1, + ) : null, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + // Icône avec indicateur de statut + Stack( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: canDelete ? Colors.blue.shade50 : Colors.orange.shade50, + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + Icons.store, + color: canDelete ? Colors.blue.shade700 : Colors.orange.shade700, + size: 20, + ), + ), + if (!canDelete) + Positioned( + right: 0, + top: 0, + child: Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.orange, + shape: BoxShape.circle, + ), + child: Text( + '$constraintCount', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + const SizedBox(width: 12), + + // Informations du point de vente + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + point['nom'] ?? 'N/A', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + ), + if (!canDelete) + Icon( + Icons.link, + size: 16, + color: Colors.orange.shade600, + ), + ], + ), + Row( + children: [ + if (point['code'] != null && point['code'].toString().isNotEmpty) + Text( + 'Code: ${point['code']}', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + ), + if (!canDelete) ...[ + const SizedBox(width: 8), + Text( + '$constraintCount contrainte(s)', + style: TextStyle( + color: Colors.orange.shade600, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ], + ), + ), + + // Boutons d'actions + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Bouton détails + IconButton( + icon: Icon( + Icons.info_outline, + size: 20, + color: Colors.blue.shade600, + ), + onPressed: () => _showPointDeVenteDetails(point), + tooltip: 'Voir les détails', + ), + // Bouton modifier + IconButton( + icon: Icon( + Icons.edit, + size: 20, + color: Colors.blue.shade600, + ), + onPressed: () => editPointDeVente(context, point), + tooltip: 'Modifier point de vente', + ), + // Bouton suppression avec indication visuelle + IconButton( + icon: Icon( + canDelete ? Icons.delete_outline : Icons.delete_forever_outlined, + size: 20, + color: canDelete ? Colors.red : Colors.orange.shade700, + ), + onPressed: () => _deletePointDeVente(point['id']), + tooltip: canDelete ? 'Supprimer' : 'Supprimer (avec contraintes)', + ), + ], + ), + ], + ), + ), + ), + ); + }, + ), ), ], ), diff --git a/lib/config/DatabaseConfig.dart b/lib/config/DatabaseConfig.dart index 1735531..c5b3d0b 100644 --- a/lib/config/DatabaseConfig.dart +++ b/lib/config/DatabaseConfig.dart @@ -11,8 +11,7 @@ class DatabaseConfig { static const String localDatabase = 'guycom'; // Production (public) MySQL settings - static const String prodHost = '185.70.105.157'; - // static const String prodHost = '102.17.52.31'; + static const String prodHost = '102.17.52.31'; static const String prodUsername = 'guycom'; static const String prodPassword = '3iV59wjRdbuXAPR'; static const String prodDatabase = 'guycom'; diff --git a/pubspec.yaml b/pubspec.yaml index 46c7c97..bf1a3f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,11 +70,6 @@ dependencies: window_manager: ^0.3.7 camera: ^0.10.5+9 - - - - - dev_dependencies: flutter_test: sdk: flutter