24 changed files with 9429 additions and 5126 deletions
@ -0,0 +1,175 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
|
|||
class CommandeDetails extends StatelessWidget { |
|||
final Commande commande; |
|||
|
|||
const CommandeDetails({required this.commande}); |
|||
|
|||
|
|||
|
|||
Widget _buildTableHeader(String text) { |
|||
return Padding( |
|||
padding: const EdgeInsets.all(8.0), |
|||
child: Text( |
|||
text, |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 14, |
|||
), |
|||
textAlign: TextAlign.center, |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildTableCell(String text) { |
|||
return Padding( |
|||
padding: const EdgeInsets.all(8.0), |
|||
child: Text( |
|||
text, |
|||
style: const TextStyle(fontSize: 13), |
|||
textAlign: TextAlign.center, |
|||
), |
|||
); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return FutureBuilder<List<DetailCommande>>( |
|||
future: AppDatabase.instance.getDetailsCommande(commande.id!), |
|||
builder: (context, snapshot) { |
|||
if (snapshot.connectionState == ConnectionState.waiting) { |
|||
return const Center(child: CircularProgressIndicator()); |
|||
} |
|||
|
|||
if (!snapshot.hasData || snapshot.data!.isEmpty) { |
|||
return const Text('Aucun détail disponible'); |
|||
} |
|||
|
|||
final details = snapshot.data!; |
|||
|
|||
return Column( |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
children: [ |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: const Text( |
|||
'Détails de la commande', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 16, |
|||
color: Colors.black87, |
|||
), |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
Container( |
|||
decoration: BoxDecoration( |
|||
border: Border.all(color: Colors.grey.shade300), |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Table( |
|||
children: [ |
|||
TableRow( |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey.shade100, |
|||
), |
|||
children: [ |
|||
_buildTableHeader('Produit'), |
|||
_buildTableHeader('Qté'), |
|||
_buildTableHeader('Prix unit.'), |
|||
_buildTableHeader('Total'), |
|||
], |
|||
), |
|||
...details.map((detail) => TableRow( |
|||
children: [ |
|||
_buildTableCell( |
|||
detail.estCadeau == true |
|||
? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)' |
|||
: detail.produitNom ?? 'Produit inconnu' |
|||
), |
|||
_buildTableCell('${detail.quantite}'), |
|||
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'), |
|||
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(2)} MGA'), |
|||
], |
|||
)), |
|||
], |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.green.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.green.shade200), |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
if (commande.montantApresRemise != null) ...[ |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Sous-total:', |
|||
style: TextStyle(fontSize: 14), |
|||
), |
|||
Text( |
|||
'${commande.montantTotal.toStringAsFixed(2)} MGA', |
|||
style: const TextStyle(fontSize: 14), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(height: 5), |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Remise:', |
|||
style: TextStyle(fontSize: 14), |
|||
), |
|||
Text( |
|||
'-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA', |
|||
style: const TextStyle( |
|||
fontSize: 14, |
|||
color: Colors.red, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const Divider(), |
|||
], |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Total de la commande:', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 16, |
|||
), |
|||
), |
|||
Text( |
|||
'${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 18, |
|||
color: Colors.green.shade700, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,226 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
|
|||
|
|||
//Classe suplementaire |
|||
|
|||
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, |
|||
}); |
|||
|
|||
|
|||
|
|||
List<Widget> _buildActionButtons(BuildContext context) { |
|||
List<Widget> buttons = []; |
|||
|
|||
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, |
|||
color: Colors.blue, |
|||
onPressed: () => onPaymentSelected(commande), |
|||
), |
|||
_buildActionButton( |
|||
label: 'Annuler', |
|||
icon: Icons.cancel, |
|||
color: Colors.red, |
|||
onPressed: () => _showConfirmDialog( |
|||
context, |
|||
'Annuler la commande', |
|||
'Êtes-vous sûr de vouloir annuler cette commande?', |
|||
() => onStatutChanged(commande.id!, StatutCommande.annulee), |
|||
), |
|||
), |
|||
]); |
|||
break; |
|||
|
|||
case StatutCommande.confirmee: |
|||
buttons.add( |
|||
Container( |
|||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), |
|||
decoration: BoxDecoration( |
|||
color: Colors.green.shade100, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.green.shade300), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon(Icons.check_circle, |
|||
color: Colors.green.shade600, size: 16), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
'Commande confirmée', |
|||
style: TextStyle( |
|||
color: Colors.green.shade700, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
break; |
|||
|
|||
case StatutCommande.annulee: |
|||
buttons.add( |
|||
Container( |
|||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), |
|||
decoration: BoxDecoration( |
|||
color: Colors.red.shade100, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.red.shade300), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon(Icons.cancel, color: Colors.red.shade600, size: 16), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
'Commande annulée', |
|||
style: TextStyle( |
|||
color: Colors.red.shade700, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
break; |
|||
} |
|||
|
|||
return buttons; |
|||
} |
|||
|
|||
Widget _buildActionButton({ |
|||
required String label, |
|||
required IconData icon, |
|||
required Color color, |
|||
required VoidCallback onPressed, |
|||
}) { |
|||
return ElevatedButton.icon( |
|||
onPressed: onPressed, |
|||
icon: Icon(icon, size: 16), |
|||
label: Text(label), |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: color, |
|||
foregroundColor: Colors.white, |
|||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
elevation: 2, |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _showConfirmDialog( |
|||
BuildContext context, |
|||
String title, |
|||
String content, |
|||
VoidCallback onConfirm, |
|||
) { |
|||
showDialog( |
|||
context: context, |
|||
builder: (BuildContext context) { |
|||
return AlertDialog( |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
title: Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.help_outline, |
|||
color: Colors.blue.shade600, |
|||
), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
title, |
|||
style: const TextStyle(fontSize: 18), |
|||
), |
|||
], |
|||
), |
|||
content: Text(content), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Navigator.of(context).pop(), |
|||
child: Text( |
|||
'Annuler', |
|||
style: TextStyle(color: Colors.grey.shade600), |
|||
), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: () { |
|||
Navigator.of(context).pop(); |
|||
onConfirm(); |
|||
}, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.blue.shade600, |
|||
foregroundColor: Colors.white, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
), |
|||
child: const Text('Confirmer'), |
|||
), |
|||
], |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.grey.shade200), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
children: [ |
|||
const Text( |
|||
'Actions sur la commande', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 16, |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
Wrap( |
|||
spacing: 8, |
|||
runSpacing: 8, |
|||
children: _buildActionButtons(context), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,189 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
|
|||
|
|||
// Dialog pour la remise |
|||
class DiscountDialog extends StatefulWidget { |
|||
final Commande commande; |
|||
|
|||
const DiscountDialog({super.key, required this.commande}); |
|||
|
|||
@override |
|||
_DiscountDialogState createState() => _DiscountDialogState(); |
|||
} |
|||
|
|||
class _DiscountDialogState extends State<DiscountDialog> { |
|||
final _pourcentageController = TextEditingController(); |
|||
final _montantController = TextEditingController(); |
|||
bool _isPercentage = true; |
|||
double _montantFinal = 0; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_montantFinal = widget.commande.montantTotal; |
|||
} |
|||
|
|||
void _calculateDiscount() { |
|||
double discount = 0; |
|||
|
|||
if (_isPercentage) { |
|||
final percentage = double.tryParse(_pourcentageController.text) ?? 0; |
|||
discount = (widget.commande.montantTotal * percentage) / 100; |
|||
} else { |
|||
discount = double.tryParse(_montantController.text) ?? 0; |
|||
} |
|||
|
|||
setState(() { |
|||
_montantFinal = widget.commande.montantTotal - discount; |
|||
if (_montantFinal < 0) _montantFinal = 0; |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return AlertDialog( |
|||
title: const Text('Appliquer une remise'), |
|||
content: SingleChildScrollView( |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Text('Montant original: ${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), |
|||
const SizedBox(height: 16), |
|||
|
|||
// Choix du type de remise |
|||
Row( |
|||
children: [ |
|||
Expanded( |
|||
child: RadioListTile<bool>( |
|||
title: const Text('Pourcentage'), |
|||
value: true, |
|||
groupValue: _isPercentage, |
|||
onChanged: (value) { |
|||
setState(() { |
|||
_isPercentage = value!; |
|||
_calculateDiscount(); |
|||
}); |
|||
}, |
|||
), |
|||
), |
|||
Expanded( |
|||
child: RadioListTile<bool>( |
|||
title: const Text('Montant fixe'), |
|||
value: false, |
|||
groupValue: _isPercentage, |
|||
onChanged: (value) { |
|||
setState(() { |
|||
_isPercentage = value!; |
|||
_calculateDiscount(); |
|||
}); |
|||
}, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
if (_isPercentage) |
|||
TextField( |
|||
controller: _pourcentageController, |
|||
decoration: const InputDecoration( |
|||
labelText: 'Pourcentage de remise', |
|||
suffixText: '%', |
|||
border: OutlineInputBorder(), |
|||
), |
|||
keyboardType: TextInputType.number, |
|||
onChanged: (value) => _calculateDiscount(), |
|||
) |
|||
else |
|||
TextField( |
|||
controller: _montantController, |
|||
decoration: const InputDecoration( |
|||
labelText: 'Montant de remise', |
|||
suffixText: 'MGA', |
|||
border: OutlineInputBorder(), |
|||
), |
|||
keyboardType: TextInputType.number, |
|||
onChanged: (value) => _calculateDiscount(), |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
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 final:'), |
|||
Text( |
|||
'${_montantFinal.toStringAsFixed(2)} MGA', |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 16, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
if (_montantFinal < widget.commande.montantTotal) |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text('Économie:'), |
|||
Text( |
|||
'${(widget.commande.montantTotal - _montantFinal).toStringAsFixed(2)} MGA', |
|||
style: const TextStyle( |
|||
color: Colors.green, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Navigator.pop(context), |
|||
child: const Text('Annuler'), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: _montantFinal < widget.commande.montantTotal |
|||
? () { |
|||
final pourcentage = _isPercentage |
|||
? double.tryParse(_pourcentageController.text) |
|||
: null; |
|||
final montant = !_isPercentage |
|||
? double.tryParse(_montantController.text) |
|||
: null; |
|||
|
|||
Navigator.pop(context, { |
|||
'pourcentage': pourcentage, |
|||
'montant': montant, |
|||
'montantFinal': _montantFinal, |
|||
}); |
|||
} |
|||
: null, |
|||
child: const Text('Appliquer'), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_pourcentageController.dispose(); |
|||
_montantController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
import 'package:youmazgestion/Models/produit.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
|
|||
|
|||
// Dialog pour sélectionner un cadeau |
|||
class GiftSelectionDialog extends StatefulWidget { |
|||
final Commande commande; |
|||
|
|||
const GiftSelectionDialog({super.key, required this.commande}); |
|||
|
|||
@override |
|||
_GiftSelectionDialogState createState() => _GiftSelectionDialogState(); |
|||
} |
|||
|
|||
class _GiftSelectionDialogState extends State<GiftSelectionDialog> { |
|||
List<Product> _products = []; |
|||
List<Product> _filteredProducts = []; |
|||
final _searchController = TextEditingController(); |
|||
Product? _selectedProduct; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_loadProducts(); |
|||
_searchController.addListener(_filterProducts); |
|||
} |
|||
|
|||
Future<void> _loadProducts() async { |
|||
final products = await AppDatabase.instance.getProducts(); |
|||
setState(() { |
|||
_products = products.where((p) => p.stock > 0).toList(); |
|||
_filteredProducts = _products; |
|||
}); |
|||
} |
|||
|
|||
void _filterProducts() { |
|||
final query = _searchController.text.toLowerCase(); |
|||
setState(() { |
|||
_filteredProducts = _products.where((product) { |
|||
return product.name.toLowerCase().contains(query) || |
|||
(product.reference?.toLowerCase().contains(query) ?? false) || |
|||
(product.category.toLowerCase().contains(query)); |
|||
}).toList(); |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return AlertDialog( |
|||
title: const Text('Sélectionner un cadeau'), |
|||
content: SizedBox( |
|||
width: double.maxFinite, |
|||
height: 400, |
|||
child: Column( |
|||
children: [ |
|||
TextField( |
|||
controller: _searchController, |
|||
decoration: const InputDecoration( |
|||
labelText: 'Rechercher un produit', |
|||
prefixIcon: Icon(Icons.search), |
|||
border: OutlineInputBorder(), |
|||
), |
|||
), |
|||
const SizedBox(height: 16), |
|||
Expanded( |
|||
child: ListView.builder( |
|||
itemCount: _filteredProducts.length, |
|||
itemBuilder: (context, index) { |
|||
final product = _filteredProducts[index]; |
|||
return Card( |
|||
child: ListTile( |
|||
leading: product.image != null |
|||
? Image.network( |
|||
product.image!, |
|||
width: 50, |
|||
height: 50, |
|||
fit: BoxFit.cover, |
|||
errorBuilder: (context, error, stackTrace) => |
|||
const Icon(Icons.image_not_supported), |
|||
) |
|||
: const Icon(Icons.phone_android), |
|||
title: Text(product.name), |
|||
subtitle: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text('Catégorie: ${product.category}'), |
|||
Text('Stock: ${product.stock}'), |
|||
if (product.reference != null) |
|||
Text('Réf: ${product.reference}'), |
|||
], |
|||
), |
|||
trailing: Radio<Product>( |
|||
value: product, |
|||
groupValue: _selectedProduct, |
|||
onChanged: (value) { |
|||
setState(() { |
|||
_selectedProduct = value; |
|||
}); |
|||
}, |
|||
), |
|||
onTap: () { |
|||
setState(() { |
|||
_selectedProduct = product; |
|||
}); |
|||
}, |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Navigator.pop(context), |
|||
child: const Text('Annuler'), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: _selectedProduct != null |
|||
? () => Navigator.pop(context, _selectedProduct) |
|||
: null, |
|||
child: const Text('Ajouter le cadeau'), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_searchController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import 'package:youmazgestion/Components/paymentType.dart'; |
|||
|
|||
class PaymentMethod { |
|||
final PaymentType type; |
|||
final double amountGiven; |
|||
|
|||
PaymentMethod({required this.type, this.amountGiven = 0}); |
|||
} |
|||
@ -0,0 +1,288 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:get/get_core/src/get_main.dart'; |
|||
import 'package:get/get_navigation/src/snackbar/snackbar.dart'; |
|||
import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart'; |
|||
import 'package:youmazgestion/Components/paymentType.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
|
|||
|
|||
class PaymentMethodDialog extends StatefulWidget { |
|||
final Commande commande; |
|||
|
|||
const PaymentMethodDialog({super.key, required this.commande}); |
|||
|
|||
@override |
|||
_PaymentMethodDialogState createState() => _PaymentMethodDialogState(); |
|||
} |
|||
|
|||
class _PaymentMethodDialogState extends State<PaymentMethodDialog> { |
|||
PaymentType _selectedPayment = PaymentType.cash; |
|||
final _amountController = TextEditingController(); |
|||
|
|||
void _validatePayment() { |
|||
final montantFinal = widget.commande.montantApresRemise ?? 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, PaymentMethod( |
|||
type: _selectedPayment, |
|||
amountGiven: _selectedPayment == PaymentType.cash |
|||
? double.parse(_amountController.text) |
|||
: montantFinal, |
|||
)); |
|||
} |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; |
|||
_amountController.text = montantFinal.toStringAsFixed(2); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_amountController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final amount = double.tryParse(_amountController.text) ?? 0; |
|||
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; |
|||
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: [ |
|||
// Affichage du montant à payer |
|||
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: [ |
|||
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 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), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
enum PaymentType { |
|||
cash, |
|||
card, |
|||
mvola, |
|||
orange, |
|||
airtel |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,258 @@ |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
|
|||
class PermissionCacheService extends GetxController { |
|||
static final PermissionCacheService instance = PermissionCacheService._init(); |
|||
PermissionCacheService._init(); |
|||
|
|||
// Cache en mémoire optimisé |
|||
final Map<String, Map<String, bool>> _permissionCache = {}; |
|||
final Map<String, List<Map<String, dynamic>>> _menuCache = {}; |
|||
bool _isLoaded = false; |
|||
String _currentUsername = ''; |
|||
|
|||
/// ✅ OPTIMISÉ: Une seule requête complexe pour charger tout |
|||
Future<void> loadUserPermissions(String username) async { |
|||
if (_isLoaded && _currentUsername == username && _permissionCache.containsKey(username)) { |
|||
print("📋 Permissions déjà en cache pour: $username"); |
|||
return; |
|||
} |
|||
|
|||
print("🔄 Chargement OPTIMISÉ des permissions pour: $username"); |
|||
final stopwatch = Stopwatch()..start(); |
|||
|
|||
try { |
|||
final db = AppDatabase.instance; |
|||
|
|||
// 🚀 UNE SEULE REQUÊTE pour tout récupérer |
|||
final userPermissions = await _getUserPermissionsOptimized(db, username); |
|||
|
|||
// Organiser les données |
|||
Map<String, bool> permissions = {}; |
|||
Set<Map<String, dynamic>> accessibleMenus = {}; |
|||
|
|||
for (var row in userPermissions) { |
|||
final menuId = row['menu_id'] as int; |
|||
final menuName = row['menu_name'] as String; |
|||
final menuRoute = row['menu_route'] as String; |
|||
final permissionName = row['permission_name'] as String; |
|||
|
|||
// Ajouter la permission |
|||
final key = "${permissionName}_$menuRoute"; |
|||
permissions[key] = true; |
|||
|
|||
// Ajouter le menu aux accessibles |
|||
accessibleMenus.add({ |
|||
'id': menuId, |
|||
'name': menuName, |
|||
'route': menuRoute, |
|||
}); |
|||
} |
|||
|
|||
// Mettre en cache |
|||
_permissionCache[username] = permissions; |
|||
_menuCache[username] = accessibleMenus.toList(); |
|||
_currentUsername = username; |
|||
_isLoaded = true; |
|||
|
|||
stopwatch.stop(); |
|||
print("✅ Permissions chargées en ${stopwatch.elapsedMilliseconds}ms"); |
|||
print(" - ${permissions.length} permissions"); |
|||
print(" - ${accessibleMenus.length} menus accessibles"); |
|||
|
|||
} catch (e) { |
|||
stopwatch.stop(); |
|||
print("❌ Erreur après ${stopwatch.elapsedMilliseconds}ms: $e"); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
/// 🚀 NOUVELLE MÉTHODE: Une seule requête optimisée |
|||
Future<List<Map<String, dynamic>>> _getUserPermissionsOptimized( |
|||
AppDatabase db, String username) async { |
|||
|
|||
final connection = await db.database; |
|||
|
|||
final result = await connection.query(''' |
|||
SELECT DISTINCT |
|||
m.id as menu_id, |
|||
m.name as menu_name, |
|||
m.route as menu_route, |
|||
p.name as permission_name |
|||
FROM users u |
|||
INNER JOIN roles r ON u.role_id = r.id |
|||
INNER JOIN role_menu_permissions rmp ON r.id = rmp.role_id |
|||
INNER JOIN menu m ON rmp.menu_id = m.id |
|||
INNER JOIN permissions p ON rmp.permission_id = p.id |
|||
WHERE u.username = ? |
|||
ORDER BY m.name, p.name |
|||
''', [username]); |
|||
|
|||
return result.map((row) => row.fields).toList(); |
|||
} |
|||
|
|||
/// ✅ Vérification rapide depuis le cache |
|||
bool hasPermission(String username, String permissionName, String menuRoute) { |
|||
final userPermissions = _permissionCache[username]; |
|||
if (userPermissions == null) { |
|||
print("⚠️ Cache non initialisé pour: $username"); |
|||
return false; |
|||
} |
|||
|
|||
final key = "${permissionName}_$menuRoute"; |
|||
return userPermissions[key] ?? false; |
|||
} |
|||
|
|||
/// ✅ Récupération rapide des menus |
|||
List<Map<String, dynamic>> getUserMenus(String username) { |
|||
return _menuCache[username] ?? []; |
|||
} |
|||
|
|||
/// ✅ Vérification d'accès menu |
|||
bool hasMenuAccess(String username, String menuRoute) { |
|||
final userMenus = _menuCache[username] ?? []; |
|||
return userMenus.any((menu) => menu['route'] == menuRoute); |
|||
} |
|||
|
|||
/// ✅ Préchargement asynchrone en arrière-plan |
|||
Future<void> preloadUserDataAsync(String username) async { |
|||
// Lancer en arrière-plan sans bloquer l'UI |
|||
unawaited(_preloadInBackground(username)); |
|||
} |
|||
|
|||
Future<void> _preloadInBackground(String username) async { |
|||
try { |
|||
print("🔄 Préchargement en arrière-plan pour: $username"); |
|||
await loadUserPermissions(username); |
|||
print("✅ Préchargement terminé"); |
|||
} catch (e) { |
|||
print("⚠️ Erreur préchargement: $e"); |
|||
} |
|||
} |
|||
|
|||
/// ✅ Préchargement synchrone (pour la connexion) |
|||
Future<void> preloadUserData(String username) async { |
|||
try { |
|||
print("🔄 Préchargement synchrone pour: $username"); |
|||
await loadUserPermissions(username); |
|||
print("✅ Données préchargées avec succès"); |
|||
} catch (e) { |
|||
print("❌ Erreur lors du préchargement: $e"); |
|||
// Ne pas bloquer la connexion |
|||
} |
|||
} |
|||
|
|||
/// ✅ Vider le cache |
|||
void clearAllCache() { |
|||
_permissionCache.clear(); |
|||
_menuCache.clear(); |
|||
_isLoaded = false; |
|||
_currentUsername = ''; |
|||
print("🗑️ Cache vidé complètement"); |
|||
} |
|||
|
|||
/// ✅ Rechargement forcé |
|||
Future<void> refreshUserPermissions(String username) async { |
|||
_permissionCache.remove(username); |
|||
_menuCache.remove(username); |
|||
_isLoaded = false; |
|||
|
|||
await loadUserPermissions(username); |
|||
print("🔄 Permissions rechargées pour: $username"); |
|||
} |
|||
|
|||
/// ✅ Status du cache |
|||
bool get isLoaded => _isLoaded && _currentUsername.isNotEmpty; |
|||
String get currentCachedUser => _currentUsername; |
|||
|
|||
/// ✅ Statistiques |
|||
Map<String, dynamic> getCacheStats() { |
|||
return { |
|||
'is_loaded': _isLoaded, |
|||
'current_user': _currentUsername, |
|||
'users_cached': _permissionCache.length, |
|||
'total_permissions': _permissionCache.values |
|||
.map((perms) => perms.length) |
|||
.fold(0, (a, b) => a + b), |
|||
'total_menus': _menuCache.values |
|||
.map((menus) => menus.length) |
|||
.fold(0, (a, b) => a + b), |
|||
}; |
|||
} |
|||
|
|||
/// ✅ Debug amélioré |
|||
void debugPrintCache() { |
|||
print("=== DEBUG CACHE OPTIMISÉ ==="); |
|||
print("Chargé: $_isLoaded"); |
|||
print("Utilisateur actuel: $_currentUsername"); |
|||
print("Utilisateurs en cache: ${_permissionCache.keys.toList()}"); |
|||
|
|||
for (var username in _permissionCache.keys) { |
|||
final permissions = _permissionCache[username]!; |
|||
final menus = _menuCache[username] ?? []; |
|||
print("$username: ${permissions.length} permissions, ${menus.length} menus"); |
|||
|
|||
// Détail des menus pour debug |
|||
for (var menu in menus.take(3)) { |
|||
print(" → ${menu['name']} (${menu['route']})"); |
|||
} |
|||
} |
|||
print("============================"); |
|||
} |
|||
|
|||
/// ✅ NOUVEAU: Validation de l'intégrité du cache |
|||
Future<bool> validateCacheIntegrity(String username) async { |
|||
if (!_permissionCache.containsKey(username)) { |
|||
return false; |
|||
} |
|||
|
|||
try { |
|||
final db = AppDatabase.instance; |
|||
final connection = await db.database; |
|||
|
|||
// Vérification rapide: compter les permissions de l'utilisateur |
|||
final result = await connection.query(''' |
|||
SELECT COUNT(DISTINCT CONCAT(p.name, '_', m.route)) as permission_count |
|||
FROM users u |
|||
INNER JOIN roles r ON u.role_id = r.id |
|||
INNER JOIN role_menu_permissions rmp ON r.id = rmp.role_id |
|||
INNER JOIN menu m ON rmp.menu_id = m.id |
|||
INNER JOIN permissions p ON rmp.permission_id = p.id |
|||
WHERE u.username = ? |
|||
''', [username]); |
|||
|
|||
final dbCount = result.first['permission_count'] as int; |
|||
final cacheCount = _permissionCache[username]!.length; |
|||
|
|||
final isValid = dbCount == cacheCount; |
|||
if (!isValid) { |
|||
print("⚠️ Cache invalide: DB=$dbCount, Cache=$cacheCount"); |
|||
} |
|||
|
|||
return isValid; |
|||
} catch (e) { |
|||
print("❌ Erreur validation cache: $e"); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// ✅ NOUVEAU: Rechargement intelligent |
|||
Future<void> smartRefresh(String username) async { |
|||
final isValid = await validateCacheIntegrity(username); |
|||
|
|||
if (!isValid) { |
|||
print("🔄 Cache invalide, rechargement nécessaire"); |
|||
await refreshUserPermissions(username); |
|||
} else { |
|||
print("✅ Cache valide, pas de rechargement nécessaire"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// ✅ Extension pour éviter l'import de dart:async |
|||
void unawaited(Future future) { |
|||
// Ignorer le warning sur le Future non attendu |
|||
future.catchError((error) { |
|||
print("Erreur tâche en arrière-plan: $error"); |
|||
}); |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue