Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
595b38e9fb | ||
|
|
525b09c81f | ||
|
|
b5a11aa3c9 | ||
|
|
831cce13da | ||
|
|
c8fedd08e5 | ||
|
|
9eafda610f | ||
|
|
2bef06a2fe | ||
|
|
57ea91b3d7 |
@ -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
Normal file
BIN
assets/Orange_money.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/airtel_money.png
Normal file
BIN
assets/airtel_money.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/fa-solid-900.ttf
Normal file
BIN
assets/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Roboto-Italic.ttf
Normal file
BIN
assets/fonts/Roboto-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/mvola.jpg
Normal file
BIN
assets/mvola.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@ -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
Normal file
417
lib/Components/AddClient.dart
Normal file
@ -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
Normal file
471
lib/Components/AddClientForm.dart
Normal file
@ -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
Normal file
176
lib/Components/DiscountDialog.dart
Normal file
@ -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
Normal file
349
lib/Components/GiftaselectedButton.dart
Normal file
@ -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
Normal file
338
lib/Components/PaymentEnchainedDialog.dart
Normal file
@ -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
Normal file
259
lib/Components/QrScan.dart
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,7 +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/pointage.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>();
|
||||||
@ -24,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,28 +35,320 @@ 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) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
children: snapshot.data as List<Widget>,
|
children: [
|
||||||
|
// Header utilisateur
|
||||||
|
_buildUserHeader(controller),
|
||||||
|
|
||||||
|
// ✅ CORRIGÉ: Construction avec gestion des valeurs null
|
||||||
|
..._buildDrawerItemsFromSessionCache(),
|
||||||
|
|
||||||
|
// Déconnexion
|
||||||
|
const Divider(),
|
||||||
|
_buildLogoutItem(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"Chargement du menu...",
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les menus depuis le cache de session
|
||||||
|
final rawUserMenus = userController.getUserMenus();
|
||||||
|
|
||||||
|
// 🛡️ VALIDATION: Filtrer les menus valides
|
||||||
|
final validMenus = <Map<String, dynamic>>[];
|
||||||
|
final invalidMenus = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
for (var menu in rawUserMenus) {
|
||||||
|
// Vérifier que les champs essentiels ne sont pas null
|
||||||
|
final name = menu['name'];
|
||||||
|
final route = menu['route'];
|
||||||
|
final id = menu['id'];
|
||||||
|
|
||||||
|
if (name != null && route != null && route.toString().isNotEmpty) {
|
||||||
|
validMenus.add({
|
||||||
|
'id': id,
|
||||||
|
'name': name.toString(),
|
||||||
|
'route': route.toString(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
invalidMenus.add(menu);
|
||||||
|
print("⚠️ Menu invalide ignoré dans CustomDrawer: id=$id, name='$name', route='$route'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher les statistiques de validation
|
||||||
|
if (invalidMenus.isNotEmpty) {
|
||||||
|
print("📊 CustomDrawer: ${validMenus.length} menus valides, ${invalidMenus.length} invalides");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validMenus.isEmpty) {
|
||||||
|
return [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
"Aucun menu accessible",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 DÉDUPLICATION: Éliminer les doublons par route
|
||||||
|
final Map<String, Map<String, dynamic>> uniqueMenus = {};
|
||||||
|
for (var menu in validMenus) {
|
||||||
|
final route = menu['route'] as String;
|
||||||
|
uniqueMenus[route] = menu;
|
||||||
|
}
|
||||||
|
final deduplicatedMenus = uniqueMenus.values.toList();
|
||||||
|
|
||||||
|
if (deduplicatedMenus.length != validMenus.length) {
|
||||||
|
print("🔧 CustomDrawer: ${validMenus.length - deduplicatedMenus.length} doublons supprimés");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organiser les menus par catégories
|
||||||
|
final Map<String, List<Map<String, dynamic>>> categorizedMenus = {
|
||||||
|
'GESTION UTILISATEURS': [],
|
||||||
|
'GESTION PRODUITS': [],
|
||||||
|
'GESTION COMMANDES': [],
|
||||||
|
'RAPPORTS': [],
|
||||||
|
'ADMINISTRATION': [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accueil toujours en premier
|
||||||
|
final accueilMenu = deduplicatedMenus.where((menu) => menu['route'] == '/accueil').firstOrNull;
|
||||||
|
if (accueilMenu != null) {
|
||||||
|
drawerItems.add(_buildDrawerItemFromMenu(accueilMenu));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catégoriser les autres menus avec validation supplémentaire
|
||||||
|
for (var menu in deduplicatedMenus) {
|
||||||
|
final route = menu['route'] as String;
|
||||||
|
|
||||||
|
// ✅ Validation supplémentaire avant categorisation
|
||||||
|
if (route.isEmpty) {
|
||||||
|
print("⚠️ Route vide ignorée: ${menu['name']}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (route) {
|
||||||
|
case '/accueil':
|
||||||
|
// Déjà traité
|
||||||
|
break;
|
||||||
|
case '/ajouter-utilisateur':
|
||||||
|
case '/modifier-utilisateur':
|
||||||
|
case '/pointage':
|
||||||
|
categorizedMenus['GESTION UTILISATEURS']!.add(menu);
|
||||||
|
break;
|
||||||
|
case '/ajouter-produit':
|
||||||
|
case '/gestion-stock':
|
||||||
|
categorizedMenus['GESTION PRODUITS']!.add(menu);
|
||||||
|
break;
|
||||||
|
case '/nouvelle-commande':
|
||||||
|
case '/gerer-commandes':
|
||||||
|
categorizedMenus['GESTION COMMANDES']!.add(menu);
|
||||||
|
break;
|
||||||
|
case '/bilan':
|
||||||
|
case '/historique':
|
||||||
|
categorizedMenus['RAPPORTS']!.add(menu);
|
||||||
|
break;
|
||||||
|
case '/gerer-roles':
|
||||||
|
case '/points-de-vente':
|
||||||
|
categorizedMenus['ADMINISTRATION']!.add(menu);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Menu non catégorisé
|
||||||
|
print("⚠️ Menu non catégorisé: $route");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
final routeData = routeMapping[route];
|
||||||
|
if (routeData == null) {
|
||||||
|
print("⚠️ Route non reconnue: '$route' pour le menu '$name'");
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.help_outline, color: Colors.grey),
|
||||||
|
title: Text(name),
|
||||||
|
subtitle: Text("Route: $route", style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
|
onTap: () {
|
||||||
|
Get.snackbar(
|
||||||
|
"Route non configurée",
|
||||||
|
"La route '$route' n'est pas encore configurée",
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.orange.shade100,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
routeData['icon'] as IconData,
|
||||||
|
color: routeData['color'] as Color,
|
||||||
|
),
|
||||||
|
title: Text(name),
|
||||||
|
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
|
||||||
|
onTap: () {
|
||||||
|
final widget = routeData['widget'];
|
||||||
|
if (widget != null) {
|
||||||
|
Get.to(widget);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
"Non implémenté",
|
||||||
|
"Cette fonctionnalité sera bientôt disponible",
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Header utilisateur amélioré
|
||||||
|
Widget _buildUserHeader(UserController controller) {
|
||||||
|
return Container(
|
||||||
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
|
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
@ -70,12 +364,13 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
backgroundImage: AssetImage("assets/youmaz2.png"),
|
backgroundImage: AssetImage("assets/youmaz2.png"),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 15),
|
const SizedBox(width: 15),
|
||||||
Column(
|
Expanded(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
controller.name.isNotEmpty
|
controller.name.isNotEmpty
|
||||||
? controller.name
|
? controller.fullName
|
||||||
: 'Utilisateur',
|
: 'Utilisateur',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -90,262 +385,223 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
fontSize: 12,
|
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();
|
||||||
drawerItems.add(
|
Get.back(); // Fermer le drawer
|
||||||
await _buildDrawerItem(
|
Get.snackbar(
|
||||||
icon: Icons.home,
|
"Cache",
|
||||||
title: "Accueil",
|
"Permissions rechargées avec succès",
|
||||||
color: Colors.blue,
|
snackPosition: SnackPosition.TOP,
|
||||||
permissionAction: 'view',
|
backgroundColor: Colors.green,
|
||||||
permissionRoute: '/accueil',
|
colorText: Colors.white,
|
||||||
onTap: () => Get.to(const AccueilPage()),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
},
|
||||||
List<Widget> gestionUtilisateursItems = [
|
tooltip: "Recharger les permissions",
|
||||||
await _buildDrawerItem(
|
|
||||||
icon: Icons.person_add,
|
|
||||||
title: "Ajouter un utilisateur",
|
|
||||||
color: Colors.green,
|
|
||||||
permissionAction: 'create',
|
|
||||||
permissionRoute: '/ajouter-utilisateur',
|
|
||||||
onTap: () => Get.to(const RegistrationPage()),
|
|
||||||
),
|
),
|
||||||
await _buildDrawerItem(
|
],
|
||||||
icon: Icons.supervised_user_circle,
|
// 🔧 Bouton de debug (à supprimer en production)
|
||||||
title: "Gérer les utilisateurs",
|
if (controller.role == 'Super Admin') ...[
|
||||||
color: const Color.fromARGB(255, 4, 54, 95),
|
IconButton(
|
||||||
permissionAction: 'update',
|
icon: const Icon(Icons.bug_report, color: Colors.white70, size: 18),
|
||||||
permissionRoute: '/modifier-utilisateur',
|
onPressed: () {
|
||||||
onTap: () => Get.to(const ListUserPage()),
|
// Debug des menus
|
||||||
),
|
final menus = controller.getUserMenus();
|
||||||
await _buildDrawerItem(
|
String debugInfo = "MENUS DEBUG:\n";
|
||||||
icon: Icons.timer,
|
for (var i = 0; i < menus.length; i++) {
|
||||||
title: "Gestion des pointages",
|
final menu = menus[i];
|
||||||
color: const Color.fromARGB(255, 4, 54, 95),
|
debugInfo += "[$i] ID:${menu['id']}, Name:'${menu['name']}', Route:'${menu['route']}'\n";
|
||||||
permissionAction: 'update',
|
|
||||||
permissionRoute: '/pointage',
|
|
||||||
onTap: () => Get.to(const PointagePage()),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
if (gestionUtilisateursItems.any((item) => item is ListTile)) {
|
|
||||||
drawerItems.add(
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
|
||||||
child: Text(
|
|
||||||
"GESTION UTILISATEURS",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
drawerItems.addAll(gestionUtilisateursItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> gestionProduitsItems = [
|
Get.dialog(
|
||||||
await _buildDrawerItem(
|
AlertDialog(
|
||||||
icon: Icons.inventory,
|
title: const Text("Debug Menus"),
|
||||||
title: "Gestion des produits",
|
content: SingleChildScrollView(
|
||||||
color: Colors.indigoAccent,
|
child: Text(debugInfo, style: const TextStyle(fontSize: 12)),
|
||||||
permissionAction: 'create',
|
|
||||||
permissionRoute: '/ajouter-produit',
|
|
||||||
onTap: () => Get.to(const ProductManagementPage()),
|
|
||||||
),
|
|
||||||
await _buildDrawerItem(
|
|
||||||
icon: Icons.storage,
|
|
||||||
title: "Gestion de stock",
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
permissionAction: 'update',
|
|
||||||
permissionRoute: '/gestion-stock',
|
|
||||||
onTap: () => Get.to(const GestionStockPage()),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (gestionProduitsItems.any((item) => item is ListTile)) {
|
|
||||||
drawerItems.add(
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
|
||||||
child: Text(
|
|
||||||
"GESTION PRODUITS",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: const Text("Fermer"),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
drawerItems.addAll(gestionProduitsItems);
|
},
|
||||||
}
|
tooltip: "Debug menus",
|
||||||
|
|
||||||
List<Widget> gestionCommandesItems = [
|
|
||||||
await _buildDrawerItem(
|
|
||||||
icon: Icons.add_shopping_cart,
|
|
||||||
title: "Nouvelle commande",
|
|
||||||
color: Colors.orange,
|
|
||||||
permissionAction: 'create',
|
|
||||||
permissionRoute: '/nouvelle-commande',
|
|
||||||
onTap: () => Get.to(const NouvelleCommandePage()),
|
|
||||||
),
|
|
||||||
await _buildDrawerItem(
|
|
||||||
icon: Icons.list_alt,
|
|
||||||
title: "Gérer les commandes",
|
|
||||||
color: Colors.deepPurple,
|
|
||||||
permissionAction: 'manage',
|
|
||||||
permissionRoute: '/gerer-commandes',
|
|
||||||
onTap: () => Get.to(const GestionCommandesPage()),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (gestionCommandesItems.any((item) => item is ListTile)) {
|
|
||||||
drawerItems.add(
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
|
||||||
child: Text(
|
|
||||||
"GESTION COMMANDES",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
drawerItems.addAll(gestionCommandesItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> rapportsItems = [
|
/// Item de déconnexion
|
||||||
await _buildDrawerItem(
|
Widget _buildLogoutItem() {
|
||||||
icon: Icons.bar_chart,
|
return ListTile(
|
||||||
title: "Bilan mensuel",
|
|
||||||
color: Colors.teal,
|
|
||||||
permissionAction: 'read',
|
|
||||||
permissionRoute: '/bilan',
|
|
||||||
onTap: () => Get.to(const BilanMois()),
|
|
||||||
),
|
|
||||||
await _buildDrawerItem(
|
|
||||||
icon: Icons.history,
|
|
||||||
title: "Historique",
|
|
||||||
color: Colors.blue,
|
|
||||||
permissionAction: 'read',
|
|
||||||
permissionRoute: '/historique',
|
|
||||||
onTap: () => Get.to(HistoryPage()),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (rapportsItems.any((item) => item is ListTile)) {
|
|
||||||
drawerItems.add(
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
|
||||||
child: Text(
|
|
||||||
"RAPPORTS",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
drawerItems.addAll(rapportsItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> administrationItems = [
|
|
||||||
await _buildDrawerItem(
|
|
||||||
icon: Icons.admin_panel_settings,
|
|
||||||
title: "Gérer les rôles",
|
|
||||||
color: Colors.redAccent,
|
|
||||||
permissionAction: 'admin',
|
|
||||||
permissionRoute: '/gerer-roles',
|
|
||||||
onTap: () => Get.to(const RoleListPage()),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (administrationItems.any((item) => item is ListTile)) {
|
|
||||||
drawerItems.add(
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
|
||||||
child: Text(
|
|
||||||
"ADMINISTRATION",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
drawerItems.addAll(administrationItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawerItems.add(const Divider());
|
|
||||||
|
|
||||||
drawerItems.add(
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.logout, color: Colors.red),
|
leading: const Icon(Icons.logout, color: Colors.red),
|
||||||
title: const Text("Déconnexion"),
|
title: const Text("Déconnexion"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.defaultDialog(
|
Get.dialog(
|
||||||
title: "Déconnexion",
|
AlertDialog(
|
||||||
content: const Text("Voulez-vous vraiment vous déconnecter ?"),
|
shape: RoundedRectangleBorder(
|
||||||
actions: [
|
borderRadius: BorderRadius.circular(16),
|
||||||
TextButton(
|
),
|
||||||
child: const Text("Non"),
|
contentPadding: EdgeInsets.zero,
|
||||||
|
content: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.logout_rounded,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.orange.shade600,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
"Déconnexion",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
"Êtes-vous sûr de vouloir vous déconnecter ?",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black87,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"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(),
|
onPressed: () => Get.back(),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
side: BorderSide(
|
||||||
style: ElevatedButton.styleFrom(
|
color: Colors.grey.shade300,
|
||||||
backgroundColor: Colors.red,
|
width: 1.5,
|
||||||
),
|
),
|
||||||
child: const Text("Oui"),
|
),
|
||||||
|
child: const Text(
|
||||||
|
"Annuler",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
// ✅ IMPORTANT: Vider le cache de session lors de la déconnexion
|
||||||
await clearUserData();
|
await clearUserData();
|
||||||
Get.offAll(const LoginPage());
|
Get.offAll(const LoginPage());
|
||||||
},
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red.shade600,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 2,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"Se déconnecter",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
barrierDismissible: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return drawerItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Widget> _buildDrawerItem({
|
|
||||||
required IconData icon,
|
|
||||||
required String title,
|
|
||||||
required Color color,
|
|
||||||
String? permissionAction,
|
|
||||||
String? permissionRoute,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) async {
|
|
||||||
if (permissionAction != null && permissionRoute != null) {
|
|
||||||
bool hasPermission =
|
|
||||||
await userController.hasPermission(permissionAction, permissionRoute);
|
|
||||||
if (!hasPermission) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(icon, color: color),
|
|
||||||
title: Text(title),
|
|
||||||
trailing: permissionAction != null
|
|
||||||
? const Icon(Icons.chevron_right, color: Colors.grey)
|
|
||||||
: null,
|
|
||||||
onTap: onTap,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// autres propriétés si besoin
|
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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
175
lib/Components/commandManagementComponents/CommandDetails.dart
Normal file
175
lib/Components/commandManagementComponents/CommandDetails.dart
Normal file
@ -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
Normal file
226
lib/Components/commandManagementComponents/CommandeActions.dart
Normal file
@ -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
Normal file
189
lib/Components/commandManagementComponents/DiscountDialog.dart
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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});
|
||||||
|
}
|
||||||
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
enum PaymentType {
|
||||||
|
cash,
|
||||||
|
card,
|
||||||
|
mvola,
|
||||||
|
orange,
|
||||||
|
airtel
|
||||||
|
}
|
||||||
7
lib/Components/paymentType.dart
Normal file
7
lib/Components/paymentType.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
enum PaymentType {
|
||||||
|
cash,
|
||||||
|
card,
|
||||||
|
mvola,
|
||||||
|
orange,
|
||||||
|
airtel
|
||||||
|
}
|
||||||
2125
lib/Components/teat.dart
Normal file
2125
lib/Components/teat.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
Normal file
64
lib/Models/Remise.dart
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,22 @@
|
|||||||
|
// Models/product.dart - Version corrigée pour gérer les Blobs
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
class Product {
|
class Product {
|
||||||
int? id;
|
final int? id;
|
||||||
final String name;
|
final String name;
|
||||||
final double price;
|
final double price;
|
||||||
final String? image;
|
final String? image;
|
||||||
final String category;
|
final String category;
|
||||||
final int? stock;
|
final int stock;
|
||||||
final String? description;
|
final String? description;
|
||||||
String? qrCode;
|
String? qrCode;
|
||||||
final String? reference;
|
final String? reference;
|
||||||
|
final int? pointDeVenteId;
|
||||||
|
final String? marque;
|
||||||
|
final String? ram;
|
||||||
|
final String? memoireInterne;
|
||||||
|
final String? imei;
|
||||||
|
|
||||||
Product({
|
Product({
|
||||||
this.id,
|
this.id,
|
||||||
@ -16,44 +25,138 @@ class Product {
|
|||||||
this.image,
|
this.image,
|
||||||
required this.category,
|
required this.category,
|
||||||
this.stock = 0,
|
this.stock = 0,
|
||||||
this.description = '',
|
this.description,
|
||||||
this.qrCode,
|
this.qrCode,
|
||||||
this.reference,
|
this.reference,
|
||||||
|
this.pointDeVenteId,
|
||||||
|
this.marque,
|
||||||
|
this.ram,
|
||||||
|
this.memoireInterne,
|
||||||
|
this.imei,
|
||||||
});
|
});
|
||||||
// Vérifie si le stock est défini
|
|
||||||
bool isStockDefined() {
|
bool isStockDefined() {
|
||||||
if (stock != null) {
|
return stock > 0;
|
||||||
print("stock is defined : $stock $name");
|
}
|
||||||
return true;
|
|
||||||
} else {
|
// Méthode helper pour convertir de façon sécurisée
|
||||||
return false;
|
static String? _convertImageFromMap(dynamic imageValue) {
|
||||||
|
if (imageValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si c'est déjà une String, on la retourne
|
||||||
|
if (imageValue is String) {
|
||||||
|
return imageValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le driver mysql1 peut retourner un Blob même pour TEXT
|
||||||
|
// Essayer de le convertir en String
|
||||||
|
try {
|
||||||
|
if (imageValue is Uint8List) {
|
||||||
|
// Convertir les bytes en String UTF-8
|
||||||
|
return utf8.decode(imageValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageValue is List<int>) {
|
||||||
|
// Convertir les bytes en String UTF-8
|
||||||
|
return utf8.decode(imageValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dernier recours : toString()
|
||||||
|
return imageValue.toString();
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur conversion image: $e, type: ${imageValue.runtimeType}");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
factory Product.fromMap(Map<String, dynamic> map) => Product(
|
||||||
|
id: map['id'] as int?,
|
||||||
|
name: map['name'] as String,
|
||||||
|
price: (map['price'] as num).toDouble(), // Conversion sécurisée
|
||||||
|
image: _convertImageFromMap(map['image']), // Utilisation de la méthode helper
|
||||||
|
category: map['category'] as String,
|
||||||
|
stock: (map['stock'] as int?) ?? 0, // Valeur par défaut
|
||||||
|
description: map['description'] as String?,
|
||||||
|
qrCode: map['qrCode'] as String?,
|
||||||
|
reference: map['reference'] as String?,
|
||||||
|
pointDeVenteId: map['point_de_vente_id'] as int?,
|
||||||
|
marque: map['marque'] as String?,
|
||||||
|
ram: map['ram'] as String?,
|
||||||
|
memoireInterne: map['memoire_interne'] as String?,
|
||||||
|
imei: map['imei'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'price': price,
|
'price': price,
|
||||||
'image': image ?? '',
|
'image': image,
|
||||||
'category': category,
|
'category': category,
|
||||||
'stock': stock ?? 0,
|
'stock': stock,
|
||||||
'description': description ?? '',
|
'description': description,
|
||||||
'qrCode': qrCode ?? '',
|
'qrCode': qrCode,
|
||||||
'reference': reference ?? '',
|
'reference': reference,
|
||||||
|
'point_de_vente_id': pointDeVenteId,
|
||||||
|
'marque': marque,
|
||||||
|
'ram': ram,
|
||||||
|
'memoire_interne': memoireInterne,
|
||||||
|
'imei': imei,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Méthode pour obtenir l'image comme base64 si nécessaire
|
||||||
|
String? getImageAsBase64() {
|
||||||
|
if (image == null) return null;
|
||||||
|
|
||||||
|
// Si l'image est déjà en base64, la retourner
|
||||||
|
if (image!.startsWith('data:') || image!.length > 100) {
|
||||||
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Product.fromMap(Map<String, dynamic> map) {
|
// 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('\\'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
Normal file
0
lib/Services/GestionStockDatabase.dart
Normal file
258
lib/Services/PermissionCacheService.dart
Normal file
258
lib/Services/PermissionCacheService.dart
Normal file
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,728 +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'},
|
|
||||||
{'name': 'Gérer les pointages', 'route': '/pointage'},
|
|
||||||
];
|
|
||||||
|
|
||||||
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],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:sqflite/sqflite.dart';
|
|
||||||
import '../Models/pointage_model.dart';
|
|
||||||
|
|
||||||
class DatabaseHelper {
|
|
||||||
static final DatabaseHelper _instance = DatabaseHelper._internal();
|
|
||||||
|
|
||||||
factory DatabaseHelper() => _instance;
|
|
||||||
|
|
||||||
DatabaseHelper._internal();
|
|
||||||
|
|
||||||
Database? _db;
|
|
||||||
|
|
||||||
Future<Database> get database async {
|
|
||||||
if (_db != null) return _db!;
|
|
||||||
_db = await _initDatabase();
|
|
||||||
return _db!;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Database> _initDatabase() async {
|
|
||||||
String databasesPath = await getDatabasesPath();
|
|
||||||
String dbPath = join(databasesPath, 'pointage.db');
|
|
||||||
return await openDatabase(dbPath, version: 1, onCreate: _onCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _onCreate(Database db, int version) async {
|
|
||||||
await db.execute('''
|
|
||||||
CREATE TABLE pointages (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
userName TEXT NOT NULL,
|
|
||||||
date TEXT NOT NULL,
|
|
||||||
heureArrivee TEXT NOT NULL,
|
|
||||||
heureDepart TEXT NOT NULL
|
|
||||||
)
|
|
||||||
''');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> insertPointage(Pointage pointage) async {
|
|
||||||
final db = await database;
|
|
||||||
return await db.insert('pointages', pointage.toMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Pointage>> getPointages() async {
|
|
||||||
final db = await database;
|
|
||||||
final pointages = await db.query('pointages');
|
|
||||||
return pointages.map((pointage) => Pointage.fromMap(pointage)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> updatePointage(Pointage pointage) async {
|
|
||||||
final db = await database;
|
|
||||||
return await db.update('pointages', pointage.toMap(),
|
|
||||||
where: 'id = ?', whereArgs: [pointage.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> deletePointage(int id) async {
|
|
||||||
final db = await database;
|
|
||||||
return await db.delete('pointages', where: 'id = ?', whereArgs: [id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
Normal file
1960
lib/Services/stock_managementDatabase.dart
Normal file
File diff suppressed because it is too large
Load Diff
1205
lib/Views/Dashboard.dart
Normal file
1205
lib/Views/Dashboard.dart
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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(
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Models/Permission.dart';
|
import 'package:youmazgestion/Models/Permission.dart';
|
||||||
import 'package:youmazgestion/Services/app_database.dart';
|
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||||
|
|
||||||
class RolePermissionsPage extends StatefulWidget {
|
class RolePermissionsPage extends StatefulWidget {
|
||||||
final Role role;
|
final Role role;
|
||||||
@ -18,6 +19,8 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
|||||||
List<Permission> permissions = [];
|
List<Permission> permissions = [];
|
||||||
List<Map<String, dynamic>> menus = [];
|
List<Map<String, dynamic>> menus = [];
|
||||||
Map<int, Map<String, bool>> menuPermissionsMap = {};
|
Map<int, Map<String, bool>> menuPermissionsMap = {};
|
||||||
|
bool isLoading = true;
|
||||||
|
String? errorMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -26,8 +29,14 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initData() async {
|
Future<void> _initData() async {
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
final perms = await db.getAllPermissions();
|
final perms = await db.getAllPermissions();
|
||||||
final menuList = await db.database.then((db) => db.query('menu'));
|
final menuList = await db.getAllMenus(); // Utilise la nouvelle méthode
|
||||||
|
|
||||||
Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
|
Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
|
||||||
|
|
||||||
@ -46,11 +55,20 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
|||||||
permissions = perms;
|
permissions = perms;
|
||||||
menus = menuList;
|
menus = menuList;
|
||||||
menuPermissionsMap = tempMenuPermissionsMap;
|
menuPermissionsMap = tempMenuPermissionsMap;
|
||||||
|
isLoading = false;
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
errorMessage = 'Erreur lors du chargement des données: $e';
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
print("Erreur lors de l'initialisation des données: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onPermissionToggle(
|
Future<void> _onPermissionToggle(
|
||||||
int menuId, String permission, bool enabled) async {
|
int menuId, String permission, bool enabled) async {
|
||||||
|
try {
|
||||||
final perm = permissions.firstWhere((p) => p.name == permission);
|
final perm = permissions.firstWhere((p) => p.name == permission);
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@ -64,61 +82,226 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
menuPermissionsMap[menuId]![permission] = enabled;
|
menuPermissionsMap[menuId]![permission] = enabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _toggleAllPermissions(int menuId, bool enabled) {
|
||||||
Widget build(BuildContext context) {
|
for (var permission in permissions) {
|
||||||
return Scaffold(
|
_onPermissionToggle(menuId, permission.name, enabled);
|
||||||
appBar: CustomAppBar(
|
}
|
||||||
title: "Permissions - ${widget.role.designation}",
|
}
|
||||||
// showBackButton: true,
|
|
||||||
),
|
int _getSelectedPermissionsCount(int menuId) {
|
||||||
body: Padding(
|
return menuPermissionsMap[menuId]?.values.where((selected) => selected).length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getPermissionPercentage(int menuId) {
|
||||||
|
if (permissions.isEmpty) return 0.0;
|
||||||
|
return _getSelectedPermissionsCount(menuId) / permissions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPermissionSummary() {
|
||||||
|
int totalPermissions = menus.length * permissions.length;
|
||||||
|
int selectedPermissions = 0;
|
||||||
|
|
||||||
|
for (var menuId in menuPermissionsMap.keys) {
|
||||||
|
selectedPermissions += _getSelectedPermissionsCount(menuId);
|
||||||
|
}
|
||||||
|
|
||||||
|
double percentage = totalPermissions > 0 ? selectedPermissions / totalPermissions : 0.0;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.analytics, color: Colors.blue.shade600),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Gestion des permissions pour le rôle: ${widget.role.designation}',
|
'Résumé des permissions',
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
],
|
||||||
const Text(
|
|
||||||
'Sélectionnez les permissions pour chaque menu:',
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 12),
|
||||||
if (permissions.isNotEmpty && menus.isNotEmpty)
|
LinearProgressIndicator(
|
||||||
Expanded(
|
value: percentage,
|
||||||
child: ListView.builder(
|
backgroundColor: Colors.grey.shade300,
|
||||||
itemCount: menus.length,
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
itemBuilder: (context, index) {
|
percentage > 0.7 ? Colors.green :
|
||||||
final menu = menus[index];
|
percentage > 0.3 ? Colors.orange : Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$selectedPermissions / $totalPermissions permissions',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${(percentage * 100).toStringAsFixed(1)}%',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenuCard(Map<String, dynamic> menu) {
|
||||||
final menuId = menu['id'] as int;
|
final menuId = menu['id'] as int;
|
||||||
final menuName = menu['name'] as String;
|
final menuName = menu['name'] as String;
|
||||||
|
final menuRoute = menu['route'] as String;
|
||||||
|
final selectedCount = _getSelectedPermissionsCount(menuId);
|
||||||
|
final percentage = _getPermissionPercentage(menuId);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 15),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
child: Padding(
|
shape: RoundedRectangleBorder(
|
||||||
padding: const EdgeInsets.all(12.0),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Column(
|
),
|
||||||
|
child: ExpansionTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: percentage == 1.0 ? Colors.green :
|
||||||
|
percentage > 0 ? Colors.orange : Colors.red.shade100,
|
||||||
|
child: Icon(
|
||||||
|
Icons.menu,
|
||||||
|
color: percentage == 1.0 ? Colors.white :
|
||||||
|
percentage > 0 ? Colors.white : Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
menuName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
menuName,
|
menuRoute,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold, fontSize: 16),
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: percentage,
|
||||||
|
backgroundColor: Colors.grey.shade300,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
percentage == 1.0 ? Colors.green :
|
||||||
|
percentage > 0 ? Colors.orange : Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'$selectedCount/${permissions.length}',
|
||||||
|
style: const TextStyle(
|
||||||
|
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(
|
Wrap(
|
||||||
spacing: 10,
|
spacing: 8,
|
||||||
runSpacing: 10,
|
runSpacing: 8,
|
||||||
children: permissions.map((perm) {
|
children: permissions.map((perm) {
|
||||||
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
|
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
|
||||||
return FilterChip(
|
return CustomFilterChip(
|
||||||
label: perm.name,
|
label: perm.name,
|
||||||
selected: isChecked,
|
selected: isChecked,
|
||||||
onSelected: (bool value) {
|
onSelected: (bool value) {
|
||||||
@ -130,48 +313,275 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: CustomAppBar(
|
||||||
|
title: "Permissions - ${widget.role.designation}",
|
||||||
|
),
|
||||||
|
body: isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: errorMessage != null
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.red.shade400,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Erreur de chargement',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.red.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Text(
|
||||||
|
errorMessage!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _initData,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Réessayer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
: Padding(
|
||||||
const Expanded(
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Center(
|
child: Column(
|
||||||
child: CircularProgressIndicator(),
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Models/Permission.dart';
|
import 'package:youmazgestion/Models/Permission.dart';
|
||||||
import 'package:youmazgestion/Services/app_database.dart';
|
//import 'package:youmazgestion/Services/app_database.dart';
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||||
|
|
||||||
class HandleUserRole extends StatefulWidget {
|
class HandleUserRole extends StatefulWidget {
|
||||||
const HandleUserRole({super.key});
|
const HandleUserRole({super.key});
|
||||||
@ -28,9 +29,12 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initData() async {
|
Future<void> _initData() async {
|
||||||
|
try {
|
||||||
final roleList = await db.getRoles();
|
final roleList = await db.getRoles();
|
||||||
final perms = await db.getAllPermissions();
|
final perms = await db.getAllPermissions();
|
||||||
final menuList = await db.database.then((db) => db.query('menu'));
|
|
||||||
|
// Récupération mise à jour des menus avec gestion d'erreur
|
||||||
|
final menuList = await db.getAllMenus();
|
||||||
|
|
||||||
Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {};
|
Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {};
|
||||||
|
|
||||||
@ -55,18 +59,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
|||||||
menus = menuList;
|
menus = menuList;
|
||||||
roleMenuPermissionsMap = tempRoleMenuPermissionsMap;
|
roleMenuPermissionsMap = tempRoleMenuPermissionsMap;
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de l'initialisation des données: $e");
|
||||||
|
// Afficher un message d'erreur à l'utilisateur
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur lors du chargement des données: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addRole() async {
|
Future<void> _addRole() async {
|
||||||
String designation = _roleController.text.trim();
|
String designation = _roleController.text.trim();
|
||||||
if (designation.isEmpty) return;
|
if (designation.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Veuillez saisir une désignation pour le rôle'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Vérifier si le rôle existe déjà
|
||||||
|
final existingRoles = roles.where((r) => r.designation.toLowerCase() == designation.toLowerCase());
|
||||||
|
if (existingRoles.isNotEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Ce rôle existe déjà'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await db.createRole(Role(designation: designation));
|
await db.createRole(Role(designation: designation));
|
||||||
_roleController.clear();
|
_roleController.clear();
|
||||||
await _initData();
|
await _initData();
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Rôle "$designation" créé avec succès'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de la création du rôle: $e");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur lors de la création du rôle: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onPermissionToggle(int roleId, int menuId, String permission, bool enabled) async {
|
Future<void> _onPermissionToggle(int roleId, int menuId, String permission, bool enabled) async {
|
||||||
|
try {
|
||||||
final perm = permissions.firstWhere((p) => p.name == permission);
|
final perm = permissions.firstWhere((p) => p.name == permission);
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@ -78,12 +130,76 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled;
|
roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled;
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de la modification de la permission: $e");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur lors de la modification de la permission: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteRole(Role role) async {
|
||||||
|
// Empêcher la suppression du Super Admin
|
||||||
|
if (role.designation == 'Super Admin') {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Impossible de supprimer le rôle Super Admin'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demander confirmation
|
||||||
|
final confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Confirmer la suppression'),
|
||||||
|
content: Text('Êtes-vous sûr de vouloir supprimer le rôle "${role.designation}" ?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: const Text('Supprimer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirm == true) {
|
||||||
|
try {
|
||||||
|
await db.deleteRole(role.id);
|
||||||
|
await _initData();
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Rôle "${role.designation}" supprimé avec succès'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de la suppression du rôle: $e");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur lors de la suppression du rôle: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const CustomAppBar(title: "Gestion des rôles"),
|
appBar: CustomAppBar(title: "Gestion des rôles"),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -103,28 +219,52 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
|||||||
controller: _roleController,
|
controller: _roleController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Nouveau rôle',
|
labelText: 'Nouveau rôle',
|
||||||
|
hintText: 'Ex: Manager, Vendeur...',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onSubmitted: (_) => _addRole(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
ElevatedButton(
|
ElevatedButton.icon(
|
||||||
onPressed: _addRole,
|
onPressed: _addRole,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Ajouter'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text('Ajouter'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Affichage des statistiques
|
||||||
|
if (roles.isNotEmpty)
|
||||||
|
Card(
|
||||||
|
elevation: 4,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_buildStatItem('Rôles', roles.length.toString(), Icons.people),
|
||||||
|
_buildStatItem('Permissions', permissions.length.toString(), Icons.security),
|
||||||
|
_buildStatItem('Menus', menus.length.toString(), Icons.menu),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Tableau des rôles et permissions
|
// Tableau des rôles et permissions
|
||||||
if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty)
|
if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty)
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -136,22 +276,64 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.vertical,
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
minWidth: MediaQuery.of(context).size.width - 32,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: menus.map((menu) {
|
children: menus.map((menu) {
|
||||||
final menuId = menu['id'] as int;
|
final menuId = menu['id'] as int;
|
||||||
return Column(
|
final menuName = menu['name'] as String;
|
||||||
|
final menuRoute = menu['route'] as String;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
elevation: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.menu, color: Colors.blue.shade700),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
menu['name'],
|
menuName,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
),
|
),
|
||||||
DataTable(
|
),
|
||||||
|
Text(
|
||||||
|
menuRoute,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: DataTable(
|
||||||
columnSpacing: 20,
|
columnSpacing: 20,
|
||||||
|
headingRowHeight: 50,
|
||||||
|
dataRowHeight: 60,
|
||||||
columns: [
|
columns: [
|
||||||
const DataColumn(
|
const DataColumn(
|
||||||
label: Text(
|
label: Text(
|
||||||
@ -160,17 +342,49 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
...permissions.map((perm) => DataColumn(
|
...permissions.map((perm) => DataColumn(
|
||||||
label: Text(
|
label: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
perm.name,
|
perm.name,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)).toList(),
|
)).toList(),
|
||||||
|
const DataColumn(
|
||||||
|
label: Text(
|
||||||
|
'Actions',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
rows: roles.map((role) {
|
rows: roles.map((role) {
|
||||||
final roleId = role.id!;
|
final roleId = role.id!;
|
||||||
return DataRow(
|
return DataRow(
|
||||||
cells: [
|
cells: [
|
||||||
DataCell(Text(role.designation)),
|
DataCell(
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: role.designation == 'Super Admin'
|
||||||
|
? Colors.red.shade50
|
||||||
|
: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
role.designation,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: role.designation == 'Super Admin'
|
||||||
|
? Colors.red.shade700
|
||||||
|
: Colors.blue.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
...permissions.map((perm) {
|
...permissions.map((perm) {
|
||||||
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
|
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
|
||||||
return DataCell(
|
return DataCell(
|
||||||
@ -179,26 +393,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
|||||||
onChanged: (bool? value) {
|
onChanged: (bool? value) {
|
||||||
_onPermissionToggle(roleId, menuId, perm.name, value ?? false);
|
_onPermissionToggle(roleId, menuId, perm.name, value ?? false);
|
||||||
},
|
},
|
||||||
|
activeColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
],
|
DataCell(
|
||||||
);
|
role.designation != 'Super Admin'
|
||||||
}).toList(),
|
? IconButton(
|
||||||
|
icon: Icon(Icons.delete, color: Colors.red.shade600),
|
||||||
|
tooltip: 'Supprimer le rôle',
|
||||||
|
onPressed: () => _deleteRole(role),
|
||||||
|
)
|
||||||
|
: Icon(Icons.lock, color: Colors.grey.shade400),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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
Normal file
416
lib/Views/gestion_point_de_vente.dart
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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(
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,7 +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');
|
||||||
} 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(() {
|
||||||
@ -51,18 +55,47 @@ 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 {
|
||||||
userController.setUserWithCredentials(user, role, userId);
|
// try {
|
||||||
print(
|
// userController.setUserWithCredentials(user, role, userId);
|
||||||
'Utilisateur sauvegardé: ${user.username}, rôle: $role, id: $userId');
|
|
||||||
} catch (error) {
|
|
||||||
print('Erreur lors de la sauvegarde: $error');
|
|
||||||
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _login() async {
|
// if (user.pointDeVenteId != null) {
|
||||||
|
// await userController.loadPointDeVenteDesignation();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// print('✅ Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}');
|
||||||
|
// } catch (error) {
|
||||||
|
// print('❌ Erreur lors de la sauvegarde: $error');
|
||||||
|
// throw Exception('Erreur lors de la sauvegarde des données utilisateur');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// ✅ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ OPTIMISÉ: Connexion avec préchargement parallèle
|
||||||
|
void _login() async {
|
||||||
if (_isLoading) return;
|
if (_isLoading) return;
|
||||||
|
|
||||||
final String username = _usernameController.text.trim();
|
final String username = _usernameController.text.trim();
|
||||||
@ -70,8 +103,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
if (username.isEmpty || password.isEmpty) {
|
if (username.isEmpty || password.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage =
|
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
|
||||||
'Veuillez saisir le nom d\'utilisateur et le mot de passe';
|
|
||||||
_isErrorVisible = true;
|
_isErrorVisible = true;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -80,31 +112,87 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_isErrorVisible = false;
|
_isErrorVisible = false;
|
||||||
|
_loadingMessage = 'Connexion...';
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
print('🔐 Tentative de connexion pour: $username');
|
||||||
final dbInstance = AppDatabase.instance;
|
final dbInstance = AppDatabase.instance;
|
||||||
|
|
||||||
// Vérifier les identifiants
|
// 1. Vérification rapide de la base
|
||||||
|
setState(() {
|
||||||
|
_loadingMessage = 'Vérification...';
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
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...';
|
||||||
|
});
|
||||||
|
|
||||||
bool isValidUser = await dbInstance.verifyUser(username, password);
|
bool isValidUser = await dbInstance.verifyUser(username, password);
|
||||||
|
|
||||||
if (isValidUser) {
|
if (isValidUser) {
|
||||||
Users user = await dbInstance.getUser(username);
|
setState(() {
|
||||||
Map<String, dynamic>? userCredentials =
|
_loadingMessage = 'Chargement du profil...';
|
||||||
await dbInstance.getUserCredentials(username, password);
|
});
|
||||||
|
|
||||||
|
// 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) {
|
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(
|
await saveUserData(
|
||||||
user,
|
user,
|
||||||
userCredentials['role'] as String,
|
userCredentials['role'] as String,
|
||||||
userCredentials['id'] as int,
|
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);
|
||||||
|
|
||||||
|
// 6. Navigation immédiate
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
if (userCredentials['role'] == 'commercial') {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const AccueilPage()),
|
MaterialPageRoute(builder: (context) => const MainLayout()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => DashboardPage()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les permissions se chargeront en arrière-plan après la navigation
|
||||||
|
print('🚀 Navigation immédiate, permissions en arrière-plan');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Erreur lors de la récupération des credentials');
|
throw Exception('Erreur lors de la récupération des credentials');
|
||||||
}
|
}
|
||||||
@ -120,9 +208,32 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
_isErrorVisible = true;
|
_isErrorVisible = true;
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _isLoading = false);
|
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) {
|
||||||
@ -142,8 +253,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
width: MediaQuery.of(context).size.width < 500
|
width: MediaQuery.of(context).size.width < 500
|
||||||
? double.infinity
|
? double.infinity
|
||||||
: 400,
|
: 400,
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||||
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: cardColor.withOpacity(0.98),
|
color: cardColor.withOpacity(0.98),
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
@ -159,6 +269,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
// Header
|
||||||
Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -192,6 +303,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Username Field
|
||||||
TextField(
|
TextField(
|
||||||
controller: _usernameController,
|
controller: _usernameController,
|
||||||
enabled: !_isLoading,
|
enabled: !_isLoading,
|
||||||
@ -214,6 +327,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18.0),
|
const SizedBox(height: 18.0),
|
||||||
|
|
||||||
|
// Password Field
|
||||||
TextField(
|
TextField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
enabled: !_isLoading,
|
enabled: !_isLoading,
|
||||||
@ -236,19 +351,104 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
onSubmitted: (_) => _login(),
|
onSubmitted: (_) => _login(),
|
||||||
),
|
),
|
||||||
if (_isErrorVisible) ...[
|
|
||||||
const SizedBox(height: 12.0),
|
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: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(accentColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_loadingMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
color: accentColor,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
_errorMessage,
|
"Le menu se chargera en arrière-plan",
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.redAccent,
|
color: Colors.grey.shade600,
|
||||||
fontSize: 15,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
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 SizedBox(height: 26.0),
|
const SizedBox(height: 26.0),
|
||||||
|
|
||||||
|
// Login Button
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isLoading ? null : _login,
|
onPressed: _isLoading ? null : _login,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@ -262,13 +462,27 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
minimumSize: const Size(double.infinity, 52),
|
minimumSize: const Size(double.infinity, 52),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const SizedBox(
|
? Row(
|
||||||
height: 24,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
width: 24,
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
strokeWidth: 2.5,
|
strokeWidth: 2,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'Connexion...',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
: const Text(
|
: const Text(
|
||||||
'Se connecter',
|
'Se connecter',
|
||||||
@ -280,16 +494,23 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Option debug, à enlever en prod
|
|
||||||
if (_isErrorVisible) ...[
|
// Debug Button (à enlever en production)
|
||||||
|
if (_isErrorVisible && !_isLoading) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
final count =
|
final count = await AppDatabase.instance.getUserCount();
|
||||||
await AppDatabase.instance.getUserCount();
|
final stats = _cacheService.getCacheStats();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('$count utilisateurs trouvés')),
|
content: Text(
|
||||||
|
'BDD: $count utilisateurs\n'
|
||||||
|
'Cache: ${stats['users_cached']} utilisateurs en cache',
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -297,7 +518,13 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Debug: Vérifier BDD'),
|
child: Text(
|
||||||
|
'Debug: Vérifier BDD & Cache',
|
||||||
|
style: TextStyle(
|
||||||
|
color: primaryColor.withOpacity(0.6),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
2213
lib/Views/mobilepage.dart
Normal file
2213
lib/Views/mobilepage.dart
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,190 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:youmazgestion/Services/pointageDatabase.dart';
|
|
||||||
import 'package:youmazgestion/Models/pointage_model.dart';
|
|
||||||
|
|
||||||
class PointagePage extends StatefulWidget {
|
|
||||||
const PointagePage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PointagePage> createState() => _PointagePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PointagePageState extends State<PointagePage> {
|
|
||||||
final DatabaseHelper _databaseHelper = DatabaseHelper();
|
|
||||||
List<Pointage> _pointages = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadPointages();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadPointages() async {
|
|
||||||
final pointages = await _databaseHelper.getPointages();
|
|
||||||
setState(() {
|
|
||||||
_pointages = pointages;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showAddDialog() async {
|
|
||||||
final _arrivalController = TextEditingController();
|
|
||||||
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text('Ajouter Pointage'),
|
|
||||||
content: TextField(
|
|
||||||
controller: _arrivalController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Heure d\'arrivée (HH:mm)',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
child: Text('Annuler'),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
child: Text('Ajouter'),
|
|
||||||
onPressed: () async {
|
|
||||||
final pointage = Pointage(
|
|
||||||
userName:
|
|
||||||
"Nom de l'utilisateur", // fixed value, customize if needed
|
|
||||||
date: DateTime.now().toString().split(' ')[0],
|
|
||||||
heureArrivee: _arrivalController.text,
|
|
||||||
heureDepart: '',
|
|
||||||
);
|
|
||||||
await _databaseHelper.insertPointage(pointage);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
_loadPointages();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scanQRCode({required bool isEntree}) {
|
|
||||||
// Ici tu peux intégrer ton scanner QR.
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(isEntree ? "Scan QR pour Entrée" : "Scan QR pour Sortie"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Pointage'),
|
|
||||||
),
|
|
||||||
body: _pointages.isEmpty
|
|
||||||
? Center(child: Text('Aucun pointage enregistré.'))
|
|
||||||
: ListView.builder(
|
|
||||||
itemCount: _pointages.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final pointage = _pointages[index];
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12.0, vertical: 6.0),
|
|
||||||
child: Card(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
side: BorderSide(color: Colors.blueGrey.shade100),
|
|
||||||
),
|
|
||||||
elevation: 4,
|
|
||||||
shadowColor: Colors.blueGrey.shade50,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8.0, vertical: 4),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
backgroundColor: Colors.blue.shade100,
|
|
||||||
child: Icon(Icons.person, color: Colors.blue),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
pointage
|
|
||||||
.userName, // suppose non-null (corrige si null possible)
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
Text(
|
|
||||||
pointage.date,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black87, fontSize: 15),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.login,
|
|
||||||
size: 18, color: Colors.green.shade700),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text("Arrivée : ${pointage.heureArrivee}",
|
|
||||||
style:
|
|
||||||
TextStyle(color: Colors.green.shade700)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.logout,
|
|
||||||
size: 18, color: Colors.red.shade700),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
"Départ : ${pointage.heureDepart.isNotEmpty ? pointage.heureDepart : "---"}",
|
|
||||||
style: TextStyle(color: Colors.red.shade700)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
floatingActionButton: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
FloatingActionButton.extended(
|
|
||||||
onPressed: () => _scanQRCode(isEntree: true),
|
|
||||||
label: Text('Entrée'),
|
|
||||||
icon: Icon(Icons.qr_code_scanner, color: Colors.green),
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: Colors.green,
|
|
||||||
heroTag: 'btnEntree',
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
FloatingActionButton.extended(
|
|
||||||
onPressed: () => _scanQRCode(isEntree: false),
|
|
||||||
label: Text('Sortie'),
|
|
||||||
icon: Icon(Icons.qr_code_scanner, color: Colors.red),
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: Colors.red,
|
|
||||||
heroTag: 'btnSortie',
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
FloatingActionButton(
|
|
||||||
onPressed: _showAddDialog,
|
|
||||||
tooltip: 'Ajouter Pointage',
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
heroTag: 'btnAdd',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Models/users.dart';
|
import 'package:youmazgestion/Models/users.dart';
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||||
|
import 'package:youmazgestion/Views/Dashboard.dart';
|
||||||
import 'package:youmazgestion/accueil.dart';
|
import 'package:youmazgestion/accueil.dart';
|
||||||
|
|
||||||
import '../Services/app_database.dart'; // Changé de authDatabase.dart
|
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
|
||||||
|
|
||||||
class RegistrationPage extends StatefulWidget {
|
class RegistrationPage extends StatefulWidget {
|
||||||
const RegistrationPage({super.key});
|
const RegistrationPage({super.key});
|
||||||
@ -23,7 +25,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
Role? _selectedRole;
|
Role? _selectedRole;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isLoadingRoles = true;
|
bool _isLoadingRoles = true;
|
||||||
|
List<Map<String, dynamic>> _availablePointsDeVente = [];
|
||||||
|
int? _selectedPointDeVenteId;
|
||||||
|
bool _isLoadingPointsDeVente = true;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -40,6 +44,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
try {
|
try {
|
||||||
await AppDatabase.instance.initDatabase();
|
await AppDatabase.instance.initDatabase();
|
||||||
await _loadRoles();
|
await _loadRoles();
|
||||||
|
await _loadPointsDeVente(); // Ajouté ici
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
print('Erreur lors de l\'initialisation: $error');
|
print('Erreur lors de l\'initialisation: $error');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -47,8 +52,26 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
'Impossible d\'initialiser l\'application. Veuillez redémarrer.');
|
'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();
|
||||||
@ -103,7 +126,8 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
_emailController.text.trim().isEmpty ||
|
_emailController.text.trim().isEmpty ||
|
||||||
_usernameController.text.trim().isEmpty ||
|
_usernameController.text.trim().isEmpty ||
|
||||||
_passwordController.text.trim().isEmpty ||
|
_passwordController.text.trim().isEmpty ||
|
||||||
_selectedRole == null) {
|
_selectedRole == null ||
|
||||||
|
_selectedPointDeVenteId == null) { // Ajouté ici
|
||||||
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
|
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -142,6 +166,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
username: _usernameController.text.trim(),
|
username: _usernameController.text.trim(),
|
||||||
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
|
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
|
||||||
roleName: _selectedRole!.designation, // Pour l'affichage
|
roleName: _selectedRole!.designation, // Pour l'affichage
|
||||||
|
pointDeVenteId: _selectedPointDeVenteId, // Ajouté ici
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sauvegarder l'utilisateur dans la base de données
|
// Sauvegarder l'utilisateur dans la base de données
|
||||||
@ -191,7 +216,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const AccueilPage()),
|
MaterialPageRoute(builder: (context) => DashboardPage()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('OK'),
|
child: const Text('OK'),
|
||||||
@ -361,6 +386,46 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Dans la méthode build, après le DropdownButton des rôles
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
_isLoadingPointsDeVente
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.grey),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<int>(
|
||||||
|
value: _selectedPointDeVenteId,
|
||||||
|
hint: const Text('Sélectionner un point de vente'),
|
||||||
|
isExpanded: true,
|
||||||
|
onChanged: _isLoading
|
||||||
|
? null
|
||||||
|
: (int? newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedPointDeVenteId = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
items: _availablePointsDeVente
|
||||||
|
.map<DropdownMenuItem<int>>((Map<String, dynamic> point) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: point['id'] as int,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.store, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(point['nom']),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
const SizedBox(height: 24.0),
|
const SizedBox(height: 24.0),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
64
lib/config/DatabaseConfig.dart
Normal file
64
lib/config/DatabaseConfig.dart
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
110
lib/main.dart
110
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...");
|
||||||
// await AppDatabase.instance.deleteDatabaseFile();
|
|
||||||
// await ProductDatabase.instance.deleteDatabaseFile();
|
|
||||||
|
|
||||||
await ProductDatabase.instance.initDatabase();
|
// Pour le développement : supprimer toutes les tables (équivalent à deleteDatabaseFile)
|
||||||
await AppDatabase.instance.initDatabase();
|
// ATTENTION: Décommentez seulement si vous voulez réinitialiser la base
|
||||||
|
// await AppDatabase.instance.deleteDatabaseFile();
|
||||||
|
|
||||||
|
// Initialiser la base de données MySQL
|
||||||
|
print("Connexion à la base de données MySQL...");
|
||||||
|
// await AppDatabase.instance.initDatabase();
|
||||||
|
print("Base de données initialisée avec succès !");
|
||||||
|
|
||||||
// Afficher les informations de la base (pour debug)
|
// 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'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|||||||
@ -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) {
|
|
||||||
return FutureBuilder<bool>(
|
|
||||||
future:
|
|
||||||
checkLocalDatabasesExist(), // Appel à la fonction de vérification
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
// Affichez un indicateur de chargement si nécessaire
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
} else if (snapshot.hasError || !(snapshot.data ?? false)) {
|
|
||||||
// S'il y a une erreur ou si les bases de données n'existent pas
|
|
||||||
return ErreurPage(
|
|
||||||
dbPath:
|
|
||||||
path); // Redirigez vers la page d'erreur en affichant le chemin de la base de données
|
|
||||||
} else {
|
|
||||||
// Si les bases de données existent, affichez la page d'accueil normalement
|
|
||||||
return Container(
|
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: MyApp.primaryGradient,
|
gradient: MyApp.primaryGradient,
|
||||||
),
|
),
|
||||||
child: const LoginPage(),
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
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
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
|
||||||
|
|||||||
@ -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…
Reference in New Issue
Block a user