Browse Source

migration mysql

10062025_01
b.razafimandimbihery 6 months ago
parent
commit
b5a11aa3c9
  1. 417
      lib/Components/AddClient.dart
  2. 471
      lib/Components/AddClientForm.dart
  3. 176
      lib/Components/DiscountDialog.dart
  4. 349
      lib/Components/GiftaselectedButton.dart
  5. 338
      lib/Components/PaymentEnchainedDialog.dart
  6. 23
      lib/Components/appDrawer.dart
  7. 214
      lib/Models/Client.dart
  8. 64
      lib/Models/Remise.dart
  9. 127
      lib/Models/produit.dart
  10. 27
      lib/Models/users.dart
  11. 60
      lib/Services/pointageDatabase.dart
  12. 2801
      lib/Services/stock_managementDatabase.dart
  13. 675
      lib/Views/HandleProduct.dart
  14. 493
      lib/Views/RolePermissionPage.dart
  15. 813
      lib/Views/commandManagement.dart
  16. 319
      lib/Views/gestionRole.dart
  17. 416
      lib/Views/gestion_point_de_vente.dart
  18. 3
      lib/Views/historique.dart
  19. 613
      lib/Views/mobilepage.dart
  20. 950
      lib/Views/newCommand.dart
  21. 190
      lib/Views/pointage.dart
  22. 64
      lib/config/DatabaseConfig.dart
  23. 108
      lib/main.dart
  24. 2
      macos/Flutter/GeneratedPluginRegistrant.swift
  25. 48
      pubspec.lock
  26. 3
      pubspec.yaml

417
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<FormState>();
// Controllers pour les champs
final _nomController = TextEditingController();
final _prenomController = TextEditingController();
final _emailController = TextEditingController();
final _telephoneController = TextEditingController();
final _adresseController = TextEditingController();
// Variables observables pour la recherche
var suggestedClients = <Client>[].obs;
var isSearching = false.obs;
var selectedClient = Rxn<Client>();
@override
void onClose() {
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
super.onClose();
}
// Méthode pour rechercher les clients existants
Future<void> searchClients(String query) async {
if (query.length < 2) {
suggestedClients.clear();
return;
}
isSearching.value = true;
try {
final clients = await AppDatabase.instance.suggestClients(query);
suggestedClients.value = clients;
} catch (e) {
print("Erreur recherche clients: $e");
suggestedClients.clear();
} finally {
isSearching.value = false;
}
}
// Méthode pour remplir automatiquement le formulaire
void fillFormWithClient(Client client) {
selectedClient.value = client;
_nomController.text = client.nom;
_prenomController.text = client.prenom;
_emailController.text = client.email;
_telephoneController.text = client.telephone;
_adresseController.text = client.adresse ?? '';
suggestedClients.clear();
}
// Méthode pour vider le formulaire
void clearForm() {
selectedClient.value = null;
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_adresseController.clear();
suggestedClients.clear();
}
// Méthode pour valider et soumettre
Future<void> submitForm() async {
if (!_formKey.currentState!.validate()) return;
try {
Client clientToUse;
if (selectedClient.value != null) {
// Utiliser le client existant
clientToUse = selectedClient.value!;
} else {
// Créer un nouveau client
final newClient = Client(
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
dateCreation: DateTime.now(),
);
clientToUse = await AppDatabase.instance.createOrGetClient(newClient);
}
// Procéder avec la commande
Get.back();
_submitOrderWithClient(clientToUse);
} catch (e) {
Get.snackbar(
'Erreur',
'Erreur lors de la création/récupération du client: $e',
backgroundColor: Colors.red.shade100,
colorText: Colors.red.shade800,
);
}
}
void _submitOrderWithClient(Client client) {
// Votre logique existante pour soumettre la commande
// avec le client fourni
}
}
// Widget pour le formulaire avec auto-completion
void _showClientFormDialog() {
final controller = Get.put(ClientFormController());
Get.dialog(
AlertDialog(
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.person_add, color: Colors.blue.shade700),
),
const SizedBox(width: 12),
const Text('Informations Client'),
const Spacer(),
// Bouton pour vider le formulaire
IconButton(
onPressed: controller.clearForm,
icon: const Icon(Icons.clear),
tooltip: 'Vider le formulaire',
),
],
),
content: Container(
width: 600,
constraints: const BoxConstraints(maxHeight: 700),
child: SingleChildScrollView(
child: Form(
key: controller._formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section de recherche rapide
_buildSearchSection(controller),
const SizedBox(height: 16),
// Indicateur client sélectionné
Obx(() {
if (controller.selectedClient.value != null) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
border: Border.all(color: Colors.green.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.check_circle, color: Colors.green.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
'Client existant sélectionné: ${controller.selectedClient.value!.nomComplet}',
style: TextStyle(
color: Colors.green.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
return const SizedBox.shrink();
}),
const SizedBox(height: 12),
// Champs du formulaire
_buildTextFormField(
controller: controller._nomController,
label: 'Nom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._prenomController,
label: 'Prénom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._emailController,
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
return 'Email invalide';
}
return null;
},
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
// Recherche automatique par email
controller.searchClients(value);
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._telephoneController,
label: 'Téléphone',
keyboardType: TextInputType.phone,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
// Recherche automatique par téléphone
controller.searchClients(value);
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._adresseController,
label: 'Adresse',
maxLines: 2,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildCommercialDropdown(),
// Liste des suggestions
Obx(() {
if (controller.isSearching.value) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
if (controller.suggestedClients.isEmpty) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
Text(
'Clients trouvés:',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
...controller.suggestedClients.map((client) =>
_buildClientSuggestionTile(client, controller),
),
],
);
}),
],
),
),
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Annuler'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: controller.submitForm,
child: const Text('Valider la commande'),
),
],
),
);
}
// Widget pour la section de recherche
Widget _buildSearchSection(ClientFormController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recherche rapide',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
TextFormField(
decoration: InputDecoration(
labelText: 'Rechercher un client existant',
hintText: 'Nom, prénom, email ou téléphone...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
onChanged: controller.searchClients,
),
],
);
}
// Widget pour afficher une suggestion de client
Widget _buildClientSuggestionTile(Client client, ClientFormController controller) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.shade100,
child: Icon(Icons.person, color: Colors.blue.shade700),
),
title: Text(
client.nomComplet,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('📧 ${client.email}'),
Text('📞 ${client.telephone}'),
if (client.adresse != null && client.adresse!.isNotEmpty)
Text('📍 ${client.adresse}'),
],
),
trailing: ElevatedButton(
onPressed: () => controller.fillFormWithClient(client),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('Utiliser'),
),
isThreeLine: true,
),
);
}
// Widget helper pour les champs de texte
Widget _buildTextFormField({
required TextEditingController controller,
required String label,
TextInputType? keyboardType,
String? Function(String?)? validator,
int maxLines = 1,
void Function(String)? onChanged,
}) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: label,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: keyboardType,
validator: validator,
maxLines: maxLines,
onChanged: onChanged,
);
}
// Votre méthode _buildCommercialDropdown existante
Widget _buildCommercialDropdown() {
// Votre implémentation existante
return Container(); // Remplacez par votre code existant
}

471
lib/Components/AddClientForm.dart

@ -0,0 +1,471 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Models/client.dart';
class ClientFormWidget extends StatefulWidget {
final Function(Client) onClientSelected;
final Client? initialClient;
const ClientFormWidget({
Key? key,
required this.onClientSelected,
this.initialClient,
}) : super(key: key);
@override
State<ClientFormWidget> createState() => _ClientFormWidgetState();
}
class _ClientFormWidgetState extends State<ClientFormWidget> {
final _formKey = GlobalKey<FormState>();
final AppDatabase _database = AppDatabase.instance;
// Contrôleurs de texte
final TextEditingController _nomController = TextEditingController();
final TextEditingController _prenomController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _telephoneController = TextEditingController();
final TextEditingController _adresseController = TextEditingController();
// Variables d'état
bool _isLoading = false;
Client? _selectedClient;
List<Client> _suggestions = [];
bool _showSuggestions = false;
String _searchQuery = '';
@override
void initState() {
super.initState();
if (widget.initialClient != null) {
_fillClientData(widget.initialClient!);
}
// Écouter les changements dans les champs pour déclencher la recherche
_emailController.addListener(_onEmailChanged);
_telephoneController.addListener(_onPhoneChanged);
_nomController.addListener(_onNameChanged);
_prenomController.addListener(_onNameChanged);
}
@override
void dispose() {
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
super.dispose();
}
void _fillClientData(Client client) {
setState(() {
_selectedClient = client;
_nomController.text = client.nom;
_prenomController.text = client.prenom;
_emailController.text = client.email;
_telephoneController.text = client.telephone;
_adresseController.text = client.adresse ?? '';
});
}
void _clearForm() {
setState(() {
_selectedClient = null;
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_adresseController.clear();
_suggestions.clear();
_showSuggestions = false;
});
}
// Recherche par email
void _onEmailChanged() async {
final email = _emailController.text.trim();
if (email.length >= 3 && email.contains('@')) {
_searchExistingClient(email: email);
}
}
// Recherche par téléphone
void _onPhoneChanged() async {
final phone = _telephoneController.text.trim();
if (phone.length >= 4) {
_searchExistingClient(telephone: phone);
}
}
// Recherche par nom/prénom
void _onNameChanged() async {
final nom = _nomController.text.trim();
final prenom = _prenomController.text.trim();
if (nom.length >= 2 || prenom.length >= 2) {
final query = '$nom $prenom'.trim();
if (query.length >= 2) {
_getSuggestions(query);
}
}
}
// Rechercher un client existant
Future<void> _searchExistingClient({
String? email,
String? telephone,
String? nom,
String? prenom,
}) async {
if (_selectedClient != null) return; // Éviter de chercher si un client est déjà sélectionné
try {
setState(() => _isLoading = true);
final existingClient = await _database.findExistingClient(
email: email,
telephone: telephone,
nom: nom,
prenom: prenom,
);
if (existingClient != null && mounted) {
_showClientFoundDialog(existingClient);
}
} catch (e) {
print('Erreur lors de la recherche: $e');
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
// Obtenir les suggestions
Future<void> _getSuggestions(String query) async {
if (query.length < 2) {
setState(() {
_suggestions.clear();
_showSuggestions = false;
});
return;
}
try {
final suggestions = await _database.suggestClients(query);
if (mounted) {
setState(() {
_suggestions = suggestions;
_showSuggestions = suggestions.isNotEmpty;
_searchQuery = query;
});
}
} catch (e) {
print('Erreur lors de la récupération des suggestions: $e');
}
}
// Afficher le dialogue de client trouvé
void _showClientFoundDialog(Client client) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Client existant trouvé'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Un client avec ces informations existe déjà :'),
const SizedBox(height: 10),
Text('Nom: ${client.nom} ${client.prenom}', style: const TextStyle(fontWeight: FontWeight.bold)),
Text('Email: ${client.email}'),
Text('Téléphone: ${client.telephone}'),
if (client.adresse != null) Text('Adresse: ${client.adresse}'),
const SizedBox(height: 10),
const Text('Voulez-vous utiliser ces informations ?'),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
// Continuer avec les nouvelles données
},
child: const Text('Non, créer nouveau'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_fillClientData(client);
},
child: const Text('Oui, utiliser'),
),
],
),
);
}
// Valider et soumettre le formulaire
void _submitForm() async {
if (!_formKey.currentState!.validate()) return;
try {
setState(() => _isLoading = true);
Client client;
if (_selectedClient != null) {
// Utiliser le client existant avec les données mises à jour
client = Client(
id: _selectedClient!.id,
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim().toLowerCase(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
dateCreation: _selectedClient!.dateCreation,
actif: _selectedClient!.actif,
);
} else {
// Créer un nouveau client
client = Client(
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim().toLowerCase(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
dateCreation: DateTime.now(),
);
// Utiliser createOrGetClient pour éviter les doublons
client = await _database.createOrGetClient(client);
}
widget.onClientSelected(client);
} catch (e) {
Get.snackbar(
'Erreur',
'Erreur lors de la sauvegarde du client: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// En-tête avec bouton de réinitialisation
Row(
children: [
const Text(
'Informations du client',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Spacer(),
if (_selectedClient != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Client existant',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
const SizedBox(width: 8),
IconButton(
onPressed: _clearForm,
icon: const Icon(Icons.refresh),
tooltip: 'Nouveau client',
),
],
),
const SizedBox(height: 16),
// Champs du formulaire
Row(
children: [
Expanded(
child: TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom *',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le nom est requis';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _prenomController,
decoration: const InputDecoration(
labelText: 'Prénom *',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le prénom est requis';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// Email avec indicateur de chargement
Stack(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email *',
border: const OutlineInputBorder(),
suffixIcon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'L\'email est requis';
}
if (!GetUtils.isEmail(value)) {
return 'Email invalide';
}
return null;
},
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _telephoneController,
decoration: const InputDecoration(
labelText: 'Téléphone *',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le téléphone est requis';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _adresseController,
decoration: const InputDecoration(
labelText: 'Adresse',
border: OutlineInputBorder(),
),
maxLines: 2,
),
// Suggestions
if (_showSuggestions && _suggestions.isNotEmpty) ...[
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
children: [
const Icon(Icons.people, size: 16),
const SizedBox(width: 8),
const Text('Clients similaires trouvés:', style: TextStyle(fontWeight: FontWeight.bold)),
const Spacer(),
IconButton(
onPressed: () => setState(() => _showSuggestions = false),
icon: const Icon(Icons.close, size: 16),
),
],
),
),
...List.generate(_suggestions.length, (index) {
final suggestion = _suggestions[index];
return ListTile(
dense: true,
leading: const Icon(Icons.person, size: 20),
title: Text('${suggestion.nom} ${suggestion.prenom}'),
subtitle: Text('${suggestion.email}${suggestion.telephone}'),
trailing: ElevatedButton(
onPressed: () => _fillClientData(suggestion),
child: const Text('Utiliser'),
),
);
}),
],
),
),
],
const SizedBox(height: 24),
// Bouton de soumission
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 8),
Text('Traitement...'),
],
)
: Text(_selectedClient != null ? 'Utiliser ce client' : 'Créer le client'),
),
),
],
),
);
}
}

176
lib/Components/DiscountDialog.dart

@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/snackbar/snackbar.dart';
import 'package:youmazgestion/Models/Remise.dart';
class DiscountDialog extends StatefulWidget {
final Function(Remise) onDiscountApplied;
const DiscountDialog({super.key, required this.onDiscountApplied});
@override
_DiscountDialogState createState() => _DiscountDialogState();
}
class _DiscountDialogState extends State<DiscountDialog> {
RemiseType _selectedType = RemiseType.pourcentage;
final _valueController = TextEditingController();
final _descriptionController = TextEditingController();
@override
void dispose() {
_valueController.dispose();
_descriptionController.dispose();
super.dispose();
}
void _applyDiscount() {
final value = double.tryParse(_valueController.text) ?? 0;
if (value <= 0) {
Get.snackbar(
'Erreur',
'Veuillez entrer une valeur valide',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
if (_selectedType == RemiseType.pourcentage && value > 100) {
Get.snackbar(
'Erreur',
'Le pourcentage ne peut pas dépasser 100%',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
final remise = Remise(
type: _selectedType,
valeur: value,
description: _descriptionController.text,
);
widget.onDiscountApplied(remise);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Icon(Icons.local_offer, color: Colors.orange.shade600),
const SizedBox(width: 8),
const Text('Appliquer une remise'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Type de remise:', style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: RadioListTile<RemiseType>(
contentPadding: EdgeInsets.zero,
title: const Text('Pourcentage'),
value: RemiseType.pourcentage,
groupValue: _selectedType,
onChanged: (value) => setState(() => _selectedType = value!),
),
),
Expanded(
child: RadioListTile<RemiseType>(
contentPadding: EdgeInsets.zero,
title: const Text('Montant fixe'),
value: RemiseType.fixe,
groupValue: _selectedType,
onChanged: (value) => setState(() => _selectedType = value!),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _valueController,
keyboardType: TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: _selectedType == RemiseType.pourcentage
? 'Pourcentage (%)'
: 'Montant (MGA)',
prefixIcon: Icon(
_selectedType == RemiseType.pourcentage
? Icons.percent
: Icons.attach_money,
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Motif de la remise (optionnel)',
prefixIcon: Icon(Icons.note),
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 16),
// Aperçu de la remise
if (_valueController.text.isNotEmpty)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Aperçu:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(
_selectedType == RemiseType.pourcentage
? 'Remise de ${_valueController.text}%'
: 'Remise de ${_valueController.text} MGA',
),
if (_descriptionController.text.isNotEmpty)
Text('Motif: ${_descriptionController.text}'),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _applyDiscount,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade600,
foregroundColor: Colors.white,
),
child: const Text('Appliquer'),
),
],
);
}
}

349
lib/Components/GiftaselectedButton.dart

@ -0,0 +1,349 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:youmazgestion/Models/Remise.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class GiftSelectionDialog extends StatefulWidget {
const GiftSelectionDialog({super.key});
@override
_GiftSelectionDialogState createState() => _GiftSelectionDialogState();
}
class _GiftSelectionDialogState extends State<GiftSelectionDialog> {
final AppDatabase _database = AppDatabase.instance;
final _searchController = TextEditingController();
List<Product> _products = [];
List<Product> _filteredProducts = [];
bool _isLoading = true;
String? _selectedCategory;
@override
void initState() {
super.initState();
_loadProducts();
_searchController.addListener(_filterProducts);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadProducts() async {
try {
final products = await _database.getProducts();
setState(() {
_products = products.where((p) => p.stock > 0).toList(); // Seulement les produits en stock
_filteredProducts = _products;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
Get.snackbar(
'Erreur',
'Impossible de charger les produits',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
void _filterProducts() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredProducts = _products.where((product) {
final matchesSearch = product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false) ||
(product.imei?.toLowerCase().contains(query) ?? false);
final matchesCategory = _selectedCategory == null ||
product.category == _selectedCategory;
return matchesSearch && matchesCategory;
}).toList();
});
}
void _selectGift(Product product) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.card_giftcard, color: Colors.purple.shade600),
const SizedBox(width: 8),
const Text('Confirmer le cadeau'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Produit sélectionné:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.purple.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
if (product.reference != null && product.reference!.isNotEmpty)
Text('Référence: ${product.reference}'),
if (product.category.isNotEmpty)
Text('Catégorie: ${product.category}'),
Text('Prix normal: ${product.price.toStringAsFixed(0)} MGA'),
Text('Stock disponible: ${product.stock}'),
],
),
),
const SizedBox(height: 16),
const Text(
'Ce produit sera ajouté à la commande avec un prix de 0 MGA.',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context); // Fermer ce dialogue
Navigator.pop(context, ProduitCadeau(produit: product)); // Retourner le produit
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
),
child: const Text('Confirmer le cadeau'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final categories = _products.map((p) => p.category).toSet().toList()..sort();
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(20),
child: Column(
children: [
// En-tête
Row(
children: [
Icon(Icons.card_giftcard, color: Colors.purple.shade600, size: 28),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Choisir un cadeau',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 16),
// Barre de recherche
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Rechercher un produit',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 12),
// Filtre par catégorie
Container(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
FilterChip(
label: const Text('Toutes'),
selected: _selectedCategory == null,
onSelected: (selected) {
setState(() {
_selectedCategory = null;
_filterProducts();
});
},
),
const SizedBox(width: 8),
...categories.map((category) => Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(category),
selected: _selectedCategory == category,
onSelected: (selected) {
setState(() {
_selectedCategory = selected ? category : null;
_filterProducts();
});
},
),
)),
],
),
),
const SizedBox(height: 16),
// Liste des produits
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredProducts.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucun produit disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'Essayez de modifier vos critères de recherche',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
)
: ListView.builder(
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
leading: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.purple.shade200),
),
child: product.image != null && product.image!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
product.image!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.image_not_supported,
color: Colors.purple.shade300),
),
)
: Icon(Icons.card_giftcard,
color: Colors.purple.shade400, size: 30),
),
title: Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (product.reference != null && product.reference!.isNotEmpty)
Text('Ref: ${product.reference}'),
Text('Catégorie: ${product.category}'),
Text(
'Prix: ${product.price.toStringAsFixed(0)} MGA',
style: TextStyle(
color: Colors.green.shade600,
fontWeight: FontWeight.w600,
),
),
],
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Stock: ${product.stock}',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => _selectGift(product),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Choisir', style: TextStyle(fontSize: 12)),
),
],
),
onTap: () => _selectGift(product),
),
);
},
),
),
],
),
),
);
}
}

338
lib/Components/PaymentEnchainedDialog.dart

@ -0,0 +1,338 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:youmazgestion/Components/DiscountDialog.dart';
import 'package:youmazgestion/Components/paymentType.dart';
import 'package:youmazgestion/Models/Client.dart';
import 'package:youmazgestion/Models/Remise.dart';
// Dialogue de paiement amélioré avec support des remises
class PaymentMethodEnhancedDialog extends StatefulWidget {
final Commande commande;
const PaymentMethodEnhancedDialog({super.key, required this.commande});
@override
_PaymentMethodEnhancedDialogState createState() => _PaymentMethodEnhancedDialogState();
}
class _PaymentMethodEnhancedDialogState extends State<PaymentMethodEnhancedDialog> {
PaymentType _selectedPayment = PaymentType.cash;
final _amountController = TextEditingController();
Remise? _appliedRemise;
@override
void initState() {
super.initState();
_amountController.text = widget.commande.montantTotal.toStringAsFixed(2);
}
@override
void dispose() {
_amountController.dispose();
super.dispose();
}
void _showDiscountDialog() {
showDialog(
context: context,
builder: (context) => DiscountDialog(
onDiscountApplied: (remise) {
setState(() {
_appliedRemise = remise;
final montantFinal = widget.commande.montantTotal - remise.calculerRemise(widget.commande.montantTotal);
_amountController.text = montantFinal.toStringAsFixed(2);
});
},
),
);
}
void _removeDiscount() {
setState(() {
_appliedRemise = null;
_amountController.text = widget.commande.montantTotal.toStringAsFixed(2);
});
}
void _validatePayment() {
final montantFinal = _appliedRemise != null
? widget.commande.montantTotal - _appliedRemise!.calculerRemise(widget.commande.montantTotal)
: widget.commande.montantTotal;
if (_selectedPayment == PaymentType.cash) {
final amountGiven = double.tryParse(_amountController.text) ?? 0;
if (amountGiven < montantFinal) {
Get.snackbar(
'Erreur',
'Le montant donné est insuffisant',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
}
Navigator.pop(context, PaymentMethodEnhanced(
type: _selectedPayment,
amountGiven: _selectedPayment == PaymentType.cash
? double.parse(_amountController.text)
: montantFinal,
remise: _appliedRemise,
));
}
@override
Widget build(BuildContext context) {
final montantOriginal = widget.commande.montantTotal;
final montantFinal = _appliedRemise != null
? montantOriginal - _appliedRemise!.calculerRemise(montantOriginal)
: montantOriginal;
final amount = double.tryParse(_amountController.text) ?? 0;
final change = amount - montantFinal;
return AlertDialog(
title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Résumé des montants
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant original:'),
Text('${montantOriginal.toStringAsFixed(0)} MGA'),
],
),
if (_appliedRemise != null) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Remise (${_appliedRemise!.libelle}):'),
Text(
'- ${_appliedRemise!.calculerRemise(montantOriginal).toStringAsFixed(0)} MGA',
style: const TextStyle(color: Colors.red),
),
],
),
const Divider(),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('${montantFinal.toStringAsFixed(0)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
],
),
),
const SizedBox(height: 16),
// Bouton remise
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _appliedRemise == null ? _showDiscountDialog : _removeDiscount,
icon: Icon(_appliedRemise == null ? Icons.local_offer : Icons.close),
label: Text(_appliedRemise == null ? 'Ajouter remise' : 'Supprimer remise'),
style: OutlinedButton.styleFrom(
foregroundColor: _appliedRemise == null ? Colors.orange : Colors.red,
side: BorderSide(
color: _appliedRemise == null ? Colors.orange : Colors.red,
),
),
),
),
],
),
const SizedBox(height: 16),
// Section Paiement mobile
const Align(
alignment: Alignment.centerLeft,
child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildMobileMoneyTile(
title: 'Mvola',
imagePath: 'assets/mvola.jpg',
value: PaymentType.mvola,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMobileMoneyTile(
title: 'Orange Money',
imagePath: 'assets/Orange_money.png',
value: PaymentType.orange,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMobileMoneyTile(
title: 'Airtel Money',
imagePath: 'assets/airtel_money.png',
value: PaymentType.airtel,
),
),
],
),
const SizedBox(height: 16),
// Section Carte bancaire
const Align(
alignment: Alignment.centerLeft,
child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)),
),
const SizedBox(height: 8),
_buildPaymentMethodTile(
title: 'Carte bancaire',
icon: Icons.credit_card,
value: PaymentType.card,
),
const SizedBox(height: 16),
// Section Paiement en liquide
const Align(
alignment: Alignment.centerLeft,
child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)),
),
const SizedBox(height: 8),
_buildPaymentMethodTile(
title: 'Paiement en liquide',
icon: Icons.money,
value: PaymentType.cash,
),
if (_selectedPayment == PaymentType.cash) ...[
const SizedBox(height: 12),
TextField(
controller: _amountController,
decoration: const InputDecoration(
labelText: 'Montant donné',
prefixText: 'MGA ',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.numberWithOptions(decimal: true),
onChanged: (value) => setState(() {}),
),
const SizedBox(height: 8),
Text(
'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: change >= 0 ? Colors.green : Colors.red,
),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler', style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
),
onPressed: _validatePayment,
child: const Text('Confirmer'),
),
],
);
}
Widget _buildMobileMoneyTile({
required String title,
required String imagePath,
required PaymentType value,
}) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2),
width: 2,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => setState(() => _selectedPayment = value),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Image.asset(
imagePath,
height: 30,
width: 30,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.mobile_friendly, size: 30),
),
const SizedBox(height: 8),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12),
),
],
),
),
),
);
}
Widget _buildPaymentMethodTile({
required String title,
required IconData icon,
required PaymentType value,
}) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2),
width: 2,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => setState(() => _selectedPayment = value),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(icon, size: 24),
const SizedBox(width: 12),
Text(title),
],
),
),
),
);
}
}

23
lib/Components/appDrawer.dart

@ -14,7 +14,7 @@ import 'package:youmazgestion/Views/newCommand.dart';
import 'package:youmazgestion/Views/registrationPage.dart'; import 'package:youmazgestion/Views/registrationPage.dart';
import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/controller/userController.dart';
import 'package:youmazgestion/Views/pointage.dart'; import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; // Nouvel import
class CustomDrawer extends StatelessWidget { class CustomDrawer extends StatelessWidget {
final UserController userController = Get.find<UserController>(); final UserController userController = Get.find<UserController>();
@ -106,7 +106,7 @@ class CustomDrawer extends StatelessWidget {
color: Colors.blue, color: Colors.blue,
permissionAction: 'view', permissionAction: 'view',
permissionRoute: '/accueil', 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), color: const Color.fromARGB(255, 4, 54, 95),
permissionAction: 'update', permissionAction: 'update',
permissionRoute: '/pointage', permissionRoute: '/pointage',
onTap: () => Get.to(const PointagePage()), onTap: () => {},
) )
]; ];
@ -233,7 +233,7 @@ class CustomDrawer extends StatelessWidget {
color: Colors.teal, color: Colors.teal,
permissionAction: 'read', permissionAction: 'read',
permissionRoute: '/bilan', permissionRoute: '/bilan',
onTap: () => Get.to( DashboardPage()), onTap: () => Get.to(DashboardPage()),
), ),
await _buildDrawerItem( await _buildDrawerItem(
icon: Icons.history, icon: Icons.history,
@ -241,7 +241,7 @@ class CustomDrawer extends StatelessWidget {
color: Colors.blue, color: Colors.blue,
permissionAction: 'read', permissionAction: 'read',
permissionRoute: '/historique', permissionRoute: '/historique',
onTap: () => Get.to(HistoryPage()), onTap: () => Get.to(const HistoriquePage()),
), ),
]; ];
@ -271,6 +271,14 @@ class CustomDrawer extends StatelessWidget {
permissionRoute: '/gerer-roles', permissionRoute: '/gerer-roles',
onTap: () => Get.to(const RoleListPage()), 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)) { if (administrationItems.any((item) => item is ListTile)) {
@ -292,7 +300,6 @@ class CustomDrawer extends StatelessWidget {
drawerItems.add(const Divider()); drawerItems.add(const Divider());
drawerItems.add( drawerItems.add(
ListTile( ListTile(
leading: const Icon(Icons.logout, color: Colors.red), leading: const Icon(Icons.logout, color: Colors.red),
@ -414,7 +421,7 @@ class CustomDrawer extends StatelessWidget {
), ),
), ),
barrierDismissible: true, barrierDismissible: true,
); );
}, },
), ),
); );
@ -449,5 +456,3 @@ class CustomDrawer extends StatelessWidget {
} }
} }
class HistoryPage {
}

214
lib/Models/Client.dart

