migration mysql
This commit is contained in:
parent
831cce13da
commit
b5a11aa3c9
417
lib/Components/AddClient.dart
Normal file
417
lib/Components/AddClient.dart
Normal file
@ -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
Normal file
471
lib/Components/AddClientForm.dart
Normal file
@ -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
Normal file
176
lib/Components/DiscountDialog.dart
Normal file
@ -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
Normal file
349
lib/Components/GiftaselectedButton.dart
Normal file
@ -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
Normal file
338
lib/Components/PaymentEnchainedDialog.dart
Normal file
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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
Normal file
64
lib/Models/Remise.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:youmazgestion/Components/paymentType.dart';
|
||||||
|
import 'package:youmazgestion/Models/produit.dart';
|
||||||
|
|
||||||
|
class Remise {
|
||||||
|
final RemiseType type;
|
||||||
|
final double valeur;
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
Remise({
|
||||||
|
required this.type,
|
||||||
|
required this.valeur,
|
||||||
|
this.description = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
double calculerRemise(double montantOriginal) {
|
||||||
|
switch (type) {
|
||||||
|
case RemiseType.pourcentage:
|
||||||
|
return montantOriginal * (valeur / 100);
|
||||||
|
case RemiseType.fixe:
|
||||||
|
return valeur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get libelle {
|
||||||
|
switch (type) {
|
||||||
|
case RemiseType.pourcentage:
|
||||||
|
return '$valeur%';
|
||||||
|
case RemiseType.fixe:
|
||||||
|
return '${valeur.toStringAsFixed(0)} MGA';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RemiseType { pourcentage, fixe }
|
||||||
|
|
||||||
|
class ProduitCadeau {
|
||||||
|
final Product produit;
|
||||||
|
final String motif;
|
||||||
|
|
||||||
|
ProduitCadeau({
|
||||||
|
required this.produit,
|
||||||
|
this.motif = 'Cadeau client',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifiez votre classe PaymentMethod pour inclure la remise
|
||||||
|
class PaymentMethodEnhanced {
|
||||||
|
final PaymentType type;
|
||||||
|
final double amountGiven;
|
||||||
|
final Remise? remise;
|
||||||
|
|
||||||
|
PaymentMethodEnhanced({
|
||||||
|
required this.type,
|
||||||
|
this.amountGiven = 0,
|
||||||
|
this.remise,
|
||||||
|
});
|
||||||
|
|
||||||
|
double calculerMontantFinal(double montantOriginal) {
|
||||||
|
if (remise != null) {
|
||||||
|
return montantOriginal - remise!.calculerRemise(montantOriginal);
|
||||||
|
}
|
||||||
|
return montantOriginal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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 ?? '';
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:sqflite/sqflite.dart';
|
|
||||||
import '../Models/pointage_model.dart';
|
|
||||||
|
|
||||||
class DatabaseHelper {
|
|
||||||
static final DatabaseHelper _instance = DatabaseHelper._internal();
|
|
||||||
|
|
||||||
factory DatabaseHelper() => _instance;
|
|
||||||
|
|
||||||
DatabaseHelper._internal();
|
|
||||||
|
|
||||||
Database? _db;
|
|
||||||
|
|
||||||
Future<Database> get database async {
|
|
||||||
if (_db != null) return _db!;
|
|
||||||
_db = await _initDatabase();
|
|
||||||
return _db!;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Database> _initDatabase() async {
|
|
||||||
String databasesPath = await getDatabasesPath();
|
|
||||||
String dbPath = join(databasesPath, 'pointage.db');
|
|
||||||
return await openDatabase(dbPath, version: 1, onCreate: _onCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _onCreate(Database db, int version) async {
|
|
||||||
await db.execute('''
|
|
||||||
CREATE TABLE pointages (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
userName TEXT NOT NULL,
|
|
||||||
date TEXT NOT NULL,
|
|
||||||
heureArrivee TEXT NOT NULL,
|
|
||||||
heureDepart TEXT NOT NULL
|
|
||||||
)
|
|
||||||
''');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> insertPointage(Pointage pointage) async {
|
|
||||||
final db = await database;
|
|
||||||
return await db.insert('pointages', pointage.toMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Pointage>> getPointages() async {
|
|
||||||
final db = await database;
|
|
||||||
final pointages = await db.query('pointages');
|
|
||||||
return pointages.map((pointage) => Pointage.fromMap(pointage)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> updatePointage(Pointage pointage) async {
|
|
||||||
final db = await database;
|
|
||||||
return await db.update('pointages', pointage.toMap(),
|
|
||||||
where: 'id = ?', whereArgs: [pointage.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> deletePointage(int id) async {
|
|
||||||
final db = await database;
|
|
||||||
return await db.delete('pointages', where: 'id = ?', whereArgs: [id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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(
|
||||||
content: Container(
|
|
||||||
width: 500,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.edit, color: Colors.orange.shade700),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text('Modifier le produit'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Container(
|
||||||
|
width: 600,
|
||||||
|
constraints: const BoxConstraints(maxHeight: 600),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) {
|
||||||
|
// Charger les points de vente une seule fois
|
||||||
|
if (isLoadingPoints && pointsDeVente.isEmpty) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
loadPointsDeVente(setDialogState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Champs obligatoires
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.orange.shade200),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info, color: Colors.orange.shade600, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
'Les champs marqués d\'un * sont obligatoires',
|
||||||
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 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(
|
TextField(
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Nom du produit*',
|
labelText: 'Nom du produit *',
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.shopping_bag),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade50,
|
||||||
),
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() {
|
||||||
|
updateQrPreview();
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
TextField(
|
// Prix et Stock sur la même ligne
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
controller: priceController,
|
controller: priceController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Prix*',
|
labelText: 'Prix (MGA) *',
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.attach_money),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade50,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
TextField(
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
controller: stockController,
|
controller: stockController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Stock',
|
labelText: 'Stock',
|
||||||
border: OutlineInputBorder(),
|
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),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
StatefulBuilder(
|
// Description
|
||||||
builder: (context, setDialogState) {
|
TextField(
|
||||||
return Column(
|
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: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.confirmation_number, color: Colors.purple.shade700),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Référence du produit',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.purple.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: referenceController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Référence',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.tag),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
helperText: 'La référence peut être modifiée avec précaution',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() {
|
||||||
|
updateQrPreview();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Spécifications techniques
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.orange.shade200),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.memory, color: Colors.orange.shade700),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Spécifications techniques',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: marqueController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Marque',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.branding_watermark),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: ramController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'RAM',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.memory),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: memoireInterneController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Mémoire interne',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.storage),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: imeiController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'IMEI (pour téléphones)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.smartphone),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Section Image
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.blue.shade200),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.image, color: Colors.blue.shade700),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Image du produit',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
Row(
|
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,43 +2854,74 @@ 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Afficher un message de confirmation
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
enabled
|
||||||
|
? 'Permission "$permission" accordée'
|
||||||
|
: 'Permission "$permission" révoquée',
|
||||||
|
),
|
||||||
|
backgroundColor: enabled ? Colors.green : Colors.orange,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de la modification de la permission: $e");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur lors de la modification: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _toggleAllPermissions(int menuId, bool enabled) {
|
||||||
Widget build(BuildContext context) {
|
for (var permission in permissions) {
|
||||||
return Scaffold(
|
_onPermissionToggle(menuId, permission.name, enabled);
|
||||||
appBar: CustomAppBar(
|
}
|
||||||
title: "Permissions - ${widget.role.designation}",
|
}
|
||||||
// showBackButton: true,
|
|
||||||
),
|
int _getSelectedPermissionsCount(int menuId) {
|
||||||
body: Padding(
|
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: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold, fontSize: 16),
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
|
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(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
: Padding(
|
||||||
const Expanded(
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Center(
|
child: Column(
|
||||||
child: CircularProgressIndicator(),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// En-tête avec informations du rôle
|
||||||
|
Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: widget.role.designation == 'Super Admin'
|
||||||
|
? Colors.red.shade100
|
||||||
|
: Colors.blue.shade100,
|
||||||
|
radius: 24,
|
||||||
|
child: Icon(
|
||||||
|
Icons.person,
|
||||||
|
color: widget.role.designation == 'Super Admin'
|
||||||
|
? Colors.red.shade700
|
||||||
|
: Colors.blue.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Gestion des permissions',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Rôle: ${widget.role.designation}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Configurez les accès pour chaque menu',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Résumé des permissions
|
||||||
|
if (permissions.isNotEmpty && menus.isNotEmpty)
|
||||||
|
_buildPermissionSummary(),
|
||||||
|
|
||||||
|
// Liste des menus et permissions
|
||||||
|
if (permissions.isNotEmpty && menus.isNotEmpty)
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: menus.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildMenuCard(menus[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.inbox,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Aucune donnée disponible',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Permissions: ${permissions.length} | Menus: ${menus.length}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _initData,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Actualiser'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: !isLoading && errorMessage == null
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
label: const Text('Enregistrer'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilterChip extends StatelessWidget {
|
class CustomFilterChip extends StatelessWidget {
|
||||||
final String label;
|
final 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||||
),
|
),
|
||||||
DataTable(
|
),
|
||||||
|
Text(
|
||||||
|
menuRoute,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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'
|
||||||
}).toList(),
|
? 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(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
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
Normal file
416
lib/Views/gestion_point_de_vente.dart
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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é'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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
Normal file
64
lib/config/DatabaseConfig.dart
Normal file
@ -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
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'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|||||||
@ -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
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:
|
||||||
|
|||||||
@ -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…
Reference in New Issue
Block a user