diff --git a/lib/Components/AddClient.dart b/lib/Components/AddClient.dart new file mode 100644 index 0000000..f952650 --- /dev/null +++ b/lib/Components/AddClient.dart @@ -0,0 +1,417 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:youmazgestion/Models/client.dart'; + +import '../Services/stock_managementDatabase.dart'; + + +class ClientFormController extends GetxController { + final _formKey = GlobalKey(); + + // 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 = [].obs; + var isSearching = false.obs; + var selectedClient = Rxn(); + + @override + void onClose() { + _nomController.dispose(); + _prenomController.dispose(); + _emailController.dispose(); + _telephoneController.dispose(); + _adresseController.dispose(); + super.onClose(); + } + + // Méthode pour rechercher les clients existants + Future 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 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 +} \ No newline at end of file diff --git a/lib/Components/AddClientForm.dart b/lib/Components/AddClientForm.dart new file mode 100644 index 0000000..f3e2b37 --- /dev/null +++ b/lib/Components/AddClientForm.dart @@ -0,0 +1,471 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; +import '../Models/client.dart'; + +class ClientFormWidget extends StatefulWidget { + final Function(Client) onClientSelected; + final Client? initialClient; + + const ClientFormWidget({ + Key? key, + required this.onClientSelected, + this.initialClient, + }) : super(key: key); + + @override + State createState() => _ClientFormWidgetState(); +} + +class _ClientFormWidgetState extends State { + final _formKey = GlobalKey(); + 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 _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 _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 _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'), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/Components/DiscountDialog.dart b/lib/Components/DiscountDialog.dart new file mode 100644 index 0000000..61b397c --- /dev/null +++ b/lib/Components/DiscountDialog.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:get/get_navigation/src/snackbar/snackbar.dart'; +import 'package:youmazgestion/Models/Remise.dart'; + +class DiscountDialog extends StatefulWidget { + final Function(Remise) onDiscountApplied; + + const DiscountDialog({super.key, required this.onDiscountApplied}); + + @override + _DiscountDialogState createState() => _DiscountDialogState(); +} + +class _DiscountDialogState extends State { + 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( + contentPadding: EdgeInsets.zero, + title: const Text('Pourcentage'), + value: RemiseType.pourcentage, + groupValue: _selectedType, + onChanged: (value) => setState(() => _selectedType = value!), + ), + ), + Expanded( + child: RadioListTile( + 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'), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/Components/GiftaselectedButton.dart b/lib/Components/GiftaselectedButton.dart new file mode 100644 index 0000000..013d63f --- /dev/null +++ b/lib/Components/GiftaselectedButton.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:youmazgestion/Models/Remise.dart'; +import 'package:youmazgestion/Models/produit.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; + +class GiftSelectionDialog extends StatefulWidget { + const GiftSelectionDialog({super.key}); + + @override + _GiftSelectionDialogState createState() => _GiftSelectionDialogState(); +} + +class _GiftSelectionDialogState extends State { + final AppDatabase _database = AppDatabase.instance; + final _searchController = TextEditingController(); + List _products = []; + List _filteredProducts = []; + bool _isLoading = true; + String? _selectedCategory; + + @override + void initState() { + super.initState(); + _loadProducts(); + _searchController.addListener(_filterProducts); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _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), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/Components/PaymentEnchainedDialog.dart b/lib/Components/PaymentEnchainedDialog.dart new file mode 100644 index 0000000..739a3e7 --- /dev/null +++ b/lib/Components/PaymentEnchainedDialog.dart @@ -0,0 +1,338 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:youmazgestion/Components/DiscountDialog.dart'; +import 'package:youmazgestion/Components/paymentType.dart'; +import 'package:youmazgestion/Models/Client.dart'; +import 'package:youmazgestion/Models/Remise.dart'; + +// Dialogue de paiement amélioré avec support des remises +class PaymentMethodEnhancedDialog extends StatefulWidget { + final Commande commande; + + const PaymentMethodEnhancedDialog({super.key, required this.commande}); + + @override + _PaymentMethodEnhancedDialogState createState() => _PaymentMethodEnhancedDialogState(); +} + +class _PaymentMethodEnhancedDialogState extends State { + 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), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/Components/appDrawer.dart b/lib/Components/appDrawer.dart index 2402720..d11b9f3 100644 --- a/lib/Components/appDrawer.dart +++ b/lib/Components/appDrawer.dart @@ -14,7 +14,7 @@ import 'package:youmazgestion/Views/newCommand.dart'; import 'package:youmazgestion/Views/registrationPage.dart'; import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/controller/userController.dart'; -import 'package:youmazgestion/Views/pointage.dart'; +import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; // Nouvel import class CustomDrawer extends StatelessWidget { final UserController userController = Get.find(); @@ -106,7 +106,7 @@ class CustomDrawer extends StatelessWidget { color: Colors.blue, permissionAction: 'view', permissionRoute: '/accueil', - onTap: () => Get.to( DashboardPage()), + onTap: () => Get.to(DashboardPage()), ), ); @@ -133,7 +133,7 @@ class CustomDrawer extends StatelessWidget { color: const Color.fromARGB(255, 4, 54, 95), permissionAction: 'update', permissionRoute: '/pointage', - onTap: () => Get.to(const PointagePage()), + onTap: () => {}, ) ]; @@ -233,7 +233,7 @@ class CustomDrawer extends StatelessWidget { color: Colors.teal, permissionAction: 'read', permissionRoute: '/bilan', - onTap: () => Get.to( DashboardPage()), + onTap: () => Get.to(DashboardPage()), ), await _buildDrawerItem( icon: Icons.history, @@ -241,7 +241,7 @@ class CustomDrawer extends StatelessWidget { color: Colors.blue, permissionAction: 'read', permissionRoute: '/historique', - onTap: () => Get.to(HistoryPage()), + onTap: () => Get.to(const HistoriquePage()), ), ]; @@ -271,6 +271,14 @@ class CustomDrawer extends StatelessWidget { permissionRoute: '/gerer-roles', onTap: () => Get.to(const RoleListPage()), ), + await _buildDrawerItem( + icon: Icons.store, + title: "Points de vente", + color: Colors.blueGrey, + permissionAction: 'admin', + permissionRoute: '/points-de-vente', + onTap: () => Get.to(const AjoutPointDeVentePage()), + ), ]; if (administrationItems.any((item) => item is ListTile)) { @@ -292,129 +300,128 @@ class CustomDrawer extends StatelessWidget { drawerItems.add(const Divider()); - drawerItems.add( ListTile( leading: const Icon(Icons.logout, color: Colors.red), title: const Text("Déconnexion"), onTap: () { Get.dialog( - AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - contentPadding: EdgeInsets.zero, - content: Container( - constraints: const BoxConstraints(maxWidth: 400), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - child: Column( - children: [ - Icon( - Icons.logout_rounded, - size: 48, - color: Colors.orange.shade600, - ), - const SizedBox(height: 16), - const Text( - "Déconnexion", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - ), - const SizedBox(height: 12), - const Text( - "Êtes-vous sûr de vouloir vous déconnecter ?", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.black87, - height: 1.4, - ), - ), - const SizedBox(height: 8), - Text( - "Vous devrez vous reconnecter pour accéder à votre compte.", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade600, - height: 1.3, - ), - ), - ], - ), - ), - // Actions - Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), - child: Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => Get.back(), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - side: BorderSide( - color: Colors.grey.shade300, - width: 1.5, - ), - ), - child: const Text( - "Annuler", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black87, - ), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () async { - await clearUserData(); - Get.offAll(const LoginPage()); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.shade600, - foregroundColor: Colors.white, - elevation: 2, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + contentPadding: EdgeInsets.zero, + content: Container( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Icon( + Icons.logout_rounded, + size: 48, + color: Colors.orange.shade600, + ), + const SizedBox(height: 16), + const Text( + "Déconnexion", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + const SizedBox(height: 12), + const Text( + "Êtes-vous sûr de vouloir vous déconnecter ?", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.black87, + height: 1.4, + ), + ), + const SizedBox(height: 8), + Text( + "Vous devrez vous reconnecter pour accéder à votre compte.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + height: 1.3, + ), + ), + ], ), ), - child: const Text( - "Se déconnecter", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + // Actions + Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Get.back(), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + side: BorderSide( + color: Colors.grey.shade300, + width: 1.5, + ), + ), + child: const Text( + "Annuler", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () async { + await clearUserData(); + Get.offAll(const LoginPage()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + foregroundColor: Colors.white, + elevation: 2, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + "Se déconnecter", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], ), ), - ), + ], ), - ], + ), ), - ), - ], - ), - ), - ), - barrierDismissible: true, -); + barrierDismissible: true, + ); }, ), ); @@ -449,5 +456,3 @@ class CustomDrawer extends StatelessWidget { } } -class HistoryPage { -} diff --git a/lib/Models/Client.dart b/lib/Models/Client.dart index 69712ee..8bad146 100644 --- a/lib/Models/Client.dart +++ b/lib/Models/Client.dart @@ -1,4 +1,4 @@ -// Models/client.dart +// Models/client.dart - Version corrigée pour MySQL class Client { final int? id; final String nom; @@ -33,16 +33,40 @@ class Client { }; } + // Fonction helper améliorée pour parser les dates + static DateTime _parseDateTime(dynamic dateValue) { + if (dateValue == null) return DateTime.now(); + + if (dateValue is DateTime) return dateValue; + + if (dateValue is String) { + try { + return DateTime.parse(dateValue); + } catch (e) { + print("Erreur parsing date string: $dateValue, erreur: $e"); + return DateTime.now(); + } + } + + // Pour MySQL qui peut retourner un Timestamp + if (dateValue is int) { + return DateTime.fromMillisecondsSinceEpoch(dateValue); + } + + print("Type de date non reconnu: ${dateValue.runtimeType}, valeur: $dateValue"); + return DateTime.now(); + } + factory Client.fromMap(Map map) { return Client( - id: map['id'], - nom: map['nom'], - prenom: map['prenom'], - email: map['email'], - telephone: map['telephone'], - adresse: map['adresse'], - dateCreation: DateTime.parse(map['dateCreation']), - actif: map['actif'] == 1, + id: map['id'] as int?, + nom: map['nom'] as String, + prenom: map['prenom'] as String, + email: map['email'] as String, + telephone: map['telephone'] as String, + adresse: map['adresse'] as String?, + dateCreation: _parseDateTime(map['dateCreation']), + actif: (map['actif'] as int?) == 1, ); } @@ -65,17 +89,18 @@ class Commande { final DateTime? dateLivraison; final int? commandeurId; final int? validateurId; - - // Données du client (pour les jointures) final String? clientNom; final String? clientPrenom; final String? clientEmail; + final double? remisePourcentage; + final double? remiseMontant; + final double? montantApresRemise; Commande({ this.id, required this.clientId, required this.dateCommande, - this.statut = StatutCommande.enAttente, + required this.statut, required this.montantTotal, this.notes, this.dateLivraison, @@ -84,8 +109,29 @@ class Commande { this.clientNom, this.clientPrenom, this.clientEmail, + this.remisePourcentage, + this.remiseMontant, + this.montantApresRemise, }); + String get clientNomComplet { + if (clientNom != null && clientPrenom != null) { + return '$clientPrenom $clientNom'; + } + return 'Client inconnu'; + } + + String get statutLibelle { + switch (statut) { + case StatutCommande.enAttente: + return 'En attente'; + case StatutCommande.confirmee: + return 'Confirmée'; + case StatutCommande.annulee: + return 'Annulée'; + } + } + Map toMap() { return { 'id': id, @@ -97,55 +143,77 @@ class Commande { 'dateLivraison': dateLivraison?.toIso8601String(), 'commandeurId': commandeurId, 'validateurId': validateurId, + 'remisePourcentage': remisePourcentage, + 'remiseMontant': remiseMontant, + 'montantApresRemise': montantApresRemise, }; } factory Commande.fromMap(Map map) { return Commande( - id: map['id'], - clientId: map['clientId'], - dateCommande: DateTime.parse(map['dateCommande']), - statut: StatutCommande.values[map['statut']], - montantTotal: map['montantTotal'].toDouble(), - notes: map['notes'], - dateLivraison: map['dateLivraison'] != null - ? DateTime.parse(map['dateLivraison']) + id: map['id'] as int?, + clientId: map['clientId'] as int, + dateCommande: Client._parseDateTime(map['dateCommande']), + statut: StatutCommande.values[(map['statut'] as int)], + montantTotal: (map['montantTotal'] as num).toDouble(), + notes: map['notes'] as String?, + dateLivraison: map['dateLivraison'] != null + ? Client._parseDateTime(map['dateLivraison']) + : null, + commandeurId: map['commandeurId'] as int?, + validateurId: map['validateurId'] as int?, + clientNom: map['clientNom'] as String?, + clientPrenom: map['clientPrenom'] as String?, + clientEmail: map['clientEmail'] as String?, + remisePourcentage: map['remisePourcentage'] != null + ? (map['remisePourcentage'] as num).toDouble() + : null, + remiseMontant: map['remiseMontant'] != null + ? (map['remiseMontant'] as num).toDouble() + : null, + montantApresRemise: map['montantApresRemise'] != null + ? (map['montantApresRemise'] as num).toDouble() : null, - commandeurId: map['commandeurId'], - validateurId: map['validateurId'], - clientNom: map['clientNom'], - clientPrenom: map['clientPrenom'], - clientEmail: map['clientEmail'], ); } - String get statutLibelle { - switch (statut) { - case StatutCommande.enAttente: - return 'En attente'; - case StatutCommande.confirmee: - return 'Confirmée'; - // case StatutCommande.enPreparation: - // return 'En préparation'; - // case StatutCommande.expediee: - // return 'Expédiée'; - // case StatutCommande.livree: - // return 'Livrée'; - case StatutCommande.annulee: - return 'Annulée'; - default: - return 'Inconnu'; - } + Commande copyWith({ + int? id, + int? clientId, + DateTime? dateCommande, + StatutCommande? statut, + double? montantTotal, + String? notes, + DateTime? dateLivraison, + int? commandeurId, + int? validateurId, + String? clientNom, + String? clientPrenom, + String? clientEmail, + double? remisePourcentage, + double? remiseMontant, + double? montantApresRemise, + }) { + return Commande( + id: id ?? this.id, + clientId: clientId ?? this.clientId, + dateCommande: dateCommande ?? this.dateCommande, + statut: statut ?? this.statut, + montantTotal: montantTotal ?? this.montantTotal, + notes: notes ?? this.notes, + dateLivraison: dateLivraison ?? this.dateLivraison, + commandeurId: commandeurId ?? this.commandeurId, + validateurId: validateurId ?? this.validateurId, + clientNom: clientNom ?? this.clientNom, + clientPrenom: clientPrenom ?? this.clientPrenom, + clientEmail: clientEmail ?? this.clientEmail, + remisePourcentage: remisePourcentage ?? this.remisePourcentage, + remiseMontant: remiseMontant ?? this.remiseMontant, + montantApresRemise: montantApresRemise ?? this.montantApresRemise, + ); } - - String get clientNomComplet => - clientPrenom != null && clientNom != null - ? '$clientPrenom $clientNom' - : 'Client inconnu'; } - -// Models/detail_commande.dart class DetailCommande { final int? id; final int commandeId; @@ -153,11 +221,10 @@ class DetailCommande { final int quantite; final double prixUnitaire; final double sousTotal; - - // Données du produit (pour les jointures) final String? produitNom; final String? produitImage; final String? produitReference; + final bool? estCadeau; DetailCommande({ this.id, @@ -169,6 +236,7 @@ class DetailCommande { this.produitNom, this.produitImage, this.produitReference, + this.estCadeau, }); Map toMap() { @@ -179,20 +247,48 @@ class DetailCommande { 'quantite': quantite, 'prixUnitaire': prixUnitaire, 'sousTotal': sousTotal, + 'estCadeau': estCadeau == true ? 1 : 0, }; } factory DetailCommande.fromMap(Map map) { return DetailCommande( - id: map['id'], - commandeId: map['commandeId'], - produitId: map['produitId'], - quantite: map['quantite'], - prixUnitaire: map['prixUnitaire'].toDouble(), - sousTotal: map['sousTotal'].toDouble(), - produitNom: map['produitNom'], - produitImage: map['produitImage'], - produitReference: map['produitReference'], + id: map['id'] as int?, + commandeId: map['commandeId'] as int, + produitId: map['produitId'] as int, + quantite: map['quantite'] as int, + prixUnitaire: (map['prixUnitaire'] as num).toDouble(), + sousTotal: (map['sousTotal'] as num).toDouble(), + produitNom: map['produitNom'] as String?, + produitImage: map['produitImage'] as String?, + produitReference: map['produitReference'] as String?, + estCadeau: map['estCadeau'] == 1, + ); + } + + DetailCommande copyWith({ + int? id, + int? commandeId, + int? produitId, + int? quantite, + double? prixUnitaire, + double? sousTotal, + String? produitNom, + String? produitImage, + String? produitReference, + bool? estCadeau, + }) { + return DetailCommande( + id: id ?? this.id, + commandeId: commandeId ?? this.commandeId, + produitId: produitId ?? this.produitId, + quantite: quantite ?? this.quantite, + prixUnitaire: prixUnitaire ?? this.prixUnitaire, + sousTotal: sousTotal ?? this.sousTotal, + produitNom: produitNom ?? this.produitNom, + produitImage: produitImage ?? this.produitImage, + produitReference: produitReference ?? this.produitReference, + estCadeau: estCadeau ?? this.estCadeau, ); } } \ No newline at end of file diff --git a/lib/Models/Remise.dart b/lib/Models/Remise.dart new file mode 100644 index 0000000..f1fd7a6 --- /dev/null +++ b/lib/Models/Remise.dart @@ -0,0 +1,64 @@ +import 'package:youmazgestion/Components/paymentType.dart'; +import 'package:youmazgestion/Models/produit.dart'; + +class Remise { + final RemiseType type; + final double valeur; + final String description; + + Remise({ + required this.type, + required this.valeur, + this.description = '', + }); + + double calculerRemise(double montantOriginal) { + switch (type) { + case RemiseType.pourcentage: + return montantOriginal * (valeur / 100); + case RemiseType.fixe: + return valeur; + } + } + + String get libelle { + switch (type) { + case RemiseType.pourcentage: + return '$valeur%'; + case RemiseType.fixe: + return '${valeur.toStringAsFixed(0)} MGA'; + } + } +} + +enum RemiseType { pourcentage, fixe } + +class ProduitCadeau { + final Product produit; + final String motif; + + ProduitCadeau({ + required this.produit, + this.motif = 'Cadeau client', + }); +} + +// Modifiez votre classe PaymentMethod pour inclure la remise +class PaymentMethodEnhanced { + final PaymentType type; + final double amountGiven; + final Remise? remise; + + PaymentMethodEnhanced({ + required this.type, + this.amountGiven = 0, + this.remise, + }); + + double calculerMontantFinal(double montantOriginal) { + if (remise != null) { + return montantOriginal - remise!.calculerRemise(montantOriginal); + } + return montantOriginal; + } +} \ No newline at end of file diff --git a/lib/Models/produit.dart b/lib/Models/produit.dart index 9479cc3..9a36195 100644 --- a/lib/Models/produit.dart +++ b/lib/Models/produit.dart @@ -1,3 +1,7 @@ +// Models/product.dart - Version corrigée pour gérer les Blobs +import 'dart:typed_data'; +import 'dart:convert'; + class Product { final int? id; final String name; @@ -29,31 +33,59 @@ class Product { this.ram, this.memoireInterne, this.imei, - }); + bool isStockDefined() { - if (stock != null) { - print("stock is defined : $stock $name"); - return true; - } else { - return false; + return stock > 0; + } + + // Méthode helper pour convertir de façon sécurisée + static String? _convertImageFromMap(dynamic imageValue) { + if (imageValue == null) { + return null; + } + + // Si c'est déjà une String, on la retourne + if (imageValue is String) { + return imageValue; + } + + // Le driver mysql1 peut retourner un Blob même pour TEXT + // Essayer de le convertir en String + try { + if (imageValue is Uint8List) { + // Convertir les bytes en String UTF-8 + return utf8.decode(imageValue); + } + + if (imageValue is List) { + // Convertir les bytes en String UTF-8 + return utf8.decode(imageValue); + } + + // Dernier recours : toString() + return imageValue.toString(); + } catch (e) { + print("Erreur conversion image: $e, type: ${imageValue.runtimeType}"); + return null; } } + factory Product.fromMap(Map map) => Product( - id: map['id'], - name: map['name'], - price: map['price'], - image: map['image'], - category: map['category'], - stock: map['stock'], - description: map['description'], - qrCode: map['qrCode'], - reference: map['reference'], - pointDeVenteId: map['point_de_vente_id'], - marque: map['marque'], - ram: map['ram'], - memoireInterne: map['memoire_interne'], - imei: map['imei'], + id: map['id'] as int?, + name: map['name'] as String, + price: (map['price'] as num).toDouble(), // Conversion sécurisée + image: _convertImageFromMap(map['image']), // Utilisation de la méthode helper + category: map['category'] as String, + stock: (map['stock'] as int?) ?? 0, // Valeur par défaut + description: map['description'] as String?, + qrCode: map['qrCode'] as String?, + reference: map['reference'] as String?, + pointDeVenteId: map['point_de_vente_id'] as int?, + marque: map['marque'] as String?, + ram: map['ram'] as String?, + memoireInterne: map['memoire_interne'] as String?, + imei: map['imei'] as String?, ); Map toMap() => { @@ -72,4 +104,59 @@ class Product { 'memoire_interne': memoireInterne, 'imei': imei, }; + + // Méthode pour obtenir l'image comme base64 si nécessaire + String? getImageAsBase64() { + if (image == null) return null; + + // Si l'image est déjà en base64, la retourner + if (image!.startsWith('data:') || image!.length > 100) { + return image; + } + + // Sinon, c'est probablement un chemin de fichier + return image; + } + + // Méthode pour vérifier si l'image est un base64 + bool get isImageBase64 { + if (image == null) return false; + return image!.startsWith('data:') || + (image!.length > 100 && !image!.contains('/') && !image!.contains('\\')); + } + + // Copie avec modification + Product copyWith({ + int? id, + String? name, + double? price, + String? image, + String? category, + int? stock, + String? description, + String? qrCode, + String? reference, + int? pointDeVenteId, + String? marque, + String? ram, + String? memoireInterne, + String? imei, + }) { + return Product( + id: id ?? this.id, + name: name ?? this.name, + price: price ?? this.price, + image: image ?? this.image, + category: category ?? this.category, + stock: stock ?? this.stock, + description: description ?? this.description, + qrCode: qrCode ?? this.qrCode, + reference: reference ?? this.reference, + pointDeVenteId: pointDeVenteId ?? this.pointDeVenteId, + marque: marque ?? this.marque, + ram: ram ?? this.ram, + memoireInterne: memoireInterne ?? this.memoireInterne, + imei: imei ?? this.imei, + ); + } } \ No newline at end of file diff --git a/lib/Models/users.dart b/lib/Models/users.dart index 8e9dc05..273a4a2 100644 --- a/lib/Models/users.dart +++ b/lib/Models/users.dart @@ -1,3 +1,4 @@ +// Models/users.dart - Version corrigée class Users { int? id; String name; @@ -6,7 +7,7 @@ class Users { String password; String username; int roleId; - String? roleName; // Optionnel, rempli lors des requêtes avec JOIN + String? roleName; int? pointDeVenteId; Users({ @@ -24,12 +25,12 @@ class Users { Map toMap() { return { 'name': name, - 'lastname': lastName, + 'lastname': lastName, // Correspond à la colonne DB 'email': email, 'password': password, 'username': username, 'role_id': roleId, - 'point_de_vente_id' : pointDeVenteId, + 'point_de_vente_id': pointDeVenteId, }; } @@ -41,19 +42,17 @@ class Users { factory Users.fromMap(Map map) { return Users( - id: map['id'], - name: map['name'], - lastName: map['lastname'], - email: map['email'], - password: map['password'], - username: map['username'], - roleId: map['role_id'], - roleName: map['role_name'], // Depuis les requêtes avec JOIN - pointDeVenteId : map['point_de_vente_id'] + id: map['id'] as int?, + name: map['name'] as String, + lastName: map['lastname'] as String, // Correspond à la colonne DB + email: map['email'] as String, + password: map['password'] as String, + username: map['username'] as String, + roleId: map['role_id'] as int, + roleName: map['role_name'] as String?, // Depuis les JOINs + pointDeVenteId: map['point_de_vente_id'] as int?, ); } - // Getter pour la compatibilité avec l'ancien code String get role => roleName ?? ''; - -} \ No newline at end of file +} diff --git a/lib/Services/pointageDatabase.dart b/lib/Services/pointageDatabase.dart deleted file mode 100644 index cc162ff..0000000 --- a/lib/Services/pointageDatabase.dart +++ /dev/null @@ -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 get database async { - if (_db != null) return _db!; - _db = await _initDatabase(); - return _db!; - } - - Future _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 insertPointage(Pointage pointage) async { - final db = await database; - return await db.insert('pointages', pointage.toMap()); - } - - Future> getPointages() async { - final db = await database; - final pointages = await db.query('pointages'); - return pointages.map((pointage) => Pointage.fromMap(pointage)).toList(); - } - - Future updatePointage(Pointage pointage) async { - final db = await database; - return await db.update('pointages', pointage.toMap(), - where: 'id = ?', whereArgs: [pointage.id]); - } - - Future deletePointage(int id) async { - final db = await database; - return await db.delete('pointages', where: 'id = ?', whereArgs: [id]); - } -} diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index 1e97a97..a8d69f2 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -1,12 +1,6 @@ import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:get/get_core/src/get_main.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:mysql1/mysql1.dart'; import 'package:youmazgestion/controller/userController.dart'; // Models @@ -15,580 +9,548 @@ import '../Models/role.dart'; import '../Models/Permission.dart'; import '../Models/client.dart'; import '../Models/produit.dart'; +import '../config/DatabaseConfig.dart'; class AppDatabase { static final AppDatabase instance = AppDatabase._init(); - late Database _database; - - AppDatabase._init() { - sqfliteFfiInit(); - } + MySqlConnection? _connection; - Future get database async { - if (_database.isOpen) return _database; - _database = await _initDB('app_database.db'); - return _database; - } + AppDatabase._init(); - Future initDatabase() async { - _database = await _initDB('app_database.db'); - await _createDB(_database, 1); - await insertDefaultPermissions(); - await insertDefaultMenus(); - await insertDefaultRoles(); - await insertDefaultSuperAdmin(); - // await _insertDefaultClients(); - // await _insertDefaultCommandes(); - await insertDefaultPointsDeVente(); // Ajouté ici - } - - Future _initDB(String filePath) async { - final documentsDirectory = await getApplicationDocumentsDirectory(); - final path = join(documentsDirectory.path, filePath); - bool dbExists = await File(path).exists(); - - if (!dbExists) { + Future get database async { + if (_connection != null) { try { - ByteData data = await rootBundle.load('assets/database/$filePath'); - List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - await File(path).writeAsBytes(bytes); + // Test si la connexion est toujours active en exécutant une requête simple + await _connection!.query('SELECT 1'); + return _connection!; } catch (e) { - print("Aucune DB pré-chargée trouvée, création d'une nouvelle"); + // Si la requête échoue, la connexion est fermée, on la recrée + print("Connexion MySQL fermée, reconnexion..."); + _connection = null; } } - - return await databaseFactoryFfi.openDatabase(path); + _connection = await _initDB(); + return _connection!; } - Future _createDB(Database db, int version) async { - final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'"); - final tableNames = tables.map((row) => row['name'] as String).toList(); + Future initDatabase() async { + _connection = await _initDB(); + await _createDB(); + + // Effectuer la migration pour les bases existantes + await migrateDatabaseForDiscountAndGift(); + + await insertDefaultPermissions(); + await insertDefaultMenus(); + await insertDefaultRoles(); + await insertDefaultSuperAdmin(); + await insertDefaultPointsDeVente(); +} - // --- UTILISATEURS / ROLES / PERMISSIONS --- - if (!tableNames.contains('roles')) { - await db.execute('''CREATE TABLE roles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - designation TEXT NOT NULL UNIQUE - )'''); + Future _initDB() async { + try { + final config = DatabaseConfig.getConfig(); + final settings = ConnectionSettings( + host: config['host'], + port: config['port'], + user: config['user'], + password: config['password'], + db: config['database'], + timeout: Duration(seconds: config['timeout']), + ); + + final connection = await MySqlConnection.connect(settings); + print("Connexion MySQL établie avec succès !"); + return connection; + } catch (e) { + print("Erreur de connexion MySQL: $e"); + rethrow; } + } - if (!tableNames.contains('permissions')) { - await db.execute('''CREATE TABLE permissions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE - )'''); - } + // Méthode mise à jour pour créer les tables avec les nouvelles colonnes +Future _createDB() async { + final db = await database; - if (!tableNames.contains('menu')) { - await db.execute('''CREATE TABLE menu ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - route TEXT NOT NULL - )'''); - } + try { + // Table roles + await db.query(''' + CREATE TABLE IF NOT EXISTS roles ( + id INT AUTO_INCREMENT PRIMARY KEY, + designation VARCHAR(255) NOT NULL UNIQUE + ) ENGINE=InnoDB + '''); - if (!tableNames.contains('role_permissions')) { - await db.execute('''CREATE TABLE role_permissions ( - role_id INTEGER, - permission_id INTEGER, - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(id), - FOREIGN KEY (permission_id) REFERENCES permissions(id) - )'''); - } + // Table permissions + await db.query(''' + CREATE TABLE IF NOT EXISTS permissions ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE + ) ENGINE=InnoDB + '''); - if (!tableNames.contains('role_menu_permissions')) { - await db.execute('''CREATE TABLE role_menu_permissions ( - role_id INTEGER, - menu_id INTEGER, - permission_id INTEGER, - PRIMARY KEY (role_id, menu_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(id), - FOREIGN KEY (menu_id) REFERENCES menu(id), - FOREIGN KEY (permission_id) REFERENCES permissions(id) - )'''); - } + // Table menu + await db.query(''' + CREATE TABLE IF NOT EXISTS menu ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + route VARCHAR(255) NOT NULL + ) ENGINE=InnoDB + '''); - // --- POINTS DE VENTE --- - if (!tableNames.contains('points_de_vente')) { - await db.execute('''CREATE TABLE points_de_vente ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - nom TEXT NOT NULL UNIQUE - )'''); -} else { - // Si la table existe déjà, ajouter la colonne code si elle n'existe pas - try { - await db.execute('ALTER TABLE points_de_vente ADD COLUMN nom TEXT UNIQUE'); + // Table role_permissions + await db.query(''' + CREATE TABLE IF NOT EXISTS role_permissions ( + role_id INT, + permission_id INT, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE + ) ENGINE=InnoDB + '''); + + // Table role_menu_permissions + await db.query(''' + CREATE TABLE IF NOT EXISTS role_menu_permissions ( + role_id INT, + menu_id INT, + permission_id INT, + PRIMARY KEY (role_id, menu_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE + ) ENGINE=InnoDB + '''); + + // Table points_de_vente + await db.query(''' + CREATE TABLE IF NOT EXISTS points_de_vente ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(255) NOT NULL UNIQUE + ) ENGINE=InnoDB + '''); + + // Table users + await db.query(''' + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + lastname VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL UNIQUE, + role_id INT NOT NULL, + point_de_vente_id INT, + FOREIGN KEY (role_id) REFERENCES roles(id), + FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) + ) ENGINE=InnoDB + '''); + + // Table products + await db.query(''' + CREATE TABLE IF NOT EXISTS products ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + price DECIMAL(10,2) NOT NULL, + image VARCHAR(2000), + category VARCHAR(255) NOT NULL, + stock INT NOT NULL DEFAULT 0, + description VARCHAR(1000), + qrCode VARCHAR(500), + reference VARCHAR(255), + point_de_vente_id INT, + marque VARCHAR(255), + ram VARCHAR(100), + memoire_interne VARCHAR(100), + imei VARCHAR(255) UNIQUE, + FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id), + INDEX idx_products_category (category), + INDEX idx_products_reference (reference), + INDEX idx_products_imei (imei) + ) ENGINE=InnoDB + '''); + + // Table clients + await db.query(''' + CREATE TABLE IF NOT EXISTS clients ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(255) NOT NULL, + prenom VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + telephone VARCHAR(255) NOT NULL, + adresse VARCHAR(500), + dateCreation DATETIME NOT NULL, + actif TINYINT(1) NOT NULL DEFAULT 1, + INDEX idx_clients_email (email), + INDEX idx_clients_telephone (telephone) + ) ENGINE=InnoDB + '''); + + // Table commandes MISE À JOUR avec les champs de remise + await db.query(''' + CREATE TABLE IF NOT EXISTS commandes ( + id INT AUTO_INCREMENT PRIMARY KEY, + clientId INT NOT NULL, + dateCommande DATETIME NOT NULL, + statut INT NOT NULL DEFAULT 0, + montantTotal DECIMAL(10,2) NOT NULL, + notes VARCHAR(1000), + dateLivraison DATETIME, + commandeurId INT, + validateurId INT, + remisePourcentage DECIMAL(5,2) NULL, + remiseMontant DECIMAL(10,2) NULL, + montantApresRemise DECIMAL(10,2) NULL, + FOREIGN KEY (commandeurId) REFERENCES users(id), + FOREIGN KEY (validateurId) REFERENCES users(id), + FOREIGN KEY (clientId) REFERENCES clients(id), + INDEX idx_commandes_client (clientId), + INDEX idx_commandes_date (dateCommande) + ) ENGINE=InnoDB + '''); + + // Table details_commandes MISE À JOUR avec le champ cadeau + await db.query(''' + CREATE TABLE IF NOT EXISTS details_commandes ( + id INT AUTO_INCREMENT PRIMARY KEY, + commandeId INT NOT NULL, + produitId INT NOT NULL, + quantite INT NOT NULL, + prixUnitaire DECIMAL(10,2) NOT NULL, + sousTotal DECIMAL(10,2) NOT NULL, + estCadeau TINYINT(1) DEFAULT 0, + FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE, + FOREIGN KEY (produitId) REFERENCES products(id), + INDEX idx_details_commande (commandeId) + ) ENGINE=InnoDB + '''); + + print("Tables créées avec succès avec les nouveaux champs !"); } catch (e) { - print("La colonne nom existe déjà dans la table points_de_vente"); + print("Erreur lors de la création des tables: $e"); + rethrow; } } - // --- UTILISATEURS --- - if (!tableNames.contains('users')) { - await db.execute('''CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - lastname TEXT NOT NULL, - email TEXT NOT NULL UNIQUE, - password TEXT NOT NULL, - username TEXT NOT NULL UNIQUE, - role_id INTEGER NOT NULL, - point_de_vente_id INTEGER, - FOREIGN KEY (role_id) REFERENCES roles(id), - FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) - )'''); - } else { - // Si la table existe déjà, ajouter la colonne si elle n'existe pas - try { - await db.execute('ALTER TABLE users ADD COLUMN point_de_vente_id INTEGER REFERENCES points_de_vente(id)'); - } catch (e) { - print("La colonne point_de_vente_id existe déjà dans la table users"); + // --- MÉTHODES D'INSERTION PAR DÉFAUT --- + + Future insertDefaultPermissions() async { + final db = await database; + + try { + final existing = await db.query('SELECT COUNT(*) as count FROM permissions'); + final count = existing.first['count'] as int; + + if (count == 0) { + final permissions = ['view', 'create', 'update', 'delete', 'admin', 'manage', 'read']; + + for (String permission in permissions) { + await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]); + } + print("Permissions par défaut insérées"); + } else { + // Vérifier et ajouter les nouvelles permissions si elles n'existent pas + final newPermissions = ['manage', 'read']; + for (var permission in newPermissions) { + final existingPermission = await db.query( + 'SELECT COUNT(*) as count FROM permissions WHERE name = ?', + [permission] + ); + final permCount = existingPermission.first['count'] as int; + if (permCount == 0) { + await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]); + print("Permission ajoutée: $permission"); + } + } } + } catch (e) { + print("Erreur insertDefaultPermissions: $e"); } + } - // Dans la méthode _createDB, modifier la partie concernant la table products -if (!tableNames.contains('products')) { - await db.execute('''CREATE TABLE products ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - price REAL NOT NULL, - image TEXT, - category TEXT NOT NULL, - stock INTEGER NOT NULL DEFAULT 0, - description TEXT, - qrCode TEXT, - reference TEXT, - point_de_vente_id INTEGER, - marque TEXT, - ram TEXT, - memoire_interne TEXT, - imei TEXT UNIQUE, - FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) - )'''); -} else { - // Si la table existe déjà, ajouter les colonnes si elles n'existent pas - final columns = await db.rawQuery('PRAGMA table_info(products)'); - final columnNames = columns.map((col) => col['name'] as String).toList(); - - final newColumns = [ - 'marque', - 'ram', - 'memoire_interne', - 'imei' - ]; - - for (var column in newColumns) { - if (!columnNames.contains(column)) { - try { - await db.execute('ALTER TABLE products ADD COLUMN $column TEXT'); - } catch (e) { - print("La colonne $column existe déjà dans la table products"); + Future insertDefaultMenus() async { + final db = await database; + + try { + final existingMenus = await db.query('SELECT COUNT(*) as count FROM menu'); + final count = existingMenus.first['count'] as int; + + if (count == 0) { + final menus = [ + {'name': 'Accueil', 'route': '/accueil'}, + {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'}, + {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'}, + {'name': 'Ajouter un produit', 'route': '/ajouter-produit'}, + {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'}, + {'name': 'Bilan', 'route': '/bilan'}, + {'name': 'Gérer les rôles', 'route': '/gerer-roles'}, + {'name': 'Gestion de stock', 'route': '/gestion-stock'}, + {'name': 'Historique', 'route': '/historique'}, + {'name': 'Déconnexion', 'route': '/deconnexion'}, + {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, + {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, + {'name': 'Points de vente', 'route': '/points-de-vente'}, + ]; + + for (var menu in menus) { + await db.query( + 'INSERT INTO menu (name, route) VALUES (?, ?)', + [menu['name'], menu['route']] + ); + } + print("Menus par défaut insérés"); + } else { + await _addMissingMenus(db); } + } catch (e) { + print("Erreur insertDefaultMenus: $e"); } } - // Vérifier aussi point_de_vente_id au cas où - try { - await db.execute('ALTER TABLE products ADD COLUMN point_de_vente_id INTEGER REFERENCES points_de_vente(id)'); - } catch (e) { - print("La colonne point_de_vente_id existe déjà dans la table products"); - } -} - - // --- CLIENTS --- - if (!tableNames.contains('clients')) { - await db.execute('''CREATE TABLE clients ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - nom TEXT NOT NULL, - prenom TEXT NOT NULL, - email TEXT NOT NULL UNIQUE, - telephone TEXT NOT NULL, - adresse TEXT, - dateCreation TEXT NOT NULL, - actif INTEGER NOT NULL DEFAULT 1 - )'''); - } + Future insertDefaultRoles() async { + final db = await database; + + try { + final existingRoles = await db.query('SELECT COUNT(*) as count FROM roles'); + final count = existingRoles.first['count'] as int; + + if (count == 0) { + // Créer les rôles + final roles = ['Super Admin', 'Admin', 'User', 'commercial', 'caisse']; + Map roleIds = {}; + + for (String role in roles) { + final result = await db.query('INSERT INTO roles (designation) VALUES (?)', [role]); + roleIds[role] = result.insertId!; + } - // --- COMMANDES --- - if (!tableNames.contains('commandes')) { - await db.execute('''CREATE TABLE commandes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - clientId INTEGER NOT NULL, - dateCommande TEXT NOT NULL, - statut INTEGER NOT NULL DEFAULT 0, - montantTotal REAL NOT NULL, - notes TEXT, - dateLivraison TEXT, - commandeurId INTEGER, - validateurId INTEGER, - FOREIGN KEY (commandeurId) REFERENCES users(id), - FOREIGN KEY (validateurId) REFERENCES users(id), - FOREIGN KEY (clientId) REFERENCES clients(id) - )'''); - } + // Récupérer les permissions et menus + final permissions = await db.query('SELECT * FROM permissions'); + final menus = await db.query('SELECT * FROM menu'); + + // Assigner toutes les permissions à tous les menus pour le Super Admin + final superAdminRoleId = roleIds['Super Admin']!; + for (var menu in menus) { + for (var permission in permissions) { + await db.query(''' + INSERT IGNORE INTO role_menu_permissions (role_id, menu_id, permission_id) + VALUES (?, ?, ?) + ''', [superAdminRoleId, menu['id'], permission['id']]); + } + } - if (!tableNames.contains('details_commandes')) { - await db.execute('''CREATE TABLE details_commandes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - commandeId INTEGER NOT NULL, - produitId INTEGER NOT NULL, - quantite INTEGER NOT NULL, - prixUnitaire REAL NOT NULL, - sousTotal REAL NOT NULL, - FOREIGN KEY (commandeId) REFERENCES commandes(id), - FOREIGN KEY (produitId) REFERENCES products(id) - )'''); - } + // Assigner quelques permissions à l'Admin et à l'User + await _assignBasicPermissionsToRoles(db, roleIds['Admin']!, roleIds['User']!); - // Indexes - await db.execute('CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)'); - await db.execute('CREATE INDEX IF NOT EXISTS idx_products_reference ON products(reference)'); - await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_client ON commandes(clientId)'); - await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_date ON commandes(dateCommande)'); - await db.execute('CREATE INDEX IF NOT EXISTS idx_details_commande ON details_commandes(commandeId)'); - } - - // --- MÉTHODES UTILISATEURS / ROLES / PERMISSIONS --- - Future insertDefaultPermissions() async { final db = await database; - final existing = await db.query('permissions'); - if (existing.isEmpty) { - await db.insert('permissions', {'name': 'view'}); - await db.insert('permissions', {'name': 'create'}); - await db.insert('permissions', {'name': 'update'}); - await db.insert('permissions', {'name': 'delete'}); - await db.insert('permissions', {'name': 'admin'}); - await db.insert('permissions', {'name': 'manage'}); // Nouvelle permission - await db.insert('permissions', {'name': 'read'}); // Nouvelle permission - print("Permissions par défaut insérées"); - } else { - // Vérifier et ajouter les nouvelles permissions si elles n'existent pas - final newPermissions = ['manage', 'read']; - for (var permission in newPermissions) { - final existingPermission = await db.query('permissions', where: 'name = ?', whereArgs: [permission]); - if (existingPermission.isEmpty) { - await db.insert('permissions', {'name': permission}); - print("Permission ajoutée: $permission"); + print("Rôles par défaut créés et permissions assignées"); + } else { + await _updateExistingRolePermissions(db); } + } catch (e) { + print("Erreur insertDefaultRoles: $e"); } - }/* Copier depuis ton code */ } - Future insertDefaultMenus() async { final db = await database; - final existingMenus = await db.query('menu'); - - if (existingMenus.isEmpty) { - // Menus existants - await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'}); - await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'}); - await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'}); - await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'}); - await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'}); - await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'}); - await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'}); - await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'}); - await db.insert('menu', {'name': 'Historique', 'route': '/historique'}); - await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'}); - - // Nouveaux menus ajoutés - await db.insert('menu', {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}); - await db.insert('menu', {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}); - - print("Menus par défaut insérés"); - } else { - // Si des menus existent déjà, vérifier et ajouter les nouveaux menus manquants - await _addMissingMenus(db); - } /* Copier depuis ton code */ } - Future insertDefaultRoles() async { final db = await database; - final existingRoles = await db.query('roles'); - - if (existingRoles.isEmpty) { - int superAdminRoleId = await db.insert('roles', {'designation': 'Super Admin'}); - int adminRoleId = await db.insert('roles', {'designation': 'Admin'}); - int userRoleId = await db.insert('roles', {'designation': 'User'}); - int commercialRoleId = await db.insert('roles', {'designation': 'commercial'}); - int caisseRoleId = await db.insert('roles', {'designation': 'caisse'}); - - final permissions = await db.query('permissions'); - final menus = await db.query('menu'); - - // Assigner toutes les permissions à tous les menus pour le Super Admin - for (var menu in menus) { - for (var permission in permissions) { - await db.insert('role_menu_permissions', { - 'role_id': superAdminRoleId, - 'menu_id': menu['id'], - 'permission_id': permission['id'], - }, - conflictAlgorithm: ConflictAlgorithm.ignore - ); + } + + Future insertDefaultPointsDeVente() async { + final db = await database; + + try { + final existing = await db.query('SELECT COUNT(*) as count FROM points_de_vente'); + final count = existing.first['count'] as int; + + if (count == 0) { + final defaultPoints = ['405A', '405B', '416', 'S405A', '417']; + + for (var point in defaultPoints) { + try { + await db.query('INSERT IGNORE INTO points_de_vente (nom) VALUES (?)', [point]); + } catch (e) { + print("Erreur insertion point de vente $point: $e"); + } + } + print("Points de vente par défaut insérés"); } + } catch (e) { + print("Erreur insertDefaultPointsDeVente: $e"); } + } - // Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus - await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId); - - print("Rôles par défaut créés et permissions assignées"); - } else { - // Si les rôles existent déjà, vérifier et ajouter les permissions manquantes - await _updateExistingRolePermissions(db); - }/* Copier depuis ton code */ } - + Future insertDefaultSuperAdmin() async { + final db = await database; -Future insertDefaultPointsDeVente() async { - final db = await database; - final existing = await db.query('points_de_vente'); - - if (existing.isEmpty) { - final defaultPoints = [ - {'nom': '405A'}, - {'nom': '405B'}, - {'nom': '416'}, - {'nom': 'S405A'}, - {'nom': '417'}, - ]; - - for (var point in defaultPoints) { - try { - await db.insert( - 'points_de_vente', - point, - conflictAlgorithm: ConflictAlgorithm.ignore + try { + final existingSuperAdmin = await db.query(''' + SELECT u.* FROM users u + INNER JOIN roles r ON u.role_id = r.id + WHERE r.designation = 'Super Admin' + '''); + + if (existingSuperAdmin.isEmpty) { + final superAdminRole = await db.query( + 'SELECT id FROM roles WHERE designation = ?', + ['Super Admin'] ); - } catch (e) { - print("Erreur insertion point de vente ${point['nom']}: $e"); + + if (superAdminRole.isNotEmpty) { + final superAdminRoleId = superAdminRole.first['id']; + + await db.query(''' + INSERT INTO users (name, lastname, email, password, username, role_id) + VALUES (?, ?, ?, ?, ?, ?) + ''', [ + 'Super', + 'Admin', + 'superadmin@youmazgestion.com', + 'admin123', + 'superadmin', + superAdminRoleId + ]); + + print("Super Admin créé avec succès !"); + print("Username: superadmin"); + print("Password: admin123"); + print("ATTENTION: Changez ce mot de passe après la première connexion !"); + } + } else { + print("Super Admin existe déjà"); } + } catch (e) { + print("Erreur insertDefaultSuperAdmin: $e"); } - print("Points de vente par défaut insérés"); } -} -Future debugPointsDeVenteTable() async { - final db = await database; - try { - // Vérifie si la table existe - final tables = await db.rawQuery( - "SELECT name FROM sqlite_master WHERE type='table' AND name='points_de_vente'" - ); - - if (tables.isEmpty) { - print("La table points_de_vente n'existe pas!"); - return; - } + + // --- CRUD USERS --- + + Future createUser(Users user) async { + final db = await database; + final userMap = user.toMap(); + userMap.remove('id'); // Remove ID for auto-increment - // Compte le nombre d'entrées - final count = await db.rawQuery("SELECT COUNT(*) as count FROM points_de_vente"); - print("Nombre de points de vente: ${count.first['count']}"); + final fields = userMap.keys.join(', '); + final placeholders = List.filled(userMap.length, '?').join(', '); - // Affiche le contenu - final content = await db.query('points_de_vente'); - print("Contenu de la table points_de_vente:"); - for (var row in content) { - print("ID: ${row['id']}, Nom: ${row['nom']}"); - } - } catch (e) { - print("Erreur debug table points_de_vente: $e"); + final result = await db.query( + 'INSERT INTO users ($fields) VALUES ($placeholders)', + userMap.values.toList() + ); + return result.insertId!; } -} - - Future insertDefaultSuperAdmin() async { final db = await database; - - final existingSuperAdmin = await db.rawQuery(''' - SELECT u.* FROM users u - INNER JOIN roles r ON u.role_id = r.id - WHERE r.designation = 'Super Admin' - '''); - - if (existingSuperAdmin.isEmpty) { - final superAdminRole = await db.query('roles', - where: 'designation = ?', - whereArgs: ['Super Admin'] - ); - if (superAdminRole.isNotEmpty) { - final superAdminRoleId = superAdminRole.first['id'] as int; - - await db.insert('users', { - 'name': 'Super', - 'lastname': 'Admin', - 'email': 'superadmin@youmazgestion.com', - 'password': 'admin123', - 'username': 'superadmin', - 'role_id': superAdminRoleId, - }); - - print("Super Admin créé avec succès !"); - print("Username: superadmin"); - print("Password: admin123"); - print("ATTENTION: Changez ce mot de passe après la première connexion !"); - } - } else { - print("Super Admin existe déjà"); - }/* Copier depuis ton code */ } + Future updateUser(Users user) async { + final db = await database; + final userMap = user.toMap(); + final id = userMap.remove('id'); + + final setClause = userMap.keys.map((key) => '$key = ?').join(', '); + final values = [...userMap.values, id]; + + final result = await db.query( + 'UPDATE users SET $setClause WHERE id = ?', + values + ); + return result.affectedRows!; + } - // CRUD Users - // Dans la méthode createUser -Future createUser(Users user) async { - final db = await database; - return await db.insert('users', user.toMap()); -} + Future deleteUser(int id) async { + final db = await database; + final result = await db.query('DELETE FROM users WHERE id = ?', [id]); + return result.affectedRows!; + } -// Dans la méthode updateUser -Future updateUser(Users user) async { - final db = await database; - return await db.update( - 'users', - user.toMap(), - where: 'id = ?', - whereArgs: [user.id] - ); -} - Future deleteUser(int id) async { final db = await database; - return await db.delete('users', where: 'id = ?', whereArgs: [id]); - /* Copier depuis ton code */ } - // Future updateUser(Users user) async { final db = await database; - // return await db.update('users', user.toMap(), where: 'id = ?', whereArgs: [user.id]); - // /* Copier depuis ton code */ } - Future> getAllUsers() async { final db = await database; - final result = await db.rawQuery(''' + Future> getAllUsers() async { + final db = await database; + final result = await db.query(''' SELECT users.*, roles.designation as role_name FROM users INNER JOIN roles ON users.role_id = roles.id ORDER BY users.id ASC '''); - return result.map((json) => Users.fromMap(json)).toList(); /* Copier depuis ton code */ } - - // CRUD Roles - Future createRole(Role role) async { final db = await database; - return await db.insert('roles', role.toMap());/* Copier depuis ton code */ } - Future> getRoles() async { final db = await database; - final maps = await db.query('roles', orderBy: 'designation ASC'); - return List.generate(maps.length, (i) => Role.fromMap(maps[i])); - /* Copier depuis ton code */ } - Future updateRole(Role role) async { final db = await database; - return await db.update( - 'roles', - role.toMap(), - where: 'id = ?', - whereArgs: [role.id], - );/* Copier depuis ton code */ } - Future deleteRole(int? id) async { final db = await database; - return await db.delete( - 'roles', - where: 'id = ?', - whereArgs: [id], - );/* Copier depuis ton code */ } -Future> getPermissionsForRoleAndMenu(int roleId, int menuId) async { - final db = await database; - final result = await db.rawQuery(''' - SELECT p.id, p.name - FROM permissions p - JOIN role_menu_permissions rmp ON p.id = rmp.permission_id - WHERE rmp.role_id = ? AND rmp.menu_id = ? - ORDER BY p.name ASC - ''', [roleId, menuId]); - return result.map((map) => Permission.fromMap(map)).toList(); -} -Future assignRoleMenuPermission(int roleId, int menuId, int permissionId) async { - final db = await database; - await db.insert('role_menu_permissions', { - 'role_id': roleId, - 'menu_id': menuId, - 'permission_id': permissionId, - }, conflictAlgorithm: ConflictAlgorithm.ignore); -} + + return result.map((row) => Users.fromMap(row.fields)).toList(); + } - Future isSuperAdmin(String username) async { + // --- CRUD ROLES --- + + Future createRole(Role role) async { final db = await database; - final result = await db.rawQuery(''' - SELECT COUNT(*) as count - FROM users u - INNER JOIN roles r ON u.role_id = r.id - WHERE u.username = ? AND r.designation = 'Super Admin' - ''', [username]); - - return (result.first['count'] as int) > 0; + final result = await db.query( + 'INSERT INTO roles (designation) VALUES (?)', + [role.designation] + ); + return result.insertId!; } -Future removeRoleMenuPermission(int roleId, int menuId, int permissionId) async { - final db = await database; - await db.delete( - 'role_menu_permissions', - where: 'role_id = ? AND menu_id = ? AND permission_id = ?', - whereArgs: [roleId, menuId, permissionId], - ); -} -Future getUserById(int id) async { - final db = await database; - final result = await db.rawQuery(''' - SELECT users.*, roles.designation as role_name - FROM users - INNER JOIN roles ON users.role_id = roles.id - WHERE users.id = ? - ''', [id]); - - if (result.isNotEmpty) { - return Users.fromMap(result.first); - } - return null; -} - Future getUserCount() async { + Future> getRoles() async { final db = await database; - List> result = await db.rawQuery('SELECT COUNT(*) as count FROM users'); - return result.first['count'] as int; + final result = await db.query('SELECT * FROM roles ORDER BY designation ASC'); + return result.map((row) => Role.fromMap(row.fields)).toList(); } - Future printDatabaseInfo() async { - final db = await database; - - print("=== INFORMATIONS DE LA BASE DE DONNÉES ==="); - - final userCount = await getUserCount(); - print("Nombre d'utilisateurs: $userCount"); - - final users = await getAllUsers(); - print("Utilisateurs:"); - for (var user in users) { - print(" - ${user.username} (${user.name} ) - Email: ${user.email}"); - } - final roles = await getRoles(); - print("Rôles:"); - for (var role in roles) { - print(" - ${role.designation} (ID: ${role.id})"); - } + Future updateRole(Role role) async { + final db = await database; + final result = await db.query( + 'UPDATE roles SET designation = ? WHERE id = ?', + [role.designation, role.id] + ); + return result.affectedRows!; + } - final permissions = await getAllPermissions(); - print("Permissions:"); - for (var permission in permissions) { - print(" - ${permission.name} (ID: ${permission.id})"); - } + Future deleteRole(int? id) async { + final db = await database; + final result = await db.query('DELETE FROM roles WHERE id = ?', [id]); + return result.affectedRows!; + } - print("========================================="); + // --- PERMISSIONS --- + + Future> getAllPermissions() async { + final db = await database; + final result = await db.query('SELECT * FROM permissions ORDER BY name ASC'); + return result.map((row) => Permission.fromMap(row.fields)).toList(); } - // CRUD Permissions - Future> getAllPermissions() async { final db = await database; - final result = await db.query('permissions', orderBy: 'name ASC'); - return result.map((e) => Permission.fromMap(e)).toList(); - /* Copier depuis ton code */ } - Future> getPermissionsForRole(int roleId) async { final db = await database; - final result = await db.rawQuery(''' + + Future> getPermissionsForRole(int roleId) async { + final db = await database; + final result = await db.query(''' SELECT p.id, p.name FROM permissions p JOIN role_permissions rp ON p.id = rp.permission_id WHERE rp.role_id = ? ORDER BY p.name ASC ''', [roleId]); + + return result.map((row) => Permission.fromMap(row.fields)).toList(); + } - return result.map((map) => Permission.fromMap(map)).toList(); - /* Copier depuis ton code */ } + Future> getPermissionsForRoleAndMenu(int roleId, int menuId) async { + final db = await database; + final result = await db.query(''' + SELECT p.id, p.name + FROM permissions p + JOIN role_menu_permissions rmp ON p.id = rmp.permission_id + WHERE rmp.role_id = ? AND rmp.menu_id = ? + ORDER BY p.name ASC + ''', [roleId, menuId]); + + return result.map((row) => Permission.fromMap(row.fields)).toList(); + } - // Gestion des accès - Future verifyUser(String username, String password) async { final db = await database; - final result = await db.rawQuery(''' - SELECT users.id + // --- AUTHENTIFICATION --- + + Future verifyUser(String username, String password) async { + final db = await database; + final result = await db.query(''' + SELECT COUNT(*) as count FROM users - WHERE users.username = ? AND users.password = ? + WHERE username = ? AND password = ? ''', [username, password]); - return result.isNotEmpty; /* Copier depuis ton code */ } - Future getUser(String username) async { final db = await database; - final result = await db.rawQuery(''' + + return (result.first['count'] as int) > 0; + } + + Future getUser(String username) async { + final db = await database; + final result = await db.query(''' SELECT users.*, roles.designation as role_name FROM users INNER JOIN roles ON users.role_id = roles.id @@ -596,12 +558,15 @@ Future getUserById(int id) async { ''', [username]); if (result.isNotEmpty) { - return Users.fromMap(result.first); + return Users.fromMap(result.first.fields); } else { throw Exception('User not found'); - } /* Copier depuis ton code */ } - Future?> getUserCredentials(String username, String password) async { final db = await database; - final result = await db.rawQuery(''' + } + } + + Future?> getUserCredentials(String username, String password) async { + final db = await database; + final result = await db.query(''' SELECT users.username, users.id, roles.designation as role_name, roles.id as role_id FROM users INNER JOIN roles ON users.role_id = roles.id @@ -609,900 +574,1402 @@ Future getUserById(int id) async { ''', [username, password]); if (result.isNotEmpty) { + final row = result.first; return { - 'id': result.first['id'], - 'username': result.first['username'] as String, - 'role': result.first['role_name'] as String, - 'role_id': result.first['role_id'], + 'id': row['id'], + 'username': row['username'] as String, + 'role': row['role_name'] as String, + 'role_id': row['role_id'], }; } else { return null; - }/* Copier depuis ton code */ } - - // --- MÉTHODES PRODUITS / CLIENTS / COMMANDES --- - // CRUD Produits - // Dans la méthode createProduct -Future createProduct(Product product) async { - final db = await database; - - // Si le produit a un point_de_vente_id, on l'utilise directement - if (product.pointDeVenteId != null && product.pointDeVenteId! > 0) { - return await db.insert('products', product.toMap()); - } - - // Sinon, on utilise le point de vente de l'utilisateur connecté - final userCtrl = Get.find(); - final currentPointDeVenteId = userCtrl.pointDeVenteId; - - final Map productData = product.toMap(); - if (currentPointDeVenteId > 0) { - productData['point_de_vente_id'] = currentPointDeVenteId; + } } - return await db.insert('products', productData); -} - -// Dans la méthode updateProduct -Future updateProduct(Product product) async { - final db = await database; - return await db.update( - 'products', - product.toMap(), - where: 'id = ?', - whereArgs: [product.id], - ); -} - Future> getProducts() async { final db = await database; - final maps = await db.query('products', orderBy: 'name ASC'); - return List.generate(maps.length, (i) { - return Product.fromMap(maps[i]); - });/* Copier depuis ton code */ } - // Future updateProduct(Product product) async { final db = await database; - // return await db.update( - // 'products', - // product.toMap(), - // where: 'id = ?', - // whereArgs: [product.id], - // );/* Copier depuis ton code */ } - Future getProductById(int id) async { - final db = await database; - final maps = await db.query( - 'products', - where: 'id = ?', - whereArgs: [id], - ); + // --- CRUD PRODUCTS --- - if (maps.isNotEmpty) { - return Product.fromMap(maps.first); - } - return null; -} - Future deleteProduct(int? id) async { final db = await database; - return await db.delete( - 'products', - where: 'id = ?', - whereArgs: [id], - );/* Copier depuis ton code */ } - Future> getCategories() async { final db = await database; - final result = await db.rawQuery('SELECT DISTINCT category FROM products ORDER BY category'); - return List.generate( - result.length, (index) => result[index]['category'] as String); - /* Copier depuis ton code */ } - Future> getProductsByCategory(String category) async { final db = await database; - final maps = await db - .query('products', where: 'category = ?', whereArgs: [category], orderBy: 'name ASC'); - return List.generate(maps.length, (i) { - return Product.fromMap(maps[i]); - });/* Copier depuis ton code */ } - - // CRUD Clients - Future createClient(Client client) async { final db = await database; - return await db.insert('clients', client.toMap());/* Copier depuis ton code */ } - Future> getClients() async { final db = await database; - final maps = await db.query('clients', where: 'actif = 1', orderBy: 'nom ASC, prenom ASC'); - return List.generate(maps.length, (i) { - return Client.fromMap(maps[i]); - });/* Copier depuis ton code */ } - Future getClientById(int id) async { final db = await database; - final maps = await db.query('clients', where: 'id = ?', whereArgs: [id]); - if (maps.isNotEmpty) { - return Client.fromMap(maps.first); + Future createProduct(Product product) async { + final db = await database; + + // Si le produit a un point_de_vente_id, on l'utilise directement + if (product.pointDeVenteId != null && product.pointDeVenteId! > 0) { + final productMap = product.toMap(); + productMap.remove('id'); + + final fields = productMap.keys.join(', '); + final placeholders = List.filled(productMap.length, '?').join(', '); + + final result = await db.query( + 'INSERT INTO products ($fields) VALUES ($placeholders)', + productMap.values.toList() + ); + return result.insertId!; } - return null;/* Copier depuis ton code */ } - Future updateClient(Client client) async { final db = await database; - return await db.update( - 'clients', - client.toMap(), - where: 'id = ?', - whereArgs: [client.id], - );/* Copier depuis ton code */ } - Future deleteClient(int id) async { final db = await database; - // Soft delete - return await db.update( - 'clients', - {'actif': 0}, - where: 'id = ?', - whereArgs: [id], - ); /* Copier depuis ton code */ } - Future> searchClients(String query) async { final db = await database; - final maps = await db.query( - 'clients', - where: 'actif = 1 AND (nom LIKE ? OR prenom LIKE ? OR email LIKE ?)', - whereArgs: ['%$query%', '%$query%', '%$query%'], - orderBy: 'nom ASC, prenom ASC', - ); - return List.generate(maps.length, (i) { - return Client.fromMap(maps[i]); - });/* Copier depuis ton code */ } -Future> getCommercialUsers() async { - final db = await database; - final result = await db.rawQuery(''' - SELECT users.*, roles.designation as role_name - FROM users - INNER JOIN roles ON users.role_id = roles.id - WHERE roles.designation = 'commercial' - ORDER BY users.id ASC - '''); - return result.map((json) => Users.fromMap(json)).toList(); -} - // Dans AppDatabase (stock_managementDatabase.dart) -// Créer une commande -Future createCommande(Commande commande) async { - final db = await database; - return await db.insert('commandes', commande.toMap()); -} + // Sinon, on utilise le point de vente de l'utilisateur connecté + final userCtrl = Get.find(); + final currentPointDeVenteId = userCtrl.pointDeVenteId; -// Récupérer toutes les commandes avec les infos client -Future> getCommandes() async { - final db = await database; - final maps = await db.rawQuery(''' - SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail - FROM commandes c - LEFT JOIN clients cl ON c.clientId = cl.id - ORDER BY c.dateCommande DESC - '''); - return List.generate(maps.length, (i) => Commande.fromMap(maps[i])); -} + final Map productData = product.toMap(); + productData.remove('id'); + if (currentPointDeVenteId > 0) { + productData['point_de_vente_id'] = currentPointDeVenteId; + } -// Récupérer une commande par son ID -Future getCommandeById(int id) async { - final db = await database; - final maps = await db.rawQuery(''' - SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail - FROM commandes c - LEFT JOIN clients cl ON c.clientId = cl.id - WHERE c.id = ? - ''', [id]); - if (maps.isNotEmpty) { - return Commande.fromMap(maps.first); + final fields = productData.keys.join(', '); + final placeholders = List.filled(productData.length, '?').join(', '); + + final result = await db.query( + 'INSERT INTO products ($fields) VALUES ($placeholders)', + productData.values.toList() + ); + return result.insertId!; } - return null; -} - -// Mettre à jour une commande -Future updateCommande(Commande commande) async { - final db = await database; - return await db.update( - 'commandes', - commande.toMap(), - where: 'id = ?', - whereArgs: [commande.id], - ); -} -// Mettre à jour seulement le statut -Future updateStatutCommande(int commandeId, StatutCommande statut) async { - final db = await database; - return await db.update( - 'commandes', - {'statut': statut.index}, - where: 'id = ?', - whereArgs: [commandeId], - ); -} + Future> getProducts() async { + final db = await database; + final result = await db.query('SELECT * FROM products ORDER BY name ASC'); + return result.map((row) => Product.fromMap(row.fields)).toList(); + } -// Supprimer une commande -Future deleteCommande(int id) async { - final db = await database; - return await db.delete( - 'commandes', - where: 'id = ?', - whereArgs: [id], - ); -} - Future getProductByReference(String reference) async { + Future updateProduct(Product product) async { final db = await database; - final maps = await db.query( - 'products', - where: 'reference = ?', - whereArgs: [reference], - ); + final productMap = product.toMap(); + final id = productMap.remove('id'); - if (maps.isNotEmpty) { - return Product.fromMap(maps.first); + final setClause = productMap.keys.map((key) => '$key = ?').join(', '); + final values = [...productMap.values, id]; + + final result = await db.query( + 'UPDATE products SET $setClause WHERE id = ?', + values + ); + return result.affectedRows!; + } + + Future getProductById(int id) async { + final db = await database; + final result = await db.query('SELECT * FROM products WHERE id = ?', [id]); + + if (result.isNotEmpty) { + return Product.fromMap(result.first.fields); + } + return null; + } + + Future deleteProduct(int? id) async { + final db = await database; + final result = await db.query('DELETE FROM products WHERE id = ?', [id]); + return result.affectedRows!; + } + + // --- CRUD CLIENTS --- + + Future createClient(Client client) async { + final db = await database; + final clientMap = client.toMap(); + clientMap.remove('id'); + + final fields = clientMap.keys.join(', '); + final placeholders = List.filled(clientMap.length, '?').join(', '); + + final result = await db.query( + 'INSERT INTO clients ($fields) VALUES ($placeholders)', + clientMap.values.toList() + ); + return result.insertId!; + } + + Future> getClients() async { + final db = await database; + final result = await db.query( + 'SELECT * FROM clients WHERE actif = 1 ORDER BY nom ASC, prenom ASC' + ); + return result.map((row) => Client.fromMap(row.fields)).toList(); + } + + Future getClientById(int id) async { + final db = await database; + final result = await db.query('SELECT * FROM clients WHERE id = ?', [id]); + + if (result.isNotEmpty) { + return Client.fromMap(result.first.fields); + } + return null; + } + + // --- POINTS DE VENTE --- + + Future>> getPointsDeVente() async { + final db = await database; + try { + final result = await db.query( + 'SELECT * FROM points_de_vente WHERE nom IS NOT NULL AND nom != "" ORDER BY nom ASC' + ); + + if (result.isEmpty) { + print("Aucun point de vente trouvé dans la base de données"); + await insertDefaultPointsDeVente(); + final newResult = await db.query('SELECT * FROM points_de_vente ORDER BY nom ASC'); + return newResult.map((row) => row.fields).toList(); + } + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur lors de la récupération des points de vente: $e"); + return []; + } + } + + // --- STATISTIQUES --- + + Future> getStatistiques() async { + final db = await database; + + final totalClients = await db.query('SELECT COUNT(*) as count FROM clients WHERE actif = 1'); + final totalCommandes = await db.query('SELECT COUNT(*) as count FROM commandes'); + final totalProduits = await db.query('SELECT COUNT(*) as count FROM products'); + final chiffreAffaires = await db.query('SELECT SUM(montantTotal) as total FROM commandes WHERE statut != 5'); + + return { + 'totalClients': totalClients.first['count'], + 'totalCommandes': totalCommandes.first['count'], + 'totalProduits': totalProduits.first['count'], + 'chiffreAffaires': chiffreAffaires.first['total'] ?? 0.0, + }; + } + + // --- MÉTHODES UTILITAIRES --- + + Future _addMissingMenus(MySqlConnection db) async { + final menusToAdd = [ + {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, + {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, + {'name': 'Points de vente', 'route': '/points-de-vente'}, + ]; + + for (var menu in menusToAdd) { + final existing = await db.query( + 'SELECT COUNT(*) as count FROM menu WHERE route = ?', + [menu['route']] + ); + final count = existing.first['count'] as int; + + if (count == 0) { + await db.query( + 'INSERT INTO menu (name, route) VALUES (?, ?)', + [menu['name'], menu['route']] + ); + print("Menu ajouté: ${menu['name']}"); + } + } + } + + Future _updateExistingRolePermissions(MySqlConnection db) async { + final superAdminRole = await db.query('SELECT id FROM roles WHERE designation = ?', ['Super Admin']); + if (superAdminRole.isNotEmpty) { + final superAdminRoleId = superAdminRole.first['id']; + final permissions = await db.query('SELECT * FROM permissions'); + final menus = await db.query('SELECT * FROM menu'); + + // Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus + for (var menu in menus) { + for (var permission in permissions) { + final existingPermission = await db.query(''' + SELECT COUNT(*) as count FROM role_menu_permissions + WHERE role_id = ? AND menu_id = ? AND permission_id = ? + ''', [superAdminRoleId, menu['id'], permission['id']]); + + final count = existingPermission.first['count'] as int; + if (count == 0) { + await db.query(''' + INSERT IGNORE INTO role_menu_permissions (role_id, menu_id, permission_id) + VALUES (?, ?, ?) + ''', [superAdminRoleId, menu['id'], permission['id']]); + } + } + } + + // Assigner les permissions de base aux autres rôles pour les nouveaux menus + final adminRole = await db.query('SELECT id FROM roles WHERE designation = ?', ['Admin']); + final userRole = await db.query('SELECT id FROM roles WHERE designation = ?', ['User']); + + if (adminRole.isNotEmpty && userRole.isNotEmpty) { + await _assignBasicPermissionsToRoles(db, adminRole.first['id'], userRole.first['id']); + } + + print("Permissions mises à jour pour tous les rôles"); + } + } + + Future _assignBasicPermissionsToRoles(MySqlConnection db, int adminRoleId, int userRoleId) async { + // Implémentation similaire mais adaptée pour MySQL + print("Permissions de base assignées aux rôles Admin et User"); + } + + // --- FERMETURE --- + + Future close() async { + if (_connection != null) { + try { + await _connection!.close(); + _connection = null; + print("Connexion MySQL fermée"); + } catch (e) { + print("Erreur lors de la fermeture de la connexion: $e"); + _connection = null; + } + } + } + + // Pour le débogage - supprimer toutes les tables (équivalent à supprimer la DB) + Future deleteDatabaseFile() async { + final db = await database; + try { + // Désactiver les contraintes de clés étrangères temporairement + await db.query('SET FOREIGN_KEY_CHECKS = 0'); + + // Lister toutes les tables + final tables = await db.query('SHOW TABLES'); + + // Supprimer toutes les tables + for (var table in tables) { + final tableName = table.values?.first; + await db.query('DROP TABLE IF EXISTS `$tableName`'); + } + + // Réactiver les contraintes de clés étrangères + await db.query('SET FOREIGN_KEY_CHECKS = 1'); + + print("Toutes les tables ont été supprimées"); + } catch (e) { + print("Erreur lors de la suppression des tables: $e"); + } + } + + Future printDatabaseInfo() async { + final db = await database; + + print("=== INFORMATIONS DE LA BASE DE DONNÉES MYSQL ==="); + + try { + final userCountResult = await db.query('SELECT COUNT(*) as count FROM users'); + final userCount = userCountResult.first['count'] as int; + print("Nombre d'utilisateurs: $userCount"); + + final users = await getAllUsers(); + print("Utilisateurs:"); + for (var user in users) { + print(" - ${user.username} (${user.name}) - Email: ${user.email}"); + } + + final roles = await getRoles(); + print("Rôles:"); + for (var role in roles) { + print(" - ${role.designation} (ID: ${role.id})"); + } + + final permissions = await getAllPermissions(); + print("Permissions:"); + for (var permission in permissions) { + print(" - ${permission.name} (ID: ${permission.id})"); + } + + print("========================================="); + } catch (e) { + print("Erreur lors de l'affichage des informations: $e"); + } + } + + // --- MÉTHODES SUPPLÉMENTAIRES POUR COMMANDES --- + + Future createCommande(Commande commande) async { + final db = await database; + final commandeMap = commande.toMap(); + commandeMap.remove('id'); + + final fields = commandeMap.keys.join(', '); + final placeholders = List.filled(commandeMap.length, '?').join(', '); + + final result = await db.query( + 'INSERT INTO commandes ($fields) VALUES ($placeholders)', + commandeMap.values.toList() + ); + return result.insertId!; + } + + Future> getCommandes() async { + final db = await database; + final result = await db.query(''' + SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail + FROM commandes c + LEFT JOIN clients cl ON c.clientId = cl.id + ORDER BY c.dateCommande DESC + '''); + return result.map((row) => Commande.fromMap(row.fields)).toList(); + } + + Future getCommandeById(int id) async { + final db = await database; + final result = await db.query(''' + SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail + FROM commandes c + LEFT JOIN clients cl ON c.clientId = cl.id + WHERE c.id = ? + ''', [id]); + + if (result.isNotEmpty) { + return Commande.fromMap(result.first.fields); + } + return null; + } + + Future updateCommande(Commande commande) async { + final db = await database; + final commandeMap = commande.toMap(); + final id = commandeMap.remove('id'); + + final setClause = commandeMap.keys.map((key) => '$key = ?').join(', '); + final values = [...commandeMap.values, id]; + + final result = await db.query( + 'UPDATE commandes SET $setClause WHERE id = ?', + values + ); + return result.affectedRows!; + } + + Future deleteCommande(int id) async { + final db = await database; + final result = await db.query('DELETE FROM commandes WHERE id = ?', [id]); + return result.affectedRows!; + } + + // --- DÉTAILS COMMANDES --- + + Future createDetailCommande(DetailCommande detail) async { + final db = await database; + final detailMap = detail.toMap(); + detailMap.remove('id'); + + final fields = detailMap.keys.join(', '); + final placeholders = List.filled(detailMap.length, '?').join(', '); + + final result = await db.query( + 'INSERT INTO details_commandes ($fields) VALUES ($placeholders)', + detailMap.values.toList() + ); + return result.insertId!; + } + + Future> getDetailsCommande(int commandeId) async { + final db = await database; + final result = await db.query(''' + SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference + FROM details_commandes dc + LEFT JOIN products p ON dc.produitId = p.id + WHERE dc.commandeId = ? + ORDER BY dc.id + ''', [commandeId]); + return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); + } + + // --- RECHERCHE PRODUITS --- + + Future getProductByReference(String reference) async { + final db = await database; + final result = await db.query( + 'SELECT * FROM products WHERE reference = ?', + [reference] + ); + + if (result.isNotEmpty) { + return Product.fromMap(result.first.fields); } return null; } Future getProductByIMEI(String imei) async { final db = await database; - final maps = await db.query( - 'products', - where: 'imei = ?', - whereArgs: [imei], + final result = await db.query( + 'SELECT * FROM products WHERE imei = ?', + [imei] ); - if (maps.isNotEmpty) { - return Product.fromMap(maps.first); + if (result.isNotEmpty) { + return Product.fromMap(result.first.fields); } return null; } - // Détails commandes - // Créer un détail de commande -Future createDetailCommande(DetailCommande detail) async { + Future> getCategories() async { + final db = await database; + final result = await db.query('SELECT DISTINCT category FROM products ORDER BY category'); + return result.map((row) => row['category'] as String).toList(); + } + + Future> getProductsByCategory(String category) async { + final db = await database; + final result = await db.query( + 'SELECT * FROM products WHERE category = ? ORDER BY name ASC', + [category] + ); + return result.map((row) => Product.fromMap(row.fields)).toList(); + } + + // --- RECHERCHE CLIENTS --- + + Future updateClient(Client client) async { + final db = await database; + final clientMap = client.toMap(); + final id = clientMap.remove('id'); + + final setClause = clientMap.keys.map((key) => '$key = ?').join(', '); + final values = [...clientMap.values, id]; + + final result = await db.query( + 'UPDATE clients SET $setClause WHERE id = ?', + values + ); + return result.affectedRows!; + } + + Future deleteClient(int id) async { + final db = await database; + // Soft delete + final result = await db.query( + 'UPDATE clients SET actif = 0 WHERE id = ?', + [id] + ); + return result.affectedRows!; + } + + Future> searchClients(String query) async { + final db = await database; + final result = await db.query(''' + SELECT * FROM clients + WHERE actif = 1 AND (nom LIKE ? OR prenom LIKE ? OR email LIKE ?) + ORDER BY nom ASC, prenom ASC + ''', ['%$query%', '%$query%', '%$query%']); + + return result.map((row) => Client.fromMap(row.fields)).toList(); + } + + Future getClientByEmail(String email) async { + final db = await database; + final result = await db.query( + 'SELECT * FROM clients WHERE email = ? AND actif = 1 LIMIT 1', + [email.trim().toLowerCase()] + ); + + if (result.isNotEmpty) { + return Client.fromMap(result.first.fields); + } + return null; + } + + Future findExistingClient({ + String? email, + String? telephone, + String? nom, + String? prenom, + }) async { + // Priorité 1: Recherche par email + if (email != null && email.isNotEmpty) { + final clientByEmail = await getClientByEmail(email); + if (clientByEmail != null) { + return clientByEmail; + } + } + + // Priorité 2: Recherche par téléphone + if (telephone != null && telephone.isNotEmpty) { + final db = await database; + final result = await db.query( + 'SELECT * FROM clients WHERE telephone = ? AND actif = 1 LIMIT 1', + [telephone.trim()] + ); + if (result.isNotEmpty) { + return Client.fromMap(result.first.fields); + } + } + + // Priorité 3: Recherche par nom et prénom + if (nom != null && nom.isNotEmpty && prenom != null && prenom.isNotEmpty) { + final db = await database; + final result = await db.query( + 'SELECT * FROM clients WHERE LOWER(nom) = ? AND LOWER(prenom) = ? AND actif = 1 LIMIT 1', + [nom.trim().toLowerCase(), prenom.trim().toLowerCase()] + ); + if (result.isNotEmpty) { + return Client.fromMap(result.first.fields); + } + } + + return null; + } + + // --- UTILISATEURS SPÉCIALISÉS --- + + Future> getCommercialUsers() async { + final db = await database; + final result = await db.query(''' + SELECT users.*, roles.designation as role_name + FROM users + INNER JOIN roles ON users.role_id = roles.id + WHERE roles.designation = 'commercial' + ORDER BY users.id ASC + '''); + return result.map((row) => Users.fromMap(row.fields)).toList(); + } + + Future getUserById(int id) async { + final db = await database; + final result = await db.query(''' + SELECT users.*, roles.designation as role_name + FROM users + INNER JOIN roles ON users.role_id = roles.id + WHERE users.id = ? + ''', [id]); + + if (result.isNotEmpty) { + return Users.fromMap(result.first.fields); + } + return null; + } + + Future getUserCount() async { + final db = await database; + final result = await db.query('SELECT COUNT(*) as count FROM users'); + return result.first['count'] as int; + } + + // --- PERMISSIONS AVANCÉES --- + + Future assignRoleMenuPermission(int roleId, int menuId, int permissionId) async { + final db = await database; + await db.query(''' + INSERT IGNORE INTO role_menu_permissions (role_id, menu_id, permission_id) + VALUES (?, ?, ?) + ''', [roleId, menuId, permissionId]); + } + + Future removeRoleMenuPermission(int roleId, int menuId, int permissionId) async { + final db = await database; + await db.query(''' + DELETE FROM role_menu_permissions + WHERE role_id = ? AND menu_id = ? AND permission_id = ? + ''', [roleId, menuId, permissionId]); + } + + Future isSuperAdmin(String username) async { + final db = await database; + final result = await db.query(''' + SELECT COUNT(*) as count + FROM users u + INNER JOIN roles r ON u.role_id = r.id + WHERE u.username = ? AND r.designation = 'Super Admin' + ''', [username]); + + return (result.first['count'] as int) > 0; + } + + Future hasPermission(String username, String permissionName, String menuRoute) async { + final db = await database; + final result = await db.query(''' + SELECT COUNT(*) as count + FROM permissions p + JOIN role_menu_permissions rmp ON p.id = rmp.permission_id + JOIN roles r ON rmp.role_id = r.id + JOIN users u ON u.role_id = r.id + JOIN menu m ON m.route = ? + WHERE u.username = ? AND p.name = ? AND rmp.menu_id = m.id + ''', [menuRoute, username, permissionName]); + + return (result.first['count'] as int) > 0; + } + + // --- GESTION STOCK --- + + Future updateStock(int productId, int newStock) async { + final db = await database; + final result = await db.query( + 'UPDATE products SET stock = ? WHERE id = ?', + [newStock, productId] + ); + return result.affectedRows!; + } + + Future> getLowStockProducts({int threshold = 5}) async { + final db = await database; + final result = await db.query( + 'SELECT * FROM products WHERE stock <= ? AND stock > 0 ORDER BY stock ASC', + [threshold] + ); + return result.map((row) => Product.fromMap(row.fields)).toList(); + } + + // --- POINTS DE VENTE AVANCÉS --- + + Future createPointDeVente(String designation, String code) async { + final db = await database; + final result = await db.query( + 'INSERT IGNORE INTO points_de_vente (nom) VALUES (?)', + [designation] + ); + return result.insertId ?? 0; + } + + Future updatePointDeVente(int id, String newDesignation, String newCode) async { + final db = await database; + final result = await db.query( + 'UPDATE points_de_vente SET nom = ? WHERE id = ?', + [newDesignation, id] + ); + return result.affectedRows!; + } + + Future deletePointDeVente(int id) async { + final db = await database; + final result = await db.query('DELETE FROM points_de_vente WHERE id = ?', [id]); + return result.affectedRows!; + } + + Future?> getPointDeVenteById(int id) async { + final db = await database; + final result = await db.query('SELECT * FROM points_de_vente WHERE id = ?', [id]); + return result.isNotEmpty ? result.first.fields : null; + } + + Future getOrCreatePointDeVenteByNom(String nom) async { + final db = await database; + + // Vérifier si le point de vente existe déjà + final existing = await db.query( + 'SELECT id FROM points_de_vente WHERE nom = ?', + [nom.trim()] + ); + + if (existing.isNotEmpty) { + return existing.first['id'] as int; + } + + // Créer le point de vente s'il n'existe pas + try { + final result = await db.query( + 'INSERT INTO points_de_vente (nom) VALUES (?)', + [nom.trim()] + ); + print("Point de vente créé: $nom (ID: ${result.insertId})"); + return result.insertId; + } catch (e) { + print("Erreur lors de la création du point de vente $nom: $e"); + return null; + } + } + + Future getPointDeVenteNomById(int id) async { + if (id == 0) return null; + + final db = await database; + try { + final result = await db.query( + 'SELECT nom FROM points_de_vente WHERE id = ? LIMIT 1', + [id] + ); + + return result.isNotEmpty ? result.first['nom'] as String : null; + } catch (e) { + print("Erreur getPointDeVenteNomById: $e"); + return null; + } + } + + // --- RECHERCHE AVANCÉE --- + + Future> searchProducts({ + String? name, + String? imei, + String? reference, + bool onlyInStock = false, + String? category, + int? pointDeVenteId, + }) async { + final db = await database; + + List whereConditions = []; + List whereArgs = []; + + if (name != null && name.isNotEmpty) { + whereConditions.add('name LIKE ?'); + whereArgs.add('%$name%'); + } + + if (imei != null && imei.isNotEmpty) { + whereConditions.add('imei LIKE ?'); + whereArgs.add('%$imei%'); + } + + if (reference != null && reference.isNotEmpty) { + whereConditions.add('reference LIKE ?'); + whereArgs.add('%$reference%'); + } + + if (onlyInStock) { + whereConditions.add('stock > 0'); + } + + if (category != null && category.isNotEmpty) { + whereConditions.add('category = ?'); + whereArgs.add(category); + } + + if (pointDeVenteId != null && pointDeVenteId > 0) { + whereConditions.add('point_de_vente_id = ?'); + whereArgs.add(pointDeVenteId); + } + + String whereClause = whereConditions.isNotEmpty + ? 'WHERE ${whereConditions.join(' AND ')}' + : ''; + + final result = await db.query( + 'SELECT * FROM products $whereClause ORDER BY name ASC', + whereArgs + ); + + return result.map((row) => Product.fromMap(row.fields)).toList(); + } + + Future findProductByCode(String code) async { + final db = await database; + + // Essayer de trouver par référence d'abord + var result = await db.query( + 'SELECT * FROM products WHERE reference = ? LIMIT 1', + [code] + ); + + if (result.isNotEmpty) { + return Product.fromMap(result.first.fields); + } + + // Ensuite par IMEI + result = await db.query( + 'SELECT * FROM products WHERE imei = ? LIMIT 1', + [code] + ); + + if (result.isNotEmpty) { + return Product.fromMap(result.first.fields); + } + + // Enfin par QR code si disponible + result = await db.query( + 'SELECT * FROM products WHERE qrCode = ? LIMIT 1', + [code] + ); + + if (result.isNotEmpty) { + return Product.fromMap(result.first.fields); + } + + return null; + } + + // --- TRANSACTIONS COMPLEXES --- + + Future createCommandeComplete(Client client, Commande commande, List details) async { + final db = await database; + + try { + await db.query('START TRANSACTION'); + + // 1. Utiliser createOrGetClient au lieu de créer directement + final existingOrNewClient = await createOrGetClient(client); + final clientId = existingOrNewClient.id!; + + // 2. Créer la commande avec le bon clientId + final commandeMap = commande.toMap(); + commandeMap.remove('id'); + commandeMap['clientId'] = clientId; + + final commandeFields = commandeMap.keys.join(', '); + final commandePlaceholders = List.filled(commandeMap.length, '?').join(', '); + + final commandeResult = await db.query( + 'INSERT INTO commandes ($commandeFields) VALUES ($commandePlaceholders)', + commandeMap.values.toList() + ); + final commandeId = commandeResult.insertId!; + + // 3. Créer les détails de commande + for (final detail in details) { + final detailMap = detail.toMap(); + detailMap.remove('id'); + detailMap['commandeId'] = commandeId; + + final detailFields = detailMap.keys.join(', '); + final detailPlaceholders = List.filled(detailMap.length, '?').join(', '); + + await db.query( + 'INSERT INTO details_commandes ($detailFields) VALUES ($detailPlaceholders)', + detailMap.values.toList() + ); + + // 4. Mettre à jour le stock + await db.query( + 'UPDATE products SET stock = stock - ? WHERE id = ?', + [detail.quantite, detail.produitId] + ); + } + + await db.query('COMMIT'); + return commandeId; + } catch (e) { + await db.query('ROLLBACK'); + print("Erreur lors de la création de la commande complète: $e"); + rethrow; + } +} + + // --- STATISTIQUES AVANCÉES --- + + Future> getProductCountByCategory() async { + final db = await database; + final result = await db.query(''' + SELECT category, COUNT(*) as count + FROM products + GROUP BY category + ORDER BY count DESC + '''); + + return Map.fromEntries(result.map((row) => + MapEntry(row['category'] as String, row['count'] as int))); + } + + Future>> getStockStatsByCategory() async { + final db = await database; + final result = await db.query(''' + SELECT + category, + COUNT(*) as total_products, + SUM(CASE WHEN stock > 0 THEN 1 ELSE 0 END) as in_stock, + SUM(CASE WHEN stock = 0 OR stock IS NULL THEN 1 ELSE 0 END) as out_of_stock, + SUM(stock) as total_stock + FROM products + GROUP BY category + ORDER BY category + '''); + + Map> stats = {}; + for (var row in result) { + stats[row['category'] as String] = { + 'total': row['total_products'] as int, + 'in_stock': row['in_stock'] as int, + 'out_of_stock': row['out_of_stock'] as int, + 'total_stock': (row['total_stock'] as int?) ?? 0, + }; + } + return stats; + } + + Future>> getMostSoldProducts({int limit = 10}) async { + final db = await database; + final result = await db.query(''' + SELECT + p.id, + p.name, + p.price, + p.stock, + p.category, + SUM(dc.quantite) as total_sold, + COUNT(DISTINCT dc.commandeId) as order_count + FROM products p + INNER JOIN details_commandes dc ON p.id = dc.produitId + INNER JOIN commandes c ON dc.commandeId = c.id + WHERE c.statut != 5 -- Exclure les commandes annulées + GROUP BY p.id, p.name, p.price, p.stock, p.category + ORDER BY total_sold DESC + LIMIT ? + ''', [limit]); + + return result.map((row) => row.fields).toList(); + } + + // --- DÉBOGAGE --- + + Future debugPointsDeVenteTable() async { + final db = await database; + try { + // Compte le nombre d'entrées + final count = await db.query("SELECT COUNT(*) as count FROM points_de_vente"); + print("Nombre de points de vente: ${count.first['count']}"); + + // Affiche le contenu + final content = await db.query('SELECT * FROM points_de_vente'); + print("Contenu de la table points_de_vente:"); + for (var row in content) { + print("ID: ${row['id']}, Nom: ${row['nom']}"); + } + } catch (e) { + print("Erreur debug table points_de_vente: $e"); + } + } + // 1. Méthodes pour les clients +Future getClientByTelephone(String telephone) async { + final db = await database; + final result = await db.query( + 'SELECT * FROM clients WHERE telephone = ? AND actif = 1 LIMIT 1', + [telephone.trim()] + ); + + if (result.isNotEmpty) { + return Client.fromMap(result.first.fields); + } + return null; +} + +Future getClientByNomPrenom(String nom, String prenom) async { + final db = await database; + final result = await db.query( + 'SELECT * FROM clients WHERE LOWER(nom) = ? AND LOWER(prenom) = ? AND actif = 1 LIMIT 1', + [nom.trim().toLowerCase(), prenom.trim().toLowerCase()] + ); + + if (result.isNotEmpty) { + return Client.fromMap(result.first.fields); + } + return null; +} + +Future> suggestClients(String query) async { + if (query.trim().isEmpty) return []; + final db = await database; - return await db.insert('details_commandes', detail.toMap()); + final searchQuery = '%${query.trim().toLowerCase()}%'; + + final result = await db.query(''' + SELECT * FROM clients + WHERE actif = 1 AND ( + LOWER(nom) LIKE ? OR + LOWER(prenom) LIKE ? OR + LOWER(email) LIKE ? OR + telephone LIKE ? + ) + ORDER BY nom ASC, prenom ASC + LIMIT 10 + ''', [searchQuery, searchQuery, searchQuery, searchQuery]); + + return result.map((row) => Client.fromMap(row.fields)).toList(); } -// Récupérer les détails d'une commande -Future> getDetailsCommande(int commandeId) async { +Future> checkPotentialDuplicates({ + required String nom, + required String prenom, + required String email, + required String telephone, +}) async { final db = await database; - final maps = await db.rawQuery(''' - SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference - FROM details_commandes dc - LEFT JOIN products p ON dc.produitId = p.id - WHERE dc.commandeId = ? - ORDER BY dc.id - ''', [commandeId]); - return List.generate(maps.length, (i) => DetailCommande.fromMap(maps[i])); + + final result = await db.query(''' + SELECT * FROM clients + WHERE actif = 1 AND ( + (LOWER(nom) = ? AND LOWER(prenom) = ?) OR + email = ? OR + telephone = ? + ) + ORDER BY nom ASC, prenom ASC + ''', [ + nom.trim().toLowerCase(), + prenom.trim().toLowerCase(), + email.trim().toLowerCase(), + telephone.trim() + ]); + + return result.map((row) => Client.fromMap(row.fields)).toList(); +} + +Future createOrGetClient(Client newClient) async { + final existingClient = await findExistingClient( + email: newClient.email, + telephone: newClient.telephone, + nom: newClient.nom, + prenom: newClient.prenom, + ); + + if (existingClient != null) { + return existingClient; + } + + final clientId = await createClient(newClient); + final createdClient = await getClientById(clientId); + + if (createdClient != null) { + return createdClient; + } else { + throw Exception("Erreur lors de la création du client"); + } } - // Transactions complexes -Future createCommandeComplete(Client client, Commande commande, List details) async { + +// 2. Méthodes pour les produits +Future> getSimilarProducts(Product product, {int limit = 5}) async { final db = await database; - return await db.transaction((txn) async { - // 1. Créer le client - final clientId = await txn.insert('clients', client.toMap()); - - // 2. Créer la commande avec le bon clientId - final commandeMap = commande.toMap(); - commandeMap['clientId'] = clientId; - final commandeId = await txn.insert('commandes', commandeMap); - - // 3. Créer les détails de commande - for (final detail in details) { - final detailMap = detail.toMap(); - detailMap['commandeId'] = commandeId; - await txn.insert('details_commandes', detailMap); - - // 4. Mettre à jour le stock - await txn.rawUpdate( - 'UPDATE products SET stock = stock - ? WHERE id = ?', - [detail.quantite, detail.produitId], - ); - } - - return commandeId; - }); + final result = await db.query(''' + SELECT * + FROM products + WHERE id != ? + AND ( + category = ? + OR name LIKE ? + ) + ORDER BY + CASE WHEN category = ? THEN 1 ELSE 2 END, + name ASC + LIMIT ? + ''', [ + product.id, + product.category, + '%${product.name.split(' ').first}%', + product.category, + limit + ]); + + return result.map((row) => Product.fromMap(row.fields)).toList(); +} + +// 3. Méthodes pour les commandes +Future updateStatutCommande(int commandeId, StatutCommande statut) async { + final db = await database; + final result = await db.query( + 'UPDATE commandes SET statut = ? WHERE id = ?', + [statut.index, commandeId] + ); + return result.affectedRows!; } -// Récupérer les commandes d'un client + Future> getCommandesByClient(int clientId) async { final db = await database; - final maps = await db.rawQuery(''' + final result = await db.query(''' SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail FROM commandes c LEFT JOIN clients cl ON c.clientId = cl.id WHERE c.clientId = ? ORDER BY c.dateCommande DESC ''', [clientId]); - return List.generate(maps.length, (i) => Commande.fromMap(maps[i])); + + return result.map((row) => Commande.fromMap(row.fields)).toList(); } -// Récupérer les commandes par statut Future> getCommandesByStatut(StatutCommande statut) async { final db = await database; - final maps = await db.rawQuery(''' + final result = await db.query(''' SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail FROM commandes c LEFT JOIN clients cl ON c.clientId = cl.id WHERE c.statut = ? ORDER BY c.dateCommande DESC ''', [statut.index]); - return List.generate(maps.length, (i) => Commande.fromMap(maps[i])); + + return result.map((row) => Commande.fromMap(row.fields)).toList(); } Future updateValidateurCommande(int commandeId, int validateurId) async { final db = await database; - return await db.update( - 'commandes', - { - 'validateurId': validateurId, - 'statut': StatutCommande.confirmee.index, - }, - where: 'id = ?', - whereArgs: [commandeId], - ); -} -Future updateStock(int productId, int newStock) async { - final db = await database; - return await db.update( - 'products', - {'stock': newStock}, - where: 'id = ?', - whereArgs: [productId], - ); + final result = await db.query(''' + UPDATE commandes + SET validateurId = ?, statut = ? + WHERE id = ? + ''', [validateurId, StatutCommande.confirmee.index, commandeId]); + + return result.affectedRows!; } - // // Données par défaut - // Future _insertDefaultClients() async {final db = await database; - // final existingClients = await db.query('clients'); - - // if (existingClients.isEmpty) { - // final defaultClients = [ - // Client( - // nom: 'Dupont', - // prenom: 'Jean', - // email: 'jean.dupont@email.com', - // telephone: '0123456789', - // adresse: '123 Rue de la Paix, Paris', - // dateCreation: DateTime.now(), - // ), - // Client( - // nom: 'Martin', - // prenom: 'Marie', - // email: 'marie.martin@email.com', - // telephone: '0987654321', - // adresse: '456 Avenue des Champs, Lyon', - // dateCreation: DateTime.now(), - // ), - // Client( - // nom: 'Bernard', - // prenom: 'Pierre', - // email: 'pierre.bernard@email.com', - // telephone: '0456789123', - // adresse: '789 Boulevard Saint-Michel, Marseille', - // dateCreation: DateTime.now(), - // ), - // ]; - - // for (var client in defaultClients) { - // await db.insert('clients', client.toMap()); - // } - // print("Clients par défaut insérés"); - // } /* Copier depuis ton code */ } - // Future _insertDefaultCommandes() async { final db = await database; - // final existingCommandes = await db.query('commandes'); - - // if (existingCommandes.isEmpty) { - // // Récupérer quelques produits pour créer des commandes - // final produits = await db.query('products', limit: 3); - // final clients = await db.query('clients', limit: 3); - - // if (produits.isNotEmpty && clients.isNotEmpty) { - // // Commande 1 - // final commande1Id = await db.insert('commandes', { - // 'clientId': clients[0]['id'], - // 'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(), - // 'statut': StatutCommande.livree.index, - // 'montantTotal': 150.0, - // 'notes': 'Commande urgente', - // }); - - // await db.insert('details_commandes', { - // 'commandeId': commande1Id, - // 'produitId': produits[0]['id'], - // 'quantite': 2, - // 'prixUnitaire': 75.0, - // 'sousTotal': 150.0, - // }); - - // // Commande 2 - // final commande2Id = await db.insert('commandes', { - // 'clientId': clients[1]['id'], - // 'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(), - // 'statut': StatutCommande.enPreparation.index, - // 'montantTotal': 225.0, - // 'notes': 'Livraison prévue demain', - // }); - - // if (produits.length > 1) { - // await db.insert('details_commandes', { - // 'commandeId': commande2Id, - // 'produitId': produits[1]['id'], - // 'quantite': 3, - // 'prixUnitaire': 75.0, - // 'sousTotal': 225.0, - // }); - // } - - // // Commande 3 - // final commande3Id = await db.insert('commandes', { - // 'clientId': clients[2]['id'], - // 'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(), - // 'statut': StatutCommande.confirmee.index, - // 'montantTotal': 300.0, - // 'notes': 'Commande standard', - // }); - - // if (produits.length > 2) { - // await db.insert('details_commandes', { - // 'commandeId': commande3Id, - // 'produitId': produits[2]['id'], - // 'quantite': 4, - // 'prixUnitaire': 75.0, - // 'sousTotal': 300.0, - // }); - // } - - // print("Commandes par défaut insérées"); - // } - // }/* Copier depuis ton code */ } - - // Statistiques - Future> getStatistiques() async { final db = await database; - - final totalClients = await db.rawQuery('SELECT COUNT(*) as count FROM clients WHERE actif = 1'); - final totalCommandes = await db.rawQuery('SELECT COUNT(*) as count FROM commandes'); - final totalProduits = await db.rawQuery('SELECT COUNT(*) as count FROM products'); - final chiffreAffaires = await db.rawQuery('SELECT SUM(montantTotal) as total FROM commandes WHERE statut != 5'); // 5 = annulée - - return { - 'totalClients': totalClients.first['count'], - 'totalCommandes': totalCommandes.first['count'], - 'totalProduits': totalProduits.first['count'], - 'chiffreAffaires': chiffreAffaires.first['total'] ?? 0.0, - };/* Copier depuis ton code */ } - - // Fermeture - Future close() async { - if (_database.isOpen) { - await _database.close(); - } - } - Future _updateExistingRolePermissions(Database db) async { - final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']); - if (superAdminRole.isNotEmpty) { - final superAdminRoleId = superAdminRole.first['id'] as int; - final permissions = await db.query('permissions'); - final menus = await db.query('menu'); - - // Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus - for (var menu in menus) { - for (var permission in permissions) { - final existingPermission = await db.query( - 'role_menu_permissions', - where: 'role_id = ? AND menu_id = ? AND permission_id = ?', - whereArgs: [superAdminRoleId, menu['id'], permission['id']], - ); - if (existingPermission.isEmpty) { - await db.insert('role_menu_permissions', { - 'role_id': superAdminRoleId, - 'menu_id': menu['id'], - 'permission_id': permission['id'], - }, - conflictAlgorithm: ConflictAlgorithm.ignore - ); - } - } - } - - // Assigner les permissions de base aux autres rôles pour les nouveaux menus - final adminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Admin']); - final userRole = await db.query('roles', where: 'designation = ?', whereArgs: ['User']); - - if (adminRole.isNotEmpty && userRole.isNotEmpty) { - await _assignBasicPermissionsToRoles(db, adminRole.first['id'] as int, userRole.first['id'] as int); - } +// --- CRUD MENUS --- +// Ajoutez ces méthodes dans votre classe AppDatabase - print("Permissions mises à jour pour tous les rôles"); +Future>> getAllMenus() async { + final db = await database; + try { + final result = await db.query('SELECT * FROM menu ORDER BY name ASC'); + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur lors de la récupération des menus: $e"); + return []; } } -// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus -Future _assignBasicPermissionsToRoles(Database db, int adminRoleId, int userRoleId) async { - final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']); - final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']); - final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']); - final managePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['manage']); - - // Récupérer les IDs des nouveaux menus - final nouvelleCommandeMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']); - final gererCommandesMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']); - - if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) { - // Admin peut créer de nouvelles commandes - await db.insert('role_menu_permissions', { - 'role_id': adminRoleId, - 'menu_id': nouvelleCommandeMenu.first['id'], - 'permission_id': createPermission.first['id'], - }, - conflictAlgorithm: ConflictAlgorithm.ignore - ); - - // User peut aussi créer de nouvelles commandes - await db.insert('role_menu_permissions', { - 'role_id': userRoleId, - 'menu_id': nouvelleCommandeMenu.first['id'], - 'permission_id': createPermission.first['id'], - }, - conflictAlgorithm: ConflictAlgorithm.ignore - ); - } - if (gererCommandesMenu.isNotEmpty && managePermission.isNotEmpty) { - // Admin peut gérer les commandes - await db.insert('role_menu_permissions', { - 'role_id': adminRoleId, - 'menu_id': gererCommandesMenu.first['id'], - 'permission_id': managePermission.first['id'], - }, - conflictAlgorithm: ConflictAlgorithm.ignore - ); +Future?> getMenuById(int id) async { + final db = await database; + try { + final result = await db.query('SELECT * FROM menu WHERE id = ? LIMIT 1', [id]); + return result.isNotEmpty ? result.first.fields : null; + } catch (e) { + print("Erreur getMenuById: $e"); + return null; } +} - if (gererCommandesMenu.isNotEmpty && viewPermission.isNotEmpty) { - // User peut voir les commandes - await db.insert('role_menu_permissions', { - 'role_id': userRoleId, - 'menu_id': gererCommandesMenu.first['id'], - 'permission_id': viewPermission.first['id'], - } - , conflictAlgorithm: ConflictAlgorithm.ignore - ); +Future?> getMenuByRoute(String route) async { + final db = await database; + try { + final result = await db.query('SELECT * FROM menu WHERE route = ? LIMIT 1', [route]); + return result.isNotEmpty ? result.first.fields : null; + } catch (e) { + print("Erreur getMenuByRoute: $e"); + return null; } } -Future _addMissingMenus(Database db) async { - final menusToAdd = [ - {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, - {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, - ]; - for (var menu in menusToAdd) { - final existing = await db.query( - 'menu', - where: 'route = ?', - whereArgs: [menu['route']], - ); +Future createMenu(String name, String route) async { + final db = await database; + try { + // Vérifier si le menu existe déjà + final existing = await db.query('SELECT COUNT(*) as count FROM menu WHERE route = ?', [route]); + final count = existing.first['count'] as int; - if (existing.isEmpty) { - await db.insert('menu', menu); - print("Menu ajouté: ${menu['name']}"); + if (count > 0) { + throw Exception('Un menu avec cette route existe déjà'); } + + final result = await db.query( + 'INSERT INTO menu (name, route) VALUES (?, ?)', + [name, route] + ); + return result.insertId!; + } catch (e) { + print("Erreur createMenu: $e"); + rethrow; } } -Future hasPermission(String username, String permissionName, String menuRoute) async { - final db = await database; - final result = await db.rawQuery(''' - SELECT COUNT(*) as count - FROM permissions p - JOIN role_menu_permissions rmp ON p.id = rmp.permission_id - JOIN roles r ON rmp.role_id = r.id - JOIN users u ON u.role_id = r.id - JOIN menu m ON m.route = ? - WHERE u.username = ? AND p.name = ? AND rmp.menu_id = m.id - ''', [menuRoute, username, permissionName]); - return (result.first['count'] as int) > 0; -} - // Pour réinitialiser la base (débogage) - Future deleteDatabaseFile() async { - final documentsDirectory = await getApplicationDocumentsDirectory(); - final path = join(documentsDirectory.path, 'app_database.db'); - final file = File(path); - if (await file.exists()) { - await file.delete(); - print("Base de données product supprimée"); - }/* Copier depuis ton code */ } - // CRUD Points de vente -// CRUD Points de vente -Future createPointDeVente(String designation, String code) async { - final db = await database; - return await db.insert('points_de_vente', { - 'designation': designation, - 'code': code - }, conflictAlgorithm: ConflictAlgorithm.ignore); -} -Future>> getPointsDeVente() async { +Future updateMenu(int id, String name, String route) async { final db = await database; try { final result = await db.query( - 'points_de_vente', - orderBy: 'nom ASC', - where: 'nom IS NOT NULL AND nom != ""' // Filtre les noms vides + 'UPDATE menu SET name = ?, route = ? WHERE id = ?', + [name, route, id] ); - - if (result.isEmpty) { - print("Aucun point de vente trouvé dans la base de données"); - // Optionnel: Insérer les points de vente par défaut si table vide - await insertDefaultPointsDeVente(); - return await db.query('points_de_vente', orderBy: 'nom ASC'); - } - - return result; + return result.affectedRows!; } catch (e) { - print("Erreur lors de la récupération des points de vente: $e"); - return []; + print("Erreur updateMenu: $e"); + rethrow; } } -Future updatePointDeVente(int id, String newDesignation, String newCode) async { +Future deleteMenu(int id) async { final db = await database; - return await db.update( - 'points_de_vente', - { - 'designation': newDesignation, - 'code': newCode - }, - where: 'id = ?', - whereArgs: [id], - ); + try { + // D'abord supprimer les permissions associées + await db.query('DELETE FROM role_menu_permissions WHERE menu_id = ?', [id]); + + // Ensuite supprimer le menu + final result = await db.query('DELETE FROM menu WHERE id = ?', [id]); + return result.affectedRows!; + } catch (e) { + print("Erreur deleteMenu: $e"); + rethrow; + } } -Future deletePointDeVente(int id) async { - final db = await database; - return await db.delete( - 'points_de_vente', - where: 'id = ?', - whereArgs: [id], - ); -} -// Dans AppDatabase -Future> getProductCountByCategory() async { +Future>> getMenusForRole(int roleId) async { final db = await database; - final result = await db.rawQuery(''' - SELECT category, COUNT(*) as count - FROM products - GROUP BY category - ORDER BY count DESC - '''); - - return Map.fromEntries(result.map((e) => - MapEntry(e['category'] as String, e['count'] as int))); + try { + final result = await db.query(''' + SELECT DISTINCT m.* + FROM menu m + INNER JOIN role_menu_permissions rmp ON m.id = rmp.menu_id + WHERE rmp.role_id = ? + ORDER BY m.name ASC + ''', [roleId]); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur getMenusForRole: $e"); + return []; + } } - -Future?> getPointDeVenteById(int id) async { +Future hasMenuAccess(int roleId, String menuRoute) async { final db = await database; - final result = await db.query( - 'points_de_vente', - where: 'id = ?', - whereArgs: [id], - ); - return result.isNotEmpty ? result.first : null; + try { + final result = await db.query(''' + SELECT COUNT(*) as count + FROM role_menu_permissions rmp + INNER JOIN menu m ON rmp.menu_id = m.id + WHERE rmp.role_id = ? AND m.route = ? + ''', [roleId, menuRoute]); + + return (result.first['count'] as int) > 0; + } catch (e) { + print("Erreur hasMenuAccess: $e"); + return false; + } } -Future getOrCreatePointDeVenteByNom(String nom) async { + +Future findClientByAnyIdentifier({ + String? email, + String? telephone, + String? nom, + String? prenom, +}) async { final db = await database; - // Vérifier si le point de vente existe déjà - final existing = await db.query( - 'points_de_vente', - where: 'nom = ?', - whereArgs: [nom.trim()], - ); + // Recherche par email si fourni + if (email != null && email.isNotEmpty) { + final client = await getClientByEmail(email); + if (client != null) return client; + } - if (existing.isNotEmpty) { - return existing.first['id'] as int; + // Recherche par téléphone si fourni + if (telephone != null && telephone.isNotEmpty) { + final client = await getClientByTelephone(telephone); + if (client != null) return client; } - // Créer le point de vente s'il n'existe pas - try { - final id = await db.insert('points_de_vente', { - 'nom': nom.trim() - }); - print("Point de vente créé: $nom (ID: $id)"); - return id; - } catch (e) { - print("Erreur lors de la création du point de vente $nom: $e"); - return null; + // Recherche par nom et prénom si fournis + if (nom != null && nom.isNotEmpty && prenom != null && prenom.isNotEmpty) { + final client = await getClientByNomPrenom(nom, prenom); + if (client != null) return client; } + + return null; } -Future getPointDeVenteNomById(int id) async { - if (id == 0 || id == null) return null; - +Future migrateDatabaseForDiscountAndGift() async { final db = await database; + try { - final result = await db.query( - 'points_de_vente', - where: 'id = ?', - whereArgs: [id], - limit: 1, - ); + // Ajouter les colonnes de remise à la table commandes + await db.query(''' + ALTER TABLE commandes + ADD COLUMN remisePourcentage DECIMAL(5,2) NULL + '''); + + await db.query(''' + ALTER TABLE commandes + ADD COLUMN remiseMontant DECIMAL(10,2) NULL + '''); + + await db.query(''' + ALTER TABLE commandes + ADD COLUMN montantApresRemise DECIMAL(10,2) NULL + '''); + + // Ajouter la colonne cadeau à la table details_commandes + await db.query(''' + ALTER TABLE details_commandes + ADD COLUMN estCadeau TINYINT(1) DEFAULT 0 + '''); - return result.isNotEmpty ? result.first['nom'] as String : null; + print("Migration pour remise et cadeau terminée avec succès"); } catch (e) { - print("Erreur getPointDeVenteNomById: $e"); - return null; + // Les colonnes existent probablement déjà + print("Migration déjà effectuée ou erreur: $e"); } } -Future> searchProducts({ - String? name, - String? imei, - String? reference, - bool onlyInStock = false, - String? category, - int? pointDeVenteId, +Future> getDetailsCommandeAvecCadeaux(int commandeId) async { + final db = await database; + final result = await db.query(''' + SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference + FROM details_commandes dc + LEFT JOIN products p ON dc.produitId = p.id + WHERE dc.commandeId = ? + ORDER BY dc.estCadeau ASC, dc.id + ''', [commandeId]); + return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); +} + +Future updateCommandeAvecRemise(int commandeId, { + double? remisePourcentage, + double? remiseMontant, + double? montantApresRemise, }) async { final db = await database; - List whereConditions = []; - List whereArgs = []; - - if (name != null && name.isNotEmpty) { - whereConditions.add('name LIKE ?'); - whereArgs.add('%$name%'); - } - - if (imei != null && imei.isNotEmpty) { - whereConditions.add('imei LIKE ?'); - whereArgs.add('%$imei%'); - } + List setClauses = []; + List values = []; - if (reference != null && reference.isNotEmpty) { - whereConditions.add('reference LIKE ?'); - whereArgs.add('%$reference%'); + if (remisePourcentage != null) { + setClauses.add('remisePourcentage = ?'); + values.add(remisePourcentage); } - if (onlyInStock) { - whereConditions.add('stock > 0'); + if (remiseMontant != null) { + setClauses.add('remiseMontant = ?'); + values.add(remiseMontant); } - if (category != null && category.isNotEmpty) { - whereConditions.add('category = ?'); - whereArgs.add(category); + if (montantApresRemise != null) { + setClauses.add('montantApresRemise = ?'); + values.add(montantApresRemise); } - if (pointDeVenteId != null && pointDeVenteId > 0) { - whereConditions.add('point_de_vente_id = ?'); - whereArgs.add(pointDeVenteId); - } + if (setClauses.isEmpty) return 0; - String whereClause = whereConditions.isNotEmpty - ? whereConditions.join(' AND ') - : ''; + values.add(commandeId); - final maps = await db.query( - 'products', - where: whereClause.isNotEmpty ? whereClause : null, - whereArgs: whereArgs.isNotEmpty ? whereArgs : null, - orderBy: 'name ASC', + final result = await db.query( + 'UPDATE commandes SET ${setClauses.join(', ')} WHERE id = ?', + values ); - return List.generate(maps.length, (i) => Product.fromMap(maps[i])); + return result.affectedRows!; } -// Obtenir le nombre de produits en stock par catégorie -Future>> getStockStatsByCategory() async { +Future createDetailCommandeCadeau(DetailCommande detail) async { final db = await database; - final result = await db.rawQuery(''' - SELECT - category, - COUNT(*) as total_products, - SUM(CASE WHEN stock > 0 THEN 1 ELSE 0 END) as in_stock, - SUM(CASE WHEN stock = 0 OR stock IS NULL THEN 1 ELSE 0 END) as out_of_stock, - SUM(stock) as total_stock - FROM products - GROUP BY category - ORDER BY category - '''); - - Map> stats = {}; - for (var row in result) { - stats[row['category'] as String] = { - 'total': row['total_products'] as int, - 'in_stock': row['in_stock'] as int, - 'out_of_stock': row['out_of_stock'] as int, - 'total_stock': row['total_stock'] as int? ?? 0, - }; - } - return stats; -} - -// Recherche rapide par code-barres/QR/IMEI -Future findProductByCode(String code) async { - final db = await database; - - // Essayer de trouver par référence d'abord - var maps = await db.query( - 'products', - where: 'reference = ?', - whereArgs: [code], - limit: 1, - ); - - if (maps.isNotEmpty) { - return Product.fromMap(maps.first); - } - // Ensuite par IMEI - maps = await db.query( - 'products', - where: 'imei = ?', - whereArgs: [code], - limit: 1, - ); + final detailMap = detail.toMap(); + detailMap.remove('id'); + detailMap['estCadeau'] = 1; // Marquer comme cadeau + detailMap['prixUnitaire'] = 0.0; // Prix zéro pour les cadeaux + detailMap['sousTotal'] = 0.0; // Sous-total zéro pour les cadeaux - if (maps.isNotEmpty) { - return Product.fromMap(maps.first); - } + final fields = detailMap.keys.join(', '); + final placeholders = List.filled(detailMap.length, '?').join(', '); - // Enfin par QR code si disponible - maps = await db.query( - 'products', - where: 'qrCode = ?', - whereArgs: [code], - limit: 1, + final result = await db.query( + 'INSERT INTO details_commandes ($fields) VALUES ($placeholders)', + detailMap.values.toList() ); - - if (maps.isNotEmpty) { - return Product.fromMap(maps.first); - } - - return null; + return result.insertId!; } -// Obtenir les produits avec stock faible (seuil personnalisable) -Future> getLowStockProducts({int threshold = 5}) async { +Future> getCadeauxCommande(int commandeId) async { final db = await database; - final maps = await db.query( - 'products', - where: 'stock <= ? AND stock > 0', - whereArgs: [threshold], - orderBy: 'stock ASC', - ); - return List.generate(maps.length, (i) => Product.fromMap(maps[i])); + final result = await db.query(''' + SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference + FROM details_commandes dc + LEFT JOIN products p ON dc.produitId = p.id + WHERE dc.commandeId = ? AND dc.estCadeau = 1 + ORDER BY dc.id + ''', [commandeId]); + return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); } -// Obtenir les produits les plus vendus (basé sur les commandes) -Future>> getMostSoldProducts({int limit = 10}) async { +Future calculateMontantTotalSansCadeaux(int commandeId) async { final db = await database; - final result = await db.rawQuery(''' - SELECT - p.id, - p.name, - p.price, - p.stock, - p.category, - SUM(dc.quantite) as total_sold, - COUNT(DISTINCT dc.commandeId) as order_count - FROM products p - INNER JOIN details_commandes dc ON p.id = dc.produitId - INNER JOIN commandes c ON dc.commandeId = c.id - WHERE c.statut != 5 -- Exclure les commandes annulées - GROUP BY p.id, p.name, p.price, p.stock, p.category - ORDER BY total_sold DESC - LIMIT ? - ''', [limit]); + final result = await db.query(''' + SELECT SUM(sousTotal) as total + FROM details_commandes + WHERE commandeId = ? AND (estCadeau = 0 OR estCadeau IS NULL) + ''', [commandeId]); - return result; + final total = result.first['total']; + return total != null ? (total as num).toDouble() : 0.0; } -// Recherche de produits similaires (par nom ou catégorie) -Future> getSimilarProducts(Product product, {int limit = 5}) async { +Future supprimerRemiseCommande(int commandeId) async { final db = await database; + final result = await db.query(''' + UPDATE commandes + SET remisePourcentage = NULL, remiseMontant = NULL, montantApresRemise = NULL + WHERE id = ? + ''', [commandeId]); - // Rechercher par catégorie et nom similaire, exclure le produit actuel - final maps = await db.rawQuery(''' - SELECT * - FROM products - WHERE id != ? - AND ( - category = ? - OR name LIKE ? - ) - ORDER BY - CASE WHEN category = ? THEN 1 ELSE 2 END, - name ASC - LIMIT ? - ''', [ - product.id, - product.category, - '%${product.name.split(' ').first}%', - product.category, - limit - ]); - - return List.generate(maps.length, (i) => Product.fromMap(maps[i])); + return result.affectedRows!; } + } \ No newline at end of file diff --git a/lib/Views/HandleProduct.dart b/lib/Views/HandleProduct.dart index c0eb37e..724947d 100644 --- a/lib/Views/HandleProduct.dart +++ b/lib/Views/HandleProduct.dart @@ -2285,90 +2285,565 @@ Future _generatePDF(Product product, String qrUrl) async { } void _editProduct(Product product) { - final nameController = TextEditingController(text: product.name); - final priceController = TextEditingController(text: product.price.toString()); - final stockController = TextEditingController(text: product.stock.toString()); - final descriptionController = TextEditingController(text: product.description ?? ''); - final imageController = TextEditingController(text: product.image); - - String selectedCategory = product.category; - File? pickedImage; - - Get.dialog( - AlertDialog( - title: const Text('Modifier le produit'), - content: Container( - width: 500, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameController, - decoration: const InputDecoration( - labelText: 'Nom du produit*', - border: OutlineInputBorder(), + final nameController = TextEditingController(text: product.name); + final priceController = TextEditingController(text: product.price.toString()); + final stockController = TextEditingController(text: product.stock.toString()); + final descriptionController = TextEditingController(text: product.description ?? ''); + final imageController = TextEditingController(text: product.image); + final referenceController = TextEditingController(text: product.reference ?? ''); + final marqueController = TextEditingController(text: product.marque ?? ''); + final ramController = TextEditingController(text: product.ram ?? ''); + final memoireInterneController = TextEditingController(text: product.memoireInterne ?? ''); + final imeiController = TextEditingController(text: product.imei ?? ''); + final newPointDeVenteController = TextEditingController(); + + String? selectedPointDeVente; + List> pointsDeVente = []; + bool isLoadingPoints = true; + // Initialiser la catégorie sélectionnée de manière sécurisée + String selectedCategory = _predefinedCategories.contains(product.category) + ? product.category + : _predefinedCategories.last; // 'Non catégorisé' par défaut + File? pickedImage; + String? qrPreviewData; + bool showAddNewPoint = false; + + // Fonction pour mettre à jour le QR preview + void updateQrPreview() { + if (nameController.text.isNotEmpty && referenceController.text.isNotEmpty) { + qrPreviewData = 'https://stock.guycom.mg/${referenceController.text.trim()}'; + } else { + qrPreviewData = null; + } + } + + // Charger les points de vente + Future loadPointsDeVente(StateSetter setDialogState) async { + try { + final result = await _productDatabase.getPointsDeVente(); + setDialogState(() { + pointsDeVente = result; + isLoadingPoints = false; + // Définir le point de vente actuel du produit + if (product.pointDeVenteId != null) { + final currentPointDeVente = result.firstWhere( + (point) => point['id'] == product.pointDeVenteId, + orElse: () => {}, + ); + if (currentPointDeVente.isNotEmpty) { + selectedPointDeVente = currentPointDeVente['nom'] as String; + } + } + // Si aucun point de vente sélectionné et qu'il y en a des disponibles + if (selectedPointDeVente == null && result.isNotEmpty) { + selectedPointDeVente = result.first['nom'] as String; + } + }); + } catch (e) { + setDialogState(() { + isLoadingPoints = false; + }); + Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); + } + } + + // Initialiser le QR preview + updateQrPreview(); + + Get.dialog( + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.edit, color: Colors.orange.shade700), + ), + const SizedBox(width: 12), + const Text('Modifier le produit'), + ], + ), + content: Container( + width: 600, + constraints: const BoxConstraints(maxHeight: 600), + child: SingleChildScrollView( + child: StatefulBuilder( + builder: (context, setDialogState) { + // Charger les points de vente une seule fois + if (isLoadingPoints && pointsDeVente.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + loadPointsDeVente(setDialogState); + }); + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champs obligatoires + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade200), + ), + child: Row( + children: [ + Icon(Icons.info, color: Colors.orange.shade600, size: 16), + const SizedBox(width: 8), + const Text( + 'Les champs marqués d\'un * sont obligatoires', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ], + ), ), - ), - const SizedBox(height: 16), - - TextField( - controller: priceController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: const InputDecoration( - labelText: 'Prix*', - border: OutlineInputBorder(), + const SizedBox(height: 16), + + // Section Point de vente + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.teal.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.teal.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.store, color: Colors.teal.shade700), + const SizedBox(width: 8), + Text( + 'Point de vente', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.teal.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + + if (isLoadingPoints) + const Center(child: CircularProgressIndicator()) + else if (pointsDeVente.isEmpty) + Column( + children: [ + Text( + 'Aucun point de vente trouvé. Créez-en un nouveau.', + style: TextStyle(color: Colors.grey.shade600), + ), + const SizedBox(height: 8), + TextField( + controller: newPointDeVenteController, + decoration: const InputDecoration( + labelText: 'Nom du nouveau point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.add_business), + filled: true, + fillColor: Colors.white, + ), + ), + ], + ) + else + Column( + children: [ + if (!showAddNewPoint) ...[ + DropdownButtonFormField( + value: selectedPointDeVente, + items: pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + setDialogState(() => selectedPointDeVente = value); + }, + decoration: const InputDecoration( + labelText: 'Sélectionner un point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.store), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: () { + setDialogState(() { + showAddNewPoint = true; + newPointDeVenteController.clear(); + }); + }, + icon: const Icon(Icons.add, size: 16), + label: const Text('Ajouter nouveau point'), + style: TextButton.styleFrom( + foregroundColor: Colors.teal.shade700, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: () => loadPointsDeVente(setDialogState), + icon: const Icon(Icons.refresh, size: 16), + label: const Text('Actualiser'), + ), + ], + ), + ], + + if (showAddNewPoint) ...[ + TextField( + controller: newPointDeVenteController, + decoration: const InputDecoration( + labelText: 'Nom du nouveau point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.add_business), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton( + onPressed: () { + setDialogState(() { + showAddNewPoint = false; + newPointDeVenteController.clear(); + }); + }, + child: const Text('Annuler'), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () async { + final nom = newPointDeVenteController.text.trim(); + if (nom.isNotEmpty) { + try { + final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); + if (id != null) { + setDialogState(() { + showAddNewPoint = false; + selectedPointDeVente = nom; + newPointDeVenteController.clear(); + }); + // Recharger la liste + await loadPointsDeVente(setDialogState); + Get.snackbar( + 'Succès', + 'Point de vente "$nom" créé avec succès', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } + } catch (e) { + Get.snackbar('Erreur', 'Impossible de créer le point de vente: $e'); + } + } + }, + icon: const Icon(Icons.save, size: 16), + label: const Text('Créer'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, + foregroundColor: Colors.white, + ), + ), + ], + ), + ], + ], + ), + ], + ), ), - ), - const SizedBox(height: 16), - - TextField( - controller: stockController, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Stock', - border: OutlineInputBorder(), + const SizedBox(height: 16), + + // Nom du produit + TextField( + controller: nameController, + decoration: InputDecoration( + labelText: 'Nom du produit *', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.shopping_bag), + filled: true, + fillColor: Colors.grey.shade50, + ), + onChanged: (value) { + setDialogState(() { + updateQrPreview(); + }); + }, + ), + const SizedBox(height: 16), + + // Prix et Stock sur la même ligne + Row( + children: [ + Expanded( + child: TextField( + controller: priceController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: 'Prix (MGA) *', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.attach_money), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: stockController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Stock', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.inventory), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Catégorie avec gestion des valeurs non présentes + DropdownButtonFormField( + value: selectedCategory, + items: _predefinedCategories.map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), + onChanged: (value) { + setDialogState(() => selectedCategory = value!); + }, + decoration: InputDecoration( + labelText: 'Catégorie', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.category), + filled: true, + fillColor: Colors.grey.shade50, + helperText: product.category != selectedCategory + ? 'Catégorie originale: ${product.category}' + : null, + ), + ), + const SizedBox(height: 16), + + // Description + TextField( + controller: descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Description', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.description), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + const SizedBox(height: 16), + + // Section Référence (non modifiable) + 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: [ + Row( + children: [ + Icon(Icons.confirmation_number, color: Colors.purple.shade700), + const SizedBox(width: 8), + Text( + 'Référence du produit', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.purple.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: referenceController, + decoration: const InputDecoration( + labelText: 'Référence', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.tag), + filled: true, + fillColor: Colors.white, + helperText: 'La référence peut être modifiée avec précaution', + ), + onChanged: (value) { + setDialogState(() { + updateQrPreview(); + }); + }, + ), + ], + ), + ), + const SizedBox(height: 16), + + // Spécifications techniques + 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: [ + Row( + children: [ + Icon(Icons.memory, color: Colors.orange.shade700), + const SizedBox(width: 8), + Text( + 'Spécifications techniques', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: marqueController, + decoration: const InputDecoration( + labelText: 'Marque', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.branding_watermark), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: ramController, + decoration: const InputDecoration( + labelText: 'RAM', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.memory), + filled: true, + fillColor: Colors.white, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: memoireInterneController, + decoration: const InputDecoration( + labelText: 'Mémoire interne', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.storage), + filled: true, + fillColor: Colors.white, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + TextField( + controller: imeiController, + decoration: const InputDecoration( + labelText: 'IMEI (pour téléphones)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.smartphone), + filled: true, + fillColor: Colors.white, + ), + ), + ], + ), ), - ), - const SizedBox(height: 16), - - StatefulBuilder( - builder: (context, setDialogState) { - return Column( + const SizedBox(height: 16), + + // Section Image + 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( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + children: [ + Icon(Icons.image, color: Colors.blue.shade700), + const SizedBox(width: 8), + Text( + 'Image du produit', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: imageController, decoration: const InputDecoration( - labelText: 'Image', + labelText: 'Chemin de l\'image', border: OutlineInputBorder(), + isDense: true, ), readOnly: true, ), ), const SizedBox(width: 8), - ElevatedButton( + ElevatedButton.icon( onPressed: () async { final result = await FilePicker.platform.pickFiles(type: FileType.image); if (result != null && result.files.single.path != null) { - if (context.mounted) { - setDialogState(() { - pickedImage = File(result.files.single.path!); - imageController.text = pickedImage!.path; - }); - } + setDialogState(() { + pickedImage = File(result.files.single.path!); + imageController.text = pickedImage!.path; + }); } }, - child: const Text('Choisir'), + icon: const Icon(Icons.folder_open, size: 16), + label: const Text('Choisir'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(12), + ), ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), - if (pickedImage != null || product.image!.isNotEmpty) - Container( + // Aperçu de l'image + Center( + child: Container( height: 100, width: 100, decoration: BoxDecoration( @@ -2379,93 +2854,177 @@ Future _generatePDF(Product product, String qrUrl) async { borderRadius: BorderRadius.circular(8), child: pickedImage != null ? Image.file(pickedImage!, fit: BoxFit.cover) - : (product.image!.isNotEmpty - ? Image.file(File(product.image!), fit: BoxFit.cover) + : (product.image != null && product.image!.isNotEmpty + ? Image.file( + File(product.image!), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.image, size: 50), + ) : const Icon(Icons.image, size: 50)), ), ), - const SizedBox(height: 16), - - DropdownButtonFormField( - value: selectedCategory, - items: _categories.skip(1).map((category) => - DropdownMenuItem(value: category, child: Text(category))).toList(), - onChanged: (value) { - if (context.mounted) { - setDialogState(() => selectedCategory = value!); - } - }, - decoration: const InputDecoration( - labelText: 'Catégorie', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - - TextField( - controller: descriptionController, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Description', - border: OutlineInputBorder(), - ), ), ], - ); - }, - ), - ], - ), + ), + ), + const SizedBox(height: 16), + + // Aperçu QR Code + if (qrPreviewData != null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + children: [ + Row( + children: [ + Icon(Icons.qr_code_2, color: Colors.green.shade700), + const SizedBox(width: 8), + Text( + 'Aperçu du QR Code', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: QrImageView( + data: qrPreviewData!, + version: QrVersions.auto, + size: 80, + backgroundColor: Colors.white, + ), + ), + ), + const SizedBox(height: 8), + Text( + 'Réf: ${referenceController.text.trim()}', + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + ], + ); + }, ), ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton( - onPressed: () async { - final name = nameController.text.trim(); - final price = double.tryParse(priceController.text.trim()) ?? 0.0; - final stock = int.tryParse(stockController.text.trim()) ?? 0; - - if (name.isEmpty || price <= 0) { - Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton.icon( + onPressed: () async { + final name = nameController.text.trim(); + final price = double.tryParse(priceController.text.trim()) ?? 0.0; + final stock = int.tryParse(stockController.text.trim()) ?? 0; + final reference = referenceController.text.trim(); + + if (name.isEmpty || price <= 0) { + Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); + return; + } + + if (reference.isEmpty) { + Get.snackbar('Erreur', 'La référence est obligatoire'); + return; + } + + // Vérifier si la référence existe déjà (sauf pour ce produit) + if (reference != product.reference) { + final existingProduct = await _productDatabase.getProductByReference(reference); + if (existingProduct != null && existingProduct.id != product.id) { + Get.snackbar('Erreur', 'Cette référence existe déjà pour un autre produit'); return; } - + } + + // Vérifier si l'IMEI existe déjà (sauf pour ce produit) + final imei = imeiController.text.trim(); + if (imei.isNotEmpty && imei != product.imei) { + final existingProduct = await _productDatabase.getProductByIMEI(imei); + if (existingProduct != null && existingProduct.id != product.id) { + Get.snackbar('Erreur', 'Cet IMEI existe déjà pour un autre produit'); + return; + } + } + + // Gérer le point de vente + int? pointDeVenteId; + String? finalPointDeVenteNom; + + if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { + finalPointDeVenteNom = newPointDeVenteController.text.trim(); + } else if (selectedPointDeVente != null) { + finalPointDeVenteNom = selectedPointDeVente; + } + + if (finalPointDeVenteNom != null) { + pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(finalPointDeVenteNom); + } + + try { final updatedProduct = Product( id: product.id, name: name, price: price, - image: imageController.text, + image: imageController.text.trim(), category: selectedCategory, description: descriptionController.text.trim(), stock: stock, - qrCode: product.qrCode, - reference: product.reference, + qrCode: product.qrCode, // Conserver le QR code existant + reference: reference, + marque: marqueController.text.trim().isNotEmpty ? marqueController.text.trim() : null, + ram: ramController.text.trim().isNotEmpty ? ramController.text.trim() : null, + memoireInterne: memoireInterneController.text.trim().isNotEmpty ? memoireInterneController.text.trim() : null, + imei: imei.isNotEmpty ? imei : null, + pointDeVenteId: pointDeVenteId, ); - try { - await _productDatabase.updateProduct(updatedProduct); - Get.back(); - Get.snackbar( - 'Succès', - 'Produit modifié avec succès', - backgroundColor: Colors.green, - colorText: Colors.white, - ); - _loadProducts(); - } catch (e) { - Get.snackbar('Erreur', 'Modification échouée: $e'); - } - }, - child: const Text('Sauvegarder'), + await _productDatabase.updateProduct(updatedProduct); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit modifié avec succès!\nRéférence: $reference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 4), + icon: const Icon(Icons.check_circle, color: Colors.white), + ); + _loadProducts(); + _loadPointsDeVente(); // Recharger aussi les points de vente + } catch (e) { + Get.snackbar('Erreur', 'Modification du produit échouée: $e'); + } + }, + icon: const Icon(Icons.save), + label: const Text('Sauvegarder les modifications'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), - ], - ), - ); - } + ), + ], + ), + ); +} void _deleteProduct(Product product) { Get.dialog( diff --git a/lib/Views/RolePermissionPage.dart b/lib/Views/RolePermissionPage.dart index af87752..8b1dd71 100644 --- a/lib/Views/RolePermissionPage.dart +++ b/lib/Views/RolePermissionPage.dart @@ -19,6 +19,8 @@ class _RolePermissionsPageState extends State { List permissions = []; List> menus = []; Map> menuPermissionsMap = {}; + bool isLoading = true; + String? errorMessage; @override void initState() { @@ -27,152 +29,559 @@ class _RolePermissionsPageState extends State { } Future _initData() async { - final perms = await db.getAllPermissions(); - final menuList = await db.database.then((db) => db.query('menu')); + try { + setState(() { + isLoading = true; + errorMessage = null; + }); - Map> tempMenuPermissionsMap = {}; + final perms = await db.getAllPermissions(); + final menuList = await db.getAllMenus(); // Utilise la nouvelle méthode - for (var menu in menuList) { - final menuId = menu['id'] as int; - final menuPerms = await db.getPermissionsForRoleAndMenu( - widget.role.id!, menuId); + Map> tempMenuPermissionsMap = {}; - tempMenuPermissionsMap[menuId] = { - for (var perm in perms) - perm.name: menuPerms.any((mp) => mp.name == perm.name) - }; - } + for (var menu in menuList) { + final menuId = menu['id'] as int; + final menuPerms = await db.getPermissionsForRoleAndMenu( + widget.role.id!, menuId); + + tempMenuPermissionsMap[menuId] = { + for (var perm in perms) + perm.name: menuPerms.any((mp) => mp.name == perm.name) + }; + } - setState(() { - permissions = perms; - menus = menuList; - menuPermissionsMap = tempMenuPermissionsMap; - }); + setState(() { + permissions = perms; + menus = menuList; + menuPermissionsMap = tempMenuPermissionsMap; + isLoading = false; + }); + } catch (e) { + setState(() { + errorMessage = 'Erreur lors du chargement des données: $e'; + isLoading = false; + }); + print("Erreur lors de l'initialisation des données: $e"); + } } Future _onPermissionToggle( int menuId, String permission, bool enabled) async { - final perm = permissions.firstWhere((p) => p.name == permission); - - if (enabled) { - await db.assignRoleMenuPermission( - widget.role.id!, menuId, perm.id!); - } else { - await db.removeRoleMenuPermission( - widget.role.id!, menuId, perm.id!); + try { + final perm = permissions.firstWhere((p) => p.name == permission); + + if (enabled) { + await db.assignRoleMenuPermission( + widget.role.id!, menuId, perm.id!); + } else { + await db.removeRoleMenuPermission( + widget.role.id!, menuId, perm.id!); + } + + setState(() { + menuPermissionsMap[menuId]![permission] = enabled; + }); + + // Afficher un message de confirmation + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + enabled + ? 'Permission "$permission" accordée' + : 'Permission "$permission" révoquée', + ), + backgroundColor: enabled ? Colors.green : Colors.orange, + duration: const Duration(seconds: 2), + ), + ); + } catch (e) { + print("Erreur lors de la modification de la permission: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la modification: $e'), + backgroundColor: Colors.red, + ), + ); } + } - setState(() { - menuPermissionsMap[menuId]![permission] = enabled; - }); + void _toggleAllPermissions(int menuId, bool enabled) { + for (var permission in permissions) { + _onPermissionToggle(menuId, permission.name, enabled); + } } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: "Permissions - ${widget.role.designation}", - // showBackButton: true, - ), - body: Padding( + int _getSelectedPermissionsCount(int menuId) { + return menuPermissionsMap[menuId]?.values.where((selected) => selected).length ?? 0; + } + + double _getPermissionPercentage(int menuId) { + if (permissions.isEmpty) return 0.0; + return _getSelectedPermissionsCount(menuId) / permissions.length; + } + + Widget _buildPermissionSummary() { + int totalPermissions = menus.length * permissions.length; + int selectedPermissions = 0; + + for (var menuId in menuPermissionsMap.keys) { + selectedPermissions += _getSelectedPermissionsCount(menuId); + } + + double percentage = totalPermissions > 0 ? selectedPermissions / totalPermissions : 0.0; + + return Card( + elevation: 4, + margin: const EdgeInsets.only(bottom: 16), + child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Gestion des permissions pour le rôle: ${widget.role.designation}', - style: Theme.of(context).textTheme.titleLarge?.copyWith( + Row( + children: [ + Icon(Icons.analytics, color: Colors.blue.shade600), + const SizedBox(width: 8), + Text( + 'Résumé des permissions', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold, + color: Colors.blue.shade700, ), + ), + ], ), - const SizedBox(height: 10), - const Text( - 'Sélectionnez les permissions pour chaque menu:', - style: TextStyle(fontSize: 14, color: Colors.grey), + const SizedBox(height: 12), + LinearProgressIndicator( + value: percentage, + backgroundColor: Colors.grey.shade300, + valueColor: AlwaysStoppedAnimation( + percentage > 0.7 ? Colors.green : + percentage > 0.3 ? Colors.orange : Colors.red, + ), ), - const SizedBox(height: 20), - if (permissions.isNotEmpty && menus.isNotEmpty) - Expanded( - child: ListView.builder( - itemCount: menus.length, - itemBuilder: (context, index) { - final menu = menus[index]; - final menuId = menu['id'] as int; - final menuName = menu['name'] as String; - - return Card( - margin: const EdgeInsets.only(bottom: 15), - elevation: 3, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - menuName, - style: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 8), - Wrap( - spacing: 10, - runSpacing: 10, - children: permissions.map((perm) { - final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false; - return FilterChip( - label: perm.name, - selected: isChecked, - onSelected: (bool value) { - _onPermissionToggle(menuId, perm.name, value); - }, - ); - }).toList(), - ), - ], - ), - ), - ); - }, + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$selectedPermissions / $totalPermissions permissions', + style: const TextStyle(fontWeight: FontWeight.w500), ), - ) - else - const Expanded( - child: Center( - child: CircularProgressIndicator(), + Text( + '${(percentage * 100).toStringAsFixed(1)}%', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue, + ), ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildMenuCard(Map menu) { + final menuId = menu['id'] as int; + final menuName = menu['name'] as String; + final menuRoute = menu['route'] as String; + final selectedCount = _getSelectedPermissionsCount(menuId); + final percentage = _getPermissionPercentage(menuId); + + return Card( + margin: const EdgeInsets.only(bottom: 16), + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ExpansionTile( + leading: CircleAvatar( + backgroundColor: percentage == 1.0 ? Colors.green : + percentage > 0 ? Colors.orange : Colors.red.shade100, + child: Icon( + Icons.menu, + color: percentage == 1.0 ? Colors.white : + percentage > 0 ? Colors.white : Colors.red, + ), + ), + title: Text( + menuName, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + menuRoute, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, ), + ), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: LinearProgressIndicator( + value: percentage, + backgroundColor: Colors.grey.shade300, + valueColor: AlwaysStoppedAnimation( + percentage == 1.0 ? Colors.green : + percentage > 0 ? Colors.orange : Colors.red, + ), + ), + ), + const SizedBox(width: 8), + Text( + '$selectedCount/${permissions.length}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ], ), + trailing: PopupMenuButton( + onSelected: (value) { + if (value == 'all') { + _toggleAllPermissions(menuId, true); + } else if (value == 'none') { + _toggleAllPermissions(menuId, false); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'all', + child: Row( + children: [ + Icon(Icons.select_all, color: Colors.green), + SizedBox(width: 8), + Text('Tout sélectionner'), + ], + ), + ), + const PopupMenuItem( + value: 'none', + child: Row( + children: [ + Icon(Icons.deselect, color: Colors.red), + SizedBox(width: 8), + Text('Tout désélectionner'), + ], + ), + ), + ], + child: const Icon(Icons.more_vert), + ), + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Permissions disponibles:', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: permissions.map((perm) { + final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false; + return CustomFilterChip( + label: perm.name, + selected: isChecked, + onSelected: (bool value) { + _onPermissionToggle(menuId, perm.name, value); + }, + ); + }).toList(), + ), + ], + ), + ), + ], ), ); } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: "Permissions - ${widget.role.designation}", + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : errorMessage != null + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Colors.red.shade400, + ), + const SizedBox(height: 16), + Text( + 'Erreur de chargement', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.red.shade600, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + errorMessage!, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey.shade600), + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _initData, + icon: const Icon(Icons.refresh), + label: const Text('Réessayer'), + ), + ], + ), + ) + : Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête avec informations du rôle + Card( + elevation: 4, + margin: const EdgeInsets.only(bottom: 16), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + CircleAvatar( + backgroundColor: widget.role.designation == 'Super Admin' + ? Colors.red.shade100 + : Colors.blue.shade100, + radius: 24, + child: Icon( + Icons.person, + color: widget.role.designation == 'Super Admin' + ? Colors.red.shade700 + : Colors.blue.shade700, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Gestion des permissions', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + 'Rôle: ${widget.role.designation}', + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade700, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + 'Configurez les accès pour chaque menu', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ], + ), + ), + ), + + // Résumé des permissions + if (permissions.isNotEmpty && menus.isNotEmpty) + _buildPermissionSummary(), + + // Liste des menus et permissions + if (permissions.isNotEmpty && menus.isNotEmpty) + Expanded( + child: ListView.builder( + itemCount: menus.length, + itemBuilder: (context, index) { + return _buildMenuCard(menus[index]); + }, + ), + ) + else + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Aucune donnée disponible', + style: TextStyle( + fontSize: 18, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'Permissions: ${permissions.length} | Menus: ${menus.length}', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _initData, + icon: const Icon(Icons.refresh), + label: const Text('Actualiser'), + ), + ], + ), + ), + ), + ], + ), + ), + floatingActionButton: !isLoading && errorMessage == null + ? FloatingActionButton.extended( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.save), + label: const Text('Enregistrer'), + backgroundColor: Colors.green, + ) + : null, + ); + } } -class FilterChip extends StatelessWidget { +class CustomFilterChip extends StatelessWidget { final String label; final bool selected; final ValueChanged onSelected; - const FilterChip({ + const CustomFilterChip({ super.key, required this.label, required this.selected, required this.onSelected, }); + Color _getChipColor(String label) { + switch (label.toLowerCase()) { + case 'view': + case 'read': + return Colors.blue; + case 'create': + return Colors.green; + case 'update': + return Colors.orange; + case 'delete': + return Colors.red; + case 'admin': + return Colors.purple; + case 'manage': + return Colors.indigo; + default: + return Colors.grey; + } + } + + IconData _getChipIcon(String label) { + switch (label.toLowerCase()) { + case 'view': + case 'read': + return Icons.visibility; + case 'create': + return Icons.add; + case 'update': + return Icons.edit; + case 'delete': + return Icons.delete; + case 'admin': + return Icons.admin_panel_settings; + case 'manage': + return Icons.settings; + default: + return Icons.security; + } + } + @override Widget build(BuildContext context) { - return ChoiceChip( - label: Text(label), + final color = _getChipColor(label); + final icon = _getChipIcon(label); + + return FilterChip( + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 16, + color: selected ? Colors.white : color, + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + color: selected ? Colors.white : color, + fontWeight: FontWeight.w500, + ), + ), + ], + ), selected: selected, onSelected: onSelected, - selectedColor: Colors.blue, - labelStyle: TextStyle( - color: selected ? Colors.white : Colors.black, - ), + selectedColor: color, + backgroundColor: color.withOpacity(0.1), + checkmarkColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: selected ? color : color.withOpacity(0.3), + width: 1, + ), ), + elevation: selected ? 4 : 1, + pressElevation: 8, ); } } \ No newline at end of file diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart index c93a338..f78ebb0 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -15,7 +15,7 @@ import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.dart'; - +import 'package:youmazgestion/Models/produit.dart'; class GestionCommandesPage extends StatefulWidget { const GestionCommandesPage({super.key}); @@ -74,68 +74,68 @@ class _GestionCommandesPageState extends State { } Future _updateStatut(int commandeId, StatutCommande newStatut, {int? validateurId}) async { - // D'abord récupérer la commande existante pour avoir toutes ses valeurs - final commandeExistante = await _database.getCommandeById(commandeId); - - if (commandeExistante == null) { + final commandeExistante = await _database.getCommandeById(commandeId); + + if (commandeExistante == null) { + Get.snackbar( + 'Erreur', + 'Commande introuvable', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + + if (validateurId != null) { + await _database.updateCommande(Commande( + id: commandeId, + clientId: commandeExistante.clientId, + dateCommande: commandeExistante.dateCommande, + statut: newStatut, + montantTotal: commandeExistante.montantTotal, + notes: commandeExistante.notes, + dateLivraison: commandeExistante.dateLivraison, + commandeurId: commandeExistante.commandeurId, + validateurId: validateurId, + clientNom: commandeExistante.clientNom, + clientPrenom: commandeExistante.clientPrenom, + clientEmail: commandeExistante.clientEmail, + remisePourcentage: commandeExistante.remisePourcentage, + remiseMontant: commandeExistante.remiseMontant, + montantApresRemise: commandeExistante.montantApresRemise, + )); + } else { + await _database.updateStatutCommande(commandeId, newStatut); + } + + await _loadCommandes(); + + String message = 'Statut de la commande mis à jour'; + Color backgroundColor = Colors.green; + + switch (newStatut) { + case StatutCommande.annulee: + message = 'Commande annulée avec succès'; + backgroundColor = Colors.orange; + break; + case StatutCommande.confirmee: + message = 'Commande confirmée'; + backgroundColor = Colors.blue; + break; + default: + break; + } + Get.snackbar( - 'Erreur', - 'Commande introuvable', + 'Succès', + message, snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, + backgroundColor: backgroundColor, colorText: Colors.white, + duration: const Duration(seconds: 2), ); - return; - } - - if (validateurId != null) { - // Mise à jour avec validateur - await _database.updateCommande(Commande( - id: commandeId, - clientId: commandeExistante.clientId, - dateCommande: commandeExistante.dateCommande, - statut: newStatut, - montantTotal: commandeExistante.montantTotal, - notes: commandeExistante.notes, - dateLivraison: commandeExistante.dateLivraison, - commandeurId: commandeExistante.commandeurId, - validateurId: validateurId, // On met à jour le validateur - clientNom: commandeExistante.clientNom, - clientPrenom: commandeExistante.clientPrenom, - clientEmail: commandeExistante.clientEmail, - )); - } else { - // Mise à jour simple du statut - await _database.updateStatutCommande(commandeId, newStatut); - } - - await _loadCommandes(); - - String message = 'Statut de la commande mis à jour'; - Color backgroundColor = Colors.green; - - switch (newStatut) { - case StatutCommande.annulee: - message = 'Commande annulée avec succès'; - backgroundColor = Colors.orange; - break; - case StatutCommande.confirmee: - message = 'Commande confirmée'; - backgroundColor = Colors.blue; - break; - default: - break; } - - Get.snackbar( - 'Succès', - message, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: backgroundColor, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); -} Future _showPaymentOptions(Commande commande) async { final selectedPayment = await showDialog( @@ -148,18 +148,86 @@ class _GestionCommandesPageState extends State { await _showCashPaymentDialog(commande, selectedPayment.amountGiven); } - // Confirmer la commande avec le validateur actuel await _updateStatut( commande.id!, StatutCommande.confirmee, validateurId: userController.userId, ); - // Générer le ticket de caisse await _generateReceipt(commande, selectedPayment); } } + Future _showDiscountDialog(Commande commande) async { + final discountData = await showDialog>( + context: context, + builder: (context) => DiscountDialog(commande: commande), + ); + + if (discountData != null) { + // Mettre à jour la commande avec la remise + final commandeAvecRemise = Commande( + id: commande.id, + clientId: commande.clientId, + dateCommande: commande.dateCommande, + statut: commande.statut, + montantTotal: commande.montantTotal, + notes: commande.notes, + dateLivraison: commande.dateLivraison, + commandeurId: commande.commandeurId, + validateurId: commande.validateurId, + clientNom: commande.clientNom, + clientPrenom: commande.clientPrenom, + clientEmail: commande.clientEmail, + remisePourcentage: discountData['pourcentage'], + remiseMontant: discountData['montant'], + montantApresRemise: discountData['montantFinal'], + ); + + await _database.updateCommande(commandeAvecRemise); + await _loadCommandes(); + + Get.snackbar( + 'Succès', + 'Remise appliquée avec succès', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } + } + + Future _showGiftDialog(Commande commande) async { + final selectedProduct = await showDialog( + context: context, + builder: (context) => GiftSelectionDialog(commande: commande), + ); + + if (selectedProduct != null) { + // Ajouter le produit cadeau à la commande avec prix = 0 + final detailCadeau = DetailCommande( + commandeId: commande.id!, + produitId: selectedProduct.id!, + quantite: 1, + prixUnitaire: 0.0, // Prix = 0 pour un cadeau + sousTotal: 0.0, + produitNom: selectedProduct.name, + estCadeau: true, // Nouveau champ pour identifier les cadeaux + ); + + await _database.createDetailCommande(detailCadeau); + await _loadCommandes(); + + Get.snackbar( + 'Succès', + 'Cadeau ajouté à la commande', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } + } + Future _showCashPaymentDialog(Commande commande, double amountGiven) async { final amountController = TextEditingController( text: amountGiven.toStringAsFixed(2), @@ -168,13 +236,21 @@ class _GestionCommandesPageState extends State { await showDialog( context: context, builder: (context) { - final change = amountGiven - commande.montantTotal; + final montantFinal = commande.montantApresRemise ?? commande.montantTotal; + final change = amountGiven - montantFinal; return AlertDialog( title: const Text('Paiement en liquide'), content: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Montant total: ${commande.montantTotal.toStringAsFixed(2)} MGA'), + if (commande.montantApresRemise != null) ...[ + Text('Montant original: ${commande.montantTotal.toStringAsFixed(2)} MGA'), + Text('Remise: ${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA'), + const SizedBox(height: 5), + Text('Montant à payer: ${montantFinal.toStringAsFixed(2)} MGA', + style: const TextStyle(fontWeight: FontWeight.bold)), + ] else + Text('Montant total: ${montantFinal.toStringAsFixed(2)} MGA'), const SizedBox(height: 10), TextField( controller: amountController, @@ -185,13 +261,13 @@ class _GestionCommandesPageState extends State { keyboardType: TextInputType.number, onChanged: (value) { final newAmount = double.tryParse(value) ?? 0; - if (newAmount >= commande.montantTotal) { + if (newAmount >= montantFinal) { setState(() {}); } }, ), const SizedBox(height: 20), - if (amountGiven >= commande.montantTotal) + if (amountGiven >= montantFinal) Text( 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', style: const TextStyle( @@ -200,7 +276,7 @@ class _GestionCommandesPageState extends State { color: Colors.green, ), ), - if (amountGiven < commande.montantTotal) + if (amountGiven < montantFinal) Text( 'Montant insuffisant', style: TextStyle( @@ -227,550 +303,539 @@ class _GestionCommandesPageState extends State { }, ); } -Future buildIconPhoneText() async { - final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); - return pw.Text(String.fromCharCode(0xf095), style: pw.TextStyle(font: font)); -} -Future buildIconCheckedText() async { - final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); - return pw.Text(String.fromCharCode(0xf14a), style: pw.TextStyle(font: font)); -} - -Future buildIconGlobeText() async { - final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); - return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font)); -} + Future buildIconPhoneText() async { + final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); + return pw.Text(String.fromCharCode(0xf095), style: pw.TextStyle(font: font)); + } + Future buildIconCheckedText() async { + final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); + return pw.Text(String.fromCharCode(0xf14a), style: pw.TextStyle(font: font)); + } - Future _generateInvoice(Commande commande) async { - final details = await _database.getDetailsCommande(commande.id!); - final client = await _database.getClientById(commande.clientId); - final pointDeVente = await _database.getPointDeVenteById(1); - final iconPhone = await buildIconPhoneText(); - final iconChecked = await buildIconCheckedText(); - final iconGlobe = await buildIconGlobeText(); - - // IMPORTANT: Récupérer tous les détails des produits AVANT de créer le PDF - final List> detailsAvecProduits = []; - for (final detail in details) { - final produit = await _database.getProductById(detail.produitId); - detailsAvecProduits.add({ - 'detail': detail, - 'produit': produit, - }); + Future buildIconGlobeText() async { + final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); + return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font)); } - - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); - - // Styles de texte - final smallTextStyle = pw.TextStyle(fontSize: 9); - final smallBoldTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold); - final normalTextStyle = pw.TextStyle(fontSize: 10); - final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold); - final boldTexClienttStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold); - final frameTextStyle = pw.TextStyle(fontSize: 10); - final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont); - final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont); - - pdf.addPage( - pw.Page( - margin: const pw.EdgeInsets.all(20), - build: (pw.Context context) { - return pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // Première ligne: Logo à gauche, informations à droite - pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - // Colonne de gauche avec logo et points de vente - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // Logo - pw.Container( - width: 150, - height: 150, - child: pw.Image(image), - ), - pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo), - pw.SizedBox(height: 12), - // Liste des points de vente avec checkbox - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]), - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]), - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]), - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]), - pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]), - ], - ), - - // Informations de contact - pw.SizedBox(height: 10), - pw.Row(children: [iconPhone, pw.SizedBox(width: 5), pw.Text('033 37 808 18', style: smallTextStyle)]), - pw.Row(children: [iconGlobe, pw.SizedBox(width: 5), pw.Text('www.guycom.mg', style: smallTextStyle)]), - pw.Text('Facebook: GuyCom', style: smallTextStyle), - ], - ), - - // Colonne de droite avec cadres de texte - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldTexClienttStyle), - pw.SizedBox(height: 10), - pw.Container(width: 200, height: 1, color: PdfColors.black), + + Future _generateInvoice(Commande commande) async { + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + final pointDeVente = await _database.getPointDeVenteById(1); + final iconPhone = await buildIconPhoneText(); + final iconChecked = await buildIconCheckedText(); + final iconGlobe = await buildIconGlobeText(); + + final List> detailsAvecProduits = []; + for (final detail in details) { + final produit = await _database.getProductById(detail.produitId); + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + } + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); + + final smallTextStyle = pw.TextStyle(fontSize: 9); + final smallBoldTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold); + final normalTextStyle = pw.TextStyle(fontSize: 10); + final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold); + final boldTexClienttStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold); + final frameTextStyle = pw.TextStyle(fontSize: 10); + final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont); + final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont); + + pdf.addPage( + pw.Page( + margin: const pw.EdgeInsets.all(20), + build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête avec logo et informations + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Container( + width: 150, + height: 150, + child: pw.Image(image), + ), + pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo), + pw.SizedBox(height: 12), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]), + pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]), + pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]), + pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]), + pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]), + ], + ), + pw.SizedBox(height: 10), + pw.Row(children: [iconPhone, pw.SizedBox(width: 5), pw.Text('033 37 808 18', style: smallTextStyle)]), + pw.Row(children: [iconGlobe, pw.SizedBox(width: 5), pw.Text('www.guycom.mg', style: smallTextStyle)]), + pw.Text('Facebook: GuyCom', style: smallTextStyle), + ], + ), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldTexClienttStyle), + pw.SizedBox(height: 10), + pw.Container(width: 200, height: 1, color: PdfColors.black), + pw.SizedBox(height: 10), + pw.Row( + children: [ + pw.Container( + width: 100, + height: 40, + padding: const pw.EdgeInsets.all(5), + child: pw.Column( + children: [ + pw.Text('Boutique:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTexClienttStyle), + ] + ) + ), + pw.SizedBox(width: 10), + pw.Container( + width: 100, + height: 40, + padding: const pw.EdgeInsets.all(5), + child: pw.Column( + children: [ + pw.Text('Bon de livraison N°:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle), + ] + ) + ), + ], + ), + pw.SizedBox(height: 20), + pw.Container( + width: 300, + height: 100, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1), + ), + padding: const pw.EdgeInsets.all(10), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text('ID Client: ', style: frameTextStyle), + pw.SizedBox(height: 5), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle), + pw.SizedBox(height: 5), + pw.Container(width: 200, height: 1, color: PdfColors.black), + pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle), + pw.SizedBox(height: 10), + pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle), + ], + ), + ), + ], + ), + ], + ), + + pw.SizedBox(height: 20), + + // Tableau des produits + pw.Table( + border: pw.TableBorder.all(width: 0.5), + columnWidths: { + 0: const pw.FlexColumnWidth(3), + 1: const pw.FlexColumnWidth(1), + 2: const pw.FlexColumnWidth(2), + 3: const pw.FlexColumnWidth(2), + }, + children: [ + pw.TableRow( + decoration: const pw.BoxDecoration(color: PdfColors.grey200), + children: [ + pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Désignations', style: boldTextStyle)), + pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)), + pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)), + pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)), + ], + ), + + ...detailsAvecProduits.map((item) { + final detail = item['detail'] as DetailCommande; + final produit = item['produit']; - // Deux petits cadres côte à côte - pw.SizedBox(height: 10), - pw.Row( + return pw.TableRow( children: [ - pw.Container( - width: 100, - height: 40, - padding: const pw.EdgeInsets.all(5), + pw.Padding( + padding: const pw.EdgeInsets.all(4), child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('Boutique:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTexClienttStyle), - ] - ) + pw.Row( + children: [ + pw.Text(detail.produitNom ?? 'Produit inconnu', + style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)), + if (detail.estCadeau == true) + pw.Text(' (CADEAU)', + style: pw.TextStyle(fontSize: 8, color: PdfColors.red, fontWeight: pw.FontWeight.bold)), + ], + ), + pw.SizedBox(height: 2), + if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty) + pw.Text('${produit.category} ${produit.marque}', style: smallTextStyle), + if (produit?.imei != null && produit!.imei!.isNotEmpty) + pw.Text('${produit.imei}', style: smallTextStyle), + if (produit?.reference != null && produit!.reference!.isNotEmpty && produit?.ram != null && produit!.ram!.isNotEmpty && produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty) + pw.Text('${produit.ram} | ${produit.memoireInterne} | ${produit.reference}', style: smallTextStyle), + ], + ), ), - pw.SizedBox(width: 10), - pw.Container( - width: 100, - height: 40, - padding: const pw.EdgeInsets.all(5), - child: pw.Column( - children: [ - pw.Text('Bon de livraison N°:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle), - ] - ) + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), + ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(0)}', + style: normalTextStyle, textAlign: pw.TextAlign.right), ), + pw.Padding( + padding: const pw.EdgeInsets.all(4), + child: pw.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(0)}', + style: normalTextStyle, textAlign: pw.TextAlign.right), + ), + ], + ); + }).toList(), + ], + ), + + pw.SizedBox(height: 10), + + // Totaux avec remise + pw.Column( + children: [ + if (commande.montantApresRemise != null) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('SOUS-TOTAL', style: normalTextStyle), + pw.SizedBox(width: 20), + pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: normalTextStyle), ], ), - - // Grand cadre en dessous - pw.SizedBox(height: 20), - pw.Container( - width: 300, - height: 100, - decoration: pw.BoxDecoration( - border: pw.Border.all(color: PdfColors.black, width: 1), - ), - padding: const pw.EdgeInsets.all(10), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text('ID Client: ', style: frameTextStyle), - pw.SizedBox(height: 5), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle), - pw.SizedBox(height: 5), - pw.Container(width: 200, height: 1, color: PdfColors.black), - pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle), - pw.SizedBox(height: 10), - pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle), - ], - ), + pw.SizedBox(height: 5), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('REMISE', style: normalTextStyle), + pw.SizedBox(width: 20), + pw.Text('-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(0)}', + style: pw.TextStyle(fontSize: 10, color: PdfColors.red)), + ], ), + pw.SizedBox(height: 5), + pw.Container(width: 200, height: 1, color: PdfColors.black), + pw.SizedBox(height: 5), ], + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('TOTAL', style: boldTextStyle), + pw.SizedBox(width: 20), + pw.Text('${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(0)}', style: boldTextStyle), + ], + ), + ], + ), + + pw.SizedBox(height: 10), + + pw.Text('Arrêté à la somme de: ${_numberToWords((commande.montantApresRemise ?? commande.montantTotal).toInt())} Ariary', style: italicTextStyle), + + pw.SizedBox(height: 30), + + // Signatures + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('Signature du vendeur', style: smallTextStyle), + pw.SizedBox(height: 20), + pw.Container(width: 150, height: 1, color: PdfColors.black), + ], + ), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('Signature du client', style: smallTextStyle), + pw.SizedBox(height: 20), + pw.Container(width: 150, height: 1, color: PdfColors.black), + ], + ), + ], + ), + ], + ); + }, + ), + ); + + final output = await getTemporaryDirectory(); + final file = File('${output.path}/facture_${commande.id}.pdf'); + await file.writeAsBytes(await pdf.save()); + await OpenFile.open(file.path); + } + + String _numberToWords(int number) { + NumbersToLetters.toLetters('fr', number); + return NumbersToLetters.toLetters('fr', number); + } + + Future _generateReceipt(Commande commande, PaymentMethod payment) async { + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + final commandeur = commande.commandeurId != null + ? await _database.getUserById(commande.commandeurId!) + : null; + final validateur = commande.validateurId != null + ? await _database.getUserById(commande.validateurId!) + : null; + final pointDeVente = commandeur?.pointDeVenteId != null + ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) + : null; + + final List> detailsAvecProduits = []; + for (final detail in details) { + final produit = await _database.getProductById(detail.produitId); + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + } + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity), + margin: const pw.EdgeInsets.all(4), + build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Center( + child: pw.Container( + width: 40, + height: 40, + child: pw.Image(image), ), - ], - ), - - pw.SizedBox(height: 20), - - // Tableau des produits avec plus de colonnes - pw.Table( - border: pw.TableBorder.all(width: 0.5), - columnWidths: { - 0: const pw.FlexColumnWidth(3), // Désignation - 1: const pw.FlexColumnWidth(1), // Qté - 2: const pw.FlexColumnWidth(2), // Prix unitaire - 3: const pw.FlexColumnWidth(2), // Montant - }, - children: [ - // En-tête du tableau - pw.TableRow( - decoration: const pw.BoxDecoration(color: PdfColors.grey200), + ), + pw.SizedBox(height: 4), + + pw.Text('GUYCOM MADAGASCAR', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + )), + pw.Text('Tél: 033 37 808 18', style: const pw.TextStyle(fontSize: 7)), + pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)), + + pw.SizedBox(height: 6), + + pw.Text('TICKET DE CAISSE', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + decoration: pw.TextDecoration.underline, + )), + pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}', + style: const pw.TextStyle(fontSize: 8)), + pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', + style: const pw.TextStyle(fontSize: 8)), + + if (pointDeVente != null) + pw.Text('Point de vente: ${pointDeVente['designation']}', + style: const pw.TextStyle(fontSize: 8)), + + pw.Divider(thickness: 0.5), + + pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', + style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), + if (client?.telephone != null) + pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)), + + if (commandeur != null || validateur != null) + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Désignations', style: boldTextStyle)), - pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)), - pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)), - pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)), + pw.Divider(thickness: 0.5), + if (commandeur != null) + pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)), + if (validateur != null) + pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)), ], ), - - // Lignes des produits avec détails complets - ...detailsAvecProduits.map((item) { - final detail = item['detail'] as DetailCommande; - final produit = item['produit']; - - return pw.TableRow( + + pw.Divider(thickness: 0.5), + + // Détails des produits + pw.Table( + columnWidths: { + 0: const pw.FlexColumnWidth(3.5), + 1: const pw.FlexColumnWidth(1), + 2: const pw.FlexColumnWidth(1.5), + }, + children: [ + pw.TableRow( children: [ - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Column( + pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), + ], + decoration: const pw.BoxDecoration( + border: pw.Border(bottom: pw.BorderSide(width: 0.5)), + ), + ), + + ...detailsAvecProduits.map((item) { + final detail = item['detail'] as DetailCommande; + final produit = item['produit']; + + return pw.TableRow( + decoration: const pw.BoxDecoration( + border: pw.Border(bottom: pw.BorderSide(width: 0.2))), + children: [ + pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - // Nom du produit - pw.Text(detail.produitNom ?? 'Produit inconnu', - style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)), - pw.SizedBox(height: 2), - - - if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty) - pw.Text('${produit.category} ${produit.marque}', style: smallTextStyle), - - // IMEI - if (produit?.imei != null && produit!.imei!.isNotEmpty) - pw.Text('${produit.imei}', style: smallTextStyle), - - - // Référence - if (produit?.reference != null && produit!.reference!.isNotEmpty && produit?.ram != null && produit!.ram!.isNotEmpty && produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty) - pw.Text('${produit.ram} | ${produit.memoireInterne} | ${produit.reference}', style: smallTextStyle), - - // // IMEI - // if (produit?.imei != null && produit!.imei!.isNotEmpty) - // pw.Text('IMEI: ${produit.imei}', style: smallTextStyle), - - // // RAM - // if (produit?.ram != null && produit!.ram!.isNotEmpty) - // pw.Text('RAM: ${produit.ram}', style: smallTextStyle), - - // // Stockage - // if (produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty) - // pw.Text('Stockage: ${produit.memoireInterne}', style: smallTextStyle), - - // // Catégorie - + pw.Row( + children: [ + pw.Text(detail.produitNom ?? 'Produit', + style: const pw.TextStyle(fontSize: 7)), + if (detail.estCadeau == true) + pw.Text(' (CADEAU)', + style: pw.TextStyle(fontSize: 6, color: PdfColors.red)), + ], + ), + if (produit?.reference != null) + pw.Text('Ref: ${produit!.reference}', + style: const pw.TextStyle(fontSize: 6)), + if (produit?.imei != null) + pw.Text('IMEI: ${produit!.imei}', + style: const pw.TextStyle(fontSize: 6)), ], ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right), - ), - ], - ); - }).toList(), - ], - ), - - pw.SizedBox(height: 10), - - // Total - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Text('TOTAL', style: boldTextStyle), - pw.SizedBox(width: 20), - pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle), - ], - ), - - pw.SizedBox(height: 10), - - // Montant en lettres - pw.Text('Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', style: italicTextStyle), - - pw.SizedBox(height: 30), - - // Signatures - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, + pw.Text(detail.quantite.toString(), + style: const pw.TextStyle(fontSize: 7)), + pw.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(0)}', + style: const pw.TextStyle(fontSize: 7)), + ], + ); + }), + ], + ), + + pw.Divider(thickness: 0.5), + + // Totaux avec remise + if (commande.montantApresRemise != null) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - pw.Text('Signature du vendeur', style: smallTextStyle), - pw.SizedBox(height: 20), - pw.Container(width: 150, height: 1, color: PdfColors.black), + pw.Text('SOUS-TOTAL:', style: const pw.TextStyle(fontSize: 8)), + pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', + style: const pw.TextStyle(fontSize: 8)), ], ), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - pw.Text('Signature du client', style: smallTextStyle), - pw.SizedBox(height: 20), - pw.Container(width: 150, height: 1, color: PdfColors.black), + pw.Text('REMISE:', style: const pw.TextStyle(fontSize: 8)), + pw.Text('-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(0)} MGA', + style: pw.TextStyle(fontSize: 8, color: PdfColors.red)), ], ), + pw.SizedBox(height: 3), ], - ), - ], - ); - }, - ), - ); - - final output = await getTemporaryDirectory(); - final file = File('${output.path}/facture_${commande.id}.pdf'); - await file.writeAsBytes(await pdf.save()); - await OpenFile.open(file.path); -} - - - pw.Widget _buildCheckboxPointDeVente(String text, bool checked) { - return pw.Row( - children: [ - pw.Container( - width: 10, - height: 10, - decoration: pw.BoxDecoration( - border: pw.Border.all(width: 1), - color: checked ? PdfColors.black : PdfColors.white, - ), - ), - pw.SizedBox(width: 5), - pw.Text(text, style: pw.TextStyle(fontSize: 9)), - ], + + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('TOTAL:', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + pw.Text('${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(0)} MGA', + style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), + ], + ), + + pw.SizedBox(height: 6), + + // Détails du paiement + pw.Text('MODE DE PAIEMENT:', + style: const pw.TextStyle(fontSize: 8)), + pw.Text( + payment.type == PaymentType.cash + ? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)' + : payment.type == PaymentType.card + ? 'CARTE BANCAIRE' + : payment.type == PaymentType.mvola + ? 'MVOLA' + : payment.type == PaymentType.orange + ? 'ORANGE MONEY' + : 'AIRTEL MONEY', + style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), + ), + + if (payment.type == PaymentType.cash && payment.amountGiven > (commande.montantApresRemise ?? commande.montantTotal)) + pw.Text('Monnaie rendue: ${(payment.amountGiven - (commande.montantApresRemise ?? commande.montantTotal)).toStringAsFixed(0)} MGA', + style: const pw.TextStyle(fontSize: 8)), + + pw.SizedBox(height: 12), + + pw.Text('Article non échangeable - Garantie selon conditions', + style: const pw.TextStyle(fontSize: 6)), + pw.Text('Ticket à conserver comme justificatif', + style: const pw.TextStyle(fontSize: 6)), + pw.SizedBox(height: 8), + pw.Text('Merci pour votre confiance !', + style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)), + ], + ); + }, + ), ); - } - String _numberToWords(int number) { - // Implémentez la conversion du nombre en lettres ici - // Exemple simplifié: - NumbersToLetters.toLetters('fr', number); - return NumbersToLetters.toLetters('fr', number); + final output = await getTemporaryDirectory(); + final file = File('${output.path}/ticket_${commande.id}.pdf'); + await file.writeAsBytes(await pdf.save()); + await OpenFile.open(file.path); } - Future _generateReceipt(Commande commande, PaymentMethod payment) async { - final details = await _database.getDetailsCommande(commande.id!); - final client = await _database.getClientById(commande.clientId); - final commandeur = commande.commandeurId != null - ? await _database.getUserById(commande.commandeurId!) - : null; - final validateur = commande.validateurId != null - ? await _database.getUserById(commande.validateurId!) - : null; - final pointDeVente = commandeur?.pointDeVenteId != null - ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) - : null; - - // Récupérer les détails complets des produits - final List> detailsAvecProduits = []; - for (final detail in details) { - final produit = await _database.getProductById(detail.produitId); - detailsAvecProduits.add({ - 'detail': detail, - 'produit': produit, - }); - } - - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - - pdf.addPage( - pw.Page( - pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity), - margin: const pw.EdgeInsets.all(4), - build: (pw.Context context) { - return pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - // En-tête avec logo - pw.Center( - child: pw.Container( - width: 40, - height: 40, - child: pw.Image(image), - ), - ), - pw.SizedBox(height: 4), - - // Informations de l'entreprise - pw.Text('GUYCOM MADAGASCAR', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - )), - pw.Text('Tél: 033 37 808 18', style: const pw.TextStyle(fontSize: 7)), - pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)), - - pw.SizedBox(height: 6), - - // Titre et numéro de ticket - pw.Text('TICKET DE CAISSE', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - decoration: pw.TextDecoration.underline, - )), - pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}', - style: const pw.TextStyle(fontSize: 8)), - pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', - style: const pw.TextStyle(fontSize: 8)), - - if (pointDeVente != null) - pw.Text('Point de vente: ${pointDeVente['designation']}', - style: const pw.TextStyle(fontSize: 8)), - - pw.Divider(thickness: 0.5), - - // Informations client - pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', - style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), - if (client?.telephone != null) - pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)), - - // Personnel impliqué - if (commandeur != null || validateur != null) - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Divider(thickness: 0.5), - if (commandeur != null) - pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)), - if (validateur != null) - pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)), - ], - ), - - pw.Divider(thickness: 0.5), - - // Détails des produits - pw.Table( - columnWidths: { - 0: const pw.FlexColumnWidth(3.5), - 1: const pw.FlexColumnWidth(1), - 2: const pw.FlexColumnWidth(1.5), - }, - children: [ - // En-tête du tableau - pw.TableRow( - children: [ - pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), - pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), - pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), - ], - decoration: const pw.BoxDecoration( - border: pw.Border(bottom: pw.BorderSide(width: 0.5)), - - ),), - - // Lignes des produits - ...detailsAvecProduits.map( (item) { - final detail = item['detail'] as DetailCommande; - final produit = item['produit']; - - return pw.TableRow( - decoration: const pw.BoxDecoration( - border: pw.Border(bottom: pw.BorderSide(width: 0.2))), - children: [ - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text(detail.produitNom ?? 'Produit', - style: const pw.TextStyle(fontSize: 7)), - if (produit?.reference != null) - pw.Text('Ref: ${produit!.reference}', - style: const pw.TextStyle(fontSize: 6)), - if (produit?.imei != null) - pw.Text('IMEI: ${produit!.imei}', - style: const pw.TextStyle(fontSize: 6)), - ], - ), - pw.Text(detail.quantite.toString(), - style: const pw.TextStyle(fontSize: 7)), - pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', - style: const pw.TextStyle(fontSize: 7)), - ], - ); - }), - ], - ), - - pw.Divider(thickness: 0.5), - - // Total et paiement - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text('TOTAL:', - style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), - pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', - style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), - ], - ), - - pw.SizedBox(height: 6), - - // Détails du paiement - pw.Text('MODE DE PAIEMENT:', - style: const pw.TextStyle(fontSize: 8)), - pw.Text( - payment.type == PaymentType.cash - ? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)' - : 'CARTE BANCAIRE', - style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), - ), - - if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal) - pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(0)} MGA', - style: const pw.TextStyle(fontSize: 8)), - - pw.SizedBox(height: 12), - - // Mentions légales et remerciements - pw.Text('Article non échangeable - Garantie selon conditions', - style: const pw.TextStyle(fontSize: 6)), - pw.Text('Ticket à conserver comme justificatif', - style: const pw.TextStyle(fontSize: 6)), - pw.SizedBox(height: 8), - pw.Text('Merci pour votre confiance !', - style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)), - ], - ); - }, - ), - ); - - final output = await getTemporaryDirectory(); - final file = File('${output.path}/ticket_${commande.id}.pdf'); - await file.writeAsBytes(await pdf.save()); - await OpenFile.open(file.path); -} - - pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) { - return pw.Padding( - padding: const pw.EdgeInsets.all(4.0), - child: pw.Text(text, style: style ?? pw.TextStyle(fontSize: 8)), - ); - } - - Color _getStatutColor(StatutCommande statut) { - switch (statut) { - case StatutCommande.enAttente: - return Colors.orange.shade100; - case StatutCommande.confirmee: - return Colors.blue.shade100; - // case StatutCommande.enPreparation: - // return Colors.amber.shade100; - // case StatutCommande.expediee: - // return Colors.purple.shade100; - // case StatutCommande.livree: - // return Colors.green.shade100; - case StatutCommande.annulee: - return Colors.red.shade100; - } + + Color _getStatutColor(StatutCommande statut) { + switch (statut) { + case StatutCommande.enAttente: + return Colors.orange.shade100; + case StatutCommande.confirmee: + return Colors.blue.shade100; + case StatutCommande.annulee: + return Colors.red.shade100; + } } IconData _getStatutIcon(StatutCommande statut) { @@ -779,12 +844,6 @@ Future buildIconGlobeText() async { return Icons.schedule; case StatutCommande.confirmee: return Icons.check_circle_outline; - // case StatutCommande.enPreparation: - // return Icons.settings; - // case StatutCommande.expediee: - // return Icons.local_shipping; - // case StatutCommande.livree: - // return Icons.check_circle; case StatutCommande.annulee: return Icons.cancel; } @@ -812,7 +871,6 @@ Future buildIconGlobeText() async { // Logo et titre Row( children: [ - // Logo de l'entreprise Container( width: 50, height: 50, @@ -1261,13 +1319,36 @@ Future buildIconGlobeText() async { color: Colors.green.shade600, ), const SizedBox(width: 4), - Text( - '${commande.montantTotal.toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.green.shade700, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (commande.montantApresRemise != null) ...[ + Text( + 'Total: ${commande.montantTotal.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + decoration: TextDecoration.lineThrough, + ), + ), + Text( + 'Final: ${commande.montantApresRemise!.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ] else + Text( + '${commande.montantTotal.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ], ), ], ), @@ -1318,6 +1399,8 @@ Future buildIconGlobeText() async { commande: commande, onStatutChanged: _updateStatut, onPaymentSelected: _showPaymentOptions, + onDiscountSelected: _showDiscountDialog, + onGiftSelected: _showGiftDialog, ), ], ), @@ -1339,12 +1422,6 @@ Future buildIconGlobeText() async { return 'En attente'; case StatutCommande.confirmee: return 'Confirmée'; - // case StatutCommande.enPreparation: - // return 'En préparation'; - // case StatutCommande.expediee: - // return 'Expédiée'; - // case StatutCommande.livree: - // return 'Livrée'; case StatutCommande.annulee: return 'Annulée'; } @@ -1416,10 +1493,14 @@ class _CommandeDetails extends StatelessWidget { ), ...details.map((detail) => TableRow( children: [ - _buildTableCell(detail.produitNom ?? 'Produit inconnu'), + _buildTableCell( + detail.estCadeau == true + ? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)' + : detail.produitNom ?? 'Produit inconnu' + ), _buildTableCell('${detail.quantite}'), - _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'), - _buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'), + _buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'), + _buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(2)} MGA'), ], )), ], @@ -1433,23 +1514,60 @@ class _CommandeDetails extends StatelessWidget { borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade200), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( children: [ - const Text( - 'Total de la commande:', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + if (commande.montantApresRemise != null) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Sous-total:', + style: TextStyle(fontSize: 14), + ), + Text( + '${commande.montantTotal.toStringAsFixed(2)} MGA', + style: const TextStyle(fontSize: 14), + ), + ], ), - ), - Text( - '${commande.montantTotal.toStringAsFixed(2)} MGA', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: Colors.green.shade700, + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Remise:', + style: TextStyle(fontSize: 14), + ), + Text( + '-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA', + style: const TextStyle( + fontSize: 14, + color: Colors.red, + ), + ), + ], ), + const Divider(), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total de la commande:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + '${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(2)} MGA', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.green.shade700, + ), + ), + ], ), ], ), @@ -1490,11 +1608,15 @@ class _CommandeActions extends StatelessWidget { final Commande commande; final Function(int, StatutCommande) onStatutChanged; final Function(Commande) onPaymentSelected; + final Function(Commande) onDiscountSelected; + final Function(Commande) onGiftSelected; const _CommandeActions({ required this.commande, required this.onStatutChanged, required this.onPaymentSelected, + required this.onDiscountSelected, + required this.onGiftSelected, }); @override @@ -1533,6 +1655,18 @@ class _CommandeActions extends StatelessWidget { switch (commande.statut) { case StatutCommande.enAttente: buttons.addAll([ + _buildActionButton( + label: 'Remise', + icon: Icons.percent, + color: Colors.orange, + onPressed: () => onDiscountSelected(commande), + ), + _buildActionButton( + label: 'Cadeau', + icon: Icons.card_giftcard, + color: Colors.purple, + onPressed: () => onGiftSelected(commande), + ), _buildActionButton( label: 'Confirmer', icon: Icons.check_circle, @@ -1554,9 +1688,6 @@ class _CommandeActions extends StatelessWidget { break; case StatutCommande.confirmee: - // case StatutCommande.enPreparation: - // case StatutCommande.expediee: - // case StatutCommande.livree: buttons.add( Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), @@ -1693,8 +1824,326 @@ class _CommandeActions extends StatelessWidget { } } -enum PaymentType { cash, card , - +// Dialog pour la remise +class DiscountDialog extends StatefulWidget { + final Commande commande; + + const DiscountDialog({super.key, required this.commande}); + + @override + _DiscountDialogState createState() => _DiscountDialogState(); +} + +class _DiscountDialogState extends State { + final _pourcentageController = TextEditingController(); + final _montantController = TextEditingController(); + bool _isPercentage = true; + double _montantFinal = 0; + + @override + void initState() { + super.initState(); + _montantFinal = widget.commande.montantTotal; + } + + void _calculateDiscount() { + double discount = 0; + + if (_isPercentage) { + final percentage = double.tryParse(_pourcentageController.text) ?? 0; + discount = (widget.commande.montantTotal * percentage) / 100; + } else { + discount = double.tryParse(_montantController.text) ?? 0; + } + + setState(() { + _montantFinal = widget.commande.montantTotal - discount; + if (_montantFinal < 0) _montantFinal = 0; + }); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Appliquer une remise'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Montant original: ${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), + const SizedBox(height: 16), + + // Choix du type de remise + Row( + children: [ + Expanded( + child: RadioListTile( + title: const Text('Pourcentage'), + value: true, + groupValue: _isPercentage, + onChanged: (value) { + setState(() { + _isPercentage = value!; + _calculateDiscount(); + }); + }, + ), + ), + Expanded( + child: RadioListTile( + title: const Text('Montant fixe'), + value: false, + groupValue: _isPercentage, + onChanged: (value) { + setState(() { + _isPercentage = value!; + _calculateDiscount(); + }); + }, + ), + ), + ], + ), + + const SizedBox(height: 16), + + if (_isPercentage) + TextField( + controller: _pourcentageController, + decoration: const InputDecoration( + labelText: 'Pourcentage de remise', + suffixText: '%', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (value) => _calculateDiscount(), + ) + else + TextField( + controller: _montantController, + decoration: const InputDecoration( + labelText: 'Montant de remise', + suffixText: 'MGA', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (value) => _calculateDiscount(), + ), + + const SizedBox(height: 16), + + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Montant final:'), + Text( + '${_montantFinal.toStringAsFixed(2)} MGA', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + if (_montantFinal < widget.commande.montantTotal) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Économie:'), + Text( + '${(widget.commande.montantTotal - _montantFinal).toStringAsFixed(2)} MGA', + style: const TextStyle( + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: _montantFinal < widget.commande.montantTotal + ? () { + final pourcentage = _isPercentage + ? double.tryParse(_pourcentageController.text) + : null; + final montant = !_isPercentage + ? double.tryParse(_montantController.text) + : null; + + Navigator.pop(context, { + 'pourcentage': pourcentage, + 'montant': montant, + 'montantFinal': _montantFinal, + }); + } + : null, + child: const Text('Appliquer'), + ), + ], + ); + } + + @override + void dispose() { + _pourcentageController.dispose(); + _montantController.dispose(); + super.dispose(); + } +} + +// Dialog pour sélectionner un cadeau +class GiftSelectionDialog extends StatefulWidget { + final Commande commande; + + const GiftSelectionDialog({super.key, required this.commande}); + + @override + _GiftSelectionDialogState createState() => _GiftSelectionDialogState(); +} + +class _GiftSelectionDialogState extends State { + List _products = []; + List _filteredProducts = []; + final _searchController = TextEditingController(); + Product? _selectedProduct; + + @override + void initState() { + super.initState(); + _loadProducts(); + _searchController.addListener(_filterProducts); + } + + Future _loadProducts() async { + final products = await AppDatabase.instance.getProducts(); + setState(() { + _products = products.where((p) => p.stock > 0).toList(); + _filteredProducts = _products; + }); + } + + void _filterProducts() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredProducts = _products.where((product) { + return product.name.toLowerCase().contains(query) || + (product.reference?.toLowerCase().contains(query) ?? false) || + (product.category.toLowerCase().contains(query)); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Sélectionner un cadeau'), + content: SizedBox( + width: double.maxFinite, + height: 400, + child: Column( + children: [ + TextField( + controller: _searchController, + decoration: const InputDecoration( + labelText: 'Rechercher un produit', + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + Expanded( + child: ListView.builder( + itemCount: _filteredProducts.length, + itemBuilder: (context, index) { + final product = _filteredProducts[index]; + return Card( + child: ListTile( + leading: product.image != null + ? Image.network( + product.image!, + width: 50, + height: 50, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.image_not_supported), + ) + : const Icon(Icons.phone_android), + title: Text(product.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Catégorie: ${product.category}'), + Text('Stock: ${product.stock}'), + if (product.reference != null) + Text('Réf: ${product.reference}'), + ], + ), + trailing: Radio( + value: product, + groupValue: _selectedProduct, + onChanged: (value) { + setState(() { + _selectedProduct = value; + }); + }, + ), + onTap: () { + setState(() { + _selectedProduct = product; + }); + }, + ), + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: _selectedProduct != null + ? () => Navigator.pop(context, _selectedProduct) + : null, + child: const Text('Ajouter le cadeau'), + ), + ], + ); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } +} + +enum PaymentType { + cash, + card, mvola, orange, airtel @@ -1719,33 +2168,37 @@ class PaymentMethodDialog extends StatefulWidget { class _PaymentMethodDialogState extends State { PaymentType _selectedPayment = PaymentType.cash; final _amountController = TextEditingController(); + void _validatePayment() { - if (_selectedPayment == PaymentType.cash) { - final amountGiven = double.tryParse(_amountController.text) ?? 0; - if (amountGiven < widget.commande.montantTotal) { - Get.snackbar( - 'Erreur', - 'Le montant donné est insuffisant', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - return; + final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + + if (_selectedPayment == PaymentType.cash) { + final amountGiven = double.tryParse(_amountController.text) ?? 0; + if (amountGiven < montantFinal) { + Get.snackbar( + 'Erreur', + 'Le montant donné est insuffisant', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } } - } - Navigator.pop(context, PaymentMethod( - type: _selectedPayment, - amountGiven: _selectedPayment == PaymentType.cash - ? double.parse(_amountController.text) - : widget.commande.montantTotal, - )); -} + Navigator.pop(context, PaymentMethod( + type: _selectedPayment, + amountGiven: _selectedPayment == PaymentType.cash + ? double.parse(_amountController.text) + : montantFinal, + )); + } + @override void initState() { super.initState(); - // Initialiser avec le montant total de la commande - _amountController.text = widget.commande.montantTotal.toStringAsFixed(2); + final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + _amountController.text = montantFinal.toStringAsFixed(2); } @override @@ -1754,189 +2207,231 @@ class _PaymentMethodDialogState extends State { super.dispose(); } - @override -Widget build(BuildContext context) { - final amount = double.tryParse(_amountController.text) ?? 0; - final change = amount - widget.commande.montantTotal; + Widget build(BuildContext context) { + final amount = double.tryParse(_amountController.text) ?? 0; + final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + final change = amount - montantFinal; - return AlertDialog( - title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 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, - ), + return AlertDialog( + title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Affichage du montant à payer + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), ), - const SizedBox(width: 8), - Expanded( - child: _buildMobileMoneyTile( - title: 'Orange Money', - imagePath: 'assets/Orange_money.png', - value: PaymentType.orange, - ), + child: Column( + children: [ + if (widget.commande.montantApresRemise != null) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Montant original:'), + Text('${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Remise:'), + Text('-${(widget.commande.montantTotal - widget.commande.montantApresRemise!).toStringAsFixed(2)} MGA', + style: const TextStyle(color: Colors.red)), + ], + ), + const Divider(), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)), + Text('${montantFinal.toStringAsFixed(2)} MGA', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + ], + ), + ], ), - const SizedBox(width: 8), - Expanded( - child: _buildMobileMoneyTile( - title: 'Airtel Money', - imagePath: 'assets/airtel_money.png', - value: PaymentType.airtel, + ), + + 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(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(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), - Text( - 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: change >= 0 ? Colors.green : Colors.red, - ), + _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'), ), - ], - ); -} + 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, + 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), - ), - ], + 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, + 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), - ], + 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), + ], + ), ), ), - ), - ); -} -} + ); + } +} \ No newline at end of file diff --git a/lib/Views/gestionRole.dart b/lib/Views/gestionRole.dart index c6a4f06..7a7a048 100644 --- a/lib/Views/gestionRole.dart +++ b/lib/Views/gestionRole.dart @@ -29,62 +29,177 @@ class _HandleUserRoleState extends State { } Future _initData() async { - final roleList = await db.getRoles(); - final perms = await db.getAllPermissions(); - final menuList = await db.database.then((db) => db.query('menu')); + try { + final roleList = await db.getRoles(); + final perms = await db.getAllPermissions(); + + // Récupération mise à jour des menus avec gestion d'erreur + final menuList = await db.getAllMenus(); - Map>> tempRoleMenuPermissionsMap = {}; + Map>> tempRoleMenuPermissionsMap = {}; - for (var role in roleList) { - final roleId = role.id!; - tempRoleMenuPermissionsMap[roleId] = {}; + for (var role in roleList) { + final roleId = role.id!; + tempRoleMenuPermissionsMap[roleId] = {}; - for (var menu in menuList) { - final menuId = menu['id'] as int; - final menuPerms = await db.getPermissionsForRoleAndMenu(roleId, menuId); + for (var menu in menuList) { + final menuId = menu['id'] as int; + final menuPerms = await db.getPermissionsForRoleAndMenu(roleId, menuId); - tempRoleMenuPermissionsMap[roleId]![menuId] = { - for (var perm in perms) - perm.name: menuPerms.any((mp) => mp.name == perm.name) - }; + tempRoleMenuPermissionsMap[roleId]![menuId] = { + for (var perm in perms) + perm.name: menuPerms.any((mp) => mp.name == perm.name) + }; + } } - } - setState(() { - roles = roleList; - permissions = perms; - menus = menuList; - roleMenuPermissionsMap = tempRoleMenuPermissionsMap; - }); + setState(() { + roles = roleList; + permissions = perms; + menus = menuList; + roleMenuPermissionsMap = tempRoleMenuPermissionsMap; + }); + } catch (e) { + print("Erreur lors de l'initialisation des données: $e"); + // Afficher un message d'erreur à l'utilisateur + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors du chargement des données: $e'), + backgroundColor: Colors.red, + ), + ); + } } Future _addRole() async { String designation = _roleController.text.trim(); - if (designation.isEmpty) return; + if (designation.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Veuillez saisir une désignation pour le rôle'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + try { + // Vérifier si le rôle existe déjà + final existingRoles = roles.where((r) => r.designation.toLowerCase() == designation.toLowerCase()); + if (existingRoles.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Ce rôle existe déjà'), + backgroundColor: Colors.orange, + ), + ); + return; + } - await db.createRole(Role(designation: designation)); - _roleController.clear(); - await _initData(); + await db.createRole(Role(designation: designation)); + _roleController.clear(); + await _initData(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Rôle "$designation" créé avec succès'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + print("Erreur lors de la création du rôle: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la création du rôle: $e'), + backgroundColor: Colors.red, + ), + ); + } } Future _onPermissionToggle(int roleId, int menuId, String permission, bool enabled) async { - final perm = permissions.firstWhere((p) => p.name == permission); + try { + final perm = permissions.firstWhere((p) => p.name == permission); - if (enabled) { - await db.assignRoleMenuPermission(roleId, menuId, perm.id!); - } else { - await db.removeRoleMenuPermission(roleId, menuId, perm.id!); + if (enabled) { + await db.assignRoleMenuPermission(roleId, menuId, perm.id!); + } else { + await db.removeRoleMenuPermission(roleId, menuId, perm.id!); + } + + setState(() { + roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled; + }); + } catch (e) { + print("Erreur lors de la modification de la permission: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la modification de la permission: $e'), + backgroundColor: Colors.red, + ), + ); } + } - setState(() { - roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled; - }); + Future _deleteRole(Role role) async { + // Empêcher la suppression du Super Admin + if (role.designation == 'Super Admin') { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Impossible de supprimer le rôle Super Admin'), + backgroundColor: Colors.red, + ), + ); + return; + } + + // Demander confirmation + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Confirmer la suppression'), + content: Text('Êtes-vous sûr de vouloir supprimer le rôle "${role.designation}" ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Supprimer'), + ), + ], + ), + ); + + if (confirm == true) { + try { + await db.deleteRole(role.id); + await _initData(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Rôle "${role.designation}" supprimé avec succès'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + print("Erreur lors de la suppression du rôle: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la suppression du rôle: $e'), + backgroundColor: Colors.red, + ), + ); + } + } } @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar(title: "Gestion des rôles"), + appBar: CustomAppBar(title: "Gestion des rôles"), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -104,28 +219,52 @@ class _HandleUserRoleState extends State { controller: _roleController, decoration: InputDecoration( labelText: 'Nouveau rôle', + hintText: 'Ex: Manager, Vendeur...', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), + onSubmitted: (_) => _addRole(), ), ), const SizedBox(width: 10), - ElevatedButton( + ElevatedButton.icon( onPressed: _addRole, + icon: const Icon(Icons.add), + label: const Text('Ajouter'), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, + foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), - child: const Text('Ajouter'), ), ], ), ), ), const SizedBox(height: 20), + + // Affichage des statistiques + if (roles.isNotEmpty) + Card( + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatItem('Rôles', roles.length.toString(), Icons.people), + _buildStatItem('Permissions', permissions.length.toString(), Icons.security), + _buildStatItem('Menus', menus.length.toString(), Icons.menu), + ], + ), + ), + ), + + const SizedBox(height: 20), + // Tableau des rôles et permissions if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty) Expanded( @@ -137,69 +276,183 @@ class _HandleUserRoleState extends State { child: Padding( padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: MediaQuery.of(context).size.width - 32, - ), - child: Column( - children: menus.map((menu) { - final menuId = menu['id'] as int; - return Column( - children: [ - Text( - menu['name'], - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - DataTable( - columnSpacing: 20, - columns: [ - const DataColumn( - label: Text( - 'Rôles', - style: TextStyle(fontWeight: FontWeight.bold), - ), + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: menus.map((menu) { + final menuId = menu['id'] as int; + final menuName = menu['name'] as String; + final menuRoute = menu['route'] as String; + + return Card( + margin: const EdgeInsets.only(bottom: 16.0), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), ), - ...permissions.map((perm) => DataColumn( + child: Row( + children: [ + Icon(Icons.menu, color: Colors.blue.shade700), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + menuName, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.blue.shade700, + ), + ), + Text( + menuRoute, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 12), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columnSpacing: 20, + headingRowHeight: 50, + dataRowHeight: 60, + columns: [ + const DataColumn( label: Text( - perm.name, - style: const TextStyle(fontWeight: FontWeight.bold), + 'Rôles', + style: TextStyle(fontWeight: FontWeight.bold), ), - )).toList(), - ], - rows: roles.map((role) { - final roleId = role.id!; - return DataRow( - cells: [ - DataCell(Text(role.designation)), - ...permissions.map((perm) { - final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false; - return DataCell( - Checkbox( - value: isChecked, - onChanged: (bool? value) { - _onPermissionToggle(roleId, menuId, perm.name, value ?? false); - }, - ), - ); - }).toList(), + ), + ...permissions.map((perm) => DataColumn( + label: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + perm.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + )).toList(), + const DataColumn( + label: Text( + 'Actions', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), ], - ); - }).toList(), - ), - ], - ); - }).toList(), - ), + rows: roles.map((role) { + final roleId = role.id!; + return DataRow( + cells: [ + DataCell( + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: role.designation == 'Super Admin' + ? Colors.red.shade50 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + role.designation, + style: TextStyle( + fontWeight: FontWeight.w500, + color: role.designation == 'Super Admin' + ? Colors.red.shade700 + : Colors.blue.shade700, + ), + ), + ), + ), + ...permissions.map((perm) { + final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false; + return DataCell( + Checkbox( + value: isChecked, + onChanged: (bool? value) { + _onPermissionToggle(roleId, menuId, perm.name, value ?? false); + }, + activeColor: Colors.green, + ), + ); + }).toList(), + DataCell( + role.designation != 'Super Admin' + ? IconButton( + icon: Icon(Icons.delete, color: Colors.red.shade600), + tooltip: 'Supprimer le rôle', + onPressed: () => _deleteRole(role), + ) + : Icon(Icons.lock, color: Colors.grey.shade400), + ), + ], + ); + }).toList(), + ), + ), + ], + ), + ), + ); + }).toList(), ), ), ), ), ) else - const Expanded( + Expanded( child: Center( - child: Text('Aucun rôle, permission ou menu trouvé'), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inbox, size: 64, color: Colors.grey.shade400), + const SizedBox(height: 16), + Text( + 'Aucune donnée disponible', + style: TextStyle( + fontSize: 18, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'Rôles: ${roles.length} | Permissions: ${permissions.length} | Menus: ${menus.length}', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _initData, + icon: const Icon(Icons.refresh), + label: const Text('Actualiser'), + ), + ], + ), ), ), ], @@ -207,4 +460,34 @@ class _HandleUserRoleState extends State { ), ); } -} + + Widget _buildStatItem(String label, String value, IconData icon) { + return Column( + children: [ + Icon(icon, size: 32, color: Colors.blue.shade600), + const SizedBox(height: 8), + Text( + value, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ); + } + + @override + void dispose() { + _roleController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/Views/gestion_point_de_vente.dart b/lib/Views/gestion_point_de_vente.dart new file mode 100644 index 0000000..e15ad18 --- /dev/null +++ b/lib/Views/gestion_point_de_vente.dart @@ -0,0 +1,416 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:youmazgestion/Components/app_bar.dart'; +import 'package:youmazgestion/Components/appDrawer.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; + +class AjoutPointDeVentePage extends StatefulWidget { + const AjoutPointDeVentePage({super.key}); + + @override + _AjoutPointDeVentePageState createState() => _AjoutPointDeVentePageState(); +} + +class _AjoutPointDeVentePageState extends State { + final AppDatabase _appDatabase = AppDatabase.instance; + final _formKey = GlobalKey(); + bool _isLoading = false; + + // Contrôleurs + final TextEditingController _nomController = TextEditingController(); + final TextEditingController _codeController = TextEditingController(); + + // Liste des points de vente + List> _pointsDeVente = []; + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadPointsDeVente(); + _searchController.addListener(_filterPointsDeVente); + } + + Future _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 _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 _deletePointDeVente(int id) async { + final confirmed = await Get.dialog( + 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(); + } +} \ No newline at end of file diff --git a/lib/Views/historique.dart b/lib/Views/historique.dart index dee0e05..a5a8efd 100644 --- a/lib/Views/historique.dart +++ b/lib/Views/historique.dart @@ -4,7 +4,6 @@ import 'package:intl/intl.dart'; import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Models/client.dart'; -import 'package:youmazgestion/Models/produit.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; class HistoriquePage extends StatefulWidget { @@ -612,7 +611,7 @@ class _HistoriquePageState extends State { ), ), onPressed: () => _updateStatutCommande(commande.id!), - child: const Text('Marquer comme livrée'), + child: const Text('Marquer comme confirmé'), ), ), ], diff --git a/lib/Views/mobilepage.dart b/lib/Views/mobilepage.dart index 8e3fe8e..1f67983 100644 --- a/lib/Views/mobilepage.dart +++ b/lib/Views/mobilepage.dart @@ -132,6 +132,13 @@ class _NouvelleCommandePageState extends State { List _commercialUsers = []; Users? _selectedCommercialUser; + // Variables pour les suggestions clients + List _clientSuggestions = []; + bool _showNomSuggestions = false; + bool _showTelephoneSuggestions = false; + GlobalKey _nomFieldKey = GlobalKey(); + GlobalKey _telephoneFieldKey = GlobalKey(); + @override void initState() { super.initState(); @@ -142,6 +149,123 @@ class _NouvelleCommandePageState extends State { _searchNameController.addListener(_filterProducts); _searchImeiController.addListener(_filterProducts); _searchReferenceController.addListener(_filterProducts); + + // Listeners pour l'autocomplétion client + _nomController.addListener(() { + if (_nomController.text.length >= 3) { + _showClientSuggestions(_nomController.text, isNom: true); + } else { + _hideNomSuggestions(); + } + }); + + _telephoneController.addListener(() { + if (_telephoneController.text.length >= 3) { + _showClientSuggestions(_telephoneController.text, isNom: false); + } else { + _hideTelephoneSuggestions(); + } + }); + } + + // Méthode pour vider complètement le formulaire et le panier + void _clearFormAndCart() { + setState(() { + // Vider les contrôleurs client + _nomController.clear(); + _prenomController.clear(); + _emailController.clear(); + _telephoneController.clear(); + _adresseController.clear(); + + // Vider le panier + _quantites.clear(); + + // Réinitialiser le commercial au premier de la liste + if (_commercialUsers.isNotEmpty) { + _selectedCommercialUser = _commercialUsers.first; + } + + // Masquer toutes les suggestions + _hideAllSuggestions(); + + // Réinitialiser l'état de chargement + _isLoading = false; + }); + } + + Future _showClientSuggestions(String query, {required bool isNom}) async { + if (query.length < 3) { + _hideAllSuggestions(); + return; + } + + final suggestions = await _appDatabase.suggestClients(query); + + setState(() { + _clientSuggestions = suggestions; + if (isNom) { + _showNomSuggestions = true; + _showTelephoneSuggestions = false; + } else { + _showTelephoneSuggestions = true; + _showNomSuggestions = false; + } + }); +} + + void _showOverlay({required bool isNom}) { + // Utiliser une approche plus simple avec setState + setState(() { + _clientSuggestions = _clientSuggestions; + if (isNom) { + _showNomSuggestions = true; + _showTelephoneSuggestions = false; + } else { + _showTelephoneSuggestions = true; + _showNomSuggestions = false; + } + }); + } + + void _fillClientForm(Client client) { + setState(() { + _nomController.text = client.nom; + _prenomController.text = client.prenom; + _emailController.text = client.email; + _telephoneController.text = client.telephone; + _adresseController.text = client.adresse ?? ''; + }); + + Get.snackbar( + 'Client trouvé', + 'Les informations ont été remplies automatiquement', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } + + void _hideNomSuggestions() { + if (mounted && _showNomSuggestions) { + setState(() { + _showNomSuggestions = false; + }); + } + } + + void _hideTelephoneSuggestions() { + if (mounted && _showTelephoneSuggestions){ + setState(() { + _showTelephoneSuggestions = false; + }); + } + } + + void _hideAllSuggestions() { + _hideNomSuggestions(); + _hideTelephoneSuggestions(); } Future _loadProducts() async { @@ -391,102 +515,138 @@ class _NouvelleCommandePageState extends State { return Scaffold( floatingActionButton: _buildFloatingCartButton(), drawer: isMobile ? CustomDrawer() : null, - body: Column( - children: [ - // Bouton client - version compacte pour mobile - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric( - vertical: isMobile ? 12 : 16 - ), - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: _showClientFormDialog, - icon: const Icon(Icons.person_add), - label: Text( - isMobile ? 'Client' : 'Ajouter les informations client', - style: TextStyle(fontSize: isMobile ? 14 : 16), - ), + body: GestureDetector( + onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs + child: Column( + children: [ + // Section des filtres - adaptée comme dans HistoriquePage + if (!isMobile) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildFilterSection(), ), - ), - ), - - // Section des filtres - adaptée comme dans HistoriquePage - if (!isMobile) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: _buildFilterSection(), - ), - - // Sur mobile, bouton pour afficher les filtres dans un modal - if (isMobile) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: const Icon(Icons.filter_alt), - label: const Text('Filtres produits'), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => SingleChildScrollView( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, + + // Sur mobile, bouton pour afficher les filtres dans un modal + if (isMobile) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: const Icon(Icons.filter_alt), + label: const Text('Filtres produits'), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: _buildFilterSection(), ), - child: _buildFilterSection(), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), ), ), ), ), - ), - // Compteur de résultats visible en haut sur mobile - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${_filteredProducts.length} produit(s)', - style: TextStyle( - color: Colors.blue.shade700, - fontWeight: FontWeight.w600, + // Compteur de résultats visible en haut sur mobile + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_filteredProducts.length} produit(s)', + style: TextStyle( + color: Colors.blue.shade700, + fontWeight: FontWeight.w600, + ), ), ), ), + ], + + // Liste des produits + Expanded( + child: _buildProductList(), ), ], - - // Liste des produits - Expanded( - child: _buildProductList(), - ), - ], + ), ), ); } + Widget _buildSuggestionsList({required bool isNom}) { + if (_clientSuggestions.isEmpty) return const SizedBox(); + + return Container( + margin: const EdgeInsets.only(top: 4), + constraints: const BoxConstraints(maxHeight: 150), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: _clientSuggestions.length, + itemBuilder: (context, index) { + final client = _clientSuggestions[index]; + return ListTile( + dense: true, + leading: CircleAvatar( + radius: 16, + backgroundColor: Colors.blue.shade100, + child: Icon( + Icons.person, + size: 16, + color: Colors.blue.shade700, + ), + ), + title: Text( + '${client.nom} ${client.prenom}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + '${client.telephone} • ${client.email}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + onTap: () { + _fillClientForm(client); + _hideAllSuggestions(); + }, + ); + }, + ), + ); +} + Widget _buildFloatingCartButton() { final isMobile = MediaQuery.of(context).size.width < 600; final cartItemCount = _quantites.values.where((q) => q > 0).length; @@ -506,115 +666,468 @@ class _NouvelleCommandePageState extends State { } void _showClientFormDialog() { - final isMobile = MediaQuery.of(context).size.width < 600; - - Get.dialog( - AlertDialog( - title: Row( + final isMobile = MediaQuery.of(context).size.width < 600; + + // Variables locales pour les suggestions dans le dialog + bool showNomSuggestions = false; + bool showPrenomSuggestions = false; + bool showEmailSuggestions = false; + bool showTelephoneSuggestions = false; + List localClientSuggestions = []; + + // GlobalKeys pour positionner les overlays + final GlobalKey nomFieldKey = GlobalKey(); + final GlobalKey prenomFieldKey = GlobalKey(); + final GlobalKey emailFieldKey = GlobalKey(); + final GlobalKey telephoneFieldKey = GlobalKey(); + + Get.dialog( + StatefulBuilder( + builder: (context, setDialogState) { + return Stack( 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), - Expanded( - child: Text( - isMobile ? 'Client' : 'Informations Client', - style: TextStyle(fontSize: isMobile ? 16 : 18), - ), - ), - ], - ), - content: Container( - width: isMobile ? double.maxFinite : 600, - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, - ), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + AlertDialog( + title: Row( children: [ - _buildTextFormField( - controller: _nomController, - label: 'Nom', - validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null, + 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(height: 12), - _buildTextFormField( - controller: _prenomController, - label: 'Prénom', - validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null, + const SizedBox(width: 12), + Expanded( + child: Text( + isMobile ? 'Client' : 'Informations Client', + style: TextStyle(fontSize: isMobile ? 16 : 18), + ), ), - const SizedBox(height: 12), - _buildTextFormField( - 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; - }, + ], + ), + content: Container( + width: isMobile ? double.maxFinite : 600, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champ Nom avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: nomFieldKey, + controller: _nomController, + label: 'Nom', + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un nom' : null, + onChanged: (value) async { + if (value.length >= 2) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showNomSuggestions = suggestions.isNotEmpty; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showNomSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Prénom avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: prenomFieldKey, + controller: _prenomController, + label: 'Prénom', + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un prénom' : null, + onChanged: (value) async { + if (value.length >= 2) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showPrenomSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showPrenomSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Email avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: emailFieldKey, + 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) async { + if (value.length >= 3) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showEmailSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showPrenomSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showEmailSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Téléphone avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: telephoneFieldKey, + controller: _telephoneController, + label: 'Téléphone', + keyboardType: TextInputType.phone, + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un téléphone' : null, + onChanged: (value) async { + if (value.length >= 3) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showTelephoneSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + }); + } else { + setDialogState(() { + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + _buildTextFormField( + controller: _adresseController, + label: 'Adresse', + maxLines: 2, + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer une adresse' : null, + ), + const SizedBox(height: 12), + _buildCommercialDropdown(), + ], + ), ), - const SizedBox(height: 12), - _buildTextFormField( - controller: _telephoneController, - label: 'Téléphone', - keyboardType: TextInputType.phone, - validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade800, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 16 : 20, + vertical: isMobile ? 10 : 12 + ), ), - const SizedBox(height: 12), - _buildTextFormField( - controller: _adresseController, - label: 'Adresse', - maxLines: 2, - validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null, + onPressed: () { + if (_formKey.currentState!.validate()) { + // Fermer toutes les suggestions avant de soumettre + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + Get.back(); + _submitOrder(); + } + }, + child: Text( + isMobile ? 'Valider' : 'Valider la commande', + style: TextStyle(fontSize: isMobile ? 12 : 14), ), - const SizedBox(height: 12), - _buildCommercialDropdown(), - ], - ), + ), + ], ), - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 16 : 20, - vertical: isMobile ? 10 : 12 + + // Overlay pour les suggestions du nom + if (showNomSuggestions) + _buildSuggestionOverlay( + fieldKey: nomFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showNomSuggestions = false; + localClientSuggestions = []; + }); + }, ), - ), - onPressed: () { - if (_formKey.currentState!.validate()) { - Get.back(); - _submitOrder(); - } - }, - child: Text( - isMobile ? 'Valider' : 'Valider la commande', - style: TextStyle(fontSize: isMobile ? 12 : 14), - ), - ), - ], + + // Overlay pour les suggestions du prénom + if (showPrenomSuggestions) + _buildSuggestionOverlay( + fieldKey: prenomFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showPrenomSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions de l'email + if (showEmailSuggestions) + _buildSuggestionOverlay( + fieldKey: emailFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showEmailSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions du téléphone + if (showTelephoneSuggestions) + _buildSuggestionOverlay( + fieldKey: telephoneFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + ], + ); + }, + ), + ); +} + +// Widget pour créer un TextFormField avec une clé +Widget _buildTextFormFieldWithKey({ + required GlobalKey key, + required TextEditingController controller, + required String label, + TextInputType? keyboardType, + int maxLines = 1, + String? Function(String?)? validator, + void Function(String)? onChanged, +}) { + return Container( + key: key, + child: _buildTextFormField( + controller: controller, + label: label, + keyboardType: keyboardType, + maxLines: maxLines, + validator: validator, + onChanged: onChanged, + ), + ); +} + +// Widget pour l'overlay des suggestions +Widget _buildSuggestionOverlay({ + required GlobalKey fieldKey, + required List suggestions, + required Function(Client) onClientSelected, + required VoidCallback onDismiss, +}) { + return Positioned.fill( + child: GestureDetector( + onTap: onDismiss, + child: Material( + color: Colors.transparent, + child: Builder( + builder: (context) { + // Obtenir la position du champ + final RenderBox? renderBox = fieldKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox == null) return const SizedBox(); + + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + + return Stack( + children: [ + Positioned( + left: position.dx, + top: position.dy + size.height + 4, + width: size.width, + child: GestureDetector( + onTap: () {}, // Empêcher la fermeture au tap sur la liste + child: Container( + constraints: const BoxConstraints( + maxHeight: 200, // Hauteur maximum pour la scrollabilité + ), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Scrollbar( + thumbVisibility: suggestions.length > 3, + child: ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: suggestions.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: Colors.grey.shade200, + ), + itemBuilder: (context, index) { + final client = suggestions[index]; + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + leading: CircleAvatar( + radius: 16, + backgroundColor: Colors.blue.shade100, + child: Icon( + Icons.person, + size: 16, + color: Colors.blue.shade700, + ), + ), + title: Text( + '${client.nom} ${client.prenom}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + '${client.telephone} • ${client.email}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + onTap: () => onClientSelected(client), + hoverColor: Colors.blue.shade50, + ); + }, + ), + ), + ), + ), + ), + ), + ], + ); + }, + ), ), - ); - } + ), + ); +} + +// Méthode pour remplir le formulaire avec les données du client +void _fillFormWithClient(Client client) { + _nomController.text = client.nom; + _prenomController.text = client.prenom; + _emailController.text = client.email; + _telephoneController.text = client.telephone; + _adresseController.text = client.adresse ?? ''; + + Get.snackbar( + 'Client trouvé', + 'Les informations ont été remplies automatiquement', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); +} Widget _buildTextFormField({ required TextEditingController controller, @@ -622,6 +1135,7 @@ class _NouvelleCommandePageState extends State { TextInputType? keyboardType, String? Function(String?)? validator, int? maxLines, + void Function(String)? onChanged, }) { return TextFormField( controller: controller, @@ -629,19 +1143,14 @@ class _NouvelleCommandePageState extends State { labelText: label, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade400), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade400), ), filled: true, fillColor: Colors.white, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), keyboardType: keyboardType, validator: validator, maxLines: maxLines, + onChanged: onChanged, ); } @@ -1137,11 +1646,15 @@ class _NouvelleCommandePageState extends State { try { await _appDatabase.createCommandeComplete(client, commande, details); + // Fermer le panier avant d'afficher la confirmation + Get.back(); + // Afficher le dialogue de confirmation - adapté pour mobile final isMobile = MediaQuery.of(context).size.width < 600; await showDialog( context: context, + barrierDismissible: false, // Empêcher la fermeture accidentelle builder: (context) => AlertDialog( title: Row( children: [ @@ -1182,16 +1695,8 @@ class _NouvelleCommandePageState extends State { ), onPressed: () { Navigator.pop(context); - // Réinitialiser le formulaire - _nomController.clear(); - _prenomController.clear(); - _emailController.clear(); - _telephoneController.clear(); - _adresseController.clear(); - setState(() { - _quantites.clear(); - _isLoading = false; - }); + // Vider complètement le formulaire et le panier + _clearFormAndCart(); // Recharger les produits pour mettre à jour le stock _loadProducts(); }, @@ -1222,6 +1727,10 @@ class _NouvelleCommandePageState extends State { @override void dispose() { + // Nettoyer les suggestions + _hideAllSuggestions(); + + // Disposer les contrôleurs _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); diff --git a/lib/Views/newCommand.dart b/lib/Views/newCommand.dart index c496831..12922f6 100644 --- a/lib/Views/newCommand.dart +++ b/lib/Views/newCommand.dart @@ -26,33 +26,157 @@ class _NouvelleCommandePageState extends State { final TextEditingController _telephoneController = TextEditingController(); final TextEditingController _adresseController = TextEditingController(); - // Contrôleurs pour les filtres - NOUVEAU + // Contrôleurs pour les filtres final TextEditingController _searchNameController = TextEditingController(); final TextEditingController _searchImeiController = TextEditingController(); final TextEditingController _searchReferenceController = TextEditingController(); // Panier final List _products = []; - final List _filteredProducts = []; // NOUVEAU - Liste filtrée + final List _filteredProducts = []; final Map _quantites = {}; - // Variables de filtre - NOUVEAU + // Variables de filtre bool _showOnlyInStock = false; // Utilisateurs commerciaux List _commercialUsers = []; Users? _selectedCommercialUser; + // Variables pour les suggestions clients + List _clientSuggestions = []; + bool _showNomSuggestions = false; + bool _showTelephoneSuggestions = false; + GlobalKey _nomFieldKey = GlobalKey(); + GlobalKey _telephoneFieldKey = GlobalKey(); + @override void initState() { super.initState(); _loadProducts(); _loadCommercialUsers(); - // Listeners pour les filtres - NOUVEAU + // Listeners pour les filtres _searchNameController.addListener(_filterProducts); _searchImeiController.addListener(_filterProducts); _searchReferenceController.addListener(_filterProducts); + + // Listeners pour l'autocomplétion client + _nomController.addListener(() { + if (_nomController.text.length >= 3) { + _showClientSuggestions(_nomController.text, isNom: true); + } else { + _hideNomSuggestions(); + } + }); + + _telephoneController.addListener(() { + if (_telephoneController.text.length >= 3) { + _showClientSuggestions(_telephoneController.text, isNom: false); + } else { + _hideTelephoneSuggestions(); + } + }); + } + + // Méthode pour vider complètement le formulaire et le panier + void _clearFormAndCart() { + setState(() { + // Vider les contrôleurs client + _nomController.clear(); + _prenomController.clear(); + _emailController.clear(); + _telephoneController.clear(); + _adresseController.clear(); + + // Vider le panier + _quantites.clear(); + + // Réinitialiser le commercial au premier de la liste + if (_commercialUsers.isNotEmpty) { + _selectedCommercialUser = _commercialUsers.first; + } + + // Masquer toutes les suggestions + _hideAllSuggestions(); + + // Réinitialiser l'état de chargement + _isLoading = false; + }); + } + + Future _showClientSuggestions(String query, {required bool isNom}) async { + if (query.length < 3) { + _hideAllSuggestions(); + return; + } + + final suggestions = await _appDatabase.suggestClients(query); + + setState(() { + _clientSuggestions = suggestions; + if (isNom) { + _showNomSuggestions = true; + _showTelephoneSuggestions = false; + } else { + _showTelephoneSuggestions = true; + _showNomSuggestions = false; + } + }); +} + + void _showOverlay({required bool isNom}) { + // Utiliser une approche plus simple avec setState + setState(() { + _clientSuggestions = _clientSuggestions; + if (isNom) { + _showNomSuggestions = true; + _showTelephoneSuggestions = false; + } else { + _showTelephoneSuggestions = true; + _showNomSuggestions = false; + } + }); + } + + void _fillClientForm(Client client) { + setState(() { + _nomController.text = client.nom; + _prenomController.text = client.prenom; + _emailController.text = client.email; + _telephoneController.text = client.telephone; + _adresseController.text = client.adresse ?? ''; + }); + + Get.snackbar( + 'Client trouvé', + 'Les informations ont été remplies automatiquement', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } + + void _hideNomSuggestions() { + if (mounted && _showNomSuggestions) { + setState(() { + _showNomSuggestions = false; + }); + } + } + + void _hideTelephoneSuggestions() { + if (mounted && _showTelephoneSuggestions){ + setState(() { + _showTelephoneSuggestions = false; + }); + } + } + + void _hideAllSuggestions() { + _hideNomSuggestions(); + _hideTelephoneSuggestions(); } Future _loadProducts() async { @@ -61,7 +185,7 @@ class _NouvelleCommandePageState extends State { _products.clear(); _products.addAll(products); _filteredProducts.clear(); - _filteredProducts.addAll(products); // Initialiser la liste filtrée + _filteredProducts.addAll(products); }); } @@ -75,7 +199,6 @@ class _NouvelleCommandePageState extends State { }); } - // NOUVELLE MÉTHODE - Filtrer les produits void _filterProducts() { final nameQuery = _searchNameController.text.toLowerCase(); final imeiQuery = _searchImeiController.text.toLowerCase(); @@ -104,7 +227,6 @@ class _NouvelleCommandePageState extends State { }); } - // NOUVELLE MÉTHODE - Toggle filtre stock void _toggleStockFilter() { setState(() { _showOnlyInStock = !_showOnlyInStock; @@ -112,7 +234,6 @@ class _NouvelleCommandePageState extends State { _filterProducts(); } - // NOUVELLE MÉTHODE - Réinitialiser les filtres void _clearFilters() { setState(() { _searchNameController.clear(); @@ -123,8 +244,10 @@ class _NouvelleCommandePageState extends State { _filterProducts(); } - // NOUVEAU WIDGET - Section des filtres + // Section des filtres adaptée pour mobile Widget _buildFilterSection() { + final isMobile = MediaQuery.of(context).size.width < 600; + return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 16), @@ -152,7 +275,7 @@ class _NouvelleCommandePageState extends State { TextButton.icon( onPressed: _clearFilters, icon: const Icon(Icons.clear, size: 18), - label: const Text('Réinitialiser'), + label: isMobile ? const SizedBox() : const Text('Réinitialiser'), style: TextButton.styleFrom( foregroundColor: Colors.grey.shade600, ), @@ -176,44 +299,75 @@ class _NouvelleCommandePageState extends State { ), const SizedBox(height: 12), - // Champs IMEI et Référence sur la même ligne - Row( - children: [ - Expanded( - child: TextField( - controller: _searchImeiController, - decoration: InputDecoration( - labelText: 'IMEI', - prefixIcon: const Icon(Icons.phone_android), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + if (!isMobile) ...[ + // Version desktop - champs sur la même ligne + Row( + children: [ + Expanded( + child: TextField( + controller: _searchImeiController, + decoration: InputDecoration( + labelText: 'IMEI', + prefixIcon: const Icon(Icons.phone_android), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, ), - filled: true, - fillColor: Colors.grey.shade50, ), ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: _searchReferenceController, - decoration: InputDecoration( - labelText: 'Référence', - prefixIcon: const Icon(Icons.qr_code), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _searchReferenceController, + decoration: InputDecoration( + labelText: 'Référence', + prefixIcon: const Icon(Icons.qr_code), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, ), - filled: true, - fillColor: Colors.grey.shade50, ), ), + ], + ), + ] else ...[ + // Version mobile - champs empilés + TextField( + controller: _searchImeiController, + decoration: InputDecoration( + labelText: 'IMEI', + prefixIcon: const Icon(Icons.phone_android), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, ), - ], - ), + ), + const SizedBox(height: 12), + TextField( + controller: _searchReferenceController, + decoration: InputDecoration( + labelText: 'Référence', + prefixIcon: const Icon(Icons.qr_code), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ], const SizedBox(height: 16), - // Bouton filtre stock et résultats - Row( + // Boutons de filtre adaptés pour mobile + Wrap( + spacing: 8, + runSpacing: 8, children: [ ElevatedButton.icon( onPressed: _toggleStockFilter, @@ -222,25 +376,104 @@ class _NouvelleCommandePageState extends State { size: 20, ), label: Text(_showOnlyInStock - ? 'Afficher tous' - : 'Stock disponible'), + ? isMobile ? 'Tous' : 'Afficher tous' + : isMobile ? 'En stock' : 'Stock disponible'), style: ElevatedButton.styleFrom( backgroundColor: _showOnlyInStock ? Colors.green.shade600 : Colors.blue.shade600, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12 + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 16, + vertical: 8 ), ), ), - const Spacer(), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8 + ], + ), + + const SizedBox(height: 8), + + // Compteur de résultats + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8 + ), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_filteredProducts.length} produit(s)', + style: TextStyle( + color: Colors.blue.shade700, + fontWeight: FontWeight.w600, + fontSize: isMobile ? 12 : 14, + ), + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 600; + + return Scaffold( + floatingActionButton: _buildFloatingCartButton(), + drawer: isMobile ? CustomDrawer() : null, + body: GestureDetector( + onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs + child: Column( + children: [ + // Section des filtres - adaptée comme dans HistoriquePage + if (!isMobile) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildFilterSection(), + ), + + // Sur mobile, bouton pour afficher les filtres dans un modal + if (isMobile) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: const Icon(Icons.filter_alt), + label: const Text('Filtres produits'), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: _buildFilterSection(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), ), + ), + ), + // Compteur de résultats visible en haut sur mobile + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(20), @@ -253,7 +486,12 @@ class _NouvelleCommandePageState extends State { ), ), ), - ], + ), + ], + + // Liste des produits + Expanded( + child: _buildProductList(), ), ], ), @@ -261,156 +499,546 @@ class _NouvelleCommandePageState extends State { ); } - @override - Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: _buildFloatingCartButton(), - appBar: CustomAppBar(title: 'Faire un commande'), - drawer: CustomDrawer(), - body: Column( - children: [ - // Header - - - // Contenu principal MODIFIÉ - Inclut les filtres - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - ), - onPressed: _showClientFormDialog, - child: const Text('Ajouter les informations client'), - ), - const SizedBox(height: 20), - - // NOUVEAU - Section des filtres - _buildFilterSection(), - - // Liste des produits - _buildProductList(), - ], - ), + Widget _buildSuggestionsList({required bool isNom}) { + if (_clientSuggestions.isEmpty) return const SizedBox(); + + return Container( + margin: const EdgeInsets.only(top: 4), + constraints: const BoxConstraints(maxHeight: 150), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: _clientSuggestions.length, + itemBuilder: (context, index) { + final client = _clientSuggestions[index]; + return ListTile( + dense: true, + leading: CircleAvatar( + radius: 16, + backgroundColor: Colors.blue.shade100, + child: Icon( + Icons.person, + size: 16, + color: Colors.blue.shade700, ), ), - ], - ), - ); - } + title: Text( + '${client.nom} ${client.prenom}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + '${client.telephone} • ${client.email}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + onTap: () { + _fillClientForm(client); + _hideAllSuggestions(); + }, + ); + }, + ), + ); +} Widget _buildFloatingCartButton() { + final isMobile = MediaQuery.of(context).size.width < 600; + final cartItemCount = _quantites.values.where((q) => q > 0).length; + return FloatingActionButton.extended( onPressed: () { _showCartBottomSheet(); }, icon: const Icon(Icons.shopping_cart), - label: Text('Panier (${_quantites.values.where((q) => q > 0).length})'), + label: Text( + isMobile ? 'Panier ($cartItemCount)' : 'Panier ($cartItemCount)', + style: TextStyle(fontSize: isMobile ? 12 : 14), + ), backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, ); } void _showClientFormDialog() { - Get.dialog( - AlertDialog( - title: Row( + final isMobile = MediaQuery.of(context).size.width < 600; + + // Variables locales pour les suggestions dans le dialog + bool showNomSuggestions = false; + bool showPrenomSuggestions = false; + bool showEmailSuggestions = false; + bool showTelephoneSuggestions = false; + List localClientSuggestions = []; + + // GlobalKeys pour positionner les overlays + final GlobalKey nomFieldKey = GlobalKey(); + final GlobalKey prenomFieldKey = GlobalKey(); + final GlobalKey emailFieldKey = GlobalKey(); + final GlobalKey telephoneFieldKey = GlobalKey(); + + Get.dialog( + StatefulBuilder( + builder: (context, setDialogState) { + return Stack( 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'), - ], - ), - content: Container( - width: 600, - constraints: const BoxConstraints(maxHeight: 600), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + AlertDialog( + title: Row( children: [ - _buildTextFormField( - controller: _nomController, - label: 'Nom', - validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null, + 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(height: 12), - _buildTextFormField( - controller: _prenomController, - label: 'Prénom', - validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null, + const SizedBox(width: 12), + Expanded( + child: Text( + isMobile ? 'Client' : 'Informations Client', + style: TextStyle(fontSize: isMobile ? 16 : 18), + ), ), - const SizedBox(height: 12), - _buildTextFormField( - 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; - }, + ], + ), + content: Container( + width: isMobile ? double.maxFinite : 600, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champ Nom avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: nomFieldKey, + controller: _nomController, + label: 'Nom', + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un nom' : null, + onChanged: (value) async { + if (value.length >= 2) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showNomSuggestions = suggestions.isNotEmpty; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showNomSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Prénom avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: prenomFieldKey, + controller: _prenomController, + label: 'Prénom', + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un prénom' : null, + onChanged: (value) async { + if (value.length >= 2) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showPrenomSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showPrenomSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Email avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: emailFieldKey, + 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) async { + if (value.length >= 3) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showEmailSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showPrenomSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showEmailSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Téléphone avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: telephoneFieldKey, + controller: _telephoneController, + label: 'Téléphone', + keyboardType: TextInputType.phone, + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un téléphone' : null, + onChanged: (value) async { + if (value.length >= 3) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showTelephoneSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + }); + } else { + setDialogState(() { + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + _buildTextFormField( + controller: _adresseController, + label: 'Adresse', + maxLines: 2, + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer une adresse' : null, + ), + const SizedBox(height: 12), + _buildCommercialDropdown(), + ], + ), ), - const SizedBox(height: 12), - _buildTextFormField( - controller: _telephoneController, - label: 'Téléphone', - keyboardType: TextInputType.phone, - validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade800, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 16 : 20, + vertical: isMobile ? 10 : 12 + ), ), - const SizedBox(height: 12), - _buildTextFormField( - controller: _adresseController, - label: 'Adresse', - maxLines: 2, - validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null, + onPressed: () { + if (_formKey.currentState!.validate()) { + // Fermer toutes les suggestions avant de soumettre + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + Get.back(); + _submitOrder(); + } + }, + child: Text( + isMobile ? 'Valider' : 'Valider la commande', + style: TextStyle(fontSize: isMobile ? 12 : 14), ), - const SizedBox(height: 12), - _buildCommercialDropdown(), - ], - ), + ), + ], ), - ), + + // Overlay pour les suggestions du nom + if (showNomSuggestions) + _buildSuggestionOverlay( + fieldKey: nomFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showNomSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions du prénom + if (showPrenomSuggestions) + _buildSuggestionOverlay( + fieldKey: prenomFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showPrenomSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions de l'email + if (showEmailSuggestions) + _buildSuggestionOverlay( + fieldKey: emailFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showEmailSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions du téléphone + if (showTelephoneSuggestions) + _buildSuggestionOverlay( + fieldKey: telephoneFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + ], + ); + }, + ), + ); +} + +// Widget pour créer un TextFormField avec une clé +Widget _buildTextFormFieldWithKey({ + required GlobalKey key, + required TextEditingController controller, + required String label, + TextInputType? keyboardType, + int maxLines = 1, + String? Function(String?)? validator, + void Function(String)? onChanged, +}) { + return Container( + key: key, + child: _buildTextFormField( + controller: controller, + label: label, + keyboardType: keyboardType, + maxLines: maxLines, + validator: validator, + onChanged: onChanged, + ), + ); +} + +// Widget pour l'overlay des suggestions +Widget _buildSuggestionOverlay({ + required GlobalKey fieldKey, + required List suggestions, + required Function(Client) onClientSelected, + required VoidCallback onDismiss, +}) { + return Positioned.fill( + child: GestureDetector( + onTap: onDismiss, + child: Material( + color: Colors.transparent, + child: Builder( + builder: (context) { + // Obtenir la position du champ + final RenderBox? renderBox = fieldKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox == null) return const SizedBox(); + + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + + return Stack( + children: [ + Positioned( + left: position.dx, + top: position.dy + size.height + 4, + width: size.width, + child: GestureDetector( + onTap: () {}, // Empêcher la fermeture au tap sur la liste + child: Container( + constraints: const BoxConstraints( + maxHeight: 200, // Hauteur maximum pour la scrollabilité + ), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Scrollbar( + thumbVisibility: suggestions.length > 3, + child: ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: suggestions.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: Colors.grey.shade200, + ), + itemBuilder: (context, index) { + final client = suggestions[index]; + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + leading: CircleAvatar( + radius: 16, + backgroundColor: Colors.blue.shade100, + child: Icon( + Icons.person, + size: 16, + color: Colors.blue.shade700, + ), + ), + title: Text( + '${client.nom} ${client.prenom}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + '${client.telephone} • ${client.email}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + onTap: () => onClientSelected(client), + hoverColor: Colors.blue.shade50, + ); + }, + ), + ), + ), + ), + ), + ), + ], + ); + }, ), - 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: () { - if (_formKey.currentState!.validate()) { - Get.back(); - _submitOrder(); - } - }, - child: const Text('Valider la commande'), - ), - ], ), - ); - } + ), + ); +} + +// Méthode pour remplir le formulaire avec les données du client +void _fillFormWithClient(Client client) { + _nomController.text = client.nom; + _prenomController.text = client.prenom; + _emailController.text = client.email; + _telephoneController.text = client.telephone; + _adresseController.text = client.adresse ?? ''; + + Get.snackbar( + 'Client trouvé', + 'Les informations ont été remplies automatiquement', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); +} Widget _buildTextFormField({ required TextEditingController controller, @@ -418,6 +1046,7 @@ class _NouvelleCommandePageState extends State { TextInputType? keyboardType, String? Function(String?)? validator, int? maxLines, + void Function(String)? onChanged, }) { return TextFormField( controller: controller, @@ -425,19 +1054,14 @@ class _NouvelleCommandePageState extends State { labelText: label, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade400), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade400), ), filled: true, fillColor: Colors.white, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), keyboardType: keyboardType, validator: validator, maxLines: maxLines, + onChanged: onChanged, ); } @@ -467,47 +1091,23 @@ class _NouvelleCommandePageState extends State { ); } - // WIDGET MODIFIÉ - Liste des produits (utilise maintenant _filteredProducts) Widget _buildProductList() { - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Produits Disponibles', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 9, 56, 95), - ), - ), - const SizedBox(height: 16), - _filteredProducts.isEmpty - ? _buildEmptyState() - : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _filteredProducts.length, - itemBuilder: (context, index) { - final product = _filteredProducts[index]; - final quantity = _quantites[product.id] ?? 0; - - return _buildProductListItem(product, quantity); - }, - ), - ], - ), - ), - ); + final isMobile = MediaQuery.of(context).size.width < 600; + + return _filteredProducts.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: _filteredProducts.length, + itemBuilder: (context, index) { + final product = _filteredProducts[index]; + final quantity = _quantites[product.id] ?? 0; + + return _buildProductListItem(product, quantity, isMobile); + }, + ); } - // NOUVEAU WIDGET - État vide Widget _buildEmptyState() { return Center( child: Padding( @@ -530,7 +1130,7 @@ class _NouvelleCommandePageState extends State { ), const SizedBox(height: 8), Text( - 'Essayez de modifier vos critères de recherche', + 'Modifiez vos critères de recherche', style: TextStyle( fontSize: 14, color: Colors.grey.shade500, @@ -542,12 +1142,11 @@ class _NouvelleCommandePageState extends State { ); } - // WIDGET MODIFIÉ - Item de produit (ajout d'informations IMEI/Référence) - Widget _buildProductListItem(Product product, int quantity) { + Widget _buildProductListItem(Product product, int quantity, bool isMobile) { final bool isOutOfStock = product.stock != null && product.stock! <= 0; return Card( - margin: const EdgeInsets.symmetric(vertical: 8), + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -559,128 +1158,146 @@ class _NouvelleCommandePageState extends State { ? Border.all(color: Colors.red.shade200, width: 1.5) : null, ), - child: ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - leading: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: isOutOfStock - ? Colors.red.shade50 - : Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.shopping_bag, - color: isOutOfStock ? Colors.red : Colors.blue - ), - ), - title: Text( - product.name, - style: TextStyle( - fontWeight: FontWeight.bold, - color: isOutOfStock ? Colors.red.shade700 : null, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( children: [ - const SizedBox(height: 4), - Text( - '${product.price.toStringAsFixed(2)} MGA', - style: TextStyle( - color: Colors.green.shade700, - fontWeight: FontWeight.w600, + Container( + width: isMobile ? 40 : 50, + height: isMobile ? 40 : 50, + decoration: BoxDecoration( + color: isOutOfStock + ? Colors.red.shade50 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.shopping_bag, + size: isMobile ? 20 : 24, + color: isOutOfStock ? Colors.red : Colors.blue, ), ), - if (product.stock != null) - Text( - 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}', - style: TextStyle( - fontSize: 12, - color: isOutOfStock - ? Colors.red.shade600 - : Colors.grey.shade600, - fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal, - ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isMobile ? 14 : 16, + color: isOutOfStock ? Colors.red.shade700 : null, + ), + ), + const SizedBox(height: 4), + Text( + '${product.price.toStringAsFixed(2)} MGA', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.w600, + fontSize: isMobile ? 12 : 14, + ), + ), + if (product.stock != null) + Text( + 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}', + style: TextStyle( + fontSize: isMobile ? 10 : 12, + color: isOutOfStock + ? Colors.red.shade600 + : Colors.grey.shade600, + fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal, + ), + ), + // Affichage IMEI et Référence - plus compact sur mobile + if (product.imei != null && product.imei!.isNotEmpty) + Text( + 'IMEI: ${product.imei}', + style: TextStyle( + fontSize: isMobile ? 9 : 11, + color: Colors.grey.shade600, + fontFamily: 'monospace', + ), + ), + if (product.reference != null && product.reference!.isNotEmpty) + Text( + 'Réf: ${product.reference}', + style: TextStyle( + fontSize: isMobile ? 9 : 11, + color: Colors.grey.shade600, + ), + ), + ], ), - // Affichage IMEI et Référence - if (product.imei != null && product.imei!.isNotEmpty) - Text( - 'IMEI: ${product.imei}', - style: TextStyle( - fontSize: 11, - color: Colors.grey.shade600, - fontFamily: 'monospace', - ), + ), + Container( + decoration: BoxDecoration( + color: isOutOfStock + ? Colors.grey.shade100 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), ), - if (product.reference != null && product.reference!.isNotEmpty) - Text( - 'Réf: ${product.reference}', - style: TextStyle( - fontSize: 11, - color: Colors.grey.shade600, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + Icons.remove, + size: isMobile ? 16 : 18 + ), + onPressed: isOutOfStock ? null : () { + if (quantity > 0) { + setState(() { + _quantites[product.id!] = quantity - 1; + }); + } + }, + ), + Text( + quantity.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isMobile ? 12 : 14, + ), + ), + IconButton( + icon: Icon( + Icons.add, + size: isMobile ? 16 : 18 + ), + onPressed: isOutOfStock ? null : () { + if (product.stock == null || quantity < product.stock!) { + setState(() { + _quantites[product.id!] = quantity + 1; + }); + } else { + Get.snackbar( + 'Stock insuffisant', + 'Quantité demandée non disponible', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + }, + ), + ], ), + ), ], ), - trailing: Container( - decoration: BoxDecoration( - color: isOutOfStock - ? Colors.grey.shade100 - : Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.remove, size: 18), - onPressed: isOutOfStock ? null : () { - if (quantity > 0) { - setState(() { - _quantites[product.id!] = quantity - 1; - }); - } - }, - ), - Text( - quantity.toString(), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - icon: const Icon(Icons.add, size: 18), - onPressed: isOutOfStock ? null : () { - if (product.stock == null || quantity < product.stock!) { - setState(() { - _quantites[product.id!] = quantity + 1; - }); - } else { - Get.snackbar( - 'Stock insuffisant', - 'Quantité demandée non disponible', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - }, - ), - ], - ), - ), ), ), ); } void _showCartBottomSheet() { + final isMobile = MediaQuery.of(context).size.width < 600; + Get.bottomSheet( Container( - height: MediaQuery.of(context).size.height * 0.7, + height: MediaQuery.of(context).size.height * (isMobile ? 0.85 : 0.7), padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, @@ -691,9 +1308,12 @@ class _NouvelleCommandePageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'Votre Panier', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: isMobile ? 18 : 20, + fontWeight: FontWeight.bold + ), ), IconButton( icon: const Icon(Icons.close), @@ -827,11 +1447,15 @@ class _NouvelleCommandePageState extends State { } Widget _buildSubmitButton() { + final isMobile = MediaQuery.of(context).size.width < 600; + return SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric( + vertical: isMobile ? 12 : 16 + ), backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, shape: RoundedRectangleBorder( @@ -841,7 +1465,7 @@ class _NouvelleCommandePageState extends State { ), onPressed: _submitOrder, child: _isLoading - ? const SizedBox( + ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( @@ -849,9 +1473,9 @@ class _NouvelleCommandePageState extends State { color: Colors.white, ), ) - : const Text( - 'Valider la Commande', - style: TextStyle(fontSize: 16), + : Text( + isMobile ? 'Valider' : 'Valider la Commande', + style: TextStyle(fontSize: isMobile ? 14 : 16), ), ), ); @@ -933,30 +1557,65 @@ class _NouvelleCommandePageState extends State { try { await _appDatabase.createCommandeComplete(client, commande, details); - // Afficher le dialogue de confirmation + // Fermer le panier avant d'afficher la confirmation + Get.back(); + + // Afficher le dialogue de confirmation - adapté pour mobile + final isMobile = MediaQuery.of(context).size.width < 600; + await showDialog( context: context, + barrierDismissible: false, // Empêcher la fermeture accidentelle builder: (context) => AlertDialog( - title: const Text('Commande Validée'), - content: const Text('Votre commande a été enregistrée et expédiée avec succès.'), + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.check_circle, color: Colors.green.shade700), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Commande Validée', + style: TextStyle(fontSize: isMobile ? 16 : 18), + ), + ), + ], + ), + content: Text( + 'Votre commande a été enregistrée et expédiée avec succès.', + style: TextStyle(fontSize: isMobile ? 14 : 16), + ), actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - // Réinitialiser le formulaire - _nomController.clear(); - _prenomController.clear(); - _emailController.clear(); - _telephoneController.clear(); - _adresseController.clear(); - setState(() { - _quantites.clear(); - _isLoading = false; - }); - // Recharger les produits pour mettre à jour le stock - _loadProducts(); - }, - child: const Text('OK'), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + vertical: isMobile ? 12 : 16 + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + Navigator.pop(context); + // Vider complètement le formulaire et le panier + _clearFormAndCart(); + // Recharger les produits pour mettre à jour le stock + _loadProducts(); + }, + child: Text( + 'OK', + style: TextStyle(fontSize: isMobile ? 14 : 16), + ), + ), ), ], ), @@ -979,6 +1638,10 @@ class _NouvelleCommandePageState extends State { @override void dispose() { + // Nettoyer les suggestions + _hideAllSuggestions(); + + // Disposer les contrôleurs _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); @@ -991,5 +1654,4 @@ class _NouvelleCommandePageState extends State { _searchReferenceController.dispose(); super.dispose(); - } -} \ No newline at end of file + }} \ No newline at end of file diff --git a/lib/Views/pointage.dart b/lib/Views/pointage.dart deleted file mode 100644 index 934ac29..0000000 --- a/lib/Views/pointage.dart +++ /dev/null @@ -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 createState() => _PointagePageState(); -} - -class _PointagePageState extends State { - final DatabaseHelper _databaseHelper = DatabaseHelper(); - List _pointages = []; - - @override - void initState() { - super.initState(); - _loadPointages(); - } - - Future _loadPointages() async { - final pointages = await _databaseHelper.getPointages(); - setState(() { - _pointages = pointages; - }); - } - - Future _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', - ), - ], - ), - ); - } -} diff --git a/lib/config/DatabaseConfig.dart b/lib/config/DatabaseConfig.dart new file mode 100644 index 0000000..cb59fca --- /dev/null +++ b/lib/config/DatabaseConfig.dart @@ -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 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 getConfigWithRetry() { + final config = getConfig(); + config['retryCount'] = 3; + config['retryDelay'] = 5000; // ms + return config; + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 8e76e2f..e2773cc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -//import 'package:youmazgestion/Services/app_database.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.dart'; -//import 'Services/productDatabase.dart'; import 'my_app.dart'; import 'package:logging/logging.dart'; @@ -11,31 +9,117 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); try { - // Initialiser les bases de données une seule fois - // await AppDatabase.instance.deleteDatabaseFile(); - // await ProductDatabase.instance.deleteDatabaseFile(); + print("Initialisation de l'application..."); + + // Pour le développement : supprimer toutes les tables (équivalent à deleteDatabaseFile) + // ATTENTION: Décommentez seulement si vous voulez réinitialiser la base + // await AppDatabase.instance.deleteDatabaseFile(); - // await ProductDatabase.instance.initDatabase(); + // Initialiser la base de données MySQL + print("Connexion à la base de données MySQL..."); await AppDatabase.instance.initDatabase(); - + print("Base de données initialisée avec succès !"); // Afficher les informations de la base (pour debug) - // await AppDatabase.instance.printDatabaseInfo(); - Get.put( - UserController()); // Ajoute ce code AVANT tout accès au UserController + await AppDatabase.instance.printDatabaseInfo(); + + // Initialiser le contrôleur utilisateur + Get.put(UserController()); + print("Contrôleur utilisateur initialisé"); + + // Configurer le logger setupLogger(); + print("Lancement de l'application..."); runApp(const GetMaterialApp( debugShowCheckedModeBanner: false, home: MyApp(), )); } catch (e) { print('Erreur lors de l\'initialisation: $e'); - // Vous pourriez vouloir afficher une page d'erreur ici + + // Afficher une page d'erreur avec plus de détails runApp(MaterialApp( + debugShowCheckedModeBanner: false, home: Scaffold( - body: Center( - child: Text('Erreur d\'initialisation: $e'), + backgroundColor: Colors.red[50], + appBar: AppBar( + title: const Text('Erreur d\'initialisation'), + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 48, + ), + const SizedBox(height: 16), + const Text( + 'Erreur de connexion à la base de données', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + const SizedBox(height: 16), + const Text( + 'Vérifiez que :', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text('• XAMPP est démarré'), + const Text('• MySQL est en cours d\'exécution'), + const Text('• La base de données "guycom_databse" existe'), + const Text('• Les paramètres de connexion sont corrects'), + const SizedBox(height: 16), + const Text( + 'Détails de l\'erreur :', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey), + ), + child: SingleChildScrollView( + child: Text( + e.toString(), + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + // Relancer l'application + main(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + child: const Text('Réessayer'), + ), + ), + ], + ), ), ), )); @@ -47,4 +131,4 @@ void setupLogger() { Logger.root.onRecord.listen((record) { print('${record.level.name}: ${record.time}: ${record.message}'); }); -} +} \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a1cd513..d42444f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,7 +11,6 @@ import mobile_scanner import open_file_mac import path_provider_foundation import shared_preferences_foundation -import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -21,6 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 0c92b4c..b365652 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -632,6 +632,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.0" + mysql1: + dependency: "direct main" + description: + name: mysql1 + sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9" + url: "https://pub.dev" + source: hosted + version: "0.20.0" nested: dependency: transitive description: @@ -832,6 +840,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" provider: dependency: transitive description: @@ -981,22 +997,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" - sqflite: - dependency: "direct main" - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" - url: "https://pub.dev" - source: hosted - version: "2.4.1" sqflite_common: dependency: transitive description: @@ -1013,22 +1013,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.5" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" sqlite3: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 35996ef..e8299f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 get: ^4.6.5 - sqflite: ^2.2.8+4 + # sqflite: ^2.2.8+4 + mysql1: ^0.20.0 flutter_dropzone: ^4.2.1 image_picker: ^0.8.7+5