26 changed files with 9141 additions and 3459 deletions
@ -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,338 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get/get_core/src/get_main.dart'; |
||||
|
import 'package:youmazgestion/Components/DiscountDialog.dart'; |
||||
|
import 'package:youmazgestion/Components/paymentType.dart'; |
||||
|
import 'package:youmazgestion/Models/Client.dart'; |
||||
|
import 'package:youmazgestion/Models/Remise.dart'; |
||||
|
|
||||
|
// Dialogue de paiement amélioré avec support des remises |
||||
|
class PaymentMethodEnhancedDialog extends StatefulWidget { |
||||
|
final Commande commande; |
||||
|
|
||||
|
const PaymentMethodEnhancedDialog({super.key, required this.commande}); |
||||
|
|
||||
|
@override |
||||
|
_PaymentMethodEnhancedDialogState createState() => _PaymentMethodEnhancedDialogState(); |
||||
|
} |
||||
|
|
||||
|
class _PaymentMethodEnhancedDialogState extends State<PaymentMethodEnhancedDialog> { |
||||
|
PaymentType _selectedPayment = PaymentType.cash; |
||||
|
final _amountController = TextEditingController(); |
||||
|
Remise? _appliedRemise; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_amountController.text = widget.commande.montantTotal.toStringAsFixed(2); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
_amountController.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
|
||||
|
void _showDiscountDialog() { |
||||
|
showDialog( |
||||
|
context: context, |
||||
|
builder: (context) => DiscountDialog( |
||||
|
onDiscountApplied: (remise) { |
||||
|
setState(() { |
||||
|
_appliedRemise = remise; |
||||
|
final montantFinal = widget.commande.montantTotal - remise.calculerRemise(widget.commande.montantTotal); |
||||
|
_amountController.text = montantFinal.toStringAsFixed(2); |
||||
|
}); |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _removeDiscount() { |
||||
|
setState(() { |
||||
|
_appliedRemise = null; |
||||
|
_amountController.text = widget.commande.montantTotal.toStringAsFixed(2); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
void _validatePayment() { |
||||
|
final montantFinal = _appliedRemise != null |
||||
|
? widget.commande.montantTotal - _appliedRemise!.calculerRemise(widget.commande.montantTotal) |
||||
|
: widget.commande.montantTotal; |
||||
|
|
||||
|
if (_selectedPayment == PaymentType.cash) { |
||||
|
final amountGiven = double.tryParse(_amountController.text) ?? 0; |
||||
|
if (amountGiven < montantFinal) { |
||||
|
Get.snackbar( |
||||
|
'Erreur', |
||||
|
'Le montant donné est insuffisant', |
||||
|
snackPosition: SnackPosition.BOTTOM, |
||||
|
backgroundColor: Colors.red, |
||||
|
colorText: Colors.white, |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Navigator.pop(context, PaymentMethodEnhanced( |
||||
|
type: _selectedPayment, |
||||
|
amountGiven: _selectedPayment == PaymentType.cash |
||||
|
? double.parse(_amountController.text) |
||||
|
: montantFinal, |
||||
|
remise: _appliedRemise, |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final montantOriginal = widget.commande.montantTotal; |
||||
|
final montantFinal = _appliedRemise != null |
||||
|
? montantOriginal - _appliedRemise!.calculerRemise(montantOriginal) |
||||
|
: montantOriginal; |
||||
|
final amount = double.tryParse(_amountController.text) ?? 0; |
||||
|
final change = amount - montantFinal; |
||||
|
|
||||
|
return AlertDialog( |
||||
|
title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)), |
||||
|
content: SingleChildScrollView( |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
// Résumé des montants |
||||
|
Container( |
||||
|
padding: const EdgeInsets.all(12), |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.blue.shade50, |
||||
|
borderRadius: BorderRadius.circular(8), |
||||
|
border: Border.all(color: Colors.blue.shade200), |
||||
|
), |
||||
|
child: Column( |
||||
|
children: [ |
||||
|
Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
const Text('Montant original:'), |
||||
|
Text('${montantOriginal.toStringAsFixed(0)} MGA'), |
||||
|
], |
||||
|
), |
||||
|
if (_appliedRemise != null) ...[ |
||||
|
const SizedBox(height: 4), |
||||
|
Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
Text('Remise (${_appliedRemise!.libelle}):'), |
||||
|
Text( |
||||
|
'- ${_appliedRemise!.calculerRemise(montantOriginal).toStringAsFixed(0)} MGA', |
||||
|
style: const TextStyle(color: Colors.red), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
const Divider(), |
||||
|
], |
||||
|
Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
const Text('Total à payer:', style: TextStyle(fontWeight: FontWeight.bold)), |
||||
|
Text('${montantFinal.toStringAsFixed(0)} MGA', |
||||
|
style: const TextStyle(fontWeight: FontWeight.bold)), |
||||
|
], |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
const SizedBox(height: 16), |
||||
|
|
||||
|
// Bouton remise |
||||
|
Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: OutlinedButton.icon( |
||||
|
onPressed: _appliedRemise == null ? _showDiscountDialog : _removeDiscount, |
||||
|
icon: Icon(_appliedRemise == null ? Icons.local_offer : Icons.close), |
||||
|
label: Text(_appliedRemise == null ? 'Ajouter remise' : 'Supprimer remise'), |
||||
|
style: OutlinedButton.styleFrom( |
||||
|
foregroundColor: _appliedRemise == null ? Colors.orange : Colors.red, |
||||
|
side: BorderSide( |
||||
|
color: _appliedRemise == null ? Colors.orange : Colors.red, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
const SizedBox(height: 16), |
||||
|
|
||||
|
// Section Paiement mobile |
||||
|
const Align( |
||||
|
alignment: Alignment.centerLeft, |
||||
|
child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)), |
||||
|
), |
||||
|
const SizedBox(height: 8), |
||||
|
Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: _buildMobileMoneyTile( |
||||
|
title: 'Mvola', |
||||
|
imagePath: 'assets/mvola.jpg', |
||||
|
value: PaymentType.mvola, |
||||
|
), |
||||
|
), |
||||
|
const SizedBox(width: 8), |
||||
|
Expanded( |
||||
|
child: _buildMobileMoneyTile( |
||||
|
title: 'Orange Money', |
||||
|
imagePath: 'assets/Orange_money.png', |
||||
|
value: PaymentType.orange, |
||||
|
), |
||||
|
), |
||||
|
const SizedBox(width: 8), |
||||
|
Expanded( |
||||
|
child: _buildMobileMoneyTile( |
||||
|
title: 'Airtel Money', |
||||
|
imagePath: 'assets/airtel_money.png', |
||||
|
value: PaymentType.airtel, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
const SizedBox(height: 16), |
||||
|
|
||||
|
// Section Carte bancaire |
||||
|
const Align( |
||||
|
alignment: Alignment.centerLeft, |
||||
|
child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)), |
||||
|
), |
||||
|
const SizedBox(height: 8), |
||||
|
_buildPaymentMethodTile( |
||||
|
title: 'Carte bancaire', |
||||
|
icon: Icons.credit_card, |
||||
|
value: PaymentType.card, |
||||
|
), |
||||
|
const SizedBox(height: 16), |
||||
|
|
||||
|
// Section Paiement en liquide |
||||
|
const Align( |
||||
|
alignment: Alignment.centerLeft, |
||||
|
child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)), |
||||
|
), |
||||
|
const SizedBox(height: 8), |
||||
|
_buildPaymentMethodTile( |
||||
|
title: 'Paiement en liquide', |
||||
|
icon: Icons.money, |
||||
|
value: PaymentType.cash, |
||||
|
), |
||||
|
if (_selectedPayment == PaymentType.cash) ...[ |
||||
|
const SizedBox(height: 12), |
||||
|
TextField( |
||||
|
controller: _amountController, |
||||
|
decoration: const InputDecoration( |
||||
|
labelText: 'Montant donné', |
||||
|
prefixText: 'MGA ', |
||||
|
border: OutlineInputBorder(), |
||||
|
), |
||||
|
keyboardType: TextInputType.numberWithOptions(decimal: true), |
||||
|
onChanged: (value) => setState(() {}), |
||||
|
), |
||||
|
const SizedBox(height: 8), |
||||
|
Text( |
||||
|
'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', |
||||
|
style: TextStyle( |
||||
|
fontSize: 16, |
||||
|
fontWeight: FontWeight.bold, |
||||
|
color: change >= 0 ? Colors.green : Colors.red, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
actions: [ |
||||
|
TextButton( |
||||
|
onPressed: () => Navigator.pop(context), |
||||
|
child: const Text('Annuler', style: TextStyle(color: Colors.grey)), |
||||
|
), |
||||
|
ElevatedButton( |
||||
|
style: ElevatedButton.styleFrom( |
||||
|
backgroundColor: Colors.blue.shade800, |
||||
|
foregroundColor: Colors.white, |
||||
|
), |
||||
|
onPressed: _validatePayment, |
||||
|
child: const Text('Confirmer'), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildMobileMoneyTile({ |
||||
|
required String title, |
||||
|
required String imagePath, |
||||
|
required PaymentType value, |
||||
|
}) { |
||||
|
return Card( |
||||
|
elevation: 2, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(8), |
||||
|
side: BorderSide( |
||||
|
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), |
||||
|
width: 2, |
||||
|
), |
||||
|
), |
||||
|
child: InkWell( |
||||
|
borderRadius: BorderRadius.circular(8), |
||||
|
onTap: () => setState(() => _selectedPayment = value), |
||||
|
child: Padding( |
||||
|
padding: const EdgeInsets.all(12), |
||||
|
child: Column( |
||||
|
children: [ |
||||
|
Image.asset( |
||||
|
imagePath, |
||||
|
height: 30, |
||||
|
width: 30, |
||||
|
fit: BoxFit.contain, |
||||
|
errorBuilder: (context, error, stackTrace) => |
||||
|
const Icon(Icons.mobile_friendly, size: 30), |
||||
|
), |
||||
|
const SizedBox(height: 8), |
||||
|
Text( |
||||
|
title, |
||||
|
textAlign: TextAlign.center, |
||||
|
style: const TextStyle(fontSize: 12), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildPaymentMethodTile({ |
||||
|
required String title, |
||||
|
required IconData icon, |
||||
|
required PaymentType value, |
||||
|
}) { |
||||
|
return Card( |
||||
|
elevation: 2, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(8), |
||||
|
side: BorderSide( |
||||
|
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), |
||||
|
width: 2, |
||||
|
), |
||||
|
), |
||||
|
child: InkWell( |
||||
|
borderRadius: BorderRadius.circular(8), |
||||
|
onTap: () => setState(() => _selectedPayment = value), |
||||
|
child: Padding( |
||||
|
padding: const EdgeInsets.all(12), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Icon(icon, size: 24), |
||||
|
const SizedBox(width: 12), |
||||
|
Text(title), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
import 'package:youmazgestion/Components/paymentType.dart'; |
||||
|
import 'package:youmazgestion/Models/produit.dart'; |
||||
|
|
||||
|
class Remise { |
||||
|
final RemiseType type; |
||||
|
final double valeur; |
||||
|
final String description; |
||||
|
|
||||
|
Remise({ |
||||
|
required this.type, |
||||
|
required this.valeur, |
||||
|
this.description = '', |
||||
|
}); |
||||
|
|
||||
|
double calculerRemise(double montantOriginal) { |
||||
|
switch (type) { |
||||
|
case RemiseType.pourcentage: |
||||
|
return montantOriginal * (valeur / 100); |
||||
|
case RemiseType.fixe: |
||||
|
return valeur; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
String get libelle { |
||||
|
switch (type) { |
||||
|
case RemiseType.pourcentage: |
||||
|
return '$valeur%'; |
||||
|
case RemiseType.fixe: |
||||
|
return '${valeur.toStringAsFixed(0)} MGA'; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
enum RemiseType { pourcentage, fixe } |
||||
|
|
||||
|
class ProduitCadeau { |
||||
|
final Product produit; |
||||
|
final String motif; |
||||
|
|
||||
|
ProduitCadeau({ |
||||
|
required this.produit, |
||||
|
this.motif = 'Cadeau client', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Modifiez votre classe PaymentMethod pour inclure la remise |
||||
|
class PaymentMethodEnhanced { |
||||
|
final PaymentType type; |
||||
|
final double amountGiven; |
||||
|
final Remise? remise; |
||||
|
|
||||
|
PaymentMethodEnhanced({ |
||||
|
required this.type, |
||||
|
this.amountGiven = 0, |
||||
|
this.remise, |
||||
|
}); |
||||
|
|
||||
|
double calculerMontantFinal(double montantOriginal) { |
||||
|
if (remise != null) { |
||||
|
return montantOriginal - remise!.calculerRemise(montantOriginal); |
||||
|
} |
||||
|
return montantOriginal; |
||||
|
} |
||||
|
} |
||||
@ -1,60 +0,0 @@ |
|||||
import 'dart:async'; |
|
||||
import 'package:path/path.dart'; |
|
||||
import 'package:sqflite/sqflite.dart'; |
|
||||
import '../Models/pointage_model.dart'; |
|
||||
|
|
||||
class DatabaseHelper { |
|
||||
static final DatabaseHelper _instance = DatabaseHelper._internal(); |
|
||||
|
|
||||
factory DatabaseHelper() => _instance; |
|
||||
|
|
||||
DatabaseHelper._internal(); |
|
||||
|
|
||||
Database? _db; |
|
||||
|
|
||||
Future<Database> get database async { |
|
||||
if (_db != null) return _db!; |
|
||||
_db = await _initDatabase(); |
|
||||
return _db!; |
|
||||
} |
|
||||
|
|
||||
Future<Database> _initDatabase() async { |
|
||||
String databasesPath = await getDatabasesPath(); |
|
||||
String dbPath = join(databasesPath, 'pointage.db'); |
|
||||
return await openDatabase(dbPath, version: 1, onCreate: _onCreate); |
|
||||
} |
|
||||
|
|
||||
Future _onCreate(Database db, int version) async { |
|
||||
await db.execute(''' |
|
||||
CREATE TABLE pointages ( |
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
||||
userName TEXT NOT NULL, |
|
||||
date TEXT NOT NULL, |
|
||||
heureArrivee TEXT NOT NULL, |
|
||||
heureDepart TEXT NOT NULL |
|
||||
) |
|
||||
'''); |
|
||||
} |
|
||||
|
|
||||
Future<int> insertPointage(Pointage pointage) async { |
|
||||
final db = await database; |
|
||||
return await db.insert('pointages', pointage.toMap()); |
|
||||
} |
|
||||
|
|
||||
Future<List<Pointage>> getPointages() async { |
|
||||
final db = await database; |
|
||||
final pointages = await db.query('pointages'); |
|
||||
return pointages.map((pointage) => Pointage.fromMap(pointage)).toList(); |
|
||||
} |
|
||||
|
|
||||
Future<int> updatePointage(Pointage pointage) async { |
|
||||
final db = await database; |
|
||||
return await db.update('pointages', pointage.toMap(), |
|
||||
where: 'id = ?', whereArgs: [pointage.id]); |
|
||||
} |
|
||||
|
|
||||
Future<int> deletePointage(int id) async { |
|
||||
final db = await database; |
|
||||
return await db.delete('pointages', where: 'id = ?', whereArgs: [id]); |
|
||||
} |
|
||||
} |
|
||||
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
@ -1,190 +0,0 @@ |
|||||
import 'package:flutter/material.dart'; |
|
||||
import 'package:youmazgestion/Services/pointageDatabase.dart'; |
|
||||
import 'package:youmazgestion/Models/pointage_model.dart'; |
|
||||
|
|
||||
class PointagePage extends StatefulWidget { |
|
||||
const PointagePage({Key? key}) : super(key: key); |
|
||||
|
|
||||
@override |
|
||||
State<PointagePage> createState() => _PointagePageState(); |
|
||||
} |
|
||||
|
|
||||
class _PointagePageState extends State<PointagePage> { |
|
||||
final DatabaseHelper _databaseHelper = DatabaseHelper(); |
|
||||
List<Pointage> _pointages = []; |
|
||||
|
|
||||
@override |
|
||||
void initState() { |
|
||||
super.initState(); |
|
||||
_loadPointages(); |
|
||||
} |
|
||||
|
|
||||
Future<void> _loadPointages() async { |
|
||||
final pointages = await _databaseHelper.getPointages(); |
|
||||
setState(() { |
|
||||
_pointages = pointages; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
Future<void> _showAddDialog() async { |
|
||||
final _arrivalController = TextEditingController(); |
|
||||
|
|
||||
await showDialog( |
|
||||
context: context, |
|
||||
builder: (context) { |
|
||||
return AlertDialog( |
|
||||
title: Text('Ajouter Pointage'), |
|
||||
content: TextField( |
|
||||
controller: _arrivalController, |
|
||||
decoration: InputDecoration( |
|
||||
labelText: 'Heure d\'arrivée (HH:mm)', |
|
||||
), |
|
||||
), |
|
||||
actions: [ |
|
||||
TextButton( |
|
||||
child: Text('Annuler'), |
|
||||
onPressed: () => Navigator.of(context).pop(), |
|
||||
), |
|
||||
ElevatedButton( |
|
||||
child: Text('Ajouter'), |
|
||||
onPressed: () async { |
|
||||
final pointage = Pointage( |
|
||||
userName: |
|
||||
"Nom de l'utilisateur", // fixed value, customize if needed |
|
||||
date: DateTime.now().toString().split(' ')[0], |
|
||||
heureArrivee: _arrivalController.text, |
|
||||
heureDepart: '', |
|
||||
); |
|
||||
await _databaseHelper.insertPointage(pointage); |
|
||||
Navigator.of(context).pop(); |
|
||||
_loadPointages(); |
|
||||
}, |
|
||||
), |
|
||||
], |
|
||||
); |
|
||||
}, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
void _scanQRCode({required bool isEntree}) { |
|
||||
// Ici tu peux intégrer ton scanner QR. |
|
||||
ScaffoldMessenger.of(context).showSnackBar( |
|
||||
SnackBar( |
|
||||
content: Text(isEntree ? "Scan QR pour Entrée" : "Scan QR pour Sortie"), |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
@override |
|
||||
Widget build(BuildContext context) { |
|
||||
return Scaffold( |
|
||||
appBar: AppBar( |
|
||||
title: const Text('Pointage'), |
|
||||
), |
|
||||
body: _pointages.isEmpty |
|
||||
? Center(child: Text('Aucun pointage enregistré.')) |
|
||||
: ListView.builder( |
|
||||
itemCount: _pointages.length, |
|
||||
itemBuilder: (context, index) { |
|
||||
final pointage = _pointages[index]; |
|
||||
return Padding( |
|
||||
padding: const EdgeInsets.symmetric( |
|
||||
horizontal: 12.0, vertical: 6.0), |
|
||||
child: Card( |
|
||||
shape: RoundedRectangleBorder( |
|
||||
borderRadius: BorderRadius.circular(16), |
|
||||
side: BorderSide(color: Colors.blueGrey.shade100), |
|
||||
), |
|
||||
elevation: 4, |
|
||||
shadowColor: Colors.blueGrey.shade50, |
|
||||
child: Padding( |
|
||||
padding: const EdgeInsets.symmetric( |
|
||||
horizontal: 8.0, vertical: 4), |
|
||||
child: Column( |
|
||||
crossAxisAlignment: CrossAxisAlignment.start, |
|
||||
children: [ |
|
||||
Row( |
|
||||
children: [ |
|
||||
CircleAvatar( |
|
||||
backgroundColor: Colors.blue.shade100, |
|
||||
child: Icon(Icons.person, color: Colors.blue), |
|
||||
), |
|
||||
const SizedBox(width: 10), |
|
||||
Expanded( |
|
||||
child: Text( |
|
||||
pointage |
|
||||
.userName, // suppose non-null (corrige si null possible) |
|
||||
style: TextStyle( |
|
||||
fontWeight: FontWeight.bold, |
|
||||
fontSize: 18), |
|
||||
), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
Divider(), |
|
||||
Text( |
|
||||
pointage.date, |
|
||||
style: const TextStyle( |
|
||||
color: Colors.black87, fontSize: 15), |
|
||||
), |
|
||||
Row( |
|
||||
children: [ |
|
||||
Icon(Icons.login, |
|
||||
size: 18, color: Colors.green.shade700), |
|
||||
const SizedBox(width: 6), |
|
||||
Text("Arrivée : ${pointage.heureArrivee}", |
|
||||
style: |
|
||||
TextStyle(color: Colors.green.shade700)), |
|
||||
], |
|
||||
), |
|
||||
Row( |
|
||||
children: [ |
|
||||
Icon(Icons.logout, |
|
||||
size: 18, color: Colors.red.shade700), |
|
||||
const SizedBox(width: 6), |
|
||||
Text( |
|
||||
"Départ : ${pointage.heureDepart.isNotEmpty ? pointage.heureDepart : "---"}", |
|
||||
style: TextStyle(color: Colors.red.shade700)), |
|
||||
], |
|
||||
), |
|
||||
const SizedBox(height: 6), |
|
||||
], |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
); |
|
||||
}, |
|
||||
), |
|
||||
floatingActionButton: Column( |
|
||||
mainAxisAlignment: MainAxisAlignment.end, |
|
||||
crossAxisAlignment: CrossAxisAlignment.end, |
|
||||
children: [ |
|
||||
FloatingActionButton.extended( |
|
||||
onPressed: () => _scanQRCode(isEntree: true), |
|
||||
label: Text('Entrée'), |
|
||||
icon: Icon(Icons.qr_code_scanner, color: Colors.green), |
|
||||
backgroundColor: Colors.white, |
|
||||
foregroundColor: Colors.green, |
|
||||
heroTag: 'btnEntree', |
|
||||
), |
|
||||
SizedBox(height: 12), |
|
||||
FloatingActionButton.extended( |
|
||||
onPressed: () => _scanQRCode(isEntree: false), |
|
||||
label: Text('Sortie'), |
|
||||
icon: Icon(Icons.qr_code_scanner, color: Colors.red), |
|
||||
backgroundColor: Colors.white, |
|
||||
foregroundColor: Colors.red, |
|
||||
heroTag: 'btnSortie', |
|
||||
), |
|
||||
SizedBox(height: 12), |
|
||||
FloatingActionButton( |
|
||||
onPressed: _showAddDialog, |
|
||||
tooltip: 'Ajouter Pointage', |
|
||||
child: const Icon(Icons.add), |
|
||||
heroTag: 'btnAdd', |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,64 @@ |
|||||
|
// Config/database_config.dart - Version améliorée |
||||
|
class DatabaseConfig { |
||||
|
static const String host = 'database.c4m.mg'; |
||||
|
static const int port = 3306; |
||||
|
static const String username = 'guycom'; |
||||
|
static const String password = '3iV59wjRdbuXAPR'; |
||||
|
static const String database = 'guycom'; |
||||
|
|
||||
|
static const String prodHost = 'database.c4m.mg'; |
||||
|
static const String prodUsername = 'guycom'; |
||||
|
static const String prodPassword = '3iV59wjRdbuXAPR'; |
||||
|
static const String prodDatabase = 'guycom'; |
||||
|
|
||||
|
static const Duration connectionTimeout = Duration(seconds: 30); |
||||
|
static const Duration queryTimeout = Duration(seconds: 15); |
||||
|
|
||||
|
static const int maxConnections = 10; |
||||
|
static const int minConnections = 2; |
||||
|
|
||||
|
static bool get isDevelopment => false; |
||||
|
|
||||
|
static Map<String, dynamic> getConfig() { |
||||
|
if (isDevelopment) { |
||||
|
return { |
||||
|
'host': host, |
||||
|
'port': port, |
||||
|
'user': username, |
||||
|
'password': password, |
||||
|
'database': database, |
||||
|
'timeout': connectionTimeout.inSeconds, |
||||
|
}; |
||||
|
} else { |
||||
|
return { |
||||
|
'host': prodHost, |
||||
|
'port': port, |
||||
|
'user': prodUsername, |
||||
|
'password': prodPassword, |
||||
|
'database': prodDatabase, |
||||
|
'timeout': connectionTimeout.inSeconds, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Validation de la configuration |
||||
|
static bool validateConfig() { |
||||
|
try { |
||||
|
final config = getConfig(); |
||||
|
return config['host']?.toString().isNotEmpty == true && |
||||
|
config['database']?.toString().isNotEmpty == true && |
||||
|
config['user'] != null; |
||||
|
} catch (e) { |
||||
|
print("Erreur de validation de la configuration: $e"); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Configuration avec retry automatique |
||||
|
static Map<String, dynamic> getConfigWithRetry() { |
||||
|
final config = getConfig(); |
||||
|
config['retryCount'] = 3; |
||||
|
config['retryDelay'] = 5000; // ms |
||||
|
return config; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue