Compare commits

...

8 Commits

Author SHA1 Message Date
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/Orange_money.png
  3. BIN
      assets/airtel_money.png
  4. BIN
      assets/fa-solid-900.ttf
  5. BIN
      assets/fonts/Roboto-Italic.ttf
  6. BIN
      assets/mvola.jpg
  7. 3
      ios/Runner/Info.plist
  8. 417
      lib/Components/AddClient.dart
  9. 471
      lib/Components/AddClientForm.dart
  10. 176
      lib/Components/DiscountDialog.dart
  11. 349
      lib/Components/GiftaselectedButton.dart
  12. 338
      lib/Components/PaymentEnchainedDialog.dart
  13. 259
      lib/Components/QrScan.dart
  14. 782
      lib/Components/appDrawer.dart
  15. 112
      lib/Components/app_bar.dart
  16. 175
      lib/Components/commandManagementComponents/CommandDetails.dart
  17. 226
      lib/Components/commandManagementComponents/CommandeActions.dart
  18. 189
      lib/Components/commandManagementComponents/DiscountDialog.dart
  19. 136
      lib/Components/commandManagementComponents/GiftSelectionDialog.dart
  20. 8
      lib/Components/commandManagementComponents/PaymentMethod.dart
  21. 288
      lib/Components/commandManagementComponents/PaymentMethodDialog.dart
  22. 7
      lib/Components/commandManagementComponents/PaymentType.dart
  23. 7
      lib/Components/paymentType.dart
  24. 2125
      lib/Components/teat.dart
  25. 267
      lib/Models/Client.dart
  26. 64
      lib/Models/Remise.dart
  27. 36
      lib/Models/pointage_model.dart
  28. 167
      lib/Models/produit.dart
  29. 26
      lib/Models/users.dart
  30. 0
      lib/Services/GestionStockDatabase.dart
  31. 258
      lib/Services/PermissionCacheService.dart
  32. 680
      lib/Services/app_database.dart
  33. 559
      lib/Services/productDatabase.dart
  34. 1960
      lib/Services/stock_managementDatabase.dart
  35. 1205
      lib/Views/Dashboard.dart
  36. 4604
      lib/Views/HandleProduct.dart
  37. 7
      lib/Views/RoleListPage.dart
  38. 610
      lib/Views/RolePermissionPage.dart
  39. 2
      lib/Views/bilanMois.dart
  40. 1374
      lib/Views/commandManagement.dart
  41. 5
      lib/Views/editProduct.dart
  42. 3
      lib/Views/editUser.dart
  43. 7
      lib/Views/gestionProduct.dart
  44. 462
      lib/Views/gestionRole.dart
  45. 7
      lib/Views/gestionStock.dart
  46. 416
      lib/Views/gestion_point_de_vente.dart
  47. 965
      lib/Views/historique.dart
  48. 2
      lib/Views/listCommandeHistory.dart
  49. 5
      lib/Views/listUser.dart
  50. 636
      lib/Views/loginPage.dart
  51. 2213
      lib/Views/mobilepage.dart
  52. 2402
      lib/Views/newCommand.dart
  53. 62
      lib/Views/produitsCard.dart
  54. 139
      lib/Views/registrationPage.dart
  55. 7
      lib/accueil.dart
  56. 64
      lib/config/DatabaseConfig.dart
  57. 5
      lib/controller/AccueilController.dart
  58. 187
      lib/controller/userController.dart
  59. 108
      lib/main.dart
  60. 58
      lib/my_app.dart
  61. 4
      macos/Flutter/GeneratedPluginRegistrant.swift
  62. 88
      pubspec.lock
  63. 15
      pubspec.yaml
  64. 3
      test/widget_test.dart

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

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

BIN
assets/Orange_money.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/airtel_money.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/fa-solid-900.ttf

Binary file not shown.

BIN
assets/fonts/Roboto-Italic.ttf

Binary file not shown.

BIN
assets/mvola.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

3
ios/Runner/Info.plist

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

417
lib/Components/AddClient.dart

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

471
lib/Components/AddClientForm.dart

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

176
lib/Components/DiscountDialog.dart

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

349
lib/Components/GiftaselectedButton.dart

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

338
lib/Components/PaymentEnchainedDialog.dart

@ -0,0 +1,338 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:youmazgestion/Components/DiscountDialog.dart';
import 'package:youmazgestion/Components/paymentType.dart';
import 'package:youmazgestion/Models/Client.dart';
import 'package:youmazgestion/Models/Remise.dart';
// Dialogue de paiement amélioré avec support des remises
class PaymentMethodEnhancedDialog extends StatefulWidget {
final Commande commande;
const PaymentMethodEnhancedDialog({super.key, required this.commande});
@override
_PaymentMethodEnhancedDialogState createState() => _PaymentMethodEnhancedDialogState();
}
class _PaymentMethodEnhancedDialogState extends State<PaymentMethodEnhancedDialog> {
PaymentType _selectedPayment = PaymentType.cash;
final _amountController = TextEditingController();
Remise? _appliedRemise;
@override
void initState() {
super.initState();
_amountController.text = widget.commande.montantTotal.toStringAsFixed(2);
}
@override
void dispose() {
_amountController.dispose();
super.dispose();
}
void _showDiscountDialog() {
showDialog(
context: context,
builder: (context) => DiscountDialog(
onDiscountApplied: (remise) {
setState(() {
_appliedRemise = remise;
final montantFinal = widget.commande.montantTotal - remise.calculerRemise(widget.commande.montantTotal);
_amountController.text = montantFinal.toStringAsFixed(2);
});
},
),
);
}
void _removeDiscount() {
setState(() {
_appliedRemise = null;
_amountController.text = widget.commande.montantTotal.toStringAsFixed(2);
});
}
void _validatePayment() {
final montantFinal = _appliedRemise != null
? widget.commande.montantTotal - _appliedRemise!.calculerRemise(widget.commande.montantTotal)
: widget.commande.montantTotal;
if (_selectedPayment == PaymentType.cash) {
final amountGiven = double.tryParse(_amountController.text) ?? 0;
if (amountGiven < montantFinal) {
Get.snackbar(
'Erreur',
'Le montant donné est insuffisant',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
}
Navigator.pop(context, PaymentMethodEnhanced(
type: _selectedPayment,
amountGiven: _selectedPayment == PaymentType.cash
? double.parse(_amountController.text)
: montantFinal,
remise: _appliedRemise,
));
}
@override
Widget build(BuildContext context) {
final montantOriginal = widget.commande.montantTotal;
final montantFinal = _appliedRemise != null
? montantOriginal - _appliedRemise!.calculerRemise(montantOriginal)
: montantOriginal;
final amount = double.tryParse(_amountController.text) ?? 0;
final change = amount - montantFinal;
return AlertDialog(
title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Résumé des montants
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant original:'),
Text('${montantOriginal.toStringAsFixed(0)} MGA'),
],
),
if (_appliedRemise != null) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Remise (${_appliedRemise!.libelle}):'),
Text(
'- ${_appliedRemise!.calculerRemise(montantOriginal).toStringAsFixed(0)} MGA',
style: const TextStyle(color: Colors.red),
),
],
),
const Divider(),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('${montantFinal.toStringAsFixed(0)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
],
),
),
const SizedBox(height: 16),
// Bouton remise
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _appliedRemise == null ? _showDiscountDialog : _removeDiscount,
icon: Icon(_appliedRemise == null ? Icons.local_offer : Icons.close),
label: Text(_appliedRemise == null ? 'Ajouter remise' : 'Supprimer remise'),
style: OutlinedButton.styleFrom(
foregroundColor: _appliedRemise == null ? Colors.orange : Colors.red,
side: BorderSide(
color: _appliedRemise == null ? Colors.orange : Colors.red,
),
),
),
),
],
),
const SizedBox(height: 16),
// Section Paiement mobile
const Align(
alignment: Alignment.centerLeft,
child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildMobileMoneyTile(
title: 'Mvola',
imagePath: 'assets/mvola.jpg',
value: PaymentType.mvola,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMobileMoneyTile(
title: 'Orange Money',
imagePath: 'assets/Orange_money.png',
value: PaymentType.orange,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMobileMoneyTile(
title: 'Airtel Money',
imagePath: 'assets/airtel_money.png',
value: PaymentType.airtel,
),
),
],
),
const SizedBox(height: 16),
// Section Carte bancaire
const Align(
alignment: Alignment.centerLeft,
child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)),
),
const SizedBox(height: 8),
_buildPaymentMethodTile(
title: 'Carte bancaire',
icon: Icons.credit_card,
value: PaymentType.card,
),
const SizedBox(height: 16),
// Section Paiement en liquide
const Align(
alignment: Alignment.centerLeft,
child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)),
),
const SizedBox(height: 8),
_buildPaymentMethodTile(
title: 'Paiement en liquide',
icon: Icons.money,
value: PaymentType.cash,
),
if (_selectedPayment == PaymentType.cash) ...[
const SizedBox(height: 12),
TextField(
controller: _amountController,
decoration: const InputDecoration(
labelText: 'Montant donné',
prefixText: 'MGA ',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.numberWithOptions(decimal: true),
onChanged: (value) => setState(() {}),
),
const SizedBox(height: 8),
Text(
'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: change >= 0 ? Colors.green : Colors.red,
),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler', style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
),
onPressed: _validatePayment,
child: const Text('Confirmer'),
),
],
);
}
Widget _buildMobileMoneyTile({
required String title,
required String imagePath,
required PaymentType value,
}) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2),
width: 2,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => setState(() => _selectedPayment = value),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Image.asset(
imagePath,
height: 30,
width: 30,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.mobile_friendly, size: 30),
),
const SizedBox(height: 8),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12),
),
],
),
),
),
);
}
Widget _buildPaymentMethodTile({
required String title,
required IconData icon,
required PaymentType value,
}) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2),
width: 2,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => setState(() => _selectedPayment = value),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(icon, size: 24),
const SizedBox(width: 12),
Text(title),
],
),
),
),
);
}
}

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

