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:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Models/client.dart';
|
import 'package:youmazgestion/Models/client.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||||
@ -7,9 +9,7 @@ class CommandeDetails extends StatelessWidget {
|
|||||||
|
|
||||||
const CommandeDetails({required this.commande});
|
const CommandeDetails({required this.commande});
|
||||||
|
|
||||||
|
Widget _buildTableHeader(String text, {bool isAmount = false}) {
|
||||||
|
|
||||||
Widget _buildTableHeader(String text) {
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -18,23 +18,122 @@ class CommandeDetails extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
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,
|
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) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<List<DetailCommande>>(
|
return FutureBuilder<List<DetailCommande>>(
|
||||||
future: AppDatabase.instance.getDetailsCommande(commande.id!),
|
future: AppDatabase.instance.getDetailsCommande(commande.id!),
|
||||||
@ -49,23 +148,66 @@ class CommandeDetails extends StatelessWidget {
|
|||||||
|
|
||||||
final details = snapshot.data!;
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.shade50,
|
color: hasRemises ? Colors.orange.shade50 : Colors.blue.shade50,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: hasRemises
|
||||||
|
? Border.all(color: Colors.orange.shade200)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Row(
|
||||||
'Détails de la commande',
|
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(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
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),
|
const SizedBox(height: 12),
|
||||||
Container(
|
Container(
|
||||||
@ -82,26 +224,72 @@ class CommandeDetails extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_buildTableHeader('Produit'),
|
_buildTableHeader('Produit'),
|
||||||
_buildTableHeader('Qté'),
|
_buildTableHeader('Qté'),
|
||||||
_buildTableHeader('Prix unit.'),
|
_buildTableHeader('Prix unit.', isAmount: true),
|
||||||
_buildTableHeader('Total'),
|
if (hasRemises) _buildTableHeader('Remise'),
|
||||||
|
_buildTableHeader('Total', isAmount: true),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
...details.map((detail) => TableRow(
|
...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: [
|
children: [
|
||||||
_buildTableCell(
|
Padding(
|
||||||
detail.estCadeau == true
|
padding: const EdgeInsets.all(8.0),
|
||||||
? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)'
|
child: Column(
|
||||||
: detail.produitNom ?? 'Produit inconnu'
|
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.quantite}'),
|
||||||
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
|
_buildPriceColumn(detail),
|
||||||
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(2)} MGA'),
|
if (hasRemises) _buildRemiseColumn(detail),
|
||||||
|
_buildTotalColumn(detail),
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Section des totaux
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -111,39 +299,63 @@ class CommandeDetails extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (commande.montantApresRemise != null) ...[
|
// Sous-total si il y a des remises
|
||||||
|
if (hasRemises) ...[
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Sous-total:',
|
'Sous-total:',
|
||||||
style: TextStyle(fontSize: 14),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${commande.montantTotal.toStringAsFixed(2)} MGA',
|
'${sousTotal.toStringAsFixed(2)} MGA',
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Row(
|
||||||
'Remise:',
|
children: [
|
||||||
style: TextStyle(fontSize: 14),
|
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(
|
Text(
|
||||||
'-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA',
|
'-${totalRemises.toStringAsFixed(2)} MGA',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.red,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(height: 16),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Total final
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -155,7 +367,7 @@ class CommandeDetails extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(2)} MGA',
|
'${commande.montantTotal.toStringAsFixed(2)} MGA',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|||||||
@ -8,15 +8,13 @@ class CommandeActions extends StatelessWidget {
|
|||||||
final Commande commande;
|
final Commande commande;
|
||||||
final Function(int, StatutCommande) onStatutChanged;
|
final Function(int, StatutCommande) onStatutChanged;
|
||||||
final Function(Commande) onPaymentSelected;
|
final Function(Commande) onPaymentSelected;
|
||||||
final Function(Commande) onDiscountSelected;
|
|
||||||
final Function(Commande) onGiftSelected;
|
|
||||||
|
|
||||||
const CommandeActions({
|
const CommandeActions({
|
||||||
required this.commande,
|
required this.commande,
|
||||||
required this.onStatutChanged,
|
required this.onStatutChanged,
|
||||||
required this.onPaymentSelected,
|
required this.onPaymentSelected,
|
||||||
required this.onDiscountSelected,
|
|
||||||
required this.onGiftSelected,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -27,18 +25,7 @@ class CommandeActions extends StatelessWidget {
|
|||||||
switch (commande.statut) {
|
switch (commande.statut) {
|
||||||
case StatutCommande.enAttente:
|
case StatutCommande.enAttente:
|
||||||
buttons.addAll([
|
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(
|
_buildActionButton(
|
||||||
label: 'Confirmer',
|
label: 'Confirmer',
|
||||||
icon: Icons.check_circle,
|
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/Components/paymentType.dart';
|
||||||
import 'package:youmazgestion/Models/client.dart';
|
import 'package:youmazgestion/Models/client.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodDialog extends StatefulWidget {
|
class PaymentMethodDialog extends StatefulWidget {
|
||||||
final Commande commande;
|
final Commande commande;
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
|||||||
final _amountController = TextEditingController();
|
final _amountController = TextEditingController();
|
||||||
|
|
||||||
void _validatePayment() {
|
void _validatePayment() {
|
||||||
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
|
final montantFinal = widget.commande.montantTotal;
|
||||||
|
|
||||||
if (_selectedPayment == PaymentType.cash) {
|
if (_selectedPayment == PaymentType.cash) {
|
||||||
final amountGiven = double.tryParse(_amountController.text) ?? 0;
|
final amountGiven = double.tryParse(_amountController.text) ?? 0;
|
||||||
@ -48,7 +47,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
|
final montantFinal = widget.commande.montantTotal;
|
||||||
_amountController.text = montantFinal.toStringAsFixed(2);
|
_amountController.text = montantFinal.toStringAsFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +60,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||||
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
|
final montantFinal = widget.commande.montantTotal;
|
||||||
final change = amount - montantFinal;
|
final change = amount - montantFinal;
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
@ -70,7 +69,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Affichage du montant à payer
|
// Affichage du montant à payer (simplifié)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -78,27 +77,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: Colors.blue.shade200),
|
border: Border.all(color: Colors.blue.shade200),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Row(
|
||||||
children: [
|
|
||||||
if (widget.commande.montantApresRemise != null) ...[
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text('Montant original:'),
|
|
||||||
Text('${widget.commande.montantTotal.toStringAsFixed(2)} MGA'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text('Remise:'),
|
|
||||||
Text('-${(widget.commande.montantTotal - widget.commande.montantApresRemise!).toStringAsFixed(2)} MGA',
|
|
||||||
style: const TextStyle(color: Colors.red)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
],
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
|
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)),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 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? clientNom;
|
||||||
final String? clientPrenom;
|
final String? clientPrenom;
|
||||||
final String? clientEmail;
|
final String? clientEmail;
|
||||||
final double? remisePourcentage;
|
|
||||||
final double? remiseMontant;
|
|
||||||
final double? montantApresRemise;
|
|
||||||
|
|
||||||
Commande({
|
Commande({
|
||||||
this.id,
|
this.id,
|
||||||
@ -109,9 +106,6 @@ class Commande {
|
|||||||
this.clientNom,
|
this.clientNom,
|
||||||
this.clientPrenom,
|
this.clientPrenom,
|
||||||
this.clientEmail,
|
this.clientEmail,
|
||||||
this.remisePourcentage,
|
|
||||||
this.remiseMontant,
|
|
||||||
this.montantApresRemise,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
String get clientNomComplet {
|
String get clientNomComplet {
|
||||||
@ -143,9 +137,6 @@ class Commande {
|
|||||||
'dateLivraison': dateLivraison?.toIso8601String(),
|
'dateLivraison': dateLivraison?.toIso8601String(),
|
||||||
'commandeurId': commandeurId,
|
'commandeurId': commandeurId,
|
||||||
'validateurId': validateurId,
|
'validateurId': validateurId,
|
||||||
'remisePourcentage': remisePourcentage,
|
|
||||||
'remiseMontant': remiseMontant,
|
|
||||||
'montantApresRemise': montantApresRemise,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,56 +156,15 @@ class Commande {
|
|||||||
clientNom: map['clientNom'] as String?,
|
clientNom: map['clientNom'] as String?,
|
||||||
clientPrenom: map['clientPrenom'] as String?,
|
clientPrenom: map['clientPrenom'] as String?,
|
||||||
clientEmail: map['clientEmail'] 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 :
|
// REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci :
|
||||||
|
enum RemiseType {
|
||||||
|
pourcentage,
|
||||||
|
montant
|
||||||
|
}
|
||||||
|
|
||||||
class DetailCommande {
|
class DetailCommande {
|
||||||
final int? id;
|
final int? id;
|
||||||
@ -222,16 +172,15 @@ class DetailCommande {
|
|||||||
final int produitId;
|
final int produitId;
|
||||||
final int quantite;
|
final int quantite;
|
||||||
final double prixUnitaire;
|
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? produitNom;
|
||||||
final String? produitImage;
|
final String? produitImage;
|
||||||
final String? produitReference;
|
final String? produitReference;
|
||||||
final bool? estCadeau;
|
|
||||||
|
|
||||||
// NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT
|
|
||||||
final double? remisePourcentage;
|
|
||||||
final double? remiseMontant;
|
|
||||||
final double? prixApresRemise;
|
|
||||||
|
|
||||||
DetailCommande({
|
DetailCommande({
|
||||||
this.id,
|
this.id,
|
||||||
@ -240,15 +189,195 @@ class DetailCommande {
|
|||||||
required this.quantite,
|
required this.quantite,
|
||||||
required this.prixUnitaire,
|
required this.prixUnitaire,
|
||||||
required this.sousTotal,
|
required this.sousTotal,
|
||||||
|
this.remiseType,
|
||||||
|
this.remiseValeur = 0.0,
|
||||||
|
this.montantRemise = 0.0,
|
||||||
|
required this.prixFinal,
|
||||||
|
this.estCadeau = false,
|
||||||
this.produitNom,
|
this.produitNom,
|
||||||
this.produitImage,
|
this.produitImage,
|
||||||
this.produitReference,
|
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() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'id': id,
|
'id': id,
|
||||||
@ -257,14 +386,24 @@ class DetailCommande {
|
|||||||
'quantite': quantite,
|
'quantite': quantite,
|
||||||
'prixUnitaire': prixUnitaire,
|
'prixUnitaire': prixUnitaire,
|
||||||
'sousTotal': sousTotal,
|
'sousTotal': sousTotal,
|
||||||
'estCadeau': estCadeau == true ? 1 : 0,
|
'remise_type': remiseType?.name,
|
||||||
'remisePourcentage': remisePourcentage,
|
'remise_valeur': remiseValeur,
|
||||||
'remiseMontant': remiseMontant,
|
'montant_remise': montantRemise,
|
||||||
'prixApresRemise': prixApresRemise,
|
'prix_final': prixFinal,
|
||||||
|
'est_cadeau': estCadeau ? 1 : 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory DetailCommande.fromMap(Map<String, dynamic> map) {
|
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(
|
return DetailCommande(
|
||||||
id: map['id'] as int?,
|
id: map['id'] as int?,
|
||||||
commandeId: map['commandeId'] as int,
|
commandeId: map['commandeId'] as int,
|
||||||
@ -272,71 +411,15 @@ class DetailCommande {
|
|||||||
quantite: map['quantite'] as int,
|
quantite: map['quantite'] as int,
|
||||||
prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
|
prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
|
||||||
sousTotal: (map['sousTotal'] 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?,
|
produitNom: map['produitNom'] as String?,
|
||||||
produitImage: map['produitImage'] as String?,
|
produitImage: map['produitImage'] as String?,
|
||||||
produitReference: map['produitReference'] 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();
|
_connection = await _initDB();
|
||||||
// await _createDB();
|
// await _createDB();
|
||||||
|
|
||||||
// Effectuer la migration pour les bases existantes
|
|
||||||
await migrateDatabaseForDiscountAndGift();
|
|
||||||
|
|
||||||
await insertDefaultPermissions();
|
await insertDefaultPermissions();
|
||||||
await insertDefaultMenus();
|
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 ---
|
// --- MÉTHODES D'INSERTION PAR DÉFAUT ---
|
||||||
|
|
||||||
@ -945,19 +781,25 @@ Future<void> _createDB() async {
|
|||||||
detailMap.values.toList()
|
detailMap.values.toList()
|
||||||
);
|
);
|
||||||
return result.insertId!;
|
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 db = await database;
|
||||||
final result = await db.query('''
|
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
|
FROM details_commandes dc
|
||||||
LEFT JOIN products p ON dc.produitId = p.id
|
LEFT JOIN products p ON dc.produitId = p.id
|
||||||
WHERE dc.commandeId = ?
|
WHERE dc.commandeId = ?
|
||||||
ORDER BY dc.id
|
ORDER BY dc.est_cadeau ASC, dc.id
|
||||||
''', [commandeId]);
|
''', [commandeId]);
|
||||||
|
|
||||||
return result.map((row) => DetailCommande.fromMap(row.fields)).toList();
|
return result.map((row) => DetailCommande.fromMap(row.fields)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RECHERCHE PRODUITS ---
|
// --- RECHERCHE PRODUITS ---
|
||||||
|
|
||||||
@ -1364,17 +1206,19 @@ Future<void> _createDB() async {
|
|||||||
|
|
||||||
// --- TRANSACTIONS COMPLEXES ---
|
// --- 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;
|
final db = await database;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.query('START TRANSACTION');
|
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 existingOrNewClient = await createOrGetClient(client);
|
||||||
final clientId = existingOrNewClient.id!;
|
final clientId = existingOrNewClient.id!;
|
||||||
|
|
||||||
// 2. Créer la commande avec le bon clientId
|
// 2. Créer la commande
|
||||||
final commandeMap = commande.toMap();
|
final commandeMap = commande.toMap();
|
||||||
commandeMap.remove('id');
|
commandeMap.remove('id');
|
||||||
commandeMap['clientId'] = clientId;
|
commandeMap['clientId'] = clientId;
|
||||||
@ -1388,7 +1232,7 @@ Future<void> _createDB() async {
|
|||||||
);
|
);
|
||||||
final commandeId = commandeResult.insertId!;
|
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) {
|
for (final detail in details) {
|
||||||
final detailMap = detail.toMap();
|
final detailMap = detail.toMap();
|
||||||
detailMap.remove('id');
|
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 ---
|
// --- STATISTIQUES AVANCÉES ---
|
||||||
|
|
||||||
Future<Map<String, int>> getProductCountByCategory() async {
|
Future<Map<String, int>> getProductCountByCategory() async {
|
||||||
@ -1799,7 +1748,6 @@ Future<Client?> findClientByAnyIdentifier({
|
|||||||
String? nom,
|
String? nom,
|
||||||
String? prenom,
|
String? prenom,
|
||||||
}) async {
|
}) async {
|
||||||
final db = await database;
|
|
||||||
|
|
||||||
// Recherche par email si fourni
|
// Recherche par email si fourni
|
||||||
if (email != null && email.isNotEmpty) {
|
if (email != null && email.isNotEmpty) {
|
||||||
@ -1821,140 +1769,440 @@ Future<Client?> findClientByAnyIdentifier({
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
//
|
||||||
Future<void> migrateDatabaseForDiscountAndGift() async {
|
// Méthode pour obtenir les statistiques des cadeaux
|
||||||
|
Future<Map<String, dynamic>> getCadeauStatistics() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ajouter les colonnes de remise à la table commandes
|
// Total des cadeaux offerts
|
||||||
await db.query('''
|
final totalCadeauxResult = await db.query('''
|
||||||
ALTER TABLE commandes
|
SELECT
|
||||||
ADD COLUMN remisePourcentage DECIMAL(5,2) NULL
|
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('''
|
// Cadeaux par produit
|
||||||
ALTER TABLE commandes
|
final cadeauxParProduitResult = await db.query('''
|
||||||
ADD COLUMN remiseMontant DECIMAL(10,2) NULL
|
SELECT
|
||||||
''');
|
p.name as produit_nom,
|
||||||
|
p.category as produit_categorie,
|
||||||
await db.query('''
|
COUNT(*) as nombre_fois_offert,
|
||||||
ALTER TABLE commandes
|
SUM(dc.quantite) as quantite_totale_offerte,
|
||||||
ADD COLUMN montantApresRemise DECIMAL(10,2) NULL
|
SUM(dc.sousTotal) as valeur_totale_offerte
|
||||||
''');
|
|
||||||
|
|
||||||
// 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
|
|
||||||
FROM details_commandes dc
|
FROM details_commandes dc
|
||||||
LEFT JOIN products p ON dc.produitId = p.id
|
INNER JOIN products p ON dc.produitId = p.id
|
||||||
WHERE dc.commandeId = ?
|
WHERE dc.est_cadeau = 1
|
||||||
ORDER BY dc.estCadeau ASC, dc.id
|
GROUP BY dc.produitId, p.name, p.category
|
||||||
''', [commandeId]);
|
ORDER BY quantite_totale_offerte DESC
|
||||||
return result.map((row) => DetailCommande.fromMap(row.fields)).toList();
|
LIMIT 10
|
||||||
}
|
''');
|
||||||
|
|
||||||
Future<int> updateCommandeAvecRemise(int commandeId, {
|
// Commandes avec cadeaux
|
||||||
double? remisePourcentage,
|
final commandesAvecCadeauxResult = await db.query('''
|
||||||
double? remiseMontant,
|
SELECT
|
||||||
double? montantApresRemise,
|
COUNT(DISTINCT c.id) as nombre_commandes_avec_cadeaux,
|
||||||
}) async {
|
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;
|
final db = await database;
|
||||||
|
|
||||||
List<String> setClauses = [];
|
try {
|
||||||
List<dynamic> values = [];
|
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) {
|
return result.map((row) => row.fields).toList();
|
||||||
setClauses.add('remisePourcentage = ?');
|
} catch (e) {
|
||||||
values.add(remisePourcentage);
|
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) {
|
try {
|
||||||
setClauses.add('remiseMontant = ?');
|
final result = await db.query('''
|
||||||
values.add(remiseMontant);
|
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) {
|
try {
|
||||||
setClauses.add('montantApresRemise = ?');
|
final result = await db.query('''
|
||||||
values.add(montantApresRemise);
|
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(
|
SELECT
|
||||||
'UPDATE commandes SET ${setClauses.join(', ')} WHERE id = ?',
|
'sans_cadeaux' as type_commande,
|
||||||
values
|
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!;
|
// 3. Créer les détails de commande avec remises et cadeaux
|
||||||
}
|
for (final detail in details) {
|
||||||
|
|
||||||
Future<int> createDetailCommandeCadeau(DetailCommande detail) async {
|
|
||||||
final db = await database;
|
|
||||||
|
|
||||||
final detailMap = detail.toMap();
|
final detailMap = detail.toMap();
|
||||||
detailMap.remove('id');
|
detailMap.remove('id');
|
||||||
detailMap['estCadeau'] = 1; // Marquer comme cadeau
|
detailMap['commandeId'] = commandeId;
|
||||||
detailMap['prixUnitaire'] = 0.0; // Prix zéro pour les cadeaux
|
|
||||||
detailMap['sousTotal'] = 0.0; // Sous-total zéro pour les cadeaux
|
|
||||||
|
|
||||||
final fields = detailMap.keys.join(', ');
|
final detailFields = detailMap.keys.join(', ');
|
||||||
final placeholders = List.filled(detailMap.length, '?').join(', ');
|
final detailPlaceholders = List.filled(detailMap.length, '?').join(', ');
|
||||||
|
|
||||||
final result = await db.query(
|
await db.query(
|
||||||
'INSERT INTO details_commandes ($fields) VALUES ($placeholders)',
|
'INSERT INTO details_commandes ($detailFields) VALUES ($detailPlaceholders)',
|
||||||
detailMap.values.toList()
|
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 db = await database;
|
||||||
final result = await db.query('''
|
List<String> erreurs = [];
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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'];
|
try {
|
||||||
return total != null ? (total as num).toDouble() : 0.0;
|
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;
|
final db = await database;
|
||||||
|
|
||||||
|
try {
|
||||||
final result = await db.query('''
|
final result = await db.query('''
|
||||||
UPDATE commandes
|
SELECT
|
||||||
SET remisePourcentage = NULL, remiseMontant = NULL, montantApresRemise = NULL
|
p.id,
|
||||||
WHERE id = ?
|
p.name as produit_nom,
|
||||||
''', [commandeId]);
|
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
|
// Histogramme des catégories de produits
|
||||||
_buildCategoryHistogram(),
|
_buildCategoryHistogram(),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
|
// NOUVEAU: Widget des ventes par point de vente
|
||||||
|
_buildVentesParPointDeVenteCard(),
|
||||||
|
SizedBox(height: 20),
|
||||||
// Section des données récentes
|
// Section des données récentes
|
||||||
_buildRecentDataSection(),
|
_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() {
|
Widget _buildLowStockCard() {
|
||||||
|
|
||||||
return Card(
|
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/Models/role.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||||
import 'package:youmazgestion/Views/Dashboard.dart';
|
import 'package:youmazgestion/Views/Dashboard.dart';
|
||||||
import 'package:youmazgestion/accueil.dart';
|
|
||||||
|
|
||||||
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
|
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:esc_pos_printer/esc_pos_printer.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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:open_file/open_file.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:pdf/pdf.dart';
|
import 'package:pdf/pdf.dart';
|
||||||
@ -31,117 +30,6 @@ class TicketPage extends StatelessWidget {
|
|||||||
required this.amountPaid,
|
required this.amountPaid,
|
||||||
}) : super(key: key);
|
}) : 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 {
|
Future<void> _generateAndSavePDF() async {
|
||||||
final pdf = pw.Document();
|
final pdf = pw.Document();
|
||||||
@ -265,11 +153,7 @@ class TicketPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Obtenir la date actuelle
|
// 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
|
// Calculer la somme rendue
|
||||||
final double change = amountPaid - totalOrderAmount;
|
final double change = amountPaid - totalOrderAmount;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -115,7 +114,7 @@ class _AccueilPageState extends State<AccueilPage> {
|
|||||||
await orderDatabase.insertOrderItem(
|
await orderDatabase.insertOrderItem(
|
||||||
orderId, product.name, quantity, price);
|
orderId, product.name, quantity, price);
|
||||||
|
|
||||||
final updatedStock = product.stock! - quantity;
|
final updatedStock = product.stock - quantity;
|
||||||
await productDatabase.updateStock(product.id!, updatedStock);
|
await productDatabase.updateStock(product.id!, updatedStock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
// Config/database_config.dart - Version améliorée
|
// Config/database_config.dart - Version améliorée
|
||||||
class DatabaseConfig {
|
class DatabaseConfig {
|
||||||
static const String host = '172.20.10.5';
|
static const String host = 'localhost';
|
||||||
static const int port = 3306;
|
static const int port = 3306;
|
||||||
static const String username = 'root';
|
static const String username = 'root';
|
||||||
static const String? password = null;
|
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 prodHost = '185.70.105.157';
|
||||||
static const String prodUsername = 'guycom';
|
static const String prodUsername = 'guycom';
|
||||||
@ -17,7 +17,7 @@ class DatabaseConfig {
|
|||||||
static const int maxConnections = 10;
|
static const int maxConnections = 10;
|
||||||
static const int minConnections = 2;
|
static const int minConnections = 2;
|
||||||
|
|
||||||
static bool get isDevelopment => false;
|
static bool get isDevelopment => true;
|
||||||
|
|
||||||
static Map<String, dynamic> getConfig() {
|
static Map<String, dynamic> getConfig() {
|
||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
|
|||||||
@ -9,7 +9,9 @@
|
|||||||
#include <charset_converter/charset_converter_plugin.h>
|
#include <charset_converter/charset_converter_plugin.h>
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <open_file_linux/open_file_linux_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 <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) charset_converter_registrar =
|
g_autoptr(FlPluginRegistrar) charset_converter_registrar =
|
||||||
@ -21,7 +23,13 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
|
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
|
||||||
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
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
|
charset_converter
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
open_file_linux
|
open_file_linux
|
||||||
|
screen_retriever
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@ -10,8 +10,10 @@ import file_selector_macos
|
|||||||
import mobile_scanner
|
import mobile_scanner
|
||||||
import open_file_mac
|
import open_file_mac
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import screen_retriever
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
@ -19,6 +21,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
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:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1205,6 +1213,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.12.0"
|
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:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -67,6 +67,7 @@ dependencies:
|
|||||||
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
|
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
|
||||||
numbers_to_letters: ^1.0.0
|
numbers_to_letters: ^1.0.0
|
||||||
qr_code_scanner_plus: ^2.0.10+1
|
qr_code_scanner_plus: ^2.0.10+1
|
||||||
|
window_manager: ^0.3.7
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -109,6 +110,7 @@ flutter:
|
|||||||
- assets/mvola.jpg
|
- assets/mvola.jpg
|
||||||
- assets/Orange_money.png
|
- assets/Orange_money.png
|
||||||
- assets/fa-solid-900.ttf
|
- assets/fa-solid-900.ttf
|
||||||
|
- assets/NotoEmoji-Regular.ttf
|
||||||
- assets/fonts/Roboto-Italic.ttf
|
- assets/fonts/Roboto-Italic.ttf
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# 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 <charset_converter/charset_converter_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.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 <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
CharsetConverterPluginRegisterWithRegistrar(
|
CharsetConverterPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("CharsetConverterPlugin"));
|
registry->GetRegistrarForPlugin("CharsetConverterPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
|
WindowManagerPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,9 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
charset_converter
|
charset_converter
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
screen_retriever
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user