lastlast update

This commit is contained in:
b.razafimandimbihery 2025-06-14 21:49:10 +03:00
parent 595b38e9fb
commit c0bbb0da2b
27 changed files with 5641 additions and 6321 deletions

Binary file not shown.

View File

@ -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),
],
),
),
),
);
}
}

View File

@ -1,3 +1,5 @@
// Remplacez complètement votre fichier CommandeDetails par celui-ci :
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
@ -7,9 +9,7 @@ class CommandeDetails extends StatelessWidget {
const CommandeDetails({required this.commande});
Widget _buildTableHeader(String text) {
Widget _buildTableHeader(String text, {bool isAmount = false}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
@ -18,23 +18,122 @@ class CommandeDetails extends StatelessWidget {
fontWeight: FontWeight.bold,
fontSize: 14,
),
textAlign: TextAlign.center,
textAlign: isAmount ? TextAlign.right : TextAlign.center,
),
);
}
Widget _buildTableCell(String text) {
Widget _buildTableCell(String text, {bool isAmount = false, Color? textColor}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
text,
style: const TextStyle(fontSize: 13),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: textColor,
),
textAlign: isAmount ? TextAlign.right : TextAlign.center,
),
);
}
@override
Widget _buildPriceColumn(DetailCommande detail) {
if (detail.aRemise) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${detail.prixUnitaire.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 11,
decoration: TextDecoration.lineThrough,
color: Colors.grey,
),
),
const SizedBox(height: 2),
Text(
'${(detail.prixFinal / detail.quantite).toStringAsFixed(2)} MGA',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
],
),
);
} else {
return _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA', isAmount: true);
}
}
Widget _buildRemiseColumn(DetailCommande detail) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: detail.aRemise
? Column(
children: [
Text(
detail.remiseDescription,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
textAlign: TextAlign.center,
),
Text(
'-${detail.montantRemise.toStringAsFixed(0)} MGA',
style: TextStyle(
fontSize: 10,
color: Colors.teal.shade700,
),
textAlign: TextAlign.center,
),
],
)
: const Text(
'-',
style: TextStyle(fontSize: 13, color: Colors.grey),
textAlign: TextAlign.center,
),
);
}
Widget _buildTotalColumn(DetailCommande detail) {
if (detail.aRemise && detail.sousTotal != detail.prixFinal) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${detail.sousTotal.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 11,
decoration: TextDecoration.lineThrough,
color: Colors.grey,
),
),
const SizedBox(height: 2),
Text(
'${detail.prixFinal.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
],
),
);
} else {
return _buildTableCell('${detail.prixFinal.toStringAsFixed(2)} MGA', isAmount: true);
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<DetailCommande>>(
future: AppDatabase.instance.getDetailsCommande(commande.id!),
@ -48,6 +147,19 @@ class CommandeDetails extends StatelessWidget {
}
final details = snapshot.data!;
// Calculer les totaux
double sousTotal = 0;
double totalRemises = 0;
double totalFinal = 0;
bool hasRemises = false;
for (final detail in details) {
sousTotal += detail.sousTotal;
totalRemises += detail.montantRemise;
totalFinal += detail.prixFinal;
if (detail.aRemise) hasRemises = true;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@ -55,16 +167,46 @@ class CommandeDetails extends StatelessWidget {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
color: hasRemises ? Colors.orange.shade50 : Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: hasRemises
? Border.all(color: Colors.orange.shade200)
: null,
),
child: const Text(
'Détails de la commande',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.black87,
),
child: Row(
children: [
Icon(
hasRemises ? Icons.discount : Icons.receipt_long,
color: hasRemises ? Colors.orange.shade700 : Colors.blue.shade700,
),
const SizedBox(width: 8),
Text(
hasRemises ? 'Détails de la commande (avec remises)' : 'Détails de la commande',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: hasRemises ? Colors.orange.shade800 : Colors.black87,
),
),
if (hasRemises) ...[
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Économies: ${totalRemises.toStringAsFixed(0)} MGA',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
),
],
],
),
),
const SizedBox(height: 12),
@ -82,26 +224,72 @@ class CommandeDetails extends StatelessWidget {
children: [
_buildTableHeader('Produit'),
_buildTableHeader('Qté'),
_buildTableHeader('Prix unit.'),
_buildTableHeader('Total'),
_buildTableHeader('Prix unit.', isAmount: true),
if (hasRemises) _buildTableHeader('Remise'),
_buildTableHeader('Total', isAmount: true),
],
),
...details.map((detail) => TableRow(
decoration: detail.aRemise
? BoxDecoration(
color: const Color.fromARGB(255, 243, 191, 114),
border: Border(
left: BorderSide(
color: Colors.orange.shade300,
width: 3,
),
),
)
: null,
children: [
_buildTableCell(
detail.estCadeau == true
? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)'
: detail.produitNom ?? 'Produit inconnu'
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
detail.produitNom ?? 'Produit inconnu',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
if (detail.aRemise) ...[
const SizedBox(height: 2),
Row(
children: [
Icon(
Icons.local_offer,
size: 12,
color: Colors.teal.shade700,
),
const SizedBox(width: 4),
Text(
'Avec remise',
style: TextStyle(
fontSize: 10,
color: Colors.teal.shade700,
fontStyle: FontStyle.italic,
),
),
],
),
],
],
),
),
_buildTableCell('${detail.quantite}'),
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(2)} MGA'),
_buildPriceColumn(detail),
if (hasRemises) _buildRemiseColumn(detail),
_buildTotalColumn(detail),
],
)),
],
),
),
const SizedBox(height: 12),
// Section des totaux
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@ -111,39 +299,63 @@ class CommandeDetails extends StatelessWidget {
),
child: Column(
children: [
if (commande.montantApresRemise != null) ...[
// Sous-total si il y a des remises
if (hasRemises) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Sous-total:',
style: TextStyle(fontSize: 14),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'${commande.montantTotal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontSize: 14),
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Remise:',
style: TextStyle(fontSize: 14),
),
Text(
'-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA',
'${sousTotal.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontSize: 14,
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
const Divider(),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.discount,
size: 16,
color: Colors.orange.shade700,
),
const SizedBox(width: 4),
Text(
'Remises totales:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.orange.shade700,
),
),
],
),
Text(
'-${totalRemises.toStringAsFixed(2)} MGA',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
],
),
const Divider(height: 16),
],
// Total final
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -155,7 +367,7 @@ class CommandeDetails extends StatelessWidget {
),
),
Text(
'${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(2)} MGA',
'${commande.montantTotal.toStringAsFixed(2)} MGA',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,

View File

@ -8,15 +8,13 @@ class CommandeActions extends StatelessWidget {
final Commande commande;
final Function(int, StatutCommande) onStatutChanged;
final Function(Commande) onPaymentSelected;
final Function(Commande) onDiscountSelected;
final Function(Commande) onGiftSelected;
const CommandeActions({
required this.commande,
required this.onStatutChanged,
required this.onPaymentSelected,
required this.onDiscountSelected,
required this.onGiftSelected,
});
@ -27,18 +25,7 @@ class CommandeActions extends StatelessWidget {
switch (commande.statut) {
case StatutCommande.enAttente:
buttons.addAll([
_buildActionButton(
label: 'Remise',
icon: Icons.percent,
color: Colors.orange,
onPressed: () => onDiscountSelected(commande),
),
_buildActionButton(
label: 'Cadeau',
icon: Icons.card_giftcard,
color: Colors.purple,
onPressed: () => onGiftSelected(commande),
),
_buildActionButton(
label: 'Confirmer',
icon: Icons.check_circle,

View 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");
}
}
}

View File

@ -6,7 +6,6 @@ import 'package:youmazgestion/Components/commandManagementComponents/PaymentMeth
import 'package:youmazgestion/Components/paymentType.dart';
import 'package:youmazgestion/Models/client.dart';
class PaymentMethodDialog extends StatefulWidget {
final Commande commande;
@ -21,7 +20,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
final _amountController = TextEditingController();
void _validatePayment() {
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
final montantFinal = widget.commande.montantTotal;
if (_selectedPayment == PaymentType.cash) {
final amountGiven = double.tryParse(_amountController.text) ?? 0;
@ -48,7 +47,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
@override
void initState() {
super.initState();
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
final montantFinal = widget.commande.montantTotal;
_amountController.text = montantFinal.toStringAsFixed(2);
}
@ -61,7 +60,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
@override
Widget build(BuildContext context) {
final amount = double.tryParse(_amountController.text) ?? 0;
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
final montantFinal = widget.commande.montantTotal;
final change = amount - montantFinal;
return AlertDialog(
@ -70,7 +69,7 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Affichage du montant à payer
// Affichage du montant à payer (simplifié)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@ -78,34 +77,12 @@ class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.commande.montantApresRemise != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant original:'),
Text('${widget.commande.montantTotal.toStringAsFixed(2)} MGA'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Remise:'),
Text('-${(widget.commande.montantTotal - widget.commande.montantApresRemise!).toStringAsFixed(2)} MGA',
style: const TextStyle(color: Colors.red)),
],
),
const Divider(),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('${montantFinal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('${montantFinal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
),

View 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,
),
],
);
}
}

View 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

View File

@ -92,9 +92,6 @@ class Commande {
final String? clientNom;
final String? clientPrenom;
final String? clientEmail;
final double? remisePourcentage;
final double? remiseMontant;
final double? montantApresRemise;
Commande({
this.id,
@ -109,9 +106,6 @@ class Commande {
this.clientNom,
this.clientPrenom,
this.clientEmail,
this.remisePourcentage,
this.remiseMontant,
this.montantApresRemise,
});
String get clientNomComplet {
@ -143,9 +137,6 @@ class Commande {
'dateLivraison': dateLivraison?.toIso8601String(),
'commandeurId': commandeurId,
'validateurId': validateurId,
'remisePourcentage': remisePourcentage,
'remiseMontant': remiseMontant,
'montantApresRemise': montantApresRemise,
};
}
@ -165,56 +156,15 @@ class Commande {
clientNom: map['clientNom'] as String?,
clientPrenom: map['clientPrenom'] as String?,
clientEmail: map['clientEmail'] as String?,
remisePourcentage: map['remisePourcentage'] != null
? (map['remisePourcentage'] as num).toDouble()
: null,
remiseMontant: map['remiseMontant'] != null
? (map['remiseMontant'] as num).toDouble()
: null,
montantApresRemise: map['montantApresRemise'] != null
? (map['montantApresRemise'] as num).toDouble()
: null,
);
}
Commande copyWith({
int? id,
int? clientId,
DateTime? dateCommande,
StatutCommande? statut,
double? montantTotal,
String? notes,
DateTime? dateLivraison,
int? commandeurId,
int? validateurId,
String? clientNom,
String? clientPrenom,
String? clientEmail,
double? remisePourcentage,
double? remiseMontant,
double? montantApresRemise,
}) {
return Commande(
id: id ?? this.id,
clientId: clientId ?? this.clientId,
dateCommande: dateCommande ?? this.dateCommande,
statut: statut ?? this.statut,
montantTotal: montantTotal ?? this.montantTotal,
notes: notes ?? this.notes,
dateLivraison: dateLivraison ?? this.dateLivraison,
commandeurId: commandeurId ?? this.commandeurId,
validateurId: validateurId ?? this.validateurId,
clientNom: clientNom ?? this.clientNom,
clientPrenom: clientPrenom ?? this.clientPrenom,
clientEmail: clientEmail ?? this.clientEmail,
remisePourcentage: remisePourcentage ?? this.remisePourcentage,
remiseMontant: remiseMontant ?? this.remiseMontant,
montantApresRemise: montantApresRemise ?? this.montantApresRemise,
);
}
}
// REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci :
enum RemiseType {
pourcentage,
montant
}
class DetailCommande {
final int? id;
@ -222,16 +172,15 @@ class DetailCommande {
final int produitId;
final int quantite;
final double prixUnitaire;
final double sousTotal;
final double sousTotal; // Prix unitaire × quantité (avant remise)
final RemiseType? remiseType;
final double remiseValeur; // Valeur de la remise (% ou montant)
final double montantRemise; // Montant de la remise calculé
final double prixFinal; // Prix final après remise
final bool estCadeau; // NOUVEAU : Indique si l'article est un cadeau
final String? produitNom;
final String? produitImage;
final String? produitReference;
final bool? estCadeau;
// NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT
final double? remisePourcentage;
final double? remiseMontant;
final double? prixApresRemise;
DetailCommande({
this.id,
@ -240,15 +189,195 @@ class DetailCommande {
required this.quantite,
required this.prixUnitaire,
required this.sousTotal,
this.remiseType,
this.remiseValeur = 0.0,
this.montantRemise = 0.0,
required this.prixFinal,
this.estCadeau = false,
this.produitNom,
this.produitImage,
this.produitReference,
this.estCadeau,
this.remisePourcentage,
this.remiseMontant,
this.prixApresRemise,
});
// Constructeur pour créer un détail sans remise
factory DetailCommande.sansRemise({
int? id,
required int commandeId,
required int produitId,
required int quantite,
required double prixUnitaire,
bool estCadeau = false,
String? produitNom,
String? produitImage,
String? produitReference,
}) {
final sousTotal = quantite * prixUnitaire;
final prixFinal = estCadeau ? 0.0 : sousTotal;
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
prixFinal: prixFinal,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Constructeur pour créer un cadeau
factory DetailCommande.cadeau({
int? id,
required int commandeId,
required int produitId,
required int quantite,
required double prixUnitaire,
String? produitNom,
String? produitImage,
String? produitReference,
}) {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: quantite * prixUnitaire,
prixFinal: 0.0, // Prix final à 0 pour un cadeau
estCadeau: true,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Méthode pour appliquer une remise (ne s'applique pas aux cadeaux)
DetailCommande appliquerRemise({
required RemiseType type,
required double valeur,
}) {
// Les remises ne s'appliquent pas aux cadeaux
if (estCadeau) return this;
double montantRemiseCalcule = 0.0;
if (type == RemiseType.pourcentage) {
final pourcentage = valeur.clamp(0.0, 100.0);
montantRemiseCalcule = sousTotal * (pourcentage / 100);
} else {
montantRemiseCalcule = valeur.clamp(0.0, sousTotal);
}
final prixFinalCalcule = sousTotal - montantRemiseCalcule;
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: type,
remiseValeur: valeur,
montantRemise: montantRemiseCalcule,
prixFinal: prixFinalCalcule,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Méthode pour supprimer la remise
DetailCommande supprimerRemise() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: null,
remiseValeur: 0.0,
montantRemise: 0.0,
prixFinal: estCadeau ? 0.0 : sousTotal,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Méthode pour convertir en cadeau
DetailCommande convertirEnCadeau() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: null, // Supprimer les remises lors de la conversion en cadeau
remiseValeur: 0.0,
montantRemise: 0.0,
prixFinal: 0.0,
estCadeau: true,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Méthode pour convertir en article normal
DetailCommande convertirEnArticleNormal() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: remiseType,
remiseValeur: remiseValeur,
montantRemise: montantRemise,
prixFinal: estCadeau ? sousTotal - montantRemise : prixFinal,
estCadeau: false,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Getters utiles
bool get aRemise => remiseType != null && montantRemise > 0 && !estCadeau;
double get pourcentageRemise {
if (!aRemise) return 0.0;
return (montantRemise / sousTotal) * 100;
}
String get remiseDescription {
if (estCadeau) return 'CADEAU';
if (!aRemise) return '';
if (remiseType == RemiseType.pourcentage) {
return '-${remiseValeur.toStringAsFixed(0)}%';
} else {
return '-${montantRemise.toStringAsFixed(2)} MGA';
}
}
// NOUVEAU : Description du statut de l'article
String get statutDescription {
if (estCadeau) return 'CADEAU OFFERT';
if (aRemise) return 'AVEC REMISE';
return 'PRIX NORMAL';
}
Map<String, dynamic> toMap() {
return {
'id': id,
@ -257,14 +386,24 @@ class DetailCommande {
'quantite': quantite,
'prixUnitaire': prixUnitaire,
'sousTotal': sousTotal,
'estCadeau': estCadeau == true ? 1 : 0,
'remisePourcentage': remisePourcentage,
'remiseMontant': remiseMontant,
'prixApresRemise': prixApresRemise,
'remise_type': remiseType?.name,
'remise_valeur': remiseValeur,
'montant_remise': montantRemise,
'prix_final': prixFinal,
'est_cadeau': estCadeau ? 1 : 0,
};
}
factory DetailCommande.fromMap(Map<String, dynamic> map) {
RemiseType? type;
if (map['remise_type'] != null) {
if (map['remise_type'] == 'pourcentage') {
type = RemiseType.pourcentage;
} else if (map['remise_type'] == 'montant') {
type = RemiseType.montant;
}
}
return DetailCommande(
id: map['id'] as int?,
commandeId: map['commandeId'] as int,
@ -272,71 +411,15 @@ class DetailCommande {
quantite: map['quantite'] as int,
prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
sousTotal: (map['sousTotal'] as num).toDouble(),
remiseType: type,
remiseValeur: (map['remise_valeur'] as num?)?.toDouble() ?? 0.0,
montantRemise: (map['montant_remise'] as num?)?.toDouble() ?? 0.0,
prixFinal: (map['prix_final'] as num?)?.toDouble() ??
(map['sousTotal'] as num).toDouble(),
estCadeau: (map['est_cadeau'] as int?) == 1,
produitNom: map['produitNom'] as String?,
produitImage: map['produitImage'] as String?,
produitReference: map['produitReference'] as String?,
estCadeau: map['estCadeau'] == 1,
remisePourcentage: map['remisePourcentage'] != null
? (map['remisePourcentage'] as num).toDouble()
: null,
remiseMontant: map['remiseMontant'] != null
? (map['remiseMontant'] as num).toDouble()
: null,
prixApresRemise: map['prixApresRemise'] != null
? (map['prixApresRemise'] as num).toDouble()
: null,
);
}
DetailCommande copyWith({
int? id,
int? commandeId,
int? produitId,
int? quantite,
double? prixUnitaire,
double? sousTotal,
String? produitNom,
String? produitImage,
String? produitReference,
bool? estCadeau,
double? remisePourcentage,
double? remiseMontant,
double? prixApresRemise,
}) {
return DetailCommande(
id: id ?? this.id,
commandeId: commandeId ?? this.commandeId,
produitId: produitId ?? this.produitId,
quantite: quantite ?? this.quantite,
prixUnitaire: prixUnitaire ?? this.prixUnitaire,
sousTotal: sousTotal ?? this.sousTotal,
produitNom: produitNom ?? this.produitNom,
produitImage: produitImage ?? this.produitImage,
produitReference: produitReference ?? this.produitReference,
estCadeau: estCadeau ?? this.estCadeau,
remisePourcentage: remisePourcentage ?? this.remisePourcentage,
remiseMontant: remiseMontant ?? this.remiseMontant,
prixApresRemise: prixApresRemise ?? this.prixApresRemise,
);
}
// GETTERS QUI RÉSOLVENT LE PROBLÈME "aUneRemise" INTROUVABLE
double get prixFinalUnitaire {
return prixApresRemise ?? prixUnitaire;
}
double get sousTotalAvecRemise {
return quantite * prixFinalUnitaire;
}
bool get aUneRemise {
return remisePourcentage != null || remiseMontant != null || prixApresRemise != null;
}
double get montantRemise {
if (prixApresRemise != null) {
return (prixUnitaire - prixApresRemise!) * quantite;
}
return 0.0;
}
}

304
lib/Services/Script.sql Normal file
View 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;

View File

@ -37,8 +37,6 @@ class AppDatabase {
_connection = await _initDB();
// await _createDB();
// Effectuer la migration pour les bases existantes
await migrateDatabaseForDiscountAndGift();
await insertDefaultPermissions();
await insertDefaultMenus();
@ -68,169 +66,7 @@ class AppDatabase {
}
}
// Méthode mise à jour pour créer les tables avec les nouvelles colonnes
Future<void> _createDB() async {
// final db = await database;
// try {
// // Table roles
// await db.query('''
// CREATE TABLE IF NOT EXISTS roles (
// id INT AUTO_INCREMENT PRIMARY KEY,
// designation VARCHAR(255) NOT NULL UNIQUE
// ) ENGINE=InnoDB
// ''');
// // Table permissions
// await db.query('''
// CREATE TABLE IF NOT EXISTS permissions (
// id INT AUTO_INCREMENT PRIMARY KEY,
// name VARCHAR(255) NOT NULL UNIQUE
// ) ENGINE=InnoDB
// ''');
// // Table menu
// await db.query('''
// CREATE TABLE IF NOT EXISTS menu (
// id INT AUTO_INCREMENT PRIMARY KEY,
// name VARCHAR(255) NOT NULL,
// route VARCHAR(255) NOT NULL
// ) ENGINE=InnoDB
// ''');
// // Table role_permissions
// await db.query('''
// CREATE TABLE IF NOT EXISTS role_permissions (
// role_id INT,
// permission_id INT,
// PRIMARY KEY (role_id, permission_id),
// FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
// FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
// ) ENGINE=InnoDB
// ''');
// // Table role_menu_permissions
// await db.query('''
// CREATE TABLE IF NOT EXISTS role_menu_permissions (
// role_id INT,
// menu_id INT,
// permission_id INT,
// PRIMARY KEY (role_id, menu_id, permission_id),
// FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
// FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
// FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
// ) ENGINE=InnoDB
// ''');
// // Table points_de_vente
// await db.query('''
// CREATE TABLE IF NOT EXISTS points_de_vente (
// id INT AUTO_INCREMENT PRIMARY KEY,
// nom VARCHAR(255) NOT NULL UNIQUE
// ) ENGINE=InnoDB
// ''');
// // Table users
// await db.query('''
// CREATE TABLE IF NOT EXISTS users (
// id INT AUTO_INCREMENT PRIMARY KEY,
// name VARCHAR(255) NOT NULL,
// lastname VARCHAR(255) NOT NULL,
// email VARCHAR(255) NOT NULL UNIQUE,
// password VARCHAR(255) NOT NULL,
// username VARCHAR(255) NOT NULL UNIQUE,
// role_id INT NOT NULL,
// point_de_vente_id INT,
// FOREIGN KEY (role_id) REFERENCES roles(id),
// FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
// ) ENGINE=InnoDB
// ''');
// // Table products
// await db.query('''
// CREATE TABLE IF NOT EXISTS products (
// id INT AUTO_INCREMENT PRIMARY KEY,
// name VARCHAR(255) NOT NULL,
// price DECIMAL(10,2) NOT NULL,
// image VARCHAR(2000),
// category VARCHAR(255) NOT NULL,
// stock INT NOT NULL DEFAULT 0,
// description VARCHAR(1000),
// qrCode VARCHAR(500),
// reference VARCHAR(255),
// point_de_vente_id INT,
// marque VARCHAR(255),
// ram VARCHAR(100),
// memoire_interne VARCHAR(100),
// imei VARCHAR(255) UNIQUE,
// FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id),
// INDEX idx_products_category (category),
// INDEX idx_products_reference (reference),
// INDEX idx_products_imei (imei)
// ) ENGINE=InnoDB
// ''');
// // Table clients
// await db.query('''
// CREATE TABLE IF NOT EXISTS clients (
// id INT AUTO_INCREMENT PRIMARY KEY,
// nom VARCHAR(255) NOT NULL,
// prenom VARCHAR(255) NOT NULL,
// email VARCHAR(255) NOT NULL UNIQUE,
// telephone VARCHAR(255) NOT NULL,
// adresse VARCHAR(500),
// dateCreation DATETIME NOT NULL,
// actif TINYINT(1) NOT NULL DEFAULT 1,
// INDEX idx_clients_email (email),
// INDEX idx_clients_telephone (telephone)
// ) ENGINE=InnoDB
// ''');
// // Table commandes MISE À JOUR avec les champs de remise
// await db.query('''
// CREATE TABLE IF NOT EXISTS commandes (
// id INT AUTO_INCREMENT PRIMARY KEY,
// clientId INT NOT NULL,
// dateCommande DATETIME NOT NULL,
// statut INT NOT NULL DEFAULT 0,
// montantTotal DECIMAL(10,2) NOT NULL,
// notes VARCHAR(1000),
// dateLivraison DATETIME,
// commandeurId INT,
// validateurId INT,
// remisePourcentage DECIMAL(5,2) NULL,
// remiseMontant DECIMAL(10,2) NULL,
// montantApresRemise DECIMAL(10,2) NULL,
// FOREIGN KEY (commandeurId) REFERENCES users(id),
// FOREIGN KEY (validateurId) REFERENCES users(id),
// FOREIGN KEY (clientId) REFERENCES clients(id),
// INDEX idx_commandes_client (clientId),
// INDEX idx_commandes_date (dateCommande)
// ) ENGINE=InnoDB
// ''');
// // Table details_commandes MISE À JOUR avec le champ cadeau
// await db.query('''
// CREATE TABLE IF NOT EXISTS details_commandes (
// id INT AUTO_INCREMENT PRIMARY KEY,
// commandeId INT NOT NULL,
// produitId INT NOT NULL,
// quantite INT NOT NULL,
// prixUnitaire DECIMAL(10,2) NOT NULL,
// sousTotal DECIMAL(10,2) NOT NULL,
// estCadeau TINYINT(1) DEFAULT 0,
// FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
// FOREIGN KEY (produitId) REFERENCES products(id),
// INDEX idx_details_commande (commandeId)
// ) ENGINE=InnoDB
// ''');
// print("Tables créées avec succès avec les nouveaux champs !");
// } catch (e) {
// print("Erreur lors de la création des tables: $e");
// rethrow;
// }
}
// --- MÉTHODES D'INSERTION PAR DÉFAUT ---
@ -933,31 +769,37 @@ Future<void> _createDB() async {
// --- DÉTAILS COMMANDES ---
Future<int> createDetailCommande(DetailCommande detail) async {
final db = await database;
final detailMap = detail.toMap();
detailMap.remove('id');
final fields = detailMap.keys.join(', ');
final placeholders = List.filled(detailMap.length, '?').join(', ');
final result = await db.query(
'INSERT INTO details_commandes ($fields) VALUES ($placeholders)',
detailMap.values.toList()
);
return result.insertId!;
}
final db = await database;
final detailMap = detail.toMap();
detailMap.remove('id');
final fields = detailMap.keys.join(', ');
final placeholders = List.filled(detailMap.length, '?').join(', ');
final result = await db.query(
'INSERT INTO details_commandes ($fields) VALUES ($placeholders)',
detailMap.values.toList()
);
return result.insertId!;
}
Future<List<DetailCommande>> getDetailsCommande(int commandeId) async {
final db = await database;
final result = await db.query('''
SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference
FROM details_commandes dc
LEFT JOIN products p ON dc.produitId = p.id
WHERE dc.commandeId = ?
ORDER BY dc.id
''', [commandeId]);
return result.map((row) => DetailCommande.fromMap(row.fields)).toList();
}
// Méthode mise à jour pour récupérer les détails avec les remises
Future<List<DetailCommande>> getDetailsCommande(int commandeId) async {
final db = await database;
final result = await db.query('''
SELECT
dc.*,
p.name as produitNom,
p.image as produitImage,
p.reference as produitReference
FROM details_commandes dc
LEFT JOIN products p ON dc.produitId = p.id
WHERE dc.commandeId = ?
ORDER BY dc.est_cadeau ASC, dc.id
''', [commandeId]);
return result.map((row) => DetailCommande.fromMap(row.fields)).toList();
}
// --- RECHERCHE PRODUITS ---
@ -1364,17 +1206,19 @@ Future<void> _createDB() async {
// --- TRANSACTIONS COMPLEXES ---
Future<int> createCommandeComplete(Client client, Commande commande, List<DetailCommande> details) async {
// Méthode pour créer une commande complète avec remises
Future<int> createCommandeComplete(Client client, Commande commande, List<DetailCommande> details) async {
final db = await database;
try {
await db.query('START TRANSACTION');
// 1. Utiliser createOrGetClient au lieu de créer directement
// 1. Créer ou récupérer le client
final existingOrNewClient = await createOrGetClient(client);
final clientId = existingOrNewClient.id!;
// 2. Créer la commande avec le bon clientId
// 2. Créer la commande
final commandeMap = commande.toMap();
commandeMap.remove('id');
commandeMap['clientId'] = clientId;
@ -1388,7 +1232,7 @@ Future<void> _createDB() async {
);
final commandeId = commandeResult.insertId!;
// 3. Créer les détails de commande
// 3. Créer les détails de commande avec remises
for (final detail in details) {
final detailMap = detail.toMap();
detailMap.remove('id');
@ -1418,6 +1262,111 @@ Future<void> _createDB() async {
}
}
// Méthode pour mettre à jour un détail de commande (utile pour modifier les remises)
Future<int> updateDetailCommande(DetailCommande detail) async {
final db = await database;
final detailMap = detail.toMap();
final id = detailMap.remove('id');
final setClause = detailMap.keys.map((key) => '$key = ?').join(', ');
final values = [...detailMap.values, id];
final result = await db.query(
'UPDATE details_commandes SET $setClause WHERE id = ?',
values
);
return result.affectedRows!;
}
// Méthode pour obtenir les statistiques des remises
Future<Map<String, dynamic>> getRemiseStatistics() async {
final db = await database;
try {
// Total des remises accordées
final totalRemisesResult = await db.query('''
SELECT
COUNT(*) as nombre_remises,
SUM(montant_remise) as total_remises,
AVG(montant_remise) as moyenne_remise
FROM details_commandes
WHERE remise_type IS NOT NULL AND montant_remise > 0
''');
// Remises par type
final remisesParTypeResult = await db.query('''
SELECT
remise_type,
COUNT(*) as nombre,
SUM(montant_remise) as total,
AVG(remise_valeur) as moyenne_valeur
FROM details_commandes
WHERE remise_type IS NOT NULL AND montant_remise > 0
GROUP BY remise_type
''');
// Produits avec le plus de remises
final produitsRemisesResult = await db.query('''
SELECT
p.name as produit_nom,
COUNT(*) as nombre_remises,
SUM(dc.montant_remise) as total_remises
FROM details_commandes dc
INNER JOIN products p ON dc.produitId = p.id
WHERE dc.remise_type IS NOT NULL AND dc.montant_remise > 0
GROUP BY dc.produitId, p.name
ORDER BY total_remises DESC
LIMIT 10
''');
return {
'total_remises': totalRemisesResult.first.fields,
'remises_par_type': remisesParTypeResult.map((row) => row.fields).toList(),
'produits_remises': produitsRemisesResult.map((row) => row.fields).toList(),
};
} catch (e) {
print("Erreur lors du calcul des statistiques de remises: $e");
return {
'total_remises': {'nombre_remises': 0, 'total_remises': 0.0, 'moyenne_remise': 0.0},
'remises_par_type': [],
'produits_remises': [],
};
}
}
// Méthode pour obtenir les commandes avec le plus de remises
Future<List<Map<String, dynamic>>> getCommandesAvecRemises({int limit = 20}) async {
final db = await database;
try {
final result = await db.query('''
SELECT
c.id as commande_id,
c.dateCommande,
c.montantTotal,
cl.nom as client_nom,
cl.prenom as client_prenom,
SUM(dc.montant_remise) as total_remises,
COUNT(CASE WHEN dc.remise_type IS NOT NULL THEN 1 END) as nombre_articles_remise,
COUNT(dc.id) as total_articles
FROM commandes c
INNER JOIN clients cl ON c.clientId = cl.id
INNER JOIN details_commandes dc ON c.id = dc.commandeId
GROUP BY c.id, c.dateCommande, c.montantTotal, cl.nom, cl.prenom
HAVING total_remises > 0
ORDER BY total_remises DESC
LIMIT ?
''', [limit]);
return result.map((row) => row.fields).toList();
} catch (e) {
print("Erreur lors de la récupération des commandes avec remises: $e");
return [];
}
}
// --- STATISTIQUES AVANCÉES ---
Future<Map<String, int>> getProductCountByCategory() async {
@ -1799,7 +1748,6 @@ Future<Client?> findClientByAnyIdentifier({
String? nom,
String? prenom,
}) async {
final db = await database;
// Recherche par email si fourni
if (email != null && email.isNotEmpty) {
@ -1821,140 +1769,440 @@ Future<Client?> findClientByAnyIdentifier({
return null;
}
Future<void> migrateDatabaseForDiscountAndGift() async {
//
// Méthode pour obtenir les statistiques des cadeaux
Future<Map<String, dynamic>> getCadeauStatistics() async {
final db = await database;
try {
// Ajouter les colonnes de remise à la table commandes
await db.query('''
ALTER TABLE commandes
ADD COLUMN remisePourcentage DECIMAL(5,2) NULL
// Total des cadeaux offerts
final totalCadeauxResult = await db.query('''
SELECT
COUNT(*) as nombre_cadeaux,
SUM(sousTotal) as valeur_totale_cadeaux,
AVG(sousTotal) as valeur_moyenne_cadeau,
SUM(quantite) as quantite_totale_cadeaux
FROM details_commandes
WHERE est_cadeau = 1
''');
await db.query('''
ALTER TABLE commandes
ADD COLUMN remiseMontant DECIMAL(10,2) NULL
// Cadeaux par produit
final cadeauxParProduitResult = await db.query('''
SELECT
p.name as produit_nom,
p.category as produit_categorie,
COUNT(*) as nombre_fois_offert,
SUM(dc.quantite) as quantite_totale_offerte,
SUM(dc.sousTotal) as valeur_totale_offerte
FROM details_commandes dc
INNER JOIN products p ON dc.produitId = p.id
WHERE dc.est_cadeau = 1
GROUP BY dc.produitId, p.name, p.category
ORDER BY quantite_totale_offerte DESC
LIMIT 10
''');
await db.query('''
ALTER TABLE commandes
ADD COLUMN montantApresRemise DECIMAL(10,2) NULL
// Commandes avec cadeaux
final commandesAvecCadeauxResult = await db.query('''
SELECT
COUNT(DISTINCT c.id) as nombre_commandes_avec_cadeaux,
AVG(cadeau_stats.nombre_cadeaux_par_commande) as moyenne_cadeaux_par_commande,
AVG(cadeau_stats.valeur_cadeaux_par_commande) as valeur_moyenne_cadeaux_par_commande
FROM commandes c
INNER JOIN (
SELECT
commandeId,
COUNT(*) as nombre_cadeaux_par_commande,
SUM(sousTotal) as valeur_cadeaux_par_commande
FROM details_commandes
WHERE est_cadeau = 1
GROUP BY commandeId
) cadeau_stats ON c.id = cadeau_stats.commandeId
''');
// Ajouter la colonne cadeau à la table details_commandes
await db.query('''
ALTER TABLE details_commandes
ADD COLUMN estCadeau TINYINT(1) DEFAULT 0
// Évolution des cadeaux par mois
final evolutionMensuelleResult = await db.query('''
SELECT
DATE_FORMAT(c.dateCommande, '%Y-%m') as mois,
COUNT(dc.id) as nombre_cadeaux,
SUM(dc.sousTotal) as valeur_cadeaux
FROM details_commandes dc
INNER JOIN commandes c ON dc.commandeId = c.id
WHERE dc.est_cadeau = 1
AND c.dateCommande >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(c.dateCommande, '%Y-%m')
ORDER BY mois DESC
LIMIT 12
''');
print("Migration pour remise et cadeau terminée avec succès");
return {
'total_cadeaux': totalCadeauxResult.first.fields,
'cadeaux_par_produit': cadeauxParProduitResult.map((row) => row.fields).toList(),
'commandes_avec_cadeaux': commandesAvecCadeauxResult.first.fields,
'evolution_mensuelle': evolutionMensuelleResult.map((row) => row.fields).toList(),
};
} catch (e) {
// Les colonnes existent probablement déjà
print("Migration déjà effectuée ou erreur: $e");
print("Erreur lors du calcul des statistiques de cadeaux: $e");
return {
'total_cadeaux': {'nombre_cadeaux': 0, 'valeur_totale_cadeaux': 0.0, 'valeur_moyenne_cadeau': 0.0, 'quantite_totale_cadeaux': 0},
'cadeaux_par_produit': [],
'commandes_avec_cadeaux': {'nombre_commandes_avec_cadeaux': 0, 'moyenne_cadeaux_par_commande': 0.0, 'valeur_moyenne_cadeaux_par_commande': 0.0},
'evolution_mensuelle': [],
};
}
}
Future<List<DetailCommande>> getDetailsCommandeAvecCadeaux(int commandeId) async {
// Méthode pour obtenir les commandes avec des cadeaux
Future<List<Map<String, dynamic>>> getCommandesAvecCadeaux({int limit = 20}) async {
final db = await database;
final result = await db.query('''
SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference
FROM details_commandes dc
LEFT JOIN products p ON dc.produitId = p.id
WHERE dc.commandeId = ?
ORDER BY dc.estCadeau ASC, dc.id
''', [commandeId]);
return result.map((row) => DetailCommande.fromMap(row.fields)).toList();
try {
final result = await db.query('''
SELECT
c.id as commande_id,
c.dateCommande,
c.montantTotal,
cl.nom as client_nom,
cl.prenom as client_prenom,
cadeau_stats.nombre_cadeaux,
cadeau_stats.valeur_cadeaux,
cadeau_stats.quantite_cadeaux,
(SELECT COUNT(*) FROM details_commandes WHERE commandeId = c.id) as total_articles
FROM commandes c
INNER JOIN clients cl ON c.clientId = cl.id
INNER JOIN (
SELECT
commandeId,
COUNT(*) as nombre_cadeaux,
SUM(sousTotal) as valeur_cadeaux,
SUM(quantite) as quantite_cadeaux
FROM details_commandes
WHERE est_cadeau = 1
GROUP BY commandeId
) cadeau_stats ON c.id = cadeau_stats.commandeId
ORDER BY cadeau_stats.valeur_cadeaux DESC
LIMIT ?
''', [limit]);
return result.map((row) => row.fields).toList();
} catch (e) {
print("Erreur lors de la récupération des commandes avec cadeaux: $e");
return [];
}
}
// 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;
try {
final result = await db.query('''
SELECT
p.id,
p.name as produit_nom,
p.price as prix_unitaire,
p.category as categorie,
p.stock,
COUNT(dc.id) as nombre_fois_offert,
SUM(dc.quantite) as quantite_totale_offerte,
SUM(dc.sousTotal) as valeur_totale_offerte,
COUNT(DISTINCT dc.commandeId) as nombre_commandes_distinctes
FROM products p
INNER JOIN details_commandes dc ON p.id = dc.produitId
WHERE dc.est_cadeau = 1
GROUP BY p.id, p.name, p.price, p.category, p.stock
ORDER BY quantite_totale_offerte DESC
LIMIT ?
''', [limit]);
return result.map((row) => row.fields).toList();
} catch (e) {
print("Erreur lors de la récupération des produits les plus offerts: $e");
return [];
}
}
// Méthode pour obtenir les clients qui ont reçu le plus de cadeaux
Future<List<Map<String, dynamic>>> getClientsAvecLePlusDeCadeaux({int limit = 10}) async {
final db = await database;
try {
final result = await db.query('''
SELECT
cl.id as client_id,
cl.nom,
cl.prenom,
cl.email,
cl.telephone,
COUNT(dc.id) as nombre_cadeaux_recus,
SUM(dc.quantite) as quantite_cadeaux_recus,
SUM(dc.sousTotal) as valeur_cadeaux_recus,
COUNT(DISTINCT c.id) as nombre_commandes_avec_cadeaux
FROM clients cl
INNER JOIN commandes c ON cl.id = c.clientId
INNER JOIN details_commandes dc ON c.id = dc.commandeId
WHERE dc.est_cadeau = 1
GROUP BY cl.id, cl.nom, cl.prenom, cl.email, cl.telephone
ORDER BY valeur_cadeaux_recus DESC
LIMIT ?
''', [limit]);
return result.map((row) => row.fields).toList();
} catch (e) {
print("Erreur lors de la récupération des clients avec le plus de cadeaux: $e");
return [];
}
}
// Méthode pour calculer l'impact des cadeaux sur les ventes
Future<Map<String, dynamic>> getImpactCadeauxSurVentes() async {
final db = await database;
try {
// Comparaison des commandes avec et sans cadeaux
final comparisonResult = await db.query('''
SELECT
'avec_cadeaux' as type_commande,
COUNT(DISTINCT c.id) as nombre_commandes,
AVG(c.montantTotal) as panier_moyen,
SUM(c.montantTotal) as chiffre_affaires_total
FROM commandes c
WHERE EXISTS (
SELECT 1 FROM details_commandes dc
WHERE dc.commandeId = c.id AND dc.est_cadeau = 1
)
UNION ALL
SELECT
'sans_cadeaux' as type_commande,
COUNT(DISTINCT c.id) as nombre_commandes,
AVG(c.montantTotal) as panier_moyen,
SUM(c.montantTotal) as chiffre_affaires_total
FROM commandes c
WHERE NOT EXISTS (
SELECT 1 FROM details_commandes dc
WHERE dc.commandeId = c.id AND dc.est_cadeau = 1
)
''');
// Ratio de conversion (commandes avec cadeaux / total commandes)
final ratioResult = await db.query('''
SELECT
(SELECT COUNT(DISTINCT c.id)
FROM commandes c
WHERE EXISTS (
SELECT 1 FROM details_commandes dc
WHERE dc.commandeId = c.id AND dc.est_cadeau = 1
)
) * 100.0 / COUNT(*) as pourcentage_commandes_avec_cadeaux
FROM commandes
''');
return {
'comparaison': comparisonResult.map((row) => row.fields).toList(),
'pourcentage_commandes_avec_cadeaux': ratioResult.first['pourcentage_commandes_avec_cadeaux'] ?? 0.0,
};
} catch (e) {
print("Erreur lors du calcul de l'impact des cadeaux: $e");
return {
'comparaison': [],
'pourcentage_commandes_avec_cadeaux': 0.0,
};
}
}
Future<int> updateCommandeAvecRemise(int commandeId, {
double? remisePourcentage,
double? remiseMontant,
double? montantApresRemise,
}) async {
// 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;
List<String> setClauses = [];
List<dynamic> values = [];
try {
await db.query('START TRANSACTION');
// 1. Créer ou récupérer le client
final existingOrNewClient = await createOrGetClient(client);
final clientId = existingOrNewClient.id!;
// 2. Créer la commande
final commandeMap = commande.toMap();
commandeMap.remove('id');
commandeMap['clientId'] = clientId;
final commandeFields = commandeMap.keys.join(', ');
final commandePlaceholders = List.filled(commandeMap.length, '?').join(', ');
final commandeResult = await db.query(
'INSERT INTO commandes ($commandeFields) VALUES ($commandePlaceholders)',
commandeMap.values.toList()
);
final commandeId = commandeResult.insertId!;
// 3. Créer les détails de commande avec remises et cadeaux
for (final detail in details) {
final detailMap = detail.toMap();
detailMap.remove('id');
detailMap['commandeId'] = commandeId;
final detailFields = detailMap.keys.join(', ');
final detailPlaceholders = List.filled(detailMap.length, '?').join(', ');
await db.query(
'INSERT INTO details_commandes ($detailFields) VALUES ($detailPlaceholders)',
detailMap.values.toList()
);
// 4. Mettre à jour le stock (même pour les cadeaux)
await db.query(
'UPDATE products SET stock = stock - ? WHERE id = ?',
[detail.quantite, detail.produitId]
);
}
await db.query('COMMIT');
// Log des cadeaux offerts (optionnel)
final cadeaux = details.where((d) => d.estCadeau).toList();
if (cadeaux.isNotEmpty) {
print("Cadeaux offerts dans la commande $commandeId:");
for (final cadeau in cadeaux) {
print(" - ${cadeau.produitNom} x${cadeau.quantite} (valeur: ${cadeau.sousTotal.toStringAsFixed(2)} MGA)");
}
}
return commandeId;
} catch (e) {
await db.query('ROLLBACK');
print("Erreur lors de la création de la commande complète avec cadeaux: $e");
rethrow;
}
}
// Méthode pour valider la disponibilité des cadeaux avant la commande
Future<List<String>> verifierDisponibiliteCadeaux(List<DetailCommande> details) async {
final db = await database;
List<String> erreurs = [];
if (remisePourcentage != null) {
setClauses.add('remisePourcentage = ?');
values.add(remisePourcentage);
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");
}
if (remiseMontant != null) {
setClauses.add('remiseMontant = ?');
values.add(remiseMontant);
return erreurs;
}
// --- MÉTHODES POUR LES VENTES PAR POINT DE VENTE ---
Future<List<Map<String, dynamic>>> getVentesParPointDeVente() async {
final db = await database;
try {
final result = await db.query('''
SELECT
pv.id as point_vente_id,
pv.nom as point_vente_nom,
COUNT(DISTINCT c.id) as nombre_commandes,
COUNT(dc.id) as nombre_articles_vendus,
SUM(dc.quantite) as quantite_totale_vendue,
SUM(c.montantTotal) as chiffre_affaires,
AVG(c.montantTotal) as panier_moyen,
MIN(c.dateCommande) as premiere_vente,
MAX(c.dateCommande) as derniere_vente
FROM points_de_vente pv
LEFT JOIN products p ON pv.id = p.point_de_vente_id
LEFT JOIN details_commandes dc ON p.id = dc.produitId
LEFT JOIN commandes c ON dc.commandeId = c.id
WHERE c.statut != 5 -- Exclure les commandes annulées
GROUP BY pv.id, pv.nom
ORDER BY chiffre_affaires DESC
''');
return result.map((row) => row.fields).toList();
} catch (e) {
print("Erreur getVentesParPointDeVente: $e");
return [];
}
}
Future<List<Map<String, dynamic>>> getTopProduitsParPointDeVente(int pointDeVenteId, {int limit = 5}) async {
final db = await database;
if (montantApresRemise != null) {
setClauses.add('montantApresRemise = ?');
values.add(montantApresRemise);
try {
final result = await db.query('''
SELECT
p.id,
p.name as produit_nom,
p.price as prix_unitaire,
p.category as categorie,
SUM(dc.quantite) as quantite_vendue,
SUM(dc.sousTotal) as chiffre_affaires_produit,
COUNT(DISTINCT dc.commandeId) as nombre_commandes
FROM products p
INNER JOIN details_commandes dc ON p.id = dc.produitId
INNER JOIN commandes c ON dc.commandeId = c.id
WHERE p.point_de_vente_id = ? AND c.statut != 5
GROUP BY p.id, p.name, p.price, p.category
ORDER BY quantite_vendue DESC
LIMIT ?
''', [pointDeVenteId, limit]);
return result.map((row) => row.fields).toList();
} catch (e) {
print("Erreur getTopProduitsParPointDeVente: $e");
return [];
}
if (setClauses.isEmpty) return 0;
values.add(commandeId);
final result = await db.query(
'UPDATE commandes SET ${setClauses.join(', ')} WHERE id = ?',
values
);
return result.affectedRows!;
}
Future<int> createDetailCommandeCadeau(DetailCommande detail) async {
Future<List<Map<String, dynamic>>> getVentesParPointDeVenteParMois(int pointDeVenteId) async {
final db = await database;
final detailMap = detail.toMap();
detailMap.remove('id');
detailMap['estCadeau'] = 1; // Marquer comme cadeau
detailMap['prixUnitaire'] = 0.0; // Prix zéro pour les cadeaux
detailMap['sousTotal'] = 0.0; // Sous-total zéro pour les cadeaux
final fields = detailMap.keys.join(', ');
final placeholders = List.filled(detailMap.length, '?').join(', ');
final result = await db.query(
'INSERT INTO details_commandes ($fields) VALUES ($placeholders)',
detailMap.values.toList()
);
return result.insertId!;
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 [];
}
}
Future<List<DetailCommande>> getCadeauxCommande(int commandeId) async {
// Dans la classe AppDatabase, ajoutez cette méthode :
Future<bool> verifyCurrentUserPassword(String password) async {
final db = await database;
final result = await db.query('''
SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference
FROM details_commandes dc
LEFT JOIN products p ON dc.produitId = p.id
WHERE dc.commandeId = ? AND dc.estCadeau = 1
ORDER BY dc.id
''', [commandeId]);
return result.map((row) => DetailCommande.fromMap(row.fields)).toList();
}
Future<double> calculateMontantTotalSansCadeaux(int commandeId) async {
final db = await database;
final result = await db.query('''
SELECT SUM(sousTotal) as total
FROM details_commandes
WHERE commandeId = ? AND (estCadeau = 0 OR estCadeau IS NULL)
''', [commandeId]);
final userController = Get.find<UserController>();
final total = result.first['total'];
return total != null ? (total as num).toDouble() : 0.0;
}
Future<int> supprimerRemiseCommande(int commandeId) async {
final db = await database;
final result = await db.query('''
UPDATE commandes
SET remisePourcentage = NULL, remiseMontant = NULL, montantApresRemise = NULL
WHERE id = ?
''', [commandeId]);
return result.affectedRows!;
try {
final result = await db.query('''
SELECT 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;
}
}
}

View File

@ -61,14 +61,14 @@ void initState() {
}
void _loadData() {
_statsFuture = _database.getStatistiques();
_recentOrdersFuture = _database.getCommandes().then((orders) => orders.take(5).toList());
_lowStockProductsFuture = _database.getProducts().then((products) {
return products.where((p) => (p.stock ?? 0) < 10).toList();
});
_recentClientsFuture = _database.getClients().then((clients) => clients.take(5).toList());
_allOrdersFuture = _database.getCommandes();
_productsByCategoryFuture = _database.getProductCountByCategory();
_statsFuture = _database.getStatistiques();
_recentOrdersFuture = _database.getCommandes().then((orders) => orders.take(5).toList());
_lowStockProductsFuture = _database.getProducts().then((products) {
return products.where((p) => (p.stock ?? 0) < 10).toList();
});
_recentClientsFuture = _database.getClients().then((clients) => clients.take(5).toList());
_allOrdersFuture = _database.getCommandes();
_productsByCategoryFuture = _database.getProductCountByCategory();
}
Future<void> _showCategoryProductsDialog(String category) async {
final products = await _database.getProductsByCategory(category);
@ -185,7 +185,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
// Histogramme des catégories de produits
_buildCategoryHistogram(),
SizedBox(height: 20),
// NOUVEAU: Widget des ventes par point de vente
_buildVentesParPointDeVenteCard(),
SizedBox(height: 20),
// Section des données récentes
_buildRecentDataSection(),
],
@ -1087,6 +1089,411 @@ Future<void> _showCategoryProductsDialog(String category) async {
);
}
//widget vente
// 2. Ajoutez cette méthode dans la classe _DashboardPageState
Widget _buildVentesParPointDeVenteCard() {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.store, color: Colors.purple),
SizedBox(width: 8),
Text(
'Ventes par Point de Vente',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: 16),
Container(
height: 400,
child: FutureBuilder<List<Map<String, dynamic>>>(
future: _database.getVentesParPointDeVente(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.store_mall_directory_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('Aucune donnée de vente par point de vente', style: TextStyle(color: Colors.grey)),
],
),
);
}
final ventesData = snapshot.data!;
return SingleChildScrollView(
child: Column(
children: [
// Graphique en barres des chiffres d'affaires
Container(
height: 200,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: _getMaxChiffreAffaires(ventesData) * 1.2,
barTouchData: BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: Colors.blueGrey,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final pointVente = ventesData[groupIndex];
final ca = pointVente['chiffre_affaires'] ?? 0.0;
final nbCommandes = pointVente['nombre_commandes'] ?? 0;
return BarTooltipItem(
'${pointVente['point_vente_nom']}\n${ca.toStringAsFixed(2)} MGA\n$nbCommandes commandes',
TextStyle(color: Colors.white, fontSize: 12),
);
},
),
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < ventesData.length) {
final nom = ventesData[index]['point_vente_nom'] as String;
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
nom.length > 5 ? nom.substring(0, 5) : nom,
style: TextStyle(
fontSize: 10,
color: Colors.grey,
),
),
);
}
return Text('');
},
reservedSize: 40,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Text(
_formatCurrency(value),
style: TextStyle(
fontSize: 10,
color: Colors.grey,
),
);
},
reservedSize: 60,
),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(
color: Colors.grey.withOpacity(0.3),
width: 1,
),
),
barGroups: ventesData.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
final ca = (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: ca,
color: _getPointVenteColor(index),
width: 16,
borderRadius: BorderRadius.circular(4),
backDrawRodData: BackgroundBarChartRodData(
show: true,
toY: _getMaxChiffreAffaires(ventesData) * 1.2,
color: Colors.grey.withOpacity(0.1),
),
),
],
showingTooltipIndicators: [0],
);
}).toList(),
),
),
),
SizedBox(height: 20),
// Tableau détaillé
_buildTableauVentesPointDeVente(ventesData),
],
),
);
},
),
),
],
),
),
);
}
Widget _buildTableauVentesPointDeVente(List<Map<String, dynamic>> ventesData) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.withOpacity(0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
// En-tête du tableau
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
children: [
Expanded(flex: 2, child: Text('Point de Vente', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
Expanded(flex: 2, child: Text('CA (MGA)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
Expanded(flex: 1, child: Text('Cmd', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
Expanded(flex: 1, child: Text('Articles', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
Expanded(flex: 2, child: Text('Panier Moy.', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
],
),
),
// Lignes du tableau
...ventesData.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
final isEven = index % 2 == 0;
return InkWell(
onTap: () => _showPointVenteDetails(data),
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: isEven ? Colors.grey.withOpacity(0.05) : Colors.white,
),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _getPointVenteColor(index),
borderRadius: BorderRadius.circular(2),
),
),
SizedBox(width: 8),
Expanded(
child: Text(
data['point_vente_nom'] ?? 'N/A',
style: TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
flex: 2,
child: Text(
'${((data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0).toStringAsFixed(2)}',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
),
Expanded(
flex: 1,
child: Text(
'${data['nombre_commandes'] ?? 0}',
style: TextStyle(fontSize: 12),
),
),
Expanded(
flex: 1,
child: Text(
'${data['nombre_articles_vendus'] ?? 0}',
style: TextStyle(fontSize: 12),
),
),
Expanded(
flex: 2,
child: Text(
'${((data['panier_moyen'] as num?)?.toDouble() ?? 0.0).toStringAsFixed(2)}',
style: TextStyle(fontSize: 12),
),
),
],
),
),
);
}).toList(),
],
),
);
}
// Méthodes utilitaires
double _getMaxChiffreAffaires(List<Map<String, dynamic>> ventesData) {
if (ventesData.isEmpty) return 100.0;
return ventesData
.map((data) => (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0)
.reduce((a, b) => a > b ? a : b);
}
Color _getPointVenteColor(int index) {
final colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.teal,
Colors.pink,
Colors.indigo,
Colors.amber,
Colors.cyan,
Colors.lime,
];
return colors[index % colors.length];
}
String _formatCurrency(double value) {
if (value >= 1000000) {
return '${(value / 1000000).toStringAsFixed(1)}M';
} else if (value >= 1000) {
return '${(value / 1000).toStringAsFixed(1)}K';
} else {
return value.toStringAsFixed(0);
}
}
void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async {
final pointVenteId = pointVenteData['point_vente_id'] as int;
final pointVenteNom = pointVenteData['point_vente_nom'] as String;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Détails - $pointVenteNom'),
content: Container(
width: double.maxFinite,
height: 400,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Statistiques générales
_buildStatRow('Chiffre d\'affaires:', '${((pointVenteData['chiffre_affaires'] as num?)?.toDouble() ?? 0.0).toStringAsFixed(2)} MGA'),
_buildStatRow('Nombre de commandes:', '${pointVenteData['nombre_commandes'] ?? 0}'),
_buildStatRow('Articles vendus:', '${pointVenteData['nombre_articles_vendus'] ?? 0}'),
_buildStatRow('Quantité totale:', '${pointVenteData['quantite_totale_vendue'] ?? 0}'),
_buildStatRow('Panier moyen:', '${((pointVenteData['panier_moyen'] as num?)?.toDouble() ?? 0.0).toStringAsFixed(2)} MGA'),
SizedBox(height: 16),
Text('Top 5 des produits:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
// Top produits
FutureBuilder<List<Map<String, dynamic>>>(
future: _database.getTopProduitsParPointDeVente(pointVenteId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
return Text('Aucun produit vendu', style: TextStyle(color: Colors.grey));
}
final produits = snapshot.data!;
return Column(
children: produits.map((produit) => Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
produit['produit_nom'] ?? 'N/A',
style: TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
Text(
'${produit['quantite_vendue'] ?? 0} vendus',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
)).toList(),
);
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Fermer'),
),
],
),
);
}
Widget _buildStatRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontSize: 12)),
Text(value, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
],
),
);
}
Widget _buildLowStockCard() {
return Card(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@ import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/accueil.dart';
//import '../Services/app_database.dart'; // Changé de authDatabase.dart

View File

@ -1,10 +1,9 @@
import 'dart:io';
import 'package:esc_pos_printer/esc_pos_printer.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:esc_pos_utils/esc_pos_utils.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
@ -31,117 +30,6 @@ class TicketPage extends StatelessWidget {
required this.amountPaid,
}) : super(key: key);
Future<void> _printTicket() async {
final profile = await CapabilityProfile.load();
final printer = NetworkPrinter(PaperSize.mm80, profile);
printer.text('Ticket de caisse',
styles: const PosStyles(
align: PosAlign.center,
height: PosTextSize.size2,
width: PosTextSize.size2,
));
printer.text('Entreprise : $businessName');
printer.text('Adresse : $businessAddress');
printer.text('Numéro de téléphone : $businessPhoneNumber');
printer.hr();
printer.row([
PosColumn(
text: 'Produit',
width: 3,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
PosColumn(
text: 'Quantité',
width: 1,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
PosColumn(
text: 'Prix unitaire',
width: 1,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
PosColumn(
text: 'Total',
width: 1,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
]);
printer.hr();
for (final cartItem in cartItems) {
final product = cartItem.product;
final quantity = cartItem.quantity;
final productTotal = product.price * quantity;
printer.row([
PosColumn(
text: product.name,
width: 3,
),
PosColumn(
text: quantity.toString(),
width: 1,
),
PosColumn(
text: '${product.price.toStringAsFixed(2)} MGA',
width: 1,
),
PosColumn(
text: '${productTotal.toStringAsFixed(2)} MGA',
width: 1,
),
]);
}
printer.hr();
printer.row([
PosColumn(
text: 'Total :',
width: 3,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
PosColumn(
text: '${totalCartPrice.toStringAsFixed(2)} MGA',
width: 1,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
]);
printer.row([
PosColumn(
text: 'Somme remise :',
width: 3,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: '${amountPaid.toStringAsFixed(2)} MGA',
width: 1,
styles: const PosStyles(align: PosAlign.left),
),
]);
printer.row([
PosColumn(
text: 'Somme rendue :',
width: 3,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: '${(amountPaid - totalCartPrice).toStringAsFixed(2)} MGA',
width: 1,
styles: const PosStyles(align: PosAlign.left),
),
]);
printer.hr();
printer.text('Youmaz vous remercie pour votre achat!!!');
printer.feed(2);
printer.cut();
printer.disconnect(); // Fermez la connexion après l'impression
Get.snackbar('Impression', 'Ticket imprimé avec succès');
}
Future<void> _generateAndSavePDF() async {
final pdf = pw.Document();
@ -265,11 +153,7 @@ class TicketPage extends StatelessWidget {
}
// Obtenir la date actuelle
final currentDate = DateTime.now();
final formattedDate = DateFormat('dd/MM/yyyy HH:mm').format(currentDate);
// Calculer la somme remise
final double discount = totalOrderAmount - totalCartPrice;
// Calculer la somme rendue
final double change = amountPaid - totalOrderAmount;

View File

@ -1,4 +1,3 @@
import 'dart:io';
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@ -115,7 +114,7 @@ class _AccueilPageState extends State<AccueilPage> {
await orderDatabase.insertOrderItem(
orderId, product.name, quantity, price);
final updatedStock = product.stock! - quantity;
final updatedStock = product.stock - quantity;
await productDatabase.updateStock(product.id!, updatedStock);
}

View File

@ -1,10 +1,10 @@
// Config/database_config.dart - Version améliorée
class DatabaseConfig {
static const String host = '172.20.10.5';
static const String host = 'localhost';
static const int port = 3306;
static const String username = 'root';
static const String? password = null;
static const String database = 'guycom_databse_v1';
static const String database = 'gico';
static const String prodHost = '185.70.105.157';
static const String prodUsername = 'guycom';
@ -17,7 +17,7 @@ class DatabaseConfig {
static const int maxConnections = 10;
static const int minConnections = 2;
static bool get isDevelopment => false;
static bool get isDevelopment => true;
static Map<String, dynamic> getConfig() {
if (isDevelopment) {

View File

@ -9,7 +9,9 @@
#include <charset_converter/charset_converter_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <open_file_linux/open_file_linux_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) charset_converter_registrar =
@ -21,7 +23,13 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}

View File

@ -6,7 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
charset_converter
file_selector_linux
open_file_linux
screen_retriever
url_launcher_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -10,8 +10,10 @@ import file_selector_macos
import mobile_scanner
import open_file_mac
import path_provider_foundation
import screen_retriever
import shared_preferences_foundation
import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
@ -19,6 +21,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View File

@ -912,6 +912,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.3"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
shared_preferences:
dependency: "direct main"
description:
@ -1205,6 +1213,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.12.0"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf"
url: "https://pub.dev"
source: hosted
version: "0.3.9"
xdg_directories:
dependency: transitive
description:

View File

@ -67,6 +67,7 @@ dependencies:
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
numbers_to_letters: ^1.0.0
qr_code_scanner_plus: ^2.0.10+1
window_manager: ^0.3.7
@ -109,6 +110,7 @@ flutter:
- assets/mvola.jpg
- assets/Orange_money.png
- assets/fa-solid-900.ttf
- assets/NotoEmoji-Regular.ttf
- assets/fonts/Roboto-Italic.ttf
# An image asset can refer to one or more resolution-specific "variants", see

View File

@ -8,13 +8,19 @@
#include <charset_converter/charset_converter_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
CharsetConverterPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CharsetConverterPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}

View File

@ -5,7 +5,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
charset_converter
file_selector_windows
screen_retriever
url_launcher_windows
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST