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