Compare commits
11 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
be8c169ad1 | 6 months ago |
|
|
48ae916f02 | 6 months ago |
|
|
c0bbb0da2b | 6 months ago |
|
|
595b38e9fb | 6 months ago |
|
|
525b09c81f | 6 months ago |
|
|
b5a11aa3c9 | 6 months ago |
|
|
831cce13da | 6 months ago |
|
|
c8fedd08e5 | 6 months ago |
|
|
9eafda610f | 6 months ago |
|
|
2bef06a2fe | 6 months ago |
|
|
57ea91b3d7 | 6 months ago |
72 changed files with 21563 additions and 4929 deletions
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@ -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 |
|||
} |
|||
@ -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'), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -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'), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
} |
|||
@ -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), |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -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,31 +1,121 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/controller/userController.dart'; |
|||
|
|||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { |
|||
final String title; |
|||
final Widget? subtitle; |
|||
|
|||
const CustomAppBar({ |
|||
final List<Widget>? actions; |
|||
final bool automaticallyImplyLeading; |
|||
final Color? backgroundColor; |
|||
final bool isDesktop; // Add this parameter |
|||
|
|||
final UserController userController = Get.put(UserController()); |
|||
|
|||
CustomAppBar({ |
|||
Key? key, |
|||
required this.title, |
|||
this.subtitle, |
|||
this.actions, |
|||
this.automaticallyImplyLeading = true, |
|||
this.backgroundColor, |
|||
this.isDesktop = false, // Add this parameter with default value |
|||
}) : super(key: key); |
|||
|
|||
|
|||
@override |
|||
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0); |
|||
|
|||
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return AppBar( |
|||
title: subtitle == null |
|||
? Text(title) |
|||
: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text(title, style: TextStyle(fontSize: 20)), |
|||
subtitle!, |
|||
return Container( |
|||
decoration: BoxDecoration( |
|||
gradient: LinearGradient( |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
colors: [ |
|||
Colors.blue.shade900, |
|||
Colors.blue.shade800, |
|||
], |
|||
), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.blue.shade900.withOpacity(0.3), |
|||
offset: const Offset(0, 2), |
|||
blurRadius: 4, |
|||
), |
|||
], |
|||
), |
|||
child: AppBar( |
|||
backgroundColor: backgroundColor ?? Colors.transparent, |
|||
elevation: 0, |
|||
automaticallyImplyLeading: automaticallyImplyLeading, |
|||
centerTitle: false, |
|||
iconTheme: const IconThemeData( |
|||
color: Colors.white, |
|||
size: 24, |
|||
), |
|||
actions: actions, |
|||
title: subtitle == null |
|||
? Text( |
|||
title, |
|||
style: const TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.white, |
|||
letterSpacing: 0.5, |
|||
), |
|||
) |
|||
: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Text( |
|||
title, |
|||
style: const TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.white, |
|||
letterSpacing: 0.5, |
|||
), |
|||
), |
|||
const SizedBox(height: 2), |
|||
Obx(() => Text( |
|||
userController.role != 'Super Admin' |
|||
? 'Point de vente: ${userController.pointDeVenteDesignation}' |
|||
: '', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
fontWeight: FontWeight.w400, |
|||
color: Colors.white.withOpacity(0.9), |
|||
letterSpacing: 0.3, |
|||
), |
|||
)), |
|||
if (subtitle != null) ...[ |
|||
const SizedBox(height: 2), |
|||
DefaultTextStyle( |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.white.withOpacity(0.8), |
|||
fontWeight: FontWeight.w400, |
|||
), |
|||
child: subtitle!, |
|||
), |
|||
], |
|||
], |
|||
), |
|||
flexibleSpace: Container( |
|||
decoration: BoxDecoration( |
|||
gradient: LinearGradient( |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
colors: [ |
|||
Colors.blue.shade900, |
|||
Colors.blue.shade800, |
|||
], |
|||
), |
|||
// autres propriétés si besoin |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,387 @@ |
|||
// Remplacez complètement votre fichier CommandeDetails par celui-ci : |
|||
|
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
|
|||
class CommandeDetails extends StatelessWidget { |
|||
final Commande commande; |
|||
|
|||
const CommandeDetails({required this.commande}); |
|||
|
|||
Widget _buildTableHeader(String text, {bool isAmount = false}) { |
|||
return Padding( |
|||
padding: const EdgeInsets.all(8.0), |
|||
child: Text( |
|||
text, |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 14, |
|||
), |
|||
textAlign: isAmount ? TextAlign.right : TextAlign.center, |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildTableCell(String text, {bool isAmount = false, Color? textColor}) { |
|||
return Padding( |
|||
padding: const EdgeInsets.all(8.0), |
|||
child: Text( |
|||
text, |
|||
style: TextStyle( |
|||
fontSize: 13, |
|||
color: textColor, |
|||
), |
|||
textAlign: isAmount ? TextAlign.right : TextAlign.center, |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildPriceColumn(DetailCommande detail) { |
|||
if (detail.aRemise) { |
|||
return Padding( |
|||
padding: const EdgeInsets.all(8.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.end, |
|||
children: [ |
|||
Text( |
|||
'${detail.prixUnitaire.toStringAsFixed(2)}', |
|||
style: const TextStyle( |
|||
fontSize: 11, |
|||
decoration: TextDecoration.lineThrough, |
|||
color: Colors.grey, |
|||
), |
|||
), |
|||
const SizedBox(height: 2), |
|||
Text( |
|||
'${(detail.prixFinal / detail.quantite).toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 13, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.orange.shade700, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} else { |
|||
return _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA', isAmount: true); |
|||
} |
|||
} |
|||
|
|||
Widget _buildRemiseColumn(DetailCommande detail) { |
|||
return Padding( |
|||
padding: const EdgeInsets.all(8.0), |
|||
child: detail.aRemise |
|||
? Column( |
|||
children: [ |
|||
Text( |
|||
detail.remiseDescription, |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.orange.shade700, |
|||
), |
|||
textAlign: TextAlign.center, |
|||
), |
|||
Text( |
|||
'-${detail.montantRemise.toStringAsFixed(0)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 10, |
|||
color: Colors.teal.shade700, |
|||
), |
|||
textAlign: TextAlign.center, |
|||
), |
|||
], |
|||
) |
|||
: const Text( |
|||
'-', |
|||
style: TextStyle(fontSize: 13, color: Colors.grey), |
|||
textAlign: TextAlign.center, |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildTotalColumn(DetailCommande detail) { |
|||
if (detail.aRemise && detail.sousTotal != detail.prixFinal) { |
|||
return Padding( |
|||
padding: const EdgeInsets.all(8.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.end, |
|||
children: [ |
|||
Text( |
|||
'${detail.sousTotal.toStringAsFixed(2)}', |
|||
style: const TextStyle( |
|||
fontSize: 11, |
|||
decoration: TextDecoration.lineThrough, |
|||
color: Colors.grey, |
|||
), |
|||
), |
|||
const SizedBox(height: 2), |
|||
Text( |
|||
'${detail.prixFinal.toStringAsFixed(2)} MGA', |
|||
style: const TextStyle( |
|||
fontSize: 13, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} else { |
|||
return _buildTableCell('${detail.prixFinal.toStringAsFixed(2)} MGA', isAmount: true); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return FutureBuilder<List<DetailCommande>>( |
|||
future: AppDatabase.instance.getDetailsCommande(commande.id!), |
|||
builder: (context, snapshot) { |
|||
if (snapshot.connectionState == ConnectionState.waiting) { |
|||
return const Center(child: CircularProgressIndicator()); |
|||
} |
|||
|
|||
if (!snapshot.hasData || snapshot.data!.isEmpty) { |
|||
return const Text('Aucun détail disponible'); |
|||
} |
|||
|
|||
final details = snapshot.data!; |
|||
|
|||
// Calculer les totaux |
|||
double sousTotal = 0; |
|||
double totalRemises = 0; |
|||
double totalFinal = 0; |
|||
bool hasRemises = false; |
|||
|
|||
for (final detail in details) { |
|||
sousTotal += detail.sousTotal; |
|||
totalRemises += detail.montantRemise; |
|||
totalFinal += detail.prixFinal; |
|||
if (detail.aRemise) hasRemises = true; |
|||
} |
|||
|
|||
return Column( |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
children: [ |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: hasRemises ? Colors.orange.shade50 : Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: hasRemises |
|||
? Border.all(color: Colors.orange.shade200) |
|||
: null, |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
Icon( |
|||
hasRemises ? Icons.discount : Icons.receipt_long, |
|||
color: hasRemises ? Colors.orange.shade700 : Colors.blue.shade700, |
|||
), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
hasRemises ? 'Détails de la commande (avec remises)' : 'Détails de la commande', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 16, |
|||
color: hasRemises ? Colors.orange.shade800 : Colors.black87, |
|||
), |
|||
), |
|||
if (hasRemises) ...[ |
|||
const Spacer(), |
|||
Container( |
|||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), |
|||
decoration: BoxDecoration( |
|||
color: Colors.orange.shade100, |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Text( |
|||
'Économies: ${totalRemises.toStringAsFixed(0)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.orange.shade700, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
Container( |
|||
decoration: BoxDecoration( |
|||
border: Border.all(color: Colors.grey.shade300), |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Table( |
|||
children: [ |
|||
TableRow( |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey.shade100, |
|||
), |
|||
children: [ |
|||
_buildTableHeader('Produit'), |
|||
_buildTableHeader('Qté'), |
|||
_buildTableHeader('Prix unit.', isAmount: true), |
|||
if (hasRemises) _buildTableHeader('Remise'), |
|||
_buildTableHeader('Total', isAmount: true), |
|||
], |
|||
), |
|||
...details.map((detail) => TableRow( |
|||
decoration: detail.aRemise |
|||
? BoxDecoration( |
|||
color: const Color.fromARGB(255, 243, 191, 114), |
|||
border: Border( |
|||
left: BorderSide( |
|||
color: Colors.orange.shade300, |
|||
width: 3, |
|||
), |
|||
), |
|||
) |
|||
: null, |
|||
children: [ |
|||
Padding( |
|||
padding: const EdgeInsets.all(8.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
detail.produitNom ?? 'Produit inconnu', |
|||
style: const TextStyle( |
|||
fontSize: 13, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
if (detail.aRemise) ...[ |
|||
const SizedBox(height: 2), |
|||
Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.local_offer, |
|||
size: 12, |
|||
color: Colors.teal.shade700, |
|||
), |
|||
const SizedBox(width: 4), |
|||
Text( |
|||
'Avec remise', |
|||
style: TextStyle( |
|||
fontSize: 10, |
|||
color: Colors.teal.shade700, |
|||
fontStyle: FontStyle.italic, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
], |
|||
), |
|||
), |
|||
_buildTableCell('${detail.quantite}'), |
|||
_buildPriceColumn(detail), |
|||
if (hasRemises) _buildRemiseColumn(detail), |
|||
_buildTotalColumn(detail), |
|||
], |
|||
)), |
|||
], |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
|
|||
// Section des totaux |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.green.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.green.shade200), |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
// Sous-total si il y a des remises |
|||
if (hasRemises) ...[ |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Sous-total:', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
Text( |
|||
'${sousTotal.toStringAsFixed(2)} MGA', |
|||
style: const TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(height: 8), |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.discount, |
|||
size: 16, |
|||
color: Colors.orange.shade700, |
|||
), |
|||
const SizedBox(width: 4), |
|||
Text( |
|||
'Remises totales:', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.w500, |
|||
color: Colors.orange.shade700, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
Text( |
|||
'-${totalRemises.toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.orange.shade700, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const Divider(height: 16), |
|||
], |
|||
|
|||
// Total final |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Total de la commande:', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 16, |
|||
), |
|||
), |
|||
Text( |
|||
'${commande.montantTotal.toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 18, |
|||
color: Colors.green.shade700, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,213 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
|
|||
|
|||
//Classe suplementaire |
|||
|
|||
class CommandeActions extends StatelessWidget { |
|||
final Commande commande; |
|||
final Function(int, StatutCommande) onStatutChanged; |
|||
final Function(Commande) onPaymentSelected; |
|||
|
|||
|
|||
const CommandeActions({ |
|||
required this.commande, |
|||
required this.onStatutChanged, |
|||
required this.onPaymentSelected, |
|||
|
|||
}); |
|||
|
|||
|
|||
|
|||
List<Widget> _buildActionButtons(BuildContext context) { |
|||
List<Widget> buttons = []; |
|||
|
|||
switch (commande.statut) { |
|||
case StatutCommande.enAttente: |
|||
buttons.addAll([ |
|||
|
|||
_buildActionButton( |
|||
label: 'Confirmer', |
|||
icon: Icons.check_circle, |
|||
color: Colors.blue, |
|||
onPressed: () => onPaymentSelected(commande), |
|||
), |
|||
_buildActionButton( |
|||
label: 'Annuler', |
|||
icon: Icons.cancel, |
|||
color: Colors.red, |
|||
onPressed: () => _showConfirmDialog( |
|||
context, |
|||
'Annuler la commande', |
|||
'Êtes-vous sûr de vouloir annuler cette commande?', |
|||
() => onStatutChanged(commande.id!, StatutCommande.annulee), |
|||
), |
|||
), |
|||
]); |
|||
break; |
|||
|
|||
case StatutCommande.confirmee: |
|||
buttons.add( |
|||
Container( |
|||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), |
|||
decoration: BoxDecoration( |
|||
color: Colors.green.shade100, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.green.shade300), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon(Icons.check_circle, |
|||
color: Colors.green.shade600, size: 16), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
'Commande confirmée', |
|||
style: TextStyle( |
|||
color: Colors.green.shade700, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
break; |
|||
|
|||
case StatutCommande.annulee: |
|||
buttons.add( |
|||
Container( |
|||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), |
|||
decoration: BoxDecoration( |
|||
color: Colors.red.shade100, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.red.shade300), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon(Icons.cancel, color: Colors.red.shade600, size: 16), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
'Commande annulée', |
|||
style: TextStyle( |
|||
color: Colors.red.shade700, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
break; |
|||
} |
|||
|
|||
return buttons; |
|||
} |
|||
|
|||
Widget _buildActionButton({ |
|||
required String label, |
|||
required IconData icon, |
|||
required Color color, |
|||
required VoidCallback onPressed, |
|||
}) { |
|||
return ElevatedButton.icon( |
|||
onPressed: onPressed, |
|||
icon: Icon(icon, size: 16), |
|||
label: Text(label), |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: color, |
|||
foregroundColor: Colors.white, |
|||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
elevation: 2, |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _showConfirmDialog( |
|||
BuildContext context, |
|||
String title, |
|||
String content, |
|||
VoidCallback onConfirm, |
|||
) { |
|||
showDialog( |
|||
context: context, |
|||
builder: (BuildContext context) { |
|||
return AlertDialog( |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
title: Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.help_outline, |
|||
color: Colors.blue.shade600, |
|||
), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
title, |
|||
style: const TextStyle(fontSize: 18), |
|||
), |
|||
], |
|||
), |
|||
content: Text(content), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Navigator.of(context).pop(), |
|||
child: Text( |
|||
'Annuler', |
|||
style: TextStyle(color: Colors.grey.shade600), |
|||
), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: () { |
|||
Navigator.of(context).pop(); |
|||
onConfirm(); |
|||
}, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.blue.shade600, |
|||
foregroundColor: Colors.white, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
), |
|||
child: const Text('Confirmer'), |
|||
), |
|||
], |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.grey.shade200), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
children: [ |
|||
const Text( |
|||
'Actions sur la commande', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 16, |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
Wrap( |
|||
spacing: 8, |
|||
runSpacing: 8, |
|||
children: _buildActionButtons(context), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -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,234 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
|
|||
class PasswordVerificationDialog extends StatefulWidget { |
|||
final String title; |
|||
final String message; |
|||
final Function(String) onPasswordVerified; |
|||
|
|||
const PasswordVerificationDialog({ |
|||
Key? key, |
|||
required this.title, |
|||
required this.message, |
|||
required this.onPasswordVerified, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
_PasswordVerificationDialogState createState() => _PasswordVerificationDialogState(); |
|||
} |
|||
|
|||
class _PasswordVerificationDialogState extends State<PasswordVerificationDialog> { |
|||
final TextEditingController _passwordController = TextEditingController(); |
|||
bool _isPasswordVisible = false; |
|||
bool _isLoading = false; |
|||
|
|||
@override |
|||
void dispose() { |
|||
_passwordController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return AlertDialog( |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(15), |
|||
), |
|||
title: Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.security, |
|||
color: Colors.blue.shade700, |
|||
size: 28, |
|||
), |
|||
const SizedBox(width: 10), |
|||
Expanded( |
|||
child: Text( |
|||
widget.title, |
|||
style: TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.blue.shade700, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
content: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
widget.message, |
|||
style: const TextStyle( |
|||
fontSize: 14, |
|||
color: Colors.black87, |
|||
), |
|||
), |
|||
const SizedBox(height: 20), |
|||
Container( |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey.shade50, |
|||
borderRadius: BorderRadius.circular(12), |
|||
border: Border.all(color: Colors.grey.shade300), |
|||
), |
|||
child: TextField( |
|||
controller: _passwordController, |
|||
obscureText: !_isPasswordVisible, |
|||
autofocus: true, |
|||
decoration: InputDecoration( |
|||
labelText: 'Mot de passe', |
|||
prefixIcon: Icon( |
|||
Icons.lock_outline, |
|||
color: Colors.blue.shade600, |
|||
), |
|||
suffixIcon: IconButton( |
|||
icon: Icon( |
|||
_isPasswordVisible ? Icons.visibility_off : Icons.visibility, |
|||
color: Colors.grey.shade600, |
|||
), |
|||
onPressed: () { |
|||
setState(() { |
|||
_isPasswordVisible = !_isPasswordVisible; |
|||
}); |
|||
}, |
|||
), |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
borderSide: BorderSide.none, |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.white, |
|||
contentPadding: const EdgeInsets.symmetric( |
|||
horizontal: 16, |
|||
vertical: 12, |
|||
), |
|||
), |
|||
onSubmitted: (value) => _verifyPassword(), |
|||
), |
|||
), |
|||
const SizedBox(height: 15), |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.amber.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.amber.shade200), |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.info_outline, |
|||
color: Colors.amber.shade700, |
|||
size: 20, |
|||
), |
|||
const SizedBox(width: 8), |
|||
Expanded( |
|||
child: Text( |
|||
'Saisissez votre mot de passe pour confirmer cette action', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.amber.shade700, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(), |
|||
child: Text( |
|||
'Annuler', |
|||
style: TextStyle( |
|||
color: Colors.grey.shade600, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: _isLoading ? null : _verifyPassword, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.blue.shade700, |
|||
foregroundColor: Colors.white, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), |
|||
), |
|||
child: _isLoading |
|||
? const SizedBox( |
|||
width: 20, |
|||
height: 20, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white), |
|||
), |
|||
) |
|||
: const Text('Vérifier'), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
void _verifyPassword() async { |
|||
final password = _passwordController.text.trim(); |
|||
|
|||
if (password.isEmpty) { |
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Veuillez saisir votre mot de passe', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 2), |
|||
); |
|||
return; |
|||
} |
|||
|
|||
setState(() { |
|||
_isLoading = true; |
|||
}); |
|||
|
|||
try { |
|||
final database = AppDatabase.instance; |
|||
final isValid = await database.verifyCurrentUserPassword(password); |
|||
|
|||
setState(() { |
|||
_isLoading = false; |
|||
}); |
|||
|
|||
if (isValid) { |
|||
Navigator.of(context).pop(); |
|||
widget.onPasswordVerified(password); |
|||
} else { |
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Mot de passe incorrect', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 3), |
|||
); |
|||
_passwordController.clear(); |
|||
} |
|||
} catch (e) { |
|||
setState(() { |
|||
_isLoading = false; |
|||
}); |
|||
|
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Une erreur est survenue lors de la vérification', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 3), |
|||
); |
|||
print("Erreur vérification mot de passe: $e"); |
|||
} |
|||
} |
|||
} |
|||
@ -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,265 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:get/get_core/src/get_main.dart'; |
|||
import 'package:get/get_navigation/src/snackbar/snackbar.dart'; |
|||
import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart'; |
|||
import 'package:youmazgestion/Components/paymentType.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
|
|||
class PaymentMethodDialog extends StatefulWidget { |
|||
final Commande commande; |
|||
|
|||
const PaymentMethodDialog({super.key, required this.commande}); |
|||
|
|||
@override |
|||
_PaymentMethodDialogState createState() => _PaymentMethodDialogState(); |
|||
} |
|||
|
|||
class _PaymentMethodDialogState extends State<PaymentMethodDialog> { |
|||
PaymentType _selectedPayment = PaymentType.cash; |
|||
final _amountController = TextEditingController(); |
|||
|
|||
void _validatePayment() { |
|||
final montantFinal = widget.commande.montantTotal; |
|||
|
|||
if (_selectedPayment == PaymentType.cash) { |
|||
final amountGiven = double.tryParse(_amountController.text) ?? 0; |
|||
if (amountGiven < montantFinal) { |
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Le montant donné est insuffisant', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
Navigator.pop(context, PaymentMethod( |
|||
type: _selectedPayment, |
|||
amountGiven: _selectedPayment == PaymentType.cash |
|||
? double.parse(_amountController.text) |
|||
: montantFinal, |
|||
)); |
|||
} |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
final montantFinal = widget.commande.montantTotal; |
|||
_amountController.text = montantFinal.toStringAsFixed(2); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_amountController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final amount = double.tryParse(_amountController.text) ?? 0; |
|||
final montantFinal = widget.commande.montantTotal; |
|||
final change = amount - montantFinal; |
|||
|
|||
return AlertDialog( |
|||
title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)), |
|||
content: SingleChildScrollView( |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
// Affichage du montant à payer (simplifié) |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.blue.shade200), |
|||
), |
|||
child: Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)), |
|||
Text('${montantFinal.toStringAsFixed(2)} MGA', |
|||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), |
|||
], |
|||
), |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
// Section Paiement mobile |
|||
const Align( |
|||
alignment: Alignment.centerLeft, |
|||
child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Row( |
|||
children: [ |
|||
Expanded( |
|||
child: _buildMobileMoneyTile( |
|||
title: 'Mvola', |
|||
imagePath: 'assets/mvola.jpg', |
|||
value: PaymentType.mvola, |
|||
), |
|||
), |
|||
const SizedBox(width: 8), |
|||
Expanded( |
|||
child: _buildMobileMoneyTile( |
|||
title: 'Orange Money', |
|||
imagePath: 'assets/Orange_money.png', |
|||
value: PaymentType.orange, |
|||
), |
|||
), |
|||
const SizedBox(width: 8), |
|||
Expanded( |
|||
child: _buildMobileMoneyTile( |
|||
title: 'Airtel Money', |
|||
imagePath: 'assets/airtel_money.png', |
|||
value: PaymentType.airtel, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(height: 16), |
|||
|
|||
// Section Carte bancaire |
|||
const Align( |
|||
alignment: Alignment.centerLeft, |
|||
child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)), |
|||
), |
|||
const SizedBox(height: 8), |
|||
_buildPaymentMethodTile( |
|||
title: 'Carte bancaire', |
|||
icon: Icons.credit_card, |
|||
value: PaymentType.card, |
|||
), |
|||
const SizedBox(height: 16), |
|||
|
|||
// Section Paiement en liquide |
|||
const Align( |
|||
alignment: Alignment.centerLeft, |
|||
child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)), |
|||
), |
|||
const SizedBox(height: 8), |
|||
_buildPaymentMethodTile( |
|||
title: 'Paiement en liquide', |
|||
icon: Icons.money, |
|||
value: PaymentType.cash, |
|||
), |
|||
if (_selectedPayment == PaymentType.cash) ...[ |
|||
const SizedBox(height: 12), |
|||
TextField( |
|||
controller: _amountController, |
|||
decoration: const InputDecoration( |
|||
labelText: 'Montant donné', |
|||
prefixText: 'MGA ', |
|||
border: OutlineInputBorder(), |
|||
), |
|||
keyboardType: TextInputType.numberWithOptions(decimal: true), |
|||
onChanged: (value) => setState(() {}), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.bold, |
|||
color: change >= 0 ? Colors.green : Colors.red, |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Navigator.pop(context), |
|||
child: const Text('Annuler', style: TextStyle(color: Colors.grey)), |
|||
), |
|||
ElevatedButton( |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.blue.shade800, |
|||
foregroundColor: Colors.white, |
|||
), |
|||
onPressed: _validatePayment, |
|||
child: const Text('Confirmer'), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
Widget _buildMobileMoneyTile({ |
|||
required String title, |
|||
required String imagePath, |
|||
required PaymentType value, |
|||
}) { |
|||
return Card( |
|||
elevation: 2, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
side: BorderSide( |
|||
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), |
|||
width: 2, |
|||
), |
|||
), |
|||
child: InkWell( |
|||
borderRadius: BorderRadius.circular(8), |
|||
onTap: () => setState(() => _selectedPayment = value), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(12), |
|||
child: Column( |
|||
children: [ |
|||
Image.asset( |
|||
imagePath, |
|||
height: 30, |
|||
width: 30, |
|||
fit: BoxFit.contain, |
|||
errorBuilder: (context, error, stackTrace) => |
|||
const Icon(Icons.mobile_friendly, size: 30), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
title, |
|||
textAlign: TextAlign.center, |
|||
style: const TextStyle(fontSize: 12), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildPaymentMethodTile({ |
|||
required String title, |
|||
required IconData icon, |
|||
required PaymentType value, |
|||
}) { |
|||
return Card( |
|||
elevation: 2, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
side: BorderSide( |
|||
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), |
|||
width: 2, |
|||
), |
|||
), |
|||
child: InkWell( |
|||
borderRadius: BorderRadius.circular(8), |
|||
onTap: () => setState(() => _selectedPayment = value), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(12), |
|||
child: Row( |
|||
children: [ |
|||
Icon(icon, size: 24), |
|||
const SizedBox(width: 12), |
|||
Text(title), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
enum PaymentType { |
|||
cash, |
|||
card, |
|||
mvola, |
|||
orange, |
|||
airtel |
|||
} |
|||
@ -0,0 +1,411 @@ |
|||
// Components/newCommandComponents/CadeauDialog.dart |
|||
|
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
import 'package:youmazgestion/Models/produit.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
|
|||
class CadeauDialog extends StatefulWidget { |
|||
final Product product; |
|||
final int quantite; |
|||
final DetailCommande? detailExistant; |
|||
|
|||
const CadeauDialog({ |
|||
Key? key, |
|||
required this.product, |
|||
required this.quantite, |
|||
this.detailExistant, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
_CadeauDialogState createState() => _CadeauDialogState(); |
|||
} |
|||
|
|||
class _CadeauDialogState extends State<CadeauDialog> { |
|||
final AppDatabase _database = AppDatabase.instance; |
|||
List<Product> _produitsDisponibles = []; |
|||
Product? _produitCadeauSelectionne; |
|||
int _quantiteCadeau = 1; |
|||
bool _isLoading = true; |
|||
String _searchQuery = ''; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_loadProduitsDisponibles(); |
|||
} |
|||
|
|||
Future<void> _loadProduitsDisponibles() async { |
|||
try { |
|||
final produits = await _database.getProducts(); |
|||
setState(() { |
|||
_produitsDisponibles = produits.where((p) => |
|||
p.id != widget.product.id && // Exclure le produit principal |
|||
(p.stock == null || p.stock! > 0) // Seulement les produits en stock |
|||
).toList(); |
|||
_isLoading = false; |
|||
}); |
|||
} catch (e) { |
|||
setState(() { |
|||
_isLoading = false; |
|||
}); |
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Impossible de charger les produits: $e', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
} |
|||
} |
|||
|
|||
List<Product> get _produitsFiltres { |
|||
if (_searchQuery.isEmpty) { |
|||
return _produitsDisponibles; |
|||
} |
|||
return _produitsDisponibles.where((p) => |
|||
p.name.toLowerCase().contains(_searchQuery.toLowerCase()) || |
|||
(p.reference?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false) |
|||
).toList(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final isMobile = MediaQuery.of(context).size.width < 600; |
|||
|
|||
return AlertDialog( |
|||
title: Row( |
|||
children: [ |
|||
Container( |
|||
padding: const EdgeInsets.all(8), |
|||
decoration: BoxDecoration( |
|||
color: Colors.green.shade100, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Icon(Icons.card_giftcard, color: Colors.green.shade700), |
|||
), |
|||
const SizedBox(width: 12), |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Ajouter un cadeau', |
|||
style: TextStyle(fontSize: isMobile ? 16 : 18), |
|||
), |
|||
Text( |
|||
'Pour: ${widget.product.name}', |
|||
style: TextStyle( |
|||
fontSize: isMobile ? 12 : 14, |
|||
color: Colors.grey.shade600, |
|||
fontWeight: FontWeight.normal, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
content: Container( |
|||
width: isMobile ? double.maxFinite : 500, |
|||
constraints: BoxConstraints( |
|||
maxHeight: MediaQuery.of(context).size.height * 0.7, |
|||
), |
|||
child: _isLoading |
|||
? const Center(child: CircularProgressIndicator()) |
|||
: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
// Information sur le produit principal |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.blue.shade200), |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
Icon(Icons.shopping_bag, color: Colors.blue.shade700), |
|||
const SizedBox(width: 8), |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Produit acheté', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.blue.shade700, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
Text( |
|||
'${widget.quantite}x ${widget.product.name}', |
|||
style: const TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
Text( |
|||
'Prix: ${widget.product.price.toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.grey.shade600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
// Barre de recherche |
|||
TextField( |
|||
decoration: InputDecoration( |
|||
labelText: 'Rechercher un produit cadeau', |
|||
prefixIcon: Icon(Icons.search, color: Colors.green.shade600), |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.green.shade50, |
|||
), |
|||
onChanged: (value) { |
|||
setState(() { |
|||
_searchQuery = value; |
|||
}); |
|||
}, |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
// Liste des produits disponibles |
|||
Expanded( |
|||
child: _produitsFiltres.isEmpty |
|||
? Center( |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon( |
|||
Icons.card_giftcard_outlined, |
|||
size: 48, |
|||
color: Colors.grey.shade400, |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'Aucun produit disponible', |
|||
style: TextStyle( |
|||
color: Colors.grey.shade600, |
|||
fontSize: 14, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
) |
|||
: ListView.builder( |
|||
itemCount: _produitsFiltres.length, |
|||
itemBuilder: (context, index) { |
|||
final produit = _produitsFiltres[index]; |
|||
final isSelected = _produitCadeauSelectionne?.id == produit.id; |
|||
|
|||
return Card( |
|||
margin: const EdgeInsets.only(bottom: 8), |
|||
elevation: isSelected ? 4 : 1, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
side: BorderSide( |
|||
color: isSelected |
|||
? Colors.green.shade300 |
|||
: Colors.grey.shade200, |
|||
width: isSelected ? 2 : 1, |
|||
), |
|||
), |
|||
child: ListTile( |
|||
contentPadding: const EdgeInsets.all(12), |
|||
leading: Container( |
|||
width: 40, |
|||
height: 40, |
|||
decoration: BoxDecoration( |
|||
color: isSelected |
|||
? Colors.green.shade100 |
|||
: Colors.grey.shade100, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Icon( |
|||
Icons.card_giftcard, |
|||
color: isSelected |
|||
? Colors.green.shade700 |
|||
: Colors.grey.shade600, |
|||
), |
|||
), |
|||
title: Text( |
|||
produit.name, |
|||
style: TextStyle( |
|||
fontWeight: isSelected |
|||
? FontWeight.bold |
|||
: FontWeight.normal, |
|||
), |
|||
), |
|||
subtitle: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Prix normal: ${produit.price.toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.grey.shade600, |
|||
decoration: TextDecoration.lineThrough, |
|||
), |
|||
), |
|||
Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.card_giftcard, |
|||
size: 14, |
|||
color: Colors.green.shade600, |
|||
), |
|||
const SizedBox(width: 4), |
|||
Text( |
|||
'GRATUIT', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.green.shade700, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
if (produit.stock != null) |
|||
Text( |
|||
'Stock: ${produit.stock}', |
|||
style: TextStyle( |
|||
fontSize: 11, |
|||
color: Colors.grey.shade500, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
trailing: isSelected |
|||
? Icon( |
|||
Icons.check_circle, |
|||
color: Colors.green.shade700, |
|||
) |
|||
: null, |
|||
onTap: () { |
|||
setState(() { |
|||
_produitCadeauSelectionne = produit; |
|||
}); |
|||
}, |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
|
|||
// Sélection de la quantité si un produit est sélectionné |
|||
if (_produitCadeauSelectionne != null) ...[ |
|||
const SizedBox(height: 16), |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.green.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.green.shade200), |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
Icon(Icons.card_giftcard, color: Colors.green.shade700), |
|||
const SizedBox(width: 8), |
|||
Expanded( |
|||
child: Text( |
|||
'Quantité de ${_produitCadeauSelectionne!.name}', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.w500, |
|||
color: Colors.green.shade700, |
|||
), |
|||
), |
|||
), |
|||
Container( |
|||
decoration: BoxDecoration( |
|||
color: Colors.white, |
|||
borderRadius: BorderRadius.circular(20), |
|||
border: Border.all(color: Colors.green.shade300), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
IconButton( |
|||
icon: const Icon(Icons.remove, size: 16), |
|||
onPressed: _quantiteCadeau > 1 |
|||
? () { |
|||
setState(() { |
|||
_quantiteCadeau--; |
|||
}); |
|||
} |
|||
: null, |
|||
), |
|||
Text( |
|||
_quantiteCadeau.toString(), |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 14, |
|||
), |
|||
), |
|||
IconButton( |
|||
icon: const Icon(Icons.add, size: 16), |
|||
onPressed: () { |
|||
final maxStock = _produitCadeauSelectionne!.stock ?? 99; |
|||
if (_quantiteCadeau < maxStock) { |
|||
setState(() { |
|||
_quantiteCadeau++; |
|||
}); |
|||
} |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Get.back(), |
|||
child: const Text('Annuler'), |
|||
), |
|||
ElevatedButton.icon( |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.green.shade700, |
|||
foregroundColor: Colors.white, |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: isMobile ? 16 : 20, |
|||
vertical: isMobile ? 10 : 12, |
|||
), |
|||
), |
|||
icon: const Icon(Icons.card_giftcard), |
|||
label: Text( |
|||
isMobile ? 'Offrir' : 'Offrir le cadeau', |
|||
style: TextStyle(fontSize: isMobile ? 12 : 14), |
|||
), |
|||
onPressed: _produitCadeauSelectionne != null |
|||
? () { |
|||
Get.back(result: { |
|||
'produit': _produitCadeauSelectionne!, |
|||
'quantite': _quantiteCadeau, |
|||
}); |
|||
} |
|||
: null, |
|||
), |
|||
], |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,331 @@ |
|||
// Components/RemiseDialog.dart |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
import 'package:youmazgestion/Models/produit.dart'; |
|||
|
|||
class RemiseDialog extends StatefulWidget { |
|||
final Product product; |
|||
final int quantite; |
|||
final double prixUnitaire; |
|||
final DetailCommande? detailExistant; |
|||
|
|||
const RemiseDialog({ |
|||
super.key, |
|||
required this.product, |
|||
required this.quantite, |
|||
required this.prixUnitaire, |
|||
this.detailExistant, |
|||
}); |
|||
|
|||
@override |
|||
State<RemiseDialog> createState() => _RemiseDialogState(); |
|||
} |
|||
|
|||
class _RemiseDialogState extends State<RemiseDialog> { |
|||
final _formKey = GlobalKey<FormState>(); |
|||
final _valeurController = TextEditingController(); |
|||
|
|||
RemiseType _selectedType = RemiseType.pourcentage; |
|||
double _montantRemise = 0.0; |
|||
double _prixFinal = 0.0; |
|||
late double _sousTotal; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_sousTotal = widget.quantite * widget.prixUnitaire; |
|||
|
|||
// Si on modifie une remise existante |
|||
if (widget.detailExistant?.aRemise == true) { |
|||
_selectedType = widget.detailExistant!.remiseType!; |
|||
_valeurController.text = widget.detailExistant!.remiseValeur.toString(); |
|||
_calculateRemise(); |
|||
} else { |
|||
_prixFinal = _sousTotal; |
|||
} |
|||
} |
|||
|
|||
void _calculateRemise() { |
|||
final valeur = double.tryParse(_valeurController.text) ?? 0.0; |
|||
|
|||
setState(() { |
|||
if (_selectedType == RemiseType.pourcentage) { |
|||
final pourcentage = valeur.clamp(0.0, 100.0); |
|||
_montantRemise = _sousTotal * (pourcentage / 100); |
|||
} else { |
|||
_montantRemise = valeur.clamp(0.0, _sousTotal); |
|||
} |
|||
_prixFinal = _sousTotal - _montantRemise; |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final isMobile = MediaQuery.of(context).size.width < 600; |
|||
|
|||
return AlertDialog( |
|||
title: Row( |
|||
children: [ |
|||
Container( |
|||
padding: const EdgeInsets.all(8), |
|||
decoration: BoxDecoration( |
|||
color: Colors.orange.shade100, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Icon(Icons.discount, color: Colors.orange.shade700), |
|||
), |
|||
const SizedBox(width: 12), |
|||
Expanded( |
|||
child: Text( |
|||
'Appliquer une remise', |
|||
style: TextStyle(fontSize: isMobile ? 16 : 18), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
content: Container( |
|||
width: isMobile ? double.maxFinite : 400, |
|||
child: Form( |
|||
key: _formKey, |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// Informations du produit |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
widget.product.name, |
|||
style: const TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
const SizedBox(height: 4), |
|||
Text( |
|||
'Quantité: ${widget.quantite}', |
|||
style: const TextStyle(fontSize: 12), |
|||
), |
|||
Text( |
|||
'Prix unitaire: ${widget.prixUnitaire.toStringAsFixed(2)} MGA', |
|||
style: const TextStyle(fontSize: 12), |
|||
), |
|||
Text( |
|||
'Sous-total: ${_sousTotal.toStringAsFixed(2)} MGA', |
|||
style: const TextStyle( |
|||
fontSize: 12, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
// Type de remise |
|||
const Text( |
|||
'Type de remise:', |
|||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), |
|||
), |
|||
const SizedBox(height: 8), |
|||
|
|||
Row( |
|||
children: [ |
|||
Expanded( |
|||
child: RadioListTile<RemiseType>( |
|||
title: const Text('Pourcentage (%)', style: TextStyle(fontSize: 12)), |
|||
value: RemiseType.pourcentage, |
|||
groupValue: _selectedType, |
|||
onChanged: (value) { |
|||
setState(() { |
|||
_selectedType = value!; |
|||
_calculateRemise(); |
|||
}); |
|||
}, |
|||
contentPadding: EdgeInsets.zero, |
|||
dense: true, |
|||
), |
|||
), |
|||
Expanded( |
|||
child: RadioListTile<RemiseType>( |
|||
title: const Text('Montant (MGA)', style: TextStyle(fontSize: 12)), |
|||
value: RemiseType.montant, |
|||
groupValue: _selectedType, |
|||
onChanged: (value) { |
|||
setState(() { |
|||
_selectedType = value!; |
|||
_calculateRemise(); |
|||
}); |
|||
}, |
|||
contentPadding: EdgeInsets.zero, |
|||
dense: true, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
// Valeur de la remise |
|||
TextFormField( |
|||
controller: _valeurController, |
|||
decoration: InputDecoration( |
|||
labelText: _selectedType == RemiseType.pourcentage |
|||
? 'Pourcentage (0-100)' |
|||
: 'Montant en MGA', |
|||
prefixIcon: Icon( |
|||
_selectedType == RemiseType.pourcentage |
|||
? Icons.percent |
|||
: Icons.attach_money, |
|||
), |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.grey.shade50, |
|||
), |
|||
keyboardType: const TextInputType.numberWithOptions(decimal: true), |
|||
inputFormatters: [ |
|||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), |
|||
], |
|||
validator: (value) { |
|||
if (value == null || value.isEmpty) { |
|||
return 'Veuillez entrer une valeur'; |
|||
} |
|||
final valeur = double.tryParse(value); |
|||
if (valeur == null || valeur < 0) { |
|||
return 'Valeur invalide'; |
|||
} |
|||
if (_selectedType == RemiseType.pourcentage && valeur > 100) { |
|||
return 'Le pourcentage ne peut pas dépasser 100%'; |
|||
} |
|||
if (_selectedType == RemiseType.montant && valeur > _sousTotal) { |
|||
return 'La remise ne peut pas dépasser le sous-total'; |
|||
} |
|||
return null; |
|||
}, |
|||
onChanged: (value) => _calculateRemise(), |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
// Aperçu du calcul |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.green.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all(color: Colors.green.shade200), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text('Sous-total:', style: TextStyle(fontSize: 12)), |
|||
Text( |
|||
'${_sousTotal.toStringAsFixed(2)} MGA', |
|||
style: const TextStyle(fontSize: 12), |
|||
), |
|||
], |
|||
), |
|||
if (_montantRemise > 0) ...[ |
|||
const SizedBox(height: 4), |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
Text( |
|||
'Remise ${_selectedType == RemiseType.pourcentage ? "(${_valeurController.text}%)" : ""}:', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.orange.shade700, |
|||
), |
|||
), |
|||
Text( |
|||
'-${_montantRemise.toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.orange.shade700, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
const Divider(height: 12), |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Prix final:', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
Text( |
|||
'${_prixFinal.toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.green.shade700, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
actions: [ |
|||
if (widget.detailExistant?.aRemise == true) |
|||
TextButton.icon( |
|||
onPressed: () => Navigator.of(context).pop('supprimer'), |
|||
icon: const Icon(Icons.delete, color: Colors.red), |
|||
label: const Text('Supprimer remise', style: TextStyle(color: Colors.red)), |
|||
), |
|||
TextButton( |
|||
onPressed: () => Navigator.of(context).pop(), |
|||
child: const Text('Annuler'), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: () { |
|||
if (_formKey.currentState!.validate()) { |
|||
final valeur = double.parse(_valeurController.text); |
|||
Navigator.of(context).pop({ |
|||
'type': _selectedType, |
|||
'valeur': valeur, |
|||
'montantRemise': _montantRemise, |
|||
'prixFinal': _prixFinal, |
|||
}); |
|||
} |
|||
}, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.orange.shade700, |
|||
foregroundColor: Colors.white, |
|||
), |
|||
child: const Text('Appliquer'), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_valeurController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
enum PaymentType { |
|||
cash, |
|||
card, |
|||
mvola, |
|||
orange, |
|||
airtel |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
class Pointage { |
|||
final int? id; |
|||
final String userName; |
|||
final String date; |
|||
final String heureArrivee; |
|||
final String heureDepart; |
|||
|
|||
Pointage({ |
|||
this.id, |
|||
required this.userName, |
|||
required this.date, |
|||
required this.heureArrivee, |
|||
required this.heureDepart, |
|||
}); |
|||
|
|||
// Pour SQLite |
|||
factory Pointage.fromMap(Map<String, dynamic> map) { |
|||
return Pointage( |
|||
id: map['id'], |
|||
userName: map['userName'] ?? '', |
|||
date: map['date'], |
|||
heureArrivee: map['heureArrivee'], |
|||
heureDepart: map['heureDepart'], |
|||
); |
|||
} |
|||
|
|||
Map<String, dynamic> toMap() { |
|||
return { |
|||
'id': id, |
|||
'userName': userName, |
|||
'date': date, |
|||
'heureArrivee': heureArrivee, |
|||
'heureDepart': heureDepart, |
|||
}; |
|||
} |
|||
} |
|||
@ -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"); |
|||
}); |
|||
} |
|||
@ -0,0 +1,304 @@ |
|||
-- Script SQL pour créer la base de données guycom_database_v1 |
|||
-- Création des tables et insertion des données par défaut |
|||
|
|||
-- ===================================================== |
|||
-- CRÉATION DES TABLES |
|||
-- ===================================================== |
|||
|
|||
-- Table permissions |
|||
CREATE TABLE `permissions` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`name` varchar(255) NOT NULL, |
|||
PRIMARY KEY (`id`), |
|||
UNIQUE KEY `name` (`name`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table menu |
|||
CREATE TABLE `menu` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`name` varchar(255) NOT NULL, |
|||
`route` varchar(255) NOT NULL, |
|||
PRIMARY KEY (`id`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table roles |
|||
CREATE TABLE `roles` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`designation` varchar(255) NOT NULL, |
|||
PRIMARY KEY (`id`), |
|||
UNIQUE KEY `designation` (`designation`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table points_de_vente |
|||
CREATE TABLE `points_de_vente` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`nom` varchar(255) NOT NULL, |
|||
PRIMARY KEY (`id`), |
|||
UNIQUE KEY `nom` (`nom`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table clients |
|||
CREATE TABLE `clients` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`nom` varchar(255) NOT NULL, |
|||
`prenom` varchar(255) NOT NULL, |
|||
`email` varchar(255) NOT NULL, |
|||
`telephone` varchar(255) NOT NULL, |
|||
`adresse` varchar(500) DEFAULT NULL, |
|||
`dateCreation` datetime NOT NULL, |
|||
`actif` tinyint(1) NOT NULL DEFAULT 1, |
|||
PRIMARY KEY (`id`), |
|||
UNIQUE KEY `email` (`email`), |
|||
KEY `idx_clients_email` (`email`), |
|||
KEY `idx_clients_telephone` (`telephone`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table users |
|||
CREATE TABLE `users` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`name` varchar(255) NOT NULL, |
|||
`lastname` varchar(255) NOT NULL, |
|||
`email` varchar(255) NOT NULL, |
|||
`password` varchar(255) NOT NULL, |
|||
`username` varchar(255) NOT NULL, |
|||
`role_id` int(11) NOT NULL, |
|||
`point_de_vente_id` int(11) DEFAULT NULL, |
|||
PRIMARY KEY (`id`), |
|||
UNIQUE KEY `email` (`email`), |
|||
UNIQUE KEY `username` (`username`), |
|||
KEY `role_id` (`role_id`), |
|||
KEY `point_de_vente_id` (`point_de_vente_id`), |
|||
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`), |
|||
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table products |
|||
CREATE TABLE `products` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`name` varchar(255) NOT NULL, |
|||
`price` decimal(10,2) NOT NULL, |
|||
`image` varchar(2000) DEFAULT NULL, |
|||
`category` varchar(255) NOT NULL, |
|||
`stock` int(11) NOT NULL DEFAULT 0, |
|||
`description` varchar(1000) DEFAULT NULL, |
|||
`qrCode` varchar(500) DEFAULT NULL, |
|||
`reference` varchar(255) DEFAULT NULL, |
|||
`point_de_vente_id` int(11) DEFAULT NULL, |
|||
`marque` varchar(255) DEFAULT NULL, |
|||
`ram` varchar(100) DEFAULT NULL, |
|||
`memoire_interne` varchar(100) DEFAULT NULL, |
|||
`imei` varchar(255) DEFAULT NULL, |
|||
PRIMARY KEY (`id`), |
|||
UNIQUE KEY `imei` (`imei`), |
|||
KEY `point_de_vente_id` (`point_de_vente_id`), |
|||
KEY `idx_products_category` (`category`), |
|||
KEY `idx_products_reference` (`reference`), |
|||
KEY `idx_products_imei` (`imei`), |
|||
CONSTRAINT `products_ibfk_1` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=127 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table commandes |
|||
CREATE TABLE `commandes` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`clientId` int(11) NOT NULL, |
|||
`dateCommande` datetime NOT NULL, |
|||
`statut` int(11) NOT NULL DEFAULT 0, |
|||
`montantTotal` decimal(10,2) NOT NULL, |
|||
`notes` varchar(1000) DEFAULT NULL, |
|||
`dateLivraison` datetime DEFAULT NULL, |
|||
`commandeurId` int(11) DEFAULT NULL, |
|||
`validateurId` int(11) DEFAULT NULL, |
|||
PRIMARY KEY (`id`), |
|||
KEY `commandeurId` (`commandeurId`), |
|||
KEY `validateurId` (`validateurId`), |
|||
KEY `idx_commandes_client` (`clientId`), |
|||
KEY `idx_commandes_date` (`dateCommande`), |
|||
CONSTRAINT `commandes_ibfk_1` FOREIGN KEY (`commandeurId`) REFERENCES `users` (`id`), |
|||
CONSTRAINT `commandes_ibfk_2` FOREIGN KEY (`validateurId`) REFERENCES `users` (`id`), |
|||
CONSTRAINT `commandes_ibfk_3` FOREIGN KEY (`clientId`) REFERENCES `clients` (`id`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table details_commandes |
|||
CREATE TABLE `details_commandes` ( |
|||
`id` int(11) NOT NULL AUTO_INCREMENT, |
|||
`commandeId` int(11) NOT NULL, |
|||
`produitId` int(11) NOT NULL, |
|||
`quantite` int(11) NOT NULL, |
|||
`prixUnitaire` decimal(10,2) NOT NULL, |
|||
`sousTotal` decimal(10,2) NOT NULL, |
|||
`remise_type` enum('pourcentage','montant') DEFAULT NULL, |
|||
`remise_valeur` decimal(10,2) DEFAULT 0.00, |
|||
`montant_remise` decimal(10,2) DEFAULT 0.00, |
|||
`prix_final` decimal(10,2) NOT NULL DEFAULT 0.00, |
|||
`est_cadeau` tinyint(1) NOT NULL DEFAULT 0, |
|||
PRIMARY KEY (`id`), |
|||
KEY `produitId` (`produitId`), |
|||
KEY `idx_details_commande` (`commandeId`), |
|||
KEY `idx_est_cadeau` (`est_cadeau`), |
|||
CONSTRAINT `details_commandes_ibfk_1` FOREIGN KEY (`commandeId`) REFERENCES `commandes` (`id`) ON DELETE CASCADE, |
|||
CONSTRAINT `details_commandes_ibfk_2` FOREIGN KEY (`produitId`) REFERENCES `products` (`id`) |
|||
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table role_permissions |
|||
CREATE TABLE `role_permissions` ( |
|||
`role_id` int(11) NOT NULL, |
|||
`permission_id` int(11) NOT NULL, |
|||
PRIMARY KEY (`role_id`,`permission_id`), |
|||
KEY `permission_id` (`permission_id`), |
|||
CONSTRAINT `role_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE, |
|||
CONSTRAINT `role_permissions_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- Table role_menu_permissions |
|||
CREATE TABLE `role_menu_permissions` ( |
|||
`role_id` int(11) NOT NULL, |
|||
`menu_id` int(11) NOT NULL, |
|||
`permission_id` int(11) NOT NULL, |
|||
PRIMARY KEY (`role_id`,`menu_id`,`permission_id`), |
|||
KEY `menu_id` (`menu_id`), |
|||
KEY `permission_id` (`permission_id`), |
|||
CONSTRAINT `role_menu_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE, |
|||
CONSTRAINT `role_menu_permissions_ibfk_2` FOREIGN KEY (`menu_id`) REFERENCES `menu` (`id`) ON DELETE CASCADE, |
|||
CONSTRAINT `role_menu_permissions_ibfk_3` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; |
|||
|
|||
-- ===================================================== |
|||
-- INSERTION DES DONNÉES PAR DÉFAUT |
|||
-- ===================================================== |
|||
|
|||
-- Insertion des permissions par défaut |
|||
INSERT INTO `permissions` (`name`) VALUES |
|||
('view'), |
|||
('create'), |
|||
('update'), |
|||
('delete'), |
|||
('admin'), |
|||
('manage'), |
|||
('read'); |
|||
|
|||
-- Insertion des menus par défaut |
|||
INSERT INTO `menu` (`name`, `route`) VALUES |
|||
('Accueil', '/accueil'), |
|||
('Ajouter un utilisateur', '/ajouter-utilisateur'), |
|||
('Modifier/Supprimer un utilisateur', '/modifier-utilisateur'), |
|||
('Ajouter un produit', '/ajouter-produit'), |
|||
('Modifier/Supprimer un produit', '/modifier-produit'), |
|||
('Bilan', '/bilan'), |
|||
('Gérer les rôles', '/gerer-roles'), |
|||
('Gestion de stock', '/gestion-stock'), |
|||
('Historique', '/historique'), |
|||
('Déconnexion', '/deconnexion'), |
|||
('Nouvelle commande', '/nouvelle-commande'), |
|||
('Gérer les commandes', '/gerer-commandes'), |
|||
('Points de vente', '/points-de-vente'); |
|||
|
|||
-- Insertion des rôles par défaut |
|||
INSERT INTO `roles` (`designation`) VALUES |
|||
('Super Admin'), |
|||
('Admin'), |
|||
('User'), |
|||
('commercial'), |
|||
('caisse'); |
|||
|
|||
-- Attribution de TOUTES les permissions à TOUS les menus pour le Super Admin |
|||
-- On utilise une sous-requête pour récupérer l'ID réel du rôle Super Admin |
|||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) |
|||
SELECT r.id, m.id, p.id |
|||
FROM menu m |
|||
CROSS JOIN permissions p |
|||
CROSS JOIN roles r |
|||
WHERE r.designation = 'Super Admin'; |
|||
|
|||
-- Attribution de permissions basiques pour Admin |
|||
-- Accès en lecture/écriture à la plupart des menus sauf gestion des rôles |
|||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) |
|||
SELECT r.id, m.id, p.id |
|||
FROM menu m |
|||
CROSS JOIN permissions p |
|||
CROSS JOIN roles r |
|||
WHERE r.designation = 'Admin' |
|||
AND m.name != 'Gérer les rôles' |
|||
AND p.name IN ('view', 'create', 'update', 'read'); |
|||
|
|||
-- Attribution de permissions basiques pour User |
|||
-- Accès principalement en lecture et quelques actions de base |
|||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) |
|||
SELECT r.id, m.id, p.id |
|||
FROM menu m |
|||
CROSS JOIN permissions p |
|||
CROSS JOIN roles r |
|||
WHERE r.designation = 'User' |
|||
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gérer les commandes', 'Gestion de stock', 'Historique') |
|||
AND p.name IN ('view', 'read', 'create'); |
|||
|
|||
-- Attribution de permissions pour Commercial |
|||
-- Accès aux commandes, clients, produits |
|||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) |
|||
SELECT r.id, m.id, p.id |
|||
FROM menu m |
|||
CROSS JOIN permissions p |
|||
CROSS JOIN roles r |
|||
WHERE r.designation = 'commercial' |
|||
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gérer les commandes', 'Bilan', 'Historique') |
|||
AND p.name IN ('view', 'create', 'update', 'read'); |
|||
|
|||
-- Attribution de permissions pour Caisse |
|||
-- Accès principalement aux commandes et stock |
|||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`) |
|||
SELECT r.id, m.id, p.id |
|||
FROM menu m |
|||
CROSS JOIN permissions p |
|||
CROSS JOIN roles r |
|||
WHERE r.designation = 'caisse' |
|||
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gestion de stock') |
|||
AND p.name IN ('view', 'create', 'read'); |
|||
|
|||
-- Insertion du Super Admin par défaut |
|||
-- On utilise une sous-requête pour récupérer l'ID réel du rôle Super Admin |
|||
INSERT INTO `users` (`name`, `lastname`, `email`, `password`, `username`, `role_id`) |
|||
SELECT 'Super', 'Admin', 'superadmin@youmazgestion.com', 'admin123', 'superadmin', r.id |
|||
FROM roles r |
|||
WHERE r.designation = 'Super Admin'; |
|||
|
|||
-- ===================================================== |
|||
-- DONNÉES D'EXEMPLE (OPTIONNEL) |
|||
-- ===================================================== |
|||
|
|||
-- Insertion d'un point de vente d'exemple |
|||
INSERT INTO `points_de_vente` (`nom`) VALUES ('Magasin Principal'); |
|||
|
|||
-- Insertion d'un client d'exemple |
|||
INSERT INTO `clients` (`nom`, `prenom`, `email`, `telephone`, `adresse`, `dateCreation`, `actif`) VALUES |
|||
('Dupont', 'Jean', 'jean.dupont@email.com', '0123456789', '123 Rue de la Paix, Paris', NOW(), 1); |
|||
|
|||
-- ===================================================== |
|||
-- VÉRIFICATIONS |
|||
-- ===================================================== |
|||
|
|||
-- Afficher les rôles créés |
|||
SELECT 'RÔLES CRÉÉS:' as info; |
|||
SELECT * FROM roles; |
|||
|
|||
-- Afficher les permissions créées |
|||
SELECT 'PERMISSIONS CRÉÉES:' as info; |
|||
SELECT * FROM permissions; |
|||
|
|||
-- Afficher les menus créés |
|||
SELECT 'MENUS CRÉÉS:' as info; |
|||
SELECT * FROM menu; |
|||
|
|||
-- Afficher le Super Admin créé |
|||
SELECT 'SUPER ADMIN CRÉÉ:' as info; |
|||
SELECT u.username, u.email, r.designation as role |
|||
FROM users u |
|||
JOIN roles r ON u.role_id = r.id |
|||
WHERE r.designation = 'Super Admin'; |
|||
|
|||
-- Vérifier les permissions du Super Admin |
|||
SELECT 'PERMISSIONS SUPER ADMIN:' as info; |
|||
SELECT COUNT(*) as total_permissions_assignees |
|||
FROM role_menu_permissions rmp |
|||
INNER JOIN roles r ON rmp.role_id = r.id |
|||
WHERE r.designation = 'Super Admin'; |
|||
|
|||
SELECT 'Script terminé avec succès!' as resultat; |
|||
@ -1,680 +0,0 @@ |
|||
import 'dart:async'; |
|||
import 'dart:io'; |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:path/path.dart'; |
|||
import 'package:path_provider/path_provider.dart'; |
|||
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; |
|||
import '../Models/users.dart'; |
|||
import '../Models/role.dart'; |
|||
import '../Models/Permission.dart'; |
|||
|
|||
class AppDatabase { |
|||
static final AppDatabase instance = AppDatabase._init(); |
|||
late Database _database; |
|||
|
|||
AppDatabase._init() { |
|||
sqfliteFfiInit(); |
|||
} |
|||
|
|||
Future<Database> get database async { |
|||
if (_database.isOpen) return _database; |
|||
_database = await _initDB('app_database.db'); |
|||
return _database; |
|||
} |
|||
|
|||
Future<void> initDatabase() async { |
|||
_database = await _initDB('app_database.db'); |
|||
await _createDB(_database, 1); |
|||
await insertDefaultPermissions(); |
|||
await insertDefaultMenus(); |
|||
await insertDefaultRoles(); |
|||
await insertDefaultSuperAdmin(); |
|||
} |
|||
|
|||
Future<Database> _initDB(String filePath) async { |
|||
final documentsDirectory = await getApplicationDocumentsDirectory(); |
|||
final path = join(documentsDirectory.path, filePath); |
|||
|
|||
bool dbExists = await File(path).exists(); |
|||
if (!dbExists) { |
|||
try { |
|||
ByteData data = await rootBundle.load('assets/database/$filePath'); |
|||
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); |
|||
await File(path).writeAsBytes(bytes); |
|||
} catch (e) { |
|||
print('Pas de fichier DB dans assets, création d\'une nouvelle DB'); |
|||
} |
|||
} |
|||
|
|||
return await databaseFactoryFfi.openDatabase(path); |
|||
} |
|||
|
|||
Future<void> _createDB(Database db, int version) async { |
|||
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'"); |
|||
final tableNames = tables.map((row) => row['name'] as String).toList(); |
|||
|
|||
if (!tableNames.contains('roles')) { |
|||
await db.execute(''' |
|||
CREATE TABLE roles ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
designation TEXT NOT NULL UNIQUE |
|||
) |
|||
'''); |
|||
print("Table 'roles' créée."); |
|||
} |
|||
|
|||
if (!tableNames.contains('permissions')) { |
|||
await db.execute(''' |
|||
CREATE TABLE permissions ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT NOT NULL UNIQUE |
|||
) |
|||
'''); |
|||
print("Table 'permissions' créée."); |
|||
} |
|||
|
|||
if (!tableNames.contains('menu')) { |
|||
await db.execute(''' |
|||
CREATE TABLE menu ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT NOT NULL UNIQUE, |
|||
route TEXT NOT NULL UNIQUE |
|||
) |
|||
'''); |
|||
print("Table 'menu' créée."); |
|||
} |
|||
|
|||
if (!tableNames.contains('role_permissions')) { |
|||
await db.execute(''' |
|||
CREATE TABLE role_permissions ( |
|||
role_id INTEGER, |
|||
permission_id INTEGER, |
|||
PRIMARY KEY (role_id, permission_id), |
|||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE |
|||
) |
|||
'''); |
|||
print("Table 'role_permissions' créée."); |
|||
} |
|||
|
|||
if (!tableNames.contains('menu_permissions')) { |
|||
await db.execute(''' |
|||
CREATE TABLE menu_permissions ( |
|||
menu_id INTEGER, |
|||
permission_id INTEGER, |
|||
PRIMARY KEY (menu_id, permission_id), |
|||
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE |
|||
) |
|||
'''); |
|||
print("Table 'menu_permissions' créée."); |
|||
} |
|||
|
|||
if (!tableNames.contains('users')) { |
|||
await db.execute(''' |
|||
CREATE TABLE users ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT NOT NULL, |
|||
lastname TEXT NOT NULL, |
|||
email TEXT NOT NULL UNIQUE, |
|||
password TEXT NOT NULL, |
|||
username TEXT NOT NULL UNIQUE, |
|||
role_id INTEGER NOT NULL, |
|||
FOREIGN KEY (role_id) REFERENCES roles(id) |
|||
) |
|||
'''); |
|||
print("Table 'users' créée."); |
|||
} |
|||
if (!tableNames.contains('role_menu_permissions')) { |
|||
await db.execute(''' |
|||
CREATE TABLE role_menu_permissions ( |
|||
role_id INTEGER, |
|||
menu_id INTEGER, |
|||
permission_id INTEGER, |
|||
PRIMARY KEY (role_id, menu_id, permission_id), |
|||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE |
|||
) |
|||
'''); |
|||
print("Table 'role_menu_permissions' créée."); |
|||
} |
|||
|
|||
} |
|||
|
|||
Future<void> insertDefaultPermissions() async { |
|||
final db = await database; |
|||
final existing = await db.query('permissions'); |
|||
if (existing.isEmpty) { |
|||
await db.insert('permissions', {'name': 'view'}); |
|||
await db.insert('permissions', {'name': 'create'}); |
|||
await db.insert('permissions', {'name': 'update'}); |
|||
await db.insert('permissions', {'name': 'delete'}); |
|||
await db.insert('permissions', {'name': 'admin'}); |
|||
await db.insert('permissions', {'name': 'manage'}); // Nouvelle permission |
|||
await db.insert('permissions', {'name': 'read'}); // Nouvelle permission |
|||
print("Permissions par défaut insérées"); |
|||
} else { |
|||
// Vérifier et ajouter les nouvelles permissions si elles n'existent pas |
|||
final newPermissions = ['manage', 'read']; |
|||
for (var permission in newPermissions) { |
|||
final existingPermission = await db.query('permissions', where: 'name = ?', whereArgs: [permission]); |
|||
if (existingPermission.isEmpty) { |
|||
await db.insert('permissions', {'name': permission}); |
|||
print("Permission ajoutée: $permission"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> insertDefaultMenus() async { |
|||
final db = await database; |
|||
final existingMenus = await db.query('menu'); |
|||
|
|||
if (existingMenus.isEmpty) { |
|||
// Menus existants |
|||
await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'}); |
|||
await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'}); |
|||
await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'}); |
|||
await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'}); |
|||
await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'}); |
|||
await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'}); |
|||
await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'}); |
|||
await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'}); |
|||
await db.insert('menu', {'name': 'Historique', 'route': '/historique'}); |
|||
await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'}); |
|||
|
|||
// Nouveaux menus ajoutés |
|||
await db.insert('menu', {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}); |
|||
await db.insert('menu', {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}); |
|||
|
|||
print("Menus par défaut insérés"); |
|||
} else { |
|||
// Si des menus existent déjà, vérifier et ajouter les nouveaux menus manquants |
|||
await _addMissingMenus(db); |
|||
} |
|||
} |
|||
|
|||
Future<void> _addMissingMenus(Database db) async { |
|||
final menusToAdd = [ |
|||
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, |
|||
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, |
|||
]; |
|||
|
|||
for (var menu in menusToAdd) { |
|||
final existing = await db.query( |
|||
'menu', |
|||
where: 'route = ?', |
|||
whereArgs: [menu['route']], |
|||
); |
|||
|
|||
if (existing.isEmpty) { |
|||
await db.insert('menu', menu); |
|||
print("Menu ajouté: ${menu['name']}"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> insertDefaultRoles() async { |
|||
final db = await database; |
|||
final existingRoles = await db.query('roles'); |
|||
|
|||
if (existingRoles.isEmpty) { |
|||
int superAdminRoleId = await db.insert('roles', {'designation': 'Super Admin'}); |
|||
int adminRoleId = await db.insert('roles', {'designation': 'Admin'}); |
|||
int userRoleId = await db.insert('roles', {'designation': 'User'}); |
|||
|
|||
final permissions = await db.query('permissions'); |
|||
final menus = await db.query('menu'); |
|||
|
|||
// Assigner toutes les permissions à tous les menus pour le Super Admin |
|||
for (var menu in menus) { |
|||
for (var permission in permissions) { |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': superAdminRoleId, |
|||
'menu_id': menu['id'], |
|||
'permission_id': permission['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
} |
|||
|
|||
// Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus |
|||
await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId); |
|||
|
|||
print("Rôles par défaut créés et permissions assignées"); |
|||
} else { |
|||
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes |
|||
await _updateExistingRolePermissions(db); |
|||
} |
|||
} |
|||
// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus |
|||
Future<void> _assignBasicPermissionsToRoles(Database db, int adminRoleId, int userRoleId) async { |
|||
final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']); |
|||
final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']); |
|||
final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']); |
|||
final managePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['manage']); |
|||
|
|||
// Récupérer les IDs des nouveaux menus |
|||
final nouvelleCommandeMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']); |
|||
final gererCommandesMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']); |
|||
|
|||
if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) { |
|||
// Admin peut créer de nouvelles commandes |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': adminRoleId, |
|||
'menu_id': nouvelleCommandeMenu.first['id'], |
|||
'permission_id': createPermission.first['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
|
|||
// User peut aussi créer de nouvelles commandes |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': userRoleId, |
|||
'menu_id': nouvelleCommandeMenu.first['id'], |
|||
'permission_id': createPermission.first['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
|
|||
if (gererCommandesMenu.isNotEmpty && managePermission.isNotEmpty) { |
|||
// Admin peut gérer les commandes |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': adminRoleId, |
|||
'menu_id': gererCommandesMenu.first['id'], |
|||
'permission_id': managePermission.first['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
|
|||
if (gererCommandesMenu.isNotEmpty && viewPermission.isNotEmpty) { |
|||
// User peut voir les commandes |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': userRoleId, |
|||
'menu_id': gererCommandesMenu.first['id'], |
|||
'permission_id': viewPermission.first['id'], |
|||
} |
|||
, conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
} |
|||
Future<void> _updateExistingRolePermissions(Database db) async { |
|||
final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']); |
|||
if (superAdminRole.isNotEmpty) { |
|||
final superAdminRoleId = superAdminRole.first['id'] as int; |
|||
final permissions = await db.query('permissions'); |
|||
final menus = await db.query('menu'); |
|||
|
|||
// Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus |
|||
for (var menu in menus) { |
|||
for (var permission in permissions) { |
|||
final existingPermission = await db.query( |
|||
'role_menu_permissions', |
|||
where: 'role_id = ? AND menu_id = ? AND permission_id = ?', |
|||
whereArgs: [superAdminRoleId, menu['id'], permission['id']], |
|||
); |
|||
if (existingPermission.isEmpty) { |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': superAdminRoleId, |
|||
'menu_id': menu['id'], |
|||
'permission_id': permission['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Assigner les permissions de base aux autres rôles pour les nouveaux menus |
|||
final adminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Admin']); |
|||
final userRole = await db.query('roles', where: 'designation = ?', whereArgs: ['User']); |
|||
|
|||
if (adminRole.isNotEmpty && userRole.isNotEmpty) { |
|||
await _assignBasicPermissionsToRoles(db, adminRole.first['id'] as int, userRole.first['id'] as int); |
|||
} |
|||
|
|||
print("Permissions mises à jour pour tous les rôles"); |
|||
} |
|||
} |
|||
|
|||
|
|||
Future<void> insertDefaultSuperAdmin() async { |
|||
final db = await database; |
|||
|
|||
final existingSuperAdmin = await db.rawQuery(''' |
|||
SELECT u.* FROM users u |
|||
INNER JOIN roles r ON u.role_id = r.id |
|||
WHERE r.designation = 'Super Admin' |
|||
'''); |
|||
|
|||
if (existingSuperAdmin.isEmpty) { |
|||
final superAdminRole = await db.query('roles', |
|||
where: 'designation = ?', |
|||
whereArgs: ['Super Admin'] |
|||
); |
|||
|
|||
if (superAdminRole.isNotEmpty) { |
|||
final superAdminRoleId = superAdminRole.first['id'] as int; |
|||
|
|||
await db.insert('users', { |
|||
'name': 'Super', |
|||
'lastname': 'Admin', |
|||
'email': 'superadmin@youmazgestion.com', |
|||
'password': 'admin123', |
|||
'username': 'superadmin', |
|||
'role_id': superAdminRoleId, |
|||
}); |
|||
|
|||
print("Super Admin créé avec succès !"); |
|||
print("Username: superadmin"); |
|||
print("Password: admin123"); |
|||
print("ATTENTION: Changez ce mot de passe après la première connexion !"); |
|||
} |
|||
} else { |
|||
print("Super Admin existe déjà"); |
|||
} |
|||
} |
|||
|
|||
Future<int> createUser(Users user) async { |
|||
final db = await database; |
|||
return await db.insert('users', user.toMap()); |
|||
} |
|||
|
|||
Future<int> deleteUser(int id) async { |
|||
final db = await database; |
|||
return await db.delete('users', where: 'id = ?', whereArgs: [id]); |
|||
} |
|||
|
|||
Future<int> updateUser(Users user) async { |
|||
final db = await database; |
|||
return await db.update('users', user.toMap(), where: 'id = ?', whereArgs: [user.id]); |
|||
} |
|||
|
|||
Future<int> getUserCount() async { |
|||
final db = await database; |
|||
List<Map<String, dynamic>> result = await db.rawQuery('SELECT COUNT(*) as count FROM users'); |
|||
return result.first['count'] as int; |
|||
} |
|||
|
|||
Future<bool> verifyUser(String username, String password) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT users.id |
|||
FROM users |
|||
WHERE users.username = ? AND users.password = ? |
|||
''', [username, password]); |
|||
return result.isNotEmpty; |
|||
} |
|||
|
|||
Future<Users> getUser(String username) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT users.*, roles.designation as role_name |
|||
FROM users |
|||
INNER JOIN roles ON users.role_id = roles.id |
|||
WHERE users.username = ? |
|||
''', [username]); |
|||
|
|||
if (result.isNotEmpty) { |
|||
return Users.fromMap(result.first); |
|||
} else { |
|||
throw Exception('User not found'); |
|||
} |
|||
} |
|||
|
|||
Future<Map<String, dynamic>?> getUserCredentials(String username, String password) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT users.username, users.id, roles.designation as role_name, roles.id as role_id |
|||
FROM users |
|||
INNER JOIN roles ON users.role_id = roles.id |
|||
WHERE username = ? AND password = ? |
|||
''', [username, password]); |
|||
|
|||
if (result.isNotEmpty) { |
|||
return { |
|||
'id': result.first['id'], |
|||
'username': result.first['username'] as String, |
|||
'role': result.first['role_name'] as String, |
|||
'role_id': result.first['role_id'], |
|||
}; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
Future<List<Users>> getAllUsers() async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT users.*, roles.designation as role_name |
|||
FROM users |
|||
INNER JOIN roles ON users.role_id = roles.id |
|||
ORDER BY users.id ASC |
|||
'''); |
|||
return result.map((json) => Users.fromMap(json)).toList(); |
|||
} |
|||
|
|||
Future<int> createRole(Role role) async { |
|||
final db = await database; |
|||
return await db.insert('roles', role.toMap()); |
|||
} |
|||
|
|||
Future<List<Role>> getRoles() async { |
|||
final db = await database; |
|||
final maps = await db.query('roles', orderBy: 'designation ASC'); |
|||
return List.generate(maps.length, (i) => Role.fromMap(maps[i])); |
|||
} |
|||
|
|||
Future<int> updateRole(Role role) async { |
|||
final db = await database; |
|||
return await db.update( |
|||
'roles', |
|||
role.toMap(), |
|||
where: 'id = ?', |
|||
whereArgs: [role.id], |
|||
); |
|||
} |
|||
|
|||
Future<int> deleteRole(int? id) async { |
|||
final db = await database; |
|||
return await db.delete( |
|||
'roles', |
|||
where: 'id = ?', |
|||
whereArgs: [id], |
|||
); |
|||
} |
|||
|
|||
Future<List<Permission>> getAllPermissions() async { |
|||
final db = await database; |
|||
final result = await db.query('permissions', orderBy: 'name ASC'); |
|||
return result.map((e) => Permission.fromMap(e)).toList(); |
|||
} |
|||
|
|||
Future<List<Permission>> getPermissionsForRole(int roleId) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT p.id, p.name |
|||
FROM permissions p |
|||
JOIN role_permissions rp ON p.id = rp.permission_id |
|||
WHERE rp.role_id = ? |
|||
ORDER BY p.name ASC |
|||
''', [roleId]); |
|||
|
|||
return result.map((map) => Permission.fromMap(map)).toList(); |
|||
} |
|||
|
|||
Future<List<Permission>> getPermissionsForUser(String username) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT DISTINCT p.id, p.name |
|||
FROM permissions p |
|||
JOIN role_permissions rp ON p.id = rp.permission_id |
|||
JOIN roles r ON rp.role_id = r.id |
|||
JOIN users u ON u.role_id = r.id |
|||
WHERE u.username = ? |
|||
ORDER BY p.name ASC |
|||
''', [username]); |
|||
|
|||
return result.map((map) => Permission.fromMap(map)).toList(); |
|||
} |
|||
|
|||
Future<void> assignPermission(int roleId, int permissionId) async { |
|||
final db = await database; |
|||
await db.insert('role_permissions', { |
|||
'role_id': roleId, |
|||
'permission_id': permissionId, |
|||
}, conflictAlgorithm: ConflictAlgorithm.ignore); |
|||
} |
|||
|
|||
Future<void> removePermission(int roleId, int permissionId) async { |
|||
final db = await database; |
|||
await db.delete( |
|||
'role_permissions', |
|||
where: 'role_id = ? AND permission_id = ?', |
|||
whereArgs: [roleId, permissionId], |
|||
); |
|||
} |
|||
|
|||
Future<void> assignMenuPermission(int menuId, int permissionId) async { |
|||
final db = await database; |
|||
await db.insert('menu_permissions', { |
|||
'menu_id': menuId, |
|||
'permission_id': permissionId, |
|||
}, conflictAlgorithm: ConflictAlgorithm.ignore); |
|||
} |
|||
|
|||
Future<void> removeMenuPermission(int menuId, int permissionId) async { |
|||
final db = await database; |
|||
await db.delete( |
|||
'menu_permissions', |
|||
where: 'menu_id = ? AND permission_id = ?', |
|||
whereArgs: [menuId, permissionId], |
|||
); |
|||
} |
|||
|
|||
Future<bool> isSuperAdmin(String username) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT COUNT(*) as count |
|||
FROM users u |
|||
INNER JOIN roles r ON u.role_id = r.id |
|||
WHERE u.username = ? AND r.designation = 'Super Admin' |
|||
''', [username]); |
|||
|
|||
return (result.first['count'] as int) > 0; |
|||
} |
|||
|
|||
Future<void> changePassword(String username, String oldPassword, String newPassword) async { |
|||
final db = await database; |
|||
|
|||
final isValidOldPassword = await verifyUser(username, oldPassword); |
|||
if (!isValidOldPassword) { |
|||
throw Exception('Ancien mot de passe incorrect'); |
|||
} |
|||
|
|||
await db.update( |
|||
'users', |
|||
{'password': newPassword}, |
|||
where: 'username = ?', |
|||
whereArgs: [username], |
|||
); |
|||
} |
|||
|
|||
Future<bool> hasPermission(String username, String permissionName, String menuRoute) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT COUNT(*) as count |
|||
FROM permissions p |
|||
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id |
|||
JOIN roles r ON rmp.role_id = r.id |
|||
JOIN users u ON u.role_id = r.id |
|||
JOIN menu m ON m.route = ? |
|||
WHERE u.username = ? AND p.name = ? AND rmp.menu_id = m.id |
|||
''', [menuRoute, username, permissionName]); |
|||
|
|||
return (result.first['count'] as int) > 0; |
|||
} |
|||
|
|||
|
|||
Future<void> close() async { |
|||
if (_database.isOpen) { |
|||
await _database.close(); |
|||
} |
|||
} |
|||
|
|||
Future<void> printDatabaseInfo() async { |
|||
final db = await database; |
|||
|
|||
print("=== INFORMATIONS DE LA BASE DE DONNÉES ==="); |
|||
|
|||
final userCount = await getUserCount(); |
|||
print("Nombre d'utilisateurs: $userCount"); |
|||
|
|||
final users = await getAllUsers(); |
|||
print("Utilisateurs:"); |
|||
for (var user in users) { |
|||
print(" - ${user.username} (${user.name} ) - Email: ${user.email}"); |
|||
} |
|||
|
|||
final roles = await getRoles(); |
|||
print("Rôles:"); |
|||
for (var role in roles) { |
|||
print(" - ${role.designation} (ID: ${role.id})"); |
|||
} |
|||
|
|||
final permissions = await getAllPermissions(); |
|||
print("Permissions:"); |
|||
for (var permission in permissions) { |
|||
print(" - ${permission.name} (ID: ${permission.id})"); |
|||
} |
|||
|
|||
print("========================================="); |
|||
} |
|||
|
|||
Future<List<Permission>> getPermissionsForRoleAndMenu(int roleId, int menuId) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT p.id, p.name |
|||
FROM permissions p |
|||
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id |
|||
WHERE rmp.role_id = ? AND rmp.menu_id = ? |
|||
ORDER BY p.name ASC |
|||
''', [roleId, menuId]); |
|||
|
|||
return result.map((map) => Permission.fromMap(map)).toList(); |
|||
} |
|||
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue |
|||
Future<void> deleteDatabaseFile() async { |
|||
final documentsDirectory = await getApplicationDocumentsDirectory(); |
|||
final path = join(documentsDirectory.path, 'app_database.db'); |
|||
final file = File(path); |
|||
if (await file.exists()) { |
|||
await file.delete(); |
|||
print("Base de données utilisateur supprimée"); |
|||
} |
|||
} |
|||
Future<void> assignRoleMenuPermission(int roleId, int menuId, int permissionId) async { |
|||
final db = await database; |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': roleId, |
|||
'menu_id': menuId, |
|||
'permission_id': permissionId, |
|||
}, conflictAlgorithm: ConflictAlgorithm.ignore); |
|||
} |
|||
|
|||
|
|||
|
|||
Future<void> removeRoleMenuPermission(int roleId, int menuId, int permissionId) async { |
|||
final db = await database; |
|||
await db.delete( |
|||
'role_menu_permissions', |
|||
where: 'role_id = ? AND menu_id = ? AND permission_id = ?', |
|||
whereArgs: [roleId, menuId, permissionId], |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -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
@ -0,0 +1,44 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/Components/appDrawer.dart'; |
|||
import 'package:youmazgestion/Views/historique.dart'; |
|||
void main() { |
|||
runApp(const MyApp()); |
|||
} |
|||
|
|||
class MyApp extends StatelessWidget { |
|||
const MyApp({super.key}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return GetMaterialApp( |
|||
title: 'Youmaz Gestion', |
|||
theme: ThemeData( |
|||
primarySwatch: Colors.blue, |
|||
visualDensity: VisualDensity.adaptivePlatformDensity, |
|||
), |
|||
home: const MainLayout(), |
|||
debugShowCheckedModeBanner: false, |
|||
); |
|||
} |
|||
} |
|||
|
|||
class MainLayout extends StatefulWidget { |
|||
const MainLayout({super.key}); |
|||
|
|||
@override |
|||
State<MainLayout> createState() => _MainLayoutState(); |
|||
} |
|||
|
|||
class _MainLayoutState extends State<MainLayout> { |
|||
// Index par défaut pour la page de commande |
|||
|
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
drawer: CustomDrawer(), |
|||
body: const HistoriquePage(), |
|||
); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,64 @@ |
|||
// Config/database_config.dart - Version améliorée |
|||
class DatabaseConfig { |
|||
static const String host = 'localhost'; |
|||
static const int port = 3306; |
|||
static const String username = 'root'; |
|||
static const String? password = null; |
|||
static const String database = 'gico'; |
|||
|
|||
static const String prodHost = '185.70.105.157'; |
|||
static const String prodUsername = 'guycom'; |
|||
static const String prodPassword = '3iV59wjRdbuXAPR'; |
|||
static const String prodDatabase = 'guycom'; |
|||
|
|||
static const Duration connectionTimeout = Duration(seconds: 30); |
|||
static const Duration queryTimeout = Duration(seconds: 15); |
|||
|
|||
static const int maxConnections = 10; |
|||
static const int minConnections = 2; |
|||
|
|||
static bool get isDevelopment => true; |
|||
|
|||
static Map<String, dynamic> getConfig() { |
|||
if (isDevelopment) { |
|||
return { |
|||
'host': host, |
|||
'port': port, |
|||
'user': username, |
|||
'password': password, |
|||
'database': database, |
|||
'timeout': connectionTimeout.inSeconds, |
|||
}; |
|||
} else { |
|||
return { |
|||
'host': prodHost, |
|||
'port': port, |
|||
'user': prodUsername, |
|||
'password': prodPassword, |
|||
'database': prodDatabase, |
|||
'timeout': connectionTimeout.inSeconds, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
// Validation de la configuration |
|||
static bool validateConfig() { |
|||
try { |
|||
final config = getConfig(); |
|||
return config['host']?.toString().isNotEmpty == true && |
|||
config['database']?.toString().isNotEmpty == true && |
|||
config['user'] != null; |
|||
} catch (e) { |
|||
print("Erreur de validation de la configuration: $e"); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// Configuration avec retry automatique |
|||
static Map<String, dynamic> getConfigWithRetry() { |
|||
final config = getConfig(); |
|||
config['retryCount'] = 3; |
|||
config['retryDelay'] = 5000; // ms |
|||
return config; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue