Compare commits

...

7 Commits

Author SHA1 Message Date
Stephane 5b6e9e1cc3 qr 6 months ago
b.razafimandimbihery b5a11aa3c9 migration mysql 6 months ago
b.razafimandimbihery 831cce13da last last last update 6 months ago
b.razafimandimbihery c8fedd08e5 last update 6 months ago
b.razafimandimbihery 9eafda610f commit fonctionnalite impec 6 months ago
b.razafimandimbihery 2bef06a2fe commit commit 6 months ago
b.razafimandimbihery 57ea91b3d7 maj dernier farany farany 6 months ago
  1. 3
      android/app/src/main/AndroidManifest.xml
  2. BIN
      assets/Orange_money.png
  3. BIN
      assets/airtel_money.png
  4. BIN
      assets/fa-solid-900.ttf
  5. BIN
      assets/fonts/Roboto-Italic.ttf
  6. BIN
      assets/mvola.jpg
  7. 3
      ios/Runner/Info.plist
  8. 417
      lib/Components/AddClient.dart
  9. 471
      lib/Components/AddClientForm.dart
  10. 176
      lib/Components/DiscountDialog.dart
  11. 349
      lib/Components/GiftaselectedButton.dart
  12. 338
      lib/Components/PaymentEnchainedDialog.dart
  13. 270
      lib/Components/QrScan.dart
  14. 151
      lib/Components/appDrawer.dart
  15. 104
      lib/Components/app_bar.dart
  16. 7
      lib/Components/paymentType.dart
  17. 219
      lib/Models/Client.dart
  18. 64
      lib/Models/Remise.dart
  19. 36
      lib/Models/pointage_model.dart
  20. 155
      lib/Models/produit.dart
  21. 26
      lib/Models/users.dart
  22. 0
      lib/Services/GestionStockDatabase.dart
  23. 680
      lib/Services/app_database.dart
  24. 559
      lib/Services/productDatabase.dart
  25. 1975
      lib/Services/stock_managementDatabase.dart
  26. 1205
      lib/Views/Dashboard.dart
  27. 3763
      lib/Views/HandleProduct.dart
  28. 7
      lib/Views/RoleListPage.dart
  29. 496
      lib/Views/RolePermissionPage.dart
  30. 2
      lib/Views/bilanMois.dart
  31. 1696
      lib/Views/commandManagement.dart
  32. 5
      lib/Views/editProduct.dart
  33. 3
      lib/Views/editUser.dart
  34. 7
      lib/Views/gestionProduct.dart
  35. 324
      lib/Views/gestionRole.dart
  36. 7
      lib/Views/gestionStock.dart
  37. 416
      lib/Views/gestion_point_de_vente.dart
  38. 939
      lib/Views/historique.dart
  39. 2
      lib/Views/listCommandeHistory.dart
  40. 5
      lib/Views/listUser.dart
  41. 210
      lib/Views/loginPage.dart
  42. 1747
      lib/Views/mobilepage.dart
  43. 1594
      lib/Views/newCommand.dart
  44. 62
      lib/Views/produitsCard.dart
  45. 73
      lib/Views/registrationPage.dart
  46. 7
      lib/accueil.dart
  47. 64
      lib/config/DatabaseConfig.dart
  48. 5
      lib/controller/AccueilController.dart
  49. 63
      lib/controller/userController.dart
  50. 106
      lib/main.dart
  51. 4
      macos/Flutter/GeneratedPluginRegistrant.swift
  52. 80
      pubspec.lock
  53. 14
      pubspec.yaml

3
android/app/src/main/AndroidManifest.xml

@ -1,4 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
<application <application
android:label="my_app" android:label="my_app"
android:name="${applicationName}" android:name="${applicationName}"
@ -12,6 +14,7 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues

BIN
assets/Orange_money.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/airtel_money.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/fa-solid-900.ttf

Binary file not shown.

BIN
assets/fonts/Roboto-Italic.ttf

Binary file not shown.

BIN
assets/mvola.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

3
ios/Runner/Info.plist

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSCameraUsageDescription</key>
<string>Cette application a besoin d'accéder à la caméra pour scanner les codes QR</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@ -47,5 +49,6 @@
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
</dict> </dict>
</plist> </plist>

417
lib/Components/AddClient.dart

@ -0,0 +1,417 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Models/client.dart';
import '../Services/stock_managementDatabase.dart';
class ClientFormController extends GetxController {
final _formKey = GlobalKey<FormState>();
// Controllers pour les champs
final _nomController = TextEditingController();
final _prenomController = TextEditingController();
final _emailController = TextEditingController();
final _telephoneController = TextEditingController();
final _adresseController = TextEditingController();
// Variables observables pour la recherche
var suggestedClients = <Client>[].obs;
var isSearching = false.obs;
var selectedClient = Rxn<Client>();
@override
void onClose() {
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
super.onClose();
}
// Méthode pour rechercher les clients existants
Future<void> searchClients(String query) async {
if (query.length < 2) {
suggestedClients.clear();
return;
}
isSearching.value = true;
try {
final clients = await AppDatabase.instance.suggestClients(query);
suggestedClients.value = clients;
} catch (e) {
print("Erreur recherche clients: $e");
suggestedClients.clear();
} finally {
isSearching.value = false;
}
}
// Méthode pour remplir automatiquement le formulaire
void fillFormWithClient(Client client) {
selectedClient.value = client;
_nomController.text = client.nom;
_prenomController.text = client.prenom;
_emailController.text = client.email;
_telephoneController.text = client.telephone;
_adresseController.text = client.adresse ?? '';
suggestedClients.clear();
}
// Méthode pour vider le formulaire
void clearForm() {
selectedClient.value = null;
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_adresseController.clear();
suggestedClients.clear();
}
// Méthode pour valider et soumettre
Future<void> submitForm() async {
if (!_formKey.currentState!.validate()) return;
try {
Client clientToUse;
if (selectedClient.value != null) {
// Utiliser le client existant
clientToUse = selectedClient.value!;
} else {
// Créer un nouveau client
final newClient = Client(
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
dateCreation: DateTime.now(),
);
clientToUse = await AppDatabase.instance.createOrGetClient(newClient);
}
// Procéder avec la commande
Get.back();
_submitOrderWithClient(clientToUse);
} catch (e) {
Get.snackbar(
'Erreur',
'Erreur lors de la création/récupération du client: $e',
backgroundColor: Colors.red.shade100,
colorText: Colors.red.shade800,
);
}
}
void _submitOrderWithClient(Client client) {
// Votre logique existante pour soumettre la commande
// avec le client fourni
}
}
// Widget pour le formulaire avec auto-completion
void _showClientFormDialog() {
final controller = Get.put(ClientFormController());
Get.dialog(
AlertDialog(
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.person_add, color: Colors.blue.shade700),
),
const SizedBox(width: 12),
const Text('Informations Client'),
const Spacer(),
// Bouton pour vider le formulaire
IconButton(
onPressed: controller.clearForm,
icon: const Icon(Icons.clear),
tooltip: 'Vider le formulaire',
),
],
),
content: Container(
width: 600,
constraints: const BoxConstraints(maxHeight: 700),
child: SingleChildScrollView(
child: Form(
key: controller._formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section de recherche rapide
_buildSearchSection(controller),
const SizedBox(height: 16),
// Indicateur client sélectionné
Obx(() {
if (controller.selectedClient.value != null) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
border: Border.all(color: Colors.green.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.check_circle, color: Colors.green.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
'Client existant sélectionné: ${controller.selectedClient.value!.nomComplet}',
style: TextStyle(
color: Colors.green.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
return const SizedBox.shrink();
}),
const SizedBox(height: 12),
// Champs du formulaire
_buildTextFormField(
controller: controller._nomController,
label: 'Nom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._prenomController,
label: 'Prénom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._emailController,
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
return 'Email invalide';
}
return null;
},
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
// Recherche automatique par email
controller.searchClients(value);
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._telephoneController,
label: 'Téléphone',
keyboardType: TextInputType.phone,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
// Recherche automatique par téléphone
controller.searchClients(value);
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._adresseController,
label: 'Adresse',
maxLines: 2,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildCommercialDropdown(),
// Liste des suggestions
Obx(() {
if (controller.isSearching.value) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
if (controller.suggestedClients.isEmpty) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
Text(
'Clients trouvés:',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
...controller.suggestedClients.map((client) =>
_buildClientSuggestionTile(client, controller),
),
],
);
}),
],
),
),
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Annuler'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: controller.submitForm,
child: const Text('Valider la commande'),
),
],
),
);
}
// Widget pour la section de recherche
Widget _buildSearchSection(ClientFormController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recherche rapide',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
TextFormField(
decoration: InputDecoration(
labelText: 'Rechercher un client existant',
hintText: 'Nom, prénom, email ou téléphone...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
onChanged: controller.searchClients,
),
],
);
}
// Widget pour afficher une suggestion de client
Widget _buildClientSuggestionTile(Client client, ClientFormController controller) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.shade100,
child: Icon(Icons.person, color: Colors.blue.shade700),
),
title: Text(
client.nomComplet,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('📧 ${client.email}'),
Text('📞 ${client.telephone}'),
if (client.adresse != null && client.adresse!.isNotEmpty)
Text('📍 ${client.adresse}'),
],
),
trailing: ElevatedButton(
onPressed: () => controller.fillFormWithClient(client),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('Utiliser'),
),
isThreeLine: true,
),
);
}
// Widget helper pour les champs de texte
Widget _buildTextFormField({
required TextEditingController controller,
required String label,
TextInputType? keyboardType,
String? Function(String?)? validator,
int maxLines = 1,
void Function(String)? onChanged,
}) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: label,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: keyboardType,
validator: validator,
maxLines: maxLines,
onChanged: onChanged,
);
}
// Votre méthode _buildCommercialDropdown existante
Widget _buildCommercialDropdown() {
// Votre implémentation existante
return Container(); // Remplacez par votre code existant
}

471
lib/Components/AddClientForm.dart

@ -0,0 +1,471 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Models/client.dart';
class ClientFormWidget extends StatefulWidget {
final Function(Client) onClientSelected;
final Client? initialClient;
const ClientFormWidget({
Key? key,
required this.onClientSelected,
this.initialClient,
}) : super(key: key);
@override
State<ClientFormWidget> createState() => _ClientFormWidgetState();
}
class _ClientFormWidgetState extends State<ClientFormWidget> {
final _formKey = GlobalKey<FormState>();
final AppDatabase _database = AppDatabase.instance;
// Contrôleurs de texte
final TextEditingController _nomController = TextEditingController();
final TextEditingController _prenomController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _telephoneController = TextEditingController();
final TextEditingController _adresseController = TextEditingController();
// Variables d'état
bool _isLoading = false;
Client? _selectedClient;
List<Client> _suggestions = [];
bool _showSuggestions = false;
String _searchQuery = '';
@override
void initState() {
super.initState();
if (widget.initialClient != null) {
_fillClientData(widget.initialClient!);
}
// Écouter les changements dans les champs pour déclencher la recherche
_emailController.addListener(_onEmailChanged);
_telephoneController.addListener(_onPhoneChanged);
_nomController.addListener(_onNameChanged);
_prenomController.addListener(_onNameChanged);
}
@override
void dispose() {
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
super.dispose();
}
void _fillClientData(Client client) {
setState(() {
_selectedClient = client;
_nomController.text = client.nom;
_prenomController.text = client.prenom;
_emailController.text = client.email;
_telephoneController.text = client.telephone;
_adresseController.text = client.adresse ?? '';
});
}
void _clearForm() {
setState(() {
_selectedClient = null;
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_adresseController.clear();
_suggestions.clear();
_showSuggestions = false;
});
}
// Recherche par email
void _onEmailChanged() async {
final email = _emailController.text.trim();
if (email.length >= 3 && email.contains('@')) {
_searchExistingClient(email: email);
}
}
// Recherche par téléphone
void _onPhoneChanged() async {
final phone = _telephoneController.text.trim();
if (phone.length >= 4) {
_searchExistingClient(telephone: phone);
}
}
// Recherche par nom/prénom
void _onNameChanged() async {
final nom = _nomController.text.trim();
final prenom = _prenomController.text.trim();
if (nom.length >= 2 || prenom.length >= 2) {
final query = '$nom $prenom'.trim();
if (query.length >= 2) {
_getSuggestions(query);
}
}
}
// Rechercher un client existant
Future<void> _searchExistingClient({
String? email,
String? telephone,
String? nom,
String? prenom,
}) async {
if (_selectedClient != null) return; // Éviter de chercher si un client est déjà sélectionné
try {
setState(() => _isLoading = true);
final existingClient = await _database.findExistingClient(
email: email,
telephone: telephone,
nom: nom,
prenom: prenom,
);
if (existingClient != null && mounted) {
_showClientFoundDialog(existingClient);
}
} catch (e) {
print('Erreur lors de la recherche: $e');
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
// Obtenir les suggestions
Future<void> _getSuggestions(String query) async {
if (query.length < 2) {
setState(() {
_suggestions.clear();
_showSuggestions = false;
});
return;
}
try {
final suggestions = await _database.suggestClients(query);
if (mounted) {
setState(() {
_suggestions = suggestions;
_showSuggestions = suggestions.isNotEmpty;
_searchQuery = query;
});
}
} catch (e) {
print('Erreur lors de la récupération des suggestions: $e');
}
}
// Afficher le dialogue de client trouvé
void _showClientFoundDialog(Client client) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Client existant trouvé'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Un client avec ces informations existe déjà :'),
const SizedBox(height: 10),
Text('Nom: ${client.nom} ${client.prenom}', style: const TextStyle(fontWeight: FontWeight.bold)),
Text('Email: ${client.email}'),
Text('Téléphone: ${client.telephone}'),
if (client.adresse != null) Text('Adresse: ${client.adresse}'),
const SizedBox(height: 10),
const Text('Voulez-vous utiliser ces informations ?'),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
// Continuer avec les nouvelles données
},
child: const Text('Non, créer nouveau'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_fillClientData(client);
},
child: const Text('Oui, utiliser'),
),
],
),
);
}
// Valider et soumettre le formulaire
void _submitForm() async {
if (!_formKey.currentState!.validate()) return;
try {
setState(() => _isLoading = true);
Client client;
if (_selectedClient != null) {
// Utiliser le client existant avec les données mises à jour
client = Client(
id: _selectedClient!.id,
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim().toLowerCase(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
dateCreation: _selectedClient!.dateCreation,
actif: _selectedClient!.actif,
);
} else {
// Créer un nouveau client
client = Client(
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim().toLowerCase(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
dateCreation: DateTime.now(),
);
// Utiliser createOrGetClient pour éviter les doublons
client = await _database.createOrGetClient(client);
}
widget.onClientSelected(client);
} catch (e) {
Get.snackbar(
'Erreur',
'Erreur lors de la sauvegarde du client: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// En-tête avec bouton de réinitialisation
Row(
children: [
const Text(
'Informations du client',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Spacer(),
if (_selectedClient != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Client existant',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
const SizedBox(width: 8),
IconButton(
onPressed: _clearForm,
icon: const Icon(Icons.refresh),
tooltip: 'Nouveau client',
),
],
),
const SizedBox(height: 16),
// Champs du formulaire
Row(
children: [
Expanded(
child: TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom *',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le nom est requis';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _prenomController,
decoration: const InputDecoration(
labelText: 'Prénom *',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le prénom est requis';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// Email avec indicateur de chargement
Stack(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email *',
border: const OutlineInputBorder(),
suffixIcon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'L\'email est requis';
}
if (!GetUtils.isEmail(value)) {
return 'Email invalide';
}
return null;
},
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _telephoneController,
decoration: const InputDecoration(
labelText: 'Téléphone *',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le téléphone est requis';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _adresseController,
decoration: const InputDecoration(
labelText: 'Adresse',
border: OutlineInputBorder(),
),
maxLines: 2,
),
// Suggestions
if (_showSuggestions && _suggestions.isNotEmpty) ...[
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
children: [
const Icon(Icons.people, size: 16),
const SizedBox(width: 8),
const Text('Clients similaires trouvés:', style: TextStyle(fontWeight: FontWeight.bold)),
const Spacer(),
IconButton(
onPressed: () => setState(() => _showSuggestions = false),
icon: const Icon(Icons.close, size: 16),
),
],
),
),
...List.generate(_suggestions.length, (index) {
final suggestion = _suggestions[index];
return ListTile(
dense: true,
leading: const Icon(Icons.person, size: 20),
title: Text('${suggestion.nom} ${suggestion.prenom}'),
subtitle: Text('${suggestion.email}${suggestion.telephone}'),
trailing: ElevatedButton(
onPressed: () => _fillClientData(suggestion),
child: const Text('Utiliser'),
),
);
}),
],
),
),
],
const SizedBox(height: 24),
// Bouton de soumission
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 8),
Text('Traitement...'),
],
)
: Text(_selectedClient != null ? 'Utiliser ce client' : 'Créer le client'),
),
),
],
),
);
}
}

176
lib/Components/DiscountDialog.dart

@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/snackbar/snackbar.dart';
import 'package:youmazgestion/Models/Remise.dart';
class DiscountDialog extends StatefulWidget {
final Function(Remise) onDiscountApplied;
const DiscountDialog({super.key, required this.onDiscountApplied});
@override
_DiscountDialogState createState() => _DiscountDialogState();
}
class _DiscountDialogState extends State<DiscountDialog> {
RemiseType _selectedType = RemiseType.pourcentage;
final _valueController = TextEditingController();
final _descriptionController = TextEditingController();
@override
void dispose() {
_valueController.dispose();
_descriptionController.dispose();
super.dispose();
}
void _applyDiscount() {
final value = double.tryParse(_valueController.text) ?? 0;
if (value <= 0) {
Get.snackbar(
'Erreur',
'Veuillez entrer une valeur valide',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
if (_selectedType == RemiseType.pourcentage && value > 100) {
Get.snackbar(
'Erreur',
'Le pourcentage ne peut pas dépasser 100%',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
final remise = Remise(
type: _selectedType,
valeur: value,
description: _descriptionController.text,
);
widget.onDiscountApplied(remise);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Icon(Icons.local_offer, color: Colors.orange.shade600),
const SizedBox(width: 8),
const Text('Appliquer une remise'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Type de remise:', style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: RadioListTile<RemiseType>(
contentPadding: EdgeInsets.zero,
title: const Text('Pourcentage'),
value: RemiseType.pourcentage,
groupValue: _selectedType,
onChanged: (value) => setState(() => _selectedType = value!),
),
),
Expanded(
child: RadioListTile<RemiseType>(
contentPadding: EdgeInsets.zero,
title: const Text('Montant fixe'),
value: RemiseType.fixe,
groupValue: _selectedType,
onChanged: (value) => setState(() => _selectedType = value!),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _valueController,
keyboardType: TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: _selectedType == RemiseType.pourcentage
? 'Pourcentage (%)'
: 'Montant (MGA)',
prefixIcon: Icon(
_selectedType == RemiseType.pourcentage
? Icons.percent
: Icons.attach_money,
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Motif de la remise (optionnel)',
prefixIcon: Icon(Icons.note),
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 16),
// Aperçu de la remise
if (_valueController.text.isNotEmpty)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Aperçu:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(
_selectedType == RemiseType.pourcentage
? 'Remise de ${_valueController.text}%'
: 'Remise de ${_valueController.text} MGA',
),
if (_descriptionController.text.isNotEmpty)
Text('Motif: ${_descriptionController.text}'),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _applyDiscount,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade600,
foregroundColor: Colors.white,
),
child: const Text('Appliquer'),
),
],
);
}
}

349
lib/Components/GiftaselectedButton.dart

@ -0,0 +1,349 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:youmazgestion/Models/Remise.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class GiftSelectionDialog extends StatefulWidget {
const GiftSelectionDialog({super.key});
@override
_GiftSelectionDialogState createState() => _GiftSelectionDialogState();
}
class _GiftSelectionDialogState extends State<GiftSelectionDialog> {
final AppDatabase _database = AppDatabase.instance;
final _searchController = TextEditingController();
List<Product> _products = [];
List<Product> _filteredProducts = [];
bool _isLoading = true;
String? _selectedCategory;
@override
void initState() {
super.initState();
_loadProducts();
_searchController.addListener(_filterProducts);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadProducts() async {
try {
final products = await _database.getProducts();
setState(() {
_products = products.where((p) => p.stock > 0).toList(); // Seulement les produits en stock
_filteredProducts = _products;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
Get.snackbar(
'Erreur',
'Impossible de charger les produits',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
void _filterProducts() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredProducts = _products.where((product) {
final matchesSearch = product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false) ||
(product.imei?.toLowerCase().contains(query) ?? false);
final matchesCategory = _selectedCategory == null ||
product.category == _selectedCategory;
return matchesSearch && matchesCategory;
}).toList();
});
}
void _selectGift(Product product) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.card_giftcard, color: Colors.purple.shade600),
const SizedBox(width: 8),
const Text('Confirmer le cadeau'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Produit sélectionné:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.purple.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
if (product.reference != null && product.reference!.isNotEmpty)
Text('Référence: ${product.reference}'),
if (product.category.isNotEmpty)
Text('Catégorie: ${product.category}'),
Text('Prix normal: ${product.price.toStringAsFixed(0)} MGA'),
Text('Stock disponible: ${product.stock}'),
],
),
),
const SizedBox(height: 16),
const Text(
'Ce produit sera ajouté à la commande avec un prix de 0 MGA.',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context); // Fermer ce dialogue
Navigator.pop(context, ProduitCadeau(produit: product)); // Retourner le produit
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
),
child: const Text('Confirmer le cadeau'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final categories = _products.map((p) => p.category).toSet().toList()..sort();
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(20),
child: Column(
children: [
// En-tête
Row(
children: [
Icon(Icons.card_giftcard, color: Colors.purple.shade600, size: 28),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Choisir un cadeau',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 16),
// Barre de recherche
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Rechercher un produit',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 12),
// Filtre par catégorie
Container(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
FilterChip(
label: const Text('Toutes'),
selected: _selectedCategory == null,
onSelected: (selected) {
setState(() {
_selectedCategory = null;
_filterProducts();
});
},
),
const SizedBox(width: 8),
...categories.map((category) => Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(category),
selected: _selectedCategory == category,
onSelected: (selected) {
setState(() {
_selectedCategory = selected ? category : null;
_filterProducts();
});
},
),
)),
],
),
),
const SizedBox(height: 16),
// Liste des produits
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredProducts.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucun produit disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'Essayez de modifier vos critères de recherche',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
)
: ListView.builder(
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
leading: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.purple.shade200),
),
child: product.image != null && product.image!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
product.image!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.image_not_supported,
color: Colors.purple.shade300),
),
)
: Icon(Icons.card_giftcard,
color: Colors.purple.shade400, size: 30),
),
title: Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (product.reference != null && product.reference!.isNotEmpty)
Text('Ref: ${product.reference}'),
Text('Catégorie: ${product.category}'),
Text(
'Prix: ${product.price.toStringAsFixed(0)} MGA',
style: TextStyle(
color: Colors.green.shade600,
fontWeight: FontWeight.w600,
),
),
],
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Stock: ${product.stock}',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => _selectGift(product),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Choisir', style: TextStyle(fontSize: 12)),
),
],
),
onTap: () => _selectGift(product),
),
);
},
),
),
],
),
),
);
}
}

