Browse Source

lastlast update

14062025
b.razafimandimbihery 6 months ago
parent
commit
c0bbb0da2b
  1. BIN
      assets/NotoEmoji-Regular.ttf
  2. 338
      lib/Components/PaymentEnchainedDialog.dart
  3. 276
      lib/Components/commandManagementComponents/CommandDetails.dart
  4. 19
      lib/Components/commandManagementComponents/CommandeActions.dart
  5. 234
      lib/Components/commandManagementComponents/PaswordRequired.dart
  6. 33
      lib/Components/commandManagementComponents/PaymentMethodDialog.dart
  7. 411
      lib/Components/newCommandComponents/CadeauDialog.dart
  8. 331
      lib/Components/newCommandComponents/RemiseDialog.dart
  9. 2125
      lib/Components/teat.dart
  10. 345
      lib/Models/Client.dart
  11. 304
      lib/Services/Script.sql
  12. 762
      lib/Services/stock_managementDatabase.dart
  13. 409
      lib/Views/Dashboard.dart
  14. 1672
      lib/Views/commandManagement.dart
  15. 2173
      lib/Views/mobilepage.dart
  16. 890
      lib/Views/newCommand.dart
  17. 1
      lib/Views/registrationPage.dart
  18. 120
      lib/Views/ticketPage.dart
  19. 3
      lib/accueil.dart
  20. 6
      lib/config/DatabaseConfig.dart
  21. 8
      linux/flutter/generated_plugin_registrant.cc
  22. 2
      linux/flutter/generated_plugins.cmake
  23. 4
      macos/Flutter/GeneratedPluginRegistrant.swift
  24. 16
      pubspec.lock
  25. 2
      pubspec.yaml
  26. 6
      windows/flutter/generated_plugin_registrant.cc
  27. 2
      windows/flutter/generated_plugins.cmake

BIN
assets/NotoEmoji-Regular.ttf

Binary file not shown.

338
lib/Components/PaymentEnchainedDialog.dart

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

276
lib/Components/commandManagementComponents/CommandDetails.dart

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

19
lib/Components/commandManagementComponents/CommandeActions.dart

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

234
lib/Components/commandManagementComponents/PaswordRequired.dart

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

33
lib/Components/commandManagementComponents/PaymentMethodDialog.dart

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

411
lib/Components/newCommandComponents/CadeauDialog.dart

