diff --git a/assets/NotoEmoji-Regular.ttf b/assets/NotoEmoji-Regular.ttf new file mode 100644 index 0000000..5c902c0 Binary files /dev/null and b/assets/NotoEmoji-Regular.ttf differ diff --git a/lib/Components/PaymentEnchainedDialog.dart b/lib/Components/PaymentEnchainedDialog.dart deleted file mode 100644 index 739a3e7..0000000 --- a/lib/Components/PaymentEnchainedDialog.dart +++ /dev/null @@ -1,338 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:get/get_core/src/get_main.dart'; -import 'package:youmazgestion/Components/DiscountDialog.dart'; -import 'package:youmazgestion/Components/paymentType.dart'; -import 'package:youmazgestion/Models/Client.dart'; -import 'package:youmazgestion/Models/Remise.dart'; - -// Dialogue de paiement amélioré avec support des remises -class PaymentMethodEnhancedDialog extends StatefulWidget { - final Commande commande; - - const PaymentMethodEnhancedDialog({super.key, required this.commande}); - - @override - _PaymentMethodEnhancedDialogState createState() => _PaymentMethodEnhancedDialogState(); -} - -class _PaymentMethodEnhancedDialogState extends State { - PaymentType _selectedPayment = PaymentType.cash; - final _amountController = TextEditingController(); - Remise? _appliedRemise; - - @override - void initState() { - super.initState(); - _amountController.text = widget.commande.montantTotal.toStringAsFixed(2); - } - - @override - void dispose() { - _amountController.dispose(); - super.dispose(); - } - - void _showDiscountDialog() { - showDialog( - context: context, - builder: (context) => DiscountDialog( - onDiscountApplied: (remise) { - setState(() { - _appliedRemise = remise; - final montantFinal = widget.commande.montantTotal - remise.calculerRemise(widget.commande.montantTotal); - _amountController.text = montantFinal.toStringAsFixed(2); - }); - }, - ), - ); - } - - void _removeDiscount() { - setState(() { - _appliedRemise = null; - _amountController.text = widget.commande.montantTotal.toStringAsFixed(2); - }); - } - - void _validatePayment() { - final montantFinal = _appliedRemise != null - ? widget.commande.montantTotal - _appliedRemise!.calculerRemise(widget.commande.montantTotal) - : widget.commande.montantTotal; - - if (_selectedPayment == PaymentType.cash) { - final amountGiven = double.tryParse(_amountController.text) ?? 0; - if (amountGiven < montantFinal) { - Get.snackbar( - 'Erreur', - 'Le montant donné est insuffisant', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - return; - } - } - - Navigator.pop(context, PaymentMethodEnhanced( - type: _selectedPayment, - amountGiven: _selectedPayment == PaymentType.cash - ? double.parse(_amountController.text) - : montantFinal, - remise: _appliedRemise, - )); - } - - @override - Widget build(BuildContext context) { - final montantOriginal = widget.commande.montantTotal; - final montantFinal = _appliedRemise != null - ? montantOriginal - _appliedRemise!.calculerRemise(montantOriginal) - : montantOriginal; - final amount = double.tryParse(_amountController.text) ?? 0; - final change = amount - montantFinal; - - return AlertDialog( - title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Résumé des montants - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Montant original:'), - Text('${montantOriginal.toStringAsFixed(0)} MGA'), - ], - ), - if (_appliedRemise != null) ...[ - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Remise (${_appliedRemise!.libelle}):'), - Text( - '- ${_appliedRemise!.calculerRemise(montantOriginal).toStringAsFixed(0)} MGA', - style: const TextStyle(color: Colors.red), - ), - ], - ), - const Divider(), - ], - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Total à payer:', style: TextStyle(fontWeight: FontWeight.bold)), - Text('${montantFinal.toStringAsFixed(0)} MGA', - style: const TextStyle(fontWeight: FontWeight.bold)), - ], - ), - ], - ), - ), - const SizedBox(height: 16), - - // Bouton remise - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: _appliedRemise == null ? _showDiscountDialog : _removeDiscount, - icon: Icon(_appliedRemise == null ? Icons.local_offer : Icons.close), - label: Text(_appliedRemise == null ? 'Ajouter remise' : 'Supprimer remise'), - style: OutlinedButton.styleFrom( - foregroundColor: _appliedRemise == null ? Colors.orange : Colors.red, - side: BorderSide( - color: _appliedRemise == null ? Colors.orange : Colors.red, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 16), - - // Section Paiement mobile - const Align( - alignment: Alignment.centerLeft, - child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: _buildMobileMoneyTile( - title: 'Mvola', - imagePath: 'assets/mvola.jpg', - value: PaymentType.mvola, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildMobileMoneyTile( - title: 'Orange Money', - imagePath: 'assets/Orange_money.png', - value: PaymentType.orange, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildMobileMoneyTile( - title: 'Airtel Money', - imagePath: 'assets/airtel_money.png', - value: PaymentType.airtel, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Section Carte bancaire - const Align( - alignment: Alignment.centerLeft, - child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)), - ), - const SizedBox(height: 8), - _buildPaymentMethodTile( - title: 'Carte bancaire', - icon: Icons.credit_card, - value: PaymentType.card, - ), - const SizedBox(height: 16), - - // Section Paiement en liquide - const Align( - alignment: Alignment.centerLeft, - child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)), - ), - const SizedBox(height: 8), - _buildPaymentMethodTile( - title: 'Paiement en liquide', - icon: Icons.money, - value: PaymentType.cash, - ), - if (_selectedPayment == PaymentType.cash) ...[ - const SizedBox(height: 12), - TextField( - controller: _amountController, - decoration: const InputDecoration( - labelText: 'Montant donné', - prefixText: 'MGA ', - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.numberWithOptions(decimal: true), - onChanged: (value) => setState(() {}), - ), - const SizedBox(height: 8), - Text( - 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: change >= 0 ? Colors.green : Colors.red, - ), - ), - ], - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Annuler', style: TextStyle(color: Colors.grey)), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - ), - onPressed: _validatePayment, - child: const Text('Confirmer'), - ), - ], - ); - } - - Widget _buildMobileMoneyTile({ - required String title, - required String imagePath, - required PaymentType value, - }) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), - width: 2, - ), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => setState(() => _selectedPayment = value), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - Image.asset( - imagePath, - height: 30, - width: 30, - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.mobile_friendly, size: 30), - ), - const SizedBox(height: 8), - Text( - title, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 12), - ), - ], - ), - ), - ), - ); - } - - Widget _buildPaymentMethodTile({ - required String title, - required IconData icon, - required PaymentType value, - }) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), - width: 2, - ), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => setState(() => _selectedPayment = value), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Icon(icon, size: 24), - const SizedBox(width: 12), - Text(title), - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/Components/commandManagementComponents/CommandDetails.dart b/lib/Components/commandManagementComponents/CommandDetails.dart index 62ab322..6633fd2 100644 --- a/lib/Components/commandManagementComponents/CommandDetails.dart +++ b/lib/Components/commandManagementComponents/CommandDetails.dart @@ -1,3 +1,5 @@ +// Remplacez complètement votre fichier CommandeDetails par celui-ci : + import 'package:flutter/material.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; @@ -7,9 +9,7 @@ class CommandeDetails extends StatelessWidget { const CommandeDetails({required this.commande}); - - - Widget _buildTableHeader(String text) { + Widget _buildTableHeader(String text, {bool isAmount = false}) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( @@ -18,23 +18,122 @@ class CommandeDetails extends StatelessWidget { fontWeight: FontWeight.bold, fontSize: 14, ), - textAlign: TextAlign.center, + textAlign: isAmount ? TextAlign.right : TextAlign.center, ), ); } - Widget _buildTableCell(String text) { + Widget _buildTableCell(String text, {bool isAmount = false, Color? textColor}) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( text, - style: const TextStyle(fontSize: 13), - textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: textColor, + ), + textAlign: isAmount ? TextAlign.right : TextAlign.center, ), ); } -@override + Widget _buildPriceColumn(DetailCommande detail) { + if (detail.aRemise) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${detail.prixUnitaire.toStringAsFixed(2)}', + style: const TextStyle( + fontSize: 11, + decoration: TextDecoration.lineThrough, + color: Colors.grey, + ), + ), + const SizedBox(height: 2), + Text( + '${(detail.prixFinal / detail.quantite).toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.orange.shade700, + ), + ), + ], + ), + ); + } else { + return _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA', isAmount: true); + } + } + + Widget _buildRemiseColumn(DetailCommande detail) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: detail.aRemise + ? Column( + children: [ + Text( + detail.remiseDescription, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.orange.shade700, + ), + textAlign: TextAlign.center, + ), + Text( + '-${detail.montantRemise.toStringAsFixed(0)} MGA', + style: TextStyle( + fontSize: 10, + color: Colors.teal.shade700, + ), + textAlign: TextAlign.center, + ), + ], + ) + : const Text( + '-', + style: TextStyle(fontSize: 13, color: Colors.grey), + textAlign: TextAlign.center, + ), + ); + } + + Widget _buildTotalColumn(DetailCommande detail) { + if (detail.aRemise && detail.sousTotal != detail.prixFinal) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${detail.sousTotal.toStringAsFixed(2)}', + style: const TextStyle( + fontSize: 11, + decoration: TextDecoration.lineThrough, + color: Colors.grey, + ), + ), + const SizedBox(height: 2), + Text( + '${detail.prixFinal.toStringAsFixed(2)} MGA', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } else { + return _buildTableCell('${detail.prixFinal.toStringAsFixed(2)} MGA', isAmount: true); + } + } + + @override Widget build(BuildContext context) { return FutureBuilder>( future: AppDatabase.instance.getDetailsCommande(commande.id!), @@ -48,6 +147,19 @@ class CommandeDetails extends StatelessWidget { } final details = snapshot.data!; + + // Calculer les totaux + double sousTotal = 0; + double totalRemises = 0; + double totalFinal = 0; + bool hasRemises = false; + + for (final detail in details) { + sousTotal += detail.sousTotal; + totalRemises += detail.montantRemise; + totalFinal += detail.prixFinal; + if (detail.aRemise) hasRemises = true; + } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -55,16 +167,46 @@ class CommandeDetails extends StatelessWidget { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.blue.shade50, + color: hasRemises ? Colors.orange.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(8), + border: hasRemises + ? Border.all(color: Colors.orange.shade200) + : null, ), - child: const Text( - 'Détails de la commande', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: Colors.black87, - ), + child: Row( + children: [ + Icon( + hasRemises ? Icons.discount : Icons.receipt_long, + color: hasRemises ? Colors.orange.shade700 : Colors.blue.shade700, + ), + const SizedBox(width: 8), + Text( + hasRemises ? 'Détails de la commande (avec remises)' : 'Détails de la commande', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: hasRemises ? Colors.orange.shade800 : Colors.black87, + ), + ), + if (hasRemises) ...[ + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Économies: ${totalRemises.toStringAsFixed(0)} MGA', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.orange.shade700, + ), + ), + ), + ], + ], ), ), const SizedBox(height: 12), @@ -82,26 +224,72 @@ class CommandeDetails extends StatelessWidget { children: [ _buildTableHeader('Produit'), _buildTableHeader('Qté'), - _buildTableHeader('Prix unit.'), - _buildTableHeader('Total'), + _buildTableHeader('Prix unit.', isAmount: true), + if (hasRemises) _buildTableHeader('Remise'), + _buildTableHeader('Total', isAmount: true), ], ), ...details.map((detail) => TableRow( + decoration: detail.aRemise + ? BoxDecoration( + color: const Color.fromARGB(255, 243, 191, 114), + border: Border( + left: BorderSide( + color: Colors.orange.shade300, + width: 3, + ), + ), + ) + : null, children: [ - _buildTableCell( - detail.estCadeau == true - ? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)' - : detail.produitNom ?? 'Produit inconnu' + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + detail.produitNom ?? 'Produit inconnu', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + if (detail.aRemise) ...[ + const SizedBox(height: 2), + Row( + children: [ + Icon( + Icons.local_offer, + size: 12, + color: Colors.teal.shade700, + ), + const SizedBox(width: 4), + Text( + 'Avec remise', + style: TextStyle( + fontSize: 10, + color: Colors.teal.shade700, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ], + ], + ), ), _buildTableCell('${detail.quantite}'), - _buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'), - _buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(2)} MGA'), + _buildPriceColumn(detail), + if (hasRemises) _buildRemiseColumn(detail), + _buildTotalColumn(detail), ], )), ], ), ), const SizedBox(height: 12), + + // Section des totaux Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -111,39 +299,63 @@ class CommandeDetails extends StatelessWidget { ), child: Column( children: [ - if (commande.montantApresRemise != null) ...[ + // Sous-total si il y a des remises + if (hasRemises) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Sous-total:', - style: TextStyle(fontSize: 14), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), Text( - '${commande.montantTotal.toStringAsFixed(2)} MGA', - style: const TextStyle(fontSize: 14), + '${sousTotal.toStringAsFixed(2)} MGA', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), ], ), - const SizedBox(height: 5), + const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Remise:', - style: TextStyle(fontSize: 14), + Row( + children: [ + Icon( + Icons.discount, + size: 16, + color: Colors.orange.shade700, + ), + const SizedBox(width: 4), + Text( + 'Remises totales:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.orange.shade700, + ), + ), + ], ), Text( - '-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA', - style: const TextStyle( + '-${totalRemises.toStringAsFixed(2)} MGA', + style: TextStyle( fontSize: 14, - color: Colors.red, + fontWeight: FontWeight.bold, + color: Colors.orange.shade700, ), ), ], ), - const Divider(), + const Divider(height: 16), ], + + // Total final Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -155,7 +367,7 @@ class CommandeDetails extends StatelessWidget { ), ), Text( - '${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(2)} MGA', + '${commande.montantTotal.toStringAsFixed(2)} MGA', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18, diff --git a/lib/Components/commandManagementComponents/CommandeActions.dart b/lib/Components/commandManagementComponents/CommandeActions.dart index 363e724..04241c1 100644 --- a/lib/Components/commandManagementComponents/CommandeActions.dart +++ b/lib/Components/commandManagementComponents/CommandeActions.dart @@ -8,15 +8,13 @@ class CommandeActions extends StatelessWidget { final Commande commande; final Function(int, StatutCommande) onStatutChanged; final Function(Commande) onPaymentSelected; - final Function(Commande) onDiscountSelected; - final Function(Commande) onGiftSelected; + const CommandeActions({ required this.commande, required this.onStatutChanged, required this.onPaymentSelected, - required this.onDiscountSelected, - required this.onGiftSelected, + }); @@ -27,18 +25,7 @@ class CommandeActions extends StatelessWidget { switch (commande.statut) { case StatutCommande.enAttente: buttons.addAll([ - _buildActionButton( - label: 'Remise', - icon: Icons.percent, - color: Colors.orange, - onPressed: () => onDiscountSelected(commande), - ), - _buildActionButton( - label: 'Cadeau', - icon: Icons.card_giftcard, - color: Colors.purple, - onPressed: () => onGiftSelected(commande), - ), + _buildActionButton( label: 'Confirmer', icon: Icons.check_circle, diff --git a/lib/Components/commandManagementComponents/PaswordRequired.dart b/lib/Components/commandManagementComponents/PaswordRequired.dart new file mode 100644 index 0000000..dec5680 --- /dev/null +++ b/lib/Components/commandManagementComponents/PaswordRequired.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; + +class PasswordVerificationDialog extends StatefulWidget { + final String title; + final String message; + final Function(String) onPasswordVerified; + + const PasswordVerificationDialog({ + Key? key, + required this.title, + required this.message, + required this.onPasswordVerified, + }) : super(key: key); + + @override + _PasswordVerificationDialogState createState() => _PasswordVerificationDialogState(); +} + +class _PasswordVerificationDialogState extends State { + final TextEditingController _passwordController = TextEditingController(); + bool _isPasswordVisible = false; + bool _isLoading = false; + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + title: Row( + children: [ + Icon( + Icons.security, + color: Colors.blue.shade700, + size: 28, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + widget.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.message, + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + const SizedBox(height: 20), + Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: TextField( + controller: _passwordController, + obscureText: !_isPasswordVisible, + autofocus: true, + decoration: InputDecoration( + labelText: 'Mot de passe', + prefixIcon: Icon( + Icons.lock_outline, + color: Colors.blue.shade600, + ), + suffixIcon: IconButton( + icon: Icon( + _isPasswordVisible ? Icons.visibility_off : Icons.visibility, + color: Colors.grey.shade600, + ), + onPressed: () { + setState(() { + _isPasswordVisible = !_isPasswordVisible; + }); + }, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onSubmitted: (value) => _verifyPassword(), + ), + ), + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.amber.shade200), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.amber.shade700, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Saisissez votre mot de passe pour confirmer cette action', + style: TextStyle( + fontSize: 12, + color: Colors.amber.shade700, + ), + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: _isLoading ? null : () => Navigator.of(context).pop(), + child: Text( + 'Annuler', + style: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + ), + ElevatedButton( + onPressed: _isLoading ? null : _verifyPassword, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + ), + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text('Vérifier'), + ), + ], + ); + } + + void _verifyPassword() async { + final password = _passwordController.text.trim(); + + if (password.isEmpty) { + Get.snackbar( + 'Erreur', + 'Veuillez saisir votre mot de passe', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final database = AppDatabase.instance; + final isValid = await database.verifyCurrentUserPassword(password); + + setState(() { + _isLoading = false; + }); + + if (isValid) { + Navigator.of(context).pop(); + widget.onPasswordVerified(password); + } else { + Get.snackbar( + 'Erreur', + 'Mot de passe incorrect', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + _passwordController.clear(); + } + } catch (e) { + setState(() { + _isLoading = false; + }); + + Get.snackbar( + 'Erreur', + 'Une erreur est survenue lors de la vérification', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + print("Erreur vérification mot de passe: $e"); + } + } +} \ No newline at end of file diff --git a/lib/Components/commandManagementComponents/PaymentMethodDialog.dart b/lib/Components/commandManagementComponents/PaymentMethodDialog.dart index b0ea0c5..875ad01 100644 --- a/lib/Components/commandManagementComponents/PaymentMethodDialog.dart +++ b/lib/Components/commandManagementComponents/PaymentMethodDialog.dart @@ -6,7 +6,6 @@ import 'package:youmazgestion/Components/commandManagementComponents/PaymentMeth import 'package:youmazgestion/Components/paymentType.dart'; import 'package:youmazgestion/Models/client.dart'; - class PaymentMethodDialog extends StatefulWidget { final Commande commande; @@ -21,7 +20,7 @@ class _PaymentMethodDialogState extends State { final _amountController = TextEditingController(); void _validatePayment() { - final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + final montantFinal = widget.commande.montantTotal; if (_selectedPayment == PaymentType.cash) { final amountGiven = double.tryParse(_amountController.text) ?? 0; @@ -48,7 +47,7 @@ class _PaymentMethodDialogState extends State { @override void initState() { super.initState(); - final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + final montantFinal = widget.commande.montantTotal; _amountController.text = montantFinal.toStringAsFixed(2); } @@ -61,7 +60,7 @@ class _PaymentMethodDialogState extends State { @override Widget build(BuildContext context) { final amount = double.tryParse(_amountController.text) ?? 0; - final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + final montantFinal = widget.commande.montantTotal; final change = amount - montantFinal; return AlertDialog( @@ -70,7 +69,7 @@ class _PaymentMethodDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Affichage du montant à payer + // Affichage du montant à payer (simplifié) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -78,34 +77,12 @@ class _PaymentMethodDialogState extends State { borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), - child: Column( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (widget.commande.montantApresRemise != null) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Montant original:'), - Text('${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Remise:'), - Text('-${(widget.commande.montantTotal - widget.commande.montantApresRemise!).toStringAsFixed(2)} MGA', - style: const TextStyle(color: Colors.red)), - ], - ), - const Divider(), - ], - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)), - Text('${montantFinal.toStringAsFixed(2)} MGA', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - ], - ), + const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)), + Text('${montantFinal.toStringAsFixed(2)} MGA', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ], ), ), diff --git a/lib/Components/newCommandComponents/CadeauDialog.dart b/lib/Components/newCommandComponents/CadeauDialog.dart new file mode 100644 index 0000000..8c2d2f7 --- /dev/null +++ b/lib/Components/newCommandComponents/CadeauDialog.dart @@ -0,0 +1,411 @@ +// Components/newCommandComponents/CadeauDialog.dart + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:youmazgestion/Models/client.dart'; +import 'package:youmazgestion/Models/produit.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; + +class CadeauDialog extends StatefulWidget { + final Product product; + final int quantite; + final DetailCommande? detailExistant; + + const CadeauDialog({ + Key? key, + required this.product, + required this.quantite, + this.detailExistant, + }) : super(key: key); + + @override + _CadeauDialogState createState() => _CadeauDialogState(); +} + +class _CadeauDialogState extends State { + final AppDatabase _database = AppDatabase.instance; + List _produitsDisponibles = []; + Product? _produitCadeauSelectionne; + int _quantiteCadeau = 1; + bool _isLoading = true; + String _searchQuery = ''; + + @override + void initState() { + super.initState(); + _loadProduitsDisponibles(); + } + + Future _loadProduitsDisponibles() async { + try { + final produits = await _database.getProducts(); + setState(() { + _produitsDisponibles = produits.where((p) => + p.id != widget.product.id && // Exclure le produit principal + (p.stock == null || p.stock! > 0) // Seulement les produits en stock + ).toList(); + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + }); + Get.snackbar( + 'Erreur', + 'Impossible de charger les produits: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + List get _produitsFiltres { + if (_searchQuery.isEmpty) { + return _produitsDisponibles; + } + return _produitsDisponibles.where((p) => + p.name.toLowerCase().contains(_searchQuery.toLowerCase()) || + (p.reference?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false) + ).toList(); + } + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 600; + + return AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.card_giftcard, color: Colors.green.shade700), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ajouter un cadeau', + style: TextStyle(fontSize: isMobile ? 16 : 18), + ), + Text( + 'Pour: ${widget.product.name}', + style: TextStyle( + fontSize: isMobile ? 12 : 14, + color: Colors.grey.shade600, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ], + ), + content: Container( + width: isMobile ? double.maxFinite : 500, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Information sur le produit principal + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Row( + children: [ + Icon(Icons.shopping_bag, color: Colors.blue.shade700), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Produit acheté', + style: TextStyle( + fontSize: 12, + color: Colors.blue.shade700, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${widget.quantite}x ${widget.product.name}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + Text( + 'Prix: ${widget.product.price.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Barre de recherche + TextField( + decoration: InputDecoration( + labelText: 'Rechercher un produit cadeau', + prefixIcon: Icon(Icons.search, color: Colors.green.shade600), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.green.shade50, + ), + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + ), + + const SizedBox(height: 16), + + // Liste des produits disponibles + Expanded( + child: _produitsFiltres.isEmpty + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.card_giftcard_outlined, + size: 48, + color: Colors.grey.shade400, + ), + const SizedBox(height: 8), + Text( + 'Aucun produit disponible', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: _produitsFiltres.length, + itemBuilder: (context, index) { + final produit = _produitsFiltres[index]; + final isSelected = _produitCadeauSelectionne?.id == produit.id; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + elevation: isSelected ? 4 : 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: isSelected + ? Colors.green.shade300 + : Colors.grey.shade200, + width: isSelected ? 2 : 1, + ), + ), + child: ListTile( + contentPadding: const EdgeInsets.all(12), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isSelected + ? Colors.green.shade100 + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.card_giftcard, + color: isSelected + ? Colors.green.shade700 + : Colors.grey.shade600, + ), + ), + title: Text( + produit.name, + style: TextStyle( + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Prix normal: ${produit.price.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + decoration: TextDecoration.lineThrough, + ), + ), + Row( + children: [ + Icon( + Icons.card_giftcard, + size: 14, + color: Colors.green.shade600, + ), + const SizedBox(width: 4), + Text( + 'GRATUIT', + style: TextStyle( + fontSize: 12, + color: Colors.green.shade700, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + if (produit.stock != null) + Text( + 'Stock: ${produit.stock}', + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade500, + ), + ), + ], + ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: Colors.green.shade700, + ) + : null, + onTap: () { + setState(() { + _produitCadeauSelectionne = produit; + }); + }, + ), + ); + }, + ), + ), + + // Sélection de la quantité si un produit est sélectionné + if (_produitCadeauSelectionne != null) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Row( + children: [ + Icon(Icons.card_giftcard, color: Colors.green.shade700), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Quantité de ${_produitCadeauSelectionne!.name}', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.green.shade700, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.green.shade300), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove, size: 16), + onPressed: _quantiteCadeau > 1 + ? () { + setState(() { + _quantiteCadeau--; + }); + } + : null, + ), + Text( + _quantiteCadeau.toString(), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + IconButton( + icon: const Icon(Icons.add, size: 16), + onPressed: () { + final maxStock = _produitCadeauSelectionne!.stock ?? 99; + if (_quantiteCadeau < maxStock) { + setState(() { + _quantiteCadeau++; + }); + } + }, + ), + ], + ), + ), + ], + ), + ), + ], + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 16 : 20, + vertical: isMobile ? 10 : 12, + ), + ), + icon: const Icon(Icons.card_giftcard), + label: Text( + isMobile ? 'Offrir' : 'Offrir le cadeau', + style: TextStyle(fontSize: isMobile ? 12 : 14), + ), + onPressed: _produitCadeauSelectionne != null + ? () { + Get.back(result: { + 'produit': _produitCadeauSelectionne!, + 'quantite': _quantiteCadeau, + }); + } + : null, + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/Components/newCommandComponents/RemiseDialog.dart b/lib/Components/newCommandComponents/RemiseDialog.dart new file mode 100644 index 0000000..e5f81a7 --- /dev/null +++ b/lib/Components/newCommandComponents/RemiseDialog.dart @@ -0,0 +1,331 @@ +// Components/RemiseDialog.dart +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:youmazgestion/Models/client.dart'; +import 'package:youmazgestion/Models/produit.dart'; + +class RemiseDialog extends StatefulWidget { + final Product product; + final int quantite; + final double prixUnitaire; + final DetailCommande? detailExistant; + + const RemiseDialog({ + super.key, + required this.product, + required this.quantite, + required this.prixUnitaire, + this.detailExistant, + }); + + @override + State createState() => _RemiseDialogState(); +} + +class _RemiseDialogState extends State { + final _formKey = GlobalKey(); + final _valeurController = TextEditingController(); + + RemiseType _selectedType = RemiseType.pourcentage; + double _montantRemise = 0.0; + double _prixFinal = 0.0; + late double _sousTotal; + + @override + void initState() { + super.initState(); + _sousTotal = widget.quantite * widget.prixUnitaire; + + // Si on modifie une remise existante + if (widget.detailExistant?.aRemise == true) { + _selectedType = widget.detailExistant!.remiseType!; + _valeurController.text = widget.detailExistant!.remiseValeur.toString(); + _calculateRemise(); + } else { + _prixFinal = _sousTotal; + } + } + + void _calculateRemise() { + final valeur = double.tryParse(_valeurController.text) ?? 0.0; + + setState(() { + if (_selectedType == RemiseType.pourcentage) { + final pourcentage = valeur.clamp(0.0, 100.0); + _montantRemise = _sousTotal * (pourcentage / 100); + } else { + _montantRemise = valeur.clamp(0.0, _sousTotal); + } + _prixFinal = _sousTotal - _montantRemise; + }); + } + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 600; + + return AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.discount, color: Colors.orange.shade700), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Appliquer une remise', + style: TextStyle(fontSize: isMobile ? 16 : 18), + ), + ), + ], + ), + content: Container( + width: isMobile ? double.maxFinite : 400, + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Informations du produit + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.product.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Quantité: ${widget.quantite}', + style: const TextStyle(fontSize: 12), + ), + Text( + 'Prix unitaire: ${widget.prixUnitaire.toStringAsFixed(2)} MGA', + style: const TextStyle(fontSize: 12), + ), + Text( + 'Sous-total: ${_sousTotal.toStringAsFixed(2)} MGA', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Type de remise + const Text( + 'Type de remise:', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 8), + + Row( + children: [ + Expanded( + child: RadioListTile( + title: const Text('Pourcentage (%)', style: TextStyle(fontSize: 12)), + value: RemiseType.pourcentage, + groupValue: _selectedType, + onChanged: (value) { + setState(() { + _selectedType = value!; + _calculateRemise(); + }); + }, + contentPadding: EdgeInsets.zero, + dense: true, + ), + ), + Expanded( + child: RadioListTile( + title: const Text('Montant (MGA)', style: TextStyle(fontSize: 12)), + value: RemiseType.montant, + groupValue: _selectedType, + onChanged: (value) { + setState(() { + _selectedType = value!; + _calculateRemise(); + }); + }, + contentPadding: EdgeInsets.zero, + dense: true, + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Valeur de la remise + TextFormField( + controller: _valeurController, + decoration: InputDecoration( + labelText: _selectedType == RemiseType.pourcentage + ? 'Pourcentage (0-100)' + : 'Montant en MGA', + prefixIcon: Icon( + _selectedType == RemiseType.pourcentage + ? Icons.percent + : Icons.attach_money, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), + ], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer une valeur'; + } + final valeur = double.tryParse(value); + if (valeur == null || valeur < 0) { + return 'Valeur invalide'; + } + if (_selectedType == RemiseType.pourcentage && valeur > 100) { + return 'Le pourcentage ne peut pas dépasser 100%'; + } + if (_selectedType == RemiseType.montant && valeur > _sousTotal) { + return 'La remise ne peut pas dépasser le sous-total'; + } + return null; + }, + onChanged: (value) => _calculateRemise(), + ), + + const SizedBox(height: 16), + + // Aperçu du calcul + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Sous-total:', style: TextStyle(fontSize: 12)), + Text( + '${_sousTotal.toStringAsFixed(2)} MGA', + style: const TextStyle(fontSize: 12), + ), + ], + ), + if (_montantRemise > 0) ...[ + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Remise ${_selectedType == RemiseType.pourcentage ? "(${_valeurController.text}%)" : ""}:', + style: TextStyle( + fontSize: 12, + color: Colors.orange.shade700, + ), + ), + Text( + '-${_montantRemise.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 12, + color: Colors.orange.shade700, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + const Divider(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Prix final:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${_prixFinal.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + actions: [ + if (widget.detailExistant?.aRemise == true) + TextButton.icon( + onPressed: () => Navigator.of(context).pop('supprimer'), + icon: const Icon(Icons.delete, color: Colors.red), + label: const Text('Supprimer remise', style: TextStyle(color: Colors.red)), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + final valeur = double.parse(_valeurController.text); + Navigator.of(context).pop({ + 'type': _selectedType, + 'valeur': valeur, + 'montantRemise': _montantRemise, + 'prixFinal': _prixFinal, + }); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Appliquer'), + ), + ], + ); + } + + @override + void dispose() { + _valeurController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/Components/teat.dart b/lib/Components/teat.dart deleted file mode 100644 index 4543ef4..0000000 --- a/lib/Components/teat.dart +++ /dev/null @@ -1,2125 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; - -import 'package:youmazgestion/Components/app_bar.dart'; -import 'package:youmazgestion/Components/appDrawer.dart'; -import 'package:youmazgestion/Models/client.dart'; -import 'package:youmazgestion/Models/users.dart'; -import 'package:youmazgestion/Models/produit.dart'; -import 'package:youmazgestion/Services/stock_managementDatabase.dart'; - -class NouvelleCommandePage extends StatefulWidget { - const NouvelleCommandePage({super.key}); - - @override - _NouvelleCommandePageState createState() => _NouvelleCommandePageState(); -} - -class _NouvelleCommandePageState extends State { - final AppDatabase _appDatabase = AppDatabase.instance; - final _formKey = GlobalKey(); - bool _isLoading = false; - - // Contrôleurs client - final TextEditingController _nomController = TextEditingController(); - final TextEditingController _prenomController = TextEditingController(); - final TextEditingController _emailController = TextEditingController(); - final TextEditingController _telephoneController = TextEditingController(); - final TextEditingController _adresseController = TextEditingController(); - - // Contrôleurs pour les filtres - final TextEditingController _searchNameController = TextEditingController(); - final TextEditingController _searchImeiController = TextEditingController(); - final TextEditingController _searchReferenceController = TextEditingController(); - - // Panier - final List _products = []; - final List _filteredProducts = []; - final Map _quantites = {}; - - // Variables de filtre - bool _showOnlyInStock = false; - - // Utilisateurs commerciaux - List _commercialUsers = []; - Users? _selectedCommercialUser; - - // Variables pour les suggestions clients - List _clientSuggestions = []; - bool _showNomSuggestions = false; - bool _showTelephoneSuggestions = false; - GlobalKey _nomFieldKey = GlobalKey(); - GlobalKey _telephoneFieldKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _loadProducts(); - _loadCommercialUsers(); - - // Listeners pour les filtres - _searchNameController.addListener(_filterProducts); - _searchImeiController.addListener(_filterProducts); - _searchReferenceController.addListener(_filterProducts); - - // Listeners pour l'autocomplétion client - _nomController.addListener(() { - if (_nomController.text.length >= 3) { - _showClientSuggestions(_nomController.text, isNom: true); - } else { - _hideNomSuggestions(); - } - }); - - _telephoneController.addListener(() { - if (_telephoneController.text.length >= 3) { - _showClientSuggestions(_telephoneController.text, isNom: false); - } else { - _hideTelephoneSuggestions(); - } - }); - } - - // Méthode pour vider complètement le formulaire et le panier - void _clearFormAndCart() { - setState(() { - // Vider les contrôleurs client - _nomController.clear(); - _prenomController.clear(); - _emailController.clear(); - _telephoneController.clear(); - _adresseController.clear(); - - // Vider le panier - _quantites.clear(); - - // Réinitialiser le commercial au premier de la liste - if (_commercialUsers.isNotEmpty) { - _selectedCommercialUser = _commercialUsers.first; - } - - // Masquer toutes les suggestions - _hideAllSuggestions(); - - // Réinitialiser l'état de chargement - _isLoading = false; - }); - } - - Future _showClientSuggestions(String query, {required bool isNom}) async { - if (query.length < 3) { - _hideAllSuggestions(); - return; - } - - final suggestions = await _appDatabase.suggestClients(query); - - setState(() { - _clientSuggestions = suggestions; - if (isNom) { - _showNomSuggestions = true; - _showTelephoneSuggestions = false; - } else { - _showTelephoneSuggestions = true; - _showNomSuggestions = false; - } - }); -} - - void _showOverlay({required bool isNom}) { - // Utiliser une approche plus simple avec setState - setState(() { - _clientSuggestions = _clientSuggestions; - if (isNom) { - _showNomSuggestions = true; - _showTelephoneSuggestions = false; - } else { - _showTelephoneSuggestions = true; - _showNomSuggestions = false; - } - }); - } - - void _fillClientForm(Client client) { - setState(() { - _nomController.text = client.nom; - _prenomController.text = client.prenom; - _emailController.text = client.email; - _telephoneController.text = client.telephone; - _adresseController.text = client.adresse ?? ''; - }); - - Get.snackbar( - 'Client trouvé', - 'Les informations ont été remplies automatiquement', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } - - void _hideNomSuggestions() { - if (mounted && _showNomSuggestions) { - setState(() { - _showNomSuggestions = false; - }); - } - } - - void _hideTelephoneSuggestions() { - if (mounted && _showTelephoneSuggestions){ - setState(() { - _showTelephoneSuggestions = false; - }); - } - } - - void _hideAllSuggestions() { - _hideNomSuggestions(); - _hideTelephoneSuggestions(); - } - - Future _loadProducts() async { - final products = await _appDatabase.getProducts(); - setState(() { - _products.clear(); - _products.addAll(products); - _filteredProducts.clear(); - _filteredProducts.addAll(products); - }); - } - - Future _loadCommercialUsers() async { - final commercialUsers = await _appDatabase.getCommercialUsers(); - setState(() { - _commercialUsers = commercialUsers; - if (_commercialUsers.isNotEmpty) { - _selectedCommercialUser = _commercialUsers.first; - } - }); - } - - void _filterProducts() { - final nameQuery = _searchNameController.text.toLowerCase(); - final imeiQuery = _searchImeiController.text.toLowerCase(); - final referenceQuery = _searchReferenceController.text.toLowerCase(); - - setState(() { - _filteredProducts.clear(); - - for (var product in _products) { - bool matchesName = nameQuery.isEmpty || - product.name.toLowerCase().contains(nameQuery); - - bool matchesImei = imeiQuery.isEmpty || - (product.imei?.toLowerCase().contains(imeiQuery) ?? false); - - bool matchesReference = referenceQuery.isEmpty || - (product.reference?.toLowerCase().contains(referenceQuery) ?? false); - - bool matchesStock = !_showOnlyInStock || - (product.stock != null && product.stock! > 0); - - if (matchesName && matchesImei && matchesReference && matchesStock) { - _filteredProducts.add(product); - } - } - }); - } - - void _toggleStockFilter() { - setState(() { - _showOnlyInStock = !_showOnlyInStock; - }); - _filterProducts(); - } - - void _clearFilters() { - setState(() { - _searchNameController.clear(); - _searchImeiController.clear(); - _searchReferenceController.clear(); - _showOnlyInStock = false; - }); - _filterProducts(); - } - - // Section des filtres adaptée pour mobile - Widget _buildFilterSection() { - final isMobile = MediaQuery.of(context).size.width < 600; - - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.filter_list, color: Colors.blue.shade700), - const SizedBox(width: 8), - const Text( - 'Filtres de recherche', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 9, 56, 95), - ), - ), - const Spacer(), - TextButton.icon( - onPressed: _clearFilters, - icon: const Icon(Icons.clear, size: 18), - label: isMobile ? const SizedBox() : const Text('Réinitialiser'), - style: TextButton.styleFrom( - foregroundColor: Colors.grey.shade600, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Champ de recherche par nom - TextField( - controller: _searchNameController, - decoration: InputDecoration( - labelText: 'Rechercher par nom', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - const SizedBox(height: 12), - - if (!isMobile) ...[ - // Version desktop - champs sur la même ligne - Row( - children: [ - Expanded( - child: TextField( - controller: _searchImeiController, - decoration: InputDecoration( - labelText: 'IMEI', - prefixIcon: const Icon(Icons.phone_android), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: _searchReferenceController, - decoration: InputDecoration( - labelText: 'Référence', - prefixIcon: const Icon(Icons.qr_code), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - ), - ], - ), - ] else ...[ - // Version mobile - champs empilés - TextField( - controller: _searchImeiController, - decoration: InputDecoration( - labelText: 'IMEI', - prefixIcon: const Icon(Icons.phone_android), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - const SizedBox(height: 12), - TextField( - controller: _searchReferenceController, - decoration: InputDecoration( - labelText: 'Référence', - prefixIcon: const Icon(Icons.qr_code), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - ], - const SizedBox(height: 16), - - // Boutons de filtre adaptés pour mobile - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - ElevatedButton.icon( - onPressed: _toggleStockFilter, - icon: Icon( - _showOnlyInStock ? Icons.inventory : Icons.inventory_2, - size: 20, - ), - label: Text(_showOnlyInStock - ? isMobile ? 'Tous' : 'Afficher tous' - : isMobile ? 'En stock' : 'Stock disponible'), - style: ElevatedButton.styleFrom( - backgroundColor: _showOnlyInStock - ? Colors.green.shade600 - : Colors.blue.shade600, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 16, - vertical: 8 - ), - ), - ), - ], - ), - - const SizedBox(height: 8), - - // Compteur de résultats - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8 - ), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${_filteredProducts.length} produit(s)', - style: TextStyle( - color: Colors.blue.shade700, - fontWeight: FontWeight.w600, - fontSize: isMobile ? 12 : 14, - ), - ), - ), - ], - ), - ), - ); - } - -// Variables pour le scanner - QRViewController? _qrController; - bool _isScanning = false; - final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); - - // 4. Méthode pour démarrer le scan - void _startBarcodeScanning() { - if (_isScanning) return; - - setState(() { - _isScanning = true; - }); - - Get.to(() => _buildScannerPage())?.then((_) { - setState(() { - _isScanning = false; - }); - }); - } - - // 5. Page du scanner - Widget _buildScannerPage() { - return Scaffold( - appBar: AppBar( - title: const Text('Scanner IMEI'), - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _qrController?.dispose(); - Get.back(); - }, - ), - actions: [ - IconButton( - icon: const Icon(Icons.flash_on), - onPressed: () async { - await _qrController?.toggleFlash(); - }, - ), - IconButton( - icon: const Icon(Icons.flip_camera_ios), - onPressed: () async { - await _qrController?.flipCamera(); - }, - ), - ], - ), - body: Stack( - children: [ - // Scanner view - QRView( - key: _qrKey, - onQRViewCreated: _onQRViewCreated, - overlay: QrScannerOverlayShape( - borderColor: Colors.green, - borderRadius: 10, - borderLength: 30, - borderWidth: 10, - cutOutSize: 250, - ), - ), - - // Instructions overlay - Positioned( - bottom: 100, - left: 20, - right: 20, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(12), - ), - child: const Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.qr_code_scanner, color: Colors.white, size: 40), - SizedBox(height: 8), - Text( - 'Pointez la caméra vers le code-barres IMEI', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - Text( - 'Le scan se fait automatiquement', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ], - ), - ); - } - - // 6. Configuration du contrôleur QR - void _onQRViewCreated(QRViewController controller) { - _qrController = controller; - - controller.scannedDataStream.listen((scanData) { - if (scanData.code != null && scanData.code!.isNotEmpty) { - // Pauser le scanner pour éviter les scans multiples - controller.pauseCamera(); - - // Fermer la page du scanner - Get.back(); - - // Traiter le résultat - _findAndAddProductByImei(scanData.code!); - } - }); - } - - // 7. Méthode pour trouver et ajouter un produit par IMEI - Future _findAndAddProductByImei(String scannedImei) async { - try { - // Montrer un indicateur de chargement - Get.dialog( - AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(color: Colors.green.shade700), - const SizedBox(height: 16), - const Text('Recherche du produit...'), - const SizedBox(height: 8), - Text( - 'IMEI: $scannedImei', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - fontFamily: 'monospace', - ), - ), - ], - ), - ), - barrierDismissible: false, - ); - - // Attendre un court instant pour l'effet visuel - await Future.delayed(const Duration(milliseconds: 300)); - - // Chercher le produit avec l'IMEI scanné - Product? foundProduct; - - for (var product in _products) { - if (product.imei?.toLowerCase().trim() == scannedImei.toLowerCase().trim()) { - foundProduct = product; - break; - } - } - - // Fermer l'indicateur de chargement - Get.back(); - - if (foundProduct == null) { - _showProductNotFoundDialog(scannedImei); - return; - } - - // Vérifier le stock - if (foundProduct.stock != null && foundProduct.stock! <= 0) { - Get.snackbar( - 'Stock insuffisant', - 'Le produit "${foundProduct.name}" n\'est plus en stock', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange.shade600, - colorText: Colors.white, - duration: const Duration(seconds: 3), - icon: const Icon(Icons.warning_amber, color: Colors.white), - ); - return; - } - - // Vérifier si le produit peut être ajouté (stock disponible) - final currentQuantity = _quantites[foundProduct.id] ?? 0; - if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) { - Get.snackbar( - 'Stock limite atteint', - 'Quantité maximum atteinte pour "${foundProduct.name}"', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange.shade600, - colorText: Colors.white, - duration: const Duration(seconds: 3), - icon: const Icon(Icons.warning_amber, color: Colors.white), - ); - return; - } - - // Ajouter le produit au panier - setState(() { - _quantites[foundProduct!.id!] = currentQuantity + 1; - }); - - // Afficher le dialogue de succès - _showSuccessDialog(foundProduct, currentQuantity + 1); - - } catch (e) { - // Fermer l'indicateur de chargement si il est encore ouvert - if (Get.isDialogOpen!) Get.back(); - - Get.snackbar( - 'Erreur', - 'Une erreur est survenue: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red.shade600, - colorText: Colors.white, - duration: const Duration(seconds: 3), - ); - } - } - - // 8. Dialogue de succès - void _showSuccessDialog(Product product, int newQuantity) { - Get.dialog( - AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.check_circle, color: Colors.green.shade700), - ), - const SizedBox(width: 12), - const Expanded(child: Text('Produit ajouté !')), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text('Prix: ${product.price.toStringAsFixed(2)} MGA'), - Text('Quantité dans le panier: $newQuantity'), - if (product.stock != null) - Text('Stock restant: ${product.stock! - newQuantity}'), - ], - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Continuer'), - ), - ElevatedButton( - onPressed: () { - Get.back(); - _showCartBottomSheet(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, - ), - child: const Text('Voir le panier'), - ), - ], - ), - ); - } - - // 9. Dialogue produit non trouvé - void _showProductNotFoundDialog(String scannedImei) { - Get.dialog( - AlertDialog( - title: Row( - children: [ - Icon(Icons.search_off, color: Colors.red.shade600), - const SizedBox(width: 8), - const Text('Produit non trouvé'), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Aucun produit trouvé avec cet IMEI:'), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - scannedImei, - style: const TextStyle( - fontFamily: 'monospace', - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 12), - Text( - 'Vérifiez que l\'IMEI est correct ou que le produit existe dans la base de données.', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Fermer'), - ), - ElevatedButton( - onPressed: () { - Get.back(); - _startBarcodeScanning(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, - ), - child: const Text('Scanner à nouveau'), - ), - ], - ), - ); - } - - - Widget _buildScanInfoCard() { - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.qr_code_scanner, - color: Colors.green.shade700, - size: 20, - ), - ), - const SizedBox(width: 12), - const Expanded( - child: Text( - 'Scanner rapidement un produit via son IMEI pour l\'ajouter au panier', - style: TextStyle( - fontSize: 14, - color: Color.fromARGB(255, 9, 56, 95), - ), - ), - ), - ElevatedButton.icon( - onPressed: _isScanning ? null : _startBarcodeScanning, - icon: _isScanning - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.qr_code_scanner, size: 18), - label: Text(_isScanning ? 'Scan...' : 'Scanner'), - style: ElevatedButton.styleFrom( - backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ), - ], - ), - ), - ); - } - - // 10. Modifier le Widget build pour ajouter le bouton de scan - @override - Widget build(BuildContext context) { - final isMobile = MediaQuery.of(context).size.width < 600; - - return Scaffold( - floatingActionButton: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - // Bouton de scan - FloatingActionButton( - heroTag: "scan", - onPressed: _isScanning ? null : _startBarcodeScanning, - backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, - foregroundColor: Colors.white, - child: _isScanning - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.qr_code_scanner), - ), - const SizedBox(height: 10), - // Bouton panier existant - _buildFloatingCartButton(), - ], - ), - appBar: CustomAppBar(title: 'Nouvelle commande'), - drawer: CustomDrawer(), - body: GestureDetector( - onTap: _hideAllSuggestions, - child: Column( - children: [ - // Section d'information sur le scan (desktop) - if (!isMobile) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: _buildScanInfoCard(), - ), - - // Section des filtres - if (!isMobile) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: _buildFilterSection(), - ), - - // Boutons pour mobile - if (isMobile) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Row( - children: [ - Expanded( - flex: 2, - child: ElevatedButton.icon( - icon: const Icon(Icons.filter_alt), - label: const Text('Filtres'), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => SingleChildScrollView( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: _buildFilterSection(), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - flex: 1, - child: ElevatedButton.icon( - icon: _isScanning - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.qr_code_scanner), - label: Text(_isScanning ? 'Scan...' : 'Scan'), - onPressed: _isScanning ? null : _startBarcodeScanning, - style: ElevatedButton.styleFrom( - backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - ], - ), - ), - // Compteur de résultats - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${_filteredProducts.length} produit(s)', - style: TextStyle( - color: Colors.blue.shade700, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - - // Liste des produits - Expanded( - child: _buildProductList(), - ), - ], - ), - ), - ); - } - - - Widget _buildSuggestionsList({required bool isNom}) { - if (_clientSuggestions.isEmpty) return const SizedBox(); - - return Container( - margin: const EdgeInsets.only(top: 4), - constraints: const BoxConstraints(maxHeight: 150), - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: _clientSuggestions.length, - itemBuilder: (context, index) { - final client = _clientSuggestions[index]; - return ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: Colors.blue.shade100, - child: Icon( - Icons.person, - size: 16, - color: Colors.blue.shade700, - ), - ), - title: Text( - '${client.nom} ${client.prenom}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - '${client.telephone} • ${client.email}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - onTap: () { - _fillClientForm(client); - _hideAllSuggestions(); - }, - ); - }, - ), - ); -} - - Widget _buildFloatingCartButton() { - final isMobile = MediaQuery.of(context).size.width < 600; - final cartItemCount = _quantites.values.where((q) => q > 0).length; - - return FloatingActionButton.extended( - onPressed: () { - _showCartBottomSheet(); - }, - icon: const Icon(Icons.shopping_cart), - label: Text( - isMobile ? 'Panier ($cartItemCount)' : 'Panier ($cartItemCount)', - style: TextStyle(fontSize: isMobile ? 12 : 14), - ), - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - ); - } - - void _showClientFormDialog() { - final isMobile = MediaQuery.of(context).size.width < 600; - - // Variables locales pour les suggestions dans le dialog - bool showNomSuggestions = false; - bool showPrenomSuggestions = false; - bool showEmailSuggestions = false; - bool showTelephoneSuggestions = false; - List localClientSuggestions = []; - - // GlobalKeys pour positionner les overlays - final GlobalKey nomFieldKey = GlobalKey(); - final GlobalKey prenomFieldKey = GlobalKey(); - final GlobalKey emailFieldKey = GlobalKey(); - final GlobalKey telephoneFieldKey = GlobalKey(); - - Get.dialog( - StatefulBuilder( - builder: (context, setDialogState) { - return Stack( - children: [ - AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.person_add, color: Colors.blue.shade700), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - isMobile ? 'Client' : 'Informations Client', - style: TextStyle(fontSize: isMobile ? 16 : 18), - ), - ), - ], - ), - content: Container( - width: isMobile ? double.maxFinite : 600, - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, - ), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Champ Nom avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: nomFieldKey, - controller: _nomController, - label: 'Nom', - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un nom' : null, - onChanged: (value) async { - if (value.length >= 2) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showNomSuggestions = suggestions.isNotEmpty; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - }); - } else { - setDialogState(() { - showNomSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - // Champ Prénom avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: prenomFieldKey, - controller: _prenomController, - label: 'Prénom', - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un prénom' : null, - onChanged: (value) async { - if (value.length >= 2) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showPrenomSuggestions = suggestions.isNotEmpty; - showNomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - }); - } else { - setDialogState(() { - showPrenomSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - // Champ Email avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: emailFieldKey, - controller: _emailController, - label: 'Email', - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value?.isEmpty ?? true) return 'Veuillez entrer un email'; - if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) { - return 'Email invalide'; - } - return null; - }, - onChanged: (value) async { - if (value.length >= 3) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showEmailSuggestions = suggestions.isNotEmpty; - showNomSuggestions = false; - showPrenomSuggestions = false; - showTelephoneSuggestions = false; - }); - } else { - setDialogState(() { - showEmailSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - // Champ Téléphone avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: telephoneFieldKey, - controller: _telephoneController, - label: 'Téléphone', - keyboardType: TextInputType.phone, - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un téléphone' : null, - onChanged: (value) async { - if (value.length >= 3) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showTelephoneSuggestions = suggestions.isNotEmpty; - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - }); - } else { - setDialogState(() { - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - _buildTextFormField( - controller: _adresseController, - label: 'Adresse', - maxLines: 2, - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer une adresse' : null, - ), - const SizedBox(height: 12), - _buildCommercialDropdown(), - ], - ), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 16 : 20, - vertical: isMobile ? 10 : 12 - ), - ), - onPressed: () { - if (_formKey.currentState!.validate()) { - // Fermer toutes les suggestions avant de soumettre - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - Get.back(); - _submitOrder(); - } - }, - child: Text( - isMobile ? 'Valider' : 'Valider la commande', - style: TextStyle(fontSize: isMobile ? 12 : 14), - ), - ), - ], - ), - - // Overlay pour les suggestions du nom - if (showNomSuggestions) - _buildSuggestionOverlay( - fieldKey: nomFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showNomSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions du prénom - if (showPrenomSuggestions) - _buildSuggestionOverlay( - fieldKey: prenomFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showPrenomSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions de l'email - if (showEmailSuggestions) - _buildSuggestionOverlay( - fieldKey: emailFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showEmailSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions du téléphone - if (showTelephoneSuggestions) - _buildSuggestionOverlay( - fieldKey: telephoneFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - ], - ); - }, - ), - ); -} - -// Widget pour créer un TextFormField avec une clé -Widget _buildTextFormFieldWithKey({ - required GlobalKey key, - required TextEditingController controller, - required String label, - TextInputType? keyboardType, - int maxLines = 1, - String? Function(String?)? validator, - void Function(String)? onChanged, -}) { - return Container( - key: key, - child: _buildTextFormField( - controller: controller, - label: label, - keyboardType: keyboardType, - maxLines: maxLines, - validator: validator, - onChanged: onChanged, - ), - ); -} - -// Widget pour l'overlay des suggestions -Widget _buildSuggestionOverlay({ - required GlobalKey fieldKey, - required List suggestions, - required Function(Client) onClientSelected, - required VoidCallback onDismiss, -}) { - return Positioned.fill( - child: GestureDetector( - onTap: onDismiss, - child: Material( - color: Colors.transparent, - child: Builder( - builder: (context) { - // Obtenir la position du champ - final RenderBox? renderBox = fieldKey.currentContext?.findRenderObject() as RenderBox?; - if (renderBox == null) return const SizedBox(); - - final position = renderBox.localToGlobal(Offset.zero); - final size = renderBox.size; - - return Stack( - children: [ - Positioned( - left: position.dx, - top: position.dy + size.height + 4, - width: size.width, - child: GestureDetector( - onTap: () {}, // Empêcher la fermeture au tap sur la liste - child: Container( - constraints: const BoxConstraints( - maxHeight: 200, // Hauteur maximum pour la scrollabilité - ), - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Scrollbar( - thumbVisibility: suggestions.length > 3, - child: ListView.separated( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: suggestions.length, - separatorBuilder: (context, index) => Divider( - height: 1, - color: Colors.grey.shade200, - ), - itemBuilder: (context, index) { - final client = suggestions[index]; - return ListTile( - dense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - leading: CircleAvatar( - radius: 16, - backgroundColor: Colors.blue.shade100, - child: Icon( - Icons.person, - size: 16, - color: Colors.blue.shade700, - ), - ), - title: Text( - '${client.nom} ${client.prenom}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - '${client.telephone} • ${client.email}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - onTap: () => onClientSelected(client), - hoverColor: Colors.blue.shade50, - ); - }, - ), - ), - ), - ), - ), - ), - ], - ); - }, - ), - ), - ), - ); -} - -// Méthode pour remplir le formulaire avec les données du client -void _fillFormWithClient(Client client) { - _nomController.text = client.nom; - _prenomController.text = client.prenom; - _emailController.text = client.email; - _telephoneController.text = client.telephone; - _adresseController.text = client.adresse ?? ''; - - Get.snackbar( - 'Client trouvé', - 'Les informations ont été remplies automatiquement', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); -} - - Widget _buildTextFormField({ - required TextEditingController controller, - required String label, - TextInputType? keyboardType, - String? Function(String?)? validator, - int? maxLines, - void Function(String)? onChanged, - }) { - return TextFormField( - controller: controller, - decoration: InputDecoration( - labelText: label, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.white, - ), - keyboardType: keyboardType, - validator: validator, - maxLines: maxLines, - onChanged: onChanged, - ); - } - - Widget _buildCommercialDropdown() { - return DropdownButtonFormField( - value: _selectedCommercialUser, - decoration: InputDecoration( - labelText: 'Commercial', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.white, - ), - items: _commercialUsers.map((Users user) { - return DropdownMenuItem( - value: user, - child: Text('${user.name} ${user.lastName}'), - ); - }).toList(), - onChanged: (Users? newValue) { - setState(() { - _selectedCommercialUser = newValue; - }); - }, - validator: (value) => value == null ? 'Veuillez sélectionner un commercial' : null, - ); - } - - Widget _buildProductList() { - final isMobile = MediaQuery.of(context).size.width < 600; - - return _filteredProducts.isEmpty - ? _buildEmptyState() - : ListView.builder( - padding: const EdgeInsets.all(16.0), - itemCount: _filteredProducts.length, - itemBuilder: (context, index) { - final product = _filteredProducts[index]; - final quantity = _quantites[product.id] ?? 0; - - return _buildProductListItem(product, quantity, isMobile); - }, - ); - } - - Widget _buildEmptyState() { - return Center( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - children: [ - Icon( - Icons.search_off, - size: 64, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'Aucun produit trouvé', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Colors.grey.shade600, - ), - ), - const SizedBox(height: 8), - Text( - 'Modifiez vos critères de recherche', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade500, - ), - ), - ], - ), - ), - ); - } - - Widget _buildProductListItem(Product product, int quantity, bool isMobile) { - final bool isOutOfStock = product.stock != null && product.stock! <= 0; - - return Card( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: isOutOfStock - ? Border.all(color: Colors.red.shade200, width: 1.5) - : null, - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - Container( - width: isMobile ? 40 : 50, - height: isMobile ? 40 : 50, - decoration: BoxDecoration( - color: isOutOfStock - ? Colors.red.shade50 - : Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.shopping_bag, - size: isMobile ? 20 : 24, - color: isOutOfStock ? Colors.red : Colors.blue, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: isMobile ? 14 : 16, - color: isOutOfStock ? Colors.red.shade700 : null, - ), - ), - const SizedBox(height: 4), - Text( - '${product.price.toStringAsFixed(2)} MGA', - style: TextStyle( - color: Colors.green.shade700, - fontWeight: FontWeight.w600, - fontSize: isMobile ? 12 : 14, - ), - ), - if (product.stock != null) - Text( - 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}', - style: TextStyle( - fontSize: isMobile ? 10 : 12, - color: isOutOfStock - ? Colors.red.shade600 - : Colors.grey.shade600, - fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal, - ), - ), - // Affichage IMEI et Référence - plus compact sur mobile - if (product.imei != null && product.imei!.isNotEmpty) - Text( - 'IMEI: ${product.imei}', - style: TextStyle( - fontSize: isMobile ? 9 : 11, - color: Colors.grey.shade600, - fontFamily: 'monospace', - ), - ), - if (product.reference != null && product.reference!.isNotEmpty) - Text( - 'Réf: ${product.reference}', - style: TextStyle( - fontSize: isMobile ? 9 : 11, - color: Colors.grey.shade600, - ), - ), - ], - ), - ), - Container( - decoration: BoxDecoration( - color: isOutOfStock - ? Colors.grey.shade100 - : Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon( - Icons.remove, - size: isMobile ? 16 : 18 - ), - onPressed: isOutOfStock ? null : () { - if (quantity > 0) { - setState(() { - _quantites[product.id!] = quantity - 1; - }); - } - }, - ), - Text( - quantity.toString(), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: isMobile ? 12 : 14, - ), - ), - IconButton( - icon: Icon( - Icons.add, - size: isMobile ? 16 : 18 - ), - onPressed: isOutOfStock ? null : () { - if (product.stock == null || quantity < product.stock!) { - setState(() { - _quantites[product.id!] = quantity + 1; - }); - } else { - Get.snackbar( - 'Stock insuffisant', - 'Quantité demandée non disponible', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } - - void _showCartBottomSheet() { - final isMobile = MediaQuery.of(context).size.width < 600; - - Get.bottomSheet( - Container( - height: MediaQuery.of(context).size.height * (isMobile ? 0.85 : 0.7), - padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Votre Panier', - style: TextStyle( - fontSize: isMobile ? 18 : 20, - fontWeight: FontWeight.bold - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Get.back(), - ), - ], - ), - const Divider(), - Expanded(child: _buildCartItemsList()), - const Divider(), - _buildCartTotalSection(), - const SizedBox(height: 16), - _buildSubmitButton(), - ], - ), - ), - isScrollControlled: true, - ); - } - - Widget _buildCartItemsList() { - final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); - - if (itemsInCart.isEmpty) { - return const Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.shopping_cart_outlined, size: 60, color: Colors.grey), - SizedBox(height: 16), - Text( - 'Votre panier est vide', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - ], - ), - ); - } - - return ListView.builder( - itemCount: itemsInCart.length, - itemBuilder: (context, index) { - final entry = itemsInCart[index]; - final product = _products.firstWhere((p) => p.id == entry.key); - - return Dismissible( - key: Key(entry.key.toString()), - background: Container( - color: Colors.red.shade100, - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 20), - child: const Icon(Icons.delete, color: Colors.red), - ), - direction: DismissDirection.endToStart, - onDismissed: (direction) { - setState(() { - _quantites.remove(entry.key); - }); - Get.snackbar( - 'Produit retiré', - '${product.name} a été retiré du panier', - snackPosition: SnackPosition.BOTTOM, - ); - }, - child: Card( - margin: const EdgeInsets.only(bottom: 8), - elevation: 1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.shopping_bag, size: 20), - ), - title: Text(product.name), - subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} MGA'), - trailing: Text( - '${(entry.value * product.price).toStringAsFixed(2)} MGA', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - ), - ), - ), - ); - }, - ); - } - - Widget _buildCartTotalSection() { - double total = 0; - _quantites.forEach((productId, quantity) { - final product = _products.firstWhere((p) => p.id == productId); - total += quantity * product.price; - }); - - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Total:', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Text( - '${total.toStringAsFixed(2)} MGA', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.green, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - '${_quantites.values.where((q) => q > 0).length} article(s)', - style: TextStyle(color: Colors.grey.shade600), - ), - ], - ); - } - - Widget _buildSubmitButton() { - final isMobile = MediaQuery.of(context).size.width < 600; - - return SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric( - vertical: isMobile ? 12 : 16 - ), - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 4, - ), - onPressed: _submitOrder, - child: _isLoading - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : Text( - isMobile ? 'Valider' : 'Valider la Commande', - style: TextStyle(fontSize: isMobile ? 14 : 16), - ), - ), - ); - } - - Future _submitOrder() async { - // Vérifier d'abord si le panier est vide - final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); - if (itemsInCart.isEmpty) { - Get.snackbar( - 'Panier vide', - 'Veuillez ajouter des produits à votre commande', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - _showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide - return; - } - - // Ensuite vérifier les informations client - if (_nomController.text.isEmpty || - _prenomController.text.isEmpty || - _emailController.text.isEmpty || - _telephoneController.text.isEmpty || - _adresseController.text.isEmpty) { - Get.snackbar( - 'Informations manquantes', - 'Veuillez remplir les informations client', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - _showClientFormDialog(); - return; - } - - setState(() { - _isLoading = true; - }); - - // Créer le client - final client = Client( - nom: _nomController.text, - prenom: _prenomController.text, - email: _emailController.text, - telephone: _telephoneController.text, - adresse: _adresseController.text, - dateCreation: DateTime.now(), - ); - - // Calculer le total et préparer les détails - double total = 0; - final details = []; - - for (final entry in itemsInCart) { - final product = _products.firstWhere((p) => p.id == entry.key); - total += entry.value * product.price; - - details.add(DetailCommande( - commandeId: 0, - produitId: product.id!, - quantite: entry.value, - prixUnitaire: product.price, - sousTotal: entry.value * product.price, - )); - } - - // Créer la commande - final commande = Commande( - clientId: 0, - dateCommande: DateTime.now(), - statut: StatutCommande.enAttente, - montantTotal: total, - notes: 'Commande passée via l\'application', - commandeurId: _selectedCommercialUser?.id, - ); - - try { - await _appDatabase.createCommandeComplete(client, commande, details); - - // Fermer le panier avant d'afficher la confirmation - Get.back(); - - // Afficher le dialogue de confirmation - adapté pour mobile - final isMobile = MediaQuery.of(context).size.width < 600; - - await showDialog( - context: context, - barrierDismissible: false, // Empêcher la fermeture accidentelle - builder: (context) => AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.check_circle, color: Colors.green.shade700), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Commande Validée', - style: TextStyle(fontSize: isMobile ? 16 : 18), - ), - ), - ], - ), - content: Text( - 'Votre commande a été enregistrée et expédiée avec succès.', - style: TextStyle(fontSize: isMobile ? 14 : 16), - ), - actions: [ - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - vertical: isMobile ? 12 : 16 - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: () { - Navigator.pop(context); - // Vider complètement le formulaire et le panier - _clearFormAndCart(); - // Recharger les produits pour mettre à jour le stock - _loadProducts(); - }, - child: Text( - 'OK', - style: TextStyle(fontSize: isMobile ? 14 : 16), - ), - ), - ), - ], - ), - ); - - } catch (e) { - setState(() { - _isLoading = false; - }); - - Get.snackbar( - 'Erreur', - 'Une erreur est survenue: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - } - - @override - void dispose() { - _qrController?.dispose(); - - // Vos disposals existants... - _hideAllSuggestions(); - _nomController.dispose(); - _prenomController.dispose(); - _emailController.dispose(); - _telephoneController.dispose(); - _adresseController.dispose(); - _searchNameController.dispose(); - _searchImeiController.dispose(); - _searchReferenceController.dispose(); - - super.dispose(); - } - } \ No newline at end of file diff --git a/lib/Models/Client.dart b/lib/Models/Client.dart index 728d162..19b96ab 100644 --- a/lib/Models/Client.dart +++ b/lib/Models/Client.dart @@ -92,9 +92,6 @@ class Commande { final String? clientNom; final String? clientPrenom; final String? clientEmail; - final double? remisePourcentage; - final double? remiseMontant; - final double? montantApresRemise; Commande({ this.id, @@ -109,9 +106,6 @@ class Commande { this.clientNom, this.clientPrenom, this.clientEmail, - this.remisePourcentage, - this.remiseMontant, - this.montantApresRemise, }); String get clientNomComplet { @@ -143,9 +137,6 @@ class Commande { 'dateLivraison': dateLivraison?.toIso8601String(), 'commandeurId': commandeurId, 'validateurId': validateurId, - 'remisePourcentage': remisePourcentage, - 'remiseMontant': remiseMontant, - 'montantApresRemise': montantApresRemise, }; } @@ -165,56 +156,15 @@ class Commande { clientNom: map['clientNom'] as String?, clientPrenom: map['clientPrenom'] as String?, clientEmail: map['clientEmail'] as String?, - remisePourcentage: map['remisePourcentage'] != null - ? (map['remisePourcentage'] as num).toDouble() - : null, - remiseMontant: map['remiseMontant'] != null - ? (map['remiseMontant'] as num).toDouble() - : null, - montantApresRemise: map['montantApresRemise'] != null - ? (map['montantApresRemise'] as num).toDouble() - : null, - ); - } - - Commande copyWith({ - int? id, - int? clientId, - DateTime? dateCommande, - StatutCommande? statut, - double? montantTotal, - String? notes, - DateTime? dateLivraison, - int? commandeurId, - int? validateurId, - String? clientNom, - String? clientPrenom, - String? clientEmail, - double? remisePourcentage, - double? remiseMontant, - double? montantApresRemise, - }) { - return Commande( - id: id ?? this.id, - clientId: clientId ?? this.clientId, - dateCommande: dateCommande ?? this.dateCommande, - statut: statut ?? this.statut, - montantTotal: montantTotal ?? this.montantTotal, - notes: notes ?? this.notes, - dateLivraison: dateLivraison ?? this.dateLivraison, - commandeurId: commandeurId ?? this.commandeurId, - validateurId: validateurId ?? this.validateurId, - clientNom: clientNom ?? this.clientNom, - clientPrenom: clientPrenom ?? this.clientPrenom, - clientEmail: clientEmail ?? this.clientEmail, - remisePourcentage: remisePourcentage ?? this.remisePourcentage, - remiseMontant: remiseMontant ?? this.remiseMontant, - montantApresRemise: montantApresRemise ?? this.montantApresRemise, ); } } // REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci : +enum RemiseType { + pourcentage, + montant +} class DetailCommande { final int? id; @@ -222,16 +172,15 @@ class DetailCommande { final int produitId; final int quantite; final double prixUnitaire; - final double sousTotal; + final double sousTotal; // Prix unitaire × quantité (avant remise) + final RemiseType? remiseType; + final double remiseValeur; // Valeur de la remise (% ou montant) + final double montantRemise; // Montant de la remise calculé + final double prixFinal; // Prix final après remise + final bool estCadeau; // NOUVEAU : Indique si l'article est un cadeau final String? produitNom; final String? produitImage; final String? produitReference; - final bool? estCadeau; - - // NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT - final double? remisePourcentage; - final double? remiseMontant; - final double? prixApresRemise; DetailCommande({ this.id, @@ -240,15 +189,195 @@ class DetailCommande { required this.quantite, required this.prixUnitaire, required this.sousTotal, + this.remiseType, + this.remiseValeur = 0.0, + this.montantRemise = 0.0, + required this.prixFinal, + this.estCadeau = false, this.produitNom, this.produitImage, this.produitReference, - this.estCadeau, - this.remisePourcentage, - this.remiseMontant, - this.prixApresRemise, }); + // Constructeur pour créer un détail sans remise + factory DetailCommande.sansRemise({ + int? id, + required int commandeId, + required int produitId, + required int quantite, + required double prixUnitaire, + bool estCadeau = false, + String? produitNom, + String? produitImage, + String? produitReference, + }) { + final sousTotal = quantite * prixUnitaire; + final prixFinal = estCadeau ? 0.0 : sousTotal; + + return DetailCommande( + id: id, + commandeId: commandeId, + produitId: produitId, + quantite: quantite, + prixUnitaire: prixUnitaire, + sousTotal: sousTotal, + prixFinal: prixFinal, + estCadeau: estCadeau, + produitNom: produitNom, + produitImage: produitImage, + produitReference: produitReference, + ); + } + + // NOUVEAU : Constructeur pour créer un cadeau + factory DetailCommande.cadeau({ + int? id, + required int commandeId, + required int produitId, + required int quantite, + required double prixUnitaire, + String? produitNom, + String? produitImage, + String? produitReference, + }) { + return DetailCommande( + id: id, + commandeId: commandeId, + produitId: produitId, + quantite: quantite, + prixUnitaire: prixUnitaire, + sousTotal: quantite * prixUnitaire, + prixFinal: 0.0, // Prix final à 0 pour un cadeau + estCadeau: true, + produitNom: produitNom, + produitImage: produitImage, + produitReference: produitReference, + ); + } + + // Méthode pour appliquer une remise (ne s'applique pas aux cadeaux) + DetailCommande appliquerRemise({ + required RemiseType type, + required double valeur, + }) { + // Les remises ne s'appliquent pas aux cadeaux + if (estCadeau) return this; + + double montantRemiseCalcule = 0.0; + + if (type == RemiseType.pourcentage) { + final pourcentage = valeur.clamp(0.0, 100.0); + montantRemiseCalcule = sousTotal * (pourcentage / 100); + } else { + montantRemiseCalcule = valeur.clamp(0.0, sousTotal); + } + + final prixFinalCalcule = sousTotal - montantRemiseCalcule; + + return DetailCommande( + id: id, + commandeId: commandeId, + produitId: produitId, + quantite: quantite, + prixUnitaire: prixUnitaire, + sousTotal: sousTotal, + remiseType: type, + remiseValeur: valeur, + montantRemise: montantRemiseCalcule, + prixFinal: prixFinalCalcule, + estCadeau: estCadeau, + produitNom: produitNom, + produitImage: produitImage, + produitReference: produitReference, + ); + } + + // Méthode pour supprimer la remise + DetailCommande supprimerRemise() { + return DetailCommande( + id: id, + commandeId: commandeId, + produitId: produitId, + quantite: quantite, + prixUnitaire: prixUnitaire, + sousTotal: sousTotal, + remiseType: null, + remiseValeur: 0.0, + montantRemise: 0.0, + prixFinal: estCadeau ? 0.0 : sousTotal, + estCadeau: estCadeau, + produitNom: produitNom, + produitImage: produitImage, + produitReference: produitReference, + ); + } + + // NOUVEAU : Méthode pour convertir en cadeau + DetailCommande convertirEnCadeau() { + return DetailCommande( + id: id, + commandeId: commandeId, + produitId: produitId, + quantite: quantite, + prixUnitaire: prixUnitaire, + sousTotal: sousTotal, + remiseType: null, // Supprimer les remises lors de la conversion en cadeau + remiseValeur: 0.0, + montantRemise: 0.0, + prixFinal: 0.0, + estCadeau: true, + produitNom: produitNom, + produitImage: produitImage, + produitReference: produitReference, + ); + } + + // NOUVEAU : Méthode pour convertir en article normal + DetailCommande convertirEnArticleNormal() { + return DetailCommande( + id: id, + commandeId: commandeId, + produitId: produitId, + quantite: quantite, + prixUnitaire: prixUnitaire, + sousTotal: sousTotal, + remiseType: remiseType, + remiseValeur: remiseValeur, + montantRemise: montantRemise, + prixFinal: estCadeau ? sousTotal - montantRemise : prixFinal, + estCadeau: false, + produitNom: produitNom, + produitImage: produitImage, + produitReference: produitReference, + ); + } + + // Getters utiles + bool get aRemise => remiseType != null && montantRemise > 0 && !estCadeau; + + double get pourcentageRemise { + if (!aRemise) return 0.0; + return (montantRemise / sousTotal) * 100; + } + + String get remiseDescription { + if (estCadeau) return 'CADEAU'; + if (!aRemise) return ''; + + if (remiseType == RemiseType.pourcentage) { + return '-${remiseValeur.toStringAsFixed(0)}%'; + } else { + return '-${montantRemise.toStringAsFixed(2)} MGA'; + } + } + + // NOUVEAU : Description du statut de l'article + String get statutDescription { + if (estCadeau) return 'CADEAU OFFERT'; + if (aRemise) return 'AVEC REMISE'; + return 'PRIX NORMAL'; + } + Map toMap() { return { 'id': id, @@ -257,14 +386,24 @@ class DetailCommande { 'quantite': quantite, 'prixUnitaire': prixUnitaire, 'sousTotal': sousTotal, - 'estCadeau': estCadeau == true ? 1 : 0, - 'remisePourcentage': remisePourcentage, - 'remiseMontant': remiseMontant, - 'prixApresRemise': prixApresRemise, + 'remise_type': remiseType?.name, + 'remise_valeur': remiseValeur, + 'montant_remise': montantRemise, + 'prix_final': prixFinal, + 'est_cadeau': estCadeau ? 1 : 0, }; } factory DetailCommande.fromMap(Map map) { + RemiseType? type; + if (map['remise_type'] != null) { + if (map['remise_type'] == 'pourcentage') { + type = RemiseType.pourcentage; + } else if (map['remise_type'] == 'montant') { + type = RemiseType.montant; + } + } + return DetailCommande( id: map['id'] as int?, commandeId: map['commandeId'] as int, @@ -272,71 +411,15 @@ class DetailCommande { quantite: map['quantite'] as int, prixUnitaire: (map['prixUnitaire'] as num).toDouble(), sousTotal: (map['sousTotal'] as num).toDouble(), + remiseType: type, + remiseValeur: (map['remise_valeur'] as num?)?.toDouble() ?? 0.0, + montantRemise: (map['montant_remise'] as num?)?.toDouble() ?? 0.0, + prixFinal: (map['prix_final'] as num?)?.toDouble() ?? + (map['sousTotal'] as num).toDouble(), + estCadeau: (map['est_cadeau'] as int?) == 1, produitNom: map['produitNom'] as String?, produitImage: map['produitImage'] as String?, produitReference: map['produitReference'] as String?, - estCadeau: map['estCadeau'] == 1, - remisePourcentage: map['remisePourcentage'] != null - ? (map['remisePourcentage'] as num).toDouble() - : null, - remiseMontant: map['remiseMontant'] != null - ? (map['remiseMontant'] as num).toDouble() - : null, - prixApresRemise: map['prixApresRemise'] != null - ? (map['prixApresRemise'] as num).toDouble() - : null, ); } - - DetailCommande copyWith({ - int? id, - int? commandeId, - int? produitId, - int? quantite, - double? prixUnitaire, - double? sousTotal, - String? produitNom, - String? produitImage, - String? produitReference, - bool? estCadeau, - double? remisePourcentage, - double? remiseMontant, - double? prixApresRemise, - }) { - return DetailCommande( - id: id ?? this.id, - commandeId: commandeId ?? this.commandeId, - produitId: produitId ?? this.produitId, - quantite: quantite ?? this.quantite, - prixUnitaire: prixUnitaire ?? this.prixUnitaire, - sousTotal: sousTotal ?? this.sousTotal, - produitNom: produitNom ?? this.produitNom, - produitImage: produitImage ?? this.produitImage, - produitReference: produitReference ?? this.produitReference, - estCadeau: estCadeau ?? this.estCadeau, - remisePourcentage: remisePourcentage ?? this.remisePourcentage, - remiseMontant: remiseMontant ?? this.remiseMontant, - prixApresRemise: prixApresRemise ?? this.prixApresRemise, - ); - } - - // GETTERS QUI RÉSOLVENT LE PROBLÈME "aUneRemise" INTROUVABLE - double get prixFinalUnitaire { - return prixApresRemise ?? prixUnitaire; - } - - double get sousTotalAvecRemise { - return quantite * prixFinalUnitaire; - } - - bool get aUneRemise { - return remisePourcentage != null || remiseMontant != null || prixApresRemise != null; - } - - double get montantRemise { - if (prixApresRemise != null) { - return (prixUnitaire - prixApresRemise!) * quantite; - } - return 0.0; - } } \ No newline at end of file diff --git a/lib/Services/Script.sql b/lib/Services/Script.sql new file mode 100644 index 0000000..84f9bf8 --- /dev/null +++ b/lib/Services/Script.sql @@ -0,0 +1,304 @@ +-- Script SQL pour créer la base de données guycom_database_v1 +-- Création des tables et insertion des données par défaut + +-- ===================================================== +-- CRÉATION DES TABLES +-- ===================================================== + +-- Table permissions +CREATE TABLE `permissions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table menu +CREATE TABLE `menu` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `route` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table roles +CREATE TABLE `roles` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `designation` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `designation` (`designation`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table points_de_vente +CREATE TABLE `points_de_vente` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `nom` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `nom` (`nom`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table clients +CREATE TABLE `clients` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `nom` varchar(255) NOT NULL, + `prenom` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `telephone` varchar(255) NOT NULL, + `adresse` varchar(500) DEFAULT NULL, + `dateCreation` datetime NOT NULL, + `actif` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + KEY `idx_clients_email` (`email`), + KEY `idx_clients_telephone` (`telephone`) +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table users +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `lastname` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, + `username` varchar(255) NOT NULL, + `role_id` int(11) NOT NULL, + `point_de_vente_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `username` (`username`), + KEY `role_id` (`role_id`), + KEY `point_de_vente_id` (`point_de_vente_id`), + CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`), + CONSTRAINT `users_ibfk_2` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table products +CREATE TABLE `products` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `price` decimal(10,2) NOT NULL, + `image` varchar(2000) DEFAULT NULL, + `category` varchar(255) NOT NULL, + `stock` int(11) NOT NULL DEFAULT 0, + `description` varchar(1000) DEFAULT NULL, + `qrCode` varchar(500) DEFAULT NULL, + `reference` varchar(255) DEFAULT NULL, + `point_de_vente_id` int(11) DEFAULT NULL, + `marque` varchar(255) DEFAULT NULL, + `ram` varchar(100) DEFAULT NULL, + `memoire_interne` varchar(100) DEFAULT NULL, + `imei` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `imei` (`imei`), + KEY `point_de_vente_id` (`point_de_vente_id`), + KEY `idx_products_category` (`category`), + KEY `idx_products_reference` (`reference`), + KEY `idx_products_imei` (`imei`), + CONSTRAINT `products_ibfk_1` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=127 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table commandes +CREATE TABLE `commandes` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `clientId` int(11) NOT NULL, + `dateCommande` datetime NOT NULL, + `statut` int(11) NOT NULL DEFAULT 0, + `montantTotal` decimal(10,2) NOT NULL, + `notes` varchar(1000) DEFAULT NULL, + `dateLivraison` datetime DEFAULT NULL, + `commandeurId` int(11) DEFAULT NULL, + `validateurId` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `commandeurId` (`commandeurId`), + KEY `validateurId` (`validateurId`), + KEY `idx_commandes_client` (`clientId`), + KEY `idx_commandes_date` (`dateCommande`), + CONSTRAINT `commandes_ibfk_1` FOREIGN KEY (`commandeurId`) REFERENCES `users` (`id`), + CONSTRAINT `commandes_ibfk_2` FOREIGN KEY (`validateurId`) REFERENCES `users` (`id`), + CONSTRAINT `commandes_ibfk_3` FOREIGN KEY (`clientId`) REFERENCES `clients` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table details_commandes +CREATE TABLE `details_commandes` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `commandeId` int(11) NOT NULL, + `produitId` int(11) NOT NULL, + `quantite` int(11) NOT NULL, + `prixUnitaire` decimal(10,2) NOT NULL, + `sousTotal` decimal(10,2) NOT NULL, + `remise_type` enum('pourcentage','montant') DEFAULT NULL, + `remise_valeur` decimal(10,2) DEFAULT 0.00, + `montant_remise` decimal(10,2) DEFAULT 0.00, + `prix_final` decimal(10,2) NOT NULL DEFAULT 0.00, + `est_cadeau` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `produitId` (`produitId`), + KEY `idx_details_commande` (`commandeId`), + KEY `idx_est_cadeau` (`est_cadeau`), + CONSTRAINT `details_commandes_ibfk_1` FOREIGN KEY (`commandeId`) REFERENCES `commandes` (`id`) ON DELETE CASCADE, + CONSTRAINT `details_commandes_ibfk_2` FOREIGN KEY (`produitId`) REFERENCES `products` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table role_permissions +CREATE TABLE `role_permissions` ( + `role_id` int(11) NOT NULL, + `permission_id` int(11) NOT NULL, + PRIMARY KEY (`role_id`,`permission_id`), + KEY `permission_id` (`permission_id`), + CONSTRAINT `role_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE, + CONSTRAINT `role_permissions_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Table role_menu_permissions +CREATE TABLE `role_menu_permissions` ( + `role_id` int(11) NOT NULL, + `menu_id` int(11) NOT NULL, + `permission_id` int(11) NOT NULL, + PRIMARY KEY (`role_id`,`menu_id`,`permission_id`), + KEY `menu_id` (`menu_id`), + KEY `permission_id` (`permission_id`), + CONSTRAINT `role_menu_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE, + CONSTRAINT `role_menu_permissions_ibfk_2` FOREIGN KEY (`menu_id`) REFERENCES `menu` (`id`) ON DELETE CASCADE, + CONSTRAINT `role_menu_permissions_ibfk_3` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ===================================================== +-- INSERTION DES DONNÉES PAR DÉFAUT +-- ===================================================== + +-- Insertion des permissions par défaut +INSERT INTO `permissions` (`name`) VALUES +('view'), +('create'), +('update'), +('delete'), +('admin'), +('manage'), +('read'); + +-- Insertion des menus par défaut +INSERT INTO `menu` (`name`, `route`) VALUES +('Accueil', '/accueil'), +('Ajouter un utilisateur', '/ajouter-utilisateur'), +('Modifier/Supprimer un utilisateur', '/modifier-utilisateur'), +('Ajouter un produit', '/ajouter-produit'), +('Modifier/Supprimer un produit', '/modifier-produit'), +('Bilan', '/bilan'), +('Gérer les rôles', '/gerer-roles'), +('Gestion de stock', '/gestion-stock'), +('Historique', '/historique'), +('Déconnexion', '/deconnexion'), +('Nouvelle commande', '/nouvelle-commande'), +('Gérer les commandes', '/gerer-commandes'), +('Points de vente', '/points-de-vente'); + +-- Insertion des rôles par défaut +INSERT INTO `roles` (`designation`) VALUES +('Super Admin'), +('Admin'), +('User'), +('commercial'), +('caisse'); + +-- Attribution de TOUTES les permissions à TOUS les menus pour le Super Admin +-- On utilise une sous-requête pour récupérer l'ID réel du rôle Super Admin +INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) +SELECT r.id, m.id, p.id +FROM menu m +CROSS JOIN permissions p +CROSS JOIN roles r +WHERE r.designation = 'Super Admin'; + +-- Attribution de permissions basiques pour Admin +-- Accès en lecture/écriture à la plupart des menus sauf gestion des rôles +INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) +SELECT r.id, m.id, p.id +FROM menu m +CROSS JOIN permissions p +CROSS JOIN roles r +WHERE r.designation = 'Admin' +AND m.name != 'Gérer les rôles' +AND p.name IN ('view', 'create', 'update', 'read'); + +-- Attribution de permissions basiques pour User +-- Accès principalement en lecture et quelques actions de base +INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) +SELECT r.id, m.id, p.id +FROM menu m +CROSS JOIN permissions p +CROSS JOIN roles r +WHERE r.designation = 'User' +AND m.name IN ('Accueil', 'Nouvelle commande', 'Gérer les commandes', 'Gestion de stock', 'Historique') +AND p.name IN ('view', 'read', 'create'); + +-- Attribution de permissions pour Commercial +-- Accès aux commandes, clients, produits +INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) +SELECT r.id, m.id, p.id +FROM menu m +CROSS JOIN permissions p +CROSS JOIN roles r +WHERE r.designation = 'commercial' +AND m.name IN ('Accueil', 'Nouvelle commande', 'Gérer les commandes', 'Bilan', 'Historique') +AND p.name IN ('view', 'create', 'update', 'read'); + +-- Attribution de permissions pour Caisse +-- Accès principalement aux commandes et stock +INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) +SELECT r.id, m.id, p.id +FROM menu m +CROSS JOIN permissions p +CROSS JOIN roles r +WHERE r.designation = 'caisse' +AND m.name IN ('Accueil', 'Nouvelle commande', 'Gestion de stock') +AND p.name IN ('view', 'create', 'read'); + +-- Insertion du Super Admin par défaut +-- On utilise une sous-requête pour récupérer l'ID réel du rôle Super Admin +INSERT INTO `users` (`name`, `lastname`, `email`, `password`, `username`, `role_id`) +SELECT 'Super', 'Admin', 'superadmin@youmazgestion.com', 'admin123', 'superadmin', r.id +FROM roles r +WHERE r.designation = 'Super Admin'; + +-- ===================================================== +-- DONNÉES D'EXEMPLE (OPTIONNEL) +-- ===================================================== + +-- Insertion d'un point de vente d'exemple +INSERT INTO `points_de_vente` (`nom`) VALUES ('Magasin Principal'); + +-- Insertion d'un client d'exemple +INSERT INTO `clients` (`nom`, `prenom`, `email`, `telephone`, `adresse`, `dateCreation`, `actif`) VALUES +('Dupont', 'Jean', 'jean.dupont@email.com', '0123456789', '123 Rue de la Paix, Paris', NOW(), 1); + +-- ===================================================== +-- VÉRIFICATIONS +-- ===================================================== + +-- Afficher les rôles créés +SELECT 'RÔLES CRÉÉS:' as info; +SELECT * FROM roles; + +-- Afficher les permissions créées +SELECT 'PERMISSIONS CRÉÉES:' as info; +SELECT * FROM permissions; + +-- Afficher les menus créés +SELECT 'MENUS CRÉÉS:' as info; +SELECT * FROM menu; + +-- Afficher le Super Admin créé +SELECT 'SUPER ADMIN CRÉÉ:' as info; +SELECT u.username, u.email, r.designation as role +FROM users u +JOIN roles r ON u.role_id = r.id +WHERE r.designation = 'Super Admin'; + +-- Vérifier les permissions du Super Admin +SELECT 'PERMISSIONS SUPER ADMIN:' as info; +SELECT COUNT(*) as total_permissions_assignees +FROM role_menu_permissions rmp +INNER JOIN roles r ON rmp.role_id = r.id +WHERE r.designation = 'Super Admin'; + +SELECT 'Script terminé avec succès!' as resultat; \ No newline at end of file diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index 005bf68..c46dc93 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -37,8 +37,6 @@ class AppDatabase { _connection = await _initDB(); // await _createDB(); - // Effectuer la migration pour les bases existantes - await migrateDatabaseForDiscountAndGift(); await insertDefaultPermissions(); await insertDefaultMenus(); @@ -68,169 +66,7 @@ class AppDatabase { } } - // Méthode mise à jour pour créer les tables avec les nouvelles colonnes -Future _createDB() async { - // final db = await database; - - // try { - // // Table roles - // await db.query(''' - // CREATE TABLE IF NOT EXISTS roles ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // designation VARCHAR(255) NOT NULL UNIQUE - // ) ENGINE=InnoDB - // '''); - - // // Table permissions - // await db.query(''' - // CREATE TABLE IF NOT EXISTS permissions ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // name VARCHAR(255) NOT NULL UNIQUE - // ) ENGINE=InnoDB - // '''); - - // // Table menu - // await db.query(''' - // CREATE TABLE IF NOT EXISTS menu ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // name VARCHAR(255) NOT NULL, - // route VARCHAR(255) NOT NULL - // ) ENGINE=InnoDB - // '''); - - // // Table role_permissions - // await db.query(''' - // CREATE TABLE IF NOT EXISTS role_permissions ( - // role_id INT, - // permission_id INT, - // PRIMARY KEY (role_id, permission_id), - // FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, - // FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE - // ) ENGINE=InnoDB - // '''); - - // // Table role_menu_permissions - // await db.query(''' - // CREATE TABLE IF NOT EXISTS role_menu_permissions ( - // role_id INT, - // menu_id INT, - // permission_id INT, - // PRIMARY KEY (role_id, menu_id, permission_id), - // FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, - // FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, - // FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE - // ) ENGINE=InnoDB - // '''); - - // // Table points_de_vente - // await db.query(''' - // CREATE TABLE IF NOT EXISTS points_de_vente ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // nom VARCHAR(255) NOT NULL UNIQUE - // ) ENGINE=InnoDB - // '''); - - // // Table users - // await db.query(''' - // CREATE TABLE IF NOT EXISTS users ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // name VARCHAR(255) NOT NULL, - // lastname VARCHAR(255) NOT NULL, - // email VARCHAR(255) NOT NULL UNIQUE, - // password VARCHAR(255) NOT NULL, - // username VARCHAR(255) NOT NULL UNIQUE, - // role_id INT NOT NULL, - // point_de_vente_id INT, - // FOREIGN KEY (role_id) REFERENCES roles(id), - // FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) - // ) ENGINE=InnoDB - // '''); - - // // Table products - // await db.query(''' - // CREATE TABLE IF NOT EXISTS products ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // name VARCHAR(255) NOT NULL, - // price DECIMAL(10,2) NOT NULL, - // image VARCHAR(2000), - // category VARCHAR(255) NOT NULL, - // stock INT NOT NULL DEFAULT 0, - // description VARCHAR(1000), - // qrCode VARCHAR(500), - // reference VARCHAR(255), - // point_de_vente_id INT, - // marque VARCHAR(255), - // ram VARCHAR(100), - // memoire_interne VARCHAR(100), - // imei VARCHAR(255) UNIQUE, - // FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id), - // INDEX idx_products_category (category), - // INDEX idx_products_reference (reference), - // INDEX idx_products_imei (imei) - // ) ENGINE=InnoDB - // '''); - - // // Table clients - // await db.query(''' - // CREATE TABLE IF NOT EXISTS clients ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // nom VARCHAR(255) NOT NULL, - // prenom VARCHAR(255) NOT NULL, - // email VARCHAR(255) NOT NULL UNIQUE, - // telephone VARCHAR(255) NOT NULL, - // adresse VARCHAR(500), - // dateCreation DATETIME NOT NULL, - // actif TINYINT(1) NOT NULL DEFAULT 1, - // INDEX idx_clients_email (email), - // INDEX idx_clients_telephone (telephone) - // ) ENGINE=InnoDB - // '''); - - // // Table commandes MISE À JOUR avec les champs de remise - // await db.query(''' - // CREATE TABLE IF NOT EXISTS commandes ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // clientId INT NOT NULL, - // dateCommande DATETIME NOT NULL, - // statut INT NOT NULL DEFAULT 0, - // montantTotal DECIMAL(10,2) NOT NULL, - // notes VARCHAR(1000), - // dateLivraison DATETIME, - // commandeurId INT, - // validateurId INT, - // remisePourcentage DECIMAL(5,2) NULL, - // remiseMontant DECIMAL(10,2) NULL, - // montantApresRemise DECIMAL(10,2) NULL, - // FOREIGN KEY (commandeurId) REFERENCES users(id), - // FOREIGN KEY (validateurId) REFERENCES users(id), - // FOREIGN KEY (clientId) REFERENCES clients(id), - // INDEX idx_commandes_client (clientId), - // INDEX idx_commandes_date (dateCommande) - // ) ENGINE=InnoDB - // '''); - - // // Table details_commandes MISE À JOUR avec le champ cadeau - // await db.query(''' - // CREATE TABLE IF NOT EXISTS details_commandes ( - // id INT AUTO_INCREMENT PRIMARY KEY, - // commandeId INT NOT NULL, - // produitId INT NOT NULL, - // quantite INT NOT NULL, - // prixUnitaire DECIMAL(10,2) NOT NULL, - // sousTotal DECIMAL(10,2) NOT NULL, - // estCadeau TINYINT(1) DEFAULT 0, - // FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE, - // FOREIGN KEY (produitId) REFERENCES products(id), - // INDEX idx_details_commande (commandeId) - // ) ENGINE=InnoDB - // '''); - - // print("Tables créées avec succès avec les nouveaux champs !"); - // } catch (e) { - // print("Erreur lors de la création des tables: $e"); - // rethrow; - // } -} + // --- MÉTHODES D'INSERTION PAR DÉFAUT --- @@ -933,31 +769,37 @@ Future _createDB() async { // --- DÉTAILS COMMANDES --- Future createDetailCommande(DetailCommande detail) async { - final db = await database; - final detailMap = detail.toMap(); - detailMap.remove('id'); - - final fields = detailMap.keys.join(', '); - final placeholders = List.filled(detailMap.length, '?').join(', '); - - final result = await db.query( - 'INSERT INTO details_commandes ($fields) VALUES ($placeholders)', - detailMap.values.toList() - ); - return result.insertId!; - } + final db = await database; + final detailMap = detail.toMap(); + detailMap.remove('id'); + + final fields = detailMap.keys.join(', '); + final placeholders = List.filled(detailMap.length, '?').join(', '); + + final result = await db.query( + 'INSERT INTO details_commandes ($fields) VALUES ($placeholders)', + detailMap.values.toList() + ); + return result.insertId!; +} - Future> getDetailsCommande(int commandeId) async { - final db = await database; - final result = await db.query(''' - SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference - FROM details_commandes dc - LEFT JOIN products p ON dc.produitId = p.id - WHERE dc.commandeId = ? - ORDER BY dc.id - ''', [commandeId]); - return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); - } + // Méthode mise à jour pour récupérer les détails avec les remises +Future> getDetailsCommande(int commandeId) async { + final db = await database; + final result = await db.query(''' + SELECT + dc.*, + p.name as produitNom, + p.image as produitImage, + p.reference as produitReference + FROM details_commandes dc + LEFT JOIN products p ON dc.produitId = p.id + WHERE dc.commandeId = ? + ORDER BY dc.est_cadeau ASC, dc.id + ''', [commandeId]); + + return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); +} // --- RECHERCHE PRODUITS --- @@ -1364,17 +1206,19 @@ Future _createDB() async { // --- TRANSACTIONS COMPLEXES --- - Future createCommandeComplete(Client client, Commande commande, List details) async { + +// Méthode pour créer une commande complète avec remises +Future createCommandeComplete(Client client, Commande commande, List details) async { final db = await database; try { await db.query('START TRANSACTION'); - // 1. Utiliser createOrGetClient au lieu de créer directement + // 1. Créer ou récupérer le client final existingOrNewClient = await createOrGetClient(client); final clientId = existingOrNewClient.id!; - // 2. Créer la commande avec le bon clientId + // 2. Créer la commande final commandeMap = commande.toMap(); commandeMap.remove('id'); commandeMap['clientId'] = clientId; @@ -1388,7 +1232,7 @@ Future _createDB() async { ); final commandeId = commandeResult.insertId!; - // 3. Créer les détails de commande + // 3. Créer les détails de commande avec remises for (final detail in details) { final detailMap = detail.toMap(); detailMap.remove('id'); @@ -1418,6 +1262,111 @@ Future _createDB() async { } } +// Méthode pour mettre à jour un détail de commande (utile pour modifier les remises) +Future updateDetailCommande(DetailCommande detail) async { + final db = await database; + final detailMap = detail.toMap(); + final id = detailMap.remove('id'); + + final setClause = detailMap.keys.map((key) => '$key = ?').join(', '); + final values = [...detailMap.values, id]; + + final result = await db.query( + 'UPDATE details_commandes SET $setClause WHERE id = ?', + values + ); + return result.affectedRows!; +} + + +// Méthode pour obtenir les statistiques des remises +Future> getRemiseStatistics() async { + final db = await database; + + try { + // Total des remises accordées + final totalRemisesResult = await db.query(''' + SELECT + COUNT(*) as nombre_remises, + SUM(montant_remise) as total_remises, + AVG(montant_remise) as moyenne_remise + FROM details_commandes + WHERE remise_type IS NOT NULL AND montant_remise > 0 + '''); + + // Remises par type + final remisesParTypeResult = await db.query(''' + SELECT + remise_type, + COUNT(*) as nombre, + SUM(montant_remise) as total, + AVG(remise_valeur) as moyenne_valeur + FROM details_commandes + WHERE remise_type IS NOT NULL AND montant_remise > 0 + GROUP BY remise_type + '''); + + // Produits avec le plus de remises + final produitsRemisesResult = await db.query(''' + SELECT + p.name as produit_nom, + COUNT(*) as nombre_remises, + SUM(dc.montant_remise) as total_remises + FROM details_commandes dc + INNER JOIN products p ON dc.produitId = p.id + WHERE dc.remise_type IS NOT NULL AND dc.montant_remise > 0 + GROUP BY dc.produitId, p.name + ORDER BY total_remises DESC + LIMIT 10 + '''); + + return { + 'total_remises': totalRemisesResult.first.fields, + 'remises_par_type': remisesParTypeResult.map((row) => row.fields).toList(), + 'produits_remises': produitsRemisesResult.map((row) => row.fields).toList(), + }; + } catch (e) { + print("Erreur lors du calcul des statistiques de remises: $e"); + return { + 'total_remises': {'nombre_remises': 0, 'total_remises': 0.0, 'moyenne_remise': 0.0}, + 'remises_par_type': [], + 'produits_remises': [], + }; + } +} + + +// Méthode pour obtenir les commandes avec le plus de remises +Future>> getCommandesAvecRemises({int limit = 20}) async { + final db = await database; + + try { + final result = await db.query(''' + SELECT + c.id as commande_id, + c.dateCommande, + c.montantTotal, + cl.nom as client_nom, + cl.prenom as client_prenom, + SUM(dc.montant_remise) as total_remises, + COUNT(CASE WHEN dc.remise_type IS NOT NULL THEN 1 END) as nombre_articles_remise, + COUNT(dc.id) as total_articles + FROM commandes c + INNER JOIN clients cl ON c.clientId = cl.id + INNER JOIN details_commandes dc ON c.id = dc.commandeId + GROUP BY c.id, c.dateCommande, c.montantTotal, cl.nom, cl.prenom + HAVING total_remises > 0 + ORDER BY total_remises DESC + LIMIT ? + ''', [limit]); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur lors de la récupération des commandes avec remises: $e"); + return []; + } +} + // --- STATISTIQUES AVANCÉES --- Future> getProductCountByCategory() async { @@ -1799,7 +1748,6 @@ Future findClientByAnyIdentifier({ String? nom, String? prenom, }) async { - final db = await database; // Recherche par email si fourni if (email != null && email.isNotEmpty) { @@ -1821,140 +1769,440 @@ Future findClientByAnyIdentifier({ return null; } - -Future migrateDatabaseForDiscountAndGift() async { +// +// Méthode pour obtenir les statistiques des cadeaux +Future> getCadeauStatistics() async { final db = await database; try { - // Ajouter les colonnes de remise à la table commandes - await db.query(''' - ALTER TABLE commandes - ADD COLUMN remisePourcentage DECIMAL(5,2) NULL + // Total des cadeaux offerts + final totalCadeauxResult = await db.query(''' + SELECT + COUNT(*) as nombre_cadeaux, + SUM(sousTotal) as valeur_totale_cadeaux, + AVG(sousTotal) as valeur_moyenne_cadeau, + SUM(quantite) as quantite_totale_cadeaux + FROM details_commandes + WHERE est_cadeau = 1 '''); - await db.query(''' - ALTER TABLE commandes - ADD COLUMN remiseMontant DECIMAL(10,2) NULL + // Cadeaux par produit + final cadeauxParProduitResult = await db.query(''' + SELECT + p.name as produit_nom, + p.category as produit_categorie, + COUNT(*) as nombre_fois_offert, + SUM(dc.quantite) as quantite_totale_offerte, + SUM(dc.sousTotal) as valeur_totale_offerte + FROM details_commandes dc + INNER JOIN products p ON dc.produitId = p.id + WHERE dc.est_cadeau = 1 + GROUP BY dc.produitId, p.name, p.category + ORDER BY quantite_totale_offerte DESC + LIMIT 10 '''); - await db.query(''' - ALTER TABLE commandes - ADD COLUMN montantApresRemise DECIMAL(10,2) NULL + // Commandes avec cadeaux + final commandesAvecCadeauxResult = await db.query(''' + SELECT + COUNT(DISTINCT c.id) as nombre_commandes_avec_cadeaux, + AVG(cadeau_stats.nombre_cadeaux_par_commande) as moyenne_cadeaux_par_commande, + AVG(cadeau_stats.valeur_cadeaux_par_commande) as valeur_moyenne_cadeaux_par_commande + FROM commandes c + INNER JOIN ( + SELECT + commandeId, + COUNT(*) as nombre_cadeaux_par_commande, + SUM(sousTotal) as valeur_cadeaux_par_commande + FROM details_commandes + WHERE est_cadeau = 1 + GROUP BY commandeId + ) cadeau_stats ON c.id = cadeau_stats.commandeId '''); - // Ajouter la colonne cadeau à la table details_commandes - await db.query(''' - ALTER TABLE details_commandes - ADD COLUMN estCadeau TINYINT(1) DEFAULT 0 + // Évolution des cadeaux par mois + final evolutionMensuelleResult = await db.query(''' + SELECT + DATE_FORMAT(c.dateCommande, '%Y-%m') as mois, + COUNT(dc.id) as nombre_cadeaux, + SUM(dc.sousTotal) as valeur_cadeaux + FROM details_commandes dc + INNER JOIN commandes c ON dc.commandeId = c.id + WHERE dc.est_cadeau = 1 + AND c.dateCommande >= DATE_SUB(NOW(), INTERVAL 12 MONTH) + GROUP BY DATE_FORMAT(c.dateCommande, '%Y-%m') + ORDER BY mois DESC + LIMIT 12 '''); - print("Migration pour remise et cadeau terminée avec succès"); + return { + 'total_cadeaux': totalCadeauxResult.first.fields, + 'cadeaux_par_produit': cadeauxParProduitResult.map((row) => row.fields).toList(), + 'commandes_avec_cadeaux': commandesAvecCadeauxResult.first.fields, + 'evolution_mensuelle': evolutionMensuelleResult.map((row) => row.fields).toList(), + }; } catch (e) { - // Les colonnes existent probablement déjà - print("Migration déjà effectuée ou erreur: $e"); + print("Erreur lors du calcul des statistiques de cadeaux: $e"); + return { + 'total_cadeaux': {'nombre_cadeaux': 0, 'valeur_totale_cadeaux': 0.0, 'valeur_moyenne_cadeau': 0.0, 'quantite_totale_cadeaux': 0}, + 'cadeaux_par_produit': [], + 'commandes_avec_cadeaux': {'nombre_commandes_avec_cadeaux': 0, 'moyenne_cadeaux_par_commande': 0.0, 'valeur_moyenne_cadeaux_par_commande': 0.0}, + 'evolution_mensuelle': [], + }; } } -Future> getDetailsCommandeAvecCadeaux(int commandeId) async { +// Méthode pour obtenir les commandes avec des cadeaux +Future>> getCommandesAvecCadeaux({int limit = 20}) async { final db = await database; - final result = await db.query(''' - SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference - FROM details_commandes dc - LEFT JOIN products p ON dc.produitId = p.id - WHERE dc.commandeId = ? - ORDER BY dc.estCadeau ASC, dc.id - ''', [commandeId]); - return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); + + try { + final result = await db.query(''' + SELECT + c.id as commande_id, + c.dateCommande, + c.montantTotal, + cl.nom as client_nom, + cl.prenom as client_prenom, + cadeau_stats.nombre_cadeaux, + cadeau_stats.valeur_cadeaux, + cadeau_stats.quantite_cadeaux, + (SELECT COUNT(*) FROM details_commandes WHERE commandeId = c.id) as total_articles + FROM commandes c + INNER JOIN clients cl ON c.clientId = cl.id + INNER JOIN ( + SELECT + commandeId, + COUNT(*) as nombre_cadeaux, + SUM(sousTotal) as valeur_cadeaux, + SUM(quantite) as quantite_cadeaux + FROM details_commandes + WHERE est_cadeau = 1 + GROUP BY commandeId + ) cadeau_stats ON c.id = cadeau_stats.commandeId + ORDER BY cadeau_stats.valeur_cadeaux DESC + LIMIT ? + ''', [limit]); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur lors de la récupération des commandes avec cadeaux: $e"); + return []; + } } - -Future updateCommandeAvecRemise(int commandeId, { - double? remisePourcentage, - double? remiseMontant, - double? montantApresRemise, -}) async { +// Méthode pour obtenir les produits les plus offerts en cadeau +Future>> getProduitsLesPlusOffertsEnCadeau({int limit = 10}) async { final db = await database; - List setClauses = []; - List values = []; - - if (remisePourcentage != null) { - setClauses.add('remisePourcentage = ?'); - values.add(remisePourcentage); + try { + final result = await db.query(''' + SELECT + p.id, + p.name as produit_nom, + p.price as prix_unitaire, + p.category as categorie, + p.stock, + COUNT(dc.id) as nombre_fois_offert, + SUM(dc.quantite) as quantite_totale_offerte, + SUM(dc.sousTotal) as valeur_totale_offerte, + COUNT(DISTINCT dc.commandeId) as nombre_commandes_distinctes + FROM products p + INNER JOIN details_commandes dc ON p.id = dc.produitId + WHERE dc.est_cadeau = 1 + GROUP BY p.id, p.name, p.price, p.category, p.stock + ORDER BY quantite_totale_offerte DESC + LIMIT ? + ''', [limit]); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur lors de la récupération des produits les plus offerts: $e"); + return []; } +} +// Méthode pour obtenir les clients qui ont reçu le plus de cadeaux +Future>> getClientsAvecLePlusDeCadeaux({int limit = 10}) async { + final db = await database; - if (remiseMontant != null) { - setClauses.add('remiseMontant = ?'); - values.add(remiseMontant); + try { + final result = await db.query(''' + SELECT + cl.id as client_id, + cl.nom, + cl.prenom, + cl.email, + cl.telephone, + COUNT(dc.id) as nombre_cadeaux_recus, + SUM(dc.quantite) as quantite_cadeaux_recus, + SUM(dc.sousTotal) as valeur_cadeaux_recus, + COUNT(DISTINCT c.id) as nombre_commandes_avec_cadeaux + FROM clients cl + INNER JOIN commandes c ON cl.id = c.clientId + INNER JOIN details_commandes dc ON c.id = dc.commandeId + WHERE dc.est_cadeau = 1 + GROUP BY cl.id, cl.nom, cl.prenom, cl.email, cl.telephone + ORDER BY valeur_cadeaux_recus DESC + LIMIT ? + ''', [limit]); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur lors de la récupération des clients avec le plus de cadeaux: $e"); + return []; } +} +// Méthode pour calculer l'impact des cadeaux sur les ventes +Future> getImpactCadeauxSurVentes() async { + final db = await database; - if (montantApresRemise != null) { - setClauses.add('montantApresRemise = ?'); - values.add(montantApresRemise); + try { + // Comparaison des commandes avec et sans cadeaux + final comparisonResult = await db.query(''' + SELECT + 'avec_cadeaux' as type_commande, + COUNT(DISTINCT c.id) as nombre_commandes, + AVG(c.montantTotal) as panier_moyen, + SUM(c.montantTotal) as chiffre_affaires_total + FROM commandes c + WHERE EXISTS ( + SELECT 1 FROM details_commandes dc + WHERE dc.commandeId = c.id AND dc.est_cadeau = 1 + ) + + UNION ALL + + SELECT + 'sans_cadeaux' as type_commande, + COUNT(DISTINCT c.id) as nombre_commandes, + AVG(c.montantTotal) as panier_moyen, + SUM(c.montantTotal) as chiffre_affaires_total + FROM commandes c + WHERE NOT EXISTS ( + SELECT 1 FROM details_commandes dc + WHERE dc.commandeId = c.id AND dc.est_cadeau = 1 + ) + '''); + + // Ratio de conversion (commandes avec cadeaux / total commandes) + final ratioResult = await db.query(''' + SELECT + (SELECT COUNT(DISTINCT c.id) + FROM commandes c + WHERE EXISTS ( + SELECT 1 FROM details_commandes dc + WHERE dc.commandeId = c.id AND dc.est_cadeau = 1 + ) + ) * 100.0 / COUNT(*) as pourcentage_commandes_avec_cadeaux + FROM commandes + '''); + + return { + 'comparaison': comparisonResult.map((row) => row.fields).toList(), + 'pourcentage_commandes_avec_cadeaux': ratioResult.first['pourcentage_commandes_avec_cadeaux'] ?? 0.0, + }; + } catch (e) { + print("Erreur lors du calcul de l'impact des cadeaux: $e"); + return { + 'comparaison': [], + 'pourcentage_commandes_avec_cadeaux': 0.0, + }; } - - if (setClauses.isEmpty) return 0; - - values.add(commandeId); - - final result = await db.query( - 'UPDATE commandes SET ${setClauses.join(', ')} WHERE id = ?', - values - ); - - return result.affectedRows!; } -Future createDetailCommandeCadeau(DetailCommande detail) async { +// Méthode pour créer une commande complète avec cadeaux (mise à jour) +Future createCommandeCompleteAvecCadeaux(Client client, Commande commande, List details) async { final db = await database; - final detailMap = detail.toMap(); - detailMap.remove('id'); - detailMap['estCadeau'] = 1; // Marquer comme cadeau - detailMap['prixUnitaire'] = 0.0; // Prix zéro pour les cadeaux - detailMap['sousTotal'] = 0.0; // Sous-total zéro pour les cadeaux + try { + await db.query('START TRANSACTION'); + + // 1. Créer ou récupérer le client + final existingOrNewClient = await createOrGetClient(client); + final clientId = existingOrNewClient.id!; + + // 2. Créer la commande + final commandeMap = commande.toMap(); + commandeMap.remove('id'); + commandeMap['clientId'] = clientId; + + final commandeFields = commandeMap.keys.join(', '); + final commandePlaceholders = List.filled(commandeMap.length, '?').join(', '); + + final commandeResult = await db.query( + 'INSERT INTO commandes ($commandeFields) VALUES ($commandePlaceholders)', + commandeMap.values.toList() + ); + final commandeId = commandeResult.insertId!; + + // 3. Créer les détails de commande avec remises et cadeaux + for (final detail in details) { + final detailMap = detail.toMap(); + detailMap.remove('id'); + detailMap['commandeId'] = commandeId; + + final detailFields = detailMap.keys.join(', '); + final detailPlaceholders = List.filled(detailMap.length, '?').join(', '); + + await db.query( + 'INSERT INTO details_commandes ($detailFields) VALUES ($detailPlaceholders)', + detailMap.values.toList() + ); + + // 4. Mettre à jour le stock (même pour les cadeaux) + await db.query( + 'UPDATE products SET stock = stock - ? WHERE id = ?', + [detail.quantite, detail.produitId] + ); + } + + await db.query('COMMIT'); + + // Log des cadeaux offerts (optionnel) + final cadeaux = details.where((d) => d.estCadeau).toList(); + if (cadeaux.isNotEmpty) { + print("Cadeaux offerts dans la commande $commandeId:"); + for (final cadeau in cadeaux) { + print(" - ${cadeau.produitNom} x${cadeau.quantite} (valeur: ${cadeau.sousTotal.toStringAsFixed(2)} MGA)"); + } + } + + return commandeId; + } catch (e) { + await db.query('ROLLBACK'); + print("Erreur lors de la création de la commande complète avec cadeaux: $e"); + rethrow; + } +} + +// Méthode pour valider la disponibilité des cadeaux avant la commande +Future> verifierDisponibiliteCadeaux(List details) async { + final db = await database; + List erreurs = []; - final fields = detailMap.keys.join(', '); - final placeholders = List.filled(detailMap.length, '?').join(', '); + try { + for (final detail in details.where((d) => d.estCadeau)) { + final produit = await getProductById(detail.produitId); + + if (produit == null) { + erreurs.add("Produit cadeau introuvable (ID: ${detail.produitId})"); + continue; + } + + if (produit.stock != null && produit.stock! < detail.quantite) { + erreurs.add("Stock insuffisant pour le cadeau: ${produit.name} (demandé: ${detail.quantite}, disponible: ${produit.stock})"); + } + } + } catch (e) { + erreurs.add("Erreur lors de la vérification des cadeaux: $e"); + } - final result = await db.query( - 'INSERT INTO details_commandes ($fields) VALUES ($placeholders)', - detailMap.values.toList() - ); - return result.insertId!; + return erreurs; } +// --- MÉTHODES POUR LES VENTES PAR POINT DE VENTE --- -Future> getCadeauxCommande(int commandeId) async { +Future>> getVentesParPointDeVente() async { final db = await database; - final result = await db.query(''' - SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference - FROM details_commandes dc - LEFT JOIN products p ON dc.produitId = p.id - WHERE dc.commandeId = ? AND dc.estCadeau = 1 - ORDER BY dc.id - ''', [commandeId]); - return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); + + try { + final result = await db.query(''' + SELECT + pv.id as point_vente_id, + pv.nom as point_vente_nom, + COUNT(DISTINCT c.id) as nombre_commandes, + COUNT(dc.id) as nombre_articles_vendus, + SUM(dc.quantite) as quantite_totale_vendue, + SUM(c.montantTotal) as chiffre_affaires, + AVG(c.montantTotal) as panier_moyen, + MIN(c.dateCommande) as premiere_vente, + MAX(c.dateCommande) as derniere_vente + FROM points_de_vente pv + LEFT JOIN products p ON pv.id = p.point_de_vente_id + LEFT JOIN details_commandes dc ON p.id = dc.produitId + LEFT JOIN commandes c ON dc.commandeId = c.id + WHERE c.statut != 5 -- Exclure les commandes annulées + GROUP BY pv.id, pv.nom + ORDER BY chiffre_affaires DESC + '''); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur getVentesParPointDeVente: $e"); + return []; + } } -Future calculateMontantTotalSansCadeaux(int commandeId) async { +Future>> getTopProduitsParPointDeVente(int pointDeVenteId, {int limit = 5}) async { final db = await database; - final result = await db.query(''' - SELECT SUM(sousTotal) as total - FROM details_commandes - WHERE commandeId = ? AND (estCadeau = 0 OR estCadeau IS NULL) - ''', [commandeId]); - final total = result.first['total']; - return total != null ? (total as num).toDouble() : 0.0; + try { + final result = await db.query(''' + SELECT + p.id, + p.name as produit_nom, + p.price as prix_unitaire, + p.category as categorie, + SUM(dc.quantite) as quantite_vendue, + SUM(dc.sousTotal) as chiffre_affaires_produit, + COUNT(DISTINCT dc.commandeId) as nombre_commandes + FROM products p + INNER JOIN details_commandes dc ON p.id = dc.produitId + INNER JOIN commandes c ON dc.commandeId = c.id + WHERE p.point_de_vente_id = ? AND c.statut != 5 + GROUP BY p.id, p.name, p.price, p.category + ORDER BY quantite_vendue DESC + LIMIT ? + ''', [pointDeVenteId, limit]); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur getTopProduitsParPointDeVente: $e"); + return []; + } } -Future supprimerRemiseCommande(int commandeId) async { +Future>> getVentesParPointDeVenteParMois(int pointDeVenteId) async { final db = await database; - final result = await db.query(''' - UPDATE commandes - SET remisePourcentage = NULL, remiseMontant = NULL, montantApresRemise = NULL - WHERE id = ? - ''', [commandeId]); - return result.affectedRows!; + try { + final result = await db.query(''' + SELECT + DATE_FORMAT(c.dateCommande, '%Y-%m') as mois, + COUNT(DISTINCT c.id) as nombre_commandes, + SUM(c.montantTotal) as chiffre_affaires, + SUM(dc.quantite) as quantite_vendue + FROM commandes c + INNER JOIN details_commandes dc ON c.id = dc.commandeId + INNER JOIN products p ON dc.produitId = p.id + WHERE p.point_de_vente_id = ? + AND c.statut != 5 + AND c.dateCommande >= DATE_SUB(NOW(), INTERVAL 12 MONTH) + GROUP BY DATE_FORMAT(c.dateCommande, '%Y-%m') + ORDER BY mois DESC + LIMIT 12 + ''', [pointDeVenteId]); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur getVentesParPointDeVenteParMois: $e"); + return []; + } +} +// Dans la classe AppDatabase, ajoutez cette méthode : +Future verifyCurrentUserPassword(String password) async { + final db = await database; + final userController = Get.find(); + + try { + final result = await db.query(''' + SELECT COUNT(*) as count + FROM users + WHERE id = ? AND password = ? + ''', [userController.userId, password]); + + return (result.first['count'] as int) > 0; + } catch (e) { + print("Erreur lors de la vérification du mot de passe: $e"); + return false; + } } } \ No newline at end of file diff --git a/lib/Views/Dashboard.dart b/lib/Views/Dashboard.dart index 07e5020..2d20280 100644 --- a/lib/Views/Dashboard.dart +++ b/lib/Views/Dashboard.dart @@ -61,14 +61,14 @@ void initState() { } void _loadData() { - _statsFuture = _database.getStatistiques(); - _recentOrdersFuture = _database.getCommandes().then((orders) => orders.take(5).toList()); - _lowStockProductsFuture = _database.getProducts().then((products) { - return products.where((p) => (p.stock ?? 0) < 10).toList(); - }); - _recentClientsFuture = _database.getClients().then((clients) => clients.take(5).toList()); - _allOrdersFuture = _database.getCommandes(); - _productsByCategoryFuture = _database.getProductCountByCategory(); + _statsFuture = _database.getStatistiques(); + _recentOrdersFuture = _database.getCommandes().then((orders) => orders.take(5).toList()); + _lowStockProductsFuture = _database.getProducts().then((products) { + return products.where((p) => (p.stock ?? 0) < 10).toList(); + }); + _recentClientsFuture = _database.getClients().then((clients) => clients.take(5).toList()); + _allOrdersFuture = _database.getCommandes(); + _productsByCategoryFuture = _database.getProductCountByCategory(); } Future _showCategoryProductsDialog(String category) async { final products = await _database.getProductsByCategory(category); @@ -185,7 +185,9 @@ Future _showCategoryProductsDialog(String category) async { // Histogramme des catégories de produits _buildCategoryHistogram(), SizedBox(height: 20), - + // NOUVEAU: Widget des ventes par point de vente + _buildVentesParPointDeVenteCard(), + SizedBox(height: 20), // Section des données récentes _buildRecentDataSection(), ], @@ -1087,6 +1089,411 @@ Future _showCategoryProductsDialog(String category) async { ); } +//widget vente +// 2. Ajoutez cette méthode dans la classe _DashboardPageState + +Widget _buildVentesParPointDeVenteCard() { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.store, color: Colors.purple), + SizedBox(width: 8), + Text( + 'Ventes par Point de Vente', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + SizedBox(height: 16), + Container( + height: 400, + child: FutureBuilder>>( + future: _database.getVentesParPointDeVente(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.store_mall_directory_outlined, size: 64, color: Colors.grey), + SizedBox(height: 16), + Text('Aucune donnée de vente par point de vente', style: TextStyle(color: Colors.grey)), + ], + ), + ); + } + + final ventesData = snapshot.data!; + + return SingleChildScrollView( + child: Column( + children: [ + // Graphique en barres des chiffres d'affaires + Container( + height: 200, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: _getMaxChiffreAffaires(ventesData) * 1.2, + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + tooltipBgColor: Colors.blueGrey, + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final pointVente = ventesData[groupIndex]; + final ca = pointVente['chiffre_affaires'] ?? 0.0; + final nbCommandes = pointVente['nombre_commandes'] ?? 0; + return BarTooltipItem( + '${pointVente['point_vente_nom']}\n${ca.toStringAsFixed(2)} MGA\n$nbCommandes commandes', + TextStyle(color: Colors.white, fontSize: 12), + ); + }, + ), + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index >= 0 && index < ventesData.length) { + final nom = ventesData[index]['point_vente_nom'] as String; + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + nom.length > 5 ? nom.substring(0, 5) : nom, + style: TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ), + ); + } + return Text(''); + }, + reservedSize: 40, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return Text( + _formatCurrency(value), + style: TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ); + }, + reservedSize: 60, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all( + color: Colors.grey.withOpacity(0.3), + width: 1, + ), + ), + barGroups: ventesData.asMap().entries.map((entry) { + final index = entry.key; + final data = entry.value; + final ca = (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0; + + return BarChartGroupData( + x: index, + barRods: [ + BarChartRodData( + toY: ca, + color: _getPointVenteColor(index), + width: 16, + borderRadius: BorderRadius.circular(4), + backDrawRodData: BackgroundBarChartRodData( + show: true, + toY: _getMaxChiffreAffaires(ventesData) * 1.2, + color: Colors.grey.withOpacity(0.1), + ), + ), + ], + showingTooltipIndicators: [0], + ); + }).toList(), + ), + ), + ), + + SizedBox(height: 20), + + // Tableau détaillé + _buildTableauVentesPointDeVente(ventesData), + ], + ), + ); + }, + ), + ), + ], + ), + ), + ); +} + +Widget _buildTableauVentesPointDeVente(List> ventesData) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.withOpacity(0.3)), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + // En-tête du tableau + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + child: Row( + children: [ + Expanded(flex: 2, child: Text('Point de Vente', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + Expanded(flex: 2, child: Text('CA (MGA)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + Expanded(flex: 1, child: Text('Cmd', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + Expanded(flex: 1, child: Text('Articles', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + Expanded(flex: 2, child: Text('Panier Moy.', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + ], + ), + ), + + // Lignes du tableau + ...ventesData.asMap().entries.map((entry) { + final index = entry.key; + final data = entry.value; + final isEven = index % 2 == 0; + + return InkWell( + onTap: () => _showPointVenteDetails(data), + child: Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: isEven ? Colors.grey.withOpacity(0.05) : Colors.white, + ), + child: Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: _getPointVenteColor(index), + borderRadius: BorderRadius.circular(2), + ), + ), + SizedBox(width: 8), + Expanded( + child: Text( + data['point_vente_nom'] ?? 'N/A', + style: TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + Expanded( + flex: 2, + child: Text( + '${((data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0).toStringAsFixed(2)}', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ), + Expanded( + flex: 1, + child: Text( + '${data['nombre_commandes'] ?? 0}', + style: TextStyle(fontSize: 12), + ), + ), + Expanded( + flex: 1, + child: Text( + '${data['nombre_articles_vendus'] ?? 0}', + style: TextStyle(fontSize: 12), + ), + ), + Expanded( + flex: 2, + child: Text( + '${((data['panier_moyen'] as num?)?.toDouble() ?? 0.0).toStringAsFixed(2)}', + style: TextStyle(fontSize: 12), + ), + ), + ], + ), + ), + ); + }).toList(), + ], + ), + ); +} + +// Méthodes utilitaires +double _getMaxChiffreAffaires(List> ventesData) { + if (ventesData.isEmpty) return 100.0; + + return ventesData + .map((data) => (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0) + .reduce((a, b) => a > b ? a : b); +} + +Color _getPointVenteColor(int index) { + final colors = [ + Colors.blue, + Colors.green, + Colors.orange, + Colors.purple, + Colors.teal, + Colors.pink, + Colors.indigo, + Colors.amber, + Colors.cyan, + Colors.lime, + ]; + return colors[index % colors.length]; +} + +String _formatCurrency(double value) { + if (value >= 1000000) { + return '${(value / 1000000).toStringAsFixed(1)}M'; + } else if (value >= 1000) { + return '${(value / 1000).toStringAsFixed(1)}K'; + } else { + return value.toStringAsFixed(0); + } +} + +void _showPointVenteDetails(Map pointVenteData) async { + final pointVenteId = pointVenteData['point_vente_id'] as int; + final pointVenteNom = pointVenteData['point_vente_nom'] as String; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Détails - $pointVenteNom'), + content: Container( + width: double.maxFinite, + height: 400, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Statistiques générales + _buildStatRow('Chiffre d\'affaires:', '${((pointVenteData['chiffre_affaires'] as num?)?.toDouble() ?? 0.0).toStringAsFixed(2)} MGA'), + _buildStatRow('Nombre de commandes:', '${pointVenteData['nombre_commandes'] ?? 0}'), + _buildStatRow('Articles vendus:', '${pointVenteData['nombre_articles_vendus'] ?? 0}'), + _buildStatRow('Quantité totale:', '${pointVenteData['quantite_totale_vendue'] ?? 0}'), + _buildStatRow('Panier moyen:', '${((pointVenteData['panier_moyen'] as num?)?.toDouble() ?? 0.0).toStringAsFixed(2)} MGA'), + + SizedBox(height: 16), + Text('Top 5 des produits:', style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox(height: 8), + + // Top produits + FutureBuilder>>( + future: _database.getTopProduitsParPointDeVente(pointVenteId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { + return Text('Aucun produit vendu', style: TextStyle(color: Colors.grey)); + } + + final produits = snapshot.data!; + return Column( + children: produits.map((produit) => Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + produit['produit_nom'] ?? 'N/A', + style: TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '${produit['quantite_vendue'] ?? 0} vendus', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + )).toList(), + ); + }, + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Fermer'), + ), + ], + ), + ); +} + +Widget _buildStatRow(String label, String value) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: TextStyle(fontSize: 12)), + Text(value, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), + ], + ), + ); +} + + + Widget _buildLowStockCard() { return Card( diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart index e31cf46..1b23a2d 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -13,16 +13,17 @@ import 'package:open_file/open_file.dart'; import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Components/commandManagementComponents/CommandeActions.dart'; -import 'package:youmazgestion/Components/commandManagementComponents/DiscountDialog.dart'; -import 'package:youmazgestion/Components/commandManagementComponents/GiftSelectionDialog.dart'; +import 'package:youmazgestion/Components/commandManagementComponents/PaswordRequired.dart'; import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart'; import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethodDialog.dart'; -import 'package:youmazgestion/Components/commandManagementComponents/PaymentType.dart'; +import 'package:youmazgestion/Components/paymentType.dart'; + import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/Models/produit.dart'; import '../Components/commandManagementComponents/CommandDetails.dart'; + class GestionCommandesPage extends StatefulWidget { const GestionCommandesPage({super.key}); @@ -108,9 +109,6 @@ class _GestionCommandesPageState extends State { clientNom: commandeExistante.clientNom, clientPrenom: commandeExistante.clientPrenom, clientEmail: commandeExistante.clientEmail, - remisePourcentage: commandeExistante.remisePourcentage, - remiseMontant: commandeExistante.remiseMontant, - montantApresRemise: commandeExistante.montantApresRemise, )); } else { await _database.updateStatutCommande(commandeId, newStatut); @@ -165,76 +163,6 @@ class _GestionCommandesPageState extends State { } } - Future _showDiscountDialog(Commande commande) async { - final discountData = await showDialog>( - context: context, - builder: (context) => DiscountDialog(commande: commande), - ); - - if (discountData != null) { - // Mettre à jour la commande avec la remise - final commandeAvecRemise = Commande( - id: commande.id, - clientId: commande.clientId, - dateCommande: commande.dateCommande, - statut: commande.statut, - montantTotal: commande.montantTotal, - notes: commande.notes, - dateLivraison: commande.dateLivraison, - commandeurId: commande.commandeurId, - validateurId: commande.validateurId, - clientNom: commande.clientNom, - clientPrenom: commande.clientPrenom, - clientEmail: commande.clientEmail, - remisePourcentage: discountData['pourcentage'], - remiseMontant: discountData['montant'], - montantApresRemise: discountData['montantFinal'], - ); - - await _database.updateCommande(commandeAvecRemise); - await _loadCommandes(); - - Get.snackbar( - 'Succès', - 'Remise appliquée avec succès', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } - } - - Future _showGiftDialog(Commande commande) async { - final selectedProduct = await showDialog( - context: context, - builder: (context) => GiftSelectionDialog(commande: commande), - ); - - if (selectedProduct != null) { - // Ajouter le produit cadeau à la commande avec prix = 0 - final detailCadeau = DetailCommande( - commandeId: commande.id!, - produitId: selectedProduct.id!, - quantite: 1, - prixUnitaire: 0.0, // Prix = 0 pour un cadeau - sousTotal: 0.0, - produitNom: selectedProduct.name, - estCadeau: true, // Nouveau champ pour identifier les cadeaux - ); - - await _database.createDetailCommande(detailCadeau); - await _loadCommandes(); - - Get.snackbar( - 'Succès', - 'Cadeau ajouté à la commande', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } - } - Future _showCashPaymentDialog(Commande commande, double amountGiven) async { final amountController = TextEditingController( text: amountGiven.toStringAsFixed(2), @@ -243,21 +171,14 @@ class _GestionCommandesPageState extends State { await showDialog( context: context, builder: (context) { - final montantFinal = commande.montantApresRemise ?? commande.montantTotal; + final montantFinal = commande.montantTotal; final change = amountGiven - montantFinal; return AlertDialog( title: const Text('Paiement en liquide'), content: Column( mainAxisSize: MainAxisSize.min, children: [ - if (commande.montantApresRemise != null) ...[ - Text('Montant original: ${commande.montantTotal.toStringAsFixed(2)} MGA'), - Text('Remise: ${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA'), - const SizedBox(height: 5), - Text('Montant à payer: ${montantFinal.toStringAsFixed(2)} MGA', - style: const TextStyle(fontWeight: FontWeight.bold)), - ] else - Text('Montant total: ${montantFinal.toStringAsFixed(2)} MGA'), + Text('Montant total: ${montantFinal.toStringAsFixed(2)} MGA'), const SizedBox(height: 10), TextField( controller: amountController, @@ -315,7 +236,10 @@ class _GestionCommandesPageState extends State { final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); return pw.Text(String.fromCharCode(0xf095), style: pw.TextStyle(font: font)); } - +Future buildIconGift() async { + final font = pw.Font.ttf(await rootBundle.load('assets/NotoEmoji-Regular.ttf')); + return pw.Text('🎁', style: pw.TextStyle(font: font, fontSize: 16)); +} Future buildIconCheckedText() async { final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); return pw.Text(String.fromCharCode(0xf14a), style: pw.TextStyle(font: font)); @@ -326,513 +250,1640 @@ class _GestionCommandesPageState extends State { return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font)); } - 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 iconPhone = await buildIconPhoneText(); - final iconChecked = await buildIconCheckedText(); - final iconGlobe = await buildIconGlobeText(); - - final List> detailsAvecProduits = []; - for (final detail in details) { - final produit = await _database.getProductById(detail.produitId); - detailsAvecProduits.add({ - 'detail': detail, - 'produit': produit, - }); +// Bon de livraison============================================== +Future _generateBonLivraison(Commande commande) async { + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + final pointDeVente = await _database.getPointDeVenteById(1); + + // Récupérer les informations des vendeurs + final commandeur = commande.commandeurId != null + ? await _database.getUserById(commande.commandeurId!) + : null; + final validateur = commande.validateurId != null + ? await _database.getUserById(commande.validateurId!) + : null; + + final iconPhone = await buildIconPhoneText(); + final iconChecked = await buildIconCheckedText(); + final iconGlobe = await buildIconGlobeText(); + + double sousTotal = 0; + double totalRemises = 0; + double totalCadeaux = 0; + int nombreCadeaux = 0; + + for (final detail in details) { + sousTotal += detail.sousTotal; + if (detail.estCadeau) { + totalCadeaux += detail.sousTotal; + nombreCadeaux += detail.quantite; + } else { + totalRemises += detail.montantRemise; } - - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); - - final smallTextStyle = pw.TextStyle(fontSize: 9); - final smallBoldTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold); - final normalTextStyle = pw.TextStyle(fontSize: 10); - final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold); - final boldTexClienttStyle = 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 italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont); - - pdf.addPage( - pw.Page( - margin: const pw.EdgeInsets.all(20), - build: (pw.Context context) { - return pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // En-tête avec logo et informations - pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Container( - width: 150, - height: 150, - child: pw.Image(image), + } + + final List> detailsAvecProduits = []; + for (final detail in details) { + final produit = await _database.getProductById(detail.produitId); + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + } + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); + + // Tailles de texte encore plus réduites pour 2 exemplaires + final tinyTextStyle = pw.TextStyle(fontSize: 6); + final smallTextStyle = pw.TextStyle(fontSize: 7); + final normalTextStyle = pw.TextStyle(fontSize: 8); + final boldTextStyle = pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold); + final boldClientStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold); + final frameTextStyle = pw.TextStyle(fontSize: 7); + final italicTextStyle = pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold, font: italicFont); + final italicLogoStyle = pw.TextStyle(fontSize: 5, fontWeight: pw.FontWeight.bold, font: italicFont); + final titleStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold); + + // Fonction pour créer un exemplaire + pw.Widget buildExemplaire(String typeExemplaire, {bool isSecond = false}) { + return pw.Container( + width: double.infinity, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête avec indication de l'exemplaire + pw.Container( + width: double.infinity, + padding: const pw.EdgeInsets.all(4), + decoration: pw.BoxDecoration( + color: typeExemplaire == "CLIENT" ? PdfColors.blue100 : PdfColors.green100, + ), + child: pw.Center( + child: pw.Text( + 'BON DE LIVRAISON - EXEMPLAIRE $typeExemplaire', + style: pw.TextStyle( + fontSize: 9, + fontWeight: pw.FontWeight.bold, + color: typeExemplaire == "CLIENT" ? PdfColors.blue800 : PdfColors.green800, + ), + ), + ), + ), + + 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 - très compact + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Container( + width: 60, + height: 60, + child: pw.Image(image), + ), + pw.Text('NOTRE COMPETENCE, A VOTRE SERVICE', style: italicLogoStyle), + pw.SizedBox(height: 4), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('📍 REMAX Andravoangy', style: tinyTextStyle), + pw.Text('📍 SUPREME CENTER Behoririka', style: tinyTextStyle), + pw.Text('📞 033 37 808 18', style: tinyTextStyle), + pw.Text('🌐 www.guycom.mg', style: tinyTextStyle), + pw.SizedBox(height: 2), + // Ajout du NIF + pw.Text('NIF: 1026/GC78-20-02-22', style: pw.TextStyle(fontSize: 6, 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: 4), + pw.Container(width: 100, height: 1, color: PdfColors.black), + pw.SizedBox(height: 4), + pw.Container( + padding: const pw.EdgeInsets.all(4), + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black), + ), + child: pw.Column( + children: [ + pw.Text('Boutique:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTextStyle), + pw.SizedBox(height: 2), + pw.Text('Bon N°:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTextStyle), + ], + ), + ), + ], + ), + + // Informations client - compact + pw.Container( + width: 130, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1), ), - pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo), - pw.SizedBox(height: 12), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, + padding: const pw.EdgeInsets.all(6), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]), - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]), - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]), - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]), - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]), + 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 ?? 'Non spécifié', style: boldTextStyle), + pw.SizedBox(height: 2), + pw.Text(client?.telephone ?? 'Non spécifié', style: tinyTextStyle), ], ), - pw.SizedBox(height: 10), - pw.Row(children: [iconPhone, pw.SizedBox(width: 5), pw.Text('033 37 808 18', style: smallTextStyle)]), - pw.Row(children: [iconGlobe, pw.SizedBox(width: 5), pw.Text('www.guycom.mg', style: smallTextStyle)]), - pw.Text('Facebook: GuyCom', style: smallTextStyle), - ], - ), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldTexClienttStyle), - pw.SizedBox(height: 10), - pw.Container(width: 200, height: 1, color: PdfColors.black), - pw.SizedBox(height: 10), - pw.Row( + ), + ], + ), + + pw.SizedBox(height: 6), + + // 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(2), child: pw.Text('Désignations', style: boldTextStyle)), + pw.Padding(padding: const pw.EdgeInsets.all(2), child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)), + pw.Padding(padding: const pw.EdgeInsets.all(2), child: pw.Text('P.U.', style: boldTextStyle, textAlign: pw.TextAlign.right)), + pw.Padding(padding: const pw.EdgeInsets.all(2), child: pw.Text('Remise/Cadeau', style: boldTextStyle, textAlign: pw.TextAlign.center)), + pw.Padding(padding: const pw.EdgeInsets.all(2), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)), + ], + ), + + ...detailsAvecProduits.map((item) { + final detail = item['detail'] as DetailCommande; + final produit = item['produit']; + + return pw.TableRow( + decoration: detail.estCadeau + ? const pw.BoxDecoration(color: PdfColors.green50) + : detail.aRemise + ? const pw.BoxDecoration(color: PdfColors.orange50) + : null, children: [ - pw.Container( - width: 100, - height: 40, - padding: const pw.EdgeInsets.all(5), + pw.Padding( + padding: const pw.EdgeInsets.all(2), child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('Boutique:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTexClienttStyle), - ] - ) + pw.Row( + children: [ + pw.Expanded( + child: pw.Text(detail.produitNom ?? 'Produit inconnu', + style: pw.TextStyle(fontSize: 7, 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.SizedBox(width: 10), - pw.Container( - width: 100, - height: 40, - padding: const pw.EdgeInsets.all(5), + pw.Padding( + padding: const pw.EdgeInsets.all(2), + child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), + ), + pw.Padding( + padding: const pw.EdgeInsets.all(2), child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ - pw.Text('Bon de livraison N°:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle), - ] - ) + if (detail.estCadeau) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 5, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), + pw.Text('GRATUIT', style: pw.TextStyle(fontSize: 6, color: PdfColors.green700, fontWeight: pw.FontWeight.bold)), + ] else if (detail.aRemise) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 5, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), + pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 7, color: PdfColors.orange)), + ] else + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: smallTextStyle), + ], + ), + ), + pw.Padding( + padding: const pw.EdgeInsets.all(2), + child: pw.Text( + detail.estCadeau + ? 'CADEAU' + : detail.aRemise + ? 'REMISE' + : '-', + style: pw.TextStyle( + fontSize: 6, + 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(2), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (detail.estCadeau) ...[ + pw.Text('${detail.sousTotal.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 5, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), + pw.Text('GRATUIT', style: pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold, color: PdfColors.green700)), + ] else if (detail.aRemise) ...[ + pw.Text('${detail.sousTotal.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 5, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), + pw.Text('${detail.prixFinal.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + ] else + pw.Text('${detail.prixFinal.toStringAsFixed(0)}', style: smallTextStyle), + ], + ), ), ], - ), - pw.SizedBox(height: 20), - pw.Container( - width: 300, - height: 100, - decoration: pw.BoxDecoration( - border: pw.Border.all(color: PdfColors.black, width: 1), - ), - padding: const pw.EdgeInsets.all(10), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text('ID Client: ', style: frameTextStyle), - pw.SizedBox(height: 5), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle), - pw.SizedBox(height: 5), - pw.Container(width: 200, height: 1, color: PdfColors.black), - pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle), - pw.SizedBox(height: 10), - pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle), + ); + }).toList(), + ], + ), + + pw.SizedBox(height: 6), + + // 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: 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: 7)), + pw.SizedBox(width: 10), + pw.Text('-${totalRemises.toStringAsFixed(0)}', style: pw.TextStyle(color: PdfColors.orange, fontSize: 7)), + ], + ), + 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: 7)), + pw.SizedBox(width: 10), + pw.Text('-${totalCadeaux.toStringAsFixed(0)}', style: pw.TextStyle(color: PdfColors.green700, fontSize: 7)), + ], + ), + pw.SizedBox(height: 2), + ], + + pw.Container(width: 120, height: 1, color: PdfColors.black, margin: const pw.EdgeInsets.symmetric(vertical: 2)), + + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('TOTAL:', style: boldTextStyle), + pw.SizedBox(width: 10), + pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', style: boldTextStyle), + ], + ), + + if (totalCadeaux > 0) ...[ + pw.SizedBox(height: 4), + pw.Container( + padding: const pw.EdgeInsets.all(4), + 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: 6, color: PdfColors.green700), + ), + ), + ], + ], ), - ], - ), - ], - ), - - pw.SizedBox(height: 20), - - // Tableau des produits - pw.Table( - border: pw.TableBorder.all(width: 0.5), - columnWidths: { - 0: const pw.FlexColumnWidth(3), - 1: const pw.FlexColumnWidth(1), - 2: const pw.FlexColumnWidth(2), - 3: const pw.FlexColumnWidth(2), - }, - children: [ - pw.TableRow( - decoration: const pw.BoxDecoration(color: PdfColors.grey200), - children: [ - pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Désignations', style: boldTextStyle)), - pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)), - pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)), - pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)), - ], - ), - - ...detailsAvecProduits.map((item) { - final detail = item['detail'] as DetailCommande; - final produit = item['produit']; + ), - return pw.TableRow( - children: [ - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, + 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(4), + decoration: pw.BoxDecoration( + color: PdfColors.grey100, + borderRadius: pw.BorderRadius.circular(3), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('VENDEURS', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 2), + 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: 6), + ), + ], + ), + ), + 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: 6), + ), + ], + ), + ), + ], + ), + ], + ), + ), + + pw.SizedBox(height: 8), + + // Signatures + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - pw.Row( + pw.Column( children: [ - pw.Text(detail.produitNom ?? 'Produit inconnu', - style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)), - if (detail.estCadeau == true) - pw.Text(' (CADEAU)', - style: pw.TextStyle(fontSize: 8, color: PdfColors.red, fontWeight: pw.FontWeight.bold)), + pw.Text('Vendeur', style: pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 10), + pw.Container(width: 60, height: 1, color: PdfColors.black), + ], + ), + pw.Column( + children: [ + pw.Text('Client', style: pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 10), + pw.Container(width: 60, height: 1, color: PdfColors.black), ], ), - pw.SizedBox(height: 2), - if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty) - pw.Text('${produit.category} ${produit.marque}', style: smallTextStyle), - if (produit?.imei != null && produit!.imei!.isNotEmpty) - pw.Text('${produit.imei}', style: smallTextStyle), - if (produit?.reference != null && produit!.reference!.isNotEmpty && produit?.ram != null && produit!.ram!.isNotEmpty && produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty) - pw.Text('${produit.ram} | ${produit.memoireInterne} | ${produit.reference}', style: smallTextStyle), ], ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(0)}', - style: normalTextStyle, textAlign: pw.TextAlign.right), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(0)}', - style: normalTextStyle, textAlign: pw.TextAlign.right), - ), - ], - ); - }).toList(), + ], + ), + ), + ], + ), + + pw.SizedBox(height: 4), + + // Note finale + pw.Text( + 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', + style: italicTextStyle, + ), + ], + ), + ), + ], + ), + ); + } + + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4.landscape, + margin: const pw.EdgeInsets.all(10), + build: (pw.Context context) { + return pw.Column( + children: [ + // Exemplaire CLIENT + pw.Expanded( + child: buildExemplaire("CLIENT"), + ), + + pw.SizedBox(height: 8), + + // Ligne de séparation avec ciseaux + pw.Container( + width: double.infinity, + child: pw.Row( + children: [ + pw.Expanded(child: pw.Container(height: 1, color: PdfColors.grey400)), + pw.Padding( + padding: const pw.EdgeInsets.symmetric(horizontal: 8), + child: pw.Text('✂️ DÉCOUPER ICI ✂️', style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600)), + ), + pw.Expanded(child: pw.Container(height: 1, color: PdfColors.grey400)), ], ), - - pw.SizedBox(height: 10), - - // Totaux avec remise - pw.Column( - children: [ - if (commande.montantApresRemise != null) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, + ), + + pw.SizedBox(height: 8), + + // Exemplaire MAGASIN + pw.Expanded( + child: buildExemplaire("MAGASIN", isSecond: true), + ), + ], + ); + }, + ), + ); + + // Sauvegarder le PDF + final output = await getTemporaryDirectory(); + final file = File("${output.path}/bon_livraison_${commande.id}.pdf"); + await file.writeAsBytes(await pdf.save()); + + // Partager ou ouvrir le fichier + await OpenFile.open(file.path); +} +//============================================================== + + + // 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); + + // NOUVEAU: Récupérer les informations des vendeurs + final commandeur = commande.commandeurId != null + ? await _database.getUserById(commande.commandeurId!) + : null; + final validateur = commande.validateurId != null + ? await _database.getUserById(commande.validateurId!) + : null; + + final iconPhone = await buildIconPhoneText(); + final iconChecked = await buildIconCheckedText(); + final iconGlobe = await buildIconGlobeText(); + + double sousTotal = 0; + double totalRemises = 0; + double totalCadeaux = 0; + int nombreCadeaux = 0; + + for (final detail in details) { + sousTotal += detail.sousTotal; + if (detail.estCadeau) { + totalCadeaux += detail.sousTotal; + nombreCadeaux += detail.quantite; + } else { + totalRemises += detail.montantRemise; + } + } + + final List> detailsAvecProduits = []; + for (final detail in details) { + final produit = await _database.getProductById(detail.produitId); + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + } + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); + + // Tailles de texte réduites pour le mode paysage + final smallTextStyle = pw.TextStyle(fontSize: 7); + final normalTextStyle = pw.TextStyle(fontSize: 8); + final boldTextStyle = pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold); + final boldTexClienttStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold); + final frameTextStyle = pw.TextStyle(fontSize: 8); + final italicTextStyle = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont); + final italicTextStyleLogo = pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold, font: italicFont); + final emojiSuportFont = pw.Font.ttf( await rootBundle.load('assets/NotoEmoji-Regular.ttf')); + final emojifont = pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); + + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4.landscape, // Mode paysage + margin: const pw.EdgeInsets.all(15), // Marges réduites + build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête avec logo et informations - optimisé pour paysage + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + // Section logo et adresses - réduite + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Container( + width: 100, // Taille logo réduite + height: 100, + child: pw.Image(image), + ), + pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo), + pw.SizedBox(height: 8), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('SOUS-TOTAL', style: normalTextStyle), - pw.SizedBox(width: 20), - pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: normalTextStyle), + pw.Row(children: [iconChecked, pw.SizedBox(width: 3), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]), + pw.Row(children: [iconChecked, pw.SizedBox(width: 3), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]), + pw.Row(children: [iconChecked, pw.SizedBox(width: 3), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]), + pw.Row(children: [iconChecked, pw.SizedBox(width: 3), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]), + pw.Row(children: [iconChecked, pw.SizedBox(width: 3), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]), ], ), - pw.SizedBox(height: 5), + pw.SizedBox(height: 6), + pw.Row(children: [iconPhone, pw.SizedBox(width: 3), pw.Text('033 37 808 18', style: smallTextStyle)]), + pw.Row(children: [iconGlobe, pw.SizedBox(width: 3), pw.Text('www.guycom.mg', style: smallTextStyle)]), + pw.Text('Facebook: GuyCom', style: smallTextStyle), + ], + ), + + // Section centrale - informations commande + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldTexClienttStyle), + pw.SizedBox(height: 6), + pw.Container(width: 150, height: 1, color: PdfColors.black), + pw.SizedBox(height: 6), pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, children: [ - pw.Text('REMISE', style: normalTextStyle), - pw.SizedBox(width: 20), - pw.Text('-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(0)}', - style: pw.TextStyle(fontSize: 10, color: PdfColors.red)), + pw.Container( + width: 80, + height: 35, + padding: const pw.EdgeInsets.all(4), + child: pw.Column( + children: [ + pw.Text('Boutique:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTexClienttStyle), + ] + ) + ), + pw.SizedBox(width: 8), + pw.Container( + width: 80, + height: 35, + padding: const pw.EdgeInsets.all(4), + child: pw.Column( + children: [ + pw.Text('Facture N°:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle), + ] + ) + ), ], ), - pw.SizedBox(height: 5), - pw.Container(width: 200, height: 1, color: PdfColors.black), - pw.SizedBox(height: 5), ], - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, + ), + + // Section client - compacte + pw.Container( + width: 200, + height: 80, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1), + ), + padding: const pw.EdgeInsets.all(8), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ - pw.Text('TOTAL', style: boldTextStyle), - pw.SizedBox(width: 20), - pw.Text('${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(0)}', style: boldTextStyle), + pw.Text('ID Client: ', style: frameTextStyle), + pw.SizedBox(height: 3), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle), + pw.SizedBox(height: 3), + pw.Container(width: 150, height: 1, color: PdfColors.black), + pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle), + pw.SizedBox(height: 3), + pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle), ], ), + ), + ], + ), + + pw.SizedBox(height: 10), + + // Tableau des produits avec cadeaux - optimisé pour paysage + pw.Table( + border: pw.TableBorder.all(width: 0.5), + columnWidths: { + 0: const pw.FlexColumnWidth(4), // Plus d'espace pour les désignations + 1: const pw.FlexColumnWidth(1), + 2: const pw.FlexColumnWidth(1.5), + 3: const pw.FlexColumnWidth(2), // Colonne remise/cadeau + 4: const pw.FlexColumnWidth(1.5), // Colonne montant + }, + 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('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)), + pw.Padding(padding: const pw.EdgeInsets.all(3), child: pw.Text('Remise/Cadeau', style: boldTextStyle, textAlign: pw.TextAlign.right)), + pw.Padding(padding: const pw.EdgeInsets.all(3), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)), ], ), - pw.SizedBox(height: 10), - - pw.Text('Arrêté à la somme de: ${_numberToWords((commande.montantApresRemise ?? commande.montantTotal).toInt())} Ariary', style: italicTextStyle), + ...detailsAvecProduits.map((item) { + final detail = item['detail'] as DetailCommande; + final produit = item['produit']; + + return pw.TableRow( + decoration: detail.estCadeau + ? const pw.BoxDecoration( + color: PdfColors.green50, + border: pw.Border( + left: pw.BorderSide( + color: PdfColors.green300, + width: 3, + ), + ), + ) + : detail.aRemise + ? const pw.BoxDecoration( + color: PdfColors.orange50, + border: pw.Border( + left: pw.BorderSide( + color: PdfColors.orange300, + width: 3, + ), + ), + ) + : null, + children: [ + pw.Padding( + padding: const pw.EdgeInsets.all(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: 8, fontWeight: pw.FontWeight.bold)), + ), + if (detail.estCadeau) + pw.Container( + padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 1), + decoration: pw.BoxDecoration( + color: PdfColors.green100, + borderRadius: pw.BorderRadius.circular(3), + ), + child: pw.Text( + 'CADEAU', + style: pw.TextStyle( + fontSize: 6, + fontWeight: pw.FontWeight.bold, + color: PdfColors.green700, + ), + ), + ), + ], + ), + pw.SizedBox(height: 1), + if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty) + pw.Text('${produit.category} - ${produit.marque}', style: smallTextStyle), + if (produit?.imei != null && produit!.imei!.isNotEmpty) + pw.Text('${produit.imei}', style: smallTextStyle), + if (produit?.reference != null && produit!.reference!.isNotEmpty && produit?.ram != null && produit!.ram!.isNotEmpty && produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty) + pw.Text('${produit.ram} | ${produit.memoireInterne} | ${produit.reference}', style: smallTextStyle), + ], + ), + ), + pw.Padding( + padding: const pw.EdgeInsets.all(3), + child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), + ), + pw.Padding( + padding: const pw.EdgeInsets.all(3), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (detail.estCadeau) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 6, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('GRATUIT', + style: pw.TextStyle( + fontSize: 7, + color: PdfColors.green700, + fontWeight: pw.FontWeight.bold, + )), + ] else if (detail.aRemise && detail.prixUnitaire != detail.sousTotal / detail.quantite) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 6, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 8, color: PdfColors.orange)), + ] else + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: normalTextStyle), + ], + ), + ), + pw.Padding( + padding: const pw.EdgeInsets.all(3), + child: pw.Text( + detail.estCadeau + ? 'CADEAU OFFERT' + : detail.aRemise + ? detail.remiseDescription + : '-', + style: pw.TextStyle( + fontSize: 7, + color: detail.estCadeau + ? PdfColors.green700 + : detail.aRemise + ? PdfColors.orange + : PdfColors.grey600, + fontWeight: detail.estCadeau ? pw.FontWeight.bold : pw.FontWeight.normal, + ), + textAlign: pw.TextAlign.right, + ), + ), + 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: 6, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('GRATUIT', + style: pw.TextStyle( + fontSize: 7, + fontWeight: pw.FontWeight.bold, + color: PdfColors.green700, + )), + ] else if (detail.aRemise && detail.sousTotal != detail.prixFinal) ...[ + pw.Text('${detail.sousTotal.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 6, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('${detail.prixFinal.toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), + ] else + pw.Text('${detail.prixFinal.toStringAsFixed(0)}', + style: normalTextStyle), + ], + ), + ), + ], + ); + }).toList(), + ], + ), + + pw.SizedBox(height: 8), + + // Sections inférieures en colonnes pour optimiser l'espace + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Colonne gauche - 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: normalTextStyle), + pw.SizedBox(width: 15), + pw.Text('${sousTotal.toStringAsFixed(0)}', style: normalTextStyle), + ], + ), + pw.SizedBox(height: 3), + ], + + if (totalRemises > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('REMISES TOTALES', style: pw.TextStyle(color: PdfColors.orange, fontSize: 8)), + pw.SizedBox(width: 15), + pw.Text('-${totalRemises.toStringAsFixed(0)}', + style: pw.TextStyle(color: PdfColors.orange, fontWeight: pw.FontWeight.bold, fontSize: 8)), + ], + ), + pw.SizedBox(height: 3), + ], + + if (totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('CADEAUX OFFERTS ($nombreCadeaux)', + style: pw.TextStyle(color: PdfColors.green700, fontSize: 8)), + pw.SizedBox(width: 15), + pw.Text('-${totalCadeaux.toStringAsFixed(0)}', + style: pw.TextStyle(color: PdfColors.green700, fontWeight: pw.FontWeight.bold, fontSize: 8)), + ], + ), + pw.SizedBox(height: 3), + ], + + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.Container( + width: 150, + height: 1, + color: PdfColors.black, + margin: const pw.EdgeInsets.symmetric(vertical: 3), + ), + ], + + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('TOTAL', style: boldTextStyle), + pw.SizedBox(width: 15), + pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle), + ], + ), + + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.SizedBox(height: 3), + pw.Text( + 'Économies réalisées: ${(totalRemises + totalCadeaux).toStringAsFixed(0)} MGA', + style: pw.TextStyle( + fontSize: 7, + color: PdfColors.green, + fontStyle: pw.FontStyle.italic, + ), + ), + ], + ], + ), + ), - pw.SizedBox(height: 30), + pw.SizedBox(width: 20), - // Signatures - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text('Signature du vendeur', style: smallTextStyle), - pw.SizedBox(height: 20), - pw.Container(width: 150, height: 1, color: PdfColors.black), - ], + // Colonne droite - Informations vendeurs + pw.Expanded( + flex: 3, + child: pw.Container( + padding: const pw.EdgeInsets.all(8), + decoration: pw.BoxDecoration( + color: PdfColors.grey100, + borderRadius: pw.BorderRadius.circular(6), + border: pw.Border.all(color: PdfColors.grey300), ), - pw.Column( + child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('Signature du client', style: smallTextStyle), - pw.SizedBox(height: 20), - pw.Container(width: 150, height: 1, color: PdfColors.black), + pw.Text( + 'INFORMATIONS VENDEURS', + style: pw.TextStyle( + fontSize: 9, + fontWeight: pw.FontWeight.bold, + color: PdfColors.blue700, + ), + ), + pw.SizedBox(height: 6), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Expanded( + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Vendeur initiateur:', + style: pw.TextStyle( + fontSize: 7, + fontWeight: pw.FontWeight.bold, + color: PdfColors.grey700, + ), + ), + pw.SizedBox(height: 1), + pw.Text( + commandeur != null + ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() + : 'Non spécifié', + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.black, + ), + ), + pw.SizedBox(height: 2), + pw.Text( + 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', + style: pw.TextStyle( + fontSize: 6, + color: PdfColors.grey600, + ), + ), + ], + ), + ), + pw.Container( + width: 1, + height: 30, + color: PdfColors.grey400, + ), + pw.SizedBox(width: 15), + pw.Expanded( + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Vendeur validateur:', + style: pw.TextStyle( + fontSize: 7, + fontWeight: pw.FontWeight.bold, + color: PdfColors.grey700, + ), + ), + pw.SizedBox(height: 1), + pw.Text( + validateur != null + ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() + : 'Non spécifié', + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.black, + ), + ), + pw.SizedBox(height: 2), + pw.Text( + 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now())}', + style: pw.TextStyle( + fontSize: 6, + color: PdfColors.grey600, + ), + ), + ], + ), + ), + ], + ), ], ), + ), + ), + ], + ), + + pw.SizedBox(height: 8), + + // Montant en lettres + pw.Text('Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', style: italicTextStyle), + + pw.SizedBox(height: 8), + + // Note de remerciement pour les cadeaux - compacte + if (totalCadeaux > 0) ...[ + pw.Container( + padding: const pw.EdgeInsets.all(6), + decoration: pw.BoxDecoration( + color: PdfColors.blue50, + borderRadius: pw.BorderRadius.circular(4), + ), + child: pw.Row( + children: [ + pw.Text('🎁 ', style: emojifont), + pw.Expanded( + child: pw.Text( + 'Merci de votre confiance ! Nous espérons que nos cadeaux vous feront plaisir. ($nombreCadeaux article(s) offert(s) - Valeur: ${totalCadeaux.toStringAsFixed(0)} MGA)', + style: pw.TextStyle( + fontSize: 7, + fontStyle: pw.FontStyle.italic, + color: PdfColors.blue700, + ), + ), + ), + ], + ), + ), + pw.SizedBox(height: 8), + ], + + // Signatures - horizontales et compactes + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Signature vendeur initiateur', + style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold), + ), + pw.SizedBox(height: 1), + pw.Text( + commandeur != null + ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() + : 'Non spécifié', + style: pw.TextStyle(fontSize: 6, color: PdfColors.grey600), + ), + pw.SizedBox(height: 15), + pw.Container(width: 100, height: 1, color: PdfColors.black), + ], + ), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Signature vendeur validateur', + style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold), + ), + pw.SizedBox(height: 1), + pw.Text( + validateur != null + ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() + : 'Non spécifié', + style: pw.TextStyle(fontSize: 6, color: PdfColors.grey600), + ), + pw.SizedBox(height: 15), + pw.Container(width: 100, height: 1, color: PdfColors.black), + ], + ), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Signature du client', + style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold), + ), + pw.SizedBox(height: 1), + pw.Text( + client?.nomComplet ?? 'Non spécifié', + style: pw.TextStyle(fontSize: 6, color: PdfColors.grey600), + ), + pw.SizedBox(height: 15), + pw.Container(width: 100, height: 1, color: PdfColors.black), ], ), ], - ); - }, - ), - ); + ), + ], + ); + }, + ), +); - final output = await getTemporaryDirectory(); - final file = File('${output.path}/facture_${commande.id}.pdf'); - await file.writeAsBytes(await pdf.save()); - await OpenFile.open(file.path); - } +final output = await getTemporaryDirectory(); +final file = File('${output.path}/facture_${commande.id}.pdf'); +await file.writeAsBytes(await pdf.save()); +await OpenFile.open(file.path); +} String _numberToWords(int number) { NumbersToLetters.toLetters('fr', number); return NumbersToLetters.toLetters('fr', number); } - Future _generateReceipt(Commande commande, PaymentMethod payment) async { - final details = await _database.getDetailsCommande(commande.id!); - final client = await _database.getClientById(commande.clientId); - final commandeur = commande.commandeurId != null - ? await _database.getUserById(commande.commandeurId!) - : null; - final validateur = commande.validateurId != null - ? await _database.getUserById(commande.validateurId!) - : null; - final pointDeVente = commandeur?.pointDeVenteId != null - ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) - : null; - final List> detailsAvecProduits = []; - for (final detail in details) { - final produit = await _database.getProductById(detail.produitId); - detailsAvecProduits.add({ - 'detail': detail, - 'produit': produit, - }); - } +Future _generateInvoiceWithPasswordVerification(Commande commande) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return PasswordVerificationDialog( + title: 'Génération de facture', + message: 'Pour générer la facture de la commande #${commande.id}, veuillez confirmer votre identité en saisissant votre mot de passe.', + onPasswordVerified: (String password) async { + // Afficher un indicateur de chargement + Get.dialog( + const Center( + child: CircularProgressIndicator(), + ), + barrierDismissible: false, + ); + + try { + await _generateInvoice(commande); + Get.back(); // Fermer l'indicateur de chargement + + Get.snackbar( + 'Succès', + 'Facture générée avec succès', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } catch (e) { + Get.back(); // Fermer l'indicateur de chargement + Get.snackbar( + 'Erreur', + 'Erreur lors de la génération de la facture: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + }, + ); + }, + ); +} +Future _generateBon_lifraisonWithPasswordVerification(Commande commande) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return PasswordVerificationDialog( + title: 'Génération de Bon de livraison', + message: 'Pour générer de Bon de livraison de la commande #${commande.id}, veuillez confirmer votre identité en saisissant votre mot de passe.', + onPasswordVerified: (String password) async { + // Afficher un indicateur de chargement + Get.dialog( + const Center( + child: CircularProgressIndicator(), + ), + barrierDismissible: false, + ); + + try { + await _generateBonLivraison(commande); + Get.back(); // Fermer l'indicateur de chargement + + Get.snackbar( + 'Succès', + 'Facture générée avec succès', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } catch (e) { + Get.back(); // Fermer l'indicateur de chargement + Get.snackbar( + 'Erreur', + 'Erreur lors de la génération de la facture: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + }, + ); + }, + ); +} - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - pdf.addPage( - pw.Page( - pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity), - margin: const pw.EdgeInsets.all(4), - build: (pw.Context context) { - return pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Center( - child: pw.Container( - width: 40, - height: 40, - child: pw.Image(image), - ), - ), - pw.SizedBox(height: 4), - - pw.Text('GUYCOM MADAGASCAR', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - )), - pw.Text('Tél: 033 37 808 18', style: const pw.TextStyle(fontSize: 7)), - pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)), - - pw.SizedBox(height: 6), - - pw.Text('TICKET DE CAISSE', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - decoration: pw.TextDecoration.underline, - )), - pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}', - style: const pw.TextStyle(fontSize: 8)), - pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', - style: const pw.TextStyle(fontSize: 8)), - - if (pointDeVente != null) - pw.Text('Point de vente: ${pointDeVente['designation']}', - style: const pw.TextStyle(fontSize: 8)), - - pw.Divider(thickness: 0.5), - - pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', - style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), - if (client?.telephone != null) - pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)), - - if (commandeur != null || validateur != null) - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, +String _getPaymentMethodLabel(PaymentMethod payment) { + switch (payment.type) { + case PaymentType.cash: + return 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)'; + case PaymentType.card: + return 'CARTE BANCAIRE'; + case PaymentType.mvola: + return 'MVOLA'; + case PaymentType.orange: + return 'ORANGE MONEY'; + case PaymentType.airtel: + return 'AIRTEL MONEY'; + default: + return 'MÉTHODE INCONNUE (${payment.type.toString()})'; // Debug info + } +} + Future _generateReceipt(Commande commande, PaymentMethod payment) async { + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + final commandeur = commande.commandeurId != null + ? await _database.getUserById(commande.commandeurId!) + : null; + final validateur = commande.validateurId != null + ? await _database.getUserById(commande.validateurId!) + : null; + final pointDeVente = commandeur?.pointDeVenteId != null + ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) + : null; +final emojiSuportFont = pw.Font.ttf( await rootBundle.load('assets/NotoEmoji-Regular.ttf')); + final emojifont = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); + final List> detailsAvecProduits = []; + for (final detail in details) { + final produit = await _database.getProductById(detail.produitId); + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + } + + double sousTotal = 0; + double totalRemises = 0; + double totalCadeaux = 0; + int nombreCadeaux = 0; + + for (final detail in details) { + sousTotal += detail.sousTotal; + if (detail.estCadeau) { + totalCadeaux += detail.sousTotal; + nombreCadeaux += detail.quantite; + } else { + totalRemises += detail.montantRemise; + } + } + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + // DEBUG: Affichage des informations de paiement + print('=== DEBUG PAYMENT METHOD ==='); + print('Payment type: ${payment.type}'); + print('Payment type toString: ${payment.type.toString()}'); + print('Payment type runtimeType: ${payment.type.runtimeType}'); + print('Payment type index: ${payment.type.index}'); + print('Amount given: ${payment.amountGiven}'); + print('PaymentType.airtel: ${PaymentType.airtel}'); + print('payment.type == PaymentType.airtel: ${payment.type == PaymentType.airtel}'); + print('=== END DEBUG ==='); + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity), + margin: const pw.EdgeInsets.all(4), + build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Center( + child: pw.Container( + width: 40, + height: 40, + child: pw.Image(image), + ), + ), + pw.SizedBox(height: 4), + + pw.Text('GUYCOM MADAGASCAR', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + )), + pw.Text('Tél: 033 37 808 18', style: const pw.TextStyle(fontSize: 7)), + pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)), + + pw.SizedBox(height: 6), + + pw.Text('TICKET DE CAISSE', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + decoration: pw.TextDecoration.underline, + )), + pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}', + style: const pw.TextStyle(fontSize: 8)), + pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', + style: const pw.TextStyle(fontSize: 8)), + + if (pointDeVente != null) + pw.Text('Point de vente: ${pointDeVente['designation']}', + style: const pw.TextStyle(fontSize: 8)), + + pw.Divider(thickness: 0.5), + + pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', + style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), + if (client?.telephone != null) + pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)), + + if (commandeur != null || validateur != null) + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Divider(thickness: 0.5), + if (commandeur != null) + pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)), + if (validateur != null) + pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)), + ], + ), + + pw.Divider(thickness: 0.5), + + // Tableau des produits avec cadeaux + pw.Table( + columnWidths: { + 0: const pw.FlexColumnWidth(3.5), + 1: const pw.FlexColumnWidth(1), + 2: const pw.FlexColumnWidth(1.5), + }, + children: [ + pw.TableRow( children: [ - pw.Divider(thickness: 0.5), - if (commandeur != null) - pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)), - if (validateur != null) - pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)), + pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), ], - ), - - pw.Divider(thickness: 0.5), - - // Détails des produits - pw.Table( - columnWidths: { - 0: const pw.FlexColumnWidth(3.5), - 1: const pw.FlexColumnWidth(1), - 2: const pw.FlexColumnWidth(1.5), - }, - children: [ - pw.TableRow( - children: [ - pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), - pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), - pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), - ], - decoration: const pw.BoxDecoration( - border: pw.Border(bottom: pw.BorderSide(width: 0.5)), - ), + decoration: const pw.BoxDecoration( + border: pw.Border(bottom: pw.BorderSide(width: 0.5)), ), + ), + + ...detailsAvecProduits.map((item) { + final detail = item['detail'] as DetailCommande; + final produit = item['produit']; - ...detailsAvecProduits.map((item) { - final detail = item['detail'] as DetailCommande; - final produit = item['produit']; - - return pw.TableRow( - decoration: const pw.BoxDecoration( - border: pw.Border(bottom: pw.BorderSide(width: 0.2))), - children: [ - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Row( - children: [ - pw.Text(detail.produitNom ?? 'Produit', + return pw.TableRow( + decoration: const pw.BoxDecoration( + border: pw.Border(bottom: pw.BorderSide(width: 0.2))), + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Row( + children: [ + pw.Expanded( + child: pw.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)), - if (detail.estCadeau == true) - pw.Text(' (CADEAU)', - style: pw.TextStyle(fontSize: 6, color: PdfColors.red)), - ], - ), - if (produit?.reference != null) - pw.Text('Ref: ${produit!.reference}', - style: const pw.TextStyle(fontSize: 6)), - if (produit?.imei != null) - pw.Text('IMEI: ${produit!.imei}', - style: const pw.TextStyle(fontSize: 6)), - ], - ), - pw.Text(detail.quantite.toString(), - style: const pw.TextStyle(fontSize: 7)), - pw.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(0)}', - style: const pw.TextStyle(fontSize: 7)), - ], - ); - }), + ), + if (detail.estCadeau) + pw.Text('🎁', style: emojifont), + ], + ), + if (produit?.reference != null) + pw.Text('Ref: ${produit!.reference}', + style: const pw.TextStyle(fontSize: 6)), + if (produit?.imei != null) + pw.Text('IMEI: ${produit!.imei}', + style: const pw.TextStyle(fontSize: 6)), + if (detail.estCadeau) + pw.Text('CADEAU OFFERT', + style: pw.TextStyle( + fontSize: 6, + color: PdfColors.green700, + fontWeight: pw.FontWeight.bold, + )), + if (detail.aRemise && !detail.estCadeau) + pw.Text('Remise: ${detail.remiseDescription}', + style: pw.TextStyle(fontSize: 6, color: PdfColors.orange)), + ], + ), + pw.Text(detail.quantite.toString(), + style: const pw.TextStyle(fontSize: 7)), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (detail.estCadeau) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 6, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('GRATUIT', + style: pw.TextStyle( + fontSize: 7, + color: PdfColors.green700, + fontWeight: pw.FontWeight.bold, + )), + ] else if (detail.aRemise && detail.prixUnitaire != detail.prixFinal / detail.quantite) ...[ + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 6, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + )), + pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', + style: const pw.TextStyle(fontSize: 7)), + ] else + pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', + style: const pw.TextStyle(fontSize: 7)), + ], + ), + ], + ); + }), + ], + ), + + pw.Divider(thickness: 0.5), + + // Totaux avec remises et cadeaux pour le ticket + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('SOUS-TOTAL:', + style: const pw.TextStyle(fontSize: 8)), + pw.Text('${sousTotal.toStringAsFixed(0)} MGA', + style: const pw.TextStyle(fontSize: 8)), ], ), - pw.Divider(thickness: 0.5), - - // Totaux avec remise - if (commande.montantApresRemise != null) ...[ + if (totalRemises > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - pw.Text('SOUS-TOTAL:', style: const pw.TextStyle(fontSize: 8)), - pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', - style: const pw.TextStyle(fontSize: 8)), + pw.Text('REMISES:', + style: pw.TextStyle(fontSize: 8, color: PdfColors.orange)), + pw.Text('-${totalRemises.toStringAsFixed(0)} MGA', + style: pw.TextStyle(fontSize: 8, color: PdfColors.orange)), ], ), + ], + + if (totalCadeaux > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - pw.Text('REMISE:', style: const pw.TextStyle(fontSize: 8)), - pw.Text('-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(0)} MGA', - style: pw.TextStyle(fontSize: 8, color: PdfColors.red)), + pw.Text('CADEAUX ($nombreCadeaux):', + style: pw.TextStyle(fontSize: 8, color: PdfColors.green700)), + pw.Text('-${totalCadeaux.toStringAsFixed(0)} MGA', + style: pw.TextStyle(fontSize: 8, color: PdfColors.green700)), ], ), - pw.SizedBox(height: 3), ], - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text('TOTAL:', - style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), - pw.Text('${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(0)} MGA', - style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), - ], + pw.Divider(thickness: 0.3), + ], + + // Total final + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('TOTAL:', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + ], + ), + + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.SizedBox(height: 4), + pw.Text('Économies: ${(totalRemises + totalCadeaux).toStringAsFixed(0)} MGA !', + style: pw.TextStyle( + fontSize: 7, + color: PdfColors.green, + fontStyle: pw.FontStyle.italic, + ), + textAlign: pw.TextAlign.center, ), - - pw.SizedBox(height: 6), - - // Détails du paiement - pw.Text('MODE DE PAIEMENT:', + ], + + pw.Divider(thickness: 0.5), + + // Détails du paiement + pw.Text('MODE DE PAIEMENT:', + style: const pw.TextStyle(fontSize: 8)), + pw.Text( + _getPaymentMethodLabel(payment), + style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), +), + + if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal) + pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(0)} MGA', style: const pw.TextStyle(fontSize: 8)), - pw.Text( - payment.type == PaymentType.cash - ? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)' - : payment.type == PaymentType.card - ? 'CARTE BANCAIRE' - : payment.type == PaymentType.mvola - ? 'MVOLA' - : payment.type == PaymentType.orange - ? 'ORANGE MONEY' - : 'AIRTEL MONEY', - style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), + + pw.SizedBox(height: 8), + + // Messages de fin avec cadeaux + if (totalCadeaux > 0) ...[ + pw.Container( + padding: const pw.EdgeInsets.all(4), + decoration: pw.BoxDecoration( + color: PdfColors.green50, + borderRadius: pw.BorderRadius.circular(4), + ), + child: pw.Column( + children: [ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + + pw.Text('🎁', + style: emojifont, + textAlign: pw.TextAlign.center, + ), + pw.Text('Profitez de vos cadeaux !', + style: pw.TextStyle( + fontSize: 7, + fontWeight: pw.FontWeight.bold, + color: PdfColors.green700, + ), + textAlign: pw.TextAlign.center, + ), + pw.Text('🎁', + style: emojifont, + textAlign: pw.TextAlign.center, + ), + ] + ), + pw.Text('$nombreCadeaux article(s) offert(s)', + style: pw.TextStyle( + fontSize: 6, + color: PdfColors.green600, + ), + textAlign: pw.TextAlign.center, + ), + ], + ), ), - - if (payment.type == PaymentType.cash && payment.amountGiven > (commande.montantApresRemise ?? commande.montantTotal)) - pw.Text('Monnaie rendue: ${(payment.amountGiven - (commande.montantApresRemise ?? commande.montantTotal)).toStringAsFixed(0)} MGA', - style: const pw.TextStyle(fontSize: 8)), - - pw.SizedBox(height: 12), - - pw.Text('Article non échangeable - Garantie selon conditions', - style: const pw.TextStyle(fontSize: 6)), - pw.Text('Ticket à conserver comme justificatif', - style: const pw.TextStyle(fontSize: 6)), - pw.SizedBox(height: 8), - pw.Text('Merci pour votre confiance !', - style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)), + pw.SizedBox(height: 6), ], - ); - }, - ), - ); + + pw.Text('Article non échangeable - Garantie selon conditions', + style: const pw.TextStyle(fontSize: 6)), + pw.Text('Ticket à conserver comme justificatif', + style: const pw.TextStyle(fontSize: 6)), + pw.SizedBox(height: 8), + pw.Text('Merci pour votre confiance !', + style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)), + ], + ); + }, + ), + ); - final output = await getTemporaryDirectory(); - final file = File('${output.path}/ticket_${commande.id}.pdf'); - await file.writeAsBytes(await pdf.save()); - await OpenFile.open(file.path); - } + final output = await getTemporaryDirectory(); + final file = File('${output.path}/ticket_${commande.id}.pdf'); + await file.writeAsBytes(await pdf.save()); + await OpenFile.open(file.path); +} Color _getStatutColor(StatutCommande statut) { switch (statut) { @@ -856,8 +1907,6 @@ class _GestionCommandesPageState extends State { } } - - String statutLibelle(StatutCommande statut) { switch (statut) { case StatutCommande.enAttente: @@ -874,7 +1923,8 @@ class _GestionCommandesPageState extends State { _searchController.dispose(); super.dispose(); } - @override + + @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar(title: 'Gestion des Commandes'), @@ -905,7 +1955,8 @@ class _GestionCommandesPageState extends State { BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, - offset: const Offset(0, 2),) + offset: const Offset(0, 2), + ) ], ), child: ClipRRect( @@ -966,7 +2017,8 @@ class _GestionCommandesPageState extends State { BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, - offset: const Offset(0, 2),) + offset: const Offset(0, 2), + ) ], ), child: TextField( @@ -1156,7 +2208,8 @@ class _GestionCommandesPageState extends State { BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, - offset: const Offset(0, 2),) + offset: const Offset(0, 2), + ) ], ), padding: @@ -1227,217 +2280,268 @@ class _GestionCommandesPageState extends State { ), ) : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: _filteredCommandes.length, - itemBuilder: (context, index) { - final commande = _filteredCommandes[index]; - return Container( - margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _filteredCommandes.length, + itemBuilder: (context, index) { + final commande = _filteredCommandes[index]; + + return FutureBuilder>( + future: _database.getDetailsCommande(commande.id!), + builder: (context, snapshot) { + double totalRemises = 0; + bool aDesRemises = false; + + if (snapshot.hasData) { + for (final detail in snapshot.data!) { + totalRemises += detail.montantRemise; + if (detail.aRemise) aDesRemises = true; + } + } + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: _getStatutColor(commande.statut), + borderRadius: BorderRadius.circular(12), + border: aDesRemises + ? Border.all(color: Colors.orange.shade300, width: 2) + : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + leading: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + border: aDesRemises + ? Border.all(color: Colors.orange.shade300, width: 2) + : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + aDesRemises ? Icons.discount : _getStatutIcon(commande.statut), + size: 20, + color: aDesRemises + ? Colors.teal.shade700 + : commande.statut == StatutCommande.annulee + ? Colors.red + : Colors.blue.shade600, + ), + Text( + '#${commande.id}', + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + title: Text( + commande.clientNomComplet, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + DateFormat('dd/MM/yyyy').format(commande.dateCommande), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + commande.statutLibelle, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: commande.statut == StatutCommande.annulee + ? Colors.red + : Colors.blue.shade700, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.attach_money, + size: 14, + color: Colors.green.shade600, + ), + const SizedBox(width: 4), + Text( + '${commande.montantTotal.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + // Affichage des remises si elles existent + if (totalRemises > 0) ...[ + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), decoration: BoxDecoration( - color: _getStatutColor(commande.statut), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(10), ), - child: ExpansionTile( - tilePadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - leading: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(25), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - _getStatutIcon(commande.statut), - size: 20, - color: - commande.statut == StatutCommande.annulee - ? Colors.red - : Colors.blue.shade600, - ), - Text( - '#${commande.id}', - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - title: Text( - commande.clientNomComplet, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.calendar_today, - size: 14, - color: Colors.grey.shade600, - ), - const SizedBox(width: 4), - Text( - DateFormat('dd/MM/yyyy') - .format(commande.dateCommande), - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - const SizedBox(width: 16), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - commande.statutLibelle, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: commande.statut == - StatutCommande.annulee - ? Colors.red - : Colors.blue.shade700, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.attach_money, - size: 14, - color: Colors.green.shade600, - ), - const SizedBox(width: 4), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (commande.montantApresRemise != null) ...[ - Text( - 'Total: ${commande.montantTotal.toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - decoration: TextDecoration.lineThrough, - ), - ), - Text( - 'Final: ${commande.montantApresRemise!.toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.green.shade700, - ), - ), - ] else - Text( - '${commande.montantTotal.toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.green.shade700, - ), - ), - ], - ), - ], - ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: IconButton( - icon: Icon( - Icons.receipt_long, - color: Colors.blue.shade600, - ), - onPressed: () => _generateInvoice(commande), - tooltip: 'Générer la facture', - ), - ), - ], - ), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: const EdgeInsets.all(16.0), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(12), - bottomRight: Radius.circular(12), - ), - ), - child: Column( - children: [ - CommandeDetails(commande: commande), - const SizedBox(height: 16), - if (commande.statut != StatutCommande.annulee) - CommandeActions( - commande: commande, - onStatutChanged: _updateStatut, - onPaymentSelected: _showPaymentOptions, - onDiscountSelected: _showDiscountDialog, - onGiftSelected: _showGiftDialog, - ), - ], + Icon( + Icons.discount, + size: 12, + color: Colors.teal.shade700, + ), + const SizedBox(width: 2), + Text( + '-${totalRemises.toStringAsFixed(0)}', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.teal.shade700, ), ), ], ), - ); - }, + ), + ], + ], + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: IconButton( + icon: Icon( + Icons.receipt_outlined, + color: Colors.blue.shade600, + ), + onPressed: () => _generateBon_lifraisonWithPasswordVerification(commande), + tooltip: 'Générer le Bon de livraison', + ), + ), + const SizedBox(width: 10,), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: IconButton( + icon: Icon( + Icons.receipt_long, + color: Colors.blue.shade600, + ), + onPressed: () => _generateInvoiceWithPasswordVerification(commande), + tooltip: 'Générer la facture', + ), + ), + ], + ), + children: [ + Container( + padding: const EdgeInsets.all(16.0), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), ), + ), + child: Column( + children: [ + CommandeDetails(commande: commande), + const SizedBox(height: 16), + if (commande.statut != StatutCommande.annulee) + CommandeActions( + commande: commande, + onStatutChanged: _updateStatut, + onPaymentSelected: _showPaymentOptions, + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, +) ), ], ), ); } -} +} \ No newline at end of file diff --git a/lib/Views/mobilepage.dart b/lib/Views/mobilepage.dart index db5711d..77f7f64 100644 --- a/lib/Views/mobilepage.dart +++ b/lib/Views/mobilepage.dart @@ -1,15 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; -import 'package:youmazgestion/Components/QrScan.dart'; -import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; -import 'package:youmazgestion/Models/client.dart'; -import 'package:youmazgestion/Models/users.dart'; -import 'package:youmazgestion/Models/produit.dart'; -import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/Views/historique.dart'; - void main() { runApp(const MyApp()); } @@ -39,2175 +31,14 @@ class MainLayout extends StatefulWidget { } class _MainLayoutState extends State { - int _currentIndex = 1; // Index par défaut pour la page de commande +// Index par défaut pour la page de commande - final List _pages = [ - const HistoriquePage(), - const NouvelleCommandePage(), // Page 1 - Nouvelle commande - const ScanQRPage(), // Page 2 - Scan QR - ]; @override Widget build(BuildContext context) { return Scaffold( - appBar: _currentIndex == 1 ? CustomAppBar(title: 'Nouvelle Commande') : null, drawer: CustomDrawer(), - body: _pages[_currentIndex], - bottomNavigationBar: _buildAdaptiveBottomNavBar(), - ); - } - - Widget _buildAdaptiveBottomNavBar() { - final isDesktop = MediaQuery.of(context).size.width > 600; - - return Container( - decoration: BoxDecoration( - border: isDesktop - ? const Border(top: BorderSide(color: Colors.grey, width: 0.5)) - : null, - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - setState(() { - _currentIndex = index; - }); - }, - // Style adapté pour desktop - type: isDesktop ? BottomNavigationBarType.fixed : BottomNavigationBarType.fixed, - selectedFontSize: isDesktop ? 14 : 12, - unselectedFontSize: isDesktop ? 14 : 12, - iconSize: isDesktop ? 28 : 24, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.history), - label: 'Historique', - ), - BottomNavigationBarItem( - icon: Icon(Icons.add_shopping_cart), - label: 'Commande', - ), - BottomNavigationBarItem( - icon: Icon(Icons.qr_code_scanner), - label: 'Scan QR', - ), - ], - ), + body: const HistoriquePage(), ); } } - -class NouvelleCommandePage extends StatefulWidget { - const NouvelleCommandePage({super.key}); - - @override - _NouvelleCommandePageState createState() => _NouvelleCommandePageState(); -} - -class _NouvelleCommandePageState extends State { - final AppDatabase _appDatabase = AppDatabase.instance; - final _formKey = GlobalKey(); - bool _isLoading = false; - - // Contrôleurs client - final TextEditingController _nomController = TextEditingController(); - final TextEditingController _prenomController = TextEditingController(); - final TextEditingController _emailController = TextEditingController(); - final TextEditingController _telephoneController = TextEditingController(); - final TextEditingController _adresseController = TextEditingController(); - - // Contrôleurs pour les filtres - final TextEditingController _searchNameController = TextEditingController(); - final TextEditingController _searchImeiController = TextEditingController(); - final TextEditingController _searchReferenceController = TextEditingController(); - - // Panier - final List _products = []; - final List _filteredProducts = []; - final Map _quantites = {}; - - // Variables de filtre - bool _showOnlyInStock = false; - - // Utilisateurs commerciaux - List _commercialUsers = []; - Users? _selectedCommercialUser; - - // Variables pour les suggestions clients - List _clientSuggestions = []; - bool _showNomSuggestions = false; - bool _showTelephoneSuggestions = false; - GlobalKey _nomFieldKey = GlobalKey(); - GlobalKey _telephoneFieldKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _loadProducts(); - _loadCommercialUsers(); - - // Listeners pour les filtres - _searchNameController.addListener(_filterProducts); - _searchImeiController.addListener(_filterProducts); - _searchReferenceController.addListener(_filterProducts); - - // Listeners pour l'autocomplétion client - _nomController.addListener(() { - if (_nomController.text.length >= 3) { - _showClientSuggestions(_nomController.text, isNom: true); - } else { - _hideNomSuggestions(); - } - }); - - _telephoneController.addListener(() { - if (_telephoneController.text.length >= 3) { - _showClientSuggestions(_telephoneController.text, isNom: false); - } else { - _hideTelephoneSuggestions(); - } - }); - } - - // Méthode pour vider complètement le formulaire et le panier - void _clearFormAndCart() { - setState(() { - // Vider les contrôleurs client - _nomController.clear(); - _prenomController.clear(); - _emailController.clear(); - _telephoneController.clear(); - _adresseController.clear(); - - // Vider le panier - _quantites.clear(); - - // Réinitialiser le commercial au premier de la liste - if (_commercialUsers.isNotEmpty) { - _selectedCommercialUser = _commercialUsers.first; - } - - // Masquer toutes les suggestions - _hideAllSuggestions(); - - // Réinitialiser l'état de chargement - _isLoading = false; - }); - } - - Future _showClientSuggestions(String query, {required bool isNom}) async { - if (query.length < 3) { - _hideAllSuggestions(); - return; - } - - final suggestions = await _appDatabase.suggestClients(query); - - setState(() { - _clientSuggestions = suggestions; - if (isNom) { - _showNomSuggestions = true; - _showTelephoneSuggestions = false; - } else { - _showTelephoneSuggestions = true; - _showNomSuggestions = false; - } - }); -} - - void _showOverlay({required bool isNom}) { - // Utiliser une approche plus simple avec setState - setState(() { - _clientSuggestions = _clientSuggestions; - if (isNom) { - _showNomSuggestions = true; - _showTelephoneSuggestions = false; - } else { - _showTelephoneSuggestions = true; - _showNomSuggestions = false; - } - }); - } - - void _fillClientForm(Client client) { - setState(() { - _nomController.text = client.nom; - _prenomController.text = client.prenom; - _emailController.text = client.email; - _telephoneController.text = client.telephone; - _adresseController.text = client.adresse ?? ''; - }); - - Get.snackbar( - 'Client trouvé', - 'Les informations ont été remplies automatiquement', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } - - void _hideNomSuggestions() { - if (mounted && _showNomSuggestions) { - setState(() { - _showNomSuggestions = false; - }); - } - } - - void _hideTelephoneSuggestions() { - if (mounted && _showTelephoneSuggestions){ - setState(() { - _showTelephoneSuggestions = false; - }); - } - } - - void _hideAllSuggestions() { - _hideNomSuggestions(); - _hideTelephoneSuggestions(); - } - - Future _loadProducts() async { - final products = await _appDatabase.getProducts(); - setState(() { - _products.clear(); - _products.addAll(products); - _filteredProducts.clear(); - _filteredProducts.addAll(products); - }); - } - - Future _loadCommercialUsers() async { - final commercialUsers = await _appDatabase.getCommercialUsers(); - setState(() { - _commercialUsers = commercialUsers; - if (_commercialUsers.isNotEmpty) { - _selectedCommercialUser = _commercialUsers.first; - } - }); - } - - void _filterProducts() { - final nameQuery = _searchNameController.text.toLowerCase(); - final imeiQuery = _searchImeiController.text.toLowerCase(); - final referenceQuery = _searchReferenceController.text.toLowerCase(); - - setState(() { - _filteredProducts.clear(); - - for (var product in _products) { - bool matchesName = nameQuery.isEmpty || - product.name.toLowerCase().contains(nameQuery); - - bool matchesImei = imeiQuery.isEmpty || - (product.imei?.toLowerCase().contains(imeiQuery) ?? false); - - bool matchesReference = referenceQuery.isEmpty || - (product.reference?.toLowerCase().contains(referenceQuery) ?? false); - - bool matchesStock = !_showOnlyInStock || - (product.stock != null && product.stock! > 0); - - if (matchesName && matchesImei && matchesReference && matchesStock) { - _filteredProducts.add(product); - } - } - }); - } - - void _toggleStockFilter() { - setState(() { - _showOnlyInStock = !_showOnlyInStock; - }); - _filterProducts(); - } - - void _clearFilters() { - setState(() { - _searchNameController.clear(); - _searchImeiController.clear(); - _searchReferenceController.clear(); - _showOnlyInStock = false; - }); - _filterProducts(); - } - - // Section des filtres adaptée pour mobile - Widget _buildFilterSection() { - final isMobile = MediaQuery.of(context).size.width < 600; - - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.filter_list, color: Colors.blue.shade700), - const SizedBox(width: 8), - const Text( - 'Filtres de recherche', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 9, 56, 95), - ), - ), - const Spacer(), - TextButton.icon( - onPressed: _clearFilters, - icon: const Icon(Icons.clear, size: 18), - label: isMobile ? const SizedBox() : const Text('Réinitialiser'), - style: TextButton.styleFrom( - foregroundColor: Colors.grey.shade600, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Champ de recherche par nom - TextField( - controller: _searchNameController, - decoration: InputDecoration( - labelText: 'Rechercher par nom', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - const SizedBox(height: 12), - - if (!isMobile) ...[ - // Version desktop - champs sur la même ligne - Row( - children: [ - Expanded( - child: TextField( - controller: _searchImeiController, - decoration: InputDecoration( - labelText: 'IMEI', - prefixIcon: const Icon(Icons.phone_android), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: _searchReferenceController, - decoration: InputDecoration( - labelText: 'Référence', - prefixIcon: const Icon(Icons.qr_code), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - ), - ], - ), - ] else ...[ - // Version mobile - champs empilés - TextField( - controller: _searchImeiController, - decoration: InputDecoration( - labelText: 'IMEI', - prefixIcon: const Icon(Icons.phone_android), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - const SizedBox(height: 12), - TextField( - controller: _searchReferenceController, - decoration: InputDecoration( - labelText: 'Référence', - prefixIcon: const Icon(Icons.qr_code), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - ], - const SizedBox(height: 16), - - // Boutons de filtre adaptés pour mobile - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - ElevatedButton.icon( - onPressed: _toggleStockFilter, - icon: Icon( - _showOnlyInStock ? Icons.inventory : Icons.inventory_2, - size: 20, - ), - label: Text(_showOnlyInStock - ? isMobile ? 'Tous' : 'Afficher tous' - : isMobile ? 'En stock' : 'Stock disponible'), - style: ElevatedButton.styleFrom( - backgroundColor: _showOnlyInStock - ? Colors.green.shade600 - : Colors.blue.shade600, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 16, - vertical: 8 - ), - ), - ), - ], - ), - - const SizedBox(height: 8), - - // Compteur de résultats - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8 - ), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${_filteredProducts.length} produit(s)', - style: TextStyle( - color: Colors.blue.shade700, - fontWeight: FontWeight.w600, - fontSize: isMobile ? 12 : 14, - ), - ), - ), - ], - ), - ), - ); - } - -// Variables pour le scanner - QRViewController? _qrController; - bool _isScanning = false; - final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); - - // 4. Méthode pour démarrer le scan - void _startBarcodeScanning() { - if (_isScanning) return; - - setState(() { - _isScanning = true; - }); - - Get.to(() => _buildScannerPage())?.then((_) { - setState(() { - _isScanning = false; - }); - }); - } - - // 5. Page du scanner - Widget _buildScannerPage() { - return Scaffold( - appBar: AppBar( - title: const Text('Scanner IMEI'), - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _qrController?.dispose(); - Get.back(); - }, - ), - actions: [ - IconButton( - icon: const Icon(Icons.flash_on), - onPressed: () async { - await _qrController?.toggleFlash(); - }, - ), - IconButton( - icon: const Icon(Icons.flip_camera_ios), - onPressed: () async { - await _qrController?.flipCamera(); - }, - ), - ], - ), - body: Stack( - children: [ - // Scanner view - QRView( - key: _qrKey, - onQRViewCreated: _onQRViewCreated, - overlay: QrScannerOverlayShape( - borderColor: Colors.green, - borderRadius: 10, - borderLength: 30, - borderWidth: 10, - cutOutSize: 250, - ), - ), - - // Instructions overlay - Positioned( - bottom: 100, - left: 20, - right: 20, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(12), - ), - child: const Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.qr_code_scanner, color: Colors.white, size: 40), - SizedBox(height: 8), - Text( - 'Pointez la caméra vers le code-barres IMEI', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - Text( - 'Le scan se fait automatiquement', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ], - ), - ); - } - - // 6. Configuration du contrôleur QR - void _onQRViewCreated(QRViewController controller) { - _qrController = controller; - - controller.scannedDataStream.listen((scanData) { - if (scanData.code != null && scanData.code!.isNotEmpty) { - // Pauser le scanner pour éviter les scans multiples - controller.pauseCamera(); - - // Fermer la page du scanner - Get.back(); - - // Traiter le résultat - _findAndAddProductByImei(scanData.code!); - } - }); - } - - // 7. Méthode pour trouver et ajouter un produit par IMEI - Future _findAndAddProductByImei(String scannedImei) async { - try { - // Montrer un indicateur de chargement - Get.dialog( - AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(color: Colors.green.shade700), - const SizedBox(height: 16), - const Text('Recherche du produit...'), - const SizedBox(height: 8), - Text( - 'IMEI: $scannedImei', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - fontFamily: 'monospace', - ), - ), - ], - ), - ), - barrierDismissible: false, - ); - - // Attendre un court instant pour l'effet visuel - await Future.delayed(const Duration(milliseconds: 300)); - - // Chercher le produit avec l'IMEI scanné - Product? foundProduct; - - for (var product in _products) { - if (product.imei?.toLowerCase().trim() == scannedImei.toLowerCase().trim()) { - foundProduct = product; - break; - } - } - - // Fermer l'indicateur de chargement - Get.back(); - - if (foundProduct == null) { - _showProductNotFoundDialog(scannedImei); - return; - } - - // Vérifier le stock - if (foundProduct.stock != null && foundProduct.stock! <= 0) { - Get.snackbar( - 'Stock insuffisant', - 'Le produit "${foundProduct.name}" n\'est plus en stock', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange.shade600, - colorText: Colors.white, - duration: const Duration(seconds: 3), - icon: const Icon(Icons.warning_amber, color: Colors.white), - ); - return; - } - - // Vérifier si le produit peut être ajouté (stock disponible) - final currentQuantity = _quantites[foundProduct.id] ?? 0; - if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) { - Get.snackbar( - 'Stock limite atteint', - 'Quantité maximum atteinte pour "${foundProduct.name}"', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange.shade600, - colorText: Colors.white, - duration: const Duration(seconds: 3), - icon: const Icon(Icons.warning_amber, color: Colors.white), - ); - return; - } - - // Ajouter le produit au panier - setState(() { - _quantites[foundProduct!.id!] = currentQuantity + 1; - }); - - // Afficher le dialogue de succès - _showSuccessDialog(foundProduct, currentQuantity + 1); - - } catch (e) { - // Fermer l'indicateur de chargement si il est encore ouvert - if (Get.isDialogOpen!) Get.back(); - - Get.snackbar( - 'Erreur', - 'Une erreur est survenue: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red.shade600, - colorText: Colors.white, - duration: const Duration(seconds: 3), - ); - } - } - - // 8. Dialogue de succès - void _showSuccessDialog(Product product, int newQuantity) { - Get.dialog( - AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.check_circle, color: Colors.green.shade700), - ), - const SizedBox(width: 12), - const Expanded(child: Text('Produit ajouté !')), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text('Prix: ${product.price.toStringAsFixed(2)} MGA'), - Text('Quantité dans le panier: $newQuantity'), - if (product.stock != null) - Text('Stock restant: ${product.stock! - newQuantity}'), - ], - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Continuer'), - ), - ElevatedButton( - onPressed: () { - Get.back(); - _showCartBottomSheet(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, - ), - child: const Text('Voir le panier'), - ), - ], - ), - ); - } - - // 9. Dialogue produit non trouvé - void _showProductNotFoundDialog(String scannedImei) { - Get.dialog( - AlertDialog( - title: Row( - children: [ - Icon(Icons.search_off, color: Colors.red.shade600), - const SizedBox(width: 8), - const Text('Produit non trouvé'), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Aucun produit trouvé avec cet IMEI:'), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - scannedImei, - style: const TextStyle( - fontFamily: 'monospace', - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 12), - Text( - 'Vérifiez que l\'IMEI est correct ou que le produit existe dans la base de données.', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Fermer'), - ), - ElevatedButton( - onPressed: () { - Get.back(); - _startBarcodeScanning(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, - ), - child: const Text('Scanner à nouveau'), - ), - ], - ), - ); - } - - - Widget _buildScanInfoCard() { - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.qr_code_scanner, - color: Colors.green.shade700, - size: 20, - ), - ), - const SizedBox(width: 12), - const Expanded( - child: Text( - 'Scanner rapidement un produit via son IMEI pour l\'ajouter au panier', - style: TextStyle( - fontSize: 14, - color: Color.fromARGB(255, 9, 56, 95), - ), - ), - ), - ElevatedButton.icon( - onPressed: _isScanning ? null : _startBarcodeScanning, - icon: _isScanning - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.qr_code_scanner, size: 18), - label: Text(_isScanning ? 'Scan...' : 'Scanner'), - style: ElevatedButton.styleFrom( - backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ), - ], - ), - ), - ); - } - - // 10. Modifier le Widget build pour ajouter le bouton de scan - @override - Widget build(BuildContext context) { - final isMobile = MediaQuery.of(context).size.width < 600; - - return Scaffold( - floatingActionButton: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - // Bouton de scan - FloatingActionButton( - heroTag: "scan", - onPressed: _isScanning ? null : _startBarcodeScanning, - backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, - foregroundColor: Colors.white, - child: _isScanning - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.qr_code_scanner), - ), - const SizedBox(height: 10), - // Bouton panier existant - _buildFloatingCartButton(), - ], - ), - appBar: CustomAppBar(title: 'Nouvelle commande'), - drawer: CustomDrawer(), - body: GestureDetector( - onTap: _hideAllSuggestions, - child: Column( - children: [ - // Section d'information sur le scan (desktop) - if (!isMobile) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: _buildScanInfoCard(), - ), - - // Section des filtres - if (!isMobile) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: _buildFilterSection(), - ), - - // Boutons pour mobile - if (isMobile) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Row( - children: [ - Expanded( - flex: 2, - child: ElevatedButton.icon( - icon: const Icon(Icons.filter_alt), - label: const Text('Filtres'), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => SingleChildScrollView( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: _buildFilterSection(), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - flex: 1, - child: ElevatedButton.icon( - icon: _isScanning - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.qr_code_scanner), - label: Text(_isScanning ? 'Scan...' : 'Scan'), - onPressed: _isScanning ? null : _startBarcodeScanning, - style: ElevatedButton.styleFrom( - backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - ], - ), - ), - // Compteur de résultats - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${_filteredProducts.length} produit(s)', - style: TextStyle( - color: Colors.blue.shade700, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - - // Liste des produits - Expanded( - child: _buildProductList(), - ), - ], - ), - ), - ); - } - - - Widget _buildSuggestionsList({required bool isNom}) { - if (_clientSuggestions.isEmpty) return const SizedBox(); - - return Container( - margin: const EdgeInsets.only(top: 4), - constraints: const BoxConstraints(maxHeight: 150), - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: _clientSuggestions.length, - itemBuilder: (context, index) { - final client = _clientSuggestions[index]; - return ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: Colors.blue.shade100, - child: Icon( - Icons.person, - size: 16, - color: Colors.blue.shade700, - ), - ), - title: Text( - '${client.nom} ${client.prenom}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - '${client.telephone} • ${client.email}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - onTap: () { - _fillClientForm(client); - _hideAllSuggestions(); - }, - ); - }, - ), - ); -} - - Widget _buildFloatingCartButton() { - final isMobile = MediaQuery.of(context).size.width < 600; - final cartItemCount = _quantites.values.where((q) => q > 0).length; - - return FloatingActionButton.extended( - onPressed: () { - _showCartBottomSheet(); - }, - icon: const Icon(Icons.shopping_cart), - label: Text( - isMobile ? 'Panier ($cartItemCount)' : 'Panier ($cartItemCount)', - style: TextStyle(fontSize: isMobile ? 12 : 14), - ), - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - ); - } - - void _showClientFormDialog() { - final isMobile = MediaQuery.of(context).size.width < 600; - - // Variables locales pour les suggestions dans le dialog - bool showNomSuggestions = false; - bool showPrenomSuggestions = false; - bool showEmailSuggestions = false; - bool showTelephoneSuggestions = false; - List localClientSuggestions = []; - - // GlobalKeys pour positionner les overlays - final GlobalKey nomFieldKey = GlobalKey(); - final GlobalKey prenomFieldKey = GlobalKey(); - final GlobalKey emailFieldKey = GlobalKey(); - final GlobalKey telephoneFieldKey = GlobalKey(); - - Get.dialog( - StatefulBuilder( - builder: (context, setDialogState) { - return Stack( - children: [ - AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.person_add, color: Colors.blue.shade700), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - isMobile ? 'Client' : 'Informations Client', - style: TextStyle(fontSize: isMobile ? 16 : 18), - ), - ), - ], - ), - content: Container( - width: isMobile ? double.maxFinite : 600, - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, - ), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Champ Nom avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: nomFieldKey, - controller: _nomController, - label: 'Nom', - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un nom' : null, - onChanged: (value) async { - if (value.length >= 2) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showNomSuggestions = suggestions.isNotEmpty; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - }); - } else { - setDialogState(() { - showNomSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - // Champ Prénom avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: prenomFieldKey, - controller: _prenomController, - label: 'Prénom', - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un prénom' : null, - onChanged: (value) async { - if (value.length >= 2) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showPrenomSuggestions = suggestions.isNotEmpty; - showNomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - }); - } else { - setDialogState(() { - showPrenomSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - // Champ Email avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: emailFieldKey, - controller: _emailController, - label: 'Email', - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value?.isEmpty ?? true) return 'Veuillez entrer un email'; - if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) { - return 'Email invalide'; - } - return null; - }, - onChanged: (value) async { - if (value.length >= 3) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showEmailSuggestions = suggestions.isNotEmpty; - showNomSuggestions = false; - showPrenomSuggestions = false; - showTelephoneSuggestions = false; - }); - } else { - setDialogState(() { - showEmailSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - // Champ Téléphone avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: telephoneFieldKey, - controller: _telephoneController, - label: 'Téléphone', - keyboardType: TextInputType.phone, - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un téléphone' : null, - onChanged: (value) async { - if (value.length >= 3) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showTelephoneSuggestions = suggestions.isNotEmpty; - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - }); - } else { - setDialogState(() { - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - _buildTextFormField( - controller: _adresseController, - label: 'Adresse', - maxLines: 2, - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer une adresse' : null, - ), - const SizedBox(height: 12), - _buildCommercialDropdown(), - ], - ), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 16 : 20, - vertical: isMobile ? 10 : 12 - ), - ), - onPressed: () { - if (_formKey.currentState!.validate()) { - // Fermer toutes les suggestions avant de soumettre - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - Get.back(); - _submitOrder(); - } - }, - child: Text( - isMobile ? 'Valider' : 'Valider la commande', - style: TextStyle(fontSize: isMobile ? 12 : 14), - ), - ), - ], - ), - - // Overlay pour les suggestions du nom - if (showNomSuggestions) - _buildSuggestionOverlay( - fieldKey: nomFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showNomSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions du prénom - if (showPrenomSuggestions) - _buildSuggestionOverlay( - fieldKey: prenomFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showPrenomSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions de l'email - if (showEmailSuggestions) - _buildSuggestionOverlay( - fieldKey: emailFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showEmailSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions du téléphone - if (showTelephoneSuggestions) - _buildSuggestionOverlay( - fieldKey: telephoneFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - ], - ); - }, - ), - ); -} - -// Widget pour créer un TextFormField avec une clé -Widget _buildTextFormFieldWithKey({ - required GlobalKey key, - required TextEditingController controller, - required String label, - TextInputType? keyboardType, - int maxLines = 1, - String? Function(String?)? validator, - void Function(String)? onChanged, -}) { - return Container( - key: key, - child: _buildTextFormField( - controller: controller, - label: label, - keyboardType: keyboardType, - maxLines: maxLines, - validator: validator, - onChanged: onChanged, - ), - ); -} - -// Widget pour l'overlay des suggestions -Widget _buildSuggestionOverlay({ - required GlobalKey fieldKey, - required List suggestions, - required Function(Client) onClientSelected, - required VoidCallback onDismiss, -}) { - return Positioned.fill( - child: GestureDetector( - onTap: onDismiss, - child: Material( - color: Colors.transparent, - child: Builder( - builder: (context) { - // Obtenir la position du champ - final RenderBox? renderBox = fieldKey.currentContext?.findRenderObject() as RenderBox?; - if (renderBox == null) return const SizedBox(); - - final position = renderBox.localToGlobal(Offset.zero); - final size = renderBox.size; - - return Stack( - children: [ - Positioned( - left: position.dx, - top: position.dy + size.height + 4, - width: size.width, - child: GestureDetector( - onTap: () {}, // Empêcher la fermeture au tap sur la liste - child: Container( - constraints: const BoxConstraints( - maxHeight: 200, // Hauteur maximum pour la scrollabilité - ), - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Scrollbar( - thumbVisibility: suggestions.length > 3, - child: ListView.separated( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: suggestions.length, - separatorBuilder: (context, index) => Divider( - height: 1, - color: Colors.grey.shade200, - ), - itemBuilder: (context, index) { - final client = suggestions[index]; - return ListTile( - dense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - leading: CircleAvatar( - radius: 16, - backgroundColor: Colors.blue.shade100, - child: Icon( - Icons.person, - size: 16, - color: Colors.blue.shade700, - ), - ), - title: Text( - '${client.nom} ${client.prenom}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - '${client.telephone} • ${client.email}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - onTap: () => onClientSelected(client), - hoverColor: Colors.blue.shade50, - ); - }, - ), - ), - ), - ), - ), - ), - ], - ); - }, - ), - ), - ), - ); -} - -// Méthode pour remplir le formulaire avec les données du client -void _fillFormWithClient(Client client) { - _nomController.text = client.nom; - _prenomController.text = client.prenom; - _emailController.text = client.email; - _telephoneController.text = client.telephone; - _adresseController.text = client.adresse ?? ''; - - Get.snackbar( - 'Client trouvé', - 'Les informations ont été remplies automatiquement', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); -} - - Widget _buildTextFormField({ - required TextEditingController controller, - required String label, - TextInputType? keyboardType, - String? Function(String?)? validator, - int? maxLines, - void Function(String)? onChanged, - }) { - return TextFormField( - controller: controller, - decoration: InputDecoration( - labelText: label, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.white, - ), - keyboardType: keyboardType, - validator: validator, - maxLines: maxLines, - onChanged: onChanged, - ); - } - - Widget _buildCommercialDropdown() { - return DropdownButtonFormField( - value: _selectedCommercialUser, - decoration: InputDecoration( - labelText: 'Commercial', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.white, - ), - items: _commercialUsers.map((Users user) { - return DropdownMenuItem( - value: user, - child: Text('${user.name} ${user.lastName}'), - ); - }).toList(), - onChanged: (Users? newValue) { - setState(() { - _selectedCommercialUser = newValue; - }); - }, - validator: (value) => value == null ? 'Veuillez sélectionner un commercial' : null, - ); - } - - Widget _buildProductList() { - final isMobile = MediaQuery.of(context).size.width < 600; - - return _filteredProducts.isEmpty - ? _buildEmptyState() - : ListView.builder( - padding: const EdgeInsets.all(16.0), - itemCount: _filteredProducts.length, - itemBuilder: (context, index) { - final product = _filteredProducts[index]; - final quantity = _quantites[product.id] ?? 0; - - return _buildProductListItem(product, quantity, isMobile); - }, - ); - } - - Widget _buildEmptyState() { - return Center( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - children: [ - Icon( - Icons.search_off, - size: 64, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'Aucun produit trouvé', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Colors.grey.shade600, - ), - ), - const SizedBox(height: 8), - Text( - 'Modifiez vos critères de recherche', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade500, - ), - ), - ], - ), - ), - ); - } - - Widget _buildProductListItem(Product product, int quantity, bool isMobile) { - final bool isOutOfStock = product.stock != null && product.stock! <= 0; - - return Card( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: isOutOfStock - ? Border.all(color: Colors.red.shade200, width: 1.5) - : null, - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - Container( - width: isMobile ? 40 : 50, - height: isMobile ? 40 : 50, - decoration: BoxDecoration( - color: isOutOfStock - ? Colors.red.shade50 - : Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.shopping_bag, - size: isMobile ? 20 : 24, - color: isOutOfStock ? Colors.red : Colors.blue, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: isMobile ? 14 : 16, - color: isOutOfStock ? Colors.red.shade700 : null, - ), - ), - const SizedBox(height: 4), - Text( - '${product.price.toStringAsFixed(2)} MGA', - style: TextStyle( - color: Colors.green.shade700, - fontWeight: FontWeight.w600, - fontSize: isMobile ? 12 : 14, - ), - ), - if (product.stock != null) - Text( - 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}', - style: TextStyle( - fontSize: isMobile ? 10 : 12, - color: isOutOfStock - ? Colors.red.shade600 - : Colors.grey.shade600, - fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal, - ), - ), - // Affichage IMEI et Référence - plus compact sur mobile - if (product.imei != null && product.imei!.isNotEmpty) - Text( - 'IMEI: ${product.imei}', - style: TextStyle( - fontSize: isMobile ? 9 : 11, - color: Colors.grey.shade600, - fontFamily: 'monospace', - ), - ), - if (product.reference != null && product.reference!.isNotEmpty) - Text( - 'Réf: ${product.reference}', - style: TextStyle( - fontSize: isMobile ? 9 : 11, - color: Colors.grey.shade600, - ), - ), - ], - ), - ), - Container( - decoration: BoxDecoration( - color: isOutOfStock - ? Colors.grey.shade100 - : Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon( - Icons.remove, - size: isMobile ? 16 : 18 - ), - onPressed: isOutOfStock ? null : () { - if (quantity > 0) { - setState(() { - _quantites[product.id!] = quantity - 1; - }); - } - }, - ), - Text( - quantity.toString(), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: isMobile ? 12 : 14, - ), - ), - IconButton( - icon: Icon( - Icons.add, - size: isMobile ? 16 : 18 - ), - onPressed: isOutOfStock ? null : () { - if (product.stock == null || quantity < product.stock!) { - setState(() { - _quantites[product.id!] = quantity + 1; - }); - } else { - Get.snackbar( - 'Stock insuffisant', - 'Quantité demandée non disponible', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } - - void _showCartBottomSheet() { - final isMobile = MediaQuery.of(context).size.width < 600; - - Get.bottomSheet( - Container( - height: MediaQuery.of(context).size.height * (isMobile ? 0.85 : 0.7), - padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Votre Panier', - style: TextStyle( - fontSize: isMobile ? 18 : 20, - fontWeight: FontWeight.bold - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Get.back(), - ), - ], - ), - const Divider(), - Expanded(child: _buildCartItemsList()), - const Divider(), - _buildCartTotalSection(), - const SizedBox(height: 16), - _buildSubmitButton(), - ], - ), - ), - isScrollControlled: true, - ); - } - - Widget _buildCartItemsList() { - final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); - - if (itemsInCart.isEmpty) { - return const Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.shopping_cart_outlined, size: 60, color: Colors.grey), - SizedBox(height: 16), - Text( - 'Votre panier est vide', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - ], - ), - ); - } - - return ListView.builder( - itemCount: itemsInCart.length, - itemBuilder: (context, index) { - final entry = itemsInCart[index]; - final product = _products.firstWhere((p) => p.id == entry.key); - - return Dismissible( - key: Key(entry.key.toString()), - background: Container( - color: Colors.red.shade100, - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 20), - child: const Icon(Icons.delete, color: Colors.red), - ), - direction: DismissDirection.endToStart, - onDismissed: (direction) { - setState(() { - _quantites.remove(entry.key); - }); - Get.snackbar( - 'Produit retiré', - '${product.name} a été retiré du panier', - snackPosition: SnackPosition.BOTTOM, - ); - }, - child: Card( - margin: const EdgeInsets.only(bottom: 8), - elevation: 1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.shopping_bag, size: 20), - ), - title: Text(product.name), - subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} MGA'), - trailing: Text( - '${(entry.value * product.price).toStringAsFixed(2)} MGA', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - ), - ), - ), - ); - }, - ); - } - - Widget _buildCartTotalSection() { - double total = 0; - _quantites.forEach((productId, quantity) { - final product = _products.firstWhere((p) => p.id == productId); - total += quantity * product.price; - }); - - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Total:', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Text( - '${total.toStringAsFixed(2)} MGA', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.green, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - '${_quantites.values.where((q) => q > 0).length} article(s)', - style: TextStyle(color: Colors.grey.shade600), - ), - ], - ); - } - - Widget _buildSubmitButton() { - final isMobile = MediaQuery.of(context).size.width < 600; - - return SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric( - vertical: isMobile ? 12 : 16 - ), - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 4, - ), - onPressed: _submitOrder, - child: _isLoading - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : Text( - isMobile ? 'Valider' : 'Valider la Commande', - style: TextStyle(fontSize: isMobile ? 14 : 16), - ), - ), - ); - } - - Future _submitOrder() async { - // Vérifier d'abord si le panier est vide - final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); - if (itemsInCart.isEmpty) { - Get.snackbar( - 'Panier vide', - 'Veuillez ajouter des produits à votre commande', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - _showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide - return; - } - - // Ensuite vérifier les informations client - if (_nomController.text.isEmpty || - _prenomController.text.isEmpty || - _emailController.text.isEmpty || - _telephoneController.text.isEmpty || - _adresseController.text.isEmpty) { - Get.snackbar( - 'Informations manquantes', - 'Veuillez remplir les informations client', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - _showClientFormDialog(); - return; - } - - setState(() { - _isLoading = true; - }); - - // Créer le client - final client = Client( - nom: _nomController.text, - prenom: _prenomController.text, - email: _emailController.text, - telephone: _telephoneController.text, - adresse: _adresseController.text, - dateCreation: DateTime.now(), - ); - - // Calculer le total et préparer les détails - double total = 0; - final details = []; - - for (final entry in itemsInCart) { - final product = _products.firstWhere((p) => p.id == entry.key); - total += entry.value * product.price; - - details.add(DetailCommande( - commandeId: 0, - produitId: product.id!, - quantite: entry.value, - prixUnitaire: product.price, - sousTotal: entry.value * product.price, - )); - } - - // Créer la commande - final commande = Commande( - clientId: 0, - dateCommande: DateTime.now(), - statut: StatutCommande.enAttente, - montantTotal: total, - notes: 'Commande passée via l\'application', - commandeurId: _selectedCommercialUser?.id, - ); - - try { - await _appDatabase.createCommandeComplete(client, commande, details); - - // Fermer le panier avant d'afficher la confirmation - Get.back(); - - // Afficher le dialogue de confirmation - adapté pour mobile - final isMobile = MediaQuery.of(context).size.width < 600; - - await showDialog( - context: context, - barrierDismissible: false, // Empêcher la fermeture accidentelle - builder: (context) => AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.check_circle, color: Colors.green.shade700), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Commande Validée', - style: TextStyle(fontSize: isMobile ? 16 : 18), - ), - ), - ], - ), - content: Text( - 'Votre commande a été enregistrée et expédiée avec succès.', - style: TextStyle(fontSize: isMobile ? 14 : 16), - ), - actions: [ - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade700, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - vertical: isMobile ? 12 : 16 - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: () { - Navigator.pop(context); - // Vider complètement le formulaire et le panier - _clearFormAndCart(); - // Recharger les produits pour mettre à jour le stock - _loadProducts(); - }, - child: Text( - 'OK', - style: TextStyle(fontSize: isMobile ? 14 : 16), - ), - ), - ), - ], - ), - ); - - } catch (e) { - setState(() { - _isLoading = false; - }); - - Get.snackbar( - 'Erreur', - 'Une erreur est survenue: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - } - - @override - void dispose() { - _qrController?.dispose(); - - // Vos disposals existants... - _hideAllSuggestions(); - _nomController.dispose(); - _prenomController.dispose(); - _emailController.dispose(); - _telephoneController.dispose(); - _adresseController.dispose(); - _searchNameController.dispose(); - _searchImeiController.dispose(); - _searchReferenceController.dispose(); - - super.dispose(); - } - } \ No newline at end of file diff --git a/lib/Views/newCommand.dart b/lib/Views/newCommand.dart index c11a3b0..e80f3c6 100644 --- a/lib/Views/newCommand.dart +++ b/lib/Views/newCommand.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; - import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; +import 'package:youmazgestion/Components/newCommandComponents/CadeauDialog.dart'; +import 'package:youmazgestion/Components/newCommandComponents/RemiseDialog.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/produit.dart'; @@ -37,7 +38,7 @@ class _NouvelleCommandePageState extends State { final List _products = []; final List _filteredProducts = []; final Map _quantites = {}; - + final Map _panierDetails = {}; // Variables de filtre bool _showOnlyInStock = false; @@ -46,7 +47,6 @@ class _NouvelleCommandePageState extends State { Users? _selectedCommercialUser; // Variables pour les suggestions clients - List _clientSuggestions = []; bool _showNomSuggestions = false; bool _showTelephoneSuggestions = false; @@ -84,6 +84,177 @@ class _NouvelleCommandePageState extends State { } }); } +// ==Gestion des remise + +// 3. Ajouter ces méthodes pour gérer les remises + + Future _showRemiseDialog(Product product) async { + final detailExistant = _panierDetails[product.id!]; + + final result = await showDialog( + context: context, + builder: (context) => RemiseDialog( + product: product, + quantite: detailExistant?.quantite ?? 1, + prixUnitaire: product.price, + detailExistant: detailExistant, + ), + ); + + if (result != null) { + if (result == 'supprimer') { + _supprimerRemise(product.id!); + } else if (result is Map) { + _appliquerRemise(product.id!, result); + } + } + } + + + + void _appliquerRemise(int productId, Map remiseData) { + final detailExistant = _panierDetails[productId]; + if (detailExistant == null) return; + + final detailAvecRemise = detailExistant.appliquerRemise( + type: remiseData['type'] as RemiseType, + valeur: remiseData['valeur'] as double, + ); + + setState(() { + _panierDetails[productId] = detailAvecRemise; + }); + + Get.snackbar( + 'Remise appliquée', + 'Remise de ${detailAvecRemise.remiseDescription} appliquée', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } + + void _supprimerRemise(int productId) { + final detailExistant = _panierDetails[productId]; + if (detailExistant == null) return; + + setState(() { + _panierDetails[productId] = detailExistant.supprimerRemise(); + }); + + Get.snackbar( + 'Remise supprimée', + 'La remise a été supprimée', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.blue.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } +// Ajout des produits au pannier +// 4. Modifier la méthode pour ajouter des produits au panier + void _ajouterAuPanier(Product product, int quantite) { + // Vérifier le stock disponible + if (product.stock != null && quantite > product.stock!) { + Get.snackbar( + 'Stock insuffisant', + 'Quantité demandée non disponible', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + + setState(() { + final detail = DetailCommande.sansRemise( + commandeId: 0, // Sera défini lors de la création + produitId: product.id!, + quantite: quantite, + prixUnitaire: product.price, + produitNom: product.name, + produitReference: product.reference, + ); + _panierDetails[product.id!] = detail; + }); + } + + // Modification de la méthode _modifierQuantite pour gérer les cadeaux +void _modifierQuantite(int productId, int nouvelleQuantite) { + final detailExistant = _panierDetails[productId]; + if (detailExistant == null) return; + + if (nouvelleQuantite <= 0) { + setState(() { + _panierDetails.remove(productId); + }); + return; + } + + final product = _products.firstWhere((p) => p.id == productId); + if (product.stock != null && nouvelleQuantite > product.stock!) { + Get.snackbar( + 'Stock insuffisant', + 'Quantité maximum: ${product.stock}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + ); + return; + } + + final nouveauSousTotal = nouvelleQuantite * detailExistant.prixUnitaire; + + setState(() { + if (detailExistant.estCadeau) { + // Pour un cadeau, le prix final reste à 0 + _panierDetails[productId] = DetailCommande( + id: detailExistant.id, + commandeId: detailExistant.commandeId, + produitId: detailExistant.produitId, + quantite: nouvelleQuantite, + prixUnitaire: detailExistant.prixUnitaire, + sousTotal: nouveauSousTotal, + prixFinal: 0.0, // Prix final à 0 pour un cadeau + estCadeau: true, + produitNom: detailExistant.produitNom, + produitReference: detailExistant.produitReference, + ); + } else if (detailExistant.aRemise) { + // Recalculer la remise si elle existe + final detail = DetailCommande( + id: detailExistant.id, + commandeId: detailExistant.commandeId, + produitId: detailExistant.produitId, + quantite: nouvelleQuantite, + prixUnitaire: detailExistant.prixUnitaire, + sousTotal: nouveauSousTotal, + prixFinal: nouveauSousTotal, + produitNom: detailExistant.produitNom, + produitReference: detailExistant.produitReference, + ).appliquerRemise( + type: detailExistant.remiseType!, + valeur: detailExistant.remiseValeur, + ); + _panierDetails[productId] = detail; + } else { + // Article normal sans remise + _panierDetails[productId] = DetailCommande( + id: detailExistant.id, + commandeId: detailExistant.commandeId, + produitId: detailExistant.produitId, + quantite: nouvelleQuantite, + prixUnitaire: detailExistant.prixUnitaire, + sousTotal: nouveauSousTotal, + prixFinal: nouveauSousTotal, + produitNom: detailExistant.produitNom, + produitReference: detailExistant.produitReference, + ); + } + }); +} + // === NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE (identiques à ProductManagementPage) === @@ -257,29 +428,27 @@ class _NouvelleCommandePageState extends State { ); return; } - + final detailExistant = _panierDetails[foundProduct!.id!]; // Vérifier si le produit peut être ajouté (stock disponible) final currentQuantity = _quantites[foundProduct.id] ?? 0; if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) { - Get.snackbar( - 'Stock limite atteint', - 'Quantité maximum atteinte pour "${foundProduct.name}"', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange.shade600, - colorText: Colors.white, - duration: const Duration(seconds: 3), - icon: const Icon(Icons.warning_amber, color: Colors.white), - ); - return; - } + Get.snackbar( + 'Stock limite atteint', + 'Quantité maximum atteinte pour "${foundProduct.name}"', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.warning_amber, color: Colors.white), + ); + return; + } // Ajouter le produit au panier - setState(() { - _quantites[foundProduct!.id!] = currentQuantity + 1; - }); + _modifierQuantite(foundProduct.id!, currentQuantity + 1); - // Afficher le dialogue de succès - _showProductFoundAndAddedDialog(foundProduct, currentQuantity + 1); + // Afficher le dialogue de succès + _showProductFoundAndAddedDialog(foundProduct, currentQuantity + 1); } catch (e) { // Fermer l'indicateur de chargement si il est encore ouvert @@ -597,7 +766,7 @@ class _NouvelleCommandePageState extends State { // === FIN DES NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE === - // Méthode pour vider complètement le formulaire et le panier + // 8. Modifier _clearFormAndCart pour vider le nouveau panier void _clearFormAndCart() { setState(() { // Vider les contrôleurs client @@ -607,8 +776,8 @@ class _NouvelleCommandePageState extends State { _telephoneController.clear(); _adresseController.clear(); - // Vider le panier - _quantites.clear(); + // Vider le nouveau panier + _panierDetails.clear(); // Réinitialiser le commercial au premier de la liste if (_commercialUsers.isNotEmpty) { @@ -623,16 +792,15 @@ class _NouvelleCommandePageState extends State { }); } + Future _showClientSuggestions(String query, {required bool isNom}) async { if (query.length < 3) { _hideAllSuggestions(); return; } - final suggestions = await _appDatabase.suggestClients(query); setState(() { - _clientSuggestions = suggestions; if (isNom) { _showNomSuggestions = true; _showTelephoneSuggestions = false; @@ -644,24 +812,6 @@ class _NouvelleCommandePageState extends State { } - void _fillClientForm(Client client) { - setState(() { - _nomController.text = client.nom; - _prenomController.text = client.prenom; - _emailController.text = client.email; - _telephoneController.text = client.telephone; - _adresseController.text = client.adresse ?? ''; - }); - - Get.snackbar( - 'Client trouvé', - 'Les informations ont été remplies automatiquement', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } void _hideNomSuggestions() { if (mounted && _showNomSuggestions) { @@ -1511,155 +1661,333 @@ Widget _buildSuggestionOverlay({ ); } - Widget _buildProductListItem(Product product, int quantity, bool isMobile) { - final bool isOutOfStock = product.stock != null && product.stock! <= 0; - - return Card( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - elevation: 2, - shape: RoundedRectangleBorder( + // Modification de la méthode _buildProductListItem pour inclure le bouton cadeau +Widget _buildProductListItem(Product product, int quantity, bool isMobile) { + final bool isOutOfStock = product.stock != null && product.stock! <= 0; + final detailPanier = _panierDetails[product.id!]; + final int currentQuantity = detailPanier?.quantite ?? 0; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Container( + decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), + border: isOutOfStock + ? Border.all(color: Colors.red.shade200, width: 1.5) + : detailPanier?.estCadeau == true + ? Border.all(color: Colors.green.shade300, width: 2) + : detailPanier?.aRemise == true + ? Border.all(color: Colors.orange.shade300, width: 2) + : null, ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: isOutOfStock - ? Border.all(color: Colors.red.shade200, width: 1.5) - : null, - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - Container( - width: isMobile ? 40 : 50, - height: isMobile ? 40 : 50, - decoration: BoxDecoration( - color: isOutOfStock - ? Colors.red.shade50 - : Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.shopping_bag, - size: isMobile ? 20 : 24, - color: isOutOfStock ? Colors.red : Colors.blue, - ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Container( + width: isMobile ? 40 : 50, + height: isMobile ? 40 : 50, + decoration: BoxDecoration( + color: isOutOfStock + ? Colors.red.shade50 + : detailPanier?.estCadeau == true + ? Colors.green.shade50 + : detailPanier?.aRemise == true + ? Colors.orange.shade50 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + child: Icon( + detailPanier?.estCadeau == true + ? Icons.card_giftcard + : detailPanier?.aRemise == true + ? Icons.discount + : Icons.shopping_bag, + size: isMobile ? 20 : 24, + color: isOutOfStock + ? Colors.red + : detailPanier?.estCadeau == true + ? Colors.green.shade700 + : detailPanier?.aRemise == true + ? Colors.orange.shade700 + : Colors.blue, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + product.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isMobile ? 14 : 16, + color: isOutOfStock ? Colors.red.shade700 : null, + ), + ), + ), + if (detailPanier?.estCadeau == true) + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + 'CADEAU', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + if (detailPanier?.estCadeau == true) ...[ + Text( + 'Gratuit', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.bold, + fontSize: isMobile ? 12 : 14, + ), + ), + const SizedBox(width: 8), + Text( + '${product.price.toStringAsFixed(2)} MGA', + style: TextStyle( + color: Colors.grey.shade500, + fontWeight: FontWeight.w600, + fontSize: isMobile ? 11 : 13, + decoration: TextDecoration.lineThrough, + ), + ), + ] else ...[ + Text( + '${product.price.toStringAsFixed(2)} MGA', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.w600, + fontSize: isMobile ? 12 : 14, + decoration: detailPanier?.aRemise == true + ? TextDecoration.lineThrough + : null, + ), + ), + if (detailPanier?.aRemise == true) ...[ + const SizedBox(width: 8), + Text( + '${(detailPanier!.prixFinal / detailPanier.quantite).toStringAsFixed(2)} MGA', + style: TextStyle( + color: Colors.orange.shade700, + fontWeight: FontWeight.bold, + fontSize: isMobile ? 12 : 14, + ), + ), + ], + ], + ], + ), + if (detailPanier?.aRemise == true && !detailPanier!.estCadeau) Text( - product.name, + 'Remise: ${detailPanier!.remiseDescription}', style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: isMobile ? 14 : 16, - color: isOutOfStock ? Colors.red.shade700 : null, + fontSize: isMobile ? 10 : 12, + color: Colors.orange.shade600, + fontWeight: FontWeight.w500, ), ), - const SizedBox(height: 4), + if (product.stock != null) Text( - '${product.price.toStringAsFixed(2)} MGA', + 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}', style: TextStyle( - color: Colors.green.shade700, - fontWeight: FontWeight.w600, - fontSize: isMobile ? 12 : 14, + fontSize: isMobile ? 10 : 12, + color: isOutOfStock + ? Colors.red.shade600 + : Colors.grey.shade600, + fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal, ), ), - if (product.stock != null) - Text( - 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}', - style: TextStyle( - fontSize: isMobile ? 10 : 12, - color: isOutOfStock - ? Colors.red.shade600 - : Colors.grey.shade600, - fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal, + // Affichage IMEI et Référence + if (product.imei != null && product.imei!.isNotEmpty) + Text( + 'IMEI: ${product.imei}', + style: TextStyle( + fontSize: isMobile ? 9 : 11, + color: Colors.grey.shade600, + fontFamily: 'monospace', + ), + ), + if (product.reference != null && product.reference!.isNotEmpty) + Text( + 'Réf: ${product.reference}', + style: TextStyle( + fontSize: isMobile ? 9 : 11, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + // Actions (quantité, remise et cadeau) + Column( + children: [ + // Boutons d'actions (seulement si le produit est dans le panier) + if (currentQuantity > 0) ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Bouton cadeau + Container( + margin: const EdgeInsets.only(right: 4), + child: IconButton( + icon: Icon( + detailPanier?.estCadeau == true + ? Icons.card_giftcard + : Icons.card_giftcard_outlined, + size: isMobile ? 16 : 18, + color: detailPanier?.estCadeau == true + ? Colors.green.shade700 + : Colors.grey.shade600, + ), + onPressed: isOutOfStock ? null : () => _basculerStatutCadeau(product.id!), + tooltip: detailPanier?.estCadeau == true + ? 'Retirer le statut cadeau' + : 'Marquer comme cadeau', + style: IconButton.styleFrom( + backgroundColor: detailPanier?.estCadeau == true + ? Colors.green.shade100 + : Colors.grey.shade100, + minimumSize: Size(isMobile ? 32 : 36, isMobile ? 32 : 36), + ), ), ), - // Affichage IMEI et Référence - plus compact sur mobile - if (product.imei != null && product.imei!.isNotEmpty) - Text( - 'IMEI: ${product.imei}', - style: TextStyle( - fontSize: isMobile ? 9 : 11, - color: Colors.grey.shade600, - fontFamily: 'monospace', + // Bouton remise (seulement pour les articles non-cadeaux) + if (!detailPanier!.estCadeau) + Container( + margin: const EdgeInsets.only(right: 4), + child: IconButton( + icon: Icon( + detailPanier.aRemise + ? Icons.discount + : Icons.local_offer, + size: isMobile ? 16 : 18, + color: detailPanier.aRemise + ? Colors.orange.shade700 + : Colors.grey.shade600, + ), + onPressed: isOutOfStock ? null : () => _showRemiseDialog(product), + tooltip: detailPanier.aRemise + ? 'Modifier la remise' + : 'Ajouter une remise', + style: IconButton.styleFrom( + backgroundColor: detailPanier.aRemise + ? Colors.orange.shade100 + : Colors.grey.shade100, + minimumSize: Size(isMobile ? 32 : 36, isMobile ? 32 : 36), + ), + ), ), + // Bouton pour ajouter un cadeau à un autre produit + Container( + margin: const EdgeInsets.only(left: 4), + child: IconButton( + icon: Icon( + Icons.add_circle_outline, + size: isMobile ? 16 : 18, + color: Colors.green.shade600, + ), + onPressed: isOutOfStock ? null : () => _showCadeauDialog(product), + tooltip: 'Ajouter un cadeau', + style: IconButton.styleFrom( + backgroundColor: Colors.green.shade50, + minimumSize: Size(isMobile ? 32 : 36, isMobile ? 32 : 36), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + ], + + // Contrôles de quantité + Container( + decoration: BoxDecoration( + color: isOutOfStock + ? Colors.grey.shade100 + : detailPanier?.estCadeau == true + ? Colors.green.shade50 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + Icons.remove, + size: isMobile ? 16 : 18 + ), + onPressed: isOutOfStock ? null : () { + if (currentQuantity > 0) { + _modifierQuantite(product.id!, currentQuantity - 1); + } + }, ), - if (product.reference != null && product.reference!.isNotEmpty) Text( - 'Réf: ${product.reference}', + currentQuantity.toString(), style: TextStyle( - fontSize: isMobile ? 9 : 11, - color: Colors.grey.shade600, + fontWeight: FontWeight.bold, + fontSize: isMobile ? 12 : 14, ), ), - ], - ), - ), - Container( - decoration: BoxDecoration( - color: isOutOfStock - ? Colors.grey.shade100 - : Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon( - Icons.remove, - size: isMobile ? 16 : 18 - ), - onPressed: isOutOfStock ? null : () { - if (quantity > 0) { - setState(() { - _quantites[product.id!] = quantity - 1; - }); - } - }, - ), - Text( - quantity.toString(), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: isMobile ? 12 : 14, - ), - ), - IconButton( - icon: Icon( - Icons.add, - size: isMobile ? 16 : 18 + IconButton( + icon: Icon( + Icons.add, + size: isMobile ? 16 : 18 + ), + onPressed: isOutOfStock ? null : () { + if (product.stock == null || currentQuantity < product.stock!) { + if (currentQuantity == 0) { + _ajouterAuPanier(product, 1); + } else { + _modifierQuantite(product.id!, currentQuantity + 1); + } + } else { + Get.snackbar( + 'Stock insuffisant', + 'Quantité demandée non disponible', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + }, ), - onPressed: isOutOfStock ? null : () { - if (product.stock == null || quantity < product.stock!) { - setState(() { - _quantites[product.id!] = quantity + 1; - }); - } else { - Get.snackbar( - 'Stock insuffisant', - 'Quantité demandée non disponible', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - }, - ), - ], + ], + ), ), - ), - ], - ), + ], + ), + ], ), ), - ); - } + ), + ); +} + void _showCartBottomSheet() { final isMobile = MediaQuery.of(context).size.width < 600; @@ -1703,117 +2031,432 @@ Widget _buildSuggestionOverlay({ ); } + // 6. Modifier _buildCartItemsList pour afficher les remises Widget _buildCartItemsList() { - final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); - - if (itemsInCart.isEmpty) { - return const Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.shopping_cart_outlined, size: 60, color: Colors.grey), - SizedBox(height: 16), - Text( - 'Votre panier est vide', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - ], - ), - ); - } + final itemsInCart = _panierDetails.entries.where((e) => e.value.quantite > 0).toList(); + + if (itemsInCart.isEmpty) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.shopping_cart_outlined, size: 60, color: Colors.grey), + SizedBox(height: 16), + Text( + 'Votre panier est vide', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ], + ), + ); + } - return ListView.builder( - itemCount: itemsInCart.length, - itemBuilder: (context, index) { - final entry = itemsInCart[index]; - final product = _products.firstWhere((p) => p.id == entry.key); - - return Dismissible( - key: Key(entry.key.toString()), - background: Container( - color: Colors.red.shade100, - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 20), - child: const Icon(Icons.delete, color: Colors.red), + return ListView.builder( + itemCount: itemsInCart.length, + itemBuilder: (context, index) { + final entry = itemsInCart[index]; + final detail = entry.value; + final product = _products.firstWhere((p) => p.id == entry.key); + + return Dismissible( + key: Key(entry.key.toString()), + background: Container( + color: Colors.red.shade100, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + child: const Icon(Icons.delete, color: Colors.red), + ), + direction: DismissDirection.endToStart, + onDismissed: (direction) { + setState(() { + _panierDetails.remove(entry.key); + }); + Get.snackbar( + 'Produit retiré', + '${product.name} a été retiré du panier', + snackPosition: SnackPosition.BOTTOM, + ); + }, + child: Card( + margin: const EdgeInsets.only(bottom: 8), + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: detail.estCadeau + ? BorderSide(color: Colors.green.shade300, width: 1.5) + : detail.aRemise + ? BorderSide(color: Colors.orange.shade300, width: 1.5) + : BorderSide.none, ), - direction: DismissDirection.endToStart, - onDismissed: (direction) { - setState(() { - _quantites.remove(entry.key); - }); - Get.snackbar( - 'Produit retiré', - '${product.name} a été retiré du panier', - snackPosition: SnackPosition.BOTTOM, - ); - }, - child: Card( - margin: const EdgeInsets.only(bottom: 8), - elevation: 1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.shopping_bag, size: 20), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: detail.estCadeau + ? Colors.green.shade50 + : detail.aRemise + ? Colors.orange.shade50 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), ), - title: Text(product.name), - subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} MGA'), - trailing: Text( - '${(entry.value * product.price).toStringAsFixed(2)} MGA', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), + child: Icon( + detail.estCadeau + ? Icons.card_giftcard + : detail.aRemise + ? Icons.discount + : Icons.shopping_bag, + size: 20, + color: detail.estCadeau + ? Colors.green.shade700 + : detail.aRemise + ? Colors.orange.shade700 + : Colors.blue.shade700, ), ), + title: Row( + children: [ + Expanded(child: Text(product.name)), + if (detail.estCadeau) + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + 'CADEAU', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('${detail.quantite} x '), + if (detail.estCadeau) ...[ + Text( + 'GRATUIT', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Text( + '(${detail.prixUnitaire.toStringAsFixed(2)} MGA)', + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade500, + decoration: TextDecoration.lineThrough, + ), + ), + ] else if (detail.aRemise) ...[ + Text( + '${detail.prixUnitaire.toStringAsFixed(2)}', + style: const TextStyle( + decoration: TextDecoration.lineThrough, + color: Colors.grey, + ), + ), + const Text(' → '), + Text( + '${(detail.prixFinal / detail.quantite).toStringAsFixed(2)} MGA', + style: TextStyle( + color: Colors.orange.shade700, + fontWeight: FontWeight.bold, + ), + ), + ] else + Text('${detail.prixUnitaire.toStringAsFixed(2)} MGA'), + ], + ), + if (detail.aRemise && !detail.estCadeau) + Text( + 'Remise: ${detail.remiseDescription} (-${detail.montantRemise.toStringAsFixed(2)} MGA)', + style: TextStyle( + fontSize: 11, + color: Colors.orange.shade600, + fontStyle: FontStyle.italic, + ), + ), + if (detail.estCadeau) + Row( + children: [ + Icon( + Icons.card_giftcard, + size: 12, + color: Colors.green.shade600, + ), + const SizedBox(width: 4), + Text( + 'Article offert gracieusement', + style: TextStyle( + fontSize: 11, + color: Colors.green.shade600, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ], + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (detail.estCadeau) ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.card_giftcard, + size: 16, + color: Colors.green.shade700, + ), + const SizedBox(width: 4), + Text( + 'GRATUIT', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + fontSize: 14, + ), + ), + ], + ), + Text( + 'Valeur: ${detail.sousTotal.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 10, + color: Colors.grey.shade500, + fontStyle: FontStyle.italic, + ), + ), + ] else if (detail.aRemise && detail.sousTotal != detail.prixFinal) ...[ + Text( + '${detail.sousTotal.toStringAsFixed(2)} MGA', + style: const TextStyle( + fontSize: 11, + decoration: TextDecoration.lineThrough, + color: Colors.grey, + ), + ), + Text( + '${detail.prixFinal.toStringAsFixed(2)} MGA', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.orange.shade700, + fontSize: 14, + ), + ), + ] else + Text( + '${detail.prixFinal.toStringAsFixed(2)} MGA', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + fontSize: 14, + ), + ), + ], + ), + onTap: () { + if (detail.estCadeau) { + _basculerStatutCadeau(product.id!); + } else { + _showRemiseDialog(product); + } + }, ), - ); - }, - ); - } + ), + ); + }, + ); +} + + // 7. Modifier _buildCartTotalSection pour afficher les totaux avec remises Widget _buildCartTotalSection() { - double total = 0; - _quantites.forEach((productId, quantity) { - final product = _products.firstWhere((p) => p.id == productId); - total += quantity * product.price; - }); + double sousTotal = 0; + double totalRemises = 0; + double totalCadeaux = 0; + double total = 0; + int nombreCadeaux = 0; + + _panierDetails.forEach((productId, detail) { + sousTotal += detail.sousTotal; + if (detail.estCadeau) { + totalCadeaux += detail.sousTotal; + nombreCadeaux += detail.quantite; + } else { + totalRemises += detail.montantRemise; + } + total += detail.prixFinal; + }); - return Column( - children: [ + return Column( + children: [ + // Sous-total + if (totalRemises > 0 || totalCadeaux > 0) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Total:', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + const Text('Sous-total:', style: TextStyle(fontSize: 14)), + Text( + '${sousTotal.toStringAsFixed(2)} MGA', + style: const TextStyle(fontSize: 14), ), + ], + ), + const SizedBox(height: 4), + ], + + // Remises totales + if (totalRemises > 0) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ Text( - '${total.toStringAsFixed(2)} MGA', - style: const TextStyle( - fontSize: 18, + 'Remises totales:', + style: TextStyle( + fontSize: 14, + color: Colors.orange.shade700, + ), + ), + Text( + '-${totalRemises.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 14, + color: Colors.orange.shade700, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 4), + ], + + // Cadeaux offerts + if (totalCadeaux > 0) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.card_giftcard, + size: 16, + color: Colors.green.shade700, + ), + const SizedBox(width: 4), + Text( + 'Cadeaux offerts ($nombreCadeaux):', + style: TextStyle( + fontSize: 14, + color: Colors.green.shade700, + ), + ), + ], + ), + Text( + '-${totalCadeaux.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 14, + color: Colors.green.shade700, fontWeight: FontWeight.bold, - color: Colors.green, ), ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 4), + ], + + if (totalRemises > 0 || totalCadeaux > 0) + const Divider(height: 16), + + // Total final + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Text( + '${total.toStringAsFixed(2)} MGA', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + + const SizedBox(height: 8), + + // Résumé + Text( + '${_panierDetails.values.where((d) => d.quantite > 0).length} article(s)', + style: TextStyle(color: Colors.grey.shade600), + ), + + // Économies totales + if (totalRemises > 0 || totalCadeaux > 0) ...[ + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.green.shade200), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.savings, + size: 16, + color: Colors.green.shade700, + ), + const SizedBox(width: 4), + Text( + 'Économies totales: ${(totalRemises + totalCadeaux).toStringAsFixed(2)} MGA', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ), + ), + ], + + // Détail des économies + if (totalRemises > 0 && totalCadeaux > 0) ...[ + const SizedBox(height: 4), Text( - '${_quantites.values.where((q) => q > 0).length} article(s)', - style: TextStyle(color: Colors.grey.shade600), + 'Remises: ${totalRemises.toStringAsFixed(2)} MGA • Cadeaux: ${totalCadeaux.toStringAsFixed(2)} MGA', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 11, + fontStyle: FontStyle.italic, + ), ), ], - ); - } + ], + ); +} Widget _buildSubmitButton() { final isMobile = MediaQuery.of(context).size.width < 600; @@ -1834,7 +2477,7 @@ Widget _buildSuggestionOverlay({ ), onPressed: _submitOrder, child: _isLoading - ? SizedBox( + ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( @@ -1850,9 +2493,9 @@ Widget _buildSuggestionOverlay({ ); } - Future _submitOrder() async { + Future _submitOrder() async { // Vérifier d'abord si le panier est vide - final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); + final itemsInCart = _panierDetails.entries.where((e) => e.value.quantite > 0).toList(); if (itemsInCart.isEmpty) { Get.snackbar( 'Panier vide', @@ -1861,11 +2504,11 @@ Widget _buildSuggestionOverlay({ backgroundColor: Colors.red, colorText: Colors.white, ); - _showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide + _showCartBottomSheet(); return; } - // Ensuite vérifier les informations client + // Vérifier les informations client if (_nomController.text.isEmpty || _prenomController.text.isEmpty || _emailController.text.isEmpty || @@ -1896,24 +2539,17 @@ Widget _buildSuggestionOverlay({ dateCreation: DateTime.now(), ); - // Calculer le total et préparer les détails + // Calculer le total final et préparer les détails double total = 0; final details = []; for (final entry in itemsInCart) { - final product = _products.firstWhere((p) => p.id == entry.key); - total += entry.value * product.price; - - details.add(DetailCommande( - commandeId: 0, - produitId: product.id!, - quantite: entry.value, - prixUnitaire: product.price, - sousTotal: entry.value * product.price, - )); + final detail = entry.value; + total += detail.prixFinal; + details.add(detail); } - // Créer la commande + // Créer la commande avec le total final (après remises) final commande = Commande( clientId: 0, dateCommande: DateTime.now(), @@ -1929,12 +2565,12 @@ Widget _buildSuggestionOverlay({ // Fermer le panier avant d'afficher la confirmation Get.back(); - // Afficher le dialogue de confirmation - adapté pour mobile + // Afficher le dialogue de confirmation final isMobile = MediaQuery.of(context).size.width < 600; await showDialog( context: context, - barrierDismissible: false, // Empêcher la fermeture accidentelle + barrierDismissible: false, builder: (context) => AlertDialog( title: Row( children: [ @@ -1975,9 +2611,7 @@ Widget _buildSuggestionOverlay({ ), onPressed: () { Navigator.pop(context); - // Vider complètement le formulaire et le panier _clearFormAndCart(); - // Recharger les produits pour mettre à jour le stock _loadProducts(); }, child: Text( @@ -2005,6 +2639,104 @@ Widget _buildSuggestionOverlay({ } } +Future _showCadeauDialog(Product product) async { + final detailExistant = _panierDetails[product.id!]; + + if (detailExistant == null || detailExistant.quantite == 0) { + Get.snackbar( + 'Produit requis', + 'Vous devez d\'abord ajouter ce produit au panier', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + ); + return; + } + + final result = await showDialog>( + context: context, + builder: (context) => CadeauDialog( + product: product, + quantite: detailExistant.quantite, + detailExistant: detailExistant, + ), + ); + + if (result != null) { + _ajouterCadeauAuPanier( + result['produit'] as Product, + result['quantite'] as int, + ); + } +} +void _ajouterCadeauAuPanier(Product produitCadeau, int quantite) { + // Vérifier le stock disponible + if (produitCadeau.stock != null && quantite > produitCadeau.stock!) { + Get.snackbar( + 'Stock insuffisant', + 'Quantité de cadeau demandée non disponible', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + + setState(() { + final detailCadeau = DetailCommande.cadeau( + commandeId: 0, // Sera défini lors de la création + produitId: produitCadeau.id!, + quantite: quantite, + prixUnitaire: produitCadeau.price, + produitNom: produitCadeau.name, + produitReference: produitCadeau.reference, + ); + _panierDetails[produitCadeau.id!] = detailCadeau; + }); + + Get.snackbar( + 'Cadeau ajouté', + '${produitCadeau.name} a été ajouté comme cadeau', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.card_giftcard, color: Colors.white), + ); +} +void _basculerStatutCadeau(int productId) { + final detailExistant = _panierDetails[productId]; + if (detailExistant == null) return; + + setState(() { + if (detailExistant.estCadeau) { + // Convertir en article normal + _panierDetails[productId] = detailExistant.convertirEnArticleNormal(); + + Get.snackbar( + 'Statut modifié', + 'L\'article n\'est plus un cadeau', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.blue.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } else { + // Convertir en cadeau + _panierDetails[productId] = detailExistant.convertirEnCadeau(); + + Get.snackbar( + 'Cadeau offert', + 'L\'article est maintenant un cadeau', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 2), + icon: const Icon(Icons.card_giftcard, color: Colors.white), + ); + } + }); +} @override void dispose() { _qrController?.dispose(); diff --git a/lib/Views/registrationPage.dart b/lib/Views/registrationPage.dart index 3bf44d5..a0ba967 100644 --- a/lib/Views/registrationPage.dart +++ b/lib/Views/registrationPage.dart @@ -3,7 +3,6 @@ import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/role.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/Views/Dashboard.dart'; -import 'package:youmazgestion/accueil.dart'; //import '../Services/app_database.dart'; // Changé de authDatabase.dart diff --git a/lib/Views/ticketPage.dart b/lib/Views/ticketPage.dart index 5e0a8fd..3d443da 100644 --- a/lib/Views/ticketPage.dart +++ b/lib/Views/ticketPage.dart @@ -1,10 +1,9 @@ import 'dart:io'; -import 'package:esc_pos_printer/esc_pos_printer.dart'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:intl/intl.dart'; -import 'package:esc_pos_utils/esc_pos_utils.dart'; + import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pdf/pdf.dart'; @@ -31,117 +30,6 @@ class TicketPage extends StatelessWidget { required this.amountPaid, }) : super(key: key); - Future _printTicket() async { - final profile = await CapabilityProfile.load(); - final printer = NetworkPrinter(PaperSize.mm80, profile); - - printer.text('Ticket de caisse', - styles: const PosStyles( - align: PosAlign.center, - height: PosTextSize.size2, - width: PosTextSize.size2, - )); - - printer.text('Entreprise : $businessName'); - printer.text('Adresse : $businessAddress'); - printer.text('Numéro de téléphone : $businessPhoneNumber'); - - printer.hr(); - printer.row([ - PosColumn( - text: 'Produit', - width: 3, - styles: const PosStyles(align: PosAlign.left, bold: true), - ), - PosColumn( - text: 'Quantité', - width: 1, - styles: const PosStyles(align: PosAlign.left, bold: true), - ), - PosColumn( - text: 'Prix unitaire', - width: 1, - styles: const PosStyles(align: PosAlign.left, bold: true), - ), - PosColumn( - text: 'Total', - width: 1, - styles: const PosStyles(align: PosAlign.left, bold: true), - ), - ]); - printer.hr(); - - for (final cartItem in cartItems) { - final product = cartItem.product; - final quantity = cartItem.quantity; - final productTotal = product.price * quantity; - - printer.row([ - PosColumn( - text: product.name, - width: 3, - ), - PosColumn( - text: quantity.toString(), - width: 1, - ), - PosColumn( - text: '${product.price.toStringAsFixed(2)} MGA', - width: 1, - ), - PosColumn( - text: '${productTotal.toStringAsFixed(2)} MGA', - width: 1, - ), - ]); - } - - printer.hr(); - printer.row([ - PosColumn( - text: 'Total :', - width: 3, - styles: const PosStyles(align: PosAlign.left, bold: true), - ), - PosColumn( - text: '${totalCartPrice.toStringAsFixed(2)} MGA', - width: 1, - styles: const PosStyles(align: PosAlign.left, bold: true), - ), - ]); - printer.row([ - PosColumn( - text: 'Somme remise :', - width: 3, - styles: const PosStyles(align: PosAlign.left), - ), - PosColumn( - text: '${amountPaid.toStringAsFixed(2)} MGA', - width: 1, - styles: const PosStyles(align: PosAlign.left), - ), - ]); - printer.row([ - PosColumn( - text: 'Somme rendue :', - width: 3, - styles: const PosStyles(align: PosAlign.left), - ), - PosColumn( - text: '${(amountPaid - totalCartPrice).toStringAsFixed(2)} MGA', - width: 1, - styles: const PosStyles(align: PosAlign.left), - ), - ]); - printer.hr(); - printer.text('Youmaz vous remercie pour votre achat!!!'); - printer.feed(2); - - printer.cut(); - printer.disconnect(); // Fermez la connexion après l'impression - - Get.snackbar('Impression', 'Ticket imprimé avec succès'); - } Future _generateAndSavePDF() async { final pdf = pw.Document(); @@ -265,11 +153,7 @@ class TicketPage extends StatelessWidget { } // Obtenir la date actuelle - final currentDate = DateTime.now(); - final formattedDate = DateFormat('dd/MM/yyyy HH:mm').format(currentDate); - // Calculer la somme remise - final double discount = totalOrderAmount - totalCartPrice; // Calculer la somme rendue final double change = amountPaid - totalOrderAmount; diff --git a/lib/accueil.dart b/lib/accueil.dart index 2bbd1f9..42039c8 100644 --- a/lib/accueil.dart +++ b/lib/accueil.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -115,7 +114,7 @@ class _AccueilPageState extends State { await orderDatabase.insertOrderItem( orderId, product.name, quantity, price); - final updatedStock = product.stock! - quantity; + final updatedStock = product.stock - quantity; await productDatabase.updateStock(product.id!, updatedStock); } diff --git a/lib/config/DatabaseConfig.dart b/lib/config/DatabaseConfig.dart index 79faab7..f0b7124 100644 --- a/lib/config/DatabaseConfig.dart +++ b/lib/config/DatabaseConfig.dart @@ -1,10 +1,10 @@ // Config/database_config.dart - Version améliorée class DatabaseConfig { - 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; - static const String database = 'guycom_databse_v1'; + static const String database = 'gico'; static const String prodHost = '185.70.105.157'; static const String prodUsername = 'guycom'; @@ -17,7 +17,7 @@ class DatabaseConfig { static const int maxConnections = 10; static const int minConnections = 2; - static bool get isDevelopment => false; + static bool get isDevelopment => true; static Map getConfig() { if (isDevelopment) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index ea57761..6713b98 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) charset_converter_registrar = @@ -21,7 +23,13 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) open_file_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e734d46..e1cc94e 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,7 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST charset_converter file_selector_linux open_file_linux + screen_retriever url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d42444f..ce26282 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,8 +10,10 @@ import file_selector_macos import mobile_scanner import open_file_mac import path_provider_foundation +import screen_retriever import shared_preferences_foundation import url_launcher_macos +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) @@ -19,6 +21,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 35e5036..3fac1ac 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -912,6 +912,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + url: "https://pub.dev" + source: hosted + version: "0.1.9" shared_preferences: dependency: "direct main" description: @@ -1205,6 +1213,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.12.0" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" + url: "https://pub.dev" + source: hosted + version: "0.3.9" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f5ede25..ed45759 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons numbers_to_letters: ^1.0.0 qr_code_scanner_plus: ^2.0.10+1 + window_manager: ^0.3.7 @@ -109,6 +110,7 @@ flutter: - assets/mvola.jpg - assets/Orange_money.png - assets/fa-solid-900.ttf + - assets/NotoEmoji-Regular.ttf - assets/fonts/Roboto-Italic.ttf # An image asset can refer to one or more resolution-specific "variants", see diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index a342058..5716043 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,13 +8,19 @@ #include #include +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { CharsetConverterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("CharsetConverterPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 5d12043..239eabd 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,7 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST charset_converter file_selector_windows + screen_retriever url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST