Compare commits

...

11 Commits

Author SHA1 Message Date
Stephane be8c169ad1 qr code reference 6 months ago
ASUS 48ae916f02 date format 6 months ago
b.razafimandimbihery c0bbb0da2b lastlast update 6 months ago
b.razafimandimbihery 595b38e9fb adding barcode scanner 6 months ago
b.razafimandimbihery 525b09c81f scan code bar 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/NotoEmoji-Regular.ttf
  3. BIN
      assets/Orange_money.png
  4. BIN
      assets/airtel_money.png
  5. BIN
      assets/fa-solid-900.ttf
  6. BIN
      assets/fonts/Roboto-Italic.ttf
  7. BIN
      assets/mvola.jpg
  8. 3
      ios/Runner/Info.plist
  9. 417
      lib/Components/AddClient.dart
  10. 471
      lib/Components/AddClientForm.dart
  11. 176
      lib/Components/DiscountDialog.dart
  12. 349
      lib/Components/GiftaselectedButton.dart
  13. 259
      lib/Components/QrScan.dart
  14. 796
      lib/Components/appDrawer.dart
  15. 120
      lib/Components/app_bar.dart
  16. 387
      lib/Components/commandManagementComponents/CommandDetails.dart
  17. 213
      lib/Components/commandManagementComponents/CommandeActions.dart
  18. 189
      lib/Components/commandManagementComponents/DiscountDialog.dart
  19. 136
      lib/Components/commandManagementComponents/GiftSelectionDialog.dart
  20. 234
      lib/Components/commandManagementComponents/PaswordRequired.dart
  21. 8
      lib/Components/commandManagementComponents/PaymentMethod.dart
  22. 265
      lib/Components/commandManagementComponents/PaymentMethodDialog.dart
  23. 7
      lib/Components/commandManagementComponents/PaymentType.dart
  24. 411
      lib/Components/newCommandComponents/CadeauDialog.dart
  25. 331
      lib/Components/newCommandComponents/RemiseDialog.dart
  26. 7
      lib/Components/paymentType.dart
  27. 354
      lib/Models/Client.dart
  28. 64
      lib/Models/Remise.dart
  29. 36
      lib/Models/pointage_model.dart
  30. 167
      lib/Models/produit.dart
  31. 28
      lib/Models/users.dart
  32. 0
      lib/Services/GestionStockDatabase.dart
  33. 258
      lib/Services/PermissionCacheService.dart
  34. 304
      lib/Services/Script.sql
  35. 680
      lib/Services/app_database.dart
  36. 559
      lib/Services/productDatabase.dart
  37. 2211
      lib/Services/stock_managementDatabase.dart
  38. 1612
      lib/Views/Dashboard.dart
  39. 5052
      lib/Views/HandleProduct.dart
  40. 7
      lib/Views/RoleListPage.dart
  41. 610
      lib/Views/RolePermissionPage.dart
  42. 2
      lib/Views/bilanMois.dart
  43. 3114
      lib/Views/commandManagement.dart
  44. 5
      lib/Views/editProduct.dart
  45. 3
      lib/Views/editUser.dart
  46. 7
      lib/Views/gestionProduct.dart
  47. 464
      lib/Views/gestionRole.dart
  48. 7
      lib/Views/gestionStock.dart
  49. 416
      lib/Views/gestion_point_de_vente.dart
  50. 967
      lib/Views/historique.dart
  51. 2
      lib/Views/listCommandeHistory.dart
  52. 5
      lib/Views/listUser.dart
  53. 632
      lib/Views/loginPage.dart
  54. 44
      lib/Views/mobilepage.dart
  55. 3174
      lib/Views/newCommand.dart
  56. 74
      lib/Views/produitsCard.dart
  57. 142
      lib/Views/registrationPage.dart
  58. 120
      lib/Views/ticketPage.dart
  59. 10
      lib/accueil.dart
  60. 64
      lib/config/DatabaseConfig.dart
  61. 5
      lib/controller/AccueilController.dart
  62. 187
      lib/controller/userController.dart
  63. 110
      lib/main.dart
  64. 64
      lib/my_app.dart
  65. 8
      linux/flutter/generated_plugin_registrant.cc
  66. 2
      linux/flutter/generated_plugins.cmake
  67. 8
      macos/Flutter/GeneratedPluginRegistrant.swift
  68. 104
      pubspec.lock
  69. 17
      pubspec.yaml
  70. 3
      test/widget_test.dart
  71. 6
      windows/flutter/generated_plugin_registrant.cc
  72. 2
      windows/flutter/generated_plugins.cmake

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

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

BIN
assets/NotoEmoji-Regular.ttf

Binary file not shown.

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">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Cette application a besoin d'accéder à la caméra pour scanner les codes QR</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@ -47,5 +49,6 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</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),
),
);
},
),
),
],
),
),
);
}
}

259
lib/Components/QrScan.dart

@ -0,0 +1,259 @@
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;
}
}

796
lib/Components/appDrawer.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/Views/HandleProduct.dart';
import 'package:youmazgestion/Views/RoleListPage.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/accueil.dart';
import 'package:youmazgestion/controller/userController.dart';
import 'package:youmazgestion/Views/gestion_point_de_vente.dart';
class CustomDrawer extends StatelessWidget {
final UserController userController = Get.find<UserController>();
@ -23,6 +25,7 @@ class CustomDrawer extends StatelessWidget {
await prefs.remove('role');
await prefs.remove('user_id');
// IMPORTANT: Vider le cache de session
userController.clearUserData();
}
@ -32,308 +35,573 @@ class CustomDrawer extends StatelessWidget {
Widget build(BuildContext context) {
return Drawer(
backgroundColor: Colors.white,
child: FutureBuilder(
future: _buildDrawerItems(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return ListView(
padding: EdgeInsets.zero,
children: snapshot.data as List<Widget>,
);
} else {
return const Center(child: CircularProgressIndicator());
}
child: GetBuilder<UserController>(
builder: (controller) {
return ListView(
padding: EdgeInsets.zero,
children: [
// Header utilisateur
_buildUserHeader(controller),
// CORRIGÉ: Construction avec gestion des valeurs null
..._buildDrawerItemsFromSessionCache(),
// Déconnexion
const Divider(),
_buildLogoutItem(),
],
);
},
),
);
}
Future<List<Widget>> _buildDrawerItems() async {
/// CORRIGÉ: Construction avec validation robuste des données
List<Widget> _buildDrawerItemsFromSessionCache() {
List<Widget> drawerItems = [];
drawerItems.add(
GetBuilder<UserController>(
builder: (controller) => Container(
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
// Vérifier si le cache est prêt
if (!userController.isCacheReady) {
return [
const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
const CircleAvatar(
radius: 30,
backgroundImage: AssetImage("assets/youmaz2.png"),
SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 15),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.name.isNotEmpty ? controller.name : 'Utilisateur',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
controller.role.isNotEmpty ? controller.role : 'Aucun rôle',
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
),
],
SizedBox(height: 8),
Text(
"Chargement du menu...",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
),
),
);
drawerItems.add(
await _buildDrawerItem(
icon: Icons.home,
title: "Accueil",
color: Colors.blue,
permissionAction: 'view',
permissionRoute: '/accueil',
onTap: () => Get.to(const AccueilPage()),
),
);
List<Widget> gestionUtilisateursItems = [
await _buildDrawerItem(
icon: Icons.person_add,
title: "Ajouter un utilisateur",
color: Colors.green,
permissionAction: 'create',
permissionRoute: '/ajouter-utilisateur',
onTap: () => Get.to(const RegistrationPage()),
),
await _buildDrawerItem(
icon: Icons.supervised_user_circle,
title: "Gérer les utilisateurs",
color: const Color.fromARGB(255, 4, 54, 95),
permissionAction: 'update',
permissionRoute: '/modifier-utilisateur',
onTap: () => Get.to(const ListUserPage()),
),
];
];
}
if (gestionUtilisateursItems.any((item) => item is ListTile)) {
drawerItems.add(
// Récupérer les menus depuis le cache de session
final rawUserMenus = userController.getUserMenus();
// 🛡 VALIDATION: Filtrer les menus valides
final validMenus = <Map<String, dynamic>>[];
final invalidMenus = <Map<String, dynamic>>[];
for (var menu in rawUserMenus) {
// Vérifier que les champs essentiels ne sont pas null
final name = menu['name'];
final route = menu['route'];
final id = menu['id'];
if (name != null && route != null && route.toString().isNotEmpty) {
validMenus.add({
'id': id,
'name': name.toString(),
'route': route.toString(),
});
} else {
invalidMenus.add(menu);
print("⚠️ Menu invalide ignoré dans CustomDrawer: id=$id, name='$name', route='$route'");
}
}
// Afficher les statistiques de validation
if (invalidMenus.isNotEmpty) {
print("📊 CustomDrawer: ${validMenus.length} menus valides, ${invalidMenus.length} invalides");
}
if (validMenus.isEmpty) {
return [
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
padding: EdgeInsets.all(16.0),
child: Text(
"GESTION UTILISATEURS",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
"Aucun menu accessible",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
),
);
drawerItems.addAll(gestionUtilisateursItems);
];
}
List<Widget> gestionProduitsItems = [
await _buildDrawerItem(
icon: Icons.inventory,
title: "Gestion des produits",
color: Colors.indigoAccent,
permissionAction: 'create',
permissionRoute: '/ajouter-produit',
onTap: () => Get.to(const ProductManagementPage()),
),
await _buildDrawerItem(
icon: Icons.storage,
title: "Gestion de stock",
color: Colors.blueAccent,
permissionAction: 'update',
permissionRoute: '/gestion-stock',
onTap: () => Get.to(const GestionStockPage()),
),
];
if (gestionProduitsItems.any((item) => item is ListTile)) {
drawerItems.add(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"GESTION PRODUITS",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
);
drawerItems.addAll(gestionProduitsItems);
// 🔧 DÉDUPLICATION: Éliminer les doublons par route
final Map<String, Map<String, dynamic>> uniqueMenus = {};
for (var menu in validMenus) {
final route = menu['route'] as String;
uniqueMenus[route] = menu;
}
final deduplicatedMenus = uniqueMenus.values.toList();
if (deduplicatedMenus.length != validMenus.length) {
print("🔧 CustomDrawer: ${validMenus.length - deduplicatedMenus.length} doublons supprimés");
}
List<Widget> gestionCommandesItems = [
await _buildDrawerItem(
icon: Icons.add_shopping_cart,
title: "Nouvelle commande",
color: Colors.orange,
permissionAction: 'create',
permissionRoute: '/nouvelle-commande',
onTap: () => Get.to(const NouvelleCommandePage()),
),
await _buildDrawerItem(
icon: Icons.list_alt,
title: "Gérer les commandes",
color: Colors.deepPurple,
permissionAction: 'manage',
permissionRoute: '/gerer-commandes',
onTap: () => Get.to(const GestionCommandesPage()),
),
];
// Organiser les menus par catégories
final Map<String, List<Map<String, dynamic>>> categorizedMenus = {
'GESTION UTILISATEURS': [],
'GESTION PRODUITS': [],
'GESTION COMMANDES': [],
'RAPPORTS': [],
'ADMINISTRATION': [],
};
if (gestionCommandesItems.any((item) => item is ListTile)) {
drawerItems.add(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"GESTION COMMANDES",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
);
drawerItems.addAll(gestionCommandesItems);
// Accueil toujours en premier
final accueilMenu = deduplicatedMenus.where((menu) => menu['route'] == '/accueil').firstOrNull;
if (accueilMenu != null) {
drawerItems.add(_buildDrawerItemFromMenu(accueilMenu));
}
List<Widget> rapportsItems = [
await _buildDrawerItem(
icon: Icons.bar_chart,
title: "Bilan mensuel",
color: Colors.teal,
permissionAction: 'read',
permissionRoute: '/bilan',
onTap: () => Get.to(const BilanMois()),
),
await _buildDrawerItem(
icon: Icons.history,
title: "Historique",
color: Colors.blue,
permissionAction: 'read',
permissionRoute: '/historique',
onTap: () => Get.to(HistoryPage()),
),
];
if (rapportsItems.any((item) => item is ListTile)) {
drawerItems.add(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"RAPPORTS",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
);
drawerItems.addAll(rapportsItems);
// Catégoriser les autres menus avec validation supplémentaire
for (var menu in deduplicatedMenus) {
final route = menu['route'] as String;
// Validation supplémentaire avant categorisation
if (route.isEmpty) {
print("⚠️ Route vide ignorée: ${menu['name']}");
continue;
}
switch (route) {
case '/accueil':
// Déjà traité
break;
case '/ajouter-utilisateur':
case '/modifier-utilisateur':
case '/pointage':
categorizedMenus['GESTION UTILISATEURS']!.add(menu);
break;
case '/ajouter-produit':
case '/gestion-stock':
categorizedMenus['GESTION PRODUITS']!.add(menu);
break;
case '/nouvelle-commande':
case '/gerer-commandes':
categorizedMenus['GESTION COMMANDES']!.add(menu);
break;
case '/bilan':
case '/historique':
categorizedMenus['RAPPORTS']!.add(menu);
break;
case '/gerer-roles':
case '/points-de-vente':
categorizedMenus['ADMINISTRATION']!.add(menu);
break;
default:
// Menu non catégorisé
print("⚠️ Menu non catégorisé: $route");
break;
}
}
List<Widget> administrationItems = [
await _buildDrawerItem(
icon: Icons.admin_panel_settings,
title: "Gérer les rôles",
color: Colors.redAccent,
permissionAction: 'admin',
permissionRoute: '/gerer-roles',
onTap: () => Get.to(const RoleListPage()),
),
];
// Ajouter les catégories avec leurs menus
categorizedMenus.forEach((categoryName, menus) {
if (menus.isNotEmpty) {
drawerItems.add(_buildCategoryHeader(categoryName));
for (var menu in menus) {
drawerItems.add(_buildDrawerItemFromMenu(menu));
}
}
});
if (administrationItems.any((item) => item is ListTile)) {
drawerItems.add(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"ADMINISTRATION",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
);
drawerItems.addAll(administrationItems);
}
return drawerItems;
}
drawerItems.add(const Divider());
/// CORRIGÉ: Construction d'un item de menu avec validation
Widget _buildDrawerItemFromMenu(Map<String, dynamic> menu) {
// 🛡 VALIDATION: Vérification des types avec gestion des null
final nameObj = menu['name'];
final routeObj = menu['route'];
if (nameObj == null || routeObj == null) {
print("⚠️ Menu invalide dans _buildDrawerItemFromMenu: name=$nameObj, route=$routeObj");
return const SizedBox.shrink();
}
final String name = nameObj.toString();
final String route = routeObj.toString();
if (name.isEmpty || route.isEmpty) {
print("⚠️ Menu avec valeurs vides: name='$name', route='$route'");
return const SizedBox.shrink();
}
// Mapping des routes vers les widgets et icônes
final Map<String, Map<String, dynamic>> routeMapping = {
'/accueil': {
'icon': Icons.home,
'color': Colors.blue,
'widget': DashboardPage(),
},
'/ajouter-utilisateur': {
'icon': Icons.person_add,
'color': Colors.green,
'widget': const RegistrationPage(),
},
'/modifier-utilisateur': {
'icon': Icons.supervised_user_circle,
'color': const Color.fromARGB(255, 4, 54, 95),
'widget': const ListUserPage(),
},
'/pointage': {
'icon': Icons.timer,
'color': const Color.fromARGB(255, 4, 54, 95),
'widget': null, // TODO: Implémenter
},
'/ajouter-produit': {
'icon': Icons.inventory,
'color': Colors.indigoAccent,
'widget': const ProductManagementPage(),
},
'/gestion-stock': {
'icon': Icons.storage,
'color': Colors.blueAccent,
'widget': const GestionStockPage(),
},
'/nouvelle-commande': {
'icon': Icons.add_shopping_cart,
'color': Colors.orange,
'widget': const NouvelleCommandePage(),
},
'/gerer-commandes': {
'icon': Icons.list_alt,
'color': Colors.deepPurple,
'widget': const GestionCommandesPage(),
},
'/bilan': {
'icon': Icons.bar_chart,
'color': Colors.teal,
'widget': DashboardPage(),
},
'/historique': {
'icon': Icons.history,
'color': Colors.blue,
'widget': const HistoriquePage(),
},
'/gerer-roles': {
'icon': Icons.admin_panel_settings,
'color': Colors.redAccent,
'widget': const RoleListPage(),
},
'/points-de-vente': {
'icon': Icons.store,
'color': Colors.blueGrey,
'widget': const AjoutPointDeVentePage(),
},
};
drawerItems.add(
ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: const Text("Déconnexion"),
final routeData = routeMapping[route];
if (routeData == null) {
print("⚠️ Route non reconnue: '$route' pour le menu '$name'");
return ListTile(
leading: const Icon(Icons.help_outline, color: Colors.grey),
title: Text(name),
subtitle: Text("Route: $route", style: const TextStyle(fontSize: 10, color: Colors.grey)),
onTap: () {
Get.defaultDialog(
title: "Déconnexion",
content: const Text("Voulez-vous vraiment vous déconnecter ?"),
actions: [
TextButton(
child: const Text("Non"),
onPressed: () => Get.back(),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text("Oui"),
onPressed: () async {
await clearUserData();
Get.offAll(const LoginPage());
},
),
],
Get.snackbar(
"Route non configurée",
"La route '$route' n'est pas encore configurée",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange.shade100,
);
},
);
}
return ListTile(
leading: Icon(
routeData['icon'] as IconData,
color: routeData['color'] as Color,
),
title: Text(name),
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
onTap: () {
final widget = routeData['widget'];
if (widget != null) {
Get.to(widget);
} else {
Get.snackbar(
"Non implémenté",
"Cette fonctionnalité sera bientôt disponible",
snackPosition: SnackPosition.BOTTOM,
);
}
},
);
}
return drawerItems;
/// Header de catégorie
Widget _buildCategoryHeader(String categoryName) {
return Padding(
padding: const EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
categoryName,
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
}
Future<Widget> _buildDrawerItem({
required IconData icon,
required String title,
required Color color,
String? permissionAction,
String? permissionRoute,
required VoidCallback onTap,
}) async {
if (permissionAction != null && permissionRoute != null) {
bool hasPermission = await userController.hasPermission(permissionAction, permissionRoute);
if (!hasPermission) {
return const SizedBox.shrink();
}
}
/// Header utilisateur amélioré
Widget _buildUserHeader(UserController controller) {
return Container(
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [
const CircleAvatar(
radius: 30,
backgroundImage: AssetImage("assets/youmaz2.png"),
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.name.isNotEmpty
? controller.fullName
: 'Utilisateur',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
controller.role.isNotEmpty ? controller.role : 'Aucun rôle',
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
),
if (controller.pointDeVenteDesignation.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
controller.pointDeVenteDesignation,
style: const TextStyle(
color: Colors.white60,
fontSize: 10,
),
),
],
// Indicateur de statut du cache
const SizedBox(height: 4),
Row(
children: [
Icon(
controller.isCacheReady ? Icons.check_circle : Icons.hourglass_empty,
color: controller.isCacheReady ? Colors.green : Colors.orange,
size: 12,
),
const SizedBox(width: 4),
Text(
controller.isCacheReady ? 'Menu prêt' : 'Chargement...',
style: const TextStyle(
color: Colors.white60,
fontSize: 10,
),
),
],
),
],
),
),
// Bouton de rafraîchissement pour les admins
if (controller.role == 'Super Admin' || controller.role == 'Admin') ...[
IconButton(
icon: const Icon(Icons.refresh, color: Colors.white70, size: 20),
onPressed: () async {
Get.snackbar(
"Cache",
"Rechargement des permissions...",
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 1),
);
await controller.refreshPermissions();
Get.back(); // Fermer le drawer
Get.snackbar(
"Cache",
"Permissions rechargées avec succès",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
},
tooltip: "Recharger les permissions",
),
],
// 🔧 Bouton de debug (à supprimer en production)
if (controller.role == 'Super Admin') ...[
IconButton(
icon: const Icon(Icons.bug_report, color: Colors.white70, size: 18),
onPressed: () {
// Debug des menus
final menus = controller.getUserMenus();
String debugInfo = "MENUS DEBUG:\n";
for (var i = 0; i < menus.length; i++) {
final menu = menus[i];
debugInfo += "[$i] ID:${menu['id']}, Name:'${menu['name']}', Route:'${menu['route']}'\n";
}
Get.dialog(
AlertDialog(
title: const Text("Debug Menus"),
content: SingleChildScrollView(
child: Text(debugInfo, style: const TextStyle(fontSize: 12)),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text("Fermer"),
),
],
),
);
},
tooltip: "Debug menus",
),
],
],
),
);
}
/// Item de déconnexion
Widget _buildLogoutItem() {
return ListTile(
leading: Icon(icon, color: color),
title: Text(title),
trailing: permissionAction != null
? const Icon(Icons.chevron_right, color: Colors.grey)
: null,
onTap: onTap,
leading: const Icon(Icons.logout, color: Colors.red),
title: const Text("Déconnexion"),
onTap: () {
Get.dialog(
AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
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(
"Vos permissions seront rechargées à la prochaine connexion.",
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(),
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,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
// IMPORTANT: Vider le cache de session lors de la déconnexion
await clearUserData();
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,
);
},
);
}
}
}

120
lib/Components/app_bar.dart

@ -1,31 +1,121 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/controller/userController.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final Widget? subtitle;
const CustomAppBar({
final List<Widget>? actions;
final bool automaticallyImplyLeading;
final Color? backgroundColor;
final bool isDesktop; // Add this parameter
final UserController userController = Get.put(UserController());
CustomAppBar({
Key? key,
required this.title,
this.subtitle,
this.actions,
this.automaticallyImplyLeading = true,
this.backgroundColor,
this.isDesktop = false, // Add this parameter with default value
}) : super(key: key);
@override
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0);
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0);
@override
Widget build(BuildContext context) {
return AppBar(
title: subtitle == null
? Text(title)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 20)),
subtitle!,
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
? Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
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
),
),
),
);
}
}
}

