lastlast update
This commit is contained in:
parent
595b38e9fb
commit
c0bbb0da2b
BIN
assets/NotoEmoji-Regular.ttf
Normal file
BIN
assets/NotoEmoji-Regular.ttf
Normal file
Binary file not shown.
@ -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<PaymentMethodEnhancedDialog> {
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: textColor,
|
||||
),
|
||||
textAlign: isAmount ? TextAlign.right : TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
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<List<DetailCommande>>(
|
||||
future: AppDatabase.instance.getDetailsCommande(commande.id!),
|
||||
@ -49,23 +148,66 @@ 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,
|
||||
children: [
|
||||
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',
|
||||
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: Colors.black87,
|
||||
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),
|
||||
Container(
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
234
lib/Components/commandManagementComponents/PaswordRequired.dart
Normal file
234
lib/Components/commandManagementComponents/PaswordRequired.dart
Normal file
@ -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<PasswordVerificationDialog> {
|
||||
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<Color>(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<PaymentMethodDialog> {
|
||||
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<PaymentMethodDialog> {
|
||||
@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<PaymentMethodDialog> {
|
||||
@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<PaymentMethodDialog> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Affichage du montant à payer
|
||||
// Affichage du montant à payer (simplifié)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@ -78,27 +77,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
||||
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(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
@ -106,8 +85,6 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
411
lib/Components/newCommandComponents/CadeauDialog.dart
Normal file
411
lib/Components/newCommandComponents/CadeauDialog.dart
Normal file
@ -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<CadeauDialog> {
|
||||
final AppDatabase _database = AppDatabase.instance;
|
||||
List<Product> _produitsDisponibles = [];
|
||||
Product? _produitCadeauSelectionne;
|
||||
int _quantiteCadeau = 1;
|
||||
bool _isLoading = true;
|
||||
String _searchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProduitsDisponibles();
|
||||
}
|
||||
|
||||
Future<void> _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<Product> 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
331
lib/Components/newCommandComponents/RemiseDialog.dart
Normal file
331
lib/Components/newCommandComponents/RemiseDialog.dart
Normal file
@ -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<RemiseDialog> createState() => _RemiseDialogState();
|
||||
}
|
||||
|
||||
class _RemiseDialogState extends State<RemiseDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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<RemiseType>(
|
||||
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<RemiseType>(
|
||||
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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
||||
}
|
||||
304
lib/Services/Script.sql
Normal file
304
lib/Services/Script.sql
Normal file
@ -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;
|
||||
@ -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<void> _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 ---
|
||||
|
||||
@ -945,19 +781,25 @@ Future<void> _createDB() async {
|
||||
detailMap.values.toList()
|
||||
);
|
||||
return result.insertId!;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DetailCommande>> getDetailsCommande(int commandeId) async {
|
||||
// Méthode mise à jour pour récupérer les détails avec les remises
|
||||
Future<List<DetailCommande>> 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
|
||||
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
|
||||
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<void> _createDB() async {
|
||||
|
||||
// --- TRANSACTIONS COMPLEXES ---
|
||||
|
||||
Future<int> createCommandeComplete(Client client, Commande commande, List<DetailCommande> details) async {
|
||||
|
||||
// Méthode pour créer une commande complète avec remises
|
||||
Future<int> createCommandeComplete(Client client, Commande commande, List<DetailCommande> 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<void> _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<void> _createDB() async {
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode pour mettre à jour un détail de commande (utile pour modifier les remises)
|
||||
Future<int> 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<Map<String, dynamic>> 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<List<Map<String, dynamic>>> 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<Map<String, int>> getProductCountByCategory() async {
|
||||
@ -1799,7 +1748,6 @@ Future<Client?> 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<Client?> findClientByAnyIdentifier({
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> migrateDatabaseForDiscountAndGift() async {
|
||||
//
|
||||
// Méthode pour obtenir les statistiques des cadeaux
|
||||
Future<Map<String, dynamic>> 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
|
||||
''');
|
||||
|
||||
await db.query('''
|
||||
ALTER TABLE commandes
|
||||
ADD COLUMN montantApresRemise DECIMAL(10,2) NULL
|
||||
''');
|
||||
|
||||
// Ajouter la colonne cadeau à la table details_commandes
|
||||
await db.query('''
|
||||
ALTER TABLE details_commandes
|
||||
ADD COLUMN estCadeau TINYINT(1) DEFAULT 0
|
||||
''');
|
||||
|
||||
print("Migration pour remise et cadeau terminée avec succès");
|
||||
} catch (e) {
|
||||
// Les colonnes existent probablement déjà
|
||||
print("Migration déjà effectuée ou erreur: $e");
|
||||
}
|
||||
}
|
||||
Future<List<DetailCommande>> getDetailsCommandeAvecCadeaux(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
|
||||
// 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
|
||||
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();
|
||||
}
|
||||
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
|
||||
''');
|
||||
|
||||
Future<int> updateCommandeAvecRemise(int commandeId, {
|
||||
double? remisePourcentage,
|
||||
double? remiseMontant,
|
||||
double? montantApresRemise,
|
||||
}) async {
|
||||
// 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
|
||||
''');
|
||||
|
||||
// É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
|
||||
''');
|
||||
|
||||
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) {
|
||||
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': [],
|
||||
};
|
||||
}
|
||||
}
|
||||
// Méthode pour obtenir les commandes avec des cadeaux
|
||||
Future<List<Map<String, dynamic>>> getCommandesAvecCadeaux({int limit = 20}) async {
|
||||
final db = await database;
|
||||
|
||||
List<String> setClauses = [];
|
||||
List<dynamic> values = [];
|
||||
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]);
|
||||
|
||||
if (remisePourcentage != null) {
|
||||
setClauses.add('remisePourcentage = ?');
|
||||
values.add(remisePourcentage);
|
||||
return result.map((row) => row.fields).toList();
|
||||
} catch (e) {
|
||||
print("Erreur lors de la récupération des commandes avec cadeaux: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// Méthode pour obtenir les produits les plus offerts en cadeau
|
||||
Future<List<Map<String, dynamic>>> getProduitsLesPlusOffertsEnCadeau({int limit = 10}) async {
|
||||
final db = await database;
|
||||
|
||||
if (remiseMontant != null) {
|
||||
setClauses.add('remiseMontant = ?');
|
||||
values.add(remiseMontant);
|
||||
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<List<Map<String, dynamic>>> getClientsAvecLePlusDeCadeaux({int limit = 10}) async {
|
||||
final db = await database;
|
||||
|
||||
if (montantApresRemise != null) {
|
||||
setClauses.add('montantApresRemise = ?');
|
||||
values.add(montantApresRemise);
|
||||
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<Map<String, dynamic>> getImpactCadeauxSurVentes() async {
|
||||
final db = await database;
|
||||
|
||||
if (setClauses.isEmpty) return 0;
|
||||
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
|
||||
)
|
||||
|
||||
values.add(commandeId);
|
||||
UNION ALL
|
||||
|
||||
final result = await db.query(
|
||||
'UPDATE commandes SET ${setClauses.join(', ')} WHERE id = ?',
|
||||
values
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode pour créer une commande complète avec cadeaux (mise à jour)
|
||||
Future<int> createCommandeCompleteAvecCadeaux(Client client, Commande commande, List<DetailCommande> details) async {
|
||||
final db = await database;
|
||||
|
||||
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!;
|
||||
|
||||
return result.affectedRows!;
|
||||
}
|
||||
|
||||
Future<int> createDetailCommandeCadeau(DetailCommande detail) async {
|
||||
final db = await database;
|
||||
|
||||
// 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['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
|
||||
detailMap['commandeId'] = commandeId;
|
||||
|
||||
final fields = detailMap.keys.join(', ');
|
||||
final placeholders = List.filled(detailMap.length, '?').join(', ');
|
||||
final detailFields = detailMap.keys.join(', ');
|
||||
final detailPlaceholders = List.filled(detailMap.length, '?').join(', ');
|
||||
|
||||
final result = await db.query(
|
||||
'INSERT INTO details_commandes ($fields) VALUES ($placeholders)',
|
||||
await db.query(
|
||||
'INSERT INTO details_commandes ($detailFields) VALUES ($detailPlaceholders)',
|
||||
detailMap.values.toList()
|
||||
);
|
||||
return result.insertId!;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DetailCommande>> getCadeauxCommande(int commandeId) async {
|
||||
// Méthode pour valider la disponibilité des cadeaux avant la commande
|
||||
Future<List<String>> verifierDisponibiliteCadeaux(List<DetailCommande> details) 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();
|
||||
}
|
||||
List<String> erreurs = [];
|
||||
|
||||
Future<double> calculateMontantTotalSansCadeaux(int commandeId) async {
|
||||
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");
|
||||
}
|
||||
|
||||
return erreurs;
|
||||
}
|
||||
// --- MÉTHODES POUR LES VENTES PAR POINT DE VENTE ---
|
||||
|
||||
Future<List<Map<String, dynamic>>> getVentesParPointDeVente() 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
|
||||
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<int> supprimerRemiseCommande(int commandeId) async {
|
||||
Future<List<Map<String, dynamic>>> getTopProduitsParPointDeVente(int pointDeVenteId, {int limit = 5}) async {
|
||||
final db = await database;
|
||||
|
||||
try {
|
||||
final result = await db.query('''
|
||||
UPDATE commandes
|
||||
SET remisePourcentage = NULL, remiseMontant = NULL, montantApresRemise = NULL
|
||||
WHERE id = ?
|
||||
''', [commandeId]);
|
||||
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.affectedRows!;
|
||||
return result.map((row) => row.fields).toList();
|
||||
} catch (e) {
|
||||
print("Erreur getTopProduitsParPointDeVente: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getVentesParPointDeVenteParMois(int pointDeVenteId) async {
|
||||
final db = await database;
|
||||
|
||||
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<bool> verifyCurrentUserPassword(String password) async {
|
||||
final db = await database;
|
||||
final userController = Get.find<UserController>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -185,7 +185,9 @@ Future<void> _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<void> _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<List<Map<String, dynamic>>>(
|
||||
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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<String, dynamic> 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<List<Map<String, dynamic>>>(
|
||||
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(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
|
||||
@ -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<void> _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<void> _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;
|
||||
|
||||
@ -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<AccueilPage> {
|
||||
await orderDatabase.insertOrderItem(
|
||||
orderId, product.name, quantity, price);
|
||||
|
||||
final updatedStock = product.stock! - quantity;
|
||||
final updatedStock = product.stock - quantity;
|
||||
await productDatabase.updateStock(product.id!, updatedStock);
|
||||
}
|
||||
|
||||
|
||||
@ -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<String, dynamic> getConfig() {
|
||||
if (isDevelopment) {
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
#include <charset_converter/charset_converter_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <open_file_linux/open_file_linux_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
|
||||
16
pubspec.lock
16
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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -8,13 +8,19 @@
|
||||
|
||||
#include <charset_converter/charset_converter_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user