@ -1,4 +1,4 @@
// Models/client.dart // Models/client.dart - Version corrigée pour MySQL
class Client { class Client {
final int? id; final int? id;
final String nom; final String nom;
@ -33,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<String, dynamic> map) { factory Client.fromMap(Map<String, dynamic> map) {
return Client( return Client(
id: map['id'], id: map['id'] as int?,
nom: map['nom'], nom: map['nom'] as String,
prenom: map['prenom'], prenom: map['prenom'] as String,
email: map['email'], email: map['email'] as String,
telephone: map['telephone'], telephone: map['telephone'] as String,
adresse: map['adresse'], adresse: map['adresse'] as String?,
dateCreation: DateTime.parse(map['dateCreation']), dateCreation: _parseDateTime(map['dateCreation']),
actif: map['actif'] == 1, actif: (map['actif'] as int?) == 1,
); );
} }
@ -65,17 +89,18 @@ class Commande {
final DateTime? dateLivraison; final DateTime? dateLivraison;
final int? commandeurId; final int? commandeurId;
final int? validateurId; final int? validateurId;
// Données du client (pour les jointures)
final String? clientNom; final String? clientNom;
final String? clientPrenom; final String? clientPrenom;
final String? clientEmail; final String? clientEmail;
final double? remisePourcentage;
final double? remiseMontant;
final double? montantApresRemise;
Commande({ Commande({
this.id, this.id,
required this.clientId, required this.clientId,
required this.dateCommande, required this.dateCommande,
this.statut = StatutCommande.enAttente, required this.statut,
required this.montantTotal, required this.montantTotal,
this.notes, this.notes,
this.dateLivraison, this.dateLivraison,
@ -84,8 +109,29 @@ class Commande {
this.clientNom, this.clientNom,
this.clientPrenom, this.clientPrenom,
this.clientEmail, this.clientEmail,
this.remisePourcentage,
this.remiseMontant,
this.montantApresRemise,
}); });
String get clientNomComplet {
if (clientNom != null && clientPrenom != null) {
return '$clientPrenom $clientNom';
}
return 'Client inconnu';
}
String get statutLibelle {
switch (statut) {
case StatutCommande.enAttente:
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.annulee:
return 'Annulée';
}
}
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
@ -97,55 +143,77 @@ class Commande {
'dateLivraison': dateLivraison?.toIso8601String(), 'dateLivraison': dateLivraison?.toIso8601String(),
'commandeurId': commandeurId, 'commandeurId': commandeurId,
'validateurId': validateurId, 'validateurId': validateurId,
'remisePourcentage': remisePourcentage,
'remiseMontant': remiseMontant,
'montantApresRemise': montantApresRemise,
}; };
} }
factory Commande.fromMap(Map<String, dynamic> map) { factory Commande.fromMap(Map<String, dynamic> map) {
return Commande( return Commande(
id: map['id'], id: map['id'] as int?,
clientId: map['clientId'], clientId: map['clientId'] as int,
dateCommande: DateTime.parse(map['dateCommande']), dateCommande: Client._parseDateTime(map['dateCommande']),
statut: StatutCommande.values[map['statut']], statut: StatutCommande.values[(map['statut'] as int)],
montantTotal: map['montantTotal'].toDouble(), montantTotal: (map['montantTotal'] as num).toDouble(),
notes: map['notes'], notes: map['notes'] as String?,
dateLivraison: map['dateLivraison'] != null dateLivraison: map['dateLivraison'] != null
? DateTime.parse(map['dateLivraison']) ? Client._parseDateTime(map['dateLivraison'])
: null,
commandeurId: map['commandeurId'] as int?,
validateurId: map['validateurId'] as int?,
clientNom: map['clientNom'] as String?,
clientPrenom: map['clientPrenom'] as String?,
clientEmail: map['clientEmail'] as String?,
remisePourcentage: map['remisePourcentage'] != null
? (map['remisePourcentage'] as num).toDouble()
: null,
remiseMontant: map['remiseMontant'] != null
? (map['remiseMontant'] as num).toDouble()
: null,
montantApresRemise: map['montantApresRemise'] != null
? (map['montantApresRemise'] as num).toDouble()
: null, : null,
commandeurId: map['commandeurId'],
validateurId: map['validateurId'],
clientNom: map['clientNom'],
clientPrenom: map['clientPrenom'],
clientEmail: map['clientEmail'],
); );
} }
String get statutLibelle { Commande copyWith({
switch (statut) { int? id,
case StatutCommande.enAttente: int? clientId,
return 'En attente'; DateTime? dateCommande,
case StatutCommande.confirmee: StatutCommande? statut,
return 'Confirmée'; double? montantTotal,
// case StatutCommande.enPreparation: String? notes,
// return 'En préparation'; DateTime? dateLivraison,
// case StatutCommande.expediee: int? commandeurId,
// return 'Expédiée'; int? validateurId,
// case StatutCommande.livree: String? clientNom,
// return 'Livrée'; String? clientPrenom,
case StatutCommande.annulee: String? clientEmail,
return 'Annulée'; double? remisePourcentage,
default: double? remiseMontant,
return 'Inconnu'; 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 { class DetailCommande {
final int? id; final int? id;
final int commandeId; final int commandeId;
@ -153,11 +221,10 @@ class DetailCommande {
final int quantite; final int quantite;
final double prixUnitaire; final double prixUnitaire;
final double sousTotal; final double sousTotal;
// Données du produit (pour les jointures)
final String? produitNom; final String? produitNom;
final String? produitImage; final String? produitImage;
final String? produitReference; final String? produitReference;
final bool? estCadeau;
DetailCommande({ DetailCommande({
this.id, this.id,
@ -169,6 +236,7 @@ class DetailCommande {
this.produitNom, this.produitNom,
this.produitImage, this.produitImage,
this.produitReference, this.produitReference,
this.estCadeau,
}); });
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@ -179,20 +247,48 @@ class DetailCommande {
'quantite': quantite, 'quantite': quantite,
'prixUnitaire': prixUnitaire, 'prixUnitaire': prixUnitaire,
'sousTotal': sousTotal, 'sousTotal': sousTotal,
'estCadeau': estCadeau == true ? 1 : 0,
}; };
} }
factory DetailCommande.fromMap(Map<String, dynamic> map) { factory DetailCommande.fromMap(Map<String, dynamic> map) {
return DetailCommande( return DetailCommande(
id: map['id'], id: map['id'] as int?,
commandeId: map['commandeId'], commandeId: map['commandeId'] as int,
produitId: map['produitId'], produitId: map['produitId'] as int,
quantite: map['quantite'], quantite: map['quantite'] as int,
prixUnitaire: map['prixUnitaire'].toDouble(), prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
sousTotal: map['sousTotal'].toDouble(), sousTotal: (map['sousTotal'] as num).toDouble(),
produitNom: map['produitNom'], produitNom: map['produitNom'] as String?,
produitImage: map['produitImage'], produitImage: map['produitImage'] as String?,
produitReference: map['produitReference'], produitReference: map['produitReference'] as String?,
estCadeau: map['estCadeau'] == 1,
);
}
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,
); );
} }
} }

64
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;
}
}

127
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 { class Product {
final int? id; final int? id;
final String name; final String name;
@ -29,31 +33,59 @@ class Product {
this.ram, this.ram,
this.memoireInterne, this.memoireInterne,
this.imei, this.imei,
}); });
bool isStockDefined() { bool isStockDefined() {
if (stock != null) { return stock > 0;
print("stock is defined : $stock $name"); }
return true;
} else { // Méthode helper pour convertir de façon sécurisée
return false; static String? _convertImageFromMap(dynamic imageValue) {
if (imageValue == null) {
return null;
}
// Si c'est déjà une String, on la retourne
if (imageValue is String) {
return imageValue;
}
// Le driver mysql1 peut retourner un Blob même pour TEXT
// Essayer de le convertir en String
try {
if (imageValue is Uint8List) {
// Convertir les bytes en String UTF-8
return utf8.decode(imageValue);
}
if (imageValue is List<int>) {
// Convertir les bytes en String UTF-8
return utf8.decode(imageValue);
}
// Dernier recours : toString()
return imageValue.toString();
} catch (e) {
print("Erreur conversion image: $e, type: ${imageValue.runtimeType}");
return null;
} }
} }
factory Product.fromMap(Map<String, dynamic> map) => Product( factory Product.fromMap(Map<String, dynamic> map) => Product(
id: map['id'], id: map['id'] as int?,
name: map['name'], name: map['name'] as String,
price: map['price'], price: (map['price'] as num).toDouble(), // Conversion sécurisée
image: map['image'], image: _convertImageFromMap(map['image']), // Utilisation de la méthode helper
category: map['category'], category: map['category'] as String,
stock: map['stock'], stock: (map['stock'] as int?) ?? 0, // Valeur par défaut
description: map['description'], description: map['description'] as String?,
qrCode: map['qrCode'], qrCode: map['qrCode'] as String?,
reference: map['reference'], reference: map['reference'] as String?,
pointDeVenteId: map['point_de_vente_id'], pointDeVenteId: map['point_de_vente_id'] as int?,
marque: map['marque'], marque: map['marque'] as String?,
ram: map['ram'], ram: map['ram'] as String?,
memoireInterne: map['memoire_interne'], memoireInterne: map['memoire_interne'] as String?,
imei: map['imei'], imei: map['imei'] as String?,
); );
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
@ -72,4 +104,59 @@ class Product {
'memoire_interne': memoireInterne, 'memoire_interne': memoireInterne,
'imei': imei, '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,
);
}
} }

27
lib/Models/users.dart