387
lib/Components/commandManagementComponents/CommandDetails.dart

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

213
lib/Components/commandManagementComponents/CommandeActions.dart

@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/client.dart';
//Classe suplementaire
class CommandeActions extends StatelessWidget {
final Commande commande;
final Function(int, StatutCommande) onStatutChanged;
final Function(Commande) onPaymentSelected;
const CommandeActions({
required this.commande,
required this.onStatutChanged,
required this.onPaymentSelected,
});
List<Widget> _buildActionButtons(BuildContext context) {
List<Widget> buttons = [];
switch (commande.statut) {
case StatutCommande.enAttente:
buttons.addAll([
_buildActionButton(
label: 'Confirmer',
icon: Icons.check_circle,
color: Colors.blue,
onPressed: () => onPaymentSelected(commande),
),
_buildActionButton(
label: 'Annuler',
icon: Icons.cancel,
color: Colors.red,
onPressed: () => _showConfirmDialog(
context,
'Annuler la commande',
'Êtes-vous sûr de vouloir annuler cette commande?',
() => onStatutChanged(commande.id!, StatutCommande.annulee),
),
),
]);
break;
case StatutCommande.confirmee:
buttons.add(
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle,
color: Colors.green.shade600, size: 16),
const SizedBox(width: 8),
Text(
'Commande confirmée',
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
break;
case StatutCommande.annulee:
buttons.add(
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
color: Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cancel, color: Colors.red.shade600, size: 16),
const SizedBox(width: 8),
Text(
'Commande annulée',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
break;
}
return buttons;
}
Widget _buildActionButton({
required String label,
required IconData icon,
required Color color,
required VoidCallback onPressed,
}) {
return ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: 16),
label: Text(label),
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 2,
),
);
}
void _showConfirmDialog(
BuildContext context,
String title,
String content,
VoidCallback onConfirm,
) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: Row(
children: [
Icon(
Icons.help_outline,
color: Colors.blue.shade600,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(fontSize: 18),
),
],
),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Annuler',
style: TextStyle(color: Colors.grey.shade600),
),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
onConfirm();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade600,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Confirmer'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Actions sur la commande',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _buildActionButtons(context),
),
],
),
);
}
}

189
lib/Components/commandManagementComponents/DiscountDialog.dart

@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/client.dart';
// Dialog pour la remise
class DiscountDialog extends StatefulWidget {
final Commande commande;
const DiscountDialog({super.key, required this.commande});
@override
_DiscountDialogState createState() => _DiscountDialogState();
}
class _DiscountDialogState extends State<DiscountDialog> {
final _pourcentageController = TextEditingController();
final _montantController = TextEditingController();
bool _isPercentage = true;
double _montantFinal = 0;
@override
void initState() {
super.initState();
_montantFinal = widget.commande.montantTotal;
}
void _calculateDiscount() {
double discount = 0;
if (_isPercentage) {
final percentage = double.tryParse(_pourcentageController.text) ?? 0;
discount = (widget.commande.montantTotal * percentage) / 100;
} else {
discount = double.tryParse(_montantController.text) ?? 0;
}
setState(() {
_montantFinal = widget.commande.montantTotal - discount;
if (_montantFinal < 0) _montantFinal = 0;
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Appliquer une remise'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Montant original: ${widget.commande.montantTotal.toStringAsFixed(2)} MGA'),
const SizedBox(height: 16),
// Choix du type de remise
Row(
children: [
Expanded(
child: RadioListTile<bool>(
title: const Text('Pourcentage'),
value: true,
groupValue: _isPercentage,
onChanged: (value) {
setState(() {
_isPercentage = value!;
_calculateDiscount();
});
},
),
),
Expanded(
child: RadioListTile<bool>(
title: const Text('Montant fixe'),
value: false,
groupValue: _isPercentage,
onChanged: (value) {
setState(() {
_isPercentage = value!;
_calculateDiscount();
});
},
),
),
],
),
const SizedBox(height: 16),
if (_isPercentage)
TextField(
controller: _pourcentageController,
decoration: const InputDecoration(
labelText: 'Pourcentage de remise',
suffixText: '%',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) => _calculateDiscount(),
)
else
TextField(
controller: _montantController,
decoration: const InputDecoration(
labelText: 'Montant de remise',
suffixText: 'MGA',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) => _calculateDiscount(),
),
const SizedBox(height: 16),
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 final:'),
Text(
'${_montantFinal.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
if (_montantFinal < widget.commande.montantTotal)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Économie:'),
Text(
'${(widget.commande.montantTotal - _montantFinal).toStringAsFixed(2)} MGA',
style: const TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _montantFinal < widget.commande.montantTotal
? () {
final pourcentage = _isPercentage
? double.tryParse(_pourcentageController.text)
: null;
final montant = !_isPercentage
? double.tryParse(_montantController.text)
: null;
Navigator.pop(context, {
'pourcentage': pourcentage,
'montant': montant,
'montantFinal': _montantFinal,
});
}
: null,
child: const Text('Appliquer'),
),
],
);
}
@override
void dispose() {
_pourcentageController.dispose();
_montantController.dispose();
super.dispose();
}
}

136
lib/Components/commandManagementComponents/GiftSelectionDialog.dart

@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
// Dialog pour sélectionner un cadeau
class GiftSelectionDialog extends StatefulWidget {
final Commande commande;
const GiftSelectionDialog({super.key, required this.commande});
@override
_GiftSelectionDialogState createState() => _GiftSelectionDialogState();
}
class _GiftSelectionDialogState extends State<GiftSelectionDialog> {
List<Product> _products = [];
List<Product> _filteredProducts = [];
final _searchController = TextEditingController();
Product? _selectedProduct;
@override
void initState() {
super.initState();
_loadProducts();
_searchController.addListener(_filterProducts);
}
Future<void> _loadProducts() async {
final products = await AppDatabase.instance.getProducts();
setState(() {
_products = products.where((p) => p.stock > 0).toList();
_filteredProducts = _products;
});
}
void _filterProducts() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredProducts = _products.where((product) {
return product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false) ||
(product.category.toLowerCase().contains(query));
}).toList();
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Sélectionner un cadeau'),
content: SizedBox(
width: double.maxFinite,
height: 400,
child: Column(
children: [
TextField(
controller: _searchController,
decoration: const InputDecoration(
labelText: 'Rechercher un produit',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
return Card(
child: ListTile(
leading: product.image != null
? Image.network(
product.image!,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.image_not_supported),
)
: const Icon(Icons.phone_android),
title: Text(product.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Catégorie: ${product.category}'),
Text('Stock: ${product.stock}'),
if (product.reference != null)
Text('Réf: ${product.reference}'),
],
),
trailing: Radio<Product>(
value: product,
groupValue: _selectedProduct,
onChanged: (value) {
setState(() {
_selectedProduct = value;
});
},
),
onTap: () {
setState(() {
_selectedProduct = product;
});
},
),
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _selectedProduct != null
? () => Navigator.pop(context, _selectedProduct)
: null,
child: const Text('Ajouter le cadeau'),
),
],
);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
}