338
lib/Components/PaymentEnchainedDialog.dart

@ -0,0 +1,338 @@
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),
],
),
),
),
);
}
}

270
lib/Components/QrScan.dart

@ -0,0 +1,270 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScanQRPage extends StatefulWidget {
const ScanQRPage({super.key});
@override
State<ScanQRPage> createState() => _ScanQRPageState();
}
class _ScanQRPageState extends State<ScanQRPage> {
MobileScannerController? cameraController;
bool _isScanComplete = false;
String? _scannedData;
bool _hasError = false;
String? _errorMessage;
bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
@override
void initState() {
super.initState();
_initializeController();
}
void _initializeController() {
// if (!isMobile) {
// setState(() {
// _hasError = true;
// _errorMessage =
// "Le scanner QR n'est pas disponible sur cette plateforme.";
// });
// return;
// }
try {
cameraController = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
facing: CameraFacing.back,
torchEnabled: false,
);
setState(() {
_hasError = false;
_errorMessage = null;
});
} catch (e) {
setState(() {
_hasError = true;
_errorMessage = 'Erreur d\'initialisation de la caméra: $e';
});
}
}
@override
void dispose() {
cameraController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scanner QR Code'),
actions: _hasError
? []
: [
if (cameraController != null) ...[
IconButton(
color: Colors.white,
icon: const Icon(Icons.flash_on, color: Colors.white),
iconSize: 32.0,
onPressed: () => cameraController!.toggleTorch(),
),
IconButton(
color: Colors.white,
icon:
const Icon(Icons.flip_camera_ios, color: Colors.white),
iconSize: 32.0,
onPressed: () => cameraController!.switchCamera(),
),
],
],
),
body: _hasError ? _buildErrorWidget() : _buildScannerWidget(),
);
}
Widget _buildErrorWidget() {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text(
'Erreur de caméra',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
_errorMessage ?? 'Une erreur s\'est produite',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
_initializeController();
},
child: const Text('Réessayer'),
),
const SizedBox(height: 16),
const Text(
'Vérifiez que:\n• Le plugin mobile_scanner est installé\n• Les permissions de caméra sont accordées\n• Votre appareil a une caméra fonctionnelle',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
);
}
Widget _buildScannerWidget() {
if (cameraController == null) {
return const Center(child: CircularProgressIndicator());
}
return Stack(
children: [
MobileScanner(
controller: cameraController!,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
if (!_isScanComplete && barcode.rawValue != null) {
_isScanComplete = true;
_scannedData = barcode.rawValue;
_showScanResult(context, _scannedData!);
}
}
},
errorBuilder: (context, error, child) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Erreur: ${error.errorDetails?.message ?? 'Erreur inconnue'}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _initializeController(),
child: const Text('Réessayer'),
),
],
),
);
},
),
CustomPaint(
painter: QrScannerOverlay(
borderColor: Colors.blue.shade800,
),
),
],
);
}
void _showScanResult(BuildContext context, String data) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Résultat du scan'),
content: SelectableText(data), // Permet de sélectionner le texte
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
setState(() {
_isScanComplete = false;
});
},
child: const Text('Fermer'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context, data); // Retourner la donnée scannée
},
child: const Text('Utiliser'),
),
],
),
).then((_) {
setState(() {
_isScanComplete = false;
});
});
}
}
class QrScannerOverlay extends CustomPainter {
final Color borderColor;
QrScannerOverlay({required this.borderColor});
@override
void paint(Canvas canvas, Size size) {
final double width = size.width;
final double height = size.height;
final double borderWidth = 2.0;
final double borderLength = 30.0;
final double areaSize = width * 0.7;
final Paint backgroundPaint = Paint()
..color = Colors.black.withOpacity(0.4);
canvas.drawRect(Rect.fromLTRB(0, 0, width, height), backgroundPaint);
final Paint transparentPaint = Paint()
..color = Colors.transparent
..blendMode = BlendMode.clear;
final double areaLeft = (width - areaSize) / 2;
final double areaTop = (height - areaSize) / 2;
canvas.drawRect(
Rect.fromLTRB(areaLeft, areaTop, areaLeft + areaSize, areaTop + areaSize),
transparentPaint,
);
final Paint borderPaint = Paint()
..color = borderColor
..strokeWidth = borderWidth
..style = PaintingStyle.stroke;
// Coins du scanner
_drawCorner(
canvas, borderPaint, areaLeft, areaTop, borderLength, true, true);
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop, borderLength,
false, true);
_drawCorner(canvas, borderPaint, areaLeft, areaTop + areaSize, borderLength,
true, false);
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop + areaSize,
borderLength, false, false);
}
void _drawCorner(Canvas canvas, Paint paint, double x, double y,
double length, bool isLeft, bool isTop) {
final double horizontalStart = isLeft ? x : x - length;
final double horizontalEnd = isLeft ? x + length : x;
final double verticalStart = isTop ? y : y - length;
final double verticalEnd = isTop ? y + length : y;
canvas.drawLine(
Offset(horizontalStart, y), Offset(horizontalEnd, y), paint);
canvas.drawLine(Offset(x, verticalStart), Offset(x, verticalEnd), paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}

151
lib/Components/appDrawer.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/Views/HandleProduct.dart'; import 'package:youmazgestion/Views/HandleProduct.dart';
import 'package:youmazgestion/Views/RoleListPage.dart'; import 'package:youmazgestion/Views/RoleListPage.dart';
import 'package:youmazgestion/Views/commandManagement.dart'; import 'package:youmazgestion/Views/commandManagement.dart';
@ -13,6 +14,7 @@ import 'package:youmazgestion/Views/newCommand.dart';
import 'package:youmazgestion/Views/registrationPage.dart'; import 'package:youmazgestion/Views/registrationPage.dart';
import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/controller/userController.dart';
import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; // Nouvel import
class CustomDrawer extends StatelessWidget { class CustomDrawer extends StatelessWidget {
final UserController userController = Get.find<UserController>(); final UserController userController = Get.find<UserController>();
@ -73,7 +75,9 @@ class CustomDrawer extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
controller.name.isNotEmpty ? controller.name : 'Utilisateur', controller.name.isNotEmpty
? controller.name
: 'Utilisateur',
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,
@ -102,7 +106,7 @@ class CustomDrawer extends StatelessWidget {
color: Colors.blue, color: Colors.blue,
permissionAction: 'view', permissionAction: 'view',
permissionRoute: '/accueil', permissionRoute: '/accueil',
onTap: () => Get.to(const AccueilPage()), onTap: () => Get.to(DashboardPage()),
), ),
); );
@ -123,6 +127,14 @@ class CustomDrawer extends StatelessWidget {
permissionRoute: '/modifier-utilisateur', permissionRoute: '/modifier-utilisateur',
onTap: () => Get.to(const ListUserPage()), onTap: () => Get.to(const ListUserPage()),
), ),
await _buildDrawerItem(
icon: Icons.timer,
title: "Gestion des pointages",
color: const Color.fromARGB(255, 4, 54, 95),
permissionAction: 'update',
permissionRoute: '/pointage',
onTap: () => {},
)
]; ];
if (gestionUtilisateursItems.any((item) => item is ListTile)) { if (gestionUtilisateursItems.any((item) => item is ListTile)) {
@ -217,11 +229,11 @@ class CustomDrawer extends StatelessWidget {
List<Widget> rapportsItems = [ List<Widget> rapportsItems = [
await _buildDrawerItem( await _buildDrawerItem(
icon: Icons.bar_chart, icon: Icons.bar_chart,
title: "Bilan mensuel", title: "Bilan ",
color: Colors.teal, color: Colors.teal,
permissionAction: 'read', permissionAction: 'read',
permissionRoute: '/bilan', permissionRoute: '/bilan',
onTap: () => Get.to(const BilanMois()), onTap: () => Get.to(DashboardPage()),
), ),
await _buildDrawerItem( await _buildDrawerItem(
icon: Icons.history, icon: Icons.history,
@ -229,7 +241,7 @@ class CustomDrawer extends StatelessWidget {
color: Colors.blue, color: Colors.blue,
permissionAction: 'read', permissionAction: 'read',
permissionRoute: '/historique', permissionRoute: '/historique',
onTap: () => Get.to(HistoryPage()), onTap: () => Get.to(const HistoriquePage()),
), ),
]; ];
@ -259,6 +271,14 @@ class CustomDrawer extends StatelessWidget {
permissionRoute: '/gerer-roles', permissionRoute: '/gerer-roles',
onTap: () => Get.to(const RoleListPage()), onTap: () => Get.to(const RoleListPage()),
), ),
await _buildDrawerItem(
icon: Icons.store,
title: "Points de vente",
color: Colors.blueGrey,
permissionAction: 'admin',
permissionRoute: '/points-de-vente',
onTap: () => Get.to(const AjoutPointDeVentePage()),
),
]; ];
if (administrationItems.any((item) => item is ListTile)) { if (administrationItems.any((item) => item is ListTile)) {
@ -285,25 +305,122 @@ class CustomDrawer extends StatelessWidget {
leading: const Icon(Icons.logout, color: Colors.red), leading: const Icon(Icons.logout, color: Colors.red),
title: const Text("Déconnexion"), title: const Text("Déconnexion"),
onTap: () { onTap: () {
Get.defaultDialog( Get.dialog(
title: "Déconnexion", AlertDialog(
content: const Text("Voulez-vous vraiment vous déconnecter ?"), shape: RoundedRectangleBorder(
actions: [ borderRadius: BorderRadius.circular(16),
TextButton( ),
child: const Text("Non"), contentPadding: EdgeInsets.zero,
content: Container(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
child: Column(
children: [
Icon(
Icons.logout_rounded,
size: 48,
color: Colors.orange.shade600,
),
const SizedBox(height: 16),
const Text(
"Déconnexion",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 12),
const Text(
"Êtes-vous sûr de vouloir vous déconnecter ?",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.black87,
height: 1.4,
),
),
const SizedBox(height: 8),
Text(
"Vous devrez vous reconnecter pour accéder à votre compte.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
height: 1.3,
),
),
],
),
),
// Actions
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Get.back(), onPressed: () => Get.back(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
side: BorderSide(
color: Colors.grey.shade300,
width: 1.5,
),
),
child: const Text(
"Annuler",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
), ),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
), ),
child: const Text("Oui"), ),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async { onPressed: () async {
await clearUserData(); await clearUserData();
Get.offAll(const LoginPage()); Get.offAll(const LoginPage());
}, },
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
foregroundColor: Colors.white,
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
"Se déconnecter",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
), ),
], ],
),
),
],
),
),
),
barrierDismissible: true,
); );
}, },
), ),
@ -321,7 +438,8 @@ class CustomDrawer extends StatelessWidget {
required VoidCallback onTap, required VoidCallback onTap,
}) async { }) async {
if (permissionAction != null && permissionRoute != null) { if (permissionAction != null && permissionRoute != null) {
bool hasPermission = await userController.hasPermission(permissionAction, permissionRoute); bool hasPermission =
await userController.hasPermission(permissionAction, permissionRoute);
if (!hasPermission) { if (!hasPermission) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -337,3 +455,4 @@ class CustomDrawer extends StatelessWidget {
); );
} }
} }

104
lib/Components/app_bar.dart

@ -1,31 +1,121 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/controller/userController.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title; final String title;
final Widget? subtitle; final Widget? subtitle;
final List<Widget>? actions;
final bool automaticallyImplyLeading;
final Color? backgroundColor;
final bool isDesktop; // Add this parameter
const CustomAppBar({ final UserController userController = Get.put(UserController());
CustomAppBar({
Key? key, Key? key,
required this.title, required this.title,
this.subtitle, this.subtitle,
this.actions,
this.automaticallyImplyLeading = true,
this.backgroundColor,
this.isDesktop = false, // Add this parameter with default value
}) : super(key: key); }) : super(key: key);
@override @override
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0); Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBar( return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.shade900,
Colors.blue.shade800,
],
),
boxShadow: [
BoxShadow(
color: Colors.blue.shade900.withOpacity(0.3),
offset: const Offset(0, 2),
blurRadius: 4,
),
],
),
child: AppBar(
backgroundColor: backgroundColor ?? Colors.transparent,
elevation: 0,
automaticallyImplyLeading: automaticallyImplyLeading,
centerTitle: false,
iconTheme: const IconThemeData(
color: Colors.white,
size: 24,
),
actions: actions,
title: subtitle == null title: subtitle == null
? Text(title) ? Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
)
: Column( : Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(title, style: TextStyle(fontSize: 20)), Text(
subtitle!, title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
),
const SizedBox(height: 2),
Obx(() => Text(
userController.role != 'Super Admin'
? 'Point de vente: ${userController.pointDeVenteDesignation}'
: '',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white.withOpacity(0.9),
letterSpacing: 0.3,
),
)),
if (subtitle != null) ...[
const SizedBox(height: 2),
DefaultTextStyle(
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.8),
fontWeight: FontWeight.w400,
),
child: subtitle!,
),
], ],
],
),
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.shade900,
Colors.blue.shade800,
],
),
),
),
), ),
// autres propriétés si besoin
); );
} }
} }

7
lib/Components/paymentType.dart

@ -0,0 +1,7 @@
enum PaymentType {
cash,
card,
mvola,
orange,
airtel
}

219
lib/Models/Client.dart

@ -1,4 +1,4 @@
// Models/client.dart // Models/client.dart - Version corrigée pour MySQL
class Client { class Client {
final int? id; final int? id;
final String nom; final String nom;
@ -33,29 +33,49 @@ class Client {
}; };
} }
// Fonction helper améliorée pour parser les dates
static DateTime _parseDateTime(dynamic dateValue) {
if (dateValue == null) return DateTime.now();
if (dateValue is DateTime) return dateValue;
if (dateValue is String) {
try {
return DateTime.parse(dateValue);
} catch (e) {
print("Erreur parsing date string: $dateValue, erreur: $e");
return DateTime.now();
}
}
// Pour MySQL qui peut retourner un Timestamp
if (dateValue is int) {
return DateTime.fromMillisecondsSinceEpoch(dateValue);
}
print("Type de date non reconnu: ${dateValue.runtimeType}, valeur: $dateValue");
return DateTime.now();
}
factory Client.fromMap(Map<String, dynamic> map) { factory Client.fromMap(Map<String, dynamic> map) {
return Client( return Client(
id: map['id'], id: map['id'] as int?,
nom: map['nom'], nom: map['nom'] as String,
prenom: map['prenom'], prenom: map['prenom'] as String,
email: map['email'], email: map['email'] as String,
telephone: map['telephone'], telephone: map['telephone'] as String,
adresse: map['adresse'], adresse: map['adresse'] as String?,
dateCreation: DateTime.parse(map['dateCreation']), dateCreation: _parseDateTime(map['dateCreation']),
actif: map['actif'] == 1, actif: (map['actif'] as int?) == 1,
); );
} }
String get nomComplet => '$prenom $nom'; String get nomComplet => '$prenom $nom';
} }
// Models/commande.dart
enum StatutCommande { enum StatutCommande {
enAttente, enAttente,
confirmee, confirmee,
enPreparation,
expediee,
livree,
annulee annulee
} }
@ -67,25 +87,51 @@ class Commande {
final double montantTotal; final double montantTotal;
final String? notes; final String? notes;
final DateTime? dateLivraison; final DateTime? dateLivraison;
final int? commandeurId;
// Données du client (pour les jointures) final int? validateurId;
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,
required this.clientId, required this.clientId,
required this.dateCommande, required this.dateCommande,
this.statut = StatutCommande.enAttente, required this.statut,
required this.montantTotal, required this.montantTotal,
this.notes, this.notes,
this.dateLivraison, this.dateLivraison,
this.commandeurId,
this.validateurId,
this.clientNom, this.clientNom,
this.clientPrenom, this.clientPrenom,
this.clientEmail, this.clientEmail,
this.remisePourcentage,
this.remiseMontant,
this.montantApresRemise,
}); });
String get clientNomComplet {
if (clientNom != null && clientPrenom != null) {
return '$clientPrenom $clientNom';
}
return 'Client inconnu';
}
String get statutLibelle {
switch (statut) {
case StatutCommande.enAttente:
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.annulee:
return 'Annulée';
}
}
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
@ -95,50 +141,79 @@ class Commande {
'montantTotal': montantTotal, 'montantTotal': montantTotal,
'notes': notes, 'notes': notes,
'dateLivraison': dateLivraison?.toIso8601String(), 'dateLivraison': dateLivraison?.toIso8601String(),
'commandeurId': commandeurId,
'validateurId': validateurId,
'remisePourcentage': remisePourcentage,
'remiseMontant': remiseMontant,
'montantApresRemise': montantApresRemise,
}; };
} }
factory Commande.fromMap(Map<String, dynamic> map) { factory Commande.fromMap(Map<String, dynamic> map) {
return Commande( return Commande(
id: map['id'], id: map['id'] as int?,
clientId: map['clientId'], clientId: map['clientId'] as int,
dateCommande: DateTime.parse(map['dateCommande']), dateCommande: Client._parseDateTime(map['dateCommande']),
statut: StatutCommande.values[map['statut']], statut: StatutCommande.values[(map['statut'] as int)],
montantTotal: map['montantTotal'].toDouble(), montantTotal: (map['montantTotal'] as num).toDouble(),
notes: map['notes'], notes: map['notes'] as String?,
dateLivraison: map['dateLivraison'] != null dateLivraison: map['dateLivraison'] != null
? DateTime.parse(map['dateLivraison']) ? Client._parseDateTime(map['dateLivraison'])
: null,
commandeurId: map['commandeurId'] as int?,
validateurId: map['validateurId'] as int?,
clientNom: map['clientNom'] as String?,
clientPrenom: map['clientPrenom'] as String?,
clientEmail: map['clientEmail'] as String?,
remisePourcentage: map['remisePourcentage'] != null
? (map['remisePourcentage'] as num).toDouble()
: null,
remiseMontant: map['remiseMontant'] != null
? (map['remiseMontant'] as num).toDouble()
: null,
montantApresRemise: map['montantApresRemise'] != null
? (map['montantApresRemise'] as num).toDouble()
: null, : null,
clientNom: map['clientNom'],
clientPrenom: map['clientPrenom'],
clientEmail: map['clientEmail'],
); );
} }
String get statutLibelle { Commande copyWith({
switch (statut) { int? id,
case StatutCommande.enAttente: int? clientId,
return 'En attente'; DateTime? dateCommande,
case StatutCommande.confirmee: StatutCommande? statut,
return 'Confirmée'; double? montantTotal,
case StatutCommande.enPreparation: String? notes,
return 'En préparation'; DateTime? dateLivraison,
case StatutCommande.expediee: int? commandeurId,
return 'Expédiée'; int? validateurId,
case StatutCommande.livree: String? clientNom,
return 'Livrée'; String? clientPrenom,
case StatutCommande.annulee: String? clientEmail,
return 'Annulée'; 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,
);
} }
String get clientNomComplet =>
clientPrenom != null && clientNom != null
? '$clientPrenom $clientNom'
: 'Client inconnu';
} }
// Models/detail_commande.dart
class DetailCommande { class DetailCommande {
final int? id; final int? id;
final int commandeId; final int commandeId;
@ -146,11 +221,10 @@ class DetailCommande {
final int quantite; final int quantite;
final double prixUnitaire; final double prixUnitaire;
final double sousTotal; final double sousTotal;
// Données du produit (pour les jointures)
final String? produitNom; final String? produitNom;
final String? produitImage; final String? produitImage;
final String? produitReference; final String? produitReference;
final bool? estCadeau;
DetailCommande({ DetailCommande({
this.id, this.id,
@ -162,6 +236,7 @@ class DetailCommande {
this.produitNom, this.produitNom,
this.produitImage, this.produitImage,
this.produitReference, this.produitReference,
this.estCadeau,
}); });
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@ -172,20 +247,48 @@ class DetailCommande {
'quantite': quantite, 'quantite': quantite,
'prixUnitaire': prixUnitaire, 'prixUnitaire': prixUnitaire,
'sousTotal': sousTotal, 'sousTotal': sousTotal,
'estCadeau': estCadeau == true ? 1 : 0,
}; };
} }
factory DetailCommande.fromMap(Map<String, dynamic> map) { factory DetailCommande.fromMap(Map<String, dynamic> map) {
return DetailCommande( return DetailCommande(
id: map['id'], id: map['id'] as int?,
commandeId: map['commandeId'], commandeId: map['commandeId'] as int,
produitId: map['produitId'], produitId: map['produitId'] as int,
quantite: map['quantite'], quantite: map['quantite'] as int,
prixUnitaire: map['prixUnitaire'].toDouble(), prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
sousTotal: map['sousTotal'].toDouble(), sousTotal: (map['sousTotal'] as num).toDouble(),
produitNom: map['produitNom'], produitNom: map['produitNom'] as String?,
produitImage: map['produitImage'], produitImage: map['produitImage'] as String?,
produitReference: map['produitReference'], produitReference: map['produitReference'] as String?,
estCadeau: map['estCadeau'] == 1,
);
}
DetailCommande copyWith({
int? id,
int? commandeId,
int? produitId,
int? quantite,
double? prixUnitaire,
double? sousTotal,
String? produitNom,
String? produitImage,
String? produitReference,
bool? estCadeau,
}) {
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,
); );
} }
} }