@ -1,3 +1,4 @@
// Models/users.dart - Version corrigée
class Users { class Users {
int? id; int? id;
String name; String name;
@ -6,7 +7,7 @@ class Users {
String password; String password;
String username; String username;
int roleId; int roleId;
String? roleName; // Optionnel, rempli lors des requêtes avec JOIN String? roleName;
int? pointDeVenteId; int? pointDeVenteId;
Users({ Users({
@ -24,12 +25,12 @@ class Users {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'name': name, 'name': name,
'lastname': lastName, 'lastname': lastName, // Correspond à la colonne DB
'email': email, 'email': email,
'password': password, 'password': password,
'username': username, 'username': username,
'role_id': roleId, 'role_id': roleId,
'point_de_vente_id' : pointDeVenteId, 'point_de_vente_id': pointDeVenteId,
}; };
} }
@ -41,19 +42,17 @@ class Users {
factory Users.fromMap(Map<String, dynamic> map) { factory Users.fromMap(Map<String, dynamic> map) {
return Users( return Users(
id: map['id'], id: map['id'] as int?,
name: map['name'], name: map['name'] as String,
lastName: map['lastname'], lastName: map['lastname'] as String, // Correspond à la colonne DB
email: map['email'], email: map['email'] as String,
password: map['password'], password: map['password'] as String,
username: map['username'], username: map['username'] as String,
roleId: map['role_id'], roleId: map['role_id'] as int,
roleName: map['role_name'], // Depuis les requêtes avec JOIN roleName: map['role_name'] as String?, // Depuis les JOINs
pointDeVenteId : map['point_de_vente_id'] pointDeVenteId: map['point_de_vente_id'] as int?,
); );
} }
// Getter pour la compatibilité avec l'ancien code
String get role => roleName ?? ''; String get role => roleName ?? '';
} }

60
lib/Services/pointageDatabase.dart

@ -1,60 +0,0 @@
import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import '../Models/pointage_model.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
Database? _db;
Future<Database> get database async {
if (_db != null) return _db!;
_db = await _initDatabase();
return _db!;
}
Future<Database> _initDatabase() async {
String databasesPath = await getDatabasesPath();
String dbPath = join(databasesPath, 'pointage.db');
return await openDatabase(dbPath, version: 1, onCreate: _onCreate);
}
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE pointages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userName TEXT NOT NULL,
date TEXT NOT NULL,
heureArrivee TEXT NOT NULL,
heureDepart TEXT NOT NULL
)
''');
}
Future<int> insertPointage(Pointage pointage) async {
final db = await database;
return await db.insert('pointages', pointage.toMap());
}
Future<List<Pointage>> getPointages() async {
final db = await database;
final pointages = await db.query('pointages');
return pointages.map((pointage) => Pointage.fromMap(pointage)).toList();
}
Future<int> updatePointage(Pointage pointage) async {
final db = await database;
return await db.update('pointages', pointage.toMap(),
where: 'id = ?', whereArgs: [pointage.id]);
}
Future<int> deletePointage(int id) async {
final db = await database;
return await db.delete('pointages', where: 'id = ?', whereArgs: [id]);
}
}

2801
lib/Services/stock_managementDatabase.dart

File diff suppressed because it is too large

675
lib/Views/HandleProduct.dart

@ -2290,85 +2290,560 @@ Future<void> _generatePDF(Product product, String qrUrl) async {
final stockController = TextEditingController(text: product.stock.toString()); final stockController = TextEditingController(text: product.stock.toString());
final descriptionController = TextEditingController(text: product.description ?? ''); final descriptionController = TextEditingController(text: product.description ?? '');
final imageController = TextEditingController(text: product.image); 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 selectedCategory = product.category; String? selectedPointDeVente;
List<Map<String, dynamic>> 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; 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<void> 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: () => <String, dynamic>{},
);
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( Get.dialog(
AlertDialog( AlertDialog(
title: const Text('Modifier le produit'), 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( content: Container(
width: 500, width: 600,
constraints: const BoxConstraints(maxHeight: 600),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( 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, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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),
// 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<String>(
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),
// 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<String>(
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( TextField(
controller: nameController, controller: referenceController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Nom du produit*', labelText: 'Référence',
border: OutlineInputBorder(), 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), 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( TextField(
controller: priceController, controller: marqueController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Prix*', labelText: 'Marque',
border: OutlineInputBorder(), border: OutlineInputBorder(),
prefixIcon: Icon(Icons.branding_watermark),
filled: true,
fillColor: Colors.white,
), ),
), ),
const SizedBox(height: 16), 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( TextField(
controller: stockController, controller: imeiController,
keyboardType: TextInputType.number,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Stock', labelText: 'IMEI (pour téléphones)',
border: OutlineInputBorder(), border: OutlineInputBorder(),
prefixIcon: Icon(Icons.smartphone),
filled: true,
fillColor: Colors.white,
),
),
],
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
StatefulBuilder( // Section Image
builder: (context, setDialogState) { Container(
return Column( 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: [ 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( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
controller: imageController, controller: imageController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Image', labelText: 'Chemin de l\'image',
border: OutlineInputBorder(), border: OutlineInputBorder(),
isDense: true,
), ),
readOnly: true, readOnly: true,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
ElevatedButton( ElevatedButton.icon(
onPressed: () async { onPressed: () async {
final result = await FilePicker.platform.pickFiles(type: FileType.image); final result = await FilePicker.platform.pickFiles(type: FileType.image);
if (result != null && result.files.single.path != null) { if (result != null && result.files.single.path != null) {
if (context.mounted) {
setDialogState(() { setDialogState(() {
pickedImage = File(result.files.single.path!); pickedImage = File(result.files.single.path!);
imageController.text = pickedImage!.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) // Aperçu de l'image
Container( Center(
child: Container(
height: 100, height: 100,
width: 100, width: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -2379,42 +2854,73 @@ Future<void> _generatePDF(Product product, String qrUrl) async {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: pickedImage != null child: pickedImage != null
? Image.file(pickedImage!, fit: BoxFit.cover) ? Image.file(pickedImage!, fit: BoxFit.cover)
: (product.image!.isNotEmpty : (product.image != null && product.image!.isNotEmpty
? Image.file(File(product.image!), fit: BoxFit.cover) ? 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 Icon(Icons.image, size: 50)),
), ),
), ),
const SizedBox(height: 16), ),
],
DropdownButtonFormField<String>(
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), const SizedBox(height: 16),
TextField( // Aperçu QR Code
controller: descriptionController, if (qrPreviewData != null)
maxLines: 3, Container(
decoration: const InputDecoration( padding: const EdgeInsets.all(12),
labelText: 'Description', decoration: BoxDecoration(
border: OutlineInputBorder(), 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),
),
],
),
), ),
], ],
);
},
), ),
), ),
), ),
@ -2423,49 +2929,102 @@ Future<void> _generatePDF(Product product, String qrUrl) async {
onPressed: () => Get.back(), onPressed: () => Get.back(),
child: const Text('Annuler'), child: const Text('Annuler'),
), ),
ElevatedButton( ElevatedButton.icon(
onPressed: () async { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
final price = double.tryParse(priceController.text.trim()) ?? 0.0; final price = double.tryParse(priceController.text.trim()) ?? 0.0;
final stock = int.tryParse(stockController.text.trim()) ?? 0; final stock = int.tryParse(stockController.text.trim()) ?? 0;
final reference = referenceController.text.trim();
if (name.isEmpty || price <= 0) { if (name.isEmpty || price <= 0) {
Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); Get.snackbar('Erreur', 'Nom et prix sont obligatoires');
return; 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( final updatedProduct = Product(
id: product.id, id: product.id,
name: name, name: name,
price: price, price: price,
image: imageController.text, image: imageController.text.trim(),
category: selectedCategory, category: selectedCategory,
description: descriptionController.text.trim(), description: descriptionController.text.trim(),
stock: stock, stock: stock,
qrCode: product.qrCode, qrCode: product.qrCode, // Conserver le QR code existant
reference: product.reference, 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); await _productDatabase.updateProduct(updatedProduct);
Get.back(); Get.back();
Get.snackbar( Get.snackbar(
'Succès', 'Succès',
'Produit modifié avec succès', 'Produit modifié avec succès!\nRéférence: $reference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}',
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 4),
icon: const Icon(Icons.check_circle, color: Colors.white),
); );
_loadProducts(); _loadProducts();
_loadPointsDeVente(); // Recharger aussi les points de vente
} catch (e) { } catch (e) {
Get.snackbar('Erreur', 'Modification échouée: $e'); Get.snackbar('Erreur', 'Modification du produit échouée: $e');
} }
}, },
child: const Text('Sauvegarder'), 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) { void _deleteProduct(Product product) {
Get.dialog( Get.dialog(

493
lib/Views/RolePermissionPage.dart

@ -19,6 +19,8 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
List<Permission> permissions = []; List<Permission> permissions = [];
List<Map<String, dynamic>> menus = []; List<Map<String, dynamic>> menus = [];
Map<int, Map<String, bool>> menuPermissionsMap = {}; Map<int, Map<String, bool>> menuPermissionsMap = {};
bool isLoading = true;
String? errorMessage;
@override @override
void initState() { void initState() {
@ -27,8 +29,14 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
} }
Future<void> _initData() async { Future<void> _initData() async {
try {
setState(() {
isLoading = true;
errorMessage = null;
});
final perms = await db.getAllPermissions(); final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu')); final menuList = await db.getAllMenus(); // Utilise la nouvelle méthode
Map<int, Map<String, bool>> tempMenuPermissionsMap = {}; Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
@ -47,11 +55,20 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
permissions = perms; permissions = perms;
menus = menuList; menus = menuList;
menuPermissionsMap = tempMenuPermissionsMap; menuPermissionsMap = tempMenuPermissionsMap;
isLoading = false;
}); });
} catch (e) {
setState(() {
errorMessage = 'Erreur lors du chargement des données: $e';
isLoading = false;
});
print("Erreur lors de l'initialisation des données: $e");
}
} }
Future<void> _onPermissionToggle( Future<void> _onPermissionToggle(
int menuId, String permission, bool enabled) async { int menuId, String permission, bool enabled) async {
try {
final perm = permissions.firstWhere((p) => p.name == permission); final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) { if (enabled) {
@ -65,61 +82,226 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
setState(() { setState(() {
menuPermissionsMap[menuId]![permission] = enabled; menuPermissionsMap[menuId]![permission] = enabled;
}); });
}
@override // Afficher un message de confirmation
Widget build(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar(
return Scaffold( SnackBar(
appBar: CustomAppBar( content: Text(
title: "Permissions - ${widget.role.designation}", enabled
// showBackButton: true, ? 'Permission "$permission" accordée'
: 'Permission "$permission" révoquée',
),
backgroundColor: enabled ? Colors.green : Colors.orange,
duration: const Duration(seconds: 2),
), ),
body: Padding( );
} 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,
),
);
}
}
void _toggleAllPermissions(int menuId, bool enabled) {
for (var permission in permissions) {
_onPermissionToggle(menuId, permission.name, enabled);
}
}
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), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row(
children: [
Icon(Icons.analytics, color: Colors.blue.shade600),
const SizedBox(width: 8),
Text( Text(
'Gestion des permissions pour le rôle: ${widget.role.designation}', 'Résumé des permissions',
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
), ),
), ),
const SizedBox(height: 10), ],
const Text(
'Sélectionnez les permissions pour chaque menu:',
style: TextStyle(fontSize: 14, color: Colors.grey),
), ),
const SizedBox(height: 20), const SizedBox(height: 12),
if (permissions.isNotEmpty && menus.isNotEmpty) LinearProgressIndicator(
Expanded( value: percentage,
child: ListView.builder( backgroundColor: Colors.grey.shade300,
itemCount: menus.length, valueColor: AlwaysStoppedAnimation<Color>(
itemBuilder: (context, index) { percentage > 0.7 ? Colors.green :
final menu = menus[index]; percentage > 0.3 ? Colors.orange : Colors.red,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$selectedPermissions / $totalPermissions permissions',
style: const TextStyle(fontWeight: FontWeight.w500),
),
Text(
'${(percentage * 100).toStringAsFixed(1)}%',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
],
),
),
);
}
Widget _buildMenuCard(Map<String, dynamic> menu) {
final menuId = menu['id'] as int; final menuId = menu['id'] as int;
final menuName = menu['name'] as String; final menuName = menu['name'] as String;
final menuRoute = menu['route'] as String;
final selectedCount = _getSelectedPermissionsCount(menuId);
final percentage = _getPermissionPercentage(menuId);
return Card( return Card(
margin: const EdgeInsets.only(bottom: 15), margin: const EdgeInsets.only(bottom: 16),
elevation: 3, elevation: 3,
child: Padding( shape: RoundedRectangleBorder(
padding: const EdgeInsets.all(12.0), borderRadius: BorderRadius.circular(12),
child: Column( ),
child: ExpansionTile(
leading: CircleAvatar(
backgroundColor: percentage == 1.0 ? Colors.green :
percentage > 0 ? Colors.orange : Colors.red.shade100,
child: Icon(
Icons.menu,
color: percentage == 1.0 ? Colors.white :
percentage > 0 ? Colors.white : Colors.red,
),
),
title: Text(
menuName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
menuName, menuRoute,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: percentage,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
percentage == 1.0 ? Colors.green :
percentage > 0 ? Colors.orange : Colors.red,
),
),
),
const SizedBox(width: 8),
Text(
'$selectedCount/${permissions.length}',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16), fontSize: 12,
fontWeight: FontWeight.w500,
), ),
const SizedBox(height: 8), ),
],
),
],
),
trailing: PopupMenuButton<String>(
onSelected: (value) {
if (value == 'all') {
_toggleAllPermissions(menuId, true);
} else if (value == 'none') {
_toggleAllPermissions(menuId, false);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'all',
child: Row(
children: [
Icon(Icons.select_all, color: Colors.green),
SizedBox(width: 8),
Text('Tout sélectionner'),
],
),
),
const PopupMenuItem(
value: 'none',
child: Row(
children: [
Icon(Icons.deselect, color: Colors.red),
SizedBox(width: 8),
Text('Tout désélectionner'),
],
),
),
],
child: const Icon(Icons.more_vert),
),
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Permissions disponibles:',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
const SizedBox(height: 12),
Wrap( Wrap(
spacing: 10, spacing: 8,
runSpacing: 10, runSpacing: 8,
children: permissions.map((perm) { children: permissions.map((perm) {
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false; final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
return FilterChip( return CustomFilterChip(
label: perm.name, label: perm.name,
selected: isChecked, selected: isChecked,
onSelected: (bool value) { onSelected: (bool value) {
@ -131,48 +313,275 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
], ],
), ),
), ),
],
),
); );
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: "Permissions - ${widget.role.designation}",
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: errorMessage != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red.shade400,
),
const SizedBox(height: 16),
Text(
'Erreur de chargement',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.red.shade600,
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
errorMessage!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade600),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
),
],
),
)
: 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 else
const Expanded( Expanded(
child: Center( child: Center(
child: CircularProgressIndicator(), child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucune donnée disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Permissions: ${permissions.length} | Menus: ${menus.length}',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
), ),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Actualiser'),
), ),
], ],
), ),
), ),
),
],
),
),
floatingActionButton: !isLoading && errorMessage == null
? FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.save),
label: const Text('Enregistrer'),
backgroundColor: Colors.green,
)
: null,
); );
} }
} }
class FilterChip extends StatelessWidget { class CustomFilterChip extends StatelessWidget {
final String label; final String label;
final bool selected; final bool selected;
final ValueChanged<bool> onSelected; final ValueChanged<bool> onSelected;
const FilterChip({ const CustomFilterChip({
super.key, super.key,
required this.label, required this.label,
required this.selected, required this.selected,
required this.onSelected, required this.onSelected,
}); });
Color _getChipColor(String label) {
switch (label.toLowerCase()) {
case 'view':
case 'read':
return Colors.blue;
case 'create':
return Colors.green;
case 'update':
return Colors.orange;
case 'delete':
return Colors.red;
case 'admin':
return Colors.purple;
case 'manage':
return Colors.indigo;
default:
return Colors.grey;
}
}
IconData _getChipIcon(String label) {
switch (label.toLowerCase()) {
case 'view':
case 'read':
return Icons.visibility;
case 'create':
return Icons.add;
case 'update':
return Icons.edit;
case 'delete':
return Icons.delete;
case 'admin':
return Icons.admin_panel_settings;
case 'manage':
return Icons.settings;
default:
return Icons.security;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChoiceChip( final color = _getChipColor(label);
label: Text(label), final icon = _getChipIcon(label);
return FilterChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: selected ? Colors.white : color,
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
color: selected ? Colors.white : color,
fontWeight: FontWeight.w500,
),
),
],
),
selected: selected, selected: selected,
onSelected: onSelected, onSelected: onSelected,
selectedColor: Colors.blue, selectedColor: color,
labelStyle: TextStyle( backgroundColor: color.withOpacity(0.1),
color: selected ? Colors.white : Colors.black, checkmarkColor: Colors.white,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: selected ? color : color.withOpacity(0.3),
width: 1,
),
), ),
elevation: selected ? 4 : 1,
pressElevation: 8,
); );
} }
} }

813
lib/Views/commandManagement.dart

File diff suppressed because it is too large

319
lib/Views/gestionRole.dart

@ -29,9 +29,12 @@ class _HandleUserRoleState extends State<HandleUserRole> {
} }
Future<void> _initData() async { Future<void> _initData() async {
try {
final roleList = await db.getRoles(); final roleList = await db.getRoles();
final perms = await db.getAllPermissions(); final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu'));
// Récupération mise à jour des menus avec gestion d'erreur
final menuList = await db.getAllMenus();
Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {}; Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {};
@ -56,18 +59,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
menus = menuList; menus = menuList;
roleMenuPermissionsMap = tempRoleMenuPermissionsMap; roleMenuPermissionsMap = tempRoleMenuPermissionsMap;
}); });
} catch (e) {
print("Erreur lors de l'initialisation des données: $e");
// Afficher un message d'erreur à l'utilisateur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du chargement des données: $e'),
backgroundColor: Colors.red,
),
);
}
} }
Future<void> _addRole() async { Future<void> _addRole() async {
String designation = _roleController.text.trim(); String designation = _roleController.text.trim();
if (designation.isEmpty) return; if (designation.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez saisir une désignation pour le rôle'),
backgroundColor: Colors.orange,
),
);
return;
}
try {
// Vérifier si le rôle existe déjà
final existingRoles = roles.where((r) => r.designation.toLowerCase() == designation.toLowerCase());
if (existingRoles.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ce rôle existe déjà'),
backgroundColor: Colors.orange,
),
);
return;
}
await db.createRole(Role(designation: designation)); await db.createRole(Role(designation: designation));
_roleController.clear(); _roleController.clear();
await _initData(); await _initData();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Rôle "$designation" créé avec succès'),
backgroundColor: Colors.green,
),
);
} catch (e) {
print("Erreur lors de la création du rôle: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la création du rôle: $e'),
backgroundColor: Colors.red,
),
);
}
} }
Future<void> _onPermissionToggle(int roleId, int menuId, String permission, bool enabled) async { Future<void> _onPermissionToggle(int roleId, int menuId, String permission, bool enabled) async {
try {
final perm = permissions.firstWhere((p) => p.name == permission); final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) { if (enabled) {
@ -79,6 +130,70 @@ class _HandleUserRoleState extends State<HandleUserRole> {
setState(() { setState(() {
roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled; roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled;
}); });
} catch (e) {
print("Erreur lors de la modification de la permission: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la modification de la permission: $e'),
backgroundColor: Colors.red,
),
);
}
}
Future<void> _deleteRole(Role role) async {
// Empêcher la suppression du Super Admin
if (role.designation == 'Super Admin') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Impossible de supprimer le rôle Super Admin'),
backgroundColor: Colors.red,
),
);
return;
}
// Demander confirmation
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text('Êtes-vous sûr de vouloir supprimer le rôle "${role.designation}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirm == true) {
try {
await db.deleteRole(role.id);
await _initData();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Rôle "${role.designation}" supprimé avec succès'),
backgroundColor: Colors.green,
),
);
} catch (e) {
print("Erreur lors de la suppression du rôle: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la suppression du rôle: $e'),
backgroundColor: Colors.red,
),
);
}
}
} }
@override @override
@ -104,28 +219,52 @@ class _HandleUserRoleState extends State<HandleUserRole> {
controller: _roleController, controller: _roleController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Nouveau rôle', labelText: 'Nouveau rôle',
hintText: 'Ex: Manager, Vendeur...',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
onSubmitted: (_) => _addRole(),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
ElevatedButton( ElevatedButton.icon(
onPressed: _addRole, onPressed: _addRole,
icon: const Icon(Icons.add),
label: const Text('Ajouter'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
child: const Text('Ajouter'),
), ),
], ],
), ),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Affichage des statistiques
if (roles.isNotEmpty)
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('Rôles', roles.length.toString(), Icons.people),
_buildStatItem('Permissions', permissions.length.toString(), Icons.security),
_buildStatItem('Menus', menus.length.toString(), Icons.menu),
],
),
),
),
const SizedBox(height: 20),
// Tableau des rôles et permissions // Tableau des rôles et permissions
if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty) if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty)
Expanded( Expanded(
@ -137,22 +276,64 @@ class _HandleUserRoleState extends State<HandleUserRole> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.vertical,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width - 32,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: menus.map((menu) { children: menus.map((menu) {
final menuId = menu['id'] as int; final menuId = menu['id'] as int;
return Column( final menuName = menu['name'] as String;
final menuRoute = menu['route'] as String;
return Card(
margin: const EdgeInsets.only(bottom: 16.0),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.menu, color: Colors.blue.shade700),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
menu['name'], menuName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.blue.shade700,
),
),
Text(
menuRoute,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
), ),
DataTable( ),
],
),
),
],
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 20, columnSpacing: 20,
headingRowHeight: 50,
dataRowHeight: 60,
columns: [ columns: [
const DataColumn( const DataColumn(
label: Text( label: Text(
@ -161,17 +342,49 @@ class _HandleUserRoleState extends State<HandleUserRole> {
), ),
), ),
...permissions.map((perm) => DataColumn( ...permissions.map((perm) => DataColumn(
label: Text( label: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
perm.name, perm.name,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
),
)).toList(), )).toList(),
const DataColumn(
label: Text(
'Actions',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
], ],
rows: roles.map((role) { rows: roles.map((role) {
final roleId = role.id!; final roleId = role.id!;
return DataRow( return DataRow(
cells: [ cells: [
DataCell(Text(role.designation)), DataCell(
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: role.designation == 'Super Admin'
? Colors.red.shade50
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(4),
),
child: Text(
role.designation,
style: TextStyle(
fontWeight: FontWeight.w500,
color: role.designation == 'Super Admin'
? Colors.red.shade700
: Colors.blue.shade700,
),
),
),
),
...permissions.map((perm) { ...permissions.map((perm) {
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false; final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
return DataCell( return DataCell(
@ -180,26 +393,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
onChanged: (bool? value) { onChanged: (bool? value) {
_onPermissionToggle(roleId, menuId, perm.name, value ?? false); _onPermissionToggle(roleId, menuId, perm.name, value ?? false);
}, },
activeColor: Colors.green,
), ),
); );
}).toList(), }).toList(),
DataCell(
role.designation != 'Super Admin'
? IconButton(
icon: Icon(Icons.delete, color: Colors.red.shade600),
tooltip: 'Supprimer le rôle',
onPressed: () => _deleteRole(role),
)
: Icon(Icons.lock, color: Colors.grey.shade400),
),
], ],
); );
}).toList(), }).toList(),
), ),
),
], ],
),
),
); );
}).toList(), }).toList(),
), ),
), ),
), ),
), ),
),
) )
else else
const Expanded( Expanded(
child: Center( child: Center(
child: Text('Aucun rôle, permission ou menu trouvé'), child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey.shade400),
const SizedBox(height: 16),
Text(
'Aucune donnée disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Rôles: ${roles.length} | Permissions: ${permissions.length} | Menus: ${menus.length}',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Actualiser'),
),
],
),
), ),
), ),
], ],
@ -207,4 +460,34 @@ class _HandleUserRoleState extends State<HandleUserRole> {
), ),
); );
} }
Widget _buildStatItem(String label, String value, IconData icon) {
return Column(
children: [
Icon(icon, size: 32, color: Colors.blue.shade600),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
);
}
@override
void dispose() {
_roleController.dispose();
super.dispose();
}
} }

416
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<AjoutPointDeVentePage> {
final AppDatabase _appDatabase = AppDatabase.instance;
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
// Contrôleurs
final TextEditingController _nomController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
// Liste des points de vente
List<Map<String, dynamic>> _pointsDeVente = [];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadPointsDeVente();
_searchController.addListener(_filterPointsDeVente);
}
Future<void> _loadPointsDeVente() async {
setState(() {
_isLoading = true;
});
try {
final points = await _appDatabase.getPointsDeVente();
setState(() {
_pointsDeVente = points;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Impossible de charger les points de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
void _filterPointsDeVente() {
final query = _searchController.text.toLowerCase();
if (query.isEmpty) {
_loadPointsDeVente();
return;
}
setState(() {
_pointsDeVente = _pointsDeVente.where((point) {
final nom = point['nom']?.toString().toLowerCase() ?? '';
return nom.contains(query);
}).toList();
});
}
Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
await _appDatabase.createPointDeVente(
_nomController.text.trim(),
_codeController.text.trim(),
);
// Réinitialiser le formulaire
_nomController.clear();
_codeController.clear();
// Recharger la liste
await _loadPointsDeVente();
Get.snackbar(
'Succès',
'Point de vente ajouté avec succès',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible d\'ajouter le point de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _deletePointDeVente(int id) async {
final confirmed = await Get.dialog<bool>(
AlertDialog(
title: const Text('Confirmer la suppression'),
content: const Text('Voulez-vous vraiment supprimer ce point de vente ?'),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Get.back(result: true),
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
setState(() {
_isLoading = true;
});
try {
await _appDatabase.deletePointDeVente(id);
await _loadPointsDeVente();
Get.snackbar(
'Succès',
'Point de vente supprimé avec succès',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de supprimer le point de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Gestion des points de vente'),
drawer: CustomDrawer(),
body: Column(
children: [
// Formulaire d'ajout
Card(
margin: const EdgeInsets.all(16),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Ajouter un point de vente',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
const SizedBox(height: 16),
// Champ Nom
TextFormField(
controller: _nomController,
decoration: InputDecoration(
labelText: 'Nom du point de vente',
prefixIcon: const Icon(Icons.store),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un nom';
}
return null;
},
),
const SizedBox(height: 12),
// Champ Code
TextFormField(
controller: _codeController,
decoration: InputDecoration(
labelText: 'Code (optionnel)',
prefixIcon: const Icon(Icons.code),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 16),
// Bouton de soumission
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Ajouter le point de vente',
style: TextStyle(fontSize: 16),
),
),
],
),
),
),
),
// Liste des points de vente
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
// Barre de recherche
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Rechercher un point de vente',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_loadPointsDeVente();
},
)
: null,
),
),
),
// En-tête de liste
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
const Expanded(
flex: 2,
child: Text(
'Nom',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
),
const Expanded(
child: Text(
'Code',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
),
SizedBox(
width: 40,
child: Text(
'Actions',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
),
],
),
),
// Liste
Expanded(
child: _isLoading && _pointsDeVente.isEmpty
? const Center(child: CircularProgressIndicator())
: _pointsDeVente.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.store_mall_directory_outlined,
size: 60, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucun point de vente trouvé',
style: TextStyle(
fontSize: 16,
color: Colors.grey),
),
],
),
)
: ListView.builder(
itemCount: _pointsDeVente.length,
itemBuilder: (context, index) {
final point = _pointsDeVente[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
flex: 2,
child: Text(
point['nom'] ?? 'N/A',
style: const TextStyle(
fontWeight: FontWeight.w500),
),
),
Expanded(
child: Text(
point['code'] ?? 'N/A',
style: TextStyle(
color: Colors.grey.shade600),
),
),
SizedBox(
width: 40,
child: IconButton(
icon: const Icon(Icons.delete,
size: 20, color: Colors.red),
onPressed: () => _deletePointDeVente(point['id']),
),
),
],
),
),
);
},
),
),
],
),
),
),
],
),
);
}
@override
void dispose() {
_nomController.dispose();
_codeController.dispose();
_searchController.dispose();
super.dispose();
}
}

3
lib/Views/historique.dart

@ -4,7 +4,6 @@ import 'package:intl/intl.dart';
import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class HistoriquePage extends StatefulWidget { class HistoriquePage extends StatefulWidget {
@ -612,7 +611,7 @@ class _HistoriquePageState extends State<HistoriquePage> {
), ),
), ),
onPressed: () => _updateStatutCommande(commande.id!), onPressed: () => _updateStatutCommande(commande.id!),
child: const Text('Marquer comme livrée'), child: const Text('Marquer comme confirmé'),
), ),
), ),
], ],

613
lib/Views/mobilepage.dart

@ -132,6 +132,13 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
List<Users> _commercialUsers = []; List<Users> _commercialUsers = [];
Users? _selectedCommercialUser; Users? _selectedCommercialUser;
// Variables pour les suggestions clients
List<Client> _clientSuggestions = [];
bool _showNomSuggestions = false;
bool _showTelephoneSuggestions = false;
GlobalKey _nomFieldKey = GlobalKey();
GlobalKey _telephoneFieldKey = GlobalKey();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -142,6 +149,123 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
_searchNameController.addListener(_filterProducts); _searchNameController.addListener(_filterProducts);
_searchImeiController.addListener(_filterProducts); _searchImeiController.addListener(_filterProducts);
_searchReferenceController.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<void> _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<void> _loadProducts() async { Future<void> _loadProducts() async {
@ -391,34 +515,10 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
return Scaffold( return Scaffold(
floatingActionButton: _buildFloatingCartButton(), floatingActionButton: _buildFloatingCartButton(),
drawer: isMobile ? CustomDrawer() : null, drawer: isMobile ? CustomDrawer() : null,
body: Column( body: GestureDetector(
onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs
child: Column(
children: [ 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),
),
),
),
),
// Section des filtres - adaptée comme dans HistoriquePage // Section des filtres - adaptée comme dans HistoriquePage
if (!isMobile) if (!isMobile)
Padding( Padding(
@ -484,9 +584,69 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
], ],
), ),
),
); );
} }
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() { Widget _buildFloatingCartButton() {
final isMobile = MediaQuery.of(context).size.width < 600; final isMobile = MediaQuery.of(context).size.width < 600;
final cartItemCount = _quantites.values.where((q) => q > 0).length; final cartItemCount = _quantites.values.where((q) => q > 0).length;
@ -508,7 +668,24 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
void _showClientFormDialog() { void _showClientFormDialog() {
final isMobile = MediaQuery.of(context).size.width < 600; 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<Client> localClientSuggestions = [];
// GlobalKeys pour positionner les overlays
final GlobalKey nomFieldKey = GlobalKey();
final GlobalKey prenomFieldKey = GlobalKey();
final GlobalKey emailFieldKey = GlobalKey();
final GlobalKey telephoneFieldKey = GlobalKey();
Get.dialog( Get.dialog(
StatefulBuilder(
builder: (context, setDialogState) {
return Stack(
children: [
AlertDialog( AlertDialog(
title: Row( title: Row(
children: [ children: [
@ -541,19 +718,63 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildTextFormField( // Champ Nom avec suggestions (SANS bouton recherche)
_buildTextFormFieldWithKey(
key: nomFieldKey,
controller: _nomController, controller: _nomController,
label: 'Nom', label: 'Nom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null, 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), const SizedBox(height: 12),
_buildTextFormField(
// Champ Prénom avec suggestions (SANS bouton recherche)
_buildTextFormFieldWithKey(
key: prenomFieldKey,
controller: _prenomController, controller: _prenomController,
label: 'Prénom', label: 'Prénom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null, 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), const SizedBox(height: 12),
_buildTextFormField(
// Champ Email avec suggestions (SANS bouton recherche)
_buildTextFormFieldWithKey(
key: emailFieldKey,
controller: _emailController, controller: _emailController,
label: 'Email', label: 'Email',
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
@ -564,20 +785,60 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
} }
return null; 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), const SizedBox(height: 12),
_buildTextFormField(
// Champ Téléphone avec suggestions (SANS bouton recherche)
_buildTextFormFieldWithKey(
key: telephoneFieldKey,
controller: _telephoneController, controller: _telephoneController,
label: 'Téléphone', label: 'Téléphone',
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null, 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), const SizedBox(height: 12),
_buildTextFormField( _buildTextFormField(
controller: _adresseController, controller: _adresseController,
label: 'Adresse', label: 'Adresse',
maxLines: 2, maxLines: 2,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null, validator: (value) => value?.isEmpty ?? true
? 'Veuillez entrer une adresse' : null,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildCommercialDropdown(), _buildCommercialDropdown(),
@ -602,6 +863,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
// Fermer toutes les suggestions avant de soumettre
setDialogState(() {
showNomSuggestions = false;
showPrenomSuggestions = false;
showEmailSuggestions = false;
showTelephoneSuggestions = false;
localClientSuggestions = [];
});
Get.back(); Get.back();
_submitOrder(); _submitOrder();
} }
@ -613,8 +882,252 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
], ],
), ),
// 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<Client> 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({ Widget _buildTextFormField({
required TextEditingController controller, required TextEditingController controller,
@ -622,6 +1135,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
TextInputType? keyboardType, TextInputType? keyboardType,
String? Function(String?)? validator, String? Function(String?)? validator,
int? maxLines, int? maxLines,
void Function(String)? onChanged,
}) { }) {
return TextFormField( return TextFormField(
controller: controller, controller: controller,
@ -629,19 +1143,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
labelText: label, labelText: label,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade400),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade400),
), ),
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
), ),
keyboardType: keyboardType, keyboardType: keyboardType,
validator: validator, validator: validator,
maxLines: maxLines, maxLines: maxLines,
onChanged: onChanged,
); );
} }
@ -1137,11 +1646,15 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
try { try {
await _appDatabase.createCommandeComplete(client, commande, details); await _appDatabase.createCommandeComplete(client, commande, details);
// Fermer le panier avant d'afficher la confirmation
Get.back();
// Afficher le dialogue de confirmation - adapté pour mobile // Afficher le dialogue de confirmation - adapté pour mobile
final isMobile = MediaQuery.of(context).size.width < 600; final isMobile = MediaQuery.of(context).size.width < 600;
await showDialog( await showDialog(
context: context, context: context,
barrierDismissible: false, // Empêcher la fermeture accidentelle
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Row( title: Row(
children: [ children: [
@ -1182,16 +1695,8 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
// Réinitialiser le formulaire // Vider complètement le formulaire et le panier
_nomController.clear(); _clearFormAndCart();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_adresseController.clear();
setState(() {
_quantites.clear();
_isLoading = false;
});
// Recharger les produits pour mettre à jour le stock // Recharger les produits pour mettre à jour le stock
_loadProducts(); _loadProducts();
}, },
@ -1222,6 +1727,10 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
@override @override
void dispose() { void dispose() {
// Nettoyer les suggestions
_hideAllSuggestions();
// Disposer les contrôleurs
_nomController.dispose(); _nomController.dispose();
_prenomController.dispose(); _prenomController.dispose();
_emailController.dispose(); _emailController.dispose();

950
lib/Views/newCommand.dart

File diff suppressed because it is too large

190
lib/Views/pointage.dart

@ -1,190 +0,0 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Services/pointageDatabase.dart';
import 'package:youmazgestion/Models/pointage_model.dart';
class PointagePage extends StatefulWidget {
const PointagePage({Key? key}) : super(key: key);
@override
State<PointagePage> createState() => _PointagePageState();
}
class _PointagePageState extends State<PointagePage> {
final DatabaseHelper _databaseHelper = DatabaseHelper();
List<Pointage> _pointages = [];
@override
void initState() {
super.initState();
_loadPointages();
}
Future<void> _loadPointages() async {
final pointages = await _databaseHelper.getPointages();
setState(() {
_pointages = pointages;
});
}
Future<void> _showAddDialog() async {
final _arrivalController = TextEditingController();
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Ajouter Pointage'),
content: TextField(
controller: _arrivalController,
decoration: InputDecoration(
labelText: 'Heure d\'arrivée (HH:mm)',
),
),
actions: [
TextButton(
child: Text('Annuler'),
onPressed: () => Navigator.of(context).pop(),
),
ElevatedButton(
child: Text('Ajouter'),
onPressed: () async {
final pointage = Pointage(
userName:
"Nom de l'utilisateur", // fixed value, customize if needed
date: DateTime.now().toString().split(' ')[0],
heureArrivee: _arrivalController.text,
heureDepart: '',
);
await _databaseHelper.insertPointage(pointage);
Navigator.of(context).pop();
_loadPointages();
},
),
],
);
},
);
}
void _scanQRCode({required bool isEntree}) {
// Ici tu peux intégrer ton scanner QR.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isEntree ? "Scan QR pour Entrée" : "Scan QR pour Sortie"),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pointage'),
),
body: _pointages.isEmpty
? Center(child: Text('Aucun pointage enregistré.'))
: ListView.builder(
itemCount: _pointages.length,
itemBuilder: (context, index) {
final pointage = _pointages[index];
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 6.0),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.blueGrey.shade100),
),
elevation: 4,
shadowColor: Colors.blueGrey.shade50,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: Colors.blue.shade100,
child: Icon(Icons.person, color: Colors.blue),
),
const SizedBox(width: 10),
Expanded(
child: Text(
pointage
.userName, // suppose non-null (corrige si null possible)
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18),
),
),
],
),
Divider(),
Text(
pointage.date,
style: const TextStyle(
color: Colors.black87, fontSize: 15),
),
Row(
children: [
Icon(Icons.login,
size: 18, color: Colors.green.shade700),
const SizedBox(width: 6),
Text("Arrivée : ${pointage.heureArrivee}",
style:
TextStyle(color: Colors.green.shade700)),
],
),
Row(
children: [
Icon(Icons.logout,
size: 18, color: Colors.red.shade700),
const SizedBox(width: 6),
Text(
"Départ : ${pointage.heureDepart.isNotEmpty ? pointage.heureDepart : "---"}",
style: TextStyle(color: Colors.red.shade700)),
],
),
const SizedBox(height: 6),
],
),
),
),
);
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FloatingActionButton.extended(
onPressed: () => _scanQRCode(isEntree: true),
label: Text('Entrée'),
icon: Icon(Icons.qr_code_scanner, color: Colors.green),
backgroundColor: Colors.white,
foregroundColor: Colors.green,
heroTag: 'btnEntree',
),
SizedBox(height: 12),
FloatingActionButton.extended(
onPressed: () => _scanQRCode(isEntree: false),
label: Text('Sortie'),
icon: Icon(Icons.qr_code_scanner, color: Colors.red),
backgroundColor: Colors.white,
foregroundColor: Colors.red,
heroTag: 'btnSortie',
),
SizedBox(height: 12),
FloatingActionButton(
onPressed: _showAddDialog,
tooltip: 'Ajouter Pointage',
child: const Icon(Icons.add),
heroTag: 'btnAdd',
),
],
),
);
}
}

64
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<String, dynamic> getConfig() {
if (isDevelopment) {
return {
'host': host,
'port': port,
'user': username,
'password': password,
'database': database,
'timeout': connectionTimeout.inSeconds,
};
} else {
return {
'host': prodHost,
'port': port,
'user': prodUsername,
'password': prodPassword,
'database': prodDatabase,
'timeout': connectionTimeout.inSeconds,
};
}
}
// Validation de la configuration
static bool validateConfig() {
try {
final config = getConfig();
return config['host']?.toString().isNotEmpty == true &&
config['database']?.toString().isNotEmpty == true &&
config['user'] != null;
} catch (e) {
print("Erreur de validation de la configuration: $e");
return false;
}
}
// Configuration avec retry automatique
static Map<String, dynamic> getConfigWithRetry() {
final config = getConfig();
config['retryCount'] = 3;
config['retryDelay'] = 5000; // ms
return config;
}
}

108
lib/main.dart

@ -1,9 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
//import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/controller/userController.dart';
//import 'Services/productDatabase.dart';
import 'my_app.dart'; import 'my_app.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -11,31 +9,117 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
try { try {
// Initialiser les bases de données une seule fois print("Initialisation de l'application...");
// 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 AppDatabase.instance.deleteDatabaseFile();
// await ProductDatabase.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(); await AppDatabase.instance.initDatabase();
print("Base de données initialisée avec succès !");
// Afficher les informations de la base (pour debug) // Afficher les informations de la base (pour debug)
// await AppDatabase.instance.printDatabaseInfo(); await AppDatabase.instance.printDatabaseInfo();
Get.put(
UserController()); // Ajoute ce code AVANT tout accès au UserController // Initialiser le contrôleur utilisateur
Get.put(UserController());
print("Contrôleur utilisateur initialisé");
// Configurer le logger
setupLogger(); setupLogger();
print("Lancement de l'application...");
runApp(const GetMaterialApp( runApp(const GetMaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: MyApp(), home: MyApp(),
)); ));
} catch (e) { } catch (e) {
print('Erreur lors de l\'initialisation: $e'); print('Erreur lors de l\'initialisation: $e');
// Vous pourriez vouloir afficher une page d'erreur ici
// Afficher une page d'erreur avec plus de détails
runApp(MaterialApp( runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold( home: Scaffold(
body: Center( backgroundColor: Colors.red[50],
child: Text('Erreur d\'initialisation: $e'), appBar: AppBar(
title: const Text('Erreur d\'initialisation'),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 48,
),
const SizedBox(height: 16),
const Text(
'Erreur de connexion à la base de données',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
const SizedBox(height: 16),
const Text(
'Vérifiez que :',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('• XAMPP est démarré'),
const Text('• MySQL est en cours d\'exécution'),
const Text('• La base de données "guycom_databse" existe'),
const Text('• Les paramètres de connexion sont corrects'),
const SizedBox(height: 16),
const Text(
'Détails de l\'erreur :',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey),
),
child: SingleChildScrollView(
child: Text(
e.toString(),
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// Relancer l'application
main();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: const Text('Réessayer'),
),
),
],
),
), ),
), ),
)); ));

2
macos/Flutter/GeneratedPluginRegistrant.swift

@ -11,7 +11,6 @@ import mobile_scanner
import open_file_mac import open_file_mac
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@ -21,6 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

48
pubspec.lock

@ -632,6 +632,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.7.0" version: "3.7.0"
mysql1:
dependency: "direct main"
description:
name: mysql1
sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9"
url: "https://pub.dev"
source: hosted
version: "0.20.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -832,6 +840,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -981,22 +997,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
@ -1013,22 +1013,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.3.5"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3: sqlite3:
dependency: transitive dependency: transitive
description: description:

3
pubspec.yaml

@ -35,7 +35,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
get: ^4.6.5 get: ^4.6.5
sqflite: ^2.2.8+4 # sqflite: ^2.2.8+4
mysql1: ^0.20.0
flutter_dropzone: ^4.2.1 flutter_dropzone: ^4.2.1
image_picker: ^0.8.7+5 image_picker: ^0.8.7+5

Loading…
Cancel
Save