234
lib/Components/commandManagementComponents/PaswordRequired.dart

@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class PasswordVerificationDialog extends StatefulWidget {
final String title;
final String message;
final Function(String) onPasswordVerified;
const PasswordVerificationDialog({
Key? key,
required this.title,
required this.message,
required this.onPasswordVerified,
}) : super(key: key);
@override
_PasswordVerificationDialogState createState() => _PasswordVerificationDialogState();
}
class _PasswordVerificationDialogState extends State<PasswordVerificationDialog> {
final TextEditingController _passwordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isLoading = false;
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
title: Row(
children: [
Icon(
Icons.security,
color: Colors.blue.shade700,
size: 28,
),
const SizedBox(width: 10),
Expanded(
child: Text(
widget.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.message,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
const SizedBox(height: 20),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: TextField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
autofocus: true,
decoration: InputDecoration(
labelText: 'Mot de passe',
prefixIcon: Icon(
Icons.lock_outline,
color: Colors.blue.shade600,
),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible ? Icons.visibility_off : Icons.visibility,
color: Colors.grey.shade600,
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: (value) => _verifyPassword(),
),
),
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber.shade200),
),
child: Row(
children: [
Icon(
Icons.info_outline,
color: Colors.amber.shade700,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Saisissez votre mot de passe pour confirmer cette action',
style: TextStyle(
fontSize: 12,
color: Colors.amber.shade700,
),
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
child: Text(
'Annuler',
style: TextStyle(
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
),
ElevatedButton(
onPressed: _isLoading ? null : _verifyPassword,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text('Vérifier'),
),
],
);
}
void _verifyPassword() async {
final password = _passwordController.text.trim();
if (password.isEmpty) {
Get.snackbar(
'Erreur',
'Veuillez saisir votre mot de passe',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
return;
}
setState(() {
_isLoading = true;
});
try {
final database = AppDatabase.instance;
final isValid = await database.verifyCurrentUserPassword(password);
setState(() {
_isLoading = false;
});
if (isValid) {
Navigator.of(context).pop();
widget.onPasswordVerified(password);
} else {
Get.snackbar(
'Erreur',
'Mot de passe incorrect',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
_passwordController.clear();
}
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Une erreur est survenue lors de la vérification',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
print("Erreur vérification mot de passe: $e");
}
}
}

8
lib/Components/commandManagementComponents/PaymentMethod.dart

@ -0,0 +1,8 @@
import 'package:youmazgestion/Components/paymentType.dart';
class PaymentMethod {
final PaymentType type;
final double amountGiven;
PaymentMethod({required this.type, this.amountGiven = 0});
}

265
lib/Components/commandManagementComponents/PaymentMethodDialog.dart

@ -0,0 +1,265 @@
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/Components/commandManagementComponents/PaymentMethod.dart';
import 'package:youmazgestion/Components/paymentType.dart';
import 'package:youmazgestion/Models/client.dart';
class PaymentMethodDialog extends StatefulWidget {
final Commande commande;
const PaymentMethodDialog({super.key, required this.commande});
@override
_PaymentMethodDialogState createState() => _PaymentMethodDialogState();
}
class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
PaymentType _selectedPayment = PaymentType.cash;
final _amountController = TextEditingController();
void _validatePayment() {
final montantFinal = 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, PaymentMethod(
type: _selectedPayment,
amountGiven: _selectedPayment == PaymentType.cash
? double.parse(_amountController.text)
: montantFinal,
));
}
@override
void initState() {
super.initState();
final montantFinal = widget.commande.montantTotal;
_amountController.text = montantFinal.toStringAsFixed(2);
}
@override
void dispose() {
_amountController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final amount = double.tryParse(_amountController.text) ?? 0;
final montantFinal = widget.commande.montantTotal;
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: [
// Affichage du montant à payer (simplifié)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('${montantFinal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
),
const 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),
],
),
),
),
);
}
}

7
lib/Components/commandManagementComponents/PaymentType.dart

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

411
lib/Components/newCommandComponents/CadeauDialog.dart

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

331
lib/Components/newCommandComponents/RemiseDialog.dart

@ -0,0 +1,331 @@
// Components/RemiseDialog.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Models/produit.dart';
class RemiseDialog extends StatefulWidget {
final Product product;
final int quantite;
final double prixUnitaire;
final DetailCommande? detailExistant;
const RemiseDialog({
super.key,
required this.product,
required this.quantite,
required this.prixUnitaire,
this.detailExistant,
});
@override
State<RemiseDialog> createState() => _RemiseDialogState();
}
class _RemiseDialogState extends State<RemiseDialog> {
final _formKey = GlobalKey<FormState>();
final _valeurController = TextEditingController();
RemiseType _selectedType = RemiseType.pourcentage;
double _montantRemise = 0.0;
double _prixFinal = 0.0;
late double _sousTotal;
@override
void initState() {
super.initState();
_sousTotal = widget.quantite * widget.prixUnitaire;
// Si on modifie une remise existante
if (widget.detailExistant?.aRemise == true) {
_selectedType = widget.detailExistant!.remiseType!;
_valeurController.text = widget.detailExistant!.remiseValeur.toString();
_calculateRemise();
} else {
_prixFinal = _sousTotal;
}
}
void _calculateRemise() {
final valeur = double.tryParse(_valeurController.text) ?? 0.0;
setState(() {
if (_selectedType == RemiseType.pourcentage) {
final pourcentage = valeur.clamp(0.0, 100.0);
_montantRemise = _sousTotal * (pourcentage / 100);
} else {
_montantRemise = valeur.clamp(0.0, _sousTotal);
}
_prixFinal = _sousTotal - _montantRemise;
});
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return AlertDialog(
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.discount, color: Colors.orange.shade700),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Appliquer une remise',
style: TextStyle(fontSize: isMobile ? 16 : 18),
),
),
],
),
content: Container(
width: isMobile ? double.maxFinite : 400,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informations du produit
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.product.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Quantité: ${widget.quantite}',
style: const TextStyle(fontSize: 12),
),
Text(
'Prix unitaire: ${widget.prixUnitaire.toStringAsFixed(2)} MGA',
style: const TextStyle(fontSize: 12),
),
Text(
'Sous-total: ${_sousTotal.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 16),
// Type de remise
const Text(
'Type de remise:',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: RadioListTile<RemiseType>(
title: const Text('Pourcentage (%)', style: TextStyle(fontSize: 12)),
value: RemiseType.pourcentage,
groupValue: _selectedType,
onChanged: (value) {
setState(() {
_selectedType = value!;
_calculateRemise();
});
},
contentPadding: EdgeInsets.zero,
dense: true,
),
),
Expanded(
child: RadioListTile<RemiseType>(
title: const Text('Montant (MGA)', style: TextStyle(fontSize: 12)),
value: RemiseType.montant,
groupValue: _selectedType,
onChanged: (value) {
setState(() {
_selectedType = value!;
_calculateRemise();
});
},
contentPadding: EdgeInsets.zero,
dense: true,
),
),
],
),
const SizedBox(height: 16),
// Valeur de la remise
TextFormField(
controller: _valeurController,
decoration: InputDecoration(
labelText: _selectedType == RemiseType.pourcentage
? 'Pourcentage (0-100)'
: 'Montant en MGA',
prefixIcon: Icon(
_selectedType == RemiseType.pourcentage
? Icons.percent
: Icons.attach_money,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer une valeur';
}
final valeur = double.tryParse(value);
if (valeur == null || valeur < 0) {
return 'Valeur invalide';
}
if (_selectedType == RemiseType.pourcentage && valeur > 100) {
return 'Le pourcentage ne peut pas dépasser 100%';
}
if (_selectedType == RemiseType.montant && valeur > _sousTotal) {
return 'La remise ne peut pas dépasser le sous-total';
}
return null;
},
onChanged: (value) => _calculateRemise(),
),
const SizedBox(height: 16),
// Aperçu du calcul
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Sous-total:', style: TextStyle(fontSize: 12)),
Text(
'${_sousTotal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontSize: 12),
),
],
),
if (_montantRemise > 0) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Remise ${_selectedType == RemiseType.pourcentage ? "(${_valeurController.text}%)" : ""}:',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
),
),
Text(
'-${_montantRemise.toStringAsFixed(2)} MGA',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
],
const Divider(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Prix final:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'${_prixFinal.toStringAsFixed(2)} MGA',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.green.shade700,
),
),
],
),
],
),
),
],
),
),
),
actions: [
if (widget.detailExistant?.aRemise == true)
TextButton.icon(
onPressed: () => Navigator.of(context).pop('supprimer'),
icon: const Icon(Icons.delete, color: Colors.red),
label: const Text('Supprimer remise', style: TextStyle(color: Colors.red)),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
final valeur = double.parse(_valeurController.text);
Navigator.of(context).pop({
'type': _selectedType,
'valeur': valeur,
'montantRemise': _montantRemise,
'prixFinal': _prixFinal,
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade700,
foregroundColor: Colors.white,
),
child: const Text('Appliquer'),
),
],
);
}
@override
void dispose() {
_valeurController.dispose();
super.dispose();
}
}

7
lib/Components/paymentType.dart

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

354
lib/Models/Client.dart

@ -1,4 +1,4 @@
// Models/client.dart
// Models/client.dart - Version corrigée pour MySQL
class Client {
final int? id;
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) {
return Client(
id: map['id'],
nom: map['nom'],
prenom: map['prenom'],
email: map['email'],
telephone: map['telephone'],
adresse: map['adresse'],
dateCreation: DateTime.parse(map['dateCreation']),
actif: map['actif'] == 1,
id: map['id'] as int?,
nom: map['nom'] as String,
prenom: map['prenom'] as String,
email: map['email'] as String,
telephone: map['telephone'] as String,
adresse: map['adresse'] as String?,
dateCreation: _parseDateTime(map['dateCreation']),
actif: (map['actif'] as int?) == 1,
);
}
String get nomComplet => '$prenom $nom';
}
// Models/commande.dart
enum StatutCommande {
enAttente,
confirmee,
enPreparation,
expediee,
livree,
annulee
}
@ -67,8 +87,8 @@ class Commande {
final double montantTotal;
final String? notes;
final DateTime? dateLivraison;
// Données du client (pour les jointures)
final int? commandeurId;
final int? validateurId;
final String? clientNom;
final String? clientPrenom;
final String? clientEmail;
@ -77,15 +97,35 @@ class Commande {
this.id,
required this.clientId,
required this.dateCommande,
this.statut = StatutCommande.enAttente,
required this.statut,
required this.montantTotal,
this.notes,
this.dateLivraison,
this.commandeurId,
this.validateurId,
this.clientNom,
this.clientPrenom,
this.clientEmail,
});
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() {
return {
'id': id,
@ -95,59 +135,49 @@ class Commande {
'montantTotal': montantTotal,
'notes': notes,
'dateLivraison': dateLivraison?.toIso8601String(),
'commandeurId': commandeurId,
'validateurId': validateurId,
};
}
factory Commande.fromMap(Map<String, dynamic> map) {
return Commande(
id: map['id'],
clientId: map['clientId'],
dateCommande: DateTime.parse(map['dateCommande']),
statut: StatutCommande.values[map['statut']],
montantTotal: map['montantTotal'].toDouble(),
notes: map['notes'],
id: map['id'] as int?,
clientId: map['clientId'] as int,
dateCommande: Client._parseDateTime(map['dateCommande']),
statut: StatutCommande.values[(map['statut'] as int)],
montantTotal: (map['montantTotal'] as num).toDouble(),
notes: map['notes'] as String?,
dateLivraison: map['dateLivraison'] != null
? DateTime.parse(map['dateLivraison'])
? Client._parseDateTime(map['dateLivraison'])
: null,
clientNom: map['clientNom'],
clientPrenom: map['clientPrenom'],
clientEmail: map['clientEmail'],
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?,
);
}
}
String get statutLibelle {
switch (statut) {
case StatutCommande.enAttente:
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.enPreparation:
return 'En préparation';
case StatutCommande.expediee:
return 'Expédiée';
case StatutCommande.livree:
return 'Livrée';
case StatutCommande.annulee:
return 'Annulée';
}
}
String get clientNomComplet =>
clientPrenom != null && clientNom != null
? '$clientPrenom $clientNom'
: 'Client inconnu';
// REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci :
enum RemiseType {
pourcentage,
montant
}
// Models/detail_commande.dart
class DetailCommande {
final int? id;
final int commandeId;
final int produitId;
final int quantite;
final double prixUnitaire;
final double sousTotal;
// Données du produit (pour les jointures)
final double sousTotal; // Prix unitaire × quantité (avant remise)
final RemiseType? remiseType;
final double remiseValeur; // Valeur de la remise (% ou montant)
final double montantRemise; // Montant de la remise calculé
final double prixFinal; // Prix final après remise
final bool estCadeau; // NOUVEAU : Indique si l'article est un cadeau
final String? produitNom;
final String? produitImage;
final String? produitReference;
@ -159,11 +189,195 @@ class DetailCommande {
required this.quantite,
required this.prixUnitaire,
required this.sousTotal,
this.remiseType,
this.remiseValeur = 0.0,
this.montantRemise = 0.0,
required this.prixFinal,
this.estCadeau = false,
this.produitNom,
this.produitImage,
this.produitReference,
});
// Constructeur pour créer un détail sans remise
factory DetailCommande.sansRemise({
int? id,
required int commandeId,
required int produitId,
required int quantite,
required double prixUnitaire,
bool estCadeau = false,
String? produitNom,
String? produitImage,
String? produitReference,
}) {
final sousTotal = quantite * prixUnitaire;
final prixFinal = estCadeau ? 0.0 : sousTotal;
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
prixFinal: prixFinal,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Constructeur pour créer un cadeau
factory DetailCommande.cadeau({
int? id,
required int commandeId,
required int produitId,
required int quantite,
required double prixUnitaire,
String? produitNom,
String? produitImage,
String? produitReference,
}) {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: quantite * prixUnitaire,
prixFinal: 0.0, // Prix final à 0 pour un cadeau
estCadeau: true,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Méthode pour appliquer une remise (ne s'applique pas aux cadeaux)
DetailCommande appliquerRemise({
required RemiseType type,
required double valeur,
}) {
// Les remises ne s'appliquent pas aux cadeaux
if (estCadeau) return this;
double montantRemiseCalcule = 0.0;
if (type == RemiseType.pourcentage) {
final pourcentage = valeur.clamp(0.0, 100.0);
montantRemiseCalcule = sousTotal * (pourcentage / 100);
} else {
montantRemiseCalcule = valeur.clamp(0.0, sousTotal);
}
final prixFinalCalcule = sousTotal - montantRemiseCalcule;
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: type,
remiseValeur: valeur,
montantRemise: montantRemiseCalcule,
prixFinal: prixFinalCalcule,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Méthode pour supprimer la remise
DetailCommande supprimerRemise() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: null,
remiseValeur: 0.0,
montantRemise: 0.0,
prixFinal: estCadeau ? 0.0 : sousTotal,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Méthode pour convertir en cadeau
DetailCommande convertirEnCadeau() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: null, // Supprimer les remises lors de la conversion en cadeau
remiseValeur: 0.0,
montantRemise: 0.0,
prixFinal: 0.0,
estCadeau: true,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Méthode pour convertir en article normal
DetailCommande convertirEnArticleNormal() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: remiseType,
remiseValeur: remiseValeur,
montantRemise: montantRemise,
prixFinal: estCadeau ? sousTotal - montantRemise : prixFinal,
estCadeau: false,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Getters utiles
bool get aRemise => remiseType != null && montantRemise > 0 && !estCadeau;
double get pourcentageRemise {
if (!aRemise) return 0.0;
return (montantRemise / sousTotal) * 100;
}
String get remiseDescription {
if (estCadeau) return 'CADEAU';
if (!aRemise) return '';
if (remiseType == RemiseType.pourcentage) {
return '-${remiseValeur.toStringAsFixed(0)}%';
} else {
return '-${montantRemise.toStringAsFixed(2)} MGA';
}
}
// NOUVEAU : Description du statut de l'article
String get statutDescription {
if (estCadeau) return 'CADEAU OFFERT';
if (aRemise) return 'AVEC REMISE';
return 'PRIX NORMAL';
}
Map<String, dynamic> toMap() {
return {
'id': id,
@ -172,20 +386,40 @@ class DetailCommande {
'quantite': quantite,
'prixUnitaire': prixUnitaire,
'sousTotal': sousTotal,
'remise_type': remiseType?.name,
'remise_valeur': remiseValeur,
'montant_remise': montantRemise,
'prix_final': prixFinal,
'est_cadeau': estCadeau ? 1 : 0,
};
}
factory DetailCommande.fromMap(Map<String, dynamic> map) {
RemiseType? type;
if (map['remise_type'] != null) {
if (map['remise_type'] == 'pourcentage') {
type = RemiseType.pourcentage;
} else if (map['remise_type'] == 'montant') {
type = RemiseType.montant;
}
}
return DetailCommande(
id: map['id'],
commandeId: map['commandeId'],
produitId: map['produitId'],
quantite: map['quantite'],
prixUnitaire: map['prixUnitaire'].toDouble(),
sousTotal: map['sousTotal'].toDouble(),
produitNom: map['produitNom'],
produitImage: map['produitImage'],
produitReference: map['produitReference'],
id: map['id'] as int?,
commandeId: map['commandeId'] as int,
produitId: map['produitId'] as int,
quantite: map['quantite'] as int,
prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
sousTotal: (map['sousTotal'] as num).toDouble(),
remiseType: type,
remiseValeur: (map['remise_valeur'] as num?)?.toDouble() ?? 0.0,
montantRemise: (map['montant_remise'] as num?)?.toDouble() ?? 0.0,
prixFinal: (map['prix_final'] as num?)?.toDouble() ??
(map['sousTotal'] as num).toDouble(),
estCadeau: (map['est_cadeau'] as int?) == 1,
produitNom: map['produitNom'] as String?,
produitImage: map['produitImage'] as String?,
produitReference: map['produitReference'] as String?,
);
}
}

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,
};
}
}

167
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 {
int? id;
final int? id;
final String name;
final double price;
final String? image;
final String category;
final int? stock;
final int stock;
final String? description;
String? qrCode;
String? qrCode;
final String? reference;
final int? pointDeVenteId;
final String? marque;
final String? ram;
final String? memoireInterne;
final String? imei;
Product({
this.id,
@ -16,44 +25,138 @@ class Product {
this.image,
required this.category,
this.stock = 0,
this.description = '',
this.description,
this.qrCode,
this.reference,
this.pointDeVenteId,
this.marque,
this.ram,
this.memoireInterne,
this.imei,
});
// Vérifie si le stock est défini
bool isStockDefined() {
if (stock != null) {
print("stock is defined : $stock $name");
return true;
} else {
return false;
return stock > 0;
}
// Méthode helper pour convertir de façon sécurisée
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 {
'id': id,
'name': name,
'price': price,
'image': image ?? '',
'category': category,
'stock': stock ?? 0,
'description': description ?? '',
'qrCode': qrCode ?? '',
'reference': reference ?? '',
};
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,
'name': name,
'price': price,
'image': image,
'category': category,
'stock': stock,
'description': description,
'qrCode': qrCode,
'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(
id: map['id'],
name: map['name'],
price: map['price'],
image: map['image'],
category: map['category'],
stock: map['stock'],
description: map['description'],
qrCode: map['qrCode'],
reference: map['reference'],
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
image: image ?? this.image,
category: category ?? this.category,
stock: stock ?? this.stock,
description: description ?? this.description,
qrCode: qrCode ?? this.qrCode,
reference: reference ?? this.reference,
pointDeVenteId: pointDeVenteId ?? this.pointDeVenteId,
marque: marque ?? this.marque,
ram: ram ?? this.ram,
memoireInterne: memoireInterne ?? this.memoireInterne,
imei: imei ?? this.imei,
);
}
}

28
lib/Models/users.dart

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

0
lib/Services/GestionStockDatabase.dart

258
lib/Services/PermissionCacheService.dart

@ -0,0 +1,258 @@
import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class PermissionCacheService extends GetxController {
static final PermissionCacheService instance = PermissionCacheService._init();
PermissionCacheService._init();
// Cache en mémoire optimisé
final Map<String, Map<String, bool>> _permissionCache = {};
final Map<String, List<Map<String, dynamic>>> _menuCache = {};
bool _isLoaded = false;
String _currentUsername = '';
/// OPTIMISÉ: Une seule requête complexe pour charger tout
Future<void> loadUserPermissions(String username) async {
if (_isLoaded && _currentUsername == username && _permissionCache.containsKey(username)) {
print("📋 Permissions déjà en cache pour: $username");
return;
}
print("🔄 Chargement OPTIMISÉ des permissions pour: $username");
final stopwatch = Stopwatch()..start();
try {
final db = AppDatabase.instance;
// 🚀 UNE SEULE REQUÊTE pour tout récupérer
final userPermissions = await _getUserPermissionsOptimized(db, username);
// Organiser les données
Map<String, bool> permissions = {};
Set<Map<String, dynamic>> accessibleMenus = {};
for (var row in userPermissions) {
final menuId = row['menu_id'] as int;
final menuName = row['menu_name'] as String;
final menuRoute = row['menu_route'] as String;
final permissionName = row['permission_name'] as String;
// Ajouter la permission
final key = "${permissionName}_$menuRoute";
permissions[key] = true;
// Ajouter le menu aux accessibles
accessibleMenus.add({
'id': menuId,
'name': menuName,
'route': menuRoute,
});
}
// Mettre en cache
_permissionCache[username] = permissions;
_menuCache[username] = accessibleMenus.toList();
_currentUsername = username;
_isLoaded = true;
stopwatch.stop();
print("✅ Permissions chargées en ${stopwatch.elapsedMilliseconds}ms");
print(" - ${permissions.length} permissions");
print(" - ${accessibleMenus.length} menus accessibles");
} catch (e) {
stopwatch.stop();
print("❌ Erreur après ${stopwatch.elapsedMilliseconds}ms: $e");
rethrow;
}
}
/// 🚀 NOUVELLE MÉTHODE: Une seule requête optimisée
Future<List<Map<String, dynamic>>> _getUserPermissionsOptimized(
AppDatabase db, String username) async {
final connection = await db.database;
final result = await connection.query('''
SELECT DISTINCT
m.id as menu_id,
m.name as menu_name,
m.route as menu_route,
p.name as permission_name
FROM users u
INNER JOIN roles r ON u.role_id = r.id
INNER JOIN role_menu_permissions rmp ON r.id = rmp.role_id
INNER JOIN menu m ON rmp.menu_id = m.id
INNER JOIN permissions p ON rmp.permission_id = p.id
WHERE u.username = ?
ORDER BY m.name, p.name
''', [username]);
return result.map((row) => row.fields).toList();
}
/// Vérification rapide depuis le cache
bool hasPermission(String username, String permissionName, String menuRoute) {
final userPermissions = _permissionCache[username];
if (userPermissions == null) {
print("⚠️ Cache non initialisé pour: $username");
return false;
}
final key = "${permissionName}_$menuRoute";
return userPermissions[key] ?? false;
}
/// Récupération rapide des menus
List<Map<String, dynamic>> getUserMenus(String username) {
return _menuCache[username] ?? [];
}
/// Vérification d'accès menu
bool hasMenuAccess(String username, String menuRoute) {
final userMenus = _menuCache[username] ?? [];
return userMenus.any((menu) => menu['route'] == menuRoute);
}
/// Préchargement asynchrone en arrière-plan
Future<void> preloadUserDataAsync(String username) async {
// Lancer en arrière-plan sans bloquer l'UI
unawaited(_preloadInBackground(username));
}
Future<void> _preloadInBackground(String username) async {
try {
print("🔄 Préchargement en arrière-plan pour: $username");
await loadUserPermissions(username);
print("✅ Préchargement terminé");
} catch (e) {
print("⚠️ Erreur préchargement: $e");
}
}
/// Préchargement synchrone (pour la connexion)
Future<void> preloadUserData(String username) async {
try {
print("🔄 Préchargement synchrone pour: $username");
await loadUserPermissions(username);
print("✅ Données préchargées avec succès");
} catch (e) {
print("❌ Erreur lors du préchargement: $e");
// Ne pas bloquer la connexion
}
}
/// Vider le cache
void clearAllCache() {
_permissionCache.clear();
_menuCache.clear();
_isLoaded = false;
_currentUsername = '';
print("🗑️ Cache vidé complètement");
}
/// Rechargement forcé
Future<void> refreshUserPermissions(String username) async {
_permissionCache.remove(username);
_menuCache.remove(username);
_isLoaded = false;
await loadUserPermissions(username);
print("🔄 Permissions rechargées pour: $username");
}
/// Status du cache
bool get isLoaded => _isLoaded && _currentUsername.isNotEmpty;
String get currentCachedUser => _currentUsername;
/// Statistiques
Map<String, dynamic> getCacheStats() {
return {
'is_loaded': _isLoaded,
'current_user': _currentUsername,
'users_cached': _permissionCache.length,
'total_permissions': _permissionCache.values
.map((perms) => perms.length)
.fold(0, (a, b) => a + b),
'total_menus': _menuCache.values
.map((menus) => menus.length)
.fold(0, (a, b) => a + b),
};
}
/// Debug amélioré
void debugPrintCache() {
print("=== DEBUG CACHE OPTIMISÉ ===");
print("Chargé: $_isLoaded");
print("Utilisateur actuel: $_currentUsername");
print("Utilisateurs en cache: ${_permissionCache.keys.toList()}");
for (var username in _permissionCache.keys) {
final permissions = _permissionCache[username]!;
final menus = _menuCache[username] ?? [];
print("$username: ${permissions.length} permissions, ${menus.length} menus");
// Détail des menus pour debug
for (var menu in menus.take(3)) {
print("${menu['name']} (${menu['route']})");
}
}
print("============================");
}
/// NOUVEAU: Validation de l'intégrité du cache
Future<bool> validateCacheIntegrity(String username) async {
if (!_permissionCache.containsKey(username)) {
return false;
}
try {
final db = AppDatabase.instance;
final connection = await db.database;
// Vérification rapide: compter les permissions de l'utilisateur
final result = await connection.query('''
SELECT COUNT(DISTINCT CONCAT(p.name, '_', m.route)) as permission_count
FROM users u
INNER JOIN roles r ON u.role_id = r.id
INNER JOIN role_menu_permissions rmp ON r.id = rmp.role_id
INNER JOIN menu m ON rmp.menu_id = m.id
INNER JOIN permissions p ON rmp.permission_id = p.id
WHERE u.username = ?
''', [username]);
final dbCount = result.first['permission_count'] as int;
final cacheCount = _permissionCache[username]!.length;
final isValid = dbCount == cacheCount;
if (!isValid) {
print("⚠️ Cache invalide: DB=$dbCount, Cache=$cacheCount");
}
return isValid;
} catch (e) {
print("❌ Erreur validation cache: $e");
return false;
}
}
/// NOUVEAU: Rechargement intelligent
Future<void> smartRefresh(String username) async {
final isValid = await validateCacheIntegrity(username);
if (!isValid) {
print("🔄 Cache invalide, rechargement nécessaire");
await refreshUserPermissions(username);
} else {
print("✅ Cache valide, pas de rechargement nécessaire");
}
}
}
/// Extension pour éviter l'import de dart:async
void unawaited(Future future) {
// Ignorer le warning sur le Future non attendu
future.catchError((error) {
print("Erreur tâche en arrière-plan: $error");
});
}

304
lib/Services/Script.sql

@ -0,0 +1,304 @@
-- Script SQL pour créer la base de données guycom_database_v1
-- Création des tables et insertion des données par défaut
-- =====================================================
-- CRÉATION DES TABLES
-- =====================================================
-- Table permissions
CREATE TABLE `permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table menu
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`route` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table roles
CREATE TABLE `roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`designation` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `designation` (`designation`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table points_de_vente
CREATE TABLE `points_de_vente` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nom` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `nom` (`nom`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table clients
CREATE TABLE `clients` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nom` varchar(255) NOT NULL,
`prenom` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`telephone` varchar(255) NOT NULL,
`adresse` varchar(500) DEFAULT NULL,
`dateCreation` datetime NOT NULL,
`actif` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
KEY `idx_clients_email` (`email`),
KEY `idx_clients_telephone` (`telephone`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table users
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`lastname` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`username` varchar(255) NOT NULL,
`role_id` int(11) NOT NULL,
`point_de_vente_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `username` (`username`),
KEY `role_id` (`role_id`),
KEY `point_de_vente_id` (`point_de_vente_id`),
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`),
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table products
CREATE TABLE `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`price` decimal(10,2) NOT NULL,
`image` varchar(2000) DEFAULT NULL,
`category` varchar(255) NOT NULL,
`stock` int(11) NOT NULL DEFAULT 0,
`description` varchar(1000) DEFAULT NULL,
`qrCode` varchar(500) DEFAULT NULL,
`reference` varchar(255) DEFAULT NULL,
`point_de_vente_id` int(11) DEFAULT NULL,
`marque` varchar(255) DEFAULT NULL,
`ram` varchar(100) DEFAULT NULL,
`memoire_interne` varchar(100) DEFAULT NULL,
`imei` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `imei` (`imei`),
KEY `point_de_vente_id` (`point_de_vente_id`),
KEY `idx_products_category` (`category`),
KEY `idx_products_reference` (`reference`),
KEY `idx_products_imei` (`imei`),
CONSTRAINT `products_ibfk_1` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=127 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table commandes
CREATE TABLE `commandes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`clientId` int(11) NOT NULL,
`dateCommande` datetime NOT NULL,
`statut` int(11) NOT NULL DEFAULT 0,
`montantTotal` decimal(10,2) NOT NULL,
`notes` varchar(1000) DEFAULT NULL,
`dateLivraison` datetime DEFAULT NULL,
`commandeurId` int(11) DEFAULT NULL,
`validateurId` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `commandeurId` (`commandeurId`),
KEY `validateurId` (`validateurId`),
KEY `idx_commandes_client` (`clientId`),
KEY `idx_commandes_date` (`dateCommande`),
CONSTRAINT `commandes_ibfk_1` FOREIGN KEY (`commandeurId`) REFERENCES `users` (`id`),
CONSTRAINT `commandes_ibfk_2` FOREIGN KEY (`validateurId`) REFERENCES `users` (`id`),
CONSTRAINT `commandes_ibfk_3` FOREIGN KEY (`clientId`) REFERENCES `clients` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table details_commandes
CREATE TABLE `details_commandes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commandeId` int(11) NOT NULL,
`produitId` int(11) NOT NULL,
`quantite` int(11) NOT NULL,
`prixUnitaire` decimal(10,2) NOT NULL,
`sousTotal` decimal(10,2) NOT NULL,
`remise_type` enum('pourcentage','montant') DEFAULT NULL,
`remise_valeur` decimal(10,2) DEFAULT 0.00,
`montant_remise` decimal(10,2) DEFAULT 0.00,
`prix_final` decimal(10,2) NOT NULL DEFAULT 0.00,
`est_cadeau` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `produitId` (`produitId`),
KEY `idx_details_commande` (`commandeId`),
KEY `idx_est_cadeau` (`est_cadeau`),
CONSTRAINT `details_commandes_ibfk_1` FOREIGN KEY (`commandeId`) REFERENCES `commandes` (`id`) ON DELETE CASCADE,
CONSTRAINT `details_commandes_ibfk_2` FOREIGN KEY (`produitId`) REFERENCES `products` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table role_permissions
CREATE TABLE `role_permissions` (
`role_id` int(11) NOT NULL,
`permission_id` int(11) NOT NULL,
PRIMARY KEY (`role_id`,`permission_id`),
KEY `permission_id` (`permission_id`),
CONSTRAINT `role_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_permissions_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table role_menu_permissions
CREATE TABLE `role_menu_permissions` (
`role_id` int(11) NOT NULL,
`menu_id` int(11) NOT NULL,
`permission_id` int(11) NOT NULL,
PRIMARY KEY (`role_id`,`menu_id`,`permission_id`),
KEY `menu_id` (`menu_id`),
KEY `permission_id` (`permission_id`),
CONSTRAINT `role_menu_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_menu_permissions_ibfk_2` FOREIGN KEY (`menu_id`) REFERENCES `menu` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_menu_permissions_ibfk_3` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- =====================================================
-- INSERTION DES DONNÉES PAR DÉFAUT
-- =====================================================
-- Insertion des permissions par défaut
INSERT INTO `permissions` (`name`) VALUES
('view'),
('create'),
('update'),
('delete'),
('admin'),
('manage'),
('read');
-- Insertion des menus par défaut
INSERT INTO `menu` (`name`, `route`) VALUES
('Accueil', '/accueil'),
('Ajouter un utilisateur', '/ajouter-utilisateur'),
('Modifier/Supprimer un utilisateur', '/modifier-utilisateur'),
('Ajouter un produit', '/ajouter-produit'),
('Modifier/Supprimer un produit', '/modifier-produit'),
('Bilan', '/bilan'),
('Gérer les rôles', '/gerer-roles'),
('Gestion de stock', '/gestion-stock'),
('Historique', '/historique'),
('Déconnexion', '/deconnexion'),
('Nouvelle commande', '/nouvelle-commande'),
('Gérer les commandes', '/gerer-commandes'),
('Points de vente', '/points-de-vente');
-- Insertion des rôles par défaut
INSERT INTO `roles` (`designation`) VALUES
('Super Admin'),
('Admin'),
('User'),
('commercial'),
('caisse');
-- Attribution de TOUTES les permissions à TOUS les menus pour le Super Admin
-- On utilise une sous-requête pour récupérer l'ID réel du rôle Super Admin
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'Super Admin';
-- Attribution de permissions basiques pour Admin
-- Accès en lecture/écriture à la plupart des menus sauf gestion des rôles
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'Admin'
AND m.name != 'Gérer les rôles'
AND p.name IN ('view', 'create', 'update', 'read');
-- Attribution de permissions basiques pour User
-- Accès principalement en lecture et quelques actions de base
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'User'
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gérer les commandes', 'Gestion de stock', 'Historique')
AND p.name IN ('view', 'read', 'create');
-- Attribution de permissions pour Commercial
-- Accès aux commandes, clients, produits
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'commercial'
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gérer les commandes', 'Bilan', 'Historique')
AND p.name IN ('view', 'create', 'update', 'read');
-- Attribution de permissions pour Caisse
-- Accès principalement aux commandes et stock
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'caisse'
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gestion de stock')
AND p.name IN ('view', 'create', 'read');
-- Insertion du Super Admin par défaut
-- On utilise une sous-requête pour récupérer l'ID réel du rôle Super Admin
INSERT INTO `users` (`name`, `lastname`, `email`, `password`, `username`, `role_id`)
SELECT 'Super', 'Admin', 'superadmin@youmazgestion.com', 'admin123', 'superadmin', r.id
FROM roles r
WHERE r.designation = 'Super Admin';
-- =====================================================
-- DONNÉES D'EXEMPLE (OPTIONNEL)
-- =====================================================
-- Insertion d'un point de vente d'exemple
INSERT INTO `points_de_vente` (`nom`) VALUES ('Magasin Principal');
-- Insertion d'un client d'exemple
INSERT INTO `clients` (`nom`, `prenom`, `email`, `telephone`, `adresse`, `dateCreation`, `actif`) VALUES
('Dupont', 'Jean', 'jean.dupont@email.com', '0123456789', '123 Rue de la Paix, Paris', NOW(), 1);
-- =====================================================
-- VÉRIFICATIONS
-- =====================================================
-- Afficher les rôles créés
SELECT 'RÔLES CRÉÉS:' as info;
SELECT * FROM roles;
-- Afficher les permissions créées
SELECT 'PERMISSIONS CRÉÉES:' as info;
SELECT * FROM permissions;
-- Afficher les menus créés
SELECT 'MENUS CRÉÉS:' as info;
SELECT * FROM menu;
-- Afficher le Super Admin créé
SELECT 'SUPER ADMIN CRÉÉ:' as info;
SELECT u.username, u.email, r.designation as role
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE r.designation = 'Super Admin';
-- Vérifier les permissions du Super Admin
SELECT 'PERMISSIONS SUPER ADMIN:' as info;
SELECT COUNT(*) as total_permissions_assignees
FROM role_menu_permissions rmp
INNER JOIN roles r ON rmp.role_id = r.id
WHERE r.designation = 'Super Admin';
SELECT 'Script terminé avec succès!' as resultat;

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

2211
lib/Services/stock_managementDatabase.dart

File diff suppressed because it is too large

1612
lib/Views/Dashboard.dart

File diff suppressed because it is too large

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

610
lib/Views/RolePermissionPage.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Models/Permission.dart';
import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class RolePermissionsPage extends StatefulWidget {
final Role role;
@ -18,6 +19,8 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
List<Permission> permissions = [];
List<Map<String, dynamic>> menus = [];
Map<int, Map<String, bool>> menuPermissionsMap = {};
bool isLoading = true;
String? errorMessage;
@override
void initState() {
@ -26,152 +29,559 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
}
Future<void> _initData() async {
final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu'));
try {
setState(() {
isLoading = true;
errorMessage = null;
});
Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
final perms = await db.getAllPermissions();
final menuList = await db.getAllMenus(); // Utilise la nouvelle méthode
for (var menu in menuList) {
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(
widget.role.id!, menuId);
Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
tempMenuPermissionsMap[menuId] = {
for (var perm in perms)
perm.name: menuPerms.any((mp) => mp.name == perm.name)
};
}
for (var menu in menuList) {
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(
widget.role.id!, menuId);
setState(() {
permissions = perms;
menus = menuList;
menuPermissionsMap = tempMenuPermissionsMap;
});
tempMenuPermissionsMap[menuId] = {
for (var perm in perms)
perm.name: menuPerms.any((mp) => mp.name == perm.name)
};
}
setState(() {
permissions = perms;
menus = menuList;
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(
int menuId, String permission, bool enabled) async {
final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) {
await db.assignRoleMenuPermission(
widget.role.id!, menuId, perm.id!);
} else {
await db.removeRoleMenuPermission(
widget.role.id!, menuId, perm.id!);
try {
final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) {
await db.assignRoleMenuPermission(
widget.role.id!, menuId, perm.id!);
} else {
await db.removeRoleMenuPermission(
widget.role.id!, menuId, perm.id!);
}
setState(() {
menuPermissionsMap[menuId]![permission] = enabled;
});
// Afficher un message de confirmation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
enabled
? 'Permission "$permission" accordée'
: 'Permission "$permission" révoquée',
),
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,
),
);
}
}
setState(() {
menuPermissionsMap[menuId]![permission] = enabled;
});
void _toggleAllPermissions(int menuId, bool enabled) {
for (var permission in permissions) {
_onPermissionToggle(menuId, permission.name, enabled);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: "Permissions - ${widget.role.designation}",
// showBackButton: true,
),
body: Padding(
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),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestion des permissions pour le rôle: ${widget.role.designation}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
Row(
children: [
Icon(Icons.analytics, color: Colors.blue.shade600),
const SizedBox(width: 8),
Text(
'Résumé des permissions',
style: TextStyle(
fontSize: 18,
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: 12),
LinearProgressIndicator(
value: percentage,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
percentage > 0.7 ? Colors.green :
percentage > 0.3 ? Colors.orange : Colors.red,
),
),
const SizedBox(height: 20),
if (permissions.isNotEmpty && menus.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: menus.length,
itemBuilder: (context, index) {
final menu = menus[index];
final menuId = menu['id'] as int;
final menuName = menu['name'] as String;
return Card(
margin: const EdgeInsets.only(bottom: 15),
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
menuName,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Wrap(
spacing: 10,
runSpacing: 10,
children: permissions.map((perm) {
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
return FilterChip(
label: perm.name,
selected: isChecked,
onSelected: (bool value) {
_onPermissionToggle(menuId, perm.name, value);
},
);
}).toList(),
),
],
),
),
);
},
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 menuName = menu['name'] as String;
final menuRoute = menu['route'] as String;
final selectedCount = _getSelectedPermissionsCount(menuId);
final percentage = _getPermissionPercentage(menuId);
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
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,
children: [
Text(
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,
),
),
),
)
else
const Expanded(
child: Center(
child: CircularProgressIndicator(),
const SizedBox(width: 8),
Text(
'$selectedCount/${permissions.length}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
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(
spacing: 8,
runSpacing: 8,
children: permissions.map((perm) {
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
return CustomFilterChip(
label: perm.name,
selected: isChecked,
onSelected: (bool value) {
_onPermissionToggle(menuId, perm.name, value);
},
);
}).toList(),
),
],
),
),
],
),
);
}
@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
Expanded(
child: Center(
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 bool selected;
final ValueChanged<bool> onSelected;
const FilterChip({
const CustomFilterChip({
super.key,
required this.label,
required this.selected,
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
Widget build(BuildContext context) {
return ChoiceChip(
label: Text(label),
final color = _getChipColor(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,
onSelected: onSelected,
selectedColor: Colors.blue,
labelStyle: TextStyle(
color: selected ? Colors.white : Colors.black,
),
selectedColor: color,
backgroundColor: color.withOpacity(0.1),
checkmarkColor: Colors.white,
shape: RoundedRectangleBorder(
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
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: 'Bilan du mois'),
appBar: CustomAppBar(title: 'Bilan du mois'),
body: Column(
children: [
// Les 3 cartes en haut

3114
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:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Models/produit.dart';
import '../Services/productDatabase.dart';
//import '../Services/productDatabase.dart';
import 'gestionProduct.dart';
class EditProductPage extends StatelessWidget {
@ -31,7 +32,7 @@ class EditProductPage extends StatelessWidget {
category: category,
);
await ProductDatabase.instance.updateProduct(updatedProduct);
await AppDatabase.instance.updateProduct(updatedProduct);
Get.to(GestionProduit());
} else {

3
lib/Views/editUser.dart

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/users.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 {
final Users user;

7
lib/Views/gestionProduct.dart

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

464
lib/Views/gestionRole.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.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/Services/stock_managementDatabase.dart';
class HandleUserRole extends StatefulWidget {
const HandleUserRole({super.key});
@ -28,62 +29,177 @@ class _HandleUserRoleState extends State<HandleUserRole> {
}
Future<void> _initData() async {
final roleList = await db.getRoles();
final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu'));
try {
final roleList = await db.getRoles();
final perms = await db.getAllPermissions();
// 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 = {};
for (var role in roleList) {
final roleId = role.id!;
tempRoleMenuPermissionsMap[roleId] = {};
for (var role in roleList) {
final roleId = role.id!;
tempRoleMenuPermissionsMap[roleId] = {};
for (var menu in menuList) {
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(roleId, menuId);
for (var menu in menuList) {
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(roleId, menuId);
tempRoleMenuPermissionsMap[roleId]![menuId] = {
for (var perm in perms)
perm.name: menuPerms.any((mp) => mp.name == perm.name)
};
tempRoleMenuPermissionsMap[roleId]![menuId] = {
for (var perm in perms)
perm.name: menuPerms.any((mp) => mp.name == perm.name)
};
}
}
}
setState(() {
roles = roleList;
permissions = perms;
menus = menuList;
roleMenuPermissionsMap = tempRoleMenuPermissionsMap;
});
setState(() {
roles = roleList;
permissions = perms;
menus = menuList;
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 {
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));
_roleController.clear();
await _initData();
await db.createRole(Role(designation: designation));
_roleController.clear();
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 {
final perm = permissions.firstWhere((p) => p.name == permission);
try {
final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) {
await db.assignRoleMenuPermission(roleId, menuId, perm.id!);
} else {
await db.removeRoleMenuPermission(roleId, menuId, perm.id!);
if (enabled) {
await db.assignRoleMenuPermission(roleId, menuId, perm.id!);
} else {
await db.removeRoleMenuPermission(roleId, menuId, perm.id!);
}
setState(() {
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,
),
);
}
}
setState(() {
roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled;
});
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
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: "Gestion des rôles"),
appBar: CustomAppBar(title: "Gestion des rôles"),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@ -103,28 +219,52 @@ class _HandleUserRoleState extends State<HandleUserRole> {
controller: _roleController,
decoration: InputDecoration(
labelText: 'Nouveau rôle',
hintText: 'Ex: Manager, Vendeur...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
onSubmitted: (_) => _addRole(),
),
),
const SizedBox(width: 10),
ElevatedButton(
ElevatedButton.icon(
onPressed: _addRole,
icon: const Icon(Icons.add),
label: const Text('Ajouter'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Ajouter'),
),
],
),
),
),
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
if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty)
Expanded(
@ -136,69 +276,183 @@ class _HandleUserRoleState extends State<HandleUserRole> {
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width - 32,
),
child: Column(
children: menus.map((menu) {
final menuId = menu['id'] as int;
return Column(
children: [
Text(
menu['name'],
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
DataTable(
columnSpacing: 20,
columns: [
const DataColumn(
label: Text(
'Rôles',
style: TextStyle(fontWeight: FontWeight.bold),
),
scrollDirection: Axis.vertical,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: menus.map((menu) {
final menuId = menu['id'] as int;
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),
),
...permissions.map((perm) => DataColumn(
child: Row(
children: [
Icon(Icons.menu, color: Colors.blue.shade700),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
menuName,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.blue.shade700,
),
),
Text(
menuRoute,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 20,
headingRowHeight: 50,
dataRowHeight: 60,
columns: [
const DataColumn(
label: Text(
perm.name,
style: const TextStyle(fontWeight: FontWeight.bold),
'Rôles',
style: TextStyle(fontWeight: FontWeight.bold),
),
)).toList(),
],
rows: roles.map((role) {
final roleId = role.id!;
return DataRow(
cells: [
DataCell(Text(role.designation)),
...permissions.map((perm) {
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
return DataCell(
Checkbox(
value: isChecked,
onChanged: (bool? value) {
_onPermissionToggle(roleId, menuId, perm.name, value ?? false);
},
),
);
}).toList(),
),
...permissions.map((perm) => DataColumn(
label: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
perm.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
)).toList(),
const DataColumn(
label: Text(
'Actions',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
);
}).toList(),
),
],
);
}).toList(),
),
rows: roles.map((role) {
final roleId = role.id!;
return DataRow(
cells: [
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) {
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
return DataCell(
Checkbox(
value: isChecked,
onChanged: (bool? value) {
_onPermissionToggle(roleId, menuId, perm.name, value ?? false);
},
activeColor: Colors.green,
),
);
}).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(),
),
),
),
),
)
else
const Expanded(
Expanded(
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_core/src/get_main.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/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class GestionStockPage extends StatefulWidget {
const GestionStockPage({super.key});
@ -14,7 +15,7 @@ class GestionStockPage extends StatefulWidget {
}
class _GestionStockPageState extends State<GestionStockPage> {
final ProductDatabase _database = ProductDatabase.instance;
final AppDatabase _database = AppDatabase.instance;
List<Product> _products = [];
List<Product> _filteredProducts = [];
String? _selectedCategory;
@ -79,7 +80,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: 'Gestion des Stocks'),
appBar: CustomAppBar(title: 'Gestion des Stocks'),
drawer: CustomDrawer(),
body: Column(
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();
}
}

967
lib/Views/historique.dart

File diff suppressed because it is too large

2
lib/Views/listCommandeHistory.dart

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

5
lib/Views/listUser.dart

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

632
lib/Views/loginPage.dart

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Services/PermissionCacheService.dart'; // Nouveau import
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/Views/mobilepage.dart';
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/Services/app_database.dart';
import '../Models/users.dart';
import '../controller/userController.dart';
@ -19,9 +20,12 @@ class _LoginPageState extends State<LoginPage> {
late TextEditingController _usernameController;
late TextEditingController _passwordController;
final UserController userController = Get.put(UserController());
final PermissionCacheService _cacheService = PermissionCacheService.instance; // Nouveau
bool _isErrorVisible = false;
bool _isLoading = false;
String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
String _loadingMessage = 'Connexion en cours...'; // Nouveau
@override
void initState() {
@ -34,19 +38,7 @@ class _LoginPageState extends State<LoginPage> {
void checkUserCount() async {
try {
final userCount = await AppDatabase.instance.getUserCount();
print('Nombre d\'utilisateurs trouvés: $userCount'); // Debug
// Commentez cette partie pour permettre le login même sans utilisateurs
/*
if (userCount == 0) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
);
}
}
*/
print('Nombre d\'utilisateurs trouvés: $userCount');
} catch (error) {
print('Erreur lors de la vérification du nombre d\'utilisateurs: $error');
setState(() {
@ -63,228 +55,480 @@ class _LoginPageState extends State<LoginPage> {
super.dispose();
}
Future<void> saveUserData(Users user, String role, int userId) async {
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);
// /// OPTIMISÉ: Sauvegarde avec préchargement des permissions
// Future<void> saveUserData(Users user, String role, int userId) async {
// try {
// userController.setUserWithCredentials(user, role, userId);
print('Utilisateur sauvegardé: ${user.username}, rôle: $role, id: $userId');
} catch (error) {
print('Erreur lors de la sauvegarde: $error');
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
}
}
// if (user.pointDeVenteId != null) {
// await userController.loadPointDeVenteDesignation();
// }
// print('✅ Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}');
// } catch (error) {
// print('❌ Erreur lors de la sauvegarde: $error');
// throw Exception('Erreur lors de la sauvegarde des données utilisateur');
// }
// }
void _login() async {
if (_isLoading) return;
/// NOUVEAU: Préchargement des permissions en arrière-plan
Future<void> _preloadUserPermissions(String username) async {
try {
setState(() {
_loadingMessage = 'Préparation du menu...';
});
// Lancer le préchargement en parallèle avec les autres tâches
final permissionFuture = _cacheService.preloadUserData(username);
// Attendre maximum 2 secondes pour les permissions
await Future.any([
permissionFuture,
Future.delayed(const Duration(seconds: 2))
]);
print('✅ Permissions préparées (ou timeout)');
} catch (e) {
print('⚠️ Erreur préchargement permissions: $e');
// Continuer même en cas d'erreur
}
}
final String username = _usernameController.text.trim();
final String password = _passwordController.text.trim();
/// OPTIMISÉ: Connexion avec préchargement parallèle
void _login() async {
if (_isLoading) return;
// Validation basique
if (username.isEmpty || password.isEmpty) {
setState(() {
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
_isErrorVisible = true;
});
return;
}
final String username = _usernameController.text.trim();
final String password = _passwordController.text.trim();
if (username.isEmpty || password.isEmpty) {
setState(() {
_isLoading = true;
_isErrorVisible = false;
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
_isErrorVisible = true;
});
return;
}
setState(() {
_isLoading = true;
_isErrorVisible = false;
_loadingMessage = 'Connexion...';
});
try {
print('🔐 Tentative de connexion pour: $username');
final dbInstance = AppDatabase.instance;
// 1. Vérification rapide de la base
setState(() {
_loadingMessage = 'Vérification...';
});
try {
print('Tentative de connexion pour: $username');
final userCount = await dbInstance.getUserCount();
print('✅ Base accessible, $userCount utilisateurs');
} catch (dbError) {
throw Exception('Base de données inaccessible: $dbError');
}
// Vérification de la connexion à la base de données
final dbInstance = AppDatabase.instance;
// Test de connexion à la base
try {
final userCount = await dbInstance.getUserCount();
print('Base de données accessible, $userCount utilisateurs trouvés');
} catch (dbError) {
throw Exception('Impossible d\'accéder à la base de données: $dbError');
}
// 2. Vérification des identifiants
setState(() {
_loadingMessage = 'Authentification...';
});
bool isValidUser = await dbInstance.verifyUser(username, password);
// Vérifier les identifiants
bool isValidUser = await dbInstance.verifyUser(username, password);
print('Résultat de la vérification: $isValidUser');
if (isValidUser) {
setState(() {
_loadingMessage = 'Chargement du profil...';
});
// 3. Récupération parallèle des données
final futures = await Future.wait([
dbInstance.getUser(username),
dbInstance.getUserCredentials(username, password),
]);
if (isValidUser) {
// Récupérer les informations complètes de l'utilisateur
Users user = await dbInstance.getUser(username);
print('Utilisateur récupéré: ${user.username}');
final user = futures[0] as Users;
final userCredentials = futures[1] as Map<String, dynamic>?;
if (userCredentials != null) {
print('✅ Connexion réussie pour: ${user.username}');
print(' Rôle: ${userCredentials['role']}');
// Récupérer les credentials
Map<String, dynamic>? userCredentials =
await dbInstance.getUserCredentials(username, password);
setState(() {
_loadingMessage = 'Préparation...';
});
// 4. Sauvegarde des données utilisateur
await saveUserData(
user,
userCredentials['role'] as String,
userCredentials['id'] as int,
);
if (userCredentials != null) {
print('Connexion réussie pour: ${user.username}');
print('Rôle: ${userCredentials['role']}');
print('ID: ${userCredentials['id']}');
// CORRECTION : Sauvegarder ET mettre à jour le contrôleur
await saveUserData(
user,
userCredentials['role'] as String,
userCredentials['id'] as int,
);
// 5. Préchargement des permissions EN PARALLÈLE avec la navigation
setState(() {
_loadingMessage = 'Finalisation...';
});
// Lancer le préchargement en arrière-plan SANS attendre
_cacheService.preloadUserDataAsync(username);
// Navigation
if (mounted) {
// 6. Navigation immédiate
if (mounted) {
if (userCredentials['role'] == 'commercial') {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainLayout()),
);
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
MaterialPageRoute(builder: (context) => DashboardPage()),
);
}
} else {
throw Exception('Erreur lors de la récupération des credentials');
}
// Les permissions se chargeront en arrière-plan après la navigation
print('🚀 Navigation immédiate, permissions en arrière-plan');
} else {
print('Identifiants invalides pour: $username');
setState(() {
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true;
});
throw Exception('Erreur lors de la récupération des credentials');
}
} catch (error) {
print('Erreur lors de la connexion: $error');
} else {
setState(() {
_errorMessage = 'Erreur de connexion: ${error.toString()}';
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true;
});
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
} catch (error) {
setState(() {
_errorMessage = 'Erreur de connexion: ${error.toString()}';
_isErrorVisible = true;
});
} finally {
if (mounted) {
setState(() {
_isLoading = false;
_loadingMessage = 'Connexion en cours...';
});
}
}
}
/// OPTIMISÉ: Sauvegarde rapide
Future<void> saveUserData(Users user, String role, int userId) async {
try {
userController.setUserWithCredentials(user, role, userId);
// Charger le point de vente en parallèle si nécessaire
if (user.pointDeVenteId != null) {
// Ne pas attendre, charger en arrière-plan
unawaited(userController.loadPointDeVenteDesignation());
}
print('✅ Utilisateur sauvegardé rapidement');
} catch (error) {
print('❌ Erreur lors de la sauvegarde: $error');
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
}
}
@override
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(
appBar: AppBar(
title: const Text(
'Login',
style: TextStyle(color: Colors.white),
),
backgroundColor: const Color.fromARGB(255, 4, 54, 95),
centerTitle: true,
),
backgroundColor: primaryColor,
body: ParticleBackground(
child: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(30.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: const Icon(
Icons.lock_outline,
size: 100.0,
color: Color.fromARGB(255, 4, 54, 95),
child: SingleChildScrollView(
child: Container(
width: MediaQuery.of(context).size.width < 500
? double.infinity
: 400,
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
decoration: BoxDecoration(
color: cardColor.withOpacity(0.98),
borderRadius: BorderRadius.circular(30.0),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.2),
blurRadius: 16,
spreadRadius: 4,
offset: const Offset(0, 8),
),
),
TextField(
controller: _usernameController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Username',
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Center(
child: Column(
children: [
CircleAvatar(
radius: 38,
backgroundColor: accentColor.withOpacity(0.15),
child: Icon(
Icons.lock_outline,
color: accentColor,
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: 16.0),
TextField(
controller: _passwordController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
const SizedBox(height: 24),
// Username Field
TextField(
controller: _usernameController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Nom d\'utilisateur',
labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
prefixIcon: Icon(Icons.person, color: accentColor),
filled: true,
fillColor: accentColor.withOpacity(0.045),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
),
),
obscureText: true,
onSubmitted: (_) => _login(),
),
const SizedBox(height: 16.0),
Visibility(
visible: _isErrorVisible,
child: Text(
_errorMessage,
style: const TextStyle(
color: Colors.red,
fontSize: 14,
const SizedBox(height: 18.0),
// Password Field
TextField(
controller: _passwordController,
enabled: !_isLoading,
obscureText: true,
decoration: InputDecoration(
labelText: 'Mot de passe',
labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
prefixIcon: Icon(Icons.lock, color: accentColor),
filled: true,
fillColor: accentColor.withOpacity(0.045),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
),
textAlign: TextAlign.center,
onSubmitted: (_) => _login(),
),
if (_isLoading) ...[
const SizedBox(height: 16.0),
Column(
children: [
// Barre de progression animée
Container(
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: accentColor.withOpacity(0.2),
),
child: LayoutBuilder(
builder: (context, constraints) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: constraints.maxWidth * 0.7,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
gradient: LinearGradient(
colors: [accentColor, accentColor.withOpacity(0.7)],
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0015B7),
elevation: 5.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
minimumSize: const Size(double.infinity, 48),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
);
},
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(accentColor),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
_loadingMessage,
style: TextStyle(
color: accentColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.left,
),
),
],
),
const SizedBox(height: 4),
Text(
"Le menu se chargera en arrière-plan",
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
],
// Error Message
if (_isErrorVisible) ...[
const SizedBox(height: 12.0),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage,
style: const TextStyle(
color: Colors.red,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
)
: const Text(
'Se connecter',
style: TextStyle(
color: Colors.white,
fontSize: 16,
],
),
),
],
const SizedBox(height: 26.0),
// Login Button
ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
disabledBackgroundColor: accentColor.withOpacity(0.3),
foregroundColor: Colors.white,
elevation: 7.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
minimumSize: const Size(double.infinity, 52),
),
child: _isLoading
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
const SizedBox(width: 12),
Text(
'Connexion...',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
)
: const Text(
'Se connecter',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: .4,
),
),
),
),
// Bouton de debug (à supprimer en production)
if (_isErrorVisible)
TextButton(
onPressed: () async {
try {
final count = await AppDatabase.instance.getUserCount();
print('Debug: $count utilisateurs dans la base');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$count utilisateurs trouvés')),
);
} catch (e) {
print('Debug error: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
},
child: const Text('Debug: Vérifier BDD'),
),
],
// Debug Button (à enlever en production)
if (_isErrorVisible && !_isLoading) ...[
const SizedBox(height: 8),
TextButton(
onPressed: () async {
try {
final count = await AppDatabase.instance.getUserCount();
final stats = _cacheService.getCacheStats();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'BDD: $count utilisateurs\n'
'Cache: ${stats['users_cached']} utilisateurs en cache',
),
duration: const Duration(seconds: 3),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
},
child: Text(
'Debug: Vérifier BDD & Cache',
style: TextStyle(
color: primaryColor.withOpacity(0.6),
fontSize: 12,
),
),
),
],
],
),
),
),
),

44
lib/Views/mobilepage.dart

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Views/historique.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Youmaz Gestion',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MainLayout(),
debugShowCheckedModeBanner: false,
);
}
}
class MainLayout extends StatefulWidget {
const MainLayout({super.key});
@override
State<MainLayout> createState() => _MainLayoutState();
}
class _MainLayoutState extends State<MainLayout> {
// Index par défaut pour la page de commande
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: CustomDrawer(),
body: const HistoriquePage(),
);
}
}

3174
lib/Views/newCommand.dart

File diff suppressed because it is too large

74
lib/Views/produitsCard.dart

@ -4,7 +4,7 @@ import 'package:youmazgestion/Models/produit.dart';
class ProductCard extends StatefulWidget {
final Product product;
final void Function(Product, int) onAddToCart;
final void Function(Product, int) onAddToCart;
const ProductCard({
Key? key,
@ -16,7 +16,8 @@ class ProductCard extends StatefulWidget {
State<ProductCard> createState() => _ProductCardState();
}
class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin {
class _ProductCardState extends State<ProductCard>
with TickerProviderStateMixin {
int selectedQuantity = 1;
late AnimationController _scaleController;
late AnimationController _fadeController;
@ -26,7 +27,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
@override
void initState() {
super.initState();
// Animations pour les interactions
_scaleController = AnimationController(
duration: const Duration(milliseconds: 200),
@ -36,7 +37,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
duration: const Duration(milliseconds: 300),
vsync: this,
)..forward();
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
@ -44,7 +45,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
parent: _scaleController,
curve: Curves.easeInOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
@ -122,7 +123,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
: _buildPlaceholderImage(),
),
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
@ -141,7 +141,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
),
),
),
if (widget.product.isStockDefined())
Positioned(
top: 12,
@ -183,7 +182,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
),
),
),
Positioned(
left: 0,
right: 0,
@ -201,7 +199,8 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
vertical: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
widget.product.name,
@ -222,7 +221,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
),
const SizedBox(height: 4),
Text(
'${widget.product.price.toStringAsFixed(2)} FCFA',
'${widget.product.price.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
@ -239,9 +238,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
],
),
),
const SizedBox(height: 12),
Row(
children: [
Container(
@ -250,7 +247,8 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color:
Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
@ -295,9 +293,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
],
),
),
const SizedBox(width: 8),
Expanded(
child: MouseRegion(
cursor: SystemMouseCursors.click,
@ -306,9 +302,11 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
onTap: () {
widget.onAddToCart(widget.product, selectedQuantity);
ScaffoldMessenger.of(context).showSnackBar(
widget.onAddToCart(widget.product,
selectedQuantity);
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Row(
children: [
@ -320,16 +318,20 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
Expanded(
child: Text(
'${widget.product.name} (x$selectedQuantity) ajouté au panier',
overflow: TextOverflow.ellipsis,
overflow: TextOverflow
.ellipsis,
),
),
],
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
duration:
const Duration(seconds: 1),
behavior:
SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
borderRadius:
BorderRadius.circular(10),
),
),
);
@ -342,21 +344,27 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color.fromARGB(255, 4, 54, 95),
Color.fromARGB(255, 6, 80, 140),
Color.fromARGB(
255, 4, 54, 95),
Color.fromARGB(
255, 6, 80, 140),
],
),
borderRadius: BorderRadius.circular(20),
borderRadius:
BorderRadius.circular(20),
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,
offset: const Offset(0, 3),
),
],
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(
Icons.add_shopping_cart,
@ -369,10 +377,12 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
'Ajouter',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontWeight:
FontWeight.bold,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
overflow:
TextOverflow.ellipsis,
),
),
],
@ -442,10 +452,12 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
child: Icon(
icon,
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,
),
),
),
);
}
}
}

142
lib/Views/registrationPage.dart

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import '../Services/app_database.dart'; // Changé de authDatabase.dart
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
class RegistrationPage extends StatefulWidget {
const RegistrationPage({super.key});
@ -23,7 +24,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
Role? _selectedRole;
bool _isLoading = false;
bool _isLoadingRoles = true;
List<Map<String, dynamic>> _availablePointsDeVente = [];
int? _selectedPointDeVenteId;
bool _isLoadingPointsDeVente = true;
@override
void initState() {
super.initState();
@ -36,19 +39,38 @@ class _RegistrationPageState extends State<RegistrationPage> {
_initializeDatabase();
}
Future<void> _initializeDatabase() async {
try {
await AppDatabase.instance.initDatabase();
await _loadRoles();
} catch (error) {
print('Erreur lors de l\'initialisation: $error');
if (mounted) {
_showErrorDialog('Erreur d\'initialisation',
'Impossible d\'initialiser l\'application. Veuillez redémarrer.');
}
Future<void> _initializeDatabase() async {
try {
await AppDatabase.instance.initDatabase();
await _loadRoles();
await _loadPointsDeVente(); // Ajouté ici
} catch (error) {
print('Erreur lors de l\'initialisation: $error');
if (mounted) {
_showErrorDialog('Erreur d\'initialisation',
'Impossible d\'initialiser l\'application. Veuillez redémarrer.');
}
}
}
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 {
try {
final roles = await AppDatabase.instance.getRoles();
@ -98,15 +120,16 @@ class _RegistrationPageState extends State<RegistrationPage> {
}
bool _validateFields() {
if (_nameController.text.trim().isEmpty ||
_lastNameController.text.trim().isEmpty ||
_emailController.text.trim().isEmpty ||
_usernameController.text.trim().isEmpty ||
_passwordController.text.trim().isEmpty ||
_selectedRole == null) {
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
return false;
}
if (_nameController.text.trim().isEmpty ||
_lastNameController.text.trim().isEmpty ||
_emailController.text.trim().isEmpty ||
_usernameController.text.trim().isEmpty ||
_passwordController.text.trim().isEmpty ||
_selectedRole == null ||
_selectedPointDeVenteId == null) { // Ajouté ici
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
return false;
}
// Validation basique de l'email
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(_emailController.text.trim())) {
@ -126,23 +149,24 @@ class _RegistrationPageState extends State<RegistrationPage> {
void _register() async {
if (_isLoading) return;
if (!_validateFields()) return;
if (!_validateFields()) return;
setState(() {
_isLoading = true;
});
setState(() {
_isLoading = true;
});
try {
// Créer l'objet utilisateur avec le nouveau modèle
final Users user = Users(
name: _nameController.text.trim(),
lastName: _lastNameController.text.trim(),
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
username: _usernameController.text.trim(),
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
roleName: _selectedRole!.designation, // Pour l'affichage
);
try {
// Créer l'objet utilisateur avec le nouveau modèle
final Users user = Users(
name: _nameController.text.trim(),
lastName: _lastNameController.text.trim(),
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
username: _usernameController.text.trim(),
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
roleName: _selectedRole!.designation, // Pour l'affichage
pointDeVenteId: _selectedPointDeVenteId, // Ajouté ici
);
// Sauvegarder l'utilisateur dans la base de données
final int userId = await AppDatabase.instance.createUser(user);
@ -191,7 +215,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
Navigator.of(context).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
MaterialPageRoute(builder: (context) => DashboardPage()),
);
},
child: const Text('OK'),
@ -361,6 +385,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),
SizedBox(
width: double.infinity,

120
lib/Views/ticketPage.dart

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

10
lib/accueil.dart

@ -1,8 +1,8 @@
import 'dart:io';
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:get/get.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/produitsCard.dart';
import 'Components/appDrawer.dart';
@ -10,7 +10,7 @@ import 'Components/app_bar.dart';
import 'Components/cartItem.dart';
import 'Models/produit.dart';
import 'Services/OrderDatabase.dart';
import 'Services/productDatabase.dart';
//import 'Services/productDatabase.dart';
import 'Views/ticketPage.dart';
import 'controller/userController.dart';
import 'my_app.dart';
@ -25,7 +25,7 @@ class AccueilPage extends StatefulWidget {
class _AccueilPageState extends State<AccueilPage> {
final UserController userController = Get.put(UserController());
final ProductDatabase productDatabase = ProductDatabase();
final AppDatabase productDatabase = AppDatabase.instance;
late Future<Map<String, List<Product>>> productsFuture;
final OrderDatabase orderDatabase = OrderDatabase.instance;
final WorkDatabase workDatabase = WorkDatabase.instance;
@ -114,7 +114,7 @@ class _AccueilPageState extends State<AccueilPage> {
await orderDatabase.insertOrderItem(
orderId, product.name, quantity, price);
final updatedStock = product.stock! - quantity;
final updatedStock = product.stock - quantity;
await productDatabase.updateStock(product.id!, updatedStock);
}
@ -448,7 +448,7 @@ class _AccueilPageState extends State<AccueilPage> {
fontSize: 16,
fontWeight: FontWeight.bold)),
Text(
'${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA',
'${NumberFormat('#,##0.00').format(calculateTotalPrice())} MGA',
style: TextStyle(
fontSize: 18,
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 = 'localhost';
static const int port = 3306;
static const String username = 'root';
static const String? password = null;
static const String database = 'gico';
static const String prodHost = '185.70.105.157';
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 => true;
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:flutter/material.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
import '../Components/cartItem.dart';
import '../Models/produit.dart';
import '../Services/OrderDatabase.dart';
import '../Services/WorkDatabase.dart';
import '../Services/productDatabase.dart';
//import '../Services/productDatabase.dart';
import '../Views/ticketPage.dart';
import '../my_app.dart';
class AccueilController extends GetxController {
final UserController userController = Get.find();
final ProductDatabase productDatabase = ProductDatabase();
final AppDatabase productDatabase = AppDatabase.instance;
final Rx<Map<String, List<Product>>> productsFuture = Rx({}); // Observable
final OrderDatabase orderDatabase = OrderDatabase.instance;
final WorkDatabase workDatabase = WorkDatabase.instance;

187
lib/controller/userController.dart

@ -1,7 +1,8 @@
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.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/PermissionCacheService.dart';
class UserController extends GetxController {
final _username = ''.obs;
@ -10,8 +11,14 @@ class UserController extends GetxController {
final _name = ''.obs;
final _lastname = ''.obs;
final _password = ''.obs;
final _userId = 0.obs; // Ajout de l'ID utilisateur
final _userId = 0.obs;
final _pointDeVenteId = 0.obs;
final _pointDeVenteDesignation = ''.obs;
// Cache service
final PermissionCacheService _cacheService = PermissionCacheService.instance;
// Getters
String get username => _username.value;
String get email => _email.value;
String get role => _role.value;
@ -19,14 +26,16 @@ class UserController extends GetxController {
String get lastname => _lastname.value;
String get password => _password.value;
int get userId => _userId.value;
int get pointDeVenteId => _pointDeVenteId.value;
String get pointDeVenteDesignation => _pointDeVenteDesignation.value;
@override
void onInit() {
super.onInit();
loadUserData(); // Charger les données au démarrage
loadUserData();
}
// CORRECTION : Charger les données complètes depuis SharedPreferences ET la base de données
/// SIMPLIFIÉ: Charge les données utilisateur sans cache persistant
Future<void> loadUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
@ -34,64 +43,100 @@ class UserController extends GetxController {
final storedUsername = prefs.getString('username') ?? '';
final storedRole = prefs.getString('role') ?? '';
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) {
try {
// Récupérer les données complètes depuis la base de données
Users user = await AppDatabase.instance.getUser(storedUsername);
// Mettre à jour TOUTES les données
_username.value = user.username;
_email.value = user.email;
_name.value = user.name;
_lastname.value = user.lastName;
_password.value = user.password;
_role.value = storedRole; // Récupéré depuis SharedPreferences
_userId.value = storedUserId; // Récupéré depuis SharedPreferences
_role.value = storedRole;
_userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) {
await loadPointDeVenteDesignation();
}
// Précharger les permissions en arrière-plan (non bloquant)
_preloadPermissionsInBackground();
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) {
print('❌ Erreur DB, chargement depuis SharedPreferences uniquement: $dbError');
// Fallback : charger depuis SharedPreferences uniquement
print("❌ Erreur BDD, utilisation du fallback: $dbError");
_username.value = storedUsername;
_email.value = prefs.getString('email') ?? '';
_role.value = storedRole;
_name.value = prefs.getString('name') ?? '';
_lastname.value = prefs.getString('lastname') ?? '';
_userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
// Précharger quand même
_preloadPermissionsInBackground();
}
} else {
print("❌ Aucun utilisateur stocké trouvé");
}
} catch (e) {
print('❌ Erreur lors du chargement des données utilisateur: $e');
}
}
// NOUVELLE MÉTHODE : Mise à jour complète avec Users + credentials
/// Précharge les permissions en arrière-plan (non bloquant)
void _preloadPermissionsInBackground() {
if (_username.value.isNotEmpty) {
// Lancer en arrière-plan sans attendre
Future.microtask(() async {
try {
await _cacheService.preloadUserData(_username.value);
} catch (e) {
print("⚠️ Erreur préchargement permissions (non critique): $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['nom'] as String;
await saveUserData();
}
} catch (e) {
print('❌ Erreur lors du chargement de la désignation du point de vente: $e');
}
}
/// Mise à jour avec préchargement des permissions
void setUserWithCredentials(Users user, String role, int userId) {
_username.value = user.username;
_email.value = user.email;
_role.value = role; // Rôle depuis les credentials
_role.value = role;
_name.value = user.name;
_lastname.value = user.lastName;
_password.value = user.password;
_userId.value = userId; // ID depuis les credentials
_userId.value = userId;
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
print("✅ Utilisateur mis à jour avec credentials:");
print(" Username: ${_username.value}");
print(" Name: ${_name.value}");
print(" Email: ${_email.value}");
print(" Role: ${_role.value}");
print(" UserID: ${_userId.value}");
// Sauvegarder dans SharedPreferences
saveUserData();
// Précharger immédiatement les permissions après connexion
_preloadPermissionsInBackground();
}
// MÉTHODE EXISTANTE AMÉLIORÉE
void setUser(Users user) {
_username.value = user.username;
_email.value = user.email;
@ -99,17 +144,11 @@ class UserController extends GetxController {
_name.value = user.name;
_lastname.value = user.lastName;
_password.value = user.password;
// Note: _userId reste inchangé si pas fourni
print("✅ Utilisateur mis à jour (méthode legacy):");
print(" Username: ${_username.value}");
print(" Role: ${_role.value}");
// Sauvegarder dans SharedPreferences
saveUserData();
_preloadPermissionsInBackground();
}
// CORRECTION : Sauvegarder TOUTES les données importantes
Future<void> saveUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
@ -119,67 +158,93 @@ class UserController extends GetxController {
await prefs.setString('role', _role.value);
await prefs.setString('name', _name.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");
} catch (e) {
print('❌ Erreur lors de la sauvegarde des données utilisateur: $e');
print('❌ Erreur lors de la sauvegarde: $e');
}
}
// CORRECTION : Vider TOUTES les données (SharedPreferences + Observables)
/// MODIFIÉ: Vider les données ET le cache de session
Future<void> clearUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
// Vider SharedPreferences
// IMPORTANT: Vider le cache de session
_cacheService.clearAllCache();
// Effacer SharedPreferences
await prefs.remove('username');
await prefs.remove('email');
await prefs.remove('role');
await prefs.remove('name');
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
// Effacer les observables
_username.value = '';
_email.value = '';
_role.value = '';
_name.value = '';
_lastname.value = '';
_password.value = '';
_userId.value = 0; // Réinitialiser l'ID
_userId.value = 0;
_pointDeVenteId.value = 0;
_pointDeVenteDesignation.value = '';
print("✅ Données utilisateur et cache de session vidés");
print("✅ Toutes les données utilisateur ont été effacées");
} catch (e) {
print('❌ Erreur lors de l\'effacement des données utilisateur: $e');
print('❌ Erreur lors de l\'effacement: $e');
}
}
// MÉTHODE UTILITAIRE : Vérifier si un utilisateur est connecté
// Getters utilitaires
bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
// MÉTHODE UTILITAIRE : Obtenir le nom complet
String get fullName => '${_name.value} ${_lastname.value}'.trim();
/// OPTIMISÉ: Vérification des permissions depuis le cache de session
Future<bool> hasPermission(String permission, String route) async {
try {
if (_username.value.isEmpty) {
print('⚠️ Username vide, rechargement des données...');
print('⚠️ Username vide, rechargement...');
await loadUserData();
}
if (_username.value.isEmpty) {
print('Impossible de vérifier les permissions : utilisateur non connecté');
print('Utilisateur non connecté');
return false;
}
return await AppDatabase.instance.hasPermission(username, permission, route);
// Essayer d'abord le cache
if (_cacheService.isLoaded) {
return _cacheService.hasPermission(_username.value, permission, route);
}
// Si pas encore chargé, charger et essayer de nouveau
print("🔄 Cache non chargé, chargement des permissions...");
await _cacheService.loadUserPermissions(_username.value);
return _cacheService.hasPermission(_username.value, permission, route);
} catch (e) {
print('❌ Erreur vérification permission: $e');
return false; // Sécurité : refuser l'accès en cas d'erreur
// Fallback vers la méthode originale en cas d'erreur
try {
return await AppDatabase.instance.hasPermission(_username.value, permission, route);
} catch (fallbackError) {
print('❌ Erreur fallback permission: $fallbackError');
return false;
}
}
}
/// Vérification de permissions multiples
Future<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async {
for (String permissionName in permissionNames) {
if (await hasPermission(permissionName, menuRoute)) {
@ -189,16 +254,40 @@ class UserController extends GetxController {
return false;
}
// MÉTHODE DEBUG : Afficher l'état actuel
/// Obtenir les menus accessibles depuis le cache
List<Map<String, dynamic>> getUserMenus() {
if (_username.value.isEmpty || !_cacheService.isLoaded) return [];
return _cacheService.getUserMenus(_username.value);
}
/// Vérifier l'accès à un menu depuis le cache
bool hasMenuAccess(String menuRoute) {
if (_username.value.isEmpty || !_cacheService.isLoaded) return false;
return _cacheService.hasMenuAccess(_username.value, menuRoute);
}
/// Forcer le rechargement des permissions (pour les admins après modification)
Future<void> refreshPermissions() async {
if (_username.value.isNotEmpty) {
await _cacheService.refreshUserPermissions(_username.value);
}
}
/// Vérifier si le cache est prêt
bool get isCacheReady => _cacheService.isLoaded && _username.value.isNotEmpty;
/// Debug
void debugPrintUserState() {
print("=== ÉTAT UTILISATEUR ===");
print("Username: ${_username.value}");
print("Name: ${_name.value}");
print("Lastname: ${_lastname.value}");
print("Email: ${_email.value}");
print("Role: ${_role.value}");
print("UserID: ${_userId.value}");
print("IsLoggedIn: $isLoggedIn");
print("Cache Ready: $isCacheReady");
print("========================");
// Debug du cache
_cacheService.debugPrintCache();
}
}

110
lib/main.dart

@ -1,8 +1,7 @@
import 'package:flutter/material.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 'Services/productDatabase.dart';
import 'my_app.dart';
import 'package:logging/logging.dart';
@ -10,30 +9,117 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
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 ProductDatabase.instance.deleteDatabaseFile();
await ProductDatabase.instance.initDatabase();
await AppDatabase.instance.initDatabase();
// Initialiser la base de données MySQL
print("Connexion à la base de données MySQL...");
// await AppDatabase.instance.initDatabase();
print("Base de données initialisée avec succès !");
// Afficher les informations de la base (pour debug)
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();
print("Lancement de l'application...");
runApp(const GetMaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp(),
));
} catch (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(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Text('Erreur d\'initialisation: $e'),
backgroundColor: Colors.red[50],
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'),
),
),
],
),
),
),
));
@ -45,4 +131,4 @@ void setupLogger() {
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
});
}
}

64
lib/my_app.dart

@ -1,21 +1,16 @@
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'Views/ErreurPage.dart';
import 'Views/loginPage.dart';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static bool isRegisterOpen = false;
static DateTime? startDate;
static late String path;
static const Gradient primaryGradient = LinearGradient(
colors: [
Colors.white,
const Color.fromARGB(255, 4, 54, 95),
Color.fromARGB(255, 4, 54, 95),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
@ -24,56 +19,17 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'GUYCOM',
debugShowCheckedModeBanner: false,
theme: ThemeData(
canvasColor: Colors.transparent,
),
home: Builder(
builder: (context) {
return FutureBuilder<bool>(
future:
checkLocalDatabasesExist(), // Appel à la fonction de vérification
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Affichez un indicateur de chargement si nécessaire
return const CircularProgressIndicator();
} else if (snapshot.hasError || !(snapshot.data ?? false)) {
// S'il y a une erreur ou si les bases de données n'existent pas
return ErreurPage(
dbPath:
path); // Redirigez vers la page d'erreur en affichant le chemin de la base de données
} else {
// Si les bases de données existent, affichez la page d'accueil normalement
return Container(
decoration: const BoxDecoration(
gradient: MyApp.primaryGradient,
),
child: const LoginPage(),
);
}
},
);
},
home: Container(
decoration: const BoxDecoration(
gradient: MyApp.primaryGradient,
),
child: const LoginPage(),
),
);
}
Future<bool> checkLocalDatabasesExist() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final dbPath = documentsDirectory.path;
path = dbPath;
// Vérifier si le fichier de base de données products2.db existe
final productsDBFile = File('$dbPath/products2.db');
final productsDBExists = await productsDBFile.exists();
// Vérifier si le fichier de base de données auth.db existe
final authDBFile = File('$dbPath/usersDb.db');
final authDBExists = await authDBFile.exists();
// Vérifier si d'autres bases de données nécessaires existent, le cas échéant
return productsDBExists && authDBExists;
}
}
}

8
linux/flutter/generated_plugin_registrant.cc

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

2
linux/flutter/generated_plugins.cmake

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

8
macos/Flutter/GeneratedPluginRegistrant.swift

@ -7,18 +7,22 @@ import Foundation
import file_picker
import file_selector_macos
import mobile_scanner
import open_file_mac
import path_provider_foundation
import screen_retriever
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

104
pubspec.lock

@ -241,6 +241,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description: flutter
@ -608,6 +616,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -616,6 +632,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.7.0"
mysql1:
dependency: "direct main"
description:
name: mysql1
sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9"
url: "https://pub.dev"
source: hosted
version: "0.20.0"
nested:
dependency: transitive
description:
@ -624,6 +648,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -808,6 +840,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
provider:
dependency: transitive
description:
@ -832,6 +872,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_code_scanner_plus:
dependency: "direct main"
description:
name: qr_code_scanner_plus
sha256: "39696b50d277097ee4d90d4292de36f38c66213a4f5216a06b2bdd2b63117859"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
qr_flutter:
dependency: "direct main"
description:
@ -864,6 +912,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.3"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
shared_preferences:
dependency: "direct main"
description:
@ -957,22 +1013,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -989,22 +1029,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -1041,18 +1065,18 @@ packages:
dependency: "direct main"
description:
name: syncfusion_flutter_charts
sha256: bdb7cc5814ceb187793cea587f4a5946afcffd96726b219cee79df8460f44b7b
sha256: "0222ac9d8cb6c671f014effe9bd5c0aef35eadb16471355345ba87cc0ac007b3"
url: "https://pub.dev"
source: hosted
version: "21.2.4"
version: "20.4.54"
syncfusion_flutter_core:
dependency: transitive
description:
name: syncfusion_flutter_core
sha256: "8db8f55c77f56968681447d3837c10f27a9e861e238a898fda116c7531def979"
sha256: "3979f0b1c5a97422cadae52d476c21fa3e0fb671ef51de6cae1d646d8b99fe1f"
url: "https://pub.dev"
source: hosted
version: "21.2.10"
version: "20.4.54"
synchronized:
dependency: transitive
description:
@ -1189,6 +1213,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.12.0"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf"
url: "https://pub.dev"
source: hosted
version: "0.3.9"
xdg_directories:
dependency: transitive
description:

17
pubspec.yaml

@ -35,7 +35,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
get: ^4.6.5
sqflite: ^2.2.8+4
# sqflite: ^2.2.8+4
mysql1: ^0.20.0
flutter_dropzone: ^4.2.1
image_picker: ^0.8.7+5
@ -50,7 +51,7 @@ dependencies:
logging: ^1.2.0
msix: ^3.7.0
flutter_charts: ^0.5.1
syncfusion_flutter_charts: ^21.2.4
syncfusion_flutter_charts: ^20.4.48
shelf: ^1.4.1
shelf_router: ^1.1.4
pdf: ^3.8.4
@ -62,7 +63,11 @@ dependencies:
path_provider: ^2.0.15
shared_preferences: ^2.2.2
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
qr_code_scanner_plus: ^2.0.10+1
window_manager: ^0.3.7
@ -101,6 +106,12 @@ flutter:
- assets/database/usersdb.db
- assets/database/work.db
- assets/database/roles.db
- assets/airtel_money.png
- assets/mvola.jpg
- assets/Orange_money.png
- assets/fa-solid-900.ttf
- assets/NotoEmoji-Regular.ttf
- assets/fonts/Roboto-Italic.ttf
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware

3
test/widget_test.dart

@ -7,8 +7,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:youmazgestion/my_app.dart';
import 'package:guycom/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {

6
windows/flutter/generated_plugin_registrant.cc

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

2
windows/flutter/generated_plugins.cmake

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

Loading…
Cancel
Save