64
lib/Models/Remise.dart

@ -0,0 +1,64 @@
import 'package:youmazgestion/Components/paymentType.dart';
import 'package:youmazgestion/Models/produit.dart';
class Remise {
final RemiseType type;
final double valeur;
final String description;
Remise({
required this.type,
required this.valeur,
this.description = '',
});
double calculerRemise(double montantOriginal) {
switch (type) {
case RemiseType.pourcentage:
return montantOriginal * (valeur / 100);
case RemiseType.fixe:
return valeur;
}
}
String get libelle {
switch (type) {
case RemiseType.pourcentage:
return '$valeur%';
case RemiseType.fixe:
return '${valeur.toStringAsFixed(0)} MGA';
}
}
}
enum RemiseType { pourcentage, fixe }
class ProduitCadeau {
final Product produit;
final String motif;
ProduitCadeau({
required this.produit,
this.motif = 'Cadeau client',
});
}
// Modifiez votre classe PaymentMethod pour inclure la remise
class PaymentMethodEnhanced {
final PaymentType type;
final double amountGiven;
final Remise? remise;
PaymentMethodEnhanced({
required this.type,
this.amountGiven = 0,
this.remise,
});
double calculerMontantFinal(double montantOriginal) {
if (remise != null) {
return montantOriginal - remise!.calculerRemise(montantOriginal);
}
return montantOriginal;
}
}

36
lib/Models/pointage_model.dart

@ -0,0 +1,36 @@
class Pointage {
final int? id;
final String userName;
final String date;
final String heureArrivee;
final String heureDepart;
Pointage({
this.id,
required this.userName,
required this.date,
required this.heureArrivee,
required this.heureDepart,
});
// Pour SQLite
factory Pointage.fromMap(Map<String, dynamic> map) {
return Pointage(
id: map['id'],
userName: map['userName'] ?? '',
date: map['date'],
heureArrivee: map['heureArrivee'],
heureDepart: map['heureDepart'],
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'userName': userName,
'date': date,
'heureArrivee': heureArrivee,
'heureDepart': heureDepart,
};
}
}

155
lib/Models/produit.dart

@ -1,13 +1,22 @@
// Models/product.dart - Version corrigée pour gérer les Blobs
import 'dart:typed_data';
import 'dart:convert';
class Product { class Product {
int? id; final int? id;
final String name; final String name;
final double price; final double price;
final String? image; final String? image;
final String category; final String category;
final int? stock; final int stock;
final String? description; final String? description;
String? qrCode; String? qrCode;
final String? reference; final String? reference;
final int? pointDeVenteId;
final String? marque;
final String? ram;
final String? memoireInterne;
final String? imei;
Product({ Product({
this.id, this.id,
@ -16,44 +25,138 @@ class Product {
this.image, this.image,
required this.category, required this.category,
this.stock = 0, this.stock = 0,
this.description = '', this.description,
this.qrCode, this.qrCode,
this.reference, this.reference,
this.pointDeVenteId,
this.marque,
this.ram,
this.memoireInterne,
this.imei,
}); });
// Vérifie si le stock est défini
bool isStockDefined() { bool isStockDefined() {
if (stock != null) { return stock > 0;
print("stock is defined : $stock $name"); }
return true;
} else { // Méthode helper pour convertir de façon sécurisée
return false; static String? _convertImageFromMap(dynamic imageValue) {
if (imageValue == null) {
return null;
}
// Si c'est déjà une String, on la retourne
if (imageValue is String) {
return imageValue;
} }
// Le driver mysql1 peut retourner un Blob même pour TEXT
// Essayer de le convertir en String
try {
if (imageValue is Uint8List) {
// Convertir les bytes en String UTF-8
return utf8.decode(imageValue);
}
if (imageValue is List<int>) {
// Convertir les bytes en String UTF-8
return utf8.decode(imageValue);
}
// Dernier recours : toString()
return imageValue.toString();
} catch (e) {
print("Erreur conversion image: $e, type: ${imageValue.runtimeType}");
return null;
} }
Map<String, dynamic> toMap() { }
return {
factory Product.fromMap(Map<String, dynamic> map) => Product(
id: map['id'] as int?,
name: map['name'] as String,
price: (map['price'] as num).toDouble(), // Conversion sécurisée
image: _convertImageFromMap(map['image']), // Utilisation de la méthode helper
category: map['category'] as String,
stock: (map['stock'] as int?) ?? 0, // Valeur par défaut
description: map['description'] as String?,
qrCode: map['qrCode'] as String?,
reference: map['reference'] as String?,
pointDeVenteId: map['point_de_vente_id'] as int?,
marque: map['marque'] as String?,
ram: map['ram'] as String?,
memoireInterne: map['memoire_interne'] as String?,
imei: map['imei'] as String?,
);
Map<String, dynamic> toMap() => {
'id': id, 'id': id,
'name': name, 'name': name,
'price': price, 'price': price,
'image': image ?? '', 'image': image,
'category': category, 'category': category,
'stock': stock ?? 0, 'stock': stock,
'description': description ?? '', 'description': description,
'qrCode': qrCode ?? '', 'qrCode': qrCode,
'reference': reference ?? '', 'reference': reference,
'point_de_vente_id': pointDeVenteId,
'marque': marque,
'ram': ram,
'memoire_interne': memoireInterne,
'imei': imei,
}; };
// Méthode pour obtenir l'image comme base64 si nécessaire
String? getImageAsBase64() {
if (image == null) return null;
// Si l'image est déjà en base64, la retourner
if (image!.startsWith('data:') || image!.length > 100) {
return image;
}
// Sinon, c'est probablement un chemin de fichier
return image;
}
// Méthode pour vérifier si l'image est un base64
bool get isImageBase64 {
if (image == null) return false;
return image!.startsWith('data:') ||
(image!.length > 100 && !image!.contains('/') && !image!.contains('\\'));
} }
factory Product.fromMap(Map<String, dynamic> map) { // Copie avec modification
Product copyWith({
int? id,
String? name,
double? price,
String? image,
String? category,
int? stock,
String? description,
String? qrCode,
String? reference,
int? pointDeVenteId,
String? marque,
String? ram,
String? memoireInterne,
String? imei,
}) {
return Product( return Product(
id: map['id'], id: id ?? this.id,
name: map['name'], name: name ?? this.name,
price: map['price'], price: price ?? this.price,
image: map['image'], image: image ?? this.image,
category: map['category'], category: category ?? this.category,
stock: map['stock'], stock: stock ?? this.stock,
description: map['description'], description: description ?? this.description,
qrCode: map['qrCode'], qrCode: qrCode ?? this.qrCode,
reference: map['reference'], reference: reference ?? this.reference,
pointDeVenteId: pointDeVenteId ?? this.pointDeVenteId,
marque: marque ?? this.marque,
ram: ram ?? this.ram,
memoireInterne: memoireInterne ?? this.memoireInterne,
imei: imei ?? this.imei,
); );
} }
} }

26
lib/Models/users.dart

@ -1,3 +1,4 @@
// Models/users.dart - Version corrigée
class Users { class Users {
int? id; int? id;
String name; String name;
@ -6,7 +7,8 @@ class Users {
String password; String password;
String username; String username;
int roleId; int roleId;
String? roleName; // Optionnel, rempli lors des requêtes avec JOIN String? roleName;
int? pointDeVenteId;
Users({ Users({
this.id, this.id,
@ -17,16 +19,18 @@ class Users {
required this.username, required this.username,
required this.roleId, required this.roleId,
this.roleName, this.roleName,
this.pointDeVenteId,
}); });
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'name': name, 'name': name,
'lastname': lastName, 'lastname': lastName, // Correspond à la colonne DB
'email': email, 'email': email,
'password': password, 'password': password,
'username': username, 'username': username,
'role_id': roleId, 'role_id': roleId,
'point_de_vente_id': pointDeVenteId,
}; };
} }
@ -38,17 +42,17 @@ class Users {
factory Users.fromMap(Map<String, dynamic> map) { factory Users.fromMap(Map<String, dynamic> map) {
return Users( return Users(
id: map['id'], id: map['id'] as int?,
name: map['name'], name: map['name'] as String,
lastName: map['lastname'], lastName: map['lastname'] as String, // Correspond à la colonne DB
email: map['email'], email: map['email'] as String,
password: map['password'], password: map['password'] as String,
username: map['username'], username: map['username'] as String,
roleId: map['role_id'], roleId: map['role_id'] as int,
roleName: map['role_name'], // Depuis les requêtes avec JOIN roleName: map['role_name'] as String?, // Depuis les JOINs
pointDeVenteId: map['point_de_vente_id'] as int?,
); );
} }
// Getter pour la compatibilité avec l'ancien code
String get role => roleName ?? ''; String get role => roleName ?? '';
} }

0
lib/Services/GestionStockDatabase.dart

680
lib/Services/app_database.dart