@ -0,0 +1,411 @@
// Components/newCommandComponents/CadeauDialog.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class CadeauDialog extends StatefulWidget {
final Product product;
final int quantite;
final DetailCommande? detailExistant;
const CadeauDialog({
Key? key,
required this.product,
required this.quantite,
this.detailExistant,
}) : super(key: key);
@override
_CadeauDialogState createState() => _CadeauDialogState();
}
class _CadeauDialogState extends State<CadeauDialog> {
final AppDatabase _database = AppDatabase.instance;
List<Product> _produitsDisponibles = [];
Product? _produitCadeauSelectionne;
int _quantiteCadeau = 1;
bool _isLoading = true;
String _searchQuery = '';
@override
void initState() {
super.initState();
_loadProduitsDisponibles();
}
Future<void> _loadProduitsDisponibles() async {
try {
final produits = await _database.getProducts();
setState(() {
_produitsDisponibles = produits.where((p) =>
p.id != widget.product.id && // Exclure le produit principal
(p.stock == null || p.stock! > 0) // Seulement les produits en stock
).toList();
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Impossible de charger les produits: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
List<Product> get _produitsFiltres {
if (_searchQuery.isEmpty) {
return _produitsDisponibles;
}
return _produitsDisponibles.where((p) =>
p.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
(p.reference?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false)
).toList();
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return AlertDialog(
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.card_giftcard, color: Colors.green.shade700),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ajouter un cadeau',
style: TextStyle(fontSize: isMobile ? 16 : 18),
),
Text(
'Pour: ${widget.product.name}',
style: TextStyle(
fontSize: isMobile ? 12 : 14,
color: Colors.grey.shade600,
fontWeight: FontWeight.normal,
),
),
],
),
),
],
),
content: Container(
width: isMobile ? double.maxFinite : 500,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Information sur le produit principal
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
children: [
Icon(Icons.shopping_bag, color: Colors.blue.shade700),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Produit acheté',
style: TextStyle(
fontSize: 12,
color: Colors.blue.shade700,
fontWeight: FontWeight.bold,
),
),
Text(
'${widget.quantite}x ${widget.product.name}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'Prix: ${widget.product.price.toStringAsFixed(2)} MGA',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Barre de recherche
TextField(
decoration: InputDecoration(
labelText: 'Rechercher un produit cadeau',
prefixIcon: Icon(Icons.search, color: Colors.green.shade600),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.green.shade50,
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
const SizedBox(height: 16),
// Liste des produits disponibles
Expanded(
child: _produitsFiltres.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.card_giftcard_outlined,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 8),
Text(
'Aucun produit disponible',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
)
: ListView.builder(
itemCount: _produitsFiltres.length,
itemBuilder: (context, index) {
final produit = _produitsFiltres[index];
final isSelected = _produitCadeauSelectionne?.id == produit.id;
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: isSelected ? 4 : 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: isSelected
? Colors.green.shade300
: Colors.grey.shade200,
width: isSelected ? 2 : 1,
),
),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected
? Colors.green.shade100
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.card_giftcard,
color: isSelected
? Colors.green.shade700
: Colors.grey.shade600,
),
),
title: Text(
produit.name,
style: TextStyle(
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Prix normal: ${produit.price.toStringAsFixed(2)} MGA',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
decoration: TextDecoration.lineThrough,
),
),
Row(
children: [
Icon(
Icons.card_giftcard,
size: 14,
color: Colors.green.shade600,
),
const SizedBox(width: 4),
Text(
'GRATUIT',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
if (produit.stock != null)
Text(
'Stock: ${produit.stock}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade500,
),
),
],
),
trailing: isSelected
? Icon(
Icons.check_circle,
color: Colors.green.shade700,
)
: null,
onTap: () {
setState(() {
_produitCadeauSelectionne = produit;
});
},
),
);
},
),
),
// Sélection de la quantité si un produit est sélectionné
if (_produitCadeauSelectionne != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
children: [
Icon(Icons.card_giftcard, color: Colors.green.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
'Quantité de ${_produitCadeauSelectionne!.name}',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.green.shade700,
),
),
),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.green.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove, size: 16),
onPressed: _quantiteCadeau > 1
? () {
setState(() {
_quantiteCadeau--;
});
}
: null,
),
Text(
_quantiteCadeau.toString(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
IconButton(
icon: const Icon(Icons.add, size: 16),
onPressed: () {
final maxStock = _produitCadeauSelectionne!.stock ?? 99;
if (_quantiteCadeau < maxStock) {
setState(() {
_quantiteCadeau++;
});
}
},
),
],
),
),
],
),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Annuler'),
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 20,
vertical: isMobile ? 10 : 12,
),
),
icon: const Icon(Icons.card_giftcard),
label: Text(
isMobile ? 'Offrir' : 'Offrir le cadeau',
style: TextStyle(fontSize: isMobile ? 12 : 14),
),
onPressed: _produitCadeauSelectionne != null
? () {
Get.back(result: {
'produit': _produitCadeauSelectionne!,
'quantite': _quantiteCadeau,
});
}
: null,
),
],
);
}
}

331
lib/Components/newCommandComponents/RemiseDialog.dart

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

2125
lib/Components/teat.dart

File diff suppressed because it is too large

345
lib/Models/Client.dart

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

304
lib/Services/Script.sql

@ -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;

762
lib/Services/stock_managementDatabase.dart

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

409
lib/Views/Dashboard.dart

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

1672
lib/Views/commandManagement.dart

File diff suppressed because it is too large

2173
lib/Views/mobilepage.dart

File diff suppressed because it is too large

890
lib/Views/newCommand.dart

File diff suppressed because it is too large

1
lib/Views/registrationPage.dart

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

120
lib/Views/ticketPage.dart

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

3
lib/accueil.dart

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

6
lib/config/DatabaseConfig.dart

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

8
linux/flutter/generated_plugin_registrant.cc

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

2
linux/flutter/generated_plugins.cmake

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

4
macos/Flutter/GeneratedPluginRegistrant.swift

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

16
pubspec.lock

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

2
pubspec.yaml

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

6
windows/flutter/generated_plugin_registrant.cc

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

2
windows/flutter/generated_plugins.cmake

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

Loading…
Cancel
Save