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