782
lib/Components/appDrawer.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/Views/HandleProduct.dart'; import 'package:youmazgestion/Views/HandleProduct.dart';
import 'package:youmazgestion/Views/RoleListPage.dart'; import 'package:youmazgestion/Views/RoleListPage.dart';
import 'package:youmazgestion/Views/commandManagement.dart'; import 'package:youmazgestion/Views/commandManagement.dart';
@ -13,6 +14,7 @@ import 'package:youmazgestion/Views/newCommand.dart';
import 'package:youmazgestion/Views/registrationPage.dart'; import 'package:youmazgestion/Views/registrationPage.dart';
import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/controller/userController.dart';
import 'package:youmazgestion/Views/gestion_point_de_vente.dart';
class CustomDrawer extends StatelessWidget { class CustomDrawer extends StatelessWidget {
final UserController userController = Get.find<UserController>(); final UserController userController = Get.find<UserController>();
@ -23,6 +25,7 @@ class CustomDrawer extends StatelessWidget {
await prefs.remove('role'); await prefs.remove('role');
await prefs.remove('user_id'); await prefs.remove('user_id');
// IMPORTANT: Vider le cache de session
userController.clearUserData(); userController.clearUserData();
} }
@ -32,308 +35,573 @@ class CustomDrawer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Drawer( return Drawer(
backgroundColor: Colors.white, backgroundColor: Colors.white,
child: FutureBuilder( child: GetBuilder<UserController>(
future: _buildDrawerItems(), builder: (controller) {
builder: (context, snapshot) { return ListView(
if (snapshot.connectionState == ConnectionState.done) { padding: EdgeInsets.zero,
return ListView( children: [
padding: EdgeInsets.zero, // Header utilisateur
children: snapshot.data as List<Widget>, _buildUserHeader(controller),
);
} else { // CORRIGÉ: Construction avec gestion des valeurs null
return const Center(child: CircularProgressIndicator()); ..._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 = []; List<Widget> drawerItems = [];
drawerItems.add( // Vérifier si le cache est prêt
GetBuilder<UserController>( if (!userController.isCacheReady) {
builder: (controller) => Container( return [
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20), const Padding(
decoration: const BoxDecoration( padding: EdgeInsets.all(16.0),
gradient: LinearGradient( child: Column(
colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [ children: [
const CircleAvatar( SizedBox(
radius: 30, height: 20,
backgroundImage: AssetImage("assets/youmaz2.png"), width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
), ),
const SizedBox(width: 15), SizedBox(height: 8),
Column( Text(
crossAxisAlignment: CrossAxisAlignment.start, "Chargement du menu...",
children: [ style: TextStyle(color: Colors.grey, fontSize: 12),
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,
),
),
],
), ),
], ],
), ),
), ),
), ];
); }
drawerItems.add( // Récupérer les menus depuis le cache de session
await _buildDrawerItem( final rawUserMenus = userController.getUserMenus();
icon: Icons.home,
title: "Accueil",
color: Colors.blue,
permissionAction: 'view',
permissionRoute: '/accueil',
onTap: () => Get.to(const AccueilPage()),
),
);
List<Widget> gestionUtilisateursItems = [ // 🛡 VALIDATION: Filtrer les menus valides
await _buildDrawerItem( final validMenus = <Map<String, dynamic>>[];
icon: Icons.person_add, final invalidMenus = <Map<String, dynamic>>[];
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)) { for (var menu in rawUserMenus) {
drawerItems.add( // Vérifier que les champs essentiels ne sont pas null
const Padding( final name = menu['name'];
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), final route = menu['route'];
child: Text( final id = menu['id'];
"GESTION UTILISATEURS",
style: TextStyle( if (name != null && route != null && route.toString().isNotEmpty) {
color: Colors.grey, validMenus.add({
fontSize: 12, 'id': id,
fontWeight: FontWeight.bold, 'name': name.toString(),
), 'route': route.toString(),
), });
), } else {
); invalidMenus.add(menu);
drawerItems.addAll(gestionUtilisateursItems); print("⚠️ Menu invalide ignoré dans CustomDrawer: id=$id, name='$name', route='$route'");
}
} }
List<Widget> gestionProduitsItems = [ // Afficher les statistiques de validation
await _buildDrawerItem( if (invalidMenus.isNotEmpty) {
icon: Icons.inventory, print("📊 CustomDrawer: ${validMenus.length} menus valides, ${invalidMenus.length} invalides");
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)) { if (validMenus.isEmpty) {
drawerItems.add( return [
const Padding( const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), padding: EdgeInsets.all(16.0),
child: Text( child: Text(
"GESTION PRODUITS", "Aucun menu accessible",
style: TextStyle( textAlign: TextAlign.center,
color: Colors.grey, style: TextStyle(color: Colors.grey),
fontSize: 12,
fontWeight: FontWeight.bold,
),
), ),
), ),
); ];
drawerItems.addAll(gestionProduitsItems);
} }
List<Widget> gestionCommandesItems = [ // 🔧 DÉDUPLICATION: Éliminer les doublons par route
await _buildDrawerItem( final Map<String, Map<String, dynamic>> uniqueMenus = {};
icon: Icons.add_shopping_cart, for (var menu in validMenus) {
title: "Nouvelle commande", final route = menu['route'] as String;
color: Colors.orange, uniqueMenus[route] = menu;
permissionAction: 'create', }
permissionRoute: '/nouvelle-commande', final deduplicatedMenus = uniqueMenus.values.toList();
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()),
),
];
if (gestionCommandesItems.any((item) => item is ListTile)) { if (deduplicatedMenus.length != validMenus.length) {
drawerItems.add( print("🔧 CustomDrawer: ${validMenus.length - deduplicatedMenus.length} doublons supprimés");
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);
} }
List<Widget> rapportsItems = [ // Organiser les menus par catégories
await _buildDrawerItem( final Map<String, List<Map<String, dynamic>>> categorizedMenus = {
icon: Icons.bar_chart, 'GESTION UTILISATEURS': [],
title: "Bilan mensuel", 'GESTION PRODUITS': [],
color: Colors.teal, 'GESTION COMMANDES': [],
permissionAction: 'read', 'RAPPORTS': [],
permissionRoute: '/bilan', 'ADMINISTRATION': [],
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)) { // Accueil toujours en premier
drawerItems.add( final accueilMenu = deduplicatedMenus.where((menu) => menu['route'] == '/accueil').firstOrNull;
const Padding( if (accueilMenu != null) {
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), drawerItems.add(_buildDrawerItemFromMenu(accueilMenu));
child: Text(
"RAPPORTS",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
);
drawerItems.addAll(rapportsItems);
} }
List<Widget> administrationItems = [ // Catégoriser les autres menus avec validation supplémentaire
await _buildDrawerItem( for (var menu in deduplicatedMenus) {
icon: Icons.admin_panel_settings, final route = menu['route'] as String;
title: "Gérer les rôles",
color: Colors.redAccent,
permissionAction: 'admin',
permissionRoute: '/gerer-roles',
onTap: () => Get.to(const RoleListPage()),
),
];
if (administrationItems.any((item) => item is ListTile)) { // Validation supplémentaire avant categorisation
drawerItems.add( if (route.isEmpty) {
const Padding( print("⚠️ Route vide ignorée: ${menu['name']}");
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), continue;
child: Text( }
"ADMINISTRATION",
style: TextStyle( switch (route) {
color: Colors.grey, case '/accueil':
fontSize: 12, // Déjà traité
fontWeight: FontWeight.bold, break;
), case '/ajouter-utilisateur':
), case '/modifier-utilisateur':
), case '/pointage':
); categorizedMenus['GESTION UTILISATEURS']!.add(menu);
drawerItems.addAll(administrationItems); 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;
}
}
// 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));
}
}
});
return drawerItems;
}
/// 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();
} }
drawerItems.add(const Divider()); // 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( final routeData = routeMapping[route];
ListTile( if (routeData == null) {
leading: const Icon(Icons.logout, color: Colors.red), print("⚠️ Route non reconnue: '$route' pour le menu '$name'");
title: const Text("Déconnexion"), 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: () { onTap: () {
Get.defaultDialog( Get.snackbar(
title: "Déconnexion", "Route non configurée",
content: const Text("Voulez-vous vraiment vous déconnecter ?"), "La route '$route' n'est pas encore configurée",
actions: [ snackPosition: SnackPosition.BOTTOM,
TextButton( backgroundColor: Colors.orange.shade100,
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());
},
),
],
); );
}, },
);
}
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({ /// Header utilisateur amélioré
required IconData icon, Widget _buildUserHeader(UserController controller) {
required String title, return Container(
required Color color, padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
String? permissionAction, decoration: const BoxDecoration(
String? permissionRoute, gradient: LinearGradient(
required VoidCallback onTap, colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
}) async { begin: Alignment.topLeft,
if (permissionAction != null && permissionRoute != null) { end: Alignment.bottomRight,
bool hasPermission = await userController.hasPermission(permissionAction, permissionRoute); ),
if (!hasPermission) { ),
return const SizedBox.shrink(); 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( return ListTile(
leading: Icon(icon, color: color), leading: const Icon(Icons.logout, color: Colors.red),
title: Text(title), title: const Text("Déconnexion"),
trailing: permissionAction != null onTap: () {
? const Icon(Icons.chevron_right, color: Colors.grey) Get.dialog(
: null, AlertDialog(
onTap: onTap, 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,
);
},
); );
} }
} }

112
lib/Components/app_bar.dart

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

175
lib/Components/commandManagementComponents/CommandDetails.dart

@ -0,0 +1,175 @@
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) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
text,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
textAlign: TextAlign.center,
),
);
}
Widget _buildTableCell(String text) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
text,
style: const TextStyle(fontSize: 13),
textAlign: TextAlign.center,
),
);
}
@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!;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'Détails de la commande',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.black87,
),
),
),
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.'),
_buildTableHeader('Total'),
],
),
...details.map((detail) => TableRow(
children: [
_buildTableCell(
detail.estCadeau == true
? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)'
: detail.produitNom ?? 'Produit inconnu'
),
_buildTableCell('${detail.quantite}'),
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(2)} MGA'),
],
)),
],
),
),
const SizedBox(height: 12),
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: [
if (commande.montantApresRemise != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Sous-total:',
style: TextStyle(fontSize: 14),
),
Text(
'${commande.montantTotal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontSize: 14),
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Remise:',
style: TextStyle(fontSize: 14),
),
Text(
'-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA',
style: const TextStyle(
fontSize: 14,
color: Colors.red,
),
),
],
),
const Divider(),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total de la commande:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(2)} MGA',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Colors.green.shade700,
),
),
],
),
],
),
),
],
);
},
);
}
}

226
lib/Components/commandManagementComponents/CommandeActions.dart

@ -0,0 +1,226 @@
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;
final Function(Commande) onDiscountSelected;
final Function(Commande) onGiftSelected;
const CommandeActions({
required this.commande,
required this.onStatutChanged,
required this.onPaymentSelected,
required this.onDiscountSelected,
required this.onGiftSelected,
});
List<Widget> _buildActionButtons(BuildContext context) {
List<Widget> buttons = [];
switch (commande.statut) {
case StatutCommande.enAttente:
buttons.addAll([
_buildActionButton(
label: 'Remise',
icon: Icons.percent,
color: Colors.orange,
onPressed: () => onDiscountSelected(commande),
),
_buildActionButton(
label: 'Cadeau',
icon: Icons.card_giftcard,
color: Colors.purple,
onPressed: () => onGiftSelected(commande),
),
_buildActionButton(
label: 'Confirmer',
icon: Icons.check_circle,
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();
}
}

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

288
lib/Components/commandManagementComponents/PaymentMethodDialog.dart

@ -0,0 +1,288 @@
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.montantApresRemise ?? 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.montantApresRemise ?? 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.montantApresRemise ?? 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
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: [
if (widget.commande.montantApresRemise != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant original:'),
Text('${widget.commande.montantTotal.toStringAsFixed(2)} MGA'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Remise:'),
Text('-${(widget.commande.montantTotal - widget.commande.montantApresRemise!).toStringAsFixed(2)} MGA',
style: const TextStyle(color: Colors.red)),
],
),
const Divider(),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('${montantFinal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
],
),
),
const 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
}

7
lib/Components/paymentType.dart

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

2125
lib/Components/teat.dart

File diff suppressed because it is too large

267
lib/Models/Client.dart

@ -1,4 +1,4 @@
// Models/client.dart // Models/client.dart - Version corrigée pour MySQL
class Client { class Client {
final int? id; final int? id;
final String nom; final String nom;
@ -33,29 +33,49 @@ class Client {
}; };
} }
// Fonction helper améliorée pour parser les dates
static DateTime _parseDateTime(dynamic dateValue) {
if (dateValue == null) return DateTime.now();
if (dateValue is DateTime) return dateValue;
if (dateValue is String) {
try {
return DateTime.parse(dateValue);
} catch (e) {
print("Erreur parsing date string: $dateValue, erreur: $e");
return DateTime.now();
}
}
// Pour MySQL qui peut retourner un Timestamp
if (dateValue is int) {
return DateTime.fromMillisecondsSinceEpoch(dateValue);
}
print("Type de date non reconnu: ${dateValue.runtimeType}, valeur: $dateValue");
return DateTime.now();
}
factory Client.fromMap(Map<String, dynamic> map) { factory Client.fromMap(Map<String, dynamic> map) {
return Client( return Client(
id: map['id'], id: map['id'] as int?,
nom: map['nom'], nom: map['nom'] as String,
prenom: map['prenom'], prenom: map['prenom'] as String,
email: map['email'], email: map['email'] as String,
telephone: map['telephone'], telephone: map['telephone'] as String,
adresse: map['adresse'], adresse: map['adresse'] as String?,
dateCreation: DateTime.parse(map['dateCreation']), dateCreation: _parseDateTime(map['dateCreation']),
actif: map['actif'] == 1, actif: (map['actif'] as int?) == 1,
); );
} }
String get nomComplet => '$prenom $nom'; String get nomComplet => '$prenom $nom';
} }
// Models/commande.dart
enum StatutCommande { enum StatutCommande {
enAttente, enAttente,
confirmee, confirmee,
enPreparation,
expediee,
livree,
annulee annulee
} }
@ -67,25 +87,51 @@ class Commande {
final double montantTotal; final double montantTotal;
final String? notes; final String? notes;
final DateTime? dateLivraison; final DateTime? dateLivraison;
final int? commandeurId;
// Données du client (pour les jointures) final int? validateurId;
final String? clientNom; final String? clientNom;
final String? clientPrenom; final String? clientPrenom;
final String? clientEmail; final String? clientEmail;
final double? remisePourcentage;
final double? remiseMontant;
final double? montantApresRemise;
Commande({ Commande({
this.id, this.id,
required this.clientId, required this.clientId,
required this.dateCommande, required this.dateCommande,
this.statut = StatutCommande.enAttente, required this.statut,
required this.montantTotal, required this.montantTotal,
this.notes, this.notes,
this.dateLivraison, this.dateLivraison,
this.commandeurId,
this.validateurId,
this.clientNom, this.clientNom,
this.clientPrenom, this.clientPrenom,
this.clientEmail, this.clientEmail,
this.remisePourcentage,
this.remiseMontant,
this.montantApresRemise,
}); });
String get clientNomComplet {
if (clientNom != null && clientPrenom != null) {
return '$clientPrenom $clientNom';
}
return 'Client inconnu';
}
String get statutLibelle {
switch (statut) {
case StatutCommande.enAttente:
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.annulee:
return 'Annulée';
}
}
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
@ -95,50 +141,81 @@ class Commande {
'montantTotal': montantTotal, 'montantTotal': montantTotal,
'notes': notes, 'notes': notes,
'dateLivraison': dateLivraison?.toIso8601String(), 'dateLivraison': dateLivraison?.toIso8601String(),
'commandeurId': commandeurId,
'validateurId': validateurId,
'remisePourcentage': remisePourcentage,
'remiseMontant': remiseMontant,
'montantApresRemise': montantApresRemise,
}; };
} }
factory Commande.fromMap(Map<String, dynamic> map) { factory Commande.fromMap(Map<String, dynamic> map) {
return Commande( return Commande(
id: map['id'], id: map['id'] as int?,
clientId: map['clientId'], clientId: map['clientId'] as int,
dateCommande: DateTime.parse(map['dateCommande']), dateCommande: Client._parseDateTime(map['dateCommande']),
statut: StatutCommande.values[map['statut']], statut: StatutCommande.values[(map['statut'] as int)],
montantTotal: map['montantTotal'].toDouble(), montantTotal: (map['montantTotal'] as num).toDouble(),
notes: map['notes'], notes: map['notes'] as String?,
dateLivraison: map['dateLivraison'] != null dateLivraison: map['dateLivraison'] != null
? DateTime.parse(map['dateLivraison']) ? Client._parseDateTime(map['dateLivraison'])
: null,
commandeurId: map['commandeurId'] as int?,
validateurId: map['validateurId'] as int?,
clientNom: map['clientNom'] as String?,
clientPrenom: map['clientPrenom'] as String?,
clientEmail: map['clientEmail'] as String?,
remisePourcentage: map['remisePourcentage'] != null
? (map['remisePourcentage'] as num).toDouble()
: null,
remiseMontant: map['remiseMontant'] != null
? (map['remiseMontant'] as num).toDouble()
: null,
montantApresRemise: map['montantApresRemise'] != null
? (map['montantApresRemise'] as num).toDouble()
: null, : null,
clientNom: map['clientNom'],
clientPrenom: map['clientPrenom'],
clientEmail: map['clientEmail'],
); );
} }
String get statutLibelle { Commande copyWith({
switch (statut) { int? id,
case StatutCommande.enAttente: int? clientId,
return 'En attente'; DateTime? dateCommande,
case StatutCommande.confirmee: StatutCommande? statut,
return 'Confirmée'; double? montantTotal,
case StatutCommande.enPreparation: String? notes,
return 'En préparation'; DateTime? dateLivraison,
case StatutCommande.expediee: int? commandeurId,
return 'Expédiée'; int? validateurId,
case StatutCommande.livree: String? clientNom,
return 'Livrée'; String? clientPrenom,
case StatutCommande.annulee: String? clientEmail,
return 'Annulée'; double? remisePourcentage,
} double? remiseMontant,
double? montantApresRemise,
}) {
return Commande(
id: id ?? this.id,
clientId: clientId ?? this.clientId,
dateCommande: dateCommande ?? this.dateCommande,
statut: statut ?? this.statut,
montantTotal: montantTotal ?? this.montantTotal,
notes: notes ?? this.notes,
dateLivraison: dateLivraison ?? this.dateLivraison,
commandeurId: commandeurId ?? this.commandeurId,
validateurId: validateurId ?? this.validateurId,
clientNom: clientNom ?? this.clientNom,
clientPrenom: clientPrenom ?? this.clientPrenom,
clientEmail: clientEmail ?? this.clientEmail,
remisePourcentage: remisePourcentage ?? this.remisePourcentage,
remiseMontant: remiseMontant ?? this.remiseMontant,
montantApresRemise: montantApresRemise ?? this.montantApresRemise,
);
} }
String get clientNomComplet =>
clientPrenom != null && clientNom != null
? '$clientPrenom $clientNom'
: 'Client inconnu';
} }
// Models/detail_commande.dart // REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci :
class DetailCommande { class DetailCommande {
final int? id; final int? id;
final int commandeId; final int commandeId;
@ -146,11 +223,15 @@ class DetailCommande {
final int quantite; final int quantite;
final double prixUnitaire; final double prixUnitaire;
final double sousTotal; final double sousTotal;
// Données du produit (pour les jointures)
final String? produitNom; final String? produitNom;
final String? produitImage; final String? produitImage;
final String? produitReference; final String? produitReference;
final bool? estCadeau;
// NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT
final double? remisePourcentage;
final double? remiseMontant;
final double? prixApresRemise;
DetailCommande({ DetailCommande({
this.id, this.id,
@ -162,6 +243,10 @@ class DetailCommande {
this.produitNom, this.produitNom,
this.produitImage, this.produitImage,
this.produitReference, this.produitReference,
this.estCadeau,
this.remisePourcentage,
this.remiseMontant,
this.prixApresRemise,
}); });
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@ -172,20 +257,86 @@ class DetailCommande {
'quantite': quantite, 'quantite': quantite,
'prixUnitaire': prixUnitaire, 'prixUnitaire': prixUnitaire,
'sousTotal': sousTotal, 'sousTotal': sousTotal,
'estCadeau': estCadeau == true ? 1 : 0,
'remisePourcentage': remisePourcentage,
'remiseMontant': remiseMontant,
'prixApresRemise': prixApresRemise,
}; };
} }
factory DetailCommande.fromMap(Map<String, dynamic> map) { factory DetailCommande.fromMap(Map<String, dynamic> map) {
return DetailCommande( return DetailCommande(
id: map['id'], id: map['id'] as int?,
commandeId: map['commandeId'], commandeId: map['commandeId'] as int,
produitId: map['produitId'], produitId: map['produitId'] as int,
quantite: map['quantite'], quantite: map['quantite'] as int,
prixUnitaire: map['prixUnitaire'].toDouble(), prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
sousTotal: map['sousTotal'].toDouble(), sousTotal: (map['sousTotal'] as num).toDouble(),
produitNom: map['produitNom'], produitNom: map['produitNom'] as String?,
produitImage: map['produitImage'], produitImage: map['produitImage'] as String?,
produitReference: map['produitReference'], produitReference: map['produitReference'] as String?,
estCadeau: map['estCadeau'] == 1,
remisePourcentage: map['remisePourcentage'] != null
? (map['remisePourcentage'] as num).toDouble()
: null,
remiseMontant: map['remiseMontant'] != null
? (map['remiseMontant'] as num).toDouble()
: null,
prixApresRemise: map['prixApresRemise'] != null
? (map['prixApresRemise'] as num).toDouble()
: null,
);
}
DetailCommande copyWith({
int? id,
int? commandeId,
int? produitId,
int? quantite,
double? prixUnitaire,
double? sousTotal,
String? produitNom,
String? produitImage,
String? produitReference,
bool? estCadeau,
double? remisePourcentage,
double? remiseMontant,
double? prixApresRemise,
}) {
return DetailCommande(
id: id ?? this.id,
commandeId: commandeId ?? this.commandeId,
produitId: produitId ?? this.produitId,
quantite: quantite ?? this.quantite,
prixUnitaire: prixUnitaire ?? this.prixUnitaire,
sousTotal: sousTotal ?? this.sousTotal,
produitNom: produitNom ?? this.produitNom,
produitImage: produitImage ?? this.produitImage,
produitReference: produitReference ?? this.produitReference,
estCadeau: estCadeau ?? this.estCadeau,
remisePourcentage: remisePourcentage ?? this.remisePourcentage,
remiseMontant: remiseMontant ?? this.remiseMontant,
prixApresRemise: prixApresRemise ?? this.prixApresRemise,
); );
} }
// GETTERS QUI RÉSOLVENT LE PROBLÈME "aUneRemise" INTROUVABLE
double get prixFinalUnitaire {
return prixApresRemise ?? prixUnitaire;
}
double get sousTotalAvecRemise {
return quantite * prixFinalUnitaire;
}
bool get aUneRemise {
return remisePourcentage != null || remiseMontant != null || prixApresRemise != null;
}
double get montantRemise {
if (prixApresRemise != null) {
return (prixUnitaire - prixApresRemise!) * quantite;
}
return 0.0;
}
} }

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 { class Product {
int? id; final int? id;
final String name; final String name;
final double price; final double price;
final String? image; final String? image;
final String category; final String category;
final int? stock; final int stock;
final String? description; final String? description;
String? qrCode; String? qrCode;
final String? reference; final String? reference;
final int? pointDeVenteId;
final String? marque;
final String? ram;
final String? memoireInterne;
final String? imei;
Product({ Product({
this.id, this.id,
@ -16,44 +25,138 @@ class Product {
this.image, this.image,
required this.category, required this.category,
this.stock = 0, this.stock = 0,
this.description = '', this.description,
this.qrCode, this.qrCode,
this.reference, this.reference,
this.pointDeVenteId,
this.marque,
this.ram,
this.memoireInterne,
this.imei,
}); });
// Vérifie si le stock est défini
bool isStockDefined() { bool isStockDefined() {
if (stock != null) { return stock > 0;
print("stock is defined : $stock $name"); }
return true;
} else { // Méthode helper pour convertir de façon sécurisée
return false; static String? _convertImageFromMap(dynamic imageValue) {
if (imageValue == null) {
return null;
}
// Si c'est déjà une String, on la retourne
if (imageValue is String) {
return imageValue;
}
// Le driver mysql1 peut retourner un Blob même pour TEXT
// Essayer de le convertir en String
try {
if (imageValue is Uint8List) {
// Convertir les bytes en String UTF-8
return utf8.decode(imageValue);
}
if (imageValue is List<int>) {
// Convertir les bytes en String UTF-8
return utf8.decode(imageValue);
}
// Dernier recours : toString()
return imageValue.toString();
} catch (e) {
print("Erreur conversion image: $e, type: ${imageValue.runtimeType}");
return null;
} }
} }
Map<String, dynamic> toMap() {
return { factory Product.fromMap(Map<String, dynamic> map) => Product(
'id': id, id: map['id'] as int?,
'name': name, name: map['name'] as String,
'price': price, price: (map['price'] as num).toDouble(), // Conversion sécurisée
'image': image ?? '', image: _convertImageFromMap(map['image']), // Utilisation de la méthode helper
'category': category, category: map['category'] as String,
'stock': stock ?? 0, stock: (map['stock'] as int?) ?? 0, // Valeur par défaut
'description': description ?? '', description: map['description'] as String?,
'qrCode': qrCode ?? '', qrCode: map['qrCode'] as String?,
'reference': reference ?? '', 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( return Product(
id: map['id'], id: id ?? this.id,
name: map['name'], name: name ?? this.name,
price: map['price'], price: price ?? this.price,
image: map['image'], image: image ?? this.image,
category: map['category'], category: category ?? this.category,
stock: map['stock'], stock: stock ?? this.stock,
description: map['description'], description: description ?? this.description,
qrCode: map['qrCode'], qrCode: qrCode ?? this.qrCode,
reference: map['reference'], reference: reference ?? this.reference,
pointDeVenteId: pointDeVenteId ?? this.pointDeVenteId,
marque: marque ?? this.marque,
ram: ram ?? this.ram,
memoireInterne: memoireInterne ?? this.memoireInterne,
imei: imei ?? this.imei,
); );
} }
} }

26
lib/Models/users.dart

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

0
lib/Services/GestionStockDatabase.dart

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

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

1960
lib/Services/stock_managementDatabase.dart

File diff suppressed because it is too large

1205
lib/Views/Dashboard.dart

File diff suppressed because it is too large

4604
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

7
lib/Views/RoleListPage.dart

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

610
lib/Views/RolePermissionPage.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Models/Permission.dart'; import 'package:youmazgestion/Models/Permission.dart';
import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Models/role.dart'; import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class RolePermissionsPage extends StatefulWidget { class RolePermissionsPage extends StatefulWidget {
final Role role; final Role role;
@ -18,6 +19,8 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
List<Permission> permissions = []; List<Permission> permissions = [];
List<Map<String, dynamic>> menus = []; List<Map<String, dynamic>> menus = [];
Map<int, Map<String, bool>> menuPermissionsMap = {}; Map<int, Map<String, bool>> menuPermissionsMap = {};
bool isLoading = true;
String? errorMessage;
@override @override
void initState() { void initState() {
@ -26,152 +29,559 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
} }
Future<void> _initData() async { Future<void> _initData() async {
final perms = await db.getAllPermissions(); try {
final menuList = await db.database.then((db) => db.query('menu')); 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) { Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(
widget.role.id!, menuId);
tempMenuPermissionsMap[menuId] = { for (var menu in menuList) {
for (var perm in perms) final menuId = menu['id'] as int;
perm.name: menuPerms.any((mp) => mp.name == perm.name) final menuPerms = await db.getPermissionsForRoleAndMenu(
}; widget.role.id!, menuId);
}
setState(() { tempMenuPermissionsMap[menuId] = {
permissions = perms; for (var perm in perms)
menus = menuList; perm.name: menuPerms.any((mp) => mp.name == perm.name)
menuPermissionsMap = tempMenuPermissionsMap; };
}); }
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( Future<void> _onPermissionToggle(
int menuId, String permission, bool enabled) async { 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( if (enabled) {
widget.role.id!, menuId, perm.id!); await db.assignRoleMenuPermission(
} else { widget.role.id!, menuId, perm.id!);
await db.removeRoleMenuPermission( } else {
widget.role.id!, menuId, perm.id!); 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(() { void _toggleAllPermissions(int menuId, bool enabled) {
menuPermissionsMap[menuId]![permission] = enabled; for (var permission in permissions) {
}); _onPermissionToggle(menuId, permission.name, enabled);
}
} }
@override int _getSelectedPermissionsCount(int menuId) {
Widget build(BuildContext context) { return menuPermissionsMap[menuId]?.values.where((selected) => selected).length ?? 0;
return Scaffold( }
appBar: CustomAppBar(
title: "Permissions - ${widget.role.designation}", double _getPermissionPercentage(int menuId) {
// showBackButton: true, if (permissions.isEmpty) return 0.0;
), return _getSelectedPermissionsCount(menuId) / permissions.length;
body: Padding( }
Widget _buildPermissionSummary() {
int totalPermissions = menus.length * permissions.length;
int selectedPermissions = 0;
for (var menuId in menuPermissionsMap.keys) {
selectedPermissions += _getSelectedPermissionsCount(menuId);
}
double percentage = totalPermissions > 0 ? selectedPermissions / totalPermissions : 0.0;
return Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
'Gestion des permissions pour le rôle: ${widget.role.designation}', children: [
style: Theme.of(context).textTheme.titleLarge?.copyWith( Icon(Icons.analytics, color: Colors.blue.shade600),
const SizedBox(width: 8),
Text(
'Résumé des permissions',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
), ),
),
],
), ),
const SizedBox(height: 10), const SizedBox(height: 12),
const Text( LinearProgressIndicator(
'Sélectionnez les permissions pour chaque menu:', value: percentage,
style: TextStyle(fontSize: 14, color: Colors.grey), backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
percentage > 0.7 ? Colors.green :
percentage > 0.3 ? Colors.orange : Colors.red,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 8),
if (permissions.isNotEmpty && menus.isNotEmpty) Row(
Expanded( mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: ListView.builder( children: [
itemCount: menus.length, Text(
itemBuilder: (context, index) { '$selectedPermissions / $totalPermissions permissions',
final menu = menus[index]; style: const TextStyle(fontWeight: FontWeight.w500),
final menuId = menu['id'] as int; ),
final menuName = menu['name'] as String; Text(
'${(percentage * 100).toStringAsFixed(1)}%',
return Card( style: const TextStyle(
margin: const EdgeInsets.only(bottom: 15), fontWeight: FontWeight.bold,
elevation: 3, color: Colors.blue,
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), Widget _buildMenuCard(Map<String, dynamic> menu) {
Wrap( final menuId = menu['id'] as int;
spacing: 10, final menuName = menu['name'] as String;
runSpacing: 10, final menuRoute = menu['route'] as String;
children: permissions.map((perm) { final selectedCount = _getSelectedPermissionsCount(menuId);
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false; final percentage = _getPermissionPercentage(menuId);
return FilterChip(
label: perm.name, return Card(
selected: isChecked, margin: const EdgeInsets.only(bottom: 16),
onSelected: (bool value) { elevation: 3,
_onPermissionToggle(menuId, perm.name, value); shape: RoundedRectangleBorder(
}, borderRadius: BorderRadius.circular(12),
); ),
}).toList(), 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,
),
),
), ),
) const SizedBox(width: 8),
else Text(
const Expanded( '$selectedCount/${permissions.length}',
child: Center( style: const TextStyle(
child: CircularProgressIndicator(), 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 String label;
final bool selected; final bool selected;
final ValueChanged<bool> onSelected; final ValueChanged<bool> onSelected;
const FilterChip({ const CustomFilterChip({
super.key, super.key,
required this.label, required this.label,
required this.selected, required this.selected,
required this.onSelected, required this.onSelected,
}); });
Color _getChipColor(String label) {
switch (label.toLowerCase()) {
case 'view':
case 'read':
return Colors.blue;
case 'create':
return Colors.green;
case 'update':
return Colors.orange;
case 'delete':
return Colors.red;
case 'admin':
return Colors.purple;
case 'manage':
return Colors.indigo;
default:
return Colors.grey;
}
}
IconData _getChipIcon(String label) {
switch (label.toLowerCase()) {
case 'view':
case 'read':
return Icons.visibility;
case 'create':
return Icons.add;
case 'update':
return Icons.edit;
case 'delete':
return Icons.delete;
case 'admin':
return Icons.admin_panel_settings;
case 'manage':
return Icons.settings;
default:
return Icons.security;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChoiceChip( final color = _getChipColor(label);
label: Text(label), final icon = _getChipIcon(label);
return FilterChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: selected ? Colors.white : color,
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
color: selected ? Colors.white : color,
fontWeight: FontWeight.w500,
),
),
],
),
selected: selected, selected: selected,
onSelected: onSelected, onSelected: onSelected,
selectedColor: Colors.blue, selectedColor: color,
labelStyle: TextStyle( backgroundColor: color.withOpacity(0.1),
color: selected ? Colors.white : Colors.black, checkmarkColor: Colors.white,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: selected ? color : color.withOpacity(0.3),
width: 1,
),
), ),
elevation: selected ? 4 : 1,
pressElevation: 8,
); );
} }
} }

2
lib/Views/bilanMois.dart

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

1374
lib/Views/commandManagement.dart

File diff suppressed because it is too large

5
lib/Views/editProduct.dart

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

3
lib/Views/editUser.dart

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

7
lib/Views/gestionProduct.dart

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

462
lib/Views/gestionRole.dart

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

7
lib/Views/gestionStock.dart

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

416
lib/Views/gestion_point_de_vente.dart

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

965
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, init: controller,
builder: (controller) { builder: (controller) {
return Scaffold( return Scaffold(
appBar: const CustomAppBar(title: 'Historique de la journée'), appBar: CustomAppBar(title: 'Historique de la journée'),
body: Column( body: Column(
children: [ children: [
Padding( Padding(

5
lib/Views/listUser.dart

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

636
lib/Views/loginPage.dart

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/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/Views/particles.dart' show ParticleBackground;
import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/Services/app_database.dart';
import '../Models/users.dart'; import '../Models/users.dart';
import '../controller/userController.dart'; import '../controller/userController.dart';
@ -19,9 +20,12 @@ class _LoginPageState extends State<LoginPage> {
late TextEditingController _usernameController; late TextEditingController _usernameController;
late TextEditingController _passwordController; late TextEditingController _passwordController;
final UserController userController = Get.put(UserController()); final UserController userController = Get.put(UserController());
final PermissionCacheService _cacheService = PermissionCacheService.instance; // Nouveau
bool _isErrorVisible = false; bool _isErrorVisible = false;
bool _isLoading = false; bool _isLoading = false;
String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide'; String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
String _loadingMessage = 'Connexion en cours...'; // Nouveau
@override @override
void initState() { void initState() {
@ -34,19 +38,7 @@ class _LoginPageState extends State<LoginPage> {
void checkUserCount() async { void checkUserCount() async {
try { try {
final userCount = await AppDatabase.instance.getUserCount(); final userCount = await AppDatabase.instance.getUserCount();
print('Nombre d\'utilisateurs trouvés: $userCount'); // Debug print('Nombre d\'utilisateurs trouvés: $userCount');
// Commentez cette partie pour permettre le login même sans utilisateurs
/*
if (userCount == 0) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
);
}
}
*/
} catch (error) { } catch (error) {
print('Erreur lors de la vérification du nombre d\'utilisateurs: $error'); print('Erreur lors de la vérification du nombre d\'utilisateurs: $error');
setState(() { setState(() {
@ -63,228 +55,480 @@ class _LoginPageState extends State<LoginPage> {
super.dispose(); super.dispose();
} }
Future<void> saveUserData(Users user, String role, int userId) async { // /// OPTIMISÉ: Sauvegarde avec préchargement des permissions
try { // Future<void> saveUserData(Users user, String role, int userId) async {
// CORRECTION : Utiliser la nouvelle méthode du contrôleur // try {
// Le contrôleur se charge maintenant de tout (observable + SharedPreferences) // userController.setUserWithCredentials(user, role, userId);
userController.setUserWithCredentials(user, role, userId);
print('Utilisateur sauvegardé: ${user.username}, rôle: $role, id: $userId'); // if (user.pointDeVenteId != null) {
} catch (error) { // await userController.loadPointDeVenteDesignation();
print('Erreur lors de la sauvegarde: $error'); // }
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
} // 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');
// }
// }
/// 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
} }
}
void _login() async { /// OPTIMISÉ: Connexion avec préchargement parallèle
if (_isLoading) return; void _login() async {
if (_isLoading) return;
final String username = _usernameController.text.trim(); final String username = _usernameController.text.trim();
final String password = _passwordController.text.trim(); final String password = _passwordController.text.trim();
// Validation basique if (username.isEmpty || password.isEmpty) {
if (username.isEmpty || password.isEmpty) { setState(() {
setState(() { _errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe'; _isErrorVisible = true;
_isErrorVisible = true; });
}); return;
return; }
}
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(() { setState(() {
_isLoading = true; _loadingMessage = 'Vérification...';
_isErrorVisible = false;
}); });
try { 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');
}
// 2. Vérification des identifiants
setState(() {
_loadingMessage = 'Authentification...';
});
// Vérification de la connexion à la base de données bool isValidUser = await dbInstance.verifyUser(username, password);
final dbInstance = AppDatabase.instance;
// Test de connexion à la base if (isValidUser) {
try { setState(() {
final userCount = await dbInstance.getUserCount(); _loadingMessage = 'Chargement du profil...';
print('Base de données accessible, $userCount utilisateurs trouvés'); });
} catch (dbError) {
throw Exception('Impossible d\'accéder à la base de données: $dbError'); // 3. Récupération parallèle des données
} final futures = await Future.wait([
dbInstance.getUser(username),
dbInstance.getUserCredentials(username, password),
]);
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']}');
setState(() {
_loadingMessage = 'Préparation...';
});
// 4. Sauvegarde des données utilisateur
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);
// Vérifier les identifiants // 6. Navigation immédiate
bool isValidUser = await dbInstance.verifyUser(username, password); if (mounted) {
print('Résultat de la vérification: $isValidUser'); if (userCredentials['role'] == 'commercial') {
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}');
// Récupérer les credentials
Map<String, dynamic>? userCredentials =
await dbInstance.getUserCredentials(username, password);
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,
);
// Navigation
if (mounted) {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (context) => const AccueilPage()), MaterialPageRoute(builder: (context) => const MainLayout()),
);
} else {
Navigator.pushReplacement(
context,
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 { } else {
print('Identifiants invalides pour: $username'); throw Exception('Erreur lors de la récupération des credentials');
setState(() {
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true;
});
} }
} catch (error) { } else {
print('Erreur lors de la connexion: $error');
setState(() { setState(() {
_errorMessage = 'Erreur de connexion: ${error.toString()}'; _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true; _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color primaryBlue = const Color(0xFF0033A1);
final Color accentRed = const Color(0xFFD70000);
final Color secondaryBlue = const Color(0xFF1976D2);
final Color primaryColor = primaryBlue;
final Color accentColor = secondaryBlue;
final Color cardColor = Colors.white;
return Scaffold( return Scaffold(
appBar: AppBar( backgroundColor: primaryColor,
title: const Text(
'Login',
style: TextStyle(color: Colors.white),
),
backgroundColor: const Color.fromARGB(255, 4, 54, 95),
centerTitle: true,
),
body: ParticleBackground( body: ParticleBackground(
child: Center( child: Center(
child: Container( child: SingleChildScrollView(
width: MediaQuery.of(context).size.width * 0.5, child: Container(
height: MediaQuery.of(context).size.height * 0.8, width: MediaQuery.of(context).size.width < 500
padding: const EdgeInsets.all(16.0), ? double.infinity
decoration: BoxDecoration( : 400,
color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
shape: BoxShape.rectangle, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.0), color: cardColor.withOpacity(0.98),
), borderRadius: BorderRadius.circular(30.0),
child: Column( boxShadow: [
crossAxisAlignment: CrossAxisAlignment.stretch, BoxShadow(
children: [ color: primaryColor.withOpacity(0.2),
Container( blurRadius: 16,
padding: const EdgeInsets.symmetric(vertical: 16.0), spreadRadius: 4,
child: const Icon( offset: const Offset(0, 8),
Icons.lock_outline,
size: 100.0,
color: Color.fromARGB(255, 4, 54, 95),
), ),
), ],
TextField( ),
controller: _usernameController, child: Column(
enabled: !_isLoading, crossAxisAlignment: CrossAxisAlignment.stretch,
decoration: InputDecoration( children: [
labelText: 'Username', // Header
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent), Center(
border: OutlineInputBorder( child: Column(
borderRadius: BorderRadius.circular(30.0), 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: 24),
const SizedBox(height: 16.0),
TextField( // Username Field
controller: _passwordController, TextField(
enabled: !_isLoading, controller: _usernameController,
decoration: InputDecoration( enabled: !_isLoading,
labelText: 'Password', decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent), labelText: 'Nom d\'utilisateur',
border: OutlineInputBorder( labelStyle: TextStyle(
borderRadius: BorderRadius.circular(30.0), 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, const SizedBox(height: 18.0),
onSubmitted: (_) => _login(),
), // Password Field
const SizedBox(height: 16.0), TextField(
Visibility( controller: _passwordController,
visible: _isErrorVisible, enabled: !_isLoading,
child: Text( obscureText: true,
_errorMessage, decoration: InputDecoration(
style: const TextStyle( labelText: 'Mot de passe',
color: Colors.red, labelStyle: TextStyle(
fontSize: 14, 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, const SizedBox(height: 12),
shape: RoundedRectangleBorder( Row(
borderRadius: BorderRadius.circular(30.0), mainAxisAlignment: MainAxisAlignment.center,
), children: [
minimumSize: const Size(double.infinity, 48), SizedBox(
), height: 16,
child: _isLoading width: 16,
? const SizedBox( child: CircularProgressIndicator(
height: 20, strokeWidth: 2,
width: 20, valueColor: AlwaysStoppedAnimation<Color>(accentColor),
child: CircularProgressIndicator( ),
color: Colors.white, ),
strokeWidth: 2, 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,
),
),
),
],
],
),
), ),
), ),
), ),

2213
lib/Views/mobilepage.dart

File diff suppressed because it is too large

2402
lib/Views/newCommand.dart

File diff suppressed because it is too large

62
lib/Views/produitsCard.dart

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

139
lib/Views/registrationPage.dart

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

7
lib/accueil.dart

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

64
lib/config/DatabaseConfig.dart

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

5
lib/controller/AccueilController.dart

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

187
lib/controller/userController.dart

@ -1,7 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Services/app_database.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Services/PermissionCacheService.dart';
class UserController extends GetxController { class UserController extends GetxController {
final _username = ''.obs; final _username = ''.obs;
@ -10,8 +11,14 @@ class UserController extends GetxController {
final _name = ''.obs; final _name = ''.obs;
final _lastname = ''.obs; final _lastname = ''.obs;
final _password = ''.obs; final _password = ''.obs;
final _userId = 0.obs; // Ajout de l'ID utilisateur final _userId = 0.obs;
final _pointDeVenteId = 0.obs;
final _pointDeVenteDesignation = ''.obs;
// Cache service
final PermissionCacheService _cacheService = PermissionCacheService.instance;
// Getters
String get username => _username.value; String get username => _username.value;
String get email => _email.value; String get email => _email.value;
String get role => _role.value; String get role => _role.value;
@ -19,14 +26,16 @@ class UserController extends GetxController {
String get lastname => _lastname.value; String get lastname => _lastname.value;
String get password => _password.value; String get password => _password.value;
int get userId => _userId.value; int get userId => _userId.value;
int get pointDeVenteId => _pointDeVenteId.value;
String get pointDeVenteDesignation => _pointDeVenteDesignation.value;
@override @override
void onInit() { void onInit() {
super.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 { Future<void> loadUserData() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -34,64 +43,100 @@ class UserController extends GetxController {
final storedUsername = prefs.getString('username') ?? ''; final storedUsername = prefs.getString('username') ?? '';
final storedRole = prefs.getString('role') ?? ''; final storedRole = prefs.getString('role') ?? '';
final storedUserId = prefs.getInt('user_id') ?? 0; final storedUserId = prefs.getInt('user_id') ?? 0;
final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0;
final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? '';
if (storedUsername.isNotEmpty) { if (storedUsername.isNotEmpty) {
try { try {
// Récupérer les données complètes depuis la base de données
Users user = await AppDatabase.instance.getUser(storedUsername); Users user = await AppDatabase.instance.getUser(storedUsername);
// Mettre à jour TOUTES les données
_username.value = user.username; _username.value = user.username;
_email.value = user.email; _email.value = user.email;
_name.value = user.name; _name.value = user.name;
_lastname.value = user.lastName; _lastname.value = user.lastName;
_password.value = user.password; _password.value = user.password;
_role.value = storedRole; // Récupéré depuis SharedPreferences _role.value = storedRole;
_userId.value = storedUserId; // Récupéré depuis SharedPreferences _userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
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) { } catch (dbError) {
print('❌ Erreur DB, chargement depuis SharedPreferences uniquement: $dbError'); print("❌ Erreur BDD, utilisation du fallback: $dbError");
// Fallback : charger depuis SharedPreferences uniquement
_username.value = storedUsername; _username.value = storedUsername;
_email.value = prefs.getString('email') ?? ''; _email.value = prefs.getString('email') ?? '';
_role.value = storedRole; _role.value = storedRole;
_name.value = prefs.getString('name') ?? ''; _name.value = prefs.getString('name') ?? '';
_lastname.value = prefs.getString('lastname') ?? ''; _lastname.value = prefs.getString('lastname') ?? '';
_userId.value = storedUserId; _userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
// Précharger quand même
_preloadPermissionsInBackground();
} }
} else {
print("❌ Aucun utilisateur stocké trouvé");
} }
} catch (e) { } catch (e) {
print('❌ Erreur lors du chargement des données utilisateur: $e'); print('❌ Erreur lors du chargement des données utilisateur: $e');
} }
} }
// 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) { void setUserWithCredentials(Users user, String role, int userId) {
_username.value = user.username; _username.value = user.username;
_email.value = user.email; _email.value = user.email;
_role.value = role; // Rôle depuis les credentials _role.value = role;
_name.value = user.name; _name.value = user.name;
_lastname.value = user.lastName; _lastname.value = user.lastName;
_password.value = user.password; _password.value = user.password;
_userId.value = userId; // ID depuis les credentials _userId.value = userId;
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
print("✅ Utilisateur mis à jour avec credentials:"); print("✅ Utilisateur mis à jour avec credentials:");
print(" Username: ${_username.value}"); print(" Username: ${_username.value}");
print(" Name: ${_name.value}");
print(" Email: ${_email.value}");
print(" Role: ${_role.value}"); print(" Role: ${_role.value}");
print(" UserID: ${_userId.value}"); print(" UserID: ${_userId.value}");
// Sauvegarder dans SharedPreferences
saveUserData(); saveUserData();
// Précharger immédiatement les permissions après connexion
_preloadPermissionsInBackground();
} }
// MÉTHODE EXISTANTE AMÉLIORÉE
void setUser(Users user) { void setUser(Users user) {
_username.value = user.username; _username.value = user.username;
_email.value = user.email; _email.value = user.email;
@ -99,17 +144,11 @@ class UserController extends GetxController {
_name.value = user.name; _name.value = user.name;
_lastname.value = user.lastName; _lastname.value = user.lastName;
_password.value = user.password; _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(); saveUserData();
_preloadPermissionsInBackground();
} }
// CORRECTION : Sauvegarder TOUTES les données importantes
Future<void> saveUserData() async { Future<void> saveUserData() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -119,67 +158,93 @@ class UserController extends GetxController {
await prefs.setString('role', _role.value); await prefs.setString('role', _role.value);
await prefs.setString('name', _name.value); await prefs.setString('name', _name.value);
await prefs.setString('lastname', _lastname.value); await prefs.setString('lastname', _lastname.value);
await prefs.setInt('user_id', _userId.value); // Sauvegarder l'ID await prefs.setInt('user_id', _userId.value);
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
print("✅ Données sauvegardées avec succès dans SharedPreferences"); print("✅ Données sauvegardées avec succès");
} catch (e) { } 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 { Future<void> clearUserData() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
// Vider SharedPreferences // IMPORTANT: Vider le cache de session
_cacheService.clearAllCache();
// Effacer SharedPreferences
await prefs.remove('username'); await prefs.remove('username');
await prefs.remove('email'); await prefs.remove('email');
await prefs.remove('role'); await prefs.remove('role');
await prefs.remove('name'); await prefs.remove('name');
await prefs.remove('lastname'); await prefs.remove('lastname');
await prefs.remove('user_id'); // Supprimer l'ID aussi await prefs.remove('user_id');
await prefs.remove('point_de_vente_id');
await prefs.remove('point_de_vente_designation');
// Vider les variables observables // Effacer les observables
_username.value = ''; _username.value = '';
_email.value = ''; _email.value = '';
_role.value = ''; _role.value = '';
_name.value = ''; _name.value = '';
_lastname.value = ''; _lastname.value = '';
_password.value = ''; _password.value = '';
_userId.value = 0; // Réinitialiser l'ID _userId.value = 0;
_pointDeVenteId.value = 0;
_pointDeVenteDesignation.value = '';
print("✅ Données utilisateur et cache de session vidés");
print("✅ Toutes les données utilisateur ont été effacées");
} catch (e) { } catch (e) {
print('❌ Erreur lors de l\'effacement des données utilisateur: $e'); print('❌ Erreur lors de l\'effacement: $e');
} }
} }
// MÉTHODE UTILITAIRE : Vérifier si un utilisateur est connecté // Getters utilitaires
bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0; bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
// MÉTHODE UTILITAIRE : Obtenir le nom complet
String get fullName => '${_name.value} ${_lastname.value}'.trim(); 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 { Future<bool> hasPermission(String permission, String route) async {
try { try {
if (_username.value.isEmpty) { if (_username.value.isEmpty) {
print('⚠️ Username vide, rechargement des données...'); print('⚠️ Username vide, rechargement...');
await loadUserData(); await loadUserData();
} }
if (_username.value.isEmpty) { if (_username.value.isEmpty) {
print('Impossible de vérifier les permissions : utilisateur non connecté'); print('Utilisateur non connecté');
return false; 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) { } catch (e) {
print('❌ Erreur vérification permission: $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 { Future<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async {
for (String permissionName in permissionNames) { for (String permissionName in permissionNames) {
if (await hasPermission(permissionName, menuRoute)) { if (await hasPermission(permissionName, menuRoute)) {
@ -189,16 +254,40 @@ class UserController extends GetxController {
return false; 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() { void debugPrintUserState() {
print("=== ÉTAT UTILISATEUR ==="); print("=== ÉTAT UTILISATEUR ===");
print("Username: ${_username.value}"); print("Username: ${_username.value}");
print("Name: ${_name.value}"); print("Name: ${_name.value}");
print("Lastname: ${_lastname.value}");
print("Email: ${_email.value}");
print("Role: ${_role.value}"); print("Role: ${_role.value}");
print("UserID: ${_userId.value}"); print("UserID: ${_userId.value}");
print("IsLoggedIn: $isLoggedIn"); print("IsLoggedIn: $isLoggedIn");
print("Cache Ready: $isCacheReady");
print("========================"); print("========================");
// Debug du cache
_cacheService.debugPrintCache();
} }
} }

108
lib/main.dart

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

58
lib/my_app.dart

@ -1,8 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'Views/ErreurPage.dart';
import 'Views/loginPage.dart'; import 'Views/loginPage.dart';
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@ -10,12 +6,11 @@ class MyApp extends StatelessWidget {
static bool isRegisterOpen = false; static bool isRegisterOpen = false;
static DateTime? startDate; static DateTime? startDate;
static late String path;
static const Gradient primaryGradient = LinearGradient( static const Gradient primaryGradient = LinearGradient(
colors: [ colors: [
Colors.white, Colors.white,
const Color.fromARGB(255, 4, 54, 95), Color.fromARGB(255, 4, 54, 95),
], ],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@ -24,56 +19,17 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'GUYCOM',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
canvasColor: Colors.transparent, canvasColor: Colors.transparent,
), ),
home: Builder( home: Container(
builder: (context) { decoration: const BoxDecoration(
return FutureBuilder<bool>( gradient: MyApp.primaryGradient,
future: ),
checkLocalDatabasesExist(), // Appel à la fonction de vérification child: const LoginPage(),
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(),
);
}
},
);
},
), ),
); );
} }
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;
}
} }

4
macos/Flutter/GeneratedPluginRegistrant.swift

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

88
pubspec.lock

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

15
pubspec.yaml

@ -35,7 +35,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
get: ^4.6.5 get: ^4.6.5
sqflite: ^2.2.8+4 # sqflite: ^2.2.8+4
mysql1: ^0.20.0
flutter_dropzone: ^4.2.1 flutter_dropzone: ^4.2.1
image_picker: ^0.8.7+5 image_picker: ^0.8.7+5
@ -50,7 +51,7 @@ dependencies:
logging: ^1.2.0 logging: ^1.2.0
msix: ^3.7.0 msix: ^3.7.0
flutter_charts: ^0.5.1 flutter_charts: ^0.5.1
syncfusion_flutter_charts: ^21.2.4 syncfusion_flutter_charts: ^20.4.48
shelf: ^1.4.1 shelf: ^1.4.1
shelf_router: ^1.1.4 shelf_router: ^1.1.4
pdf: ^3.8.4 pdf: ^3.8.4
@ -62,7 +63,10 @@ dependencies:
path_provider: ^2.0.15 path_provider: ^2.0.15
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
excel: ^2.0.1 excel: ^2.0.1
mobile_scanner: ^5.0.0 # ou la version la plus récente
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
numbers_to_letters: ^1.0.0
qr_code_scanner_plus: ^2.0.10+1
@ -101,6 +105,11 @@ flutter:
- assets/database/usersdb.db - assets/database/usersdb.db
- assets/database/work.db - assets/database/work.db
- assets/database/roles.db - assets/database/roles.db
- assets/airtel_money.png
- assets/mvola.jpg
- assets/Orange_money.png
- assets/fa-solid-900.ttf
- assets/fonts/Roboto-Italic.ttf
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware

3
test/widget_test.dart

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

Loading…
Cancel
Save