@ -1,680 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import '../Models/users.dart';
import '../Models/role.dart';
import '../Models/Permission.dart';
class AppDatabase {
static final AppDatabase instance = AppDatabase._init();
late Database _database;
AppDatabase._init() {
sqfliteFfiInit();
}
Future<Database> get database async {
if (_database.isOpen) return _database;
_database = await _initDB('app_database.db');
return _database;
}
Future<void> initDatabase() async {
_database = await _initDB('app_database.db');
await _createDB(_database, 1);
await insertDefaultPermissions();
await insertDefaultMenus();
await insertDefaultRoles();
await insertDefaultSuperAdmin();
}
Future<Database> _initDB(String filePath) async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, filePath);
bool dbExists = await File(path).exists();
if (!dbExists) {
try {
ByteData data = await rootBundle.load('assets/database/$filePath');
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await File(path).writeAsBytes(bytes);
} catch (e) {
print('Pas de fichier DB dans assets, création d\'une nouvelle DB');
}
}
return await databaseFactoryFfi.openDatabase(path);
}
Future<void> _createDB(Database db, int version) async {
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
final tableNames = tables.map((row) => row['name'] as String).toList();
if (!tableNames.contains('roles')) {
await db.execute('''
CREATE TABLE roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
designation TEXT NOT NULL UNIQUE
)
''');
print("Table 'roles' créée.");
}
if (!tableNames.contains('permissions')) {
await db.execute('''
CREATE TABLE permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)
''');
print("Table 'permissions' créée.");
}
if (!tableNames.contains('menu')) {
await db.execute('''
CREATE TABLE menu (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
route TEXT NOT NULL UNIQUE
)
''');
print("Table 'menu' créée.");
}
if (!tableNames.contains('role_permissions')) {
await db.execute('''
CREATE TABLE role_permissions (
role_id INTEGER,
permission_id INTEGER,
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
)
''');
print("Table 'role_permissions' créée.");
}
if (!tableNames.contains('menu_permissions')) {
await db.execute('''
CREATE TABLE menu_permissions (
menu_id INTEGER,
permission_id INTEGER,
PRIMARY KEY (menu_id, permission_id),
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
)
''');
print("Table 'menu_permissions' créée.");
}
if (!tableNames.contains('users')) {
await db.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
lastname TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
username TEXT NOT NULL UNIQUE,
role_id INTEGER NOT NULL,
FOREIGN KEY (role_id) REFERENCES roles(id)
)
''');
print("Table 'users' créée.");
}
if (!tableNames.contains('role_menu_permissions')) {
await db.execute('''
CREATE TABLE role_menu_permissions (
role_id INTEGER,
menu_id INTEGER,
permission_id INTEGER,
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
)
''');
print("Table 'role_menu_permissions' créée.");
}
}
Future<void> insertDefaultPermissions() async {
final db = await database;
final existing = await db.query('permissions');
if (existing.isEmpty) {
await db.insert('permissions', {'name': 'view'});
await db.insert('permissions', {'name': 'create'});
await db.insert('permissions', {'name': 'update'});
await db.insert('permissions', {'name': 'delete'});
await db.insert('permissions', {'name': 'admin'});
await db.insert('permissions', {'name': 'manage'}); // Nouvelle permission
await db.insert('permissions', {'name': 'read'}); // Nouvelle permission
print("Permissions par défaut insérées");
} else {
// Vérifier et ajouter les nouvelles permissions si elles n'existent pas
final newPermissions = ['manage', 'read'];
for (var permission in newPermissions) {
final existingPermission = await db.query('permissions', where: 'name = ?', whereArgs: [permission]);
if (existingPermission.isEmpty) {
await db.insert('permissions', {'name': permission});
print("Permission ajoutée: $permission");
}
}
}
}
Future<void> insertDefaultMenus() async {
final db = await database;
final existingMenus = await db.query('menu');
if (existingMenus.isEmpty) {
// Menus existants
await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'});
await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'});
await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'});
await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'});
await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'});
await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'});
await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'});
await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'});
await db.insert('menu', {'name': 'Historique', 'route': '/historique'});
await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'});
// Nouveaux menus ajoutés
await db.insert('menu', {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'});
await db.insert('menu', {'name': 'Gérer les commandes', 'route': '/gerer-commandes'});
print("Menus par défaut insérés");
} else {
// Si des menus existent déjà, vérifier et ajouter les nouveaux menus manquants
await _addMissingMenus(db);
}
}
Future<void> _addMissingMenus(Database db) async {
final menusToAdd = [
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'},
];
for (var menu in menusToAdd) {
final existing = await db.query(
'menu',
where: 'route = ?',
whereArgs: [menu['route']],
);
if (existing.isEmpty) {
await db.insert('menu', menu);
print("Menu ajouté: ${menu['name']}");
}
}
}
Future<void> insertDefaultRoles() async {
final db = await database;
final existingRoles = await db.query('roles');
if (existingRoles.isEmpty) {
int superAdminRoleId = await db.insert('roles', {'designation': 'Super Admin'});
int adminRoleId = await db.insert('roles', {'designation': 'Admin'});
int userRoleId = await db.insert('roles', {'designation': 'User'});
final permissions = await db.query('permissions');
final menus = await db.query('menu');
// Assigner toutes les permissions à tous les menus pour le Super Admin
for (var menu in menus) {
for (var permission in permissions) {
await db.insert('role_menu_permissions', {
'role_id': superAdminRoleId,
'menu_id': menu['id'],
'permission_id': permission['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
}
}
// Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus
await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId);
print("Rôles par défaut créés et permissions assignées");
} else {
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes
await _updateExistingRolePermissions(db);
}
}
// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus
Future<void> _assignBasicPermissionsToRoles(Database db, int adminRoleId, int userRoleId) async {
final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']);
final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']);
final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']);
final managePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['manage']);
// Récupérer les IDs des nouveaux menus
final nouvelleCommandeMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']);
final gererCommandesMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']);
if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) {
// Admin peut créer de nouvelles commandes
await db.insert('role_menu_permissions', {
'role_id': adminRoleId,
'menu_id': nouvelleCommandeMenu.first['id'],
'permission_id': createPermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
// User peut aussi créer de nouvelles commandes
await db.insert('role_menu_permissions', {
'role_id': userRoleId,
'menu_id': nouvelleCommandeMenu.first['id'],
'permission_id': createPermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
}
if (gererCommandesMenu.isNotEmpty && managePermission.isNotEmpty) {
// Admin peut gérer les commandes
await db.insert('role_menu_permissions', {
'role_id': adminRoleId,
'menu_id': gererCommandesMenu.first['id'],
'permission_id': managePermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
}
if (gererCommandesMenu.isNotEmpty && viewPermission.isNotEmpty) {
// User peut voir les commandes
await db.insert('role_menu_permissions', {
'role_id': userRoleId,
'menu_id': gererCommandesMenu.first['id'],
'permission_id': viewPermission.first['id'],
}
, conflictAlgorithm: ConflictAlgorithm.ignore
);
}
}
Future<void> _updateExistingRolePermissions(Database db) async {
final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']);
if (superAdminRole.isNotEmpty) {
final superAdminRoleId = superAdminRole.first['id'] as int;
final permissions = await db.query('permissions');
final menus = await db.query('menu');
// Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus
for (var menu in menus) {
for (var permission in permissions) {
final existingPermission = await db.query(
'role_menu_permissions',
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
whereArgs: [superAdminRoleId, menu['id'], permission['id']],
);
if (existingPermission.isEmpty) {
await db.insert('role_menu_permissions', {
'role_id': superAdminRoleId,
'menu_id': menu['id'],
'permission_id': permission['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
}
}
}
// Assigner les permissions de base aux autres rôles pour les nouveaux menus
final adminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Admin']);
final userRole = await db.query('roles', where: 'designation = ?', whereArgs: ['User']);
if (adminRole.isNotEmpty && userRole.isNotEmpty) {
await _assignBasicPermissionsToRoles(db, adminRole.first['id'] as int, userRole.first['id'] as int);
}
print("Permissions mises à jour pour tous les rôles");
}
}
Future<void> insertDefaultSuperAdmin() async {
final db = await database;
final existingSuperAdmin = await db.rawQuery('''
SELECT u.* FROM users u
INNER JOIN roles r ON u.role_id = r.id
WHERE r.designation = 'Super Admin'
''');
if (existingSuperAdmin.isEmpty) {
final superAdminRole = await db.query('roles',
where: 'designation = ?',
whereArgs: ['Super Admin']
);
if (superAdminRole.isNotEmpty) {
final superAdminRoleId = superAdminRole.first['id'] as int;
await db.insert('users', {
'name': 'Super',
'lastname': 'Admin',
'email': 'superadmin@youmazgestion.com',
'password': 'admin123',
'username': 'superadmin',
'role_id': superAdminRoleId,
});
print("Super Admin créé avec succès !");
print("Username: superadmin");
print("Password: admin123");
print("ATTENTION: Changez ce mot de passe après la première connexion !");
}
} else {
print("Super Admin existe déjà");
}
}
Future<int> createUser(Users user) async {
final db = await database;
return await db.insert('users', user.toMap());
}
Future<int> deleteUser(int id) async {
final db = await database;
return await db.delete('users', where: 'id = ?', whereArgs: [id]);
}
Future<int> updateUser(Users user) async {
final db = await database;
return await db.update('users', user.toMap(), where: 'id = ?', whereArgs: [user.id]);
}
Future<int> getUserCount() async {
final db = await database;
List<Map<String, dynamic>> result = await db.rawQuery('SELECT COUNT(*) as count FROM users');
return result.first['count'] as int;
}
Future<bool> verifyUser(String username, String password) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.id
FROM users
WHERE users.username = ? AND users.password = ?
''', [username, password]);
return result.isNotEmpty;
}
Future<Users> getUser(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.*, roles.designation as role_name
FROM users
INNER JOIN roles ON users.role_id = roles.id
WHERE users.username = ?
''', [username]);
if (result.isNotEmpty) {
return Users.fromMap(result.first);
} else {
throw Exception('User not found');
}
}
Future<Map<String, dynamic>?> getUserCredentials(String username, String password) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.username, users.id, roles.designation as role_name, roles.id as role_id
FROM users
INNER JOIN roles ON users.role_id = roles.id
WHERE username = ? AND password = ?
''', [username, password]);
if (result.isNotEmpty) {
return {
'id': result.first['id'],
'username': result.first['username'] as String,
'role': result.first['role_name'] as String,
'role_id': result.first['role_id'],
};
} else {
return null;
}
}
Future<List<Users>> getAllUsers() async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.*, roles.designation as role_name
FROM users
INNER JOIN roles ON users.role_id = roles.id
ORDER BY users.id ASC
''');
return result.map((json) => Users.fromMap(json)).toList();
}
Future<int> createRole(Role role) async {
final db = await database;
return await db.insert('roles', role.toMap());
}
Future<List<Role>> getRoles() async {
final db = await database;
final maps = await db.query('roles', orderBy: 'designation ASC');
return List.generate(maps.length, (i) => Role.fromMap(maps[i]));
}
Future<int> updateRole(Role role) async {
final db = await database;
return await db.update(
'roles',
role.toMap(),
where: 'id = ?',
whereArgs: [role.id],
);
}
Future<int> deleteRole(int? id) async {
final db = await database;
return await db.delete(
'roles',
where: 'id = ?',
whereArgs: [id],
);
}
Future<List<Permission>> getAllPermissions() async {
final db = await database;
final result = await db.query('permissions', orderBy: 'name ASC');
return result.map((e) => Permission.fromMap(e)).toList();
}
Future<List<Permission>> getPermissionsForRole(int roleId) async {
final db = await database;
final result = await db.rawQuery('''
SELECT p.id, p.name
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = ?
ORDER BY p.name ASC
''', [roleId]);
return result.map((map) => Permission.fromMap(map)).toList();
}
Future<List<Permission>> getPermissionsForUser(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT DISTINCT p.id, p.name
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
JOIN roles r ON rp.role_id = r.id
JOIN users u ON u.role_id = r.id
WHERE u.username = ?
ORDER BY p.name ASC
''', [username]);
return result.map((map) => Permission.fromMap(map)).toList();
}
Future<void> assignPermission(int roleId, int permissionId) async {
final db = await database;
await db.insert('role_permissions', {
'role_id': roleId,
'permission_id': permissionId,
}, conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removePermission(int roleId, int permissionId) async {
final db = await database;
await db.delete(
'role_permissions',
where: 'role_id = ? AND permission_id = ?',
whereArgs: [roleId, permissionId],
);
}
Future<void> assignMenuPermission(int menuId, int permissionId) async {
final db = await database;
await db.insert('menu_permissions', {
'menu_id': menuId,
'permission_id': permissionId,
}, conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removeMenuPermission(int menuId, int permissionId) async {
final db = await database;
await db.delete(
'menu_permissions',
where: 'menu_id = ? AND permission_id = ?',
whereArgs: [menuId, permissionId],
);
}
Future<bool> isSuperAdmin(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT COUNT(*) as count
FROM users u
INNER JOIN roles r ON u.role_id = r.id
WHERE u.username = ? AND r.designation = 'Super Admin'
''', [username]);
return (result.first['count'] as int) > 0;
}
Future<void> changePassword(String username, String oldPassword, String newPassword) async {
final db = await database;
final isValidOldPassword = await verifyUser(username, oldPassword);
if (!isValidOldPassword) {
throw Exception('Ancien mot de passe incorrect');
}
await db.update(
'users',
{'password': newPassword},
where: 'username = ?',
whereArgs: [username],
);
}
Future<bool> hasPermission(String username, String permissionName, String menuRoute) async {
final db = await database;
final result = await db.rawQuery('''
SELECT COUNT(*) as count
FROM permissions p
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id
JOIN roles r ON rmp.role_id = r.id
JOIN users u ON u.role_id = r.id
JOIN menu m ON m.route = ?
WHERE u.username = ? AND p.name = ? AND rmp.menu_id = m.id
''', [menuRoute, username, permissionName]);
return (result.first['count'] as int) > 0;
}
Future<void> close() async {
if (_database.isOpen) {
await _database.close();
}
}
Future<void> printDatabaseInfo() async {
final db = await database;
print("=== INFORMATIONS DE LA BASE DE DONNÉES ===");
final userCount = await getUserCount();
print("Nombre d'utilisateurs: $userCount");
final users = await getAllUsers();
print("Utilisateurs:");
for (var user in users) {
print(" - ${user.username} (${user.name} ) - Email: ${user.email}");
}
final roles = await getRoles();
print("Rôles:");
for (var role in roles) {
print(" - ${role.designation} (ID: ${role.id})");
}
final permissions = await getAllPermissions();
print("Permissions:");
for (var permission in permissions) {
print(" - ${permission.name} (ID: ${permission.id})");
}
print("=========================================");
}
Future<List<Permission>> getPermissionsForRoleAndMenu(int roleId, int menuId) async {
final db = await database;
final result = await db.rawQuery('''
SELECT p.id, p.name
FROM permissions p
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id
WHERE rmp.role_id = ? AND rmp.menu_id = ?
ORDER BY p.name ASC
''', [roleId, menuId]);
return result.map((map) => Permission.fromMap(map)).toList();
}
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue
Future<void> deleteDatabaseFile() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, 'app_database.db');
final file = File(path);
if (await file.exists()) {
await file.delete();
print("Base de données utilisateur supprimée");
}
}
Future<void> assignRoleMenuPermission(int roleId, int menuId, int permissionId) async {
final db = await database;
await db.insert('role_menu_permissions', {
'role_id': roleId,
'menu_id': menuId,
'permission_id': permissionId,
}, conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removeRoleMenuPermission(int roleId, int menuId, int permissionId) async {
final db = await database;
await db.delete(
'role_menu_permissions',
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
whereArgs: [roleId, menuId, permissionId],
);
}
}

559
lib/Services/productDatabase.dart

@ -1,559 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import '../Models/produit.dart';
import '../Models/client.dart';
class ProductDatabase {
static final ProductDatabase instance = ProductDatabase._init();
late Database _database;
ProductDatabase._init() {
sqfliteFfiInit();
}
ProductDatabase();
Future<Database> get database async {
if (_database.isOpen) return _database;
_database = await _initDB('products2.db');
return _database;
}
Future<void> initDatabase() async {
_database = await _initDB('products2.db');
await _createDB(_database, 1);
await _insertDefaultClients();
await _insertDefaultCommandes();
}
Future<Database> _initDB(String filePath) async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, filePath);
bool dbExists = await File(path).exists();
if (!dbExists) {
try {
ByteData data = await rootBundle.load('assets/database/$filePath');
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await File(path).writeAsBytes(bytes);
} catch (e) {
print('Pas de fichier DB dans assets, création nouvelle DB');
}
}
return await databaseFactoryFfi.openDatabase(path);
}
Future<void> _createDB(Database db, int version) async {
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
final tableNames = tables.map((row) => row['name'] as String).toList();
// Table products (existante avec améliorations)
if (!tableNames.contains('products')) {
await db.execute('''
CREATE TABLE products(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
image TEXT,
category TEXT NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
description TEXT,
qrCode TEXT,
reference TEXT UNIQUE
)
''');
print("Table 'products' créée.");
} else {
// Vérifier et ajouter les colonnes manquantes
await _updateProductsTable(db);
}
// Table clients
if (!tableNames.contains('clients')) {
await db.execute('''
CREATE TABLE clients(
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
prenom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
telephone TEXT NOT NULL,
adresse TEXT,
dateCreation TEXT NOT NULL,
actif INTEGER NOT NULL DEFAULT 1
)
''');
print("Table 'clients' créée.");
}
// Table commandes
if (!tableNames.contains('commandes')) {
await db.execute('''
CREATE TABLE commandes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
clientId INTEGER NOT NULL,
dateCommande TEXT NOT NULL,
statut INTEGER NOT NULL DEFAULT 0,
montantTotal REAL NOT NULL,
notes TEXT,
dateLivraison TEXT,
FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE
)
''');
print("Table 'commandes' créée.");
}
// Table détails commandes
if (!tableNames.contains('details_commandes')) {
await db.execute('''
CREATE TABLE details_commandes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
commandeId INTEGER NOT NULL,
produitId INTEGER NOT NULL,
quantite INTEGER NOT NULL,
prixUnitaire REAL NOT NULL,
sousTotal REAL NOT NULL,
FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
FOREIGN KEY (produitId) REFERENCES products(id) ON DELETE CASCADE
)
''');
print("Table 'details_commandes' créée.");
}
// Créer les index pour optimiser les performances
await _createIndexes(db);
}
Future<void> _updateProductsTable(Database db) async {
final columns = await db.rawQuery('PRAGMA table_info(products)');
final columnNames = columns.map((e) => e['name'] as String).toList();
if (!columnNames.contains('description')) {
await db.execute("ALTER TABLE products ADD COLUMN description TEXT");
print("Colonne 'description' ajoutée.");
}
if (!columnNames.contains('qrCode')) {
await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT");
print("Colonne 'qrCode' ajoutée.");
}
if (!columnNames.contains('reference')) {
await db.execute("ALTER TABLE products ADD COLUMN reference TEXT");
print("Colonne 'reference' ajoutée.");
}
}
Future<void> _createIndexes(Database db) async {
await db.execute('CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_products_reference ON products(reference)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_client ON commandes(clientId)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_date ON commandes(dateCommande)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_details_commande ON details_commandes(commandeId)');
print("Index créés pour optimiser les performances.");
}
// =========================
// MÉTHODES PRODUCTS (existantes)
// =========================
Future<int> createProduct(Product product) async {
final db = await database;
return await db.insert('products', product.toMap());
}
Future<List<Product>> getProducts() async {
final db = await database;
final maps = await db.query('products', orderBy: 'name ASC');
return List.generate(maps.length, (i) {
return Product.fromMap(maps[i]);
});
}
Future<int> updateProduct(Product product) async {
final db = await database;
return await db.update(
'products',
product.toMap(),
where: 'id = ?',
whereArgs: [product.id],
);
}
Future<int> deleteProduct(int? id) async {
final db = await database;
return await db.delete(
'products',
where: 'id = ?',
whereArgs: [id],
);
}
Future<List<String>> getCategories() async {
final db = await database;
final result = await db.rawQuery('SELECT DISTINCT category FROM products ORDER BY category');
return List.generate(
result.length, (index) => result[index]['category'] as String);
}
Future<List<Product>> getProductsByCategory(String category) async {
final db = await database;
final maps = await db
.query('products', where: 'category = ?', whereArgs: [category], orderBy: 'name ASC');
return List.generate(maps.length, (i) {
return Product.fromMap(maps[i]);
});
}
Future<int> updateStock(int id, int stock) async {
final db = await database;
return await db
.rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]);
}
Future<Product?> getProductByReference(String reference) async {
final db = await database;
final maps = await db.query(
'products',
where: 'reference = ?',
whereArgs: [reference],
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
}
return null;
}
// =========================
// MÉTHODES CLIENTS
// =========================
Future<int> createClient(Client client) async {
final db = await database;
return await db.insert('clients', client.toMap());
}
Future<List<Client>> getClients() async {
final db = await database;
final maps = await db.query('clients', where: 'actif = 1', orderBy: 'nom ASC, prenom ASC');
return List.generate(maps.length, (i) {
return Client.fromMap(maps[i]);
});
}
Future<Client?> getClientById(int id) async {
final db = await database;
final maps = await db.query('clients', where: 'id = ?', whereArgs: [id]);
if (maps.isNotEmpty) {
return Client.fromMap(maps.first);
}
return null;
}
Future<int> updateClient(Client client) async {
final db = await database;
return await db.update(
'clients',
client.toMap(),
where: 'id = ?',
whereArgs: [client.id],
);
}
Future<int> deleteClient(int id) async {
final db = await database;
// Soft delete
return await db.update(
'clients',
{'actif': 0},
where: 'id = ?',
whereArgs: [id],
);
}
Future<List<Client>> searchClients(String query) async {
final db = await database;
final maps = await db.query(
'clients',
where: 'actif = 1 AND (nom LIKE ? OR prenom LIKE ? OR email LIKE ?)',
whereArgs: ['%$query%', '%$query%', '%$query%'],
orderBy: 'nom ASC, prenom ASC',
);
return List.generate(maps.length, (i) {
return Client.fromMap(maps[i]);
});
}
// =========================
// MÉTHODES COMMANDES
// =========================
Future<int> createCommande(Commande commande) async {
final db = await database;
return await db.insert('commandes', commande.toMap());
}
Future<List<Commande>> getCommandes() async {
final db = await database;
final maps = await db.rawQuery('''
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
FROM commandes c
LEFT JOIN clients cl ON c.clientId = cl.id
ORDER BY c.dateCommande DESC
''');
return List.generate(maps.length, (i) {
return Commande.fromMap(maps[i]);
});
}
Future<List<Commande>> getCommandesByClient(int clientId) async {
final db = await database;
final maps = await db.rawQuery('''
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
FROM commandes c
LEFT JOIN clients cl ON c.clientId = cl.id
WHERE c.clientId = ?
ORDER BY c.dateCommande DESC
''', [clientId]);
return List.generate(maps.length, (i) {
return Commande.fromMap(maps[i]);
});
}
Future<Commande?> getCommandeById(int id) async {
final db = await database;
final maps = await db.rawQuery('''
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
FROM commandes c
LEFT JOIN clients cl ON c.clientId = cl.id
WHERE c.id = ?
''', [id]);
if (maps.isNotEmpty) {
return Commande.fromMap(maps.first);
}
return null;
}
Future<int> updateCommande(Commande commande) async {
final db = await database;
return await db.update(
'commandes',
commande.toMap(),
where: 'id = ?',
whereArgs: [commande.id],
);
}
Future<int> updateStatutCommande(int commandeId, StatutCommande statut) async {
final db = await database;
return await db.update(
'commandes',
{'statut': statut.index},
where: 'id = ?',
whereArgs: [commandeId],
);
}
// =========================
// MÉTHODES DÉTAILS COMMANDES
// =========================
Future<int> createDetailCommande(DetailCommande detail) async {
final db = await database;
return await db.insert('details_commandes', detail.toMap());
}
Future<List<DetailCommande>> getDetailsCommande(int commandeId) async {
final db = await database;
final maps = await db.rawQuery('''
SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference
FROM details_commandes dc
LEFT JOIN products p ON dc.produitId = p.id
WHERE dc.commandeId = ?
ORDER BY dc.id
''', [commandeId]);
return List.generate(maps.length, (i) {
return DetailCommande.fromMap(maps[i]);
});
}
// =========================
// MÉTHODES TRANSACTION COMPLÈTE
// =========================
Future<int> createCommandeComplete(Client client, Commande commande, List<DetailCommande> details) async {
final db = await database;
return await db.transaction((txn) async {
// Créer le client
final clientId = await txn.insert('clients', client.toMap());
// Créer la commande
final commandeMap = commande.toMap();
commandeMap['clientId'] = clientId;
final commandeId = await txn.insert('commandes', commandeMap);
// Créer les détails et mettre à jour le stock
for (var detail in details) {
final detailMap = detail.toMap();
detailMap['commandeId'] = commandeId; // Ajoute l'ID de la commande
await txn.insert('details_commandes', detailMap);
// Mettre à jour le stock du produit
await txn.rawUpdate(
'UPDATE products SET stock = stock - ? WHERE id = ?',
[detail.quantite, detail.produitId],
);
}
return commandeId;
});
}
// =========================
// STATISTIQUES
// =========================
Future<Map<String, dynamic>> getStatistiques() async {
final db = await database;
final totalClients = await db.rawQuery('SELECT COUNT(*) as count FROM clients WHERE actif = 1');
final totalCommandes = await db.rawQuery('SELECT COUNT(*) as count FROM commandes');
final totalProduits = await db.rawQuery('SELECT COUNT(*) as count FROM products');
final chiffreAffaires = await db.rawQuery('SELECT SUM(montantTotal) as total FROM commandes WHERE statut != 5'); // 5 = annulée
return {
'totalClients': totalClients.first['count'],
'totalCommandes': totalCommandes.first['count'],
'totalProduits': totalProduits.first['count'],
'chiffreAffaires': chiffreAffaires.first['total'] ?? 0.0,
};
}
// =========================
// DONNÉES PAR DÉFAUT
// =========================
Future<void> _insertDefaultClients() async {
final db = await database;
final existingClients = await db.query('clients');
if (existingClients.isEmpty) {
final defaultClients = [
Client(
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@email.com',
telephone: '0123456789',
adresse: '123 Rue de la Paix, Paris',
dateCreation: DateTime.now(),
),
Client(
nom: 'Martin',
prenom: 'Marie',
email: 'marie.martin@email.com',
telephone: '0987654321',
adresse: '456 Avenue des Champs, Lyon',
dateCreation: DateTime.now(),
),
Client(
nom: 'Bernard',
prenom: 'Pierre',
email: 'pierre.bernard@email.com',
telephone: '0456789123',
adresse: '789 Boulevard Saint-Michel, Marseille',
dateCreation: DateTime.now(),
),
];
for (var client in defaultClients) {
await db.insert('clients', client.toMap());
}
print("Clients par défaut insérés");
}
}
Future<void> _insertDefaultCommandes() async {
final db = await database;
final existingCommandes = await db.query('commandes');
if (existingCommandes.isEmpty) {
// Récupérer quelques produits pour créer des commandes
final produits = await db.query('products', limit: 3);
final clients = await db.query('clients', limit: 3);
if (produits.isNotEmpty && clients.isNotEmpty) {
// Commande 1
final commande1Id = await db.insert('commandes', {
'clientId': clients[0]['id'],
'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(),
'statut': StatutCommande.livree.index,
'montantTotal': 150.0,
'notes': 'Commande urgente',
});
await db.insert('details_commandes', {
'commandeId': commande1Id,
'produitId': produits[0]['id'],
'quantite': 2,
'prixUnitaire': 75.0,
'sousTotal': 150.0,
});
// Commande 2
final commande2Id = await db.insert('commandes', {
'clientId': clients[1]['id'],
'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(),
'statut': StatutCommande.enPreparation.index,
'montantTotal': 225.0,
'notes': 'Livraison prévue demain',
});
if (produits.length > 1) {
await db.insert('details_commandes', {
'commandeId': commande2Id,
'produitId': produits[1]['id'],
'quantite': 3,
'prixUnitaire': 75.0,
'sousTotal': 225.0,
});
}
// Commande 3
final commande3Id = await db.insert('commandes', {
'clientId': clients[2]['id'],
'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(),
'statut': StatutCommande.confirmee.index,
'montantTotal': 300.0,
'notes': 'Commande standard',
});
if (produits.length > 2) {
await db.insert('details_commandes', {
'commandeId': commande3Id,
'produitId': produits[2]['id'],
'quantite': 4,
'prixUnitaire': 75.0,
'sousTotal': 300.0,
});
}
print("Commandes par défaut insérées");
}
}
}
Future<void> close() async {
if (_database.isOpen) {
await _database.close();
}
}
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue
Future<void> deleteDatabaseFile() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, 'products2.db');
final file = File(path);
if (await file.exists()) {
await file.delete();
print("Base de données product supprimée");
}
}
}

1975
lib/Services/stock_managementDatabase.dart

File diff suppressed because it is too large

1205
lib/Views/Dashboard.dart

File diff suppressed because it is too large

3763
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

7
lib/Views/RoleListPage.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Models/Permission.dart'; //import 'package:youmazgestion/Models/Permission.dart';
import 'package:youmazgestion/Services/app_database.dart'; //import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Models/role.dart'; import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/RolePermissionPage.dart'; import 'package:youmazgestion/Views/RolePermissionPage.dart';
class RoleListPage extends StatefulWidget { class RoleListPage extends StatefulWidget {
@ -47,7 +48,7 @@ class _RoleListPageState extends State<RoleListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const CustomAppBar(title: "Gestion des rôles"), appBar: CustomAppBar(title: "Gestion des rôles"),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(

496
lib/Views/RolePermissionPage.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Models/Permission.dart'; import 'package:youmazgestion/Models/Permission.dart';
import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Models/role.dart'; import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class RolePermissionsPage extends StatefulWidget { class RolePermissionsPage extends StatefulWidget {
final Role role; final Role role;
@ -18,6 +19,8 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
List<Permission> permissions = []; List<Permission> permissions = [];
List<Map<String, dynamic>> menus = []; List<Map<String, dynamic>> menus = [];
Map<int, Map<String, bool>> menuPermissionsMap = {}; Map<int, Map<String, bool>> menuPermissionsMap = {};
bool isLoading = true;
String? errorMessage;
@override @override
void initState() { void initState() {
@ -26,8 +29,14 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
} }
Future<void> _initData() async { Future<void> _initData() async {
try {
setState(() {
isLoading = true;
errorMessage = null;
});
final perms = await db.getAllPermissions(); final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu')); final menuList = await db.getAllMenus(); // Utilise la nouvelle méthode
Map<int, Map<String, bool>> tempMenuPermissionsMap = {}; Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
@ -46,11 +55,20 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
permissions = perms; permissions = perms;
menus = menuList; menus = menuList;
menuPermissionsMap = tempMenuPermissionsMap; menuPermissionsMap = tempMenuPermissionsMap;
isLoading = false;
}); });
} catch (e) {
setState(() {
errorMessage = 'Erreur lors du chargement des données: $e';
isLoading = false;
});
print("Erreur lors de l'initialisation des données: $e");
}
} }
Future<void> _onPermissionToggle( Future<void> _onPermissionToggle(
int menuId, String permission, bool enabled) async { int menuId, String permission, bool enabled) async {
try {
final perm = permissions.firstWhere((p) => p.name == permission); final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) { if (enabled) {
@ -64,61 +82,226 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
setState(() { setState(() {
menuPermissionsMap[menuId]![permission] = enabled; menuPermissionsMap[menuId]![permission] = enabled;
}); });
}
@override // Afficher un message de confirmation
Widget build(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar(
return Scaffold( SnackBar(
appBar: CustomAppBar( content: Text(
title: "Permissions - ${widget.role.designation}", enabled
// showBackButton: true, ? 'Permission "$permission" accordée'
: 'Permission "$permission" révoquée',
), ),
body: Padding( backgroundColor: enabled ? Colors.green : Colors.orange,
duration: const Duration(seconds: 2),
),
);
} catch (e) {
print("Erreur lors de la modification de la permission: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la modification: $e'),
backgroundColor: Colors.red,
),
);
}
}
void _toggleAllPermissions(int menuId, bool enabled) {
for (var permission in permissions) {
_onPermissionToggle(menuId, permission.name, enabled);
}
}
int _getSelectedPermissionsCount(int menuId) {
return menuPermissionsMap[menuId]?.values.where((selected) => selected).length ?? 0;
}
double _getPermissionPercentage(int menuId) {
if (permissions.isEmpty) return 0.0;
return _getSelectedPermissionsCount(menuId) / permissions.length;
}
Widget _buildPermissionSummary() {
int totalPermissions = menus.length * permissions.length;
int selectedPermissions = 0;
for (var menuId in menuPermissionsMap.keys) {
selectedPermissions += _getSelectedPermissionsCount(menuId);
}
double percentage = totalPermissions > 0 ? selectedPermissions / totalPermissions : 0.0;
return Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row(
children: [
Icon(Icons.analytics, color: Colors.blue.shade600),
const SizedBox(width: 8),
Text( Text(
'Gestion des permissions pour le rôle: ${widget.role.designation}', 'Résumé des permissions',
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
), ),
), ),
const SizedBox(height: 10), ],
const Text(
'Sélectionnez les permissions pour chaque menu:',
style: TextStyle(fontSize: 14, color: Colors.grey),
), ),
const SizedBox(height: 20), const SizedBox(height: 12),
if (permissions.isNotEmpty && menus.isNotEmpty) LinearProgressIndicator(
Expanded( value: percentage,
child: ListView.builder( backgroundColor: Colors.grey.shade300,
itemCount: menus.length, valueColor: AlwaysStoppedAnimation<Color>(
itemBuilder: (context, index) { percentage > 0.7 ? Colors.green :
final menu = menus[index]; percentage > 0.3 ? Colors.orange : Colors.red,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$selectedPermissions / $totalPermissions permissions',
style: const TextStyle(fontWeight: FontWeight.w500),
),
Text(
'${(percentage * 100).toStringAsFixed(1)}%',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
],
),
),
);
}
Widget _buildMenuCard(Map<String, dynamic> menu) {
final menuId = menu['id'] as int; final menuId = menu['id'] as int;
final menuName = menu['name'] as String; final menuName = menu['name'] as String;
final menuRoute = menu['route'] as String;
final selectedCount = _getSelectedPermissionsCount(menuId);
final percentage = _getPermissionPercentage(menuId);
return Card( return Card(
margin: const EdgeInsets.only(bottom: 15), margin: const EdgeInsets.only(bottom: 16),
elevation: 3, elevation: 3,
child: Padding( shape: RoundedRectangleBorder(
padding: const EdgeInsets.all(12.0), borderRadius: BorderRadius.circular(12),
child: Column( ),
child: ExpansionTile(
leading: CircleAvatar(
backgroundColor: percentage == 1.0 ? Colors.green :
percentage > 0 ? Colors.orange : Colors.red.shade100,
child: Icon(
Icons.menu,
color: percentage == 1.0 ? Colors.white :
percentage > 0 ? Colors.white : Colors.red,
),
),
title: Text(
menuName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
menuName, menuRoute,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: percentage,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
percentage == 1.0 ? Colors.green :
percentage > 0 ? Colors.orange : Colors.red,
),
),
),
const SizedBox(width: 8),
Text(
'$selectedCount/${permissions.length}',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16), fontSize: 12,
fontWeight: FontWeight.w500,
), ),
const SizedBox(height: 8), ),
],
),
],
),
trailing: PopupMenuButton<String>(
onSelected: (value) {
if (value == 'all') {
_toggleAllPermissions(menuId, true);
} else if (value == 'none') {
_toggleAllPermissions(menuId, false);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'all',
child: Row(
children: [
Icon(Icons.select_all, color: Colors.green),
SizedBox(width: 8),
Text('Tout sélectionner'),
],
),
),
const PopupMenuItem(
value: 'none',
child: Row(
children: [
Icon(Icons.deselect, color: Colors.red),
SizedBox(width: 8),
Text('Tout désélectionner'),
],
),
),
],
child: const Icon(Icons.more_vert),
),
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Permissions disponibles:',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
const SizedBox(height: 12),
Wrap( Wrap(
spacing: 10, spacing: 8,
runSpacing: 10, runSpacing: 8,
children: permissions.map((perm) { children: permissions.map((perm) {
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false; final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
return FilterChip( return CustomFilterChip(
label: perm.name, label: perm.name,
selected: isChecked, selected: isChecked,
onSelected: (bool value) { onSelected: (bool value) {
@ -130,48 +313,275 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
], ],
), ),
), ),
],
),
); );
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: "Permissions - ${widget.role.designation}",
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: errorMessage != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red.shade400,
),
const SizedBox(height: 16),
Text(
'Erreur de chargement',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.red.shade600,
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
errorMessage!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade600),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
),
],
),
)
: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec informations du rôle
Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
CircleAvatar(
backgroundColor: widget.role.designation == 'Super Admin'
? Colors.red.shade100
: Colors.blue.shade100,
radius: 24,
child: Icon(
Icons.person,
color: widget.role.designation == 'Super Admin'
? Colors.red.shade700
: Colors.blue.shade700,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestion des permissions',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Rôle: ${widget.role.designation}',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'Configurez les accès pour chaque menu',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
),
// Résumé des permissions
if (permissions.isNotEmpty && menus.isNotEmpty)
_buildPermissionSummary(),
// Liste des menus et permissions
if (permissions.isNotEmpty && menus.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: menus.length,
itemBuilder: (context, index) {
return _buildMenuCard(menus[index]);
}, },
), ),
) )
else else
const Expanded( Expanded(
child: Center( child: Center(
child: CircularProgressIndicator(), child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucune donnée disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
), ),
), ),
const SizedBox(height: 8),
Text(
'Permissions: ${permissions.length} | Menus: ${menus.length}',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Actualiser'),
),
], ],
), ),
), ),
),
],
),
),
floatingActionButton: !isLoading && errorMessage == null
? FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.save),
label: const Text('Enregistrer'),
backgroundColor: Colors.green,
)
: null,
); );
} }
} }
class FilterChip extends StatelessWidget { class CustomFilterChip extends StatelessWidget {
final String label; final String label;
final bool selected; final bool selected;
final ValueChanged<bool> onSelected; final ValueChanged<bool> onSelected;
const FilterChip({ const CustomFilterChip({
super.key, super.key,
required this.label, required this.label,
required this.selected, required this.selected,
required this.onSelected, required this.onSelected,
}); });
Color _getChipColor(String label) {
switch (label.toLowerCase()) {
case 'view':
case 'read':
return Colors.blue;
case 'create':
return Colors.green;
case 'update':
return Colors.orange;
case 'delete':
return Colors.red;
case 'admin':
return Colors.purple;
case 'manage':
return Colors.indigo;
default:
return Colors.grey;
}
}
IconData _getChipIcon(String label) {
switch (label.toLowerCase()) {
case 'view':
case 'read':
return Icons.visibility;
case 'create':
return Icons.add;
case 'update':
return Icons.edit;
case 'delete':
return Icons.delete;
case 'admin':
return Icons.admin_panel_settings;
case 'manage':
return Icons.settings;
default:
return Icons.security;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChoiceChip( final color = _getChipColor(label);
label: Text(label), final icon = _getChipIcon(label);
return FilterChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: selected ? Colors.white : color,
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
color: selected ? Colors.white : color,
fontWeight: FontWeight.w500,
),
),
],
),
selected: selected, selected: selected,
onSelected: onSelected, onSelected: onSelected,
selectedColor: Colors.blue, selectedColor: color,
labelStyle: TextStyle( backgroundColor: color.withOpacity(0.1),
color: selected ? Colors.white : Colors.black, checkmarkColor: Colors.white,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: selected ? color : color.withOpacity(0.3),
width: 1,
),
), ),
elevation: selected ? 4 : 1,
pressElevation: 8,
); );
} }
} }

2
lib/Views/bilanMois.dart

@ -29,7 +29,7 @@ class _BilanMoisState extends State<BilanMois> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const CustomAppBar(title: 'Bilan du mois'), appBar: CustomAppBar(title: 'Bilan du mois'),
body: Column( body: Column(
children: [ children: [
// Les 3 cartes en haut // Les 3 cartes en haut

1696
lib/Views/commandManagement.dart

File diff suppressed because it is too large

5
lib/Views/editProduct.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Models/produit.dart'; import '../Models/produit.dart';
import '../Services/productDatabase.dart'; //import '../Services/productDatabase.dart';
import 'gestionProduct.dart'; import 'gestionProduct.dart';
class EditProductPage extends StatelessWidget { class EditProductPage extends StatelessWidget {
@ -31,7 +32,7 @@ class EditProductPage extends StatelessWidget {
category: category, category: category,
); );
await ProductDatabase.instance.updateProduct(updatedProduct); await AppDatabase.instance.updateProduct(updatedProduct);
Get.to(GestionProduit()); Get.to(GestionProduit());
} else { } else {

3
lib/Views/editUser.dart

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Models/role.dart'; import 'package:youmazgestion/Models/role.dart';
import '../Services/app_database.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
//import '../Services/app_database.dart';
class EditUserPage extends StatefulWidget { class EditUserPage extends StatefulWidget {
final Users user; final Users user;

7
lib/Views/gestionProduct.dart

@ -1,14 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Components/appDrawer.dart'; import '../Components/appDrawer.dart';
import '../Models/produit.dart'; import '../Models/produit.dart';
import '../Services/productDatabase.dart'; // import '../Services/productDatabase.dart';
import 'editProduct.dart'; import 'editProduct.dart';
import 'dart:io'; import 'dart:io';
class GestionProduit extends StatelessWidget { class GestionProduit extends StatelessWidget {
final ProductDatabase _productDatabase = ProductDatabase.instance; final AppDatabase _productDatabase = AppDatabase.instance;
GestionProduit({super.key}); GestionProduit({super.key});
@ -17,7 +18,7 @@ class GestionProduit extends StatelessWidget {
final screenWidth = MediaQuery.of(context).size.width * 0.8; final screenWidth = MediaQuery.of(context).size.width * 0.8;
return Scaffold( return Scaffold(
appBar: const CustomAppBar(title: 'Gestion des produits'), appBar: CustomAppBar(title: 'Gestion des produits'),
drawer: CustomDrawer(), drawer: CustomDrawer(),
body: FutureBuilder<List<Product>>( body: FutureBuilder<List<Product>>(
future: _productDatabase.getProducts(), future: _productDatabase.getProducts(),

324
lib/Views/gestionRole.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Models/Permission.dart'; import 'package:youmazgestion/Models/Permission.dart';
import 'package:youmazgestion/Services/app_database.dart'; //import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Models/role.dart'; import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class HandleUserRole extends StatefulWidget { class HandleUserRole extends StatefulWidget {
const HandleUserRole({super.key}); const HandleUserRole({super.key});
@ -28,9 +29,12 @@ class _HandleUserRoleState extends State<HandleUserRole> {
} }
Future<void> _initData() async { Future<void> _initData() async {
try {
final roleList = await db.getRoles(); final roleList = await db.getRoles();
final perms = await db.getAllPermissions(); final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu'));
// Récupération mise à jour des menus avec gestion d'erreur
final menuList = await db.getAllMenus();
Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {}; Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {};
@ -55,18 +59,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
menus = menuList; menus = menuList;
roleMenuPermissionsMap = tempRoleMenuPermissionsMap; roleMenuPermissionsMap = tempRoleMenuPermissionsMap;
}); });
} catch (e) {
print("Erreur lors de l'initialisation des données: $e");
// Afficher un message d'erreur à l'utilisateur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du chargement des données: $e'),
backgroundColor: Colors.red,
),
);
}
} }
Future<void> _addRole() async { Future<void> _addRole() async {
String designation = _roleController.text.trim(); String designation = _roleController.text.trim();
if (designation.isEmpty) return; if (designation.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez saisir une désignation pour le rôle'),
backgroundColor: Colors.orange,
),
);
return;
}
try {
// Vérifier si le rôle existe déjà
final existingRoles = roles.where((r) => r.designation.toLowerCase() == designation.toLowerCase());
if (existingRoles.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ce rôle existe déjà'),
backgroundColor: Colors.orange,
),
);
return;
}
await db.createRole(Role(designation: designation)); await db.createRole(Role(designation: designation));
_roleController.clear(); _roleController.clear();
await _initData(); await _initData();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Rôle "$designation" créé avec succès'),
backgroundColor: Colors.green,
),
);
} catch (e) {
print("Erreur lors de la création du rôle: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la création du rôle: $e'),
backgroundColor: Colors.red,
),
);
}
} }
Future<void> _onPermissionToggle(int roleId, int menuId, String permission, bool enabled) async { Future<void> _onPermissionToggle(int roleId, int menuId, String permission, bool enabled) async {
try {
final perm = permissions.firstWhere((p) => p.name == permission); final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) { if (enabled) {
@ -78,12 +130,76 @@ class _HandleUserRoleState extends State<HandleUserRole> {
setState(() { setState(() {
roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled; roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled;
}); });
} catch (e) {
print("Erreur lors de la modification de la permission: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la modification de la permission: $e'),
backgroundColor: Colors.red,
),
);
}
}
Future<void> _deleteRole(Role role) async {
// Empêcher la suppression du Super Admin
if (role.designation == 'Super Admin') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Impossible de supprimer le rôle Super Admin'),
backgroundColor: Colors.red,
),
);
return;
}
// Demander confirmation
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text('Êtes-vous sûr de vouloir supprimer le rôle "${role.designation}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirm == true) {
try {
await db.deleteRole(role.id);
await _initData();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Rôle "${role.designation}" supprimé avec succès'),
backgroundColor: Colors.green,
),
);
} catch (e) {
print("Erreur lors de la suppression du rôle: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la suppression du rôle: $e'),
backgroundColor: Colors.red,
),
);
}
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const CustomAppBar(title: "Gestion des rôles"), appBar: CustomAppBar(title: "Gestion des rôles"),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@ -103,28 +219,52 @@ class _HandleUserRoleState extends State<HandleUserRole> {
controller: _roleController, controller: _roleController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Nouveau rôle', labelText: 'Nouveau rôle',
hintText: 'Ex: Manager, Vendeur...',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
onSubmitted: (_) => _addRole(),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
ElevatedButton( ElevatedButton.icon(
onPressed: _addRole, onPressed: _addRole,
icon: const Icon(Icons.add),
label: const Text('Ajouter'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
child: const Text('Ajouter'),
), ),
], ],
), ),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Affichage des statistiques
if (roles.isNotEmpty)
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('Rôles', roles.length.toString(), Icons.people),
_buildStatItem('Permissions', permissions.length.toString(), Icons.security),
_buildStatItem('Menus', menus.length.toString(), Icons.menu),
],
),
),
),
const SizedBox(height: 20),
// Tableau des rôles et permissions // Tableau des rôles et permissions
if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty) if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty)
Expanded( Expanded(
@ -136,22 +276,64 @@ class _HandleUserRoleState extends State<HandleUserRole> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.vertical,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width - 32,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: menus.map((menu) { children: menus.map((menu) {
final menuId = menu['id'] as int; final menuId = menu['id'] as int;
return Column( final menuName = menu['name'] as String;
final menuRoute = menu['route'] as String;
return Card(
margin: const EdgeInsets.only(bottom: 16.0),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.menu, color: Colors.blue.shade700),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
menu['name'], menuName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.blue.shade700,
),
),
Text(
menuRoute,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
), ),
DataTable( ),
],
),
),
],
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 20, columnSpacing: 20,
headingRowHeight: 50,
dataRowHeight: 60,
columns: [ columns: [
const DataColumn( const DataColumn(
label: Text( label: Text(
@ -160,17 +342,49 @@ class _HandleUserRoleState extends State<HandleUserRole> {
), ),
), ),
...permissions.map((perm) => DataColumn( ...permissions.map((perm) => DataColumn(
label: Text( label: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
perm.name, perm.name,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
),
)).toList(), )).toList(),
const DataColumn(
label: Text(
'Actions',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
], ],
rows: roles.map((role) { rows: roles.map((role) {
final roleId = role.id!; final roleId = role.id!;
return DataRow( return DataRow(
cells: [ cells: [
DataCell(Text(role.designation)), DataCell(
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: role.designation == 'Super Admin'
? Colors.red.shade50
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(4),
),
child: Text(
role.designation,
style: TextStyle(
fontWeight: FontWeight.w500,
color: role.designation == 'Super Admin'
? Colors.red.shade700
: Colors.blue.shade700,
),
),
),
),
...permissions.map((perm) { ...permissions.map((perm) {
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false; final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
return DataCell( return DataCell(
@ -179,26 +393,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
onChanged: (bool? value) { onChanged: (bool? value) {
_onPermissionToggle(roleId, menuId, perm.name, value ?? false); _onPermissionToggle(roleId, menuId, perm.name, value ?? false);
}, },
activeColor: Colors.green,
), ),
); );
}).toList(), }).toList(),
DataCell(
role.designation != 'Super Admin'
? IconButton(
icon: Icon(Icons.delete, color: Colors.red.shade600),
tooltip: 'Supprimer le rôle',
onPressed: () => _deleteRole(role),
)
: Icon(Icons.lock, color: Colors.grey.shade400),
),
], ],
); );
}).toList(), }).toList(),
), ),
),
], ],
),
),
); );
}).toList(), }).toList(),
), ),
), ),
), ),
), ),
),
) )
else else
const Expanded( Expanded(
child: Center( child: Center(
child: Text('Aucun rôle, permission ou menu trouvé'), child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey.shade400),
const SizedBox(height: 16),
Text(
'Aucune donnée disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Rôles: ${roles.length} | Permissions: ${permissions.length} | Menus: ${menus.length}',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Actualiser'),
),
],
),
), ),
), ),
], ],
@ -206,4 +460,34 @@ class _HandleUserRoleState extends State<HandleUserRole> {
), ),
); );
} }
Widget _buildStatItem(String label, String value, IconData icon) {
return Column(
children: [
Icon(icon, size: 32, color: Colors.blue.shade600),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
);
}
@override
void dispose() {
_roleController.dispose();
super.dispose();
}
} }

7
lib/Views/gestionStock.dart

@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_core/src/get_main.dart';
import 'package:youmazgestion/Models/produit.dart'; import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/productDatabase.dart'; //import 'package:youmazgestion/Services/productDatabase.dart';
import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class GestionStockPage extends StatefulWidget { class GestionStockPage extends StatefulWidget {
const GestionStockPage({super.key}); const GestionStockPage({super.key});
@ -14,7 +15,7 @@ class GestionStockPage extends StatefulWidget {
} }
class _GestionStockPageState extends State<GestionStockPage> { class _GestionStockPageState extends State<GestionStockPage> {
final ProductDatabase _database = ProductDatabase.instance; final AppDatabase _database = AppDatabase.instance;
List<Product> _products = []; List<Product> _products = [];
List<Product> _filteredProducts = []; List<Product> _filteredProducts = [];
String? _selectedCategory; String? _selectedCategory;
@ -79,7 +80,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const CustomAppBar(title: 'Gestion des Stocks'), appBar: CustomAppBar(title: 'Gestion des Stocks'),
drawer: CustomDrawer(), drawer: CustomDrawer(),
body: Column( body: Column(
children: [ children: [

416
lib/Views/gestion_point_de_vente.dart

@ -0,0 +1,416 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class AjoutPointDeVentePage extends StatefulWidget {
const AjoutPointDeVentePage({super.key});
@override
_AjoutPointDeVentePageState createState() => _AjoutPointDeVentePageState();
}
class _AjoutPointDeVentePageState extends State<AjoutPointDeVentePage> {
final AppDatabase _appDatabase = AppDatabase.instance;
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
// Contrôleurs
final TextEditingController _nomController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
// Liste des points de vente
List<Map<String, dynamic>> _pointsDeVente = [];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadPointsDeVente();
_searchController.addListener(_filterPointsDeVente);
}
Future<void> _loadPointsDeVente() async {
setState(() {
_isLoading = true;
});
try {
final points = await _appDatabase.getPointsDeVente();
setState(() {
_pointsDeVente = points;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Impossible de charger les points de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
void _filterPointsDeVente() {
final query = _searchController.text.toLowerCase();
if (query.isEmpty) {
_loadPointsDeVente();
return;
}
setState(() {
_pointsDeVente = _pointsDeVente.where((point) {
final nom = point['nom']?.toString().toLowerCase() ?? '';
return nom.contains(query);
}).toList();
});
}
Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
await _appDatabase.createPointDeVente(
_nomController.text.trim(),
_codeController.text.trim(),
);
// Réinitialiser le formulaire
_nomController.clear();
_codeController.clear();
// Recharger la liste
await _loadPointsDeVente();
Get.snackbar(
'Succès',
'Point de vente ajouté avec succès',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible d\'ajouter le point de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _deletePointDeVente(int id) async {
final confirmed = await Get.dialog<bool>(
AlertDialog(
title: const Text('Confirmer la suppression'),
content: const Text('Voulez-vous vraiment supprimer ce point de vente ?'),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Get.back(result: true),
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
setState(() {
_isLoading = true;
});
try {
await _appDatabase.deletePointDeVente(id);
await _loadPointsDeVente();
Get.snackbar(
'Succès',
'Point de vente supprimé avec succès',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de supprimer le point de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Gestion des points de vente'),
drawer: CustomDrawer(),
body: Column(
children: [
// Formulaire d'ajout
Card(
margin: const EdgeInsets.all(16),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Ajouter un point de vente',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
const SizedBox(height: 16),
// Champ Nom
TextFormField(
controller: _nomController,
decoration: InputDecoration(
labelText: 'Nom du point de vente',
prefixIcon: const Icon(Icons.store),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un nom';
}
return null;
},
),
const SizedBox(height: 12),
// Champ Code
TextFormField(
controller: _codeController,
decoration: InputDecoration(
labelText: 'Code (optionnel)',
prefixIcon: const Icon(Icons.code),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 16),
// Bouton de soumission
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Ajouter le point de vente',
style: TextStyle(fontSize: 16),
),
),
],
),
),
),
),
// Liste des points de vente
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
// Barre de recherche
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Rechercher un point de vente',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_loadPointsDeVente();
},
)
: null,
),
),
),
// En-tête de liste
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
const Expanded(
flex: 2,
child: Text(
'Nom',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
),
const Expanded(
child: Text(
'Code',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
),
SizedBox(
width: 40,
child: Text(
'Actions',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
),
],
),
),
// Liste
Expanded(
child: _isLoading && _pointsDeVente.isEmpty
? const Center(child: CircularProgressIndicator())
: _pointsDeVente.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.store_mall_directory_outlined,
size: 60, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucun point de vente trouvé',
style: TextStyle(
fontSize: 16,
color: Colors.grey),
),
],
),
)
: ListView.builder(
itemCount: _pointsDeVente.length,
itemBuilder: (context, index) {
final point = _pointsDeVente[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
flex: 2,
child: Text(
point['nom'] ?? 'N/A',
style: const TextStyle(
fontWeight: FontWeight.w500),
),
),
Expanded(
child: Text(
point['code'] ?? 'N/A',
style: TextStyle(
color: Colors.grey.shade600),
),
),
SizedBox(
width: 40,
child: IconButton(
icon: const Icon(Icons.delete,
size: 20, color: Colors.red),
onPressed: () => _deletePointDeVente(point['id']),
),
),
],
),
),
);
},
),
),
],
),
),
),
],
),
);
}
@override
void dispose() {
_nomController.dispose();
_codeController.dispose();
_searchController.dispose();
super.dispose();
}
}

939
lib/Views/historique.dart

@ -1,97 +1,639 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../Components/appDrawer.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import '../controller/HistoryController.dart'; import 'package:youmazgestion/Components/appDrawer.dart';
import 'listCommandeHistory.dart'; import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class HistoriquePage extends StatefulWidget {
const HistoriquePage({super.key});
class HistoryPage extends GetView<HistoryController> {
@override @override
HistoryController controller = Get.put(HistoryController()); _HistoriquePageState createState() => _HistoriquePageState();
}
class _HistoriquePageState extends State<HistoriquePage> {
final AppDatabase _appDatabase = AppDatabase.instance;
HistoryPage({super.key}); // Listes pour les commandes
final List<Commande> _commandes = [];
final List<Commande> _filteredCommandes = [];
bool _isLoading = true;
DateTimeRange? _dateRange;
// Contrôleurs pour les filtres
final TextEditingController _searchController = TextEditingController();
final TextEditingController _searchClientController = TextEditingController();
final TextEditingController _searchCommandeIdController = TextEditingController();
// Variables de filtre
StatutCommande? _selectedStatut;
bool _showOnlyToday = false;
double? _minAmount;
double? _maxAmount;
@override @override
Widget build(BuildContext context) { void initState() {
return Scaffold( super.initState();
appBar: const CustomAppBar(title: 'Historique'), _loadCommandes();
drawer: CustomDrawer(),
body: Column( // Listeners pour les filtres
children: [ _searchController.addListener(_filterCommandes);
Padding( _searchClientController.addListener(_filterCommandes);
_searchCommandeIdController.addListener(_filterCommandes);
}
Future<void> _loadCommandes() async {
setState(() {
_isLoading = true;
});
try {
final commandes = await _appDatabase.getCommandes();
setState(() {
_commandes.clear();
_commandes.addAll(commandes);
_filteredCommandes.clear();
_filteredCommandes.addAll(commandes);
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Impossible de charger les commandes: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
Future<void> _selectDateRange(BuildContext context) async {
final DateTimeRange? picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime.now().add(const Duration(days: 365)),
initialDateRange: _dateRange ?? DateTimeRange(
start: DateTime.now().subtract(const Duration(days: 30)),
end: DateTime.now(),
),
);
if (picked != null) {
setState(() {
_dateRange = picked;
});
_filterCommandes();
}
}
// Méthode pour filtrer les commandes
void _filterCommandes() {
final searchText = _searchController.text.toLowerCase();
final clientQuery = _searchClientController.text.toLowerCase();
final commandeIdQuery = _searchCommandeIdController.text.toLowerCase();
setState(() {
_filteredCommandes.clear();
for (var commande in _commandes) {
bool matchesSearch = searchText.isEmpty ||
commande.clientNom!.toLowerCase().contains(searchText) ||
commande.clientPrenom!.toLowerCase().contains(searchText) ||
commande.id.toString().contains(searchText);
bool matchesClient = clientQuery.isEmpty ||
commande.clientNom!.toLowerCase().contains(clientQuery) ||
commande.clientPrenom!.toLowerCase().contains(clientQuery);
bool matchesCommandeId = commandeIdQuery.isEmpty ||
commande.id.toString().contains(commandeIdQuery);
bool matchesStatut = _selectedStatut == null ||
commande.statut == _selectedStatut;
bool matchesDate = true;
if (_dateRange != null) {
final date = commande.dateCommande;
matchesDate = date.isAfter(_dateRange!.start) &&
date.isBefore(_dateRange!.end.add(const Duration(days: 1)));
}
bool matchesToday = !_showOnlyToday ||
_isToday(commande.dateCommande);
bool matchesAmount = true;
if (_minAmount != null && commande.montantTotal < _minAmount!) {
matchesAmount = false;
}
if (_maxAmount != null && commande.montantTotal > _maxAmount!) {
matchesAmount = false;
}
if (matchesSearch && matchesClient && matchesCommandeId &&
matchesStatut && matchesDate && matchesToday && matchesAmount) {
_filteredCommandes.add(commande);
}
}
});
}
bool _isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year &&
date.month == now.month &&
date.day == now.day;
}
// Toggle filtre aujourd'hui
void _toggleTodayFilter() {
setState(() {
_showOnlyToday = !_showOnlyToday;
});
_filterCommandes();
}
// Réinitialiser les filtres
void _clearFilters() {
setState(() {
_searchController.clear();
_searchClientController.clear();
_searchCommandeIdController.clear();
_selectedStatut = null;
_dateRange = null;
_showOnlyToday = false;
_minAmount = null;
_maxAmount = null;
});
_filterCommandes();
}
// Widget pour la section des filtres (adapté pour mobile)
Widget _buildFilterSection() {
final isMobile = MediaQuery.of(context).size.width < 600;
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: ElevatedButton( child: Column(
onPressed: () { crossAxisAlignment: CrossAxisAlignment.start,
controller.refreshOrders(); children: [
controller.onInit(); Row(
children: [
Icon(Icons.filter_list, color: Colors.blue.shade700),
const SizedBox(width: 8),
const Text(
'Filtres',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
const Spacer(),
TextButton.icon(
onPressed: _clearFilters,
icon: const Icon(Icons.clear, size: 18),
label: isMobile ? const SizedBox() : const Text('Réinitialiser'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 16),
// Champ de recherche générale
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Recherche',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 12),
if (!isMobile) ...[
// Version desktop - champs sur la même ligne
Row(
children: [
Expanded(
child: TextField(
controller: _searchClientController,
decoration: InputDecoration(
labelText: 'Client',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _searchCommandeIdController,
decoration: InputDecoration(
labelText: 'ID Commande',
prefixIcon: const Icon(Icons.confirmation_number),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: TextInputType.number,
),
),
],
),
] else ...[
// Version mobile - champs empilés
TextField(
controller: _searchClientController,
decoration: InputDecoration(
labelText: 'Client',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 12),
TextField(
controller: _searchCommandeIdController,
decoration: InputDecoration(
labelText: 'ID Commande',
prefixIcon: const Icon(Icons.confirmation_number),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: TextInputType.number,
),
],
const SizedBox(height: 12),
// Dropdown pour le statut
DropdownButtonFormField<StatutCommande>(
value: _selectedStatut,
decoration: InputDecoration(
labelText: 'Statut',
prefixIcon: const Icon(Icons.assignment),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
items: [
const DropdownMenuItem<StatutCommande>(
value: null,
child: Text('Tous les statuts'),
),
...StatutCommande.values.map((StatutCommande statut) {
return DropdownMenuItem<StatutCommande>(
value: statut,
child: Text(_getStatutText(statut)),
);
}).toList(),
],
onChanged: (StatutCommande? newValue) {
setState(() {
_selectedStatut = newValue;
});
_filterCommandes();
}, },
),
const SizedBox(height: 16),
// Boutons de filtre rapide - adaptés pour mobile
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _toggleTodayFilter,
icon: Icon(
_showOnlyToday ? Icons.today : Icons.calendar_today,
size: 20,
),
label: Text(_showOnlyToday
? isMobile ? 'Toutes dates' : 'Toutes les dates'
: isMobile ? 'Aujourd\'hui' : 'Aujourd\'hui seulement'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepOrangeAccent, backgroundColor: _showOnlyToday
shape: RoundedRectangleBorder( ? Colors.green.shade600
borderRadius: BorderRadius.circular(20.0), : Colors.blue.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 12 : 16,
vertical: 8
),
),
),
ElevatedButton.icon(
onPressed: () => _selectDateRange(context),
icon: const Icon(Icons.date_range, size: 20),
label: Text(_dateRange != null
? isMobile ? 'Période' : 'Période sélectionnée'
: isMobile ? 'Période' : 'Choisir période'),
style: ElevatedButton.styleFrom(
backgroundColor: _dateRange != null
? Colors.orange.shade600
: Colors.grey.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 12 : 16,
vertical: 8
),
),
),
],
), ),
if (_dateRange != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
), ),
child: const Text( child: Row(
'Rafraîchir', mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.date_range,
size: 16,
color: Colors.orange.shade700),
const SizedBox(width: 4),
Text(
'${DateFormat('dd/MM/yyyy').format(_dateRange!.start)} - ${DateFormat('dd/MM/yyyy').format(_dateRange!.end)}',
style: TextStyle( style: TextStyle(
color: Colors.white, fontSize: isMobile ? 10 : 12,
fontSize: 16.0, color: Colors.orange.shade700,
fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(width: 4),
GestureDetector(
onTap: () {
setState(() {
_dateRange = null;
});
_filterCommandes();
},
child: Icon(Icons.close,
size: 16,
color: Colors.orange.shade700),
),
],
),
), ),
), ),
Expanded(
child: Obx(
() {
final distinctDates = controller.workDays;
if (distinctDates.isEmpty) { const SizedBox(height: 8),
return const Center(
// Compteur de résultats
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8
),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Text( child: Text(
'Aucune journée de travail trouvée', '${_filteredCommandes.length} commande(s)',
style: TextStyle( style: TextStyle(
fontSize: 18.0, color: Colors.blue.shade700,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
fontSize: isMobile ? 12 : 14,
),
),
),
],
), ),
), ),
); );
} }
return ListView.builder( void _showCommandeDetails(Commande commande) async {
itemCount: distinctDates.length, final details = await _appDatabase.getDetailsCommande(commande.id!);
itemBuilder: (context, index) { final client = await _appDatabase.getClientById(commande.clientId);
final date = distinctDates[index];
return Card( Get.bottomSheet(
elevation: 2.0, Container(
margin: const EdgeInsets.symmetric( padding: const EdgeInsets.all(16),
horizontal: 16.0, height: MediaQuery.of(context).size.height * 0.85, // Plus grand sur mobile
vertical: 8.0, decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
), ),
child: ListTile( child: Column(
title: Text( crossAxisAlignment: CrossAxisAlignment.stretch,
'Journée du $date', children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.receipt_long, color: Colors.blue.shade700),
),
const SizedBox(width: 12),
Text(
'Commande #${commande.id}',
style: const TextStyle( style: const TextStyle(
fontSize: 18, // Taille réduite pour mobile
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
leading: const CircleAvatar( ],
backgroundColor: Colors.deepOrange, ),
child: Icon( IconButton(
Icons.calendar_today, icon: const Icon(Icons.close),
color: Colors.white, onPressed: () => Get.back(),
),
],
), ),
const Divider(),
// Informations de la commande - version compacte pour mobile
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('Client', '${client?.nom} ${client?.prenom}', Icons.person),
_buildDetailRow('Date', DateFormat('dd/MM/yyyy à HH:mm').format(commande.dateCommande), Icons.calendar_today),
Row(
children: [
Icon(Icons.assignment, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
const Text('Statut: ', style: TextStyle(fontWeight: FontWeight.w500)),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatutColor(commande.statut).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getStatutText(commande.statut),
style: TextStyle(
color: _getStatutColor(commande.statut),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
Text(
'${commande.montantTotal.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
],
),
),
const SizedBox(height: 12),
const Text(
'Articles:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Expanded(
child: details.isEmpty
? const Center(
child: Text('Aucun détail disponible'),
)
: ListView.builder(
itemCount: details.length,
itemBuilder: (context, index) {
final detail = details[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
elevation: 1,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.shopping_bag, size: 20),
),
title: Text(
detail.produitNom ?? 'Produit inconnu',
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
'${detail.quantite} x ${detail.prixUnitaire.toStringAsFixed(2)} MGA',
),
trailing: Text(
'${detail.sousTotal.toStringAsFixed(2)} MGA',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
), ),
trailing: const Icon(
Icons.arrow_forward,
color: Colors.deepOrange,
), ),
onTap: () => navigateToDetailPage(date),
), ),
); );
}, },
),
),
if (commande.statut == StatutCommande.enAttente)
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14), // Plus compact
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () => _updateStatutCommande(commande.id!),
child: const Text('Marquer comme confirmé'),
),
),
],
),
),
isScrollControlled: true,
); );
}, }
Widget _buildDetailRow(String label, String value, IconData icon) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
Text('$label: ', style: const TextStyle(fontWeight: FontWeight.w500)),
Expanded(
child: Text(
value,
overflow: TextOverflow.ellipsis,
maxLines: 1,
), ),
), ),
], ],
@ -99,25 +641,282 @@ class HistoryPage extends GetView<HistoryController> {
); );
} }
String formatDate(String date) { String _getStatutText(StatutCommande statut) {
switch (statut) {
case StatutCommande.enAttente:
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.annulee:
return 'Annulée';
default:
return 'Inconnu';
}
}
Color _getStatutColor(StatutCommande statut) {
switch (statut) {
case StatutCommande.enAttente:
return Colors.orange;
case StatutCommande.confirmee:
return Colors.green;
case StatutCommande.annulee:
return Colors.red;
default:
return Colors.grey;
}
}
Future<void> _updateStatutCommande(int commandeId) async {
try { try {
final parsedDate = DateFormat('dd-MM-yyyy').parse(date); await _appDatabase.updateStatutCommande(
print('parsedDate1: $parsedDate'); commandeId, StatutCommande.confirmee);
final formattedDate = DateFormat('yyyy-MM-dd').format(parsedDate); Get.back(); // Ferme le bottom sheet
print('formattedDate1: $formattedDate'); _loadCommandes();
return formattedDate; Get.snackbar(
'Succès',
'Statut de la commande mis à jour',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) { } catch (e) {
print('Error parsing date: $date'); Get.snackbar(
return ''; 'Erreur',
'Impossible de mettre à jour le statut: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} }
} }
// transformer string en DateTime // Widget pour l'état vide
void navigateToDetailPage(String selectedDate) { Widget _buildEmptyState() {
print('selectedDate: $selectedDate'); return Center(
DateTime parsedDate = DateFormat('yyyy-MM-dd').parse(selectedDate); child: Padding(
print('parsedDate: $parsedDate'); padding: const EdgeInsets.all(32.0),
child: Column(
children: [
Icon(
Icons.receipt_long_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucune commande trouvée',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'Modifiez vos critères de recherche',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
),
);
}
Get.to(() => HistoryDetailPage(selectedDate: parsedDate)); // Widget pour l'item de commande (adapté pour mobile)
Widget _buildCommandeListItem(Commande commande) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => _showCommandeDetails(commande),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Container(
width: isMobile ? 40 : 50,
height: isMobile ? 40 : 50,
decoration: BoxDecoration(
color: _getStatutColor(commande.statut).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.shopping_cart,
size: isMobile ? 20 : 24,
color: _getStatutColor(commande.statut),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Commande #${commande.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
'${commande.clientNom} ${commande.clientPrenom}',
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
Text(
DateFormat('dd/MM/yyyy').format(commande.dateCommande),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${commande.montantTotal.toStringAsFixed(2)} MGA',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green.shade700,
fontSize: isMobile ? 14 : 16,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatutColor(commande.statut).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getStatutText(commande.statut),
style: TextStyle(
color: _getStatutColor(commande.statut),
fontWeight: FontWeight.bold,
fontSize: isMobile ? 10 : 12,
),
),
),
],
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
appBar: CustomAppBar(
title: 'Historique',
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadCommandes,
),
],
),
drawer: CustomDrawer(),
body: Column(
children: [
// Section des filtres - toujours visible mais plus compacte sur mobile
if (!isMobile) _buildFilterSection(),
// Sur mobile, on ajoute un bouton pour afficher les filtres dans un modal
if (isMobile) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: ElevatedButton.icon(
icon: const Icon(Icons.filter_alt),
label: const Text('Filtres'),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom),
child: _buildFilterSection(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
// Compteur de résultats visible en haut sur mobile
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${_filteredCommandes.length} commande(s)',
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w600,
),
),
),
),
],
// Liste des commandes
Expanded(
child: _isLoading
? const Center(
child: CircularProgressIndicator(),
)
: _filteredCommandes.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _filteredCommandes.length,
itemBuilder: (context, index) {
final commande = _filteredCommandes[index];
return _buildCommandeListItem(commande);
},
),
),
],
),
);
}
@override
void dispose() {
_searchController.dispose();
_searchClientController.dispose();
_searchCommandeIdController.dispose();
super.dispose();
} }
} }

2
lib/Views/listCommandeHistory.dart

@ -32,7 +32,7 @@ class HistoryDetailPage extends StatelessWidget {
init: controller, init: controller,
builder: (controller) { builder: (controller) {
return Scaffold( return Scaffold(
appBar: const CustomAppBar(title: 'Historique de la journée'), appBar: CustomAppBar(title: 'Historique de la journée'),
body: Column( body: Column(
children: [ children: [
Padding( Padding(

5
lib/Views/listUser.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Components/app_bar.dart'; import '../Components/app_bar.dart';
import '../Services/app_database.dart'; //import '../Services/app_database.dart';
import 'editUser.dart'; import 'editUser.dart';
class ListUserPage extends StatefulWidget { class ListUserPage extends StatefulWidget {
@ -35,7 +36,7 @@ class _ListUserPageState extends State<ListUserPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const CustomAppBar(title: 'Liste des utilisateurs'), appBar: CustomAppBar(title: 'Liste des utilisateurs'),
body: ListView.builder( body: ListView.builder(
itemCount: userList.length, itemCount: userList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {

210
lib/Views/loginPage.dart

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/Views/mobilepage.dart';
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground; import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/Services/app_database.dart';
import '../Models/users.dart'; import '../Models/users.dart';
import '../controller/userController.dart'; import '../controller/userController.dart';
@ -34,19 +34,7 @@ class _LoginPageState extends State<LoginPage> {
void checkUserCount() async { void checkUserCount() async {
try { try {
final userCount = await AppDatabase.instance.getUserCount(); final userCount = await AppDatabase.instance.getUserCount();
print('Nombre d\'utilisateurs trouvés: $userCount'); // Debug print('Nombre d\'utilisateurs trouvés: $userCount');
// Commentez cette partie pour permettre le login même sans utilisateurs
/*
if (userCount == 0) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
);
}
}
*/
} catch (error) { } catch (error) {
print('Erreur lors de la vérification du nombre d\'utilisateurs: $error'); print('Erreur lors de la vérification du nombre d\'utilisateurs: $error');
setState(() { setState(() {
@ -65,11 +53,13 @@ class _LoginPageState extends State<LoginPage> {
Future<void> saveUserData(Users user, String role, int userId) async { Future<void> saveUserData(Users user, String role, int userId) async {
try { try {
// CORRECTION : Utiliser la nouvelle méthode du contrôleur
// Le contrôleur se charge maintenant de tout (observable + SharedPreferences)
userController.setUserWithCredentials(user, role, userId); userController.setUserWithCredentials(user, role, userId);
print('Utilisateur sauvegardé: ${user.username}, rôle: $role, id: $userId'); if (user.pointDeVenteId != null) {
await userController.loadPointDeVenteDesignation();
}
print('Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}');
} catch (error) { } catch (error) {
print('Erreur lors de la sauvegarde: $error'); print('Erreur lors de la sauvegarde: $error');
throw Exception('Erreur lors de la sauvegarde des données utilisateur'); throw Exception('Erreur lors de la sauvegarde des données utilisateur');
@ -82,10 +72,10 @@ class _LoginPageState extends State<LoginPage> {
final String username = _usernameController.text.trim(); final String username = _usernameController.text.trim();
final String password = _passwordController.text.trim(); final String password = _passwordController.text.trim();
// Validation basique
if (username.isEmpty || password.isEmpty) { if (username.isEmpty || password.isEmpty) {
setState(() { setState(() {
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe'; _errorMessage =
'Veuillez saisir le nom d\'utilisateur et le mot de passe';
_isErrorVisible = true; _isErrorVisible = true;
}); });
return; return;
@ -98,11 +88,8 @@ class _LoginPageState extends State<LoginPage> {
try { try {
print('Tentative de connexion pour: $username'); print('Tentative de connexion pour: $username');
// Vérification de la connexion à la base de données
final dbInstance = AppDatabase.instance; final dbInstance = AppDatabase.instance;
// Test de connexion à la base
try { try {
final userCount = await dbInstance.getUserCount(); final userCount = await dbInstance.getUserCount();
print('Base de données accessible, $userCount utilisateurs trouvés'); print('Base de données accessible, $userCount utilisateurs trouvés');
@ -110,16 +97,12 @@ class _LoginPageState extends State<LoginPage> {
throw Exception('Impossible d\'accéder à la base de données: $dbError'); throw Exception('Impossible d\'accéder à la base de données: $dbError');
} }
// Vérifier les identifiants
bool isValidUser = await dbInstance.verifyUser(username, password); bool isValidUser = await dbInstance.verifyUser(username, password);
print('Résultat de la vérification: $isValidUser');
if (isValidUser) { if (isValidUser) {
// Récupérer les informations complètes de l'utilisateur
Users user = await dbInstance.getUser(username); Users user = await dbInstance.getUser(username);
print('Utilisateur récupéré: ${user.username}'); print('Utilisateur récupéré: ${user.username}');
// Récupérer les credentials
Map<String, dynamic>? userCredentials = Map<String, dynamic>? userCredentials =
await dbInstance.getUserCredentials(username, password); await dbInstance.getUserCredentials(username, password);
@ -128,155 +111,214 @@ class _LoginPageState extends State<LoginPage> {
print('Rôle: ${userCredentials['role']}'); print('Rôle: ${userCredentials['role']}');
print('ID: ${userCredentials['id']}'); print('ID: ${userCredentials['id']}');
// CORRECTION : Sauvegarder ET mettre à jour le contrôleur
await saveUserData( await saveUserData(
user, user,
userCredentials['role'] as String, userCredentials['role'] as String,
userCredentials['id'] as int, userCredentials['id'] as int,
); );
// Navigation // MODIFICATION PRINCIPALE ICI
if (mounted) { if (mounted) {
if (userCredentials['role'] == 'commercial') {
// Redirection vers MainLayout pour les commerciaux
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainLayout()),
);
} else {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (context) => const AccueilPage()), MaterialPageRoute(builder: (context) => DashboardPage()),
); );
} }
}
} else { } else {
throw Exception('Erreur lors de la récupération des credentials'); throw Exception('Erreur lors de la récupération des credentials');
} }
} else { } else {
print('Identifiants invalides pour: $username');
setState(() { setState(() {
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide'; _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true; _isErrorVisible = true;
}); });
} }
} catch (error) { } catch (error) {
print('Erreur lors de la connexion: $error');
setState(() { setState(() {
_errorMessage = 'Erreur de connexion: ${error.toString()}'; _errorMessage = 'Erreur de connexion: ${error.toString()}';
_isErrorVisible = true; _isErrorVisible = true;
}); });
} finally { } finally {
if (mounted) { if (mounted) setState(() => _isLoading = false);
setState(() {
_isLoading = false;
});
}
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color primaryBlue = const Color(0xFF0033A1);
final Color accentRed = const Color(0xFFD70000);
final Color secondaryBlue = const Color(0xFF1976D2);
final Color primaryColor = primaryBlue;
final Color accentColor = secondaryBlue;
final Color cardColor = Colors.white;
return Scaffold( return Scaffold(
appBar: AppBar( backgroundColor: primaryColor,
title: const Text(
'Login',
style: TextStyle(color: Colors.white),
),
backgroundColor: const Color.fromARGB(255, 4, 54, 95),
centerTitle: true,
),
body: ParticleBackground( body: ParticleBackground(
child: Center( child: Center(
child: SingleChildScrollView(
child: Container( child: Container(
width: MediaQuery.of(context).size.width * 0.5, width: MediaQuery.of(context).size.width < 500
height: MediaQuery.of(context).size.height * 0.8, ? double.infinity
padding: const EdgeInsets.all(16.0), : 400,
padding:
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: cardColor.withOpacity(0.98),
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.2),
blurRadius: 16,
spreadRadius: 4,
offset: const Offset(0, 8),
),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Container( Center(
padding: const EdgeInsets.symmetric(vertical: 16.0), child: Column(
child: const Icon( children: [
CircleAvatar(
radius: 38,
backgroundColor: accentColor.withOpacity(0.15),
child: Icon(
Icons.lock_outline, Icons.lock_outline,
size: 100.0, color: accentColor,
color: Color.fromARGB(255, 4, 54, 95), size: 50,
), ),
), ),
const SizedBox(height: 14),
Text(
'GUYCOM',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
fontSize: 28,
),
),
const SizedBox(height: 4),
Text(
'Connectez-vous à votre compte',
style: TextStyle(
color: primaryColor.withOpacity(.8),
fontSize: 16,
),
),
],
),
),
const SizedBox(height: 24),
TextField( TextField(
controller: _usernameController, controller: _usernameController,
enabled: !_isLoading, enabled: !_isLoading,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Username', labelText: 'Nom d\'utilisateur',
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent), labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
prefixIcon: Icon(Icons.person, color: accentColor),
filled: true,
fillColor: accentColor.withOpacity(0.045),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
), ),
), ),
), ),
const SizedBox(height: 16.0), const SizedBox(height: 18.0),
TextField( TextField(
controller: _passwordController, controller: _passwordController,
enabled: !_isLoading, enabled: !_isLoading,
obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Password', labelText: 'Mot de passe',
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent), labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
prefixIcon: Icon(Icons.lock, color: accentColor),
filled: true,
fillColor: accentColor.withOpacity(0.045),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
), ),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
), ),
obscureText: true,
onSubmitted: (_) => _login(), onSubmitted: (_) => _login(),
), ),
const SizedBox(height: 16.0), if (_isErrorVisible) ...[
Visibility( const SizedBox(height: 12.0),
visible: _isErrorVisible, Text(
child: Text(
_errorMessage, _errorMessage,
style: const TextStyle( style: const TextStyle(
color: Colors.red, color: Colors.redAccent,
fontSize: 14, fontSize: 15,
fontWeight: FontWeight.w600,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ],
const SizedBox(height: 16.0), const SizedBox(height: 26.0),
ElevatedButton( ElevatedButton(
onPressed: _isLoading ? null : _login, onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0015B7), backgroundColor: accentColor,
elevation: 5.0, disabledBackgroundColor: accentColor.withOpacity(0.3),
foregroundColor: Colors.white,
elevation: 7.0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
), ),
minimumSize: const Size(double.infinity, 48), minimumSize: const Size(double.infinity, 52),
), ),
child: _isLoading child: _isLoading
? const SizedBox( ? const SizedBox(
height: 20, height: 24,
width: 20, width: 24,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 2, strokeWidth: 2.5,
), ),
) )
: const Text( : const Text(
'Se connecter', 'Se connecter',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 16, fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: .4,
), ),
), ),
), ),
// Bouton de debug (à supprimer en production) // Option debug, à enlever en prod
if (_isErrorVisible) if (_isErrorVisible) ...[
TextButton( TextButton(
onPressed: () async { onPressed: () async {
try { try {
final count = await AppDatabase.instance.getUserCount(); final count =
print('Debug: $count utilisateurs dans la base'); await AppDatabase.instance.getUserCount();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$count utilisateurs trouvés')), SnackBar(
content: Text('$count utilisateurs trouvés')),
); );
} catch (e) { } catch (e) {
print('Debug error: $e');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')), SnackBar(content: Text('Erreur: $e')),
); );
@ -285,6 +327,8 @@ class _LoginPageState extends State<LoginPage> {
child: const Text('Debug: Vérifier BDD'), child: const Text('Debug: Vérifier BDD'),
), ),
], ],
],
),
), ),
), ),
), ),

1747
lib/Views/mobilepage.dart

File diff suppressed because it is too large

1594
lib/Views/newCommand.dart

File diff suppressed because it is too large

62
lib/Views/produitsCard.dart

@ -16,7 +16,8 @@ class ProductCard extends StatefulWidget {
State<ProductCard> createState() => _ProductCardState(); State<ProductCard> createState() => _ProductCardState();
} }
class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin { class _ProductCardState extends State<ProductCard>
with TickerProviderStateMixin {
int selectedQuantity = 1; int selectedQuantity = 1;
late AnimationController _scaleController; late AnimationController _scaleController;
late AnimationController _fadeController; late AnimationController _fadeController;
@ -122,7 +123,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
: _buildPlaceholderImage(), : _buildPlaceholderImage(),
), ),
), ),
Positioned.fill( Positioned.fill(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -141,7 +141,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
), ),
), ),
), ),
if (widget.product.isStockDefined()) if (widget.product.isStockDefined())
Positioned( Positioned(
top: 12, top: 12,
@ -183,7 +182,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
), ),
), ),
), ),
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@ -201,7 +199,8 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
vertical: 8, vertical: 8,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Text( Text(
widget.product.name, widget.product.name,
@ -222,7 +221,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${widget.product.price.toStringAsFixed(2)} FCFA', '${widget.product.price.toStringAsFixed(2)} MGA',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 13, fontSize: 13,
@ -239,9 +238,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
], ],
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
children: [ children: [
Container( Container(
@ -250,7 +247,8 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color:
Colors.black.withOpacity(0.1),
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@ -295,9 +293,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
], ],
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
@ -306,9 +302,11 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
onTapUp: _onTapUp, onTapUp: _onTapUp,
onTapCancel: _onTapCancel, onTapCancel: _onTapCancel,
onTap: () { onTap: () {
widget.onAddToCart(widget.product, selectedQuantity); widget.onAddToCart(widget.product,
selectedQuantity);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar( SnackBar(
content: Row( content: Row(
children: [ children: [
@ -320,16 +318,20 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
Expanded( Expanded(
child: Text( child: Text(
'${widget.product.name} (x$selectedQuantity) ajouté au panier', '${widget.product.name} (x$selectedQuantity) ajouté au panier',
overflow: TextOverflow.ellipsis, overflow: TextOverflow
.ellipsis,
), ),
), ),
], ],
), ),
backgroundColor: Colors.green, backgroundColor: Colors.green,
duration: const Duration(seconds: 1), duration:
behavior: SnackBarBehavior.floating, const Duration(seconds: 1),
behavior:
SnackBarBehavior.floating,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius:
BorderRadius.circular(10),
), ),
), ),
); );
@ -342,21 +344,27 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: const LinearGradient( gradient: const LinearGradient(
colors: [ colors: [
Color.fromARGB(255, 4, 54, 95), Color.fromARGB(
Color.fromARGB(255, 6, 80, 140), 255, 4, 54, 95),
Color.fromARGB(
255, 6, 80, 140),
], ],
), ),
borderRadius: BorderRadius.circular(20), borderRadius:
BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: const Color.fromARGB(255, 4, 54, 95).withOpacity(0.3), color: const Color.fromARGB(
255, 4, 54, 95)
.withOpacity(0.3),
blurRadius: 6, blurRadius: 6,
offset: const Offset(0, 3), offset: const Offset(0, 3),
), ),
], ],
), ),
child: const Row( child: const Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment:
MainAxisAlignment.center,
children: [ children: [
const Icon( const Icon(
Icons.add_shopping_cart, Icons.add_shopping_cart,
@ -369,10 +377,12 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
'Ajouter', 'Ajouter',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight:
FontWeight.bold,
fontSize: 12, fontSize: 12,
), ),
overflow: TextOverflow.ellipsis, overflow:
TextOverflow.ellipsis,
), ),
), ),
], ],
@ -442,7 +452,9 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
child: Icon( child: Icon(
icon, icon,
size: 16, size: 16,
color: onPressed != null ? const Color.fromARGB(255, 4, 54, 95) : Colors.grey, color: onPressed != null
? const Color.fromARGB(255, 4, 54, 95)
: Colors.grey,
), ),
), ),
), ),

73
lib/Views/registrationPage.dart

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/users.dart'; 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/Views/Dashboard.dart';
import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/accueil.dart';
import '../Services/app_database.dart'; // Changé de authDatabase.dart //import '../Services/app_database.dart'; // Changé de authDatabase.dart
class RegistrationPage extends StatefulWidget { class RegistrationPage extends StatefulWidget {
const RegistrationPage({super.key}); const RegistrationPage({super.key});
@ -23,7 +25,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
Role? _selectedRole; Role? _selectedRole;
bool _isLoading = false; bool _isLoading = false;
bool _isLoadingRoles = true; bool _isLoadingRoles = true;
List<Map<String, dynamic>> _availablePointsDeVente = [];
int? _selectedPointDeVenteId;
bool _isLoadingPointsDeVente = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -40,6 +44,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
try { try {
await AppDatabase.instance.initDatabase(); await AppDatabase.instance.initDatabase();
await _loadRoles(); await _loadRoles();
await _loadPointsDeVente(); // Ajouté ici
} catch (error) { } catch (error) {
print('Erreur lors de l\'initialisation: $error'); print('Erreur lors de l\'initialisation: $error');
if (mounted) { if (mounted) {
@ -48,7 +53,25 @@ class _RegistrationPageState extends State<RegistrationPage> {
} }
} }
} }
Future<void> _loadPointsDeVente() async {
try {
final points = await AppDatabase.instance.getPointsDeVente();
if (mounted) {
setState(() {
_availablePointsDeVente = points;
_isLoadingPointsDeVente = false;
});
}
} catch (error) {
print('Erreur lors du chargement des points de vente: $error');
if (mounted) {
setState(() {
_isLoadingPointsDeVente = false;
});
}
}
}
Future<void> _loadRoles() async { Future<void> _loadRoles() async {
try { try {
final roles = await AppDatabase.instance.getRoles(); final roles = await AppDatabase.instance.getRoles();
@ -103,7 +126,8 @@ class _RegistrationPageState extends State<RegistrationPage> {
_emailController.text.trim().isEmpty || _emailController.text.trim().isEmpty ||
_usernameController.text.trim().isEmpty || _usernameController.text.trim().isEmpty ||
_passwordController.text.trim().isEmpty || _passwordController.text.trim().isEmpty ||
_selectedRole == null) { _selectedRole == null ||
_selectedPointDeVenteId == null) { // Ajouté ici
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.'); _showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
return false; return false;
} }
@ -142,6 +166,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
username: _usernameController.text.trim(), username: _usernameController.text.trim(),
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
roleName: _selectedRole!.designation, // Pour l'affichage roleName: _selectedRole!.designation, // Pour l'affichage
pointDeVenteId: _selectedPointDeVenteId, // Ajouté ici
); );
// Sauvegarder l'utilisateur dans la base de données // Sauvegarder l'utilisateur dans la base de données
@ -191,7 +216,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (context) => const AccueilPage()), MaterialPageRoute(builder: (context) => DashboardPage()),
); );
}, },
child: const Text('OK'), child: const Text('OK'),
@ -361,6 +386,46 @@ class _RegistrationPageState extends State<RegistrationPage> {
), ),
), ),
), ),
// Dans la méthode build, après le DropdownButton des rôles
const SizedBox(height: 16.0),
_isLoadingPointsDeVente
? const CircularProgressIndicator()
: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _selectedPointDeVenteId,
hint: const Text('Sélectionner un point de vente'),
isExpanded: true,
onChanged: _isLoading
? null
: (int? newValue) {
setState(() {
_selectedPointDeVenteId = newValue;
});
},
items: _availablePointsDeVente
.map<DropdownMenuItem<int>>((Map<String, dynamic> point) {
return DropdownMenuItem<int>(
value: point['id'] as int,
child: Row(
children: [
const Icon(Icons.store, size: 20),
const SizedBox(width: 8),
Text(point['nom']),
],
),
);
}).toList(),
),
),
),
const SizedBox(height: 16.0),
const SizedBox(height: 24.0), const SizedBox(height: 24.0),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,

7
lib/accueil.dart

@ -3,6 +3,7 @@ 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';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground; import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
import 'package:youmazgestion/Views/produitsCard.dart'; import 'package:youmazgestion/Views/produitsCard.dart';
import 'Components/appDrawer.dart'; import 'Components/appDrawer.dart';
@ -10,7 +11,7 @@ import 'Components/app_bar.dart';
import 'Components/cartItem.dart'; import 'Components/cartItem.dart';
import 'Models/produit.dart'; import 'Models/produit.dart';
import 'Services/OrderDatabase.dart'; import 'Services/OrderDatabase.dart';
import 'Services/productDatabase.dart'; //import 'Services/productDatabase.dart';
import 'Views/ticketPage.dart'; import 'Views/ticketPage.dart';
import 'controller/userController.dart'; import 'controller/userController.dart';
import 'my_app.dart'; import 'my_app.dart';
@ -25,7 +26,7 @@ class AccueilPage extends StatefulWidget {
class _AccueilPageState extends State<AccueilPage> { class _AccueilPageState extends State<AccueilPage> {
final UserController userController = Get.put(UserController()); final UserController userController = Get.put(UserController());
final ProductDatabase productDatabase = ProductDatabase(); final AppDatabase productDatabase = AppDatabase.instance;
late Future<Map<String, List<Product>>> productsFuture; late Future<Map<String, List<Product>>> productsFuture;
final OrderDatabase orderDatabase = OrderDatabase.instance; final OrderDatabase orderDatabase = OrderDatabase.instance;
final WorkDatabase workDatabase = WorkDatabase.instance; final WorkDatabase workDatabase = WorkDatabase.instance;
@ -448,7 +449,7 @@ class _AccueilPageState extends State<AccueilPage> {
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
Text( Text(
'${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA', '${NumberFormat('#,##0.00').format(calculateTotalPrice())} MGA',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

64
lib/config/DatabaseConfig.dart

@ -0,0 +1,64 @@
// Config/database_config.dart - Version améliorée
class DatabaseConfig {
static const String host = 'database.c4m.mg';
static const int port = 3306;
static const String username = 'guycom';
static const String password = '3iV59wjRdbuXAPR';
static const String database = 'guycom';
static const String prodHost = 'database.c4m.mg';
static const String prodUsername = 'guycom';
static const String prodPassword = '3iV59wjRdbuXAPR';
static const String prodDatabase = 'guycom';
static const Duration connectionTimeout = Duration(seconds: 30);
static const Duration queryTimeout = Duration(seconds: 15);
static const int maxConnections = 10;
static const int minConnections = 2;
static bool get isDevelopment => false;
static Map<String, dynamic> getConfig() {
if (isDevelopment) {
return {
'host': host,
'port': port,
'user': username,
'password': password,
'database': database,
'timeout': connectionTimeout.inSeconds,
};
} else {
return {
'host': prodHost,
'port': port,
'user': prodUsername,
'password': prodPassword,
'database': prodDatabase,
'timeout': connectionTimeout.inSeconds,
};
}
}
// Validation de la configuration
static bool validateConfig() {
try {
final config = getConfig();
return config['host']?.toString().isNotEmpty == true &&
config['database']?.toString().isNotEmpty == true &&
config['user'] != null;
} catch (e) {
print("Erreur de validation de la configuration: $e");
return false;
}
}
// Configuration avec retry automatique
static Map<String, dynamic> getConfigWithRetry() {
final config = getConfig();
config['retryCount'] = 3;
config['retryDelay'] = 5000; // ms
return config;
}
}

5
lib/controller/AccueilController.dart

@ -1,17 +1,18 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/controller/userController.dart';
import '../Components/cartItem.dart'; import '../Components/cartItem.dart';
import '../Models/produit.dart'; import '../Models/produit.dart';
import '../Services/OrderDatabase.dart'; import '../Services/OrderDatabase.dart';
import '../Services/WorkDatabase.dart'; import '../Services/WorkDatabase.dart';
import '../Services/productDatabase.dart'; //import '../Services/productDatabase.dart';
import '../Views/ticketPage.dart'; import '../Views/ticketPage.dart';
import '../my_app.dart'; import '../my_app.dart';
class AccueilController extends GetxController { class AccueilController extends GetxController {
final UserController userController = Get.find(); final UserController userController = Get.find();
final ProductDatabase productDatabase = ProductDatabase(); final AppDatabase productDatabase = AppDatabase.instance;
final Rx<Map<String, List<Product>>> productsFuture = Rx({}); // Observable final Rx<Map<String, List<Product>>> productsFuture = Rx({}); // Observable
final OrderDatabase orderDatabase = OrderDatabase.instance; final OrderDatabase orderDatabase = OrderDatabase.instance;
final WorkDatabase workDatabase = WorkDatabase.instance; final WorkDatabase workDatabase = WorkDatabase.instance;

63
lib/controller/userController.dart

@ -1,7 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Services/app_database.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
//import 'package:youmazgestion/Services/app_database.dart';
class UserController extends GetxController { class UserController extends GetxController {
final _username = ''.obs; final _username = ''.obs;
@ -11,6 +12,8 @@ class UserController extends GetxController {
final _lastname = ''.obs; final _lastname = ''.obs;
final _password = ''.obs; final _password = ''.obs;
final _userId = 0.obs; // Ajout de l'ID utilisateur final _userId = 0.obs; // Ajout de l'ID utilisateur
final _pointDeVenteId = 0.obs;
final _pointDeVenteDesignation = ''.obs;
String get username => _username.value; String get username => _username.value;
String get email => _email.value; String get email => _email.value;
@ -19,6 +22,8 @@ class UserController extends GetxController {
String get lastname => _lastname.value; String get lastname => _lastname.value;
String get password => _password.value; String get password => _password.value;
int get userId => _userId.value; int get userId => _userId.value;
int get pointDeVenteId => _pointDeVenteId.value;
String get pointDeVenteDesignation => _pointDeVenteDesignation.value;
@override @override
void onInit() { void onInit() {
@ -34,41 +39,57 @@ class UserController extends GetxController {
final storedUsername = prefs.getString('username') ?? ''; final storedUsername = prefs.getString('username') ?? '';
final storedRole = prefs.getString('role') ?? ''; final storedRole = prefs.getString('role') ?? '';
final storedUserId = prefs.getInt('user_id') ?? 0; final storedUserId = prefs.getInt('user_id') ?? 0;
final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0;
final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? '';
if (storedUsername.isNotEmpty) { if (storedUsername.isNotEmpty) {
try { try {
// Récupérer les données complètes depuis la base de données
Users user = await AppDatabase.instance.getUser(storedUsername); Users user = await AppDatabase.instance.getUser(storedUsername);
// Mettre à jour TOUTES les données
_username.value = user.username; _username.value = user.username;
_email.value = user.email; _email.value = user.email;
_name.value = user.name; _name.value = user.name;
_lastname.value = user.lastName; _lastname.value = user.lastName;
_password.value = user.password; _password.value = user.password;
_role.value = storedRole; // Récupéré depuis SharedPreferences _role.value = storedRole;
_userId.value = storedUserId; // Récupéré depuis SharedPreferences _userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
// Si la désignation n'est pas sauvegardée, on peut la récupérer
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) {
await loadPointDeVenteDesignation();
}
print("✅ Données chargées depuis la DB - Username: ${_username.value}");
print("✅ Name: ${_name.value}, Email: ${_email.value}");
print("✅ Role: ${_role.value}, UserID: ${_userId.value}");
} catch (dbError) { } catch (dbError) {
print('❌ Erreur DB, chargement depuis SharedPreferences uniquement: $dbError'); // Fallback
// Fallback : charger depuis SharedPreferences uniquement
_username.value = storedUsername; _username.value = storedUsername;
_email.value = prefs.getString('email') ?? ''; _email.value = prefs.getString('email') ?? '';
_role.value = storedRole; _role.value = storedRole;
_name.value = prefs.getString('name') ?? ''; _name.value = prefs.getString('name') ?? '';
_lastname.value = prefs.getString('lastname') ?? ''; _lastname.value = prefs.getString('lastname') ?? '';
_userId.value = storedUserId; _userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
} }
} else {
print("❌ Aucun utilisateur stocké trouvé");
} }
} catch (e) { } catch (e) {
print('❌ Erreur lors du chargement des données utilisateur: $e'); print('❌ Erreur lors du chargement des données utilisateur: $e');
} }
} }
Future<void> loadPointDeVenteDesignation() async {
if (_pointDeVenteId.value <= 0) return;
try {
final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value);
if (pointDeVente != null) {
_pointDeVenteDesignation.value = pointDeVente['designation'] as String;
await saveUserData(); // Sauvegarder la désignation
}
} catch (e) {
print('❌ Erreur lors du chargement de la désignation du point de vente: $e');
}
}
// NOUVELLE MÉTHODE : Mise à jour complète avec Users + credentials // NOUVELLE MÉTHODE : Mise à jour complète avec Users + credentials
void setUserWithCredentials(Users user, String role, int userId) { void setUserWithCredentials(Users user, String role, int userId) {
@ -79,6 +100,7 @@ class UserController extends GetxController {
_lastname.value = user.lastName; _lastname.value = user.lastName;
_password.value = user.password; _password.value = user.password;
_userId.value = userId; // ID depuis les credentials _userId.value = userId; // ID depuis les credentials
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
print("✅ Utilisateur mis à jour avec credentials:"); print("✅ Utilisateur mis à jour avec credentials:");
print(" Username: ${_username.value}"); print(" Username: ${_username.value}");
@ -119,7 +141,9 @@ class UserController extends GetxController {
await prefs.setString('role', _role.value); await prefs.setString('role', _role.value);
await prefs.setString('name', _name.value); await prefs.setString('name', _name.value);
await prefs.setString('lastname', _lastname.value); await prefs.setString('lastname', _lastname.value);
await prefs.setInt('user_id', _userId.value); // Sauvegarder l'ID await prefs.setInt('user_id', _userId.value);
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
print("✅ Données sauvegardées avec succès dans SharedPreferences"); print("✅ Données sauvegardées avec succès dans SharedPreferences");
} catch (e) { } catch (e) {
@ -132,24 +156,25 @@ class UserController extends GetxController {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
// Vider SharedPreferences
await prefs.remove('username'); await prefs.remove('username');
await prefs.remove('email'); await prefs.remove('email');
await prefs.remove('role'); await prefs.remove('role');
await prefs.remove('name'); await prefs.remove('name');
await prefs.remove('lastname'); await prefs.remove('lastname');
await prefs.remove('user_id'); // Supprimer l'ID aussi await prefs.remove('user_id');
await prefs.remove('point_de_vente_id');
await prefs.remove('point_de_vente_designation');
// Vider les variables observables
_username.value = ''; _username.value = '';
_email.value = ''; _email.value = '';
_role.value = ''; _role.value = '';
_name.value = ''; _name.value = '';
_lastname.value = ''; _lastname.value = '';
_password.value = ''; _password.value = '';
_userId.value = 0; // Réinitialiser l'ID _userId.value = 0;
_pointDeVenteId.value = 0;
_pointDeVenteDesignation.value = '';
print("✅ Toutes les données utilisateur ont été effacées");
} catch (e) { } catch (e) {
print('❌ Erreur lors de l\'effacement des données utilisateur: $e'); print('❌ Erreur lors de l\'effacement des données utilisateur: $e');
} }
@ -198,6 +223,8 @@ class UserController extends GetxController {
print("Email: ${_email.value}"); print("Email: ${_email.value}");
print("Role: ${_role.value}"); print("Role: ${_role.value}");
print("UserID: ${_userId.value}"); print("UserID: ${_userId.value}");
print("PointDeVenteID: ${_pointDeVenteId.value}");
print("PointDeVente: ${_pointDeVenteDesignation.value}");
print("IsLoggedIn: $isLoggedIn"); print("IsLoggedIn: $isLoggedIn");
print("========================"); print("========================");
} }

106
lib/main.dart

@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:youmazgestion/Services/app_database.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/controller/userController.dart';
import 'Services/productDatabase.dart';
import 'my_app.dart'; import 'my_app.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -10,30 +9,117 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
try { try {
// Initialiser les bases de données une seule fois print("Initialisation de l'application...");
// Pour le développement : supprimer toutes les tables (équivalent à deleteDatabaseFile)
// ATTENTION: Décommentez seulement si vous voulez réinitialiser la base
// await AppDatabase.instance.deleteDatabaseFile(); // await AppDatabase.instance.deleteDatabaseFile();
// await ProductDatabase.instance.deleteDatabaseFile();
await ProductDatabase.instance.initDatabase(); // Initialiser la base de données MySQL
print("Connexion à la base de données MySQL...");
await AppDatabase.instance.initDatabase(); await AppDatabase.instance.initDatabase();
print("Base de données initialisée avec succès !");
// Afficher les informations de la base (pour debug) // Afficher les informations de la base (pour debug)
await AppDatabase.instance.printDatabaseInfo(); await AppDatabase.instance.printDatabaseInfo();
Get.put(
UserController()); // Ajoute ce code AVANT tout accès au UserController // Initialiser le contrôleur utilisateur
Get.put(UserController());
print("Contrôleur utilisateur initialisé");
// Configurer le logger
setupLogger(); setupLogger();
print("Lancement de l'application...");
runApp(const GetMaterialApp( runApp(const GetMaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: MyApp(), home: MyApp(),
)); ));
} catch (e) { } catch (e) {
print('Erreur lors de l\'initialisation: $e'); print('Erreur lors de l\'initialisation: $e');
// Vous pourriez vouloir afficher une page d'erreur ici
// Afficher une page d'erreur avec plus de détails
runApp(MaterialApp( runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold( home: Scaffold(
body: Center( backgroundColor: Colors.red[50],
child: Text('Erreur d\'initialisation: $e'), appBar: AppBar(
title: const Text('Erreur d\'initialisation'),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 48,
),
const SizedBox(height: 16),
const Text(
'Erreur de connexion à la base de données',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
const SizedBox(height: 16),
const Text(
'Vérifiez que :',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('• XAMPP est démarré'),
const Text('• MySQL est en cours d\'exécution'),
const Text('• La base de données "guycom_databse" existe'),
const Text('• Les paramètres de connexion sont corrects'),
const SizedBox(height: 16),
const Text(
'Détails de l\'erreur :',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey),
),
child: SingleChildScrollView(
child: Text(
e.toString(),
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// Relancer l'application
main();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: const Text('Réessayer'),
),
),
],
),
), ),
), ),
)); ));

4
macos/Flutter/GeneratedPluginRegistrant.swift

@ -7,18 +7,18 @@ import Foundation
import file_picker import file_picker
import file_selector_macos import file_selector_macos
import mobile_scanner
import open_file_mac import open_file_mac
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
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"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

80
pubspec.lock

@ -241,6 +241,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+4" version: "0.9.3+4"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79"
url: "https://pub.dev"
source: hosted
version: "0.65.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -608,6 +616,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" version: "1.0.6"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760
url: "https://pub.dev"
source: hosted
version: "5.2.3"
msix: msix:
dependency: "direct main" dependency: "direct main"
description: description:
@ -616,6 +632,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.7.0" version: "3.7.0"
mysql1:
dependency: "direct main"
description:
name: mysql1
sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9"
url: "https://pub.dev"
source: hosted
version: "0.20.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -624,6 +648,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
numbers_to_letters:
dependency: "direct main"
description:
name: numbers_to_letters
sha256: "70c7ed2f04c1982a299e753101fbc2d52ed5b39a2b3dd2a9c07ba131e9c0948e"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
open_file: open_file:
dependency: "direct main" dependency: "direct main"
description: description:
@ -808,6 +840,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -957,22 +997,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
@ -989,22 +1013,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.3.5"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3: sqlite3:
dependency: transitive dependency: transitive
description: description:
@ -1041,18 +1049,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: syncfusion_flutter_charts name: syncfusion_flutter_charts
sha256: bdb7cc5814ceb187793cea587f4a5946afcffd96726b219cee79df8460f44b7b sha256: "0222ac9d8cb6c671f014effe9bd5c0aef35eadb16471355345ba87cc0ac007b3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "21.2.4" version: "20.4.54"
syncfusion_flutter_core: syncfusion_flutter_core:
dependency: transitive dependency: transitive
description: description:
name: syncfusion_flutter_core name: syncfusion_flutter_core
sha256: "8db8f55c77f56968681447d3837c10f27a9e861e238a898fda116c7531def979" sha256: "3979f0b1c5a97422cadae52d476c21fa3e0fb671ef51de6cae1d646d8b99fe1f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "21.2.10" version: "20.4.54"
synchronized: synchronized:
dependency: transitive dependency: transitive
description: description:

14
pubspec.yaml

@ -35,7 +35,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
get: ^4.6.5 get: ^4.6.5
sqflite: ^2.2.8+4 # sqflite: ^2.2.8+4
mysql1: ^0.20.0
flutter_dropzone: ^4.2.1 flutter_dropzone: ^4.2.1
image_picker: ^0.8.7+5 image_picker: ^0.8.7+5
@ -50,7 +51,7 @@ dependencies:
logging: ^1.2.0 logging: ^1.2.0
msix: ^3.7.0 msix: ^3.7.0
flutter_charts: ^0.5.1 flutter_charts: ^0.5.1
syncfusion_flutter_charts: ^21.2.4 syncfusion_flutter_charts: ^20.4.48
shelf: ^1.4.1 shelf: ^1.4.1
shelf_router: ^1.1.4 shelf_router: ^1.1.4
pdf: ^3.8.4 pdf: ^3.8.4
@ -62,7 +63,9 @@ dependencies:
path_provider: ^2.0.15 path_provider: ^2.0.15
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
excel: ^2.0.1 excel: ^2.0.1
mobile_scanner: ^5.0.0 # ou la version la plus récente
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
numbers_to_letters: ^1.0.0
@ -101,6 +104,11 @@ flutter:
- assets/database/usersdb.db - assets/database/usersdb.db
- assets/database/work.db - assets/database/work.db
- assets/database/roles.db - assets/database/roles.db
- assets/airtel_money.png
- assets/mvola.jpg
- assets/Orange_money.png
- assets/fa-solid-900.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
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware

Loading…
Cancel
Save