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/accueil.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
import 'package:youmazgestion/Views/pointage.dart';
|
||||
import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; // Nouvel import
|
||||
|
||||
class CustomDrawer extends StatelessWidget {
|
||||
final UserController userController = Get.find<UserController>();
|
||||
@ -106,7 +106,7 @@ class CustomDrawer extends StatelessWidget {
|
||||
color: Colors.blue,
|
||||
permissionAction: 'view',
|
||||
permissionRoute: '/accueil',
|
||||
onTap: () => Get.to( DashboardPage()),
|
||||
onTap: () => Get.to(DashboardPage()),
|
||||
),
|
||||
);
|
||||
|
||||
@ -133,7 +133,7 @@ class CustomDrawer extends StatelessWidget {
|
||||
color: const Color.fromARGB(255, 4, 54, 95),
|
||||
permissionAction: 'update',
|
||||
permissionRoute: '/pointage',
|
||||
onTap: () => Get.to(const PointagePage()),
|
||||
onTap: () => {},
|
||||
)
|
||||
];
|
||||
|
||||
@ -233,7 +233,7 @@ class CustomDrawer extends StatelessWidget {
|
||||
color: Colors.teal,
|
||||
permissionAction: 'read',
|
||||
permissionRoute: '/bilan',
|
||||
onTap: () => Get.to( DashboardPage()),
|
||||
onTap: () => Get.to(DashboardPage()),
|
||||
),
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.history,
|
||||
@ -241,7 +241,7 @@ class CustomDrawer extends StatelessWidget {
|
||||
color: Colors.blue,
|
||||
permissionAction: 'read',
|
||||
permissionRoute: '/historique',
|
||||
onTap: () => Get.to(HistoryPage()),
|
||||
onTap: () => Get.to(const HistoriquePage()),
|
||||
),
|
||||
];
|
||||
|
||||
@ -271,6 +271,14 @@ class CustomDrawer extends StatelessWidget {
|
||||
permissionRoute: '/gerer-roles',
|
||||
onTap: () => Get.to(const RoleListPage()),
|
||||
),
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.store,
|
||||
title: "Points de vente",
|
||||
color: Colors.blueGrey,
|
||||
permissionAction: 'admin',
|
||||
permissionRoute: '/points-de-vente',
|
||||
onTap: () => Get.to(const AjoutPointDeVentePage()),
|
||||
),
|
||||
];
|
||||
|
||||
if (administrationItems.any((item) => item is ListTile)) {
|
||||
@ -292,7 +300,6 @@ class CustomDrawer extends StatelessWidget {
|
||||
|
||||
drawerItems.add(const Divider());
|
||||
|
||||
|
||||
drawerItems.add(
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout, color: Colors.red),
|
||||
@ -414,7 +421,7 @@ class CustomDrawer extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
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 {
|
||||
final int? id;
|
||||
final String nom;
|
||||
@ -33,16 +33,40 @@ class Client {
|
||||
};
|
||||
}
|
||||
|
||||
// Fonction helper améliorée pour parser les dates
|
||||
static DateTime _parseDateTime(dynamic dateValue) {
|
||||
if (dateValue == null) return DateTime.now();
|
||||
|
||||
if (dateValue is DateTime) return dateValue;
|
||||
|
||||
if (dateValue is String) {
|
||||
try {
|
||||
return DateTime.parse(dateValue);
|
||||
} catch (e) {
|
||||
print("Erreur parsing date string: $dateValue, erreur: $e");
|
||||
return DateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
// Pour MySQL qui peut retourner un Timestamp
|
||||
if (dateValue is int) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(dateValue);
|
||||
}
|
||||
|
||||
print("Type de date non reconnu: ${dateValue.runtimeType}, valeur: $dateValue");
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
factory Client.fromMap(Map<String, dynamic> map) {
|
||||
return Client(
|
||||
id: map['id'],
|
||||
nom: map['nom'],
|
||||
prenom: map['prenom'],
|
||||
email: map['email'],
|
||||
telephone: map['telephone'],
|
||||
adresse: map['adresse'],
|
||||
dateCreation: DateTime.parse(map['dateCreation']),
|
||||
actif: map['actif'] == 1,
|
||||
id: map['id'] as int?,
|
||||
nom: map['nom'] as String,
|
||||
prenom: map['prenom'] as String,
|
||||
email: map['email'] as String,
|
||||
telephone: map['telephone'] as String,
|
||||
adresse: map['adresse'] as String?,
|
||||
dateCreation: _parseDateTime(map['dateCreation']),
|
||||
actif: (map['actif'] as int?) == 1,
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,17 +89,18 @@ class Commande {
|
||||
final DateTime? dateLivraison;
|
||||
final int? commandeurId;
|
||||
final int? validateurId;
|
||||
|
||||
// Données du client (pour les jointures)
|
||||
final String? clientNom;
|
||||
final String? clientPrenom;
|
||||
final String? clientEmail;
|
||||
final double? remisePourcentage;
|
||||
final double? remiseMontant;
|
||||
final double? montantApresRemise;
|
||||
|
||||
Commande({
|
||||
this.id,
|
||||
required this.clientId,
|
||||
required this.dateCommande,
|
||||
this.statut = StatutCommande.enAttente,
|
||||
required this.statut,
|
||||
required this.montantTotal,
|
||||
this.notes,
|
||||
this.dateLivraison,
|
||||
@ -84,8 +109,29 @@ class Commande {
|
||||
this.clientNom,
|
||||
this.clientPrenom,
|
||||
this.clientEmail,
|
||||
this.remisePourcentage,
|
||||
this.remiseMontant,
|
||||
this.montantApresRemise,
|
||||
});
|
||||
|
||||
String get clientNomComplet {
|
||||
if (clientNom != null && clientPrenom != null) {
|
||||
return '$clientPrenom $clientNom';
|
||||
}
|
||||
return 'Client inconnu';
|
||||
}
|
||||
|
||||
String get statutLibelle {
|
||||
switch (statut) {
|
||||
case StatutCommande.enAttente:
|
||||
return 'En attente';
|
||||
case StatutCommande.confirmee:
|
||||
return 'Confirmée';
|
||||
case StatutCommande.annulee:
|
||||
return 'Annulée';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
@ -97,55 +143,77 @@ class Commande {
|
||||
'dateLivraison': dateLivraison?.toIso8601String(),
|
||||
'commandeurId': commandeurId,
|
||||
'validateurId': validateurId,
|
||||
'remisePourcentage': remisePourcentage,
|
||||
'remiseMontant': remiseMontant,
|
||||
'montantApresRemise': montantApresRemise,
|
||||
};
|
||||
}
|
||||
|
||||
factory Commande.fromMap(Map<String, dynamic> map) {
|
||||
return Commande(
|
||||
id: map['id'],
|
||||
clientId: map['clientId'],
|
||||
dateCommande: DateTime.parse(map['dateCommande']),
|
||||
statut: StatutCommande.values[map['statut']],
|
||||
montantTotal: map['montantTotal'].toDouble(),
|
||||
notes: map['notes'],
|
||||
id: map['id'] as int?,
|
||||
clientId: map['clientId'] as int,
|
||||
dateCommande: Client._parseDateTime(map['dateCommande']),
|
||||
statut: StatutCommande.values[(map['statut'] as int)],
|
||||
montantTotal: (map['montantTotal'] as num).toDouble(),
|
||||
notes: map['notes'] as String?,
|
||||
dateLivraison: map['dateLivraison'] != null
|
||||
? 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,
|
||||
commandeurId: map['commandeurId'],
|
||||
validateurId: map['validateurId'],
|
||||
clientNom: map['clientNom'],
|
||||
clientPrenom: map['clientPrenom'],
|
||||
clientEmail: map['clientEmail'],
|
||||
);
|
||||
}
|
||||
|
||||
String get statutLibelle {
|
||||
switch (statut) {
|
||||
case StatutCommande.enAttente:
|
||||
return 'En attente';
|
||||
case StatutCommande.confirmee:
|
||||
return 'Confirmée';
|
||||
// case StatutCommande.enPreparation:
|
||||
// return 'En préparation';
|
||||
// case StatutCommande.expediee:
|
||||
// return 'Expédiée';
|
||||
// case StatutCommande.livree:
|
||||
// return 'Livrée';
|
||||
case StatutCommande.annulee:
|
||||
return 'Annulée';
|
||||
default:
|
||||
return 'Inconnu';
|
||||
Commande copyWith({
|
||||
int? id,
|
||||
int? clientId,
|
||||
DateTime? dateCommande,
|
||||
StatutCommande? statut,
|
||||
double? montantTotal,
|
||||
String? notes,
|
||||
DateTime? dateLivraison,
|
||||
int? commandeurId,
|
||||
int? validateurId,
|
||||
String? clientNom,
|
||||
String? clientPrenom,
|
||||
String? clientEmail,
|
||||
double? remisePourcentage,
|
||||
double? remiseMontant,
|
||||
double? montantApresRemise,
|
||||
}) {
|
||||
return Commande(
|
||||
id: id ?? this.id,
|
||||
clientId: clientId ?? this.clientId,
|
||||
dateCommande: dateCommande ?? this.dateCommande,
|
||||
statut: statut ?? this.statut,
|
||||
montantTotal: montantTotal ?? this.montantTotal,
|
||||
notes: notes ?? this.notes,
|
||||
dateLivraison: dateLivraison ?? this.dateLivraison,
|
||||
commandeurId: commandeurId ?? this.commandeurId,
|
||||
validateurId: validateurId ?? this.validateurId,
|
||||
clientNom: clientNom ?? this.clientNom,
|
||||
clientPrenom: clientPrenom ?? this.clientPrenom,
|
||||
clientEmail: clientEmail ?? this.clientEmail,
|
||||
remisePourcentage: remisePourcentage ?? this.remisePourcentage,
|
||||
remiseMontant: remiseMontant ?? this.remiseMontant,
|
||||
montantApresRemise: montantApresRemise ?? this.montantApresRemise,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String get clientNomComplet =>
|
||||
clientPrenom != null && clientNom != null
|
||||
? '$clientPrenom $clientNom'
|
||||
: 'Client inconnu';
|
||||
}
|
||||
|
||||
|
||||
// Models/detail_commande.dart
|
||||
class DetailCommande {
|
||||
final int? id;
|
||||
final int commandeId;
|
||||
@ -153,11 +221,10 @@ class DetailCommande {
|
||||
final int quantite;
|
||||
final double prixUnitaire;
|
||||
final double sousTotal;
|
||||
|
||||
// Données du produit (pour les jointures)
|
||||
final String? produitNom;
|
||||
final String? produitImage;
|
||||
final String? produitReference;
|
||||
final bool? estCadeau;
|
||||
|
||||
DetailCommande({
|
||||
this.id,
|
||||
@ -169,6 +236,7 @@ class DetailCommande {
|
||||
this.produitNom,
|
||||
this.produitImage,
|
||||
this.produitReference,
|
||||
this.estCadeau,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
@ -179,20 +247,48 @@ class DetailCommande {
|
||||
'quantite': quantite,
|
||||
'prixUnitaire': prixUnitaire,
|
||||
'sousTotal': sousTotal,
|
||||
'estCadeau': estCadeau == true ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
factory DetailCommande.fromMap(Map<String, dynamic> map) {
|
||||
return DetailCommande(
|
||||
id: map['id'],
|
||||
commandeId: map['commandeId'],
|
||||
produitId: map['produitId'],
|
||||
quantite: map['quantite'],
|
||||
prixUnitaire: map['prixUnitaire'].toDouble(),
|
||||
sousTotal: map['sousTotal'].toDouble(),
|
||||
produitNom: map['produitNom'],
|
||||
produitImage: map['produitImage'],
|
||||
produitReference: map['produitReference'],
|
||||
id: map['id'] as int?,
|
||||
commandeId: map['commandeId'] as int,
|
||||
produitId: map['produitId'] as int,
|
||||
quantite: map['quantite'] as int,
|
||||
prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
|
||||
sousTotal: (map['sousTotal'] as num).toDouble(),
|
||||
produitNom: map['produitNom'] as String?,
|
||||
produitImage: map['produitImage'] as String?,
|
||||
produitReference: map['produitReference'] as String?,
|
||||
estCadeau: map['estCadeau'] == 1,
|
||||
);
|
||||
}
|
||||
|
||||
DetailCommande copyWith({
|
||||
int? id,
|
||||
int? commandeId,
|
||||
int? produitId,
|
||||
int? quantite,
|
||||
double? prixUnitaire,
|
||||
double? sousTotal,
|
||||
String? produitNom,
|
||||
String? produitImage,
|
||||
String? produitReference,
|
||||
bool? estCadeau,
|
||||
}) {
|
||||
return DetailCommande(
|
||||
id: id ?? this.id,
|
||||
commandeId: commandeId ?? this.commandeId,
|
||||
produitId: produitId ?? this.produitId,
|
||||
quantite: quantite ?? this.quantite,
|
||||
prixUnitaire: prixUnitaire ?? this.prixUnitaire,
|
||||
sousTotal: sousTotal ?? this.sousTotal,
|
||||
produitNom: produitNom ?? this.produitNom,
|
||||
produitImage: produitImage ?? this.produitImage,
|
||||
produitReference: produitReference ?? this.produitReference,
|
||||
estCadeau: estCadeau ?? this.estCadeau,
|
||||
);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
final int? id;
|
||||
final String name;
|
||||
@ -29,31 +33,59 @@ class Product {
|
||||
this.ram,
|
||||
this.memoireInterne,
|
||||
this.imei,
|
||||
|
||||
});
|
||||
|
||||
bool isStockDefined() {
|
||||
if (stock != null) {
|
||||
print("stock is defined : $stock $name");
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
return stock > 0;
|
||||
}
|
||||
|
||||
// Méthode helper pour convertir de façon sécurisée
|
||||
static String? _convertImageFromMap(dynamic imageValue) {
|
||||
if (imageValue == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Si c'est déjà une String, on la retourne
|
||||
if (imageValue is String) {
|
||||
return imageValue;
|
||||
}
|
||||
|
||||
// Le driver mysql1 peut retourner un Blob même pour TEXT
|
||||
// Essayer de le convertir en String
|
||||
try {
|
||||
if (imageValue is Uint8List) {
|
||||
// Convertir les bytes en String UTF-8
|
||||
return utf8.decode(imageValue);
|
||||
}
|
||||
|
||||
if (imageValue is List<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(
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
price: map['price'],
|
||||
image: map['image'],
|
||||
category: map['category'],
|
||||
stock: map['stock'],
|
||||
description: map['description'],
|
||||
qrCode: map['qrCode'],
|
||||
reference: map['reference'],
|
||||
pointDeVenteId: map['point_de_vente_id'],
|
||||
marque: map['marque'],
|
||||
ram: map['ram'],
|
||||
memoireInterne: map['memoire_interne'],
|
||||
imei: map['imei'],
|
||||
id: map['id'] as int?,
|
||||
name: map['name'] as String,
|
||||
price: (map['price'] as num).toDouble(), // Conversion sécurisée
|
||||
image: _convertImageFromMap(map['image']), // Utilisation de la méthode helper
|
||||
category: map['category'] as String,
|
||||
stock: (map['stock'] as int?) ?? 0, // Valeur par défaut
|
||||
description: map['description'] as String?,
|
||||
qrCode: map['qrCode'] as String?,
|
||||
reference: map['reference'] as String?,
|
||||
pointDeVenteId: map['point_de_vente_id'] as int?,
|
||||
marque: map['marque'] as String?,
|
||||
ram: map['ram'] as String?,
|
||||
memoireInterne: map['memoire_interne'] as String?,
|
||||
imei: map['imei'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
@ -72,4 +104,59 @@ class Product {
|
||||
'memoire_interne': memoireInterne,
|
||||
'imei': imei,
|
||||
};
|
||||
|
||||
// Méthode pour obtenir l'image comme base64 si nécessaire
|
||||
String? getImageAsBase64() {
|
||||
if (image == null) return null;
|
||||
|
||||
// Si l'image est déjà en base64, la retourner
|
||||
if (image!.startsWith('data:') || image!.length > 100) {
|
||||
return image;
|
||||
}
|
||||
|
||||
// Sinon, c'est probablement un chemin de fichier
|
||||
return image;
|
||||
}
|
||||
|
||||
// Méthode pour vérifier si l'image est un base64
|
||||
bool get isImageBase64 {
|
||||
if (image == null) return false;
|
||||
return image!.startsWith('data:') ||
|
||||
(image!.length > 100 && !image!.contains('/') && !image!.contains('\\'));
|
||||
}
|
||||
|
||||
// Copie avec modification
|
||||
Product copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
double? price,
|
||||
String? image,
|
||||
String? category,
|
||||
int? stock,
|
||||
String? description,
|
||||
String? qrCode,
|
||||
String? reference,
|
||||
int? pointDeVenteId,
|
||||
String? marque,
|
||||
String? ram,
|
||||
String? memoireInterne,
|
||||
String? imei,
|
||||
}) {
|
||||
return Product(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
price: price ?? this.price,
|
||||
image: image ?? this.image,
|
||||
category: category ?? this.category,
|
||||
stock: stock ?? this.stock,
|
||||
description: description ?? this.description,
|
||||
qrCode: qrCode ?? this.qrCode,
|
||||
reference: reference ?? this.reference,
|
||||
pointDeVenteId: pointDeVenteId ?? this.pointDeVenteId,
|
||||
marque: marque ?? this.marque,
|
||||
ram: ram ?? this.ram,
|
||||
memoireInterne: memoireInterne ?? this.memoireInterne,
|
||||
imei: imei ?? this.imei,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
// Models/users.dart - Version corrigée
|
||||
class Users {
|
||||
int? id;
|
||||
String name;
|
||||
@ -6,7 +7,7 @@ class Users {
|
||||
String password;
|
||||
String username;
|
||||
int roleId;
|
||||
String? roleName; // Optionnel, rempli lors des requêtes avec JOIN
|
||||
String? roleName;
|
||||
int? pointDeVenteId;
|
||||
|
||||
Users({
|
||||
@ -24,12 +25,12 @@ class Users {
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'name': name,
|
||||
'lastname': lastName,
|
||||
'lastname': lastName, // Correspond à la colonne DB
|
||||
'email': email,
|
||||
'password': password,
|
||||
'username': username,
|
||||
'role_id': roleId,
|
||||
'point_de_vente_id' : pointDeVenteId,
|
||||
'point_de_vente_id': pointDeVenteId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,19 +42,17 @@ class Users {
|
||||
|
||||
factory Users.fromMap(Map<String, dynamic> map) {
|
||||
return Users(
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
lastName: map['lastname'],
|
||||
email: map['email'],
|
||||
password: map['password'],
|
||||
username: map['username'],
|
||||
roleId: map['role_id'],
|
||||
roleName: map['role_name'], // Depuis les requêtes avec JOIN
|
||||
pointDeVenteId : map['point_de_vente_id']
|
||||
id: map['id'] as int?,
|
||||
name: map['name'] as String,
|
||||
lastName: map['lastname'] as String, // Correspond à la colonne DB
|
||||
email: map['email'] as String,
|
||||
password: map['password'] as String,
|
||||
username: map['username'] as String,
|
||||
roleId: map['role_id'] as int,
|
||||
roleName: map['role_name'] as String?, // Depuis les JOINs
|
||||
pointDeVenteId: map['point_de_vente_id'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
// Getter pour la compatibilité avec l'ancien code
|
||||
String get role => roleName ?? '';
|
||||
|
||||
}
|
||||
@ -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 descriptionController = TextEditingController(text: product.description ?? '');
|
||||
final imageController = TextEditingController(text: product.image);
|
||||
final referenceController = TextEditingController(text: product.reference ?? '');
|
||||
final marqueController = TextEditingController(text: product.marque ?? '');
|
||||
final ramController = TextEditingController(text: product.ram ?? '');
|
||||
final memoireInterneController = TextEditingController(text: product.memoireInterne ?? '');
|
||||
final imeiController = TextEditingController(text: product.imei ?? '');
|
||||
final newPointDeVenteController = TextEditingController();
|
||||
|
||||
String 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;
|
||||
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(
|
||||
AlertDialog(
|
||||
title: const Text('Modifier le produit'),
|
||||
content: Container(
|
||||
width: 500,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.edit, color: Colors.orange.shade700),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Modifier le produit'),
|
||||
],
|
||||
),
|
||||
content: Container(
|
||||
width: 600,
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: SingleChildScrollView(
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
// Charger les points de vente une seule fois
|
||||
if (isLoadingPoints && pointsDeVente.isEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadPointsDeVente(setDialogState);
|
||||
});
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Champs obligatoires
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info, color: Colors.orange.shade600, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Les champs marqués d\'un * sont obligatoires',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section Point de vente
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.teal.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.store, color: Colors.teal.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Point de vente',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.teal.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
if (isLoadingPoints)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (pointsDeVente.isEmpty)
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'Aucun point de vente trouvé. Créez-en un nouveau.',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: newPointDeVenteController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom du nouveau point de vente',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.add_business),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
if (!showAddNewPoint) ...[
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedPointDeVente,
|
||||
items: pointsDeVente.map((point) {
|
||||
return DropdownMenuItem(
|
||||
value: point['nom'] as String,
|
||||
child: Text(point['nom'] as String),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setDialogState(() => selectedPointDeVente = value);
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Sélectionner un point de vente',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.store),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setDialogState(() {
|
||||
showAddNewPoint = true;
|
||||
newPointDeVenteController.clear();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Ajouter nouveau point'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.teal.shade700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () => loadPointsDeVente(setDialogState),
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
label: const Text('Actualiser'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
if (showAddNewPoint) ...[
|
||||
TextField(
|
||||
controller: newPointDeVenteController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom du nouveau point de vente',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.add_business),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setDialogState(() {
|
||||
showAddNewPoint = false;
|
||||
newPointDeVenteController.clear();
|
||||
});
|
||||
},
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final nom = newPointDeVenteController.text.trim();
|
||||
if (nom.isNotEmpty) {
|
||||
try {
|
||||
final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom);
|
||||
if (id != null) {
|
||||
setDialogState(() {
|
||||
showAddNewPoint = false;
|
||||
selectedPointDeVente = nom;
|
||||
newPointDeVenteController.clear();
|
||||
});
|
||||
// Recharger la liste
|
||||
await loadPointsDeVente(setDialogState);
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Point de vente "$nom" créé avec succès',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Erreur', 'Impossible de créer le point de vente: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.save, size: 16),
|
||||
label: const Text('Créer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.teal,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Nom du produit
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom du produit*',
|
||||
border: OutlineInputBorder(),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nom du produit *',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.shopping_bag),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setDialogState(() {
|
||||
updateQrPreview();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
// Prix et Stock sur la même ligne
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: priceController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prix*',
|
||||
border: OutlineInputBorder(),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Prix (MGA) *',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.attach_money),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: stockController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
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),
|
||||
|
||||
StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return Column(
|
||||
// Description
|
||||
TextField(
|
||||
controller: descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Description',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.description),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section Référence (non modifiable)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.purple.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.confirmation_number, color: Colors.purple.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Référence du produit',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.purple.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: referenceController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Référence',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.tag),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
helperText: 'La référence peut être modifiée avec précaution',
|
||||
),
|
||||
onChanged: (value) {
|
||||
setDialogState(() {
|
||||
updateQrPreview();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Spécifications techniques
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.memory, color: Colors.orange.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Spécifications techniques',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: marqueController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Marque',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.branding_watermark),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: ramController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'RAM',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.memory),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: memoireInterneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mémoire interne',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.storage),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: imeiController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'IMEI (pour téléphones)',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.smartphone),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section Image
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.image, color: Colors.blue.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Image du produit',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: imageController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Image',
|
||||
labelText: 'Chemin de l\'image',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.image);
|
||||
if (result != null && result.files.single.path != null) {
|
||||
if (context.mounted) {
|
||||
setDialogState(() {
|
||||
pickedImage = File(result.files.single.path!);
|
||||
imageController.text = pickedImage!.path;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Choisir'),
|
||||
icon: const Icon(Icons.folder_open, size: 16),
|
||||
label: const Text('Choisir'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
if (pickedImage != null || product.image!.isNotEmpty)
|
||||
Container(
|
||||
// Aperçu de l'image
|
||||
Center(
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
decoration: BoxDecoration(
|
||||
@ -2379,43 +2854,74 @@ Future<void> _generatePDF(Product product, String qrUrl) async {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: pickedImage != null
|
||||
? Image.file(pickedImage!, fit: BoxFit.cover)
|
||||
: (product.image!.isNotEmpty
|
||||
? Image.file(File(product.image!), fit: BoxFit.cover)
|
||||
: (product.image != null && product.image!.isNotEmpty
|
||||
? Image.file(
|
||||
File(product.image!),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Icon(Icons.image, size: 50),
|
||||
)
|
||||
: const Icon(Icons.image, size: 50)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
DropdownButtonFormField<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),
|
||||
|
||||
TextField(
|
||||
controller: descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
border: OutlineInputBorder(),
|
||||
// Aperçu QR Code
|
||||
if (qrPreviewData != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.qr_code_2, color: Colors.green.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Aperçu du QR Code',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: QrImageView(
|
||||
data: qrPreviewData!,
|
||||
version: QrVersions.auto,
|
||||
size: 80,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Réf: ${referenceController.text.trim()}',
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
@ -2423,49 +2929,102 @@ Future<void> _generatePDF(Product product, String qrUrl) async {
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final name = nameController.text.trim();
|
||||
final price = double.tryParse(priceController.text.trim()) ?? 0.0;
|
||||
final stock = int.tryParse(stockController.text.trim()) ?? 0;
|
||||
final reference = referenceController.text.trim();
|
||||
|
||||
if (name.isEmpty || price <= 0) {
|
||||
Get.snackbar('Erreur', 'Nom et prix sont obligatoires');
|
||||
return;
|
||||
}
|
||||
|
||||
if (reference.isEmpty) {
|
||||
Get.snackbar('Erreur', 'La référence est obligatoire');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si la référence existe déjà (sauf pour ce produit)
|
||||
if (reference != product.reference) {
|
||||
final existingProduct = await _productDatabase.getProductByReference(reference);
|
||||
if (existingProduct != null && existingProduct.id != product.id) {
|
||||
Get.snackbar('Erreur', 'Cette référence existe déjà pour un autre produit');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier si l'IMEI existe déjà (sauf pour ce produit)
|
||||
final imei = imeiController.text.trim();
|
||||
if (imei.isNotEmpty && imei != product.imei) {
|
||||
final existingProduct = await _productDatabase.getProductByIMEI(imei);
|
||||
if (existingProduct != null && existingProduct.id != product.id) {
|
||||
Get.snackbar('Erreur', 'Cet IMEI existe déjà pour un autre produit');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Gérer le point de vente
|
||||
int? pointDeVenteId;
|
||||
String? finalPointDeVenteNom;
|
||||
|
||||
if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) {
|
||||
finalPointDeVenteNom = newPointDeVenteController.text.trim();
|
||||
} else if (selectedPointDeVente != null) {
|
||||
finalPointDeVenteNom = selectedPointDeVente;
|
||||
}
|
||||
|
||||
if (finalPointDeVenteNom != null) {
|
||||
pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(finalPointDeVenteNom);
|
||||
}
|
||||
|
||||
try {
|
||||
final updatedProduct = Product(
|
||||
id: product.id,
|
||||
name: name,
|
||||
price: price,
|
||||
image: imageController.text,
|
||||
image: imageController.text.trim(),
|
||||
category: selectedCategory,
|
||||
description: descriptionController.text.trim(),
|
||||
stock: stock,
|
||||
qrCode: product.qrCode,
|
||||
reference: product.reference,
|
||||
qrCode: product.qrCode, // Conserver le QR code existant
|
||||
reference: reference,
|
||||
marque: marqueController.text.trim().isNotEmpty ? marqueController.text.trim() : null,
|
||||
ram: ramController.text.trim().isNotEmpty ? ramController.text.trim() : null,
|
||||
memoireInterne: memoireInterneController.text.trim().isNotEmpty ? memoireInterneController.text.trim() : null,
|
||||
imei: imei.isNotEmpty ? imei : null,
|
||||
pointDeVenteId: pointDeVenteId,
|
||||
);
|
||||
|
||||
try {
|
||||
await _productDatabase.updateProduct(updatedProduct);
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Produit modifié avec succès',
|
||||
'Produit modifié avec succès!\nRéférence: $reference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 4),
|
||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
||||
);
|
||||
_loadProducts();
|
||||
_loadPointsDeVente(); // Recharger aussi les points de vente
|
||||
} catch (e) {
|
||||
Get.snackbar('Erreur', 'Modification é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) {
|
||||
Get.dialog(
|
||||
|
||||
@ -19,6 +19,8 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
||||
List<Permission> permissions = [];
|
||||
List<Map<String, dynamic>> menus = [];
|
||||
Map<int, Map<String, bool>> menuPermissionsMap = {};
|
||||
bool isLoading = true;
|
||||
String? errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -27,8 +29,14 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
||||
}
|
||||
|
||||
Future<void> _initData() async {
|
||||
try {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
});
|
||||
|
||||
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 = {};
|
||||
|
||||
@ -47,11 +55,20 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
||||
permissions = perms;
|
||||
menus = menuList;
|
||||
menuPermissionsMap = tempMenuPermissionsMap;
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
errorMessage = 'Erreur lors du chargement des données: $e';
|
||||
isLoading = false;
|
||||
});
|
||||
print("Erreur lors de l'initialisation des données: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPermissionToggle(
|
||||
int menuId, String permission, bool enabled) async {
|
||||
try {
|
||||
final perm = permissions.firstWhere((p) => p.name == permission);
|
||||
|
||||
if (enabled) {
|
||||
@ -65,61 +82,226 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
|
||||
setState(() {
|
||||
menuPermissionsMap[menuId]![permission] = enabled;
|
||||
});
|
||||
|
||||
// Afficher un message de confirmation
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
enabled
|
||||
? 'Permission "$permission" accordée'
|
||||
: 'Permission "$permission" révoquée',
|
||||
),
|
||||
backgroundColor: enabled ? Colors.green : Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
print("Erreur lors de la modification de la permission: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur lors de la modification: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: "Permissions - ${widget.role.designation}",
|
||||
// showBackButton: true,
|
||||
),
|
||||
body: Padding(
|
||||
void _toggleAllPermissions(int menuId, bool enabled) {
|
||||
for (var permission in permissions) {
|
||||
_onPermissionToggle(menuId, permission.name, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
int _getSelectedPermissionsCount(int menuId) {
|
||||
return menuPermissionsMap[menuId]?.values.where((selected) => selected).length ?? 0;
|
||||
}
|
||||
|
||||
double _getPermissionPercentage(int menuId) {
|
||||
if (permissions.isEmpty) return 0.0;
|
||||
return _getSelectedPermissionsCount(menuId) / permissions.length;
|
||||
}
|
||||
|
||||
Widget _buildPermissionSummary() {
|
||||
int totalPermissions = menus.length * permissions.length;
|
||||
int selectedPermissions = 0;
|
||||
|
||||
for (var menuId in menuPermissionsMap.keys) {
|
||||
selectedPermissions += _getSelectedPermissionsCount(menuId);
|
||||
}
|
||||
|
||||
double percentage = totalPermissions > 0 ? selectedPermissions / totalPermissions : 0.0;
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.analytics, color: Colors.blue.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Gestion des permissions pour le rôle: ${widget.role.designation}',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
'Résumé des permissions',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Sélectionnez les permissions pour chaque menu:',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (permissions.isNotEmpty && menus.isNotEmpty)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: menus.length,
|
||||
itemBuilder: (context, index) {
|
||||
final menu = menus[index];
|
||||
const SizedBox(height: 12),
|
||||
LinearProgressIndicator(
|
||||
value: percentage,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
percentage > 0.7 ? Colors.green :
|
||||
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 menuName = menu['name'] as String;
|
||||
final menuRoute = menu['route'] as String;
|
||||
final selectedCount = _getSelectedPermissionsCount(menuId);
|
||||
final percentage = _getPermissionPercentage(menuId);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 15),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ExpansionTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: percentage == 1.0 ? Colors.green :
|
||||
percentage > 0 ? Colors.orange : Colors.red.shade100,
|
||||
child: Icon(
|
||||
Icons.menu,
|
||||
color: percentage == 1.0 ? Colors.white :
|
||||
percentage > 0 ? Colors.white : Colors.red,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
menuName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
menuName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16),
|
||||
menuRoute,
|
||||
style: TextStyle(
|
||||
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(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: permissions.map((perm) {
|
||||
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
|
||||
return FilterChip(
|
||||
return CustomFilterChip(
|
||||
label: perm.name,
|
||||
selected: isChecked,
|
||||
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
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec informations du rôle
|
||||
Card(
|
||||
elevation: 4,
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: widget.role.designation == 'Super Admin'
|
||||
? Colors.red.shade100
|
||||
: Colors.blue.shade100,
|
||||
radius: 24,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: widget.role.designation == 'Super Admin'
|
||||
? Colors.red.shade700
|
||||
: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Gestion des permissions',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Rôle: ${widget.role.designation}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Configurez les accès pour chaque menu',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Résumé des permissions
|
||||
if (permissions.isNotEmpty && menus.isNotEmpty)
|
||||
_buildPermissionSummary(),
|
||||
|
||||
// Liste des menus et permissions
|
||||
if (permissions.isNotEmpty && menus.isNotEmpty)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: menus.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildMenuCard(menus[index]);
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inbox,
|
||||
size: 64,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucune donnée disponible',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Permissions: ${permissions.length} | Menus: ${menus.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _initData,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Actualiser'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: !isLoading && errorMessage == null
|
||||
? FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('Enregistrer'),
|
||||
backgroundColor: Colors.green,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FilterChip extends StatelessWidget {
|
||||
class CustomFilterChip extends StatelessWidget {
|
||||
final String label;
|
||||
final bool selected;
|
||||
final ValueChanged<bool> onSelected;
|
||||
|
||||
const FilterChip({
|
||||
const CustomFilterChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
Color _getChipColor(String label) {
|
||||
switch (label.toLowerCase()) {
|
||||
case 'view':
|
||||
case 'read':
|
||||
return Colors.blue;
|
||||
case 'create':
|
||||
return Colors.green;
|
||||
case 'update':
|
||||
return Colors.orange;
|
||||
case 'delete':
|
||||
return Colors.red;
|
||||
case 'admin':
|
||||
return Colors.purple;
|
||||
case 'manage':
|
||||
return Colors.indigo;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getChipIcon(String label) {
|
||||
switch (label.toLowerCase()) {
|
||||
case 'view':
|
||||
case 'read':
|
||||
return Icons.visibility;
|
||||
case 'create':
|
||||
return Icons.add;
|
||||
case 'update':
|
||||
return Icons.edit;
|
||||
case 'delete':
|
||||
return Icons.delete;
|
||||
case 'admin':
|
||||
return Icons.admin_panel_settings;
|
||||
case 'manage':
|
||||
return Icons.settings;
|
||||
default:
|
||||
return Icons.security;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChoiceChip(
|
||||
label: Text(label),
|
||||
final color = _getChipColor(label);
|
||||
final icon = _getChipIcon(label);
|
||||
|
||||
return FilterChip(
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: selected ? Colors.white : color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: selected ? Colors.white : color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
selected: selected,
|
||||
onSelected: onSelected,
|
||||
selectedColor: Colors.blue,
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? Colors.white : Colors.black,
|
||||
),
|
||||
selectedColor: color,
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
checkmarkColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(
|
||||
color: selected ? color : color.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
elevation: selected ? 4 : 1,
|
||||
pressElevation: 8,
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -29,9 +29,12 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
||||
}
|
||||
|
||||
Future<void> _initData() async {
|
||||
try {
|
||||
final roleList = await db.getRoles();
|
||||
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 = {};
|
||||
|
||||
@ -56,18 +59,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
||||
menus = menuList;
|
||||
roleMenuPermissionsMap = tempRoleMenuPermissionsMap;
|
||||
});
|
||||
} catch (e) {
|
||||
print("Erreur lors de l'initialisation des données: $e");
|
||||
// Afficher un message d'erreur à l'utilisateur
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur lors du chargement des données: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addRole() async {
|
||||
String designation = _roleController.text.trim();
|
||||
if (designation.isEmpty) return;
|
||||
if (designation.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Veuillez saisir une désignation pour le rôle'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Vérifier si le rôle existe déjà
|
||||
final existingRoles = roles.where((r) => r.designation.toLowerCase() == designation.toLowerCase());
|
||||
if (existingRoles.isNotEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Ce rôle existe déjà'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await db.createRole(Role(designation: designation));
|
||||
_roleController.clear();
|
||||
await _initData();
|
||||
|
||||
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 {
|
||||
try {
|
||||
final perm = permissions.firstWhere((p) => p.name == permission);
|
||||
|
||||
if (enabled) {
|
||||
@ -79,6 +130,70 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
||||
setState(() {
|
||||
roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled;
|
||||
});
|
||||
} catch (e) {
|
||||
print("Erreur lors de la modification de la permission: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur lors de la modification de la permission: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -104,28 +219,52 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
||||
controller: _roleController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nouveau rôle',
|
||||
hintText: 'Ex: Manager, Vendeur...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _addRole(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
ElevatedButton.icon(
|
||||
onPressed: _addRole,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Ajouter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Affichage des statistiques
|
||||
if (roles.isNotEmpty)
|
||||
Card(
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildStatItem('Rôles', roles.length.toString(), Icons.people),
|
||||
_buildStatItem('Permissions', permissions.length.toString(), Icons.security),
|
||||
_buildStatItem('Menus', menus.length.toString(), Icons.menu),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Tableau des rôles et permissions
|
||||
if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty)
|
||||
Expanded(
|
||||
@ -137,22 +276,64 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: MediaQuery.of(context).size.width - 32,
|
||||
),
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: menus.map((menu) {
|
||||
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: [
|
||||
Text(
|
||||
menu['name'],
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
menuName,
|
||||
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,
|
||||
headingRowHeight: 50,
|
||||
dataRowHeight: 60,
|
||||
columns: [
|
||||
const DataColumn(
|
||||
label: Text(
|
||||
@ -161,17 +342,49 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
||||
),
|
||||
),
|
||||
...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,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)).toList(),
|
||||
const DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: roles.map((role) {
|
||||
final roleId = role.id!;
|
||||
return DataRow(
|
||||
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) {
|
||||
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
|
||||
return DataCell(
|
||||
@ -180,26 +393,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
||||
onChanged: (bool? value) {
|
||||
_onPermissionToggle(roleId, menuId, perm.name, value ?? false);
|
||||
},
|
||||
activeColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
DataCell(
|
||||
role.designation != 'Super Admin'
|
||||
? IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.red.shade600),
|
||||
tooltip: 'Supprimer le rôle',
|
||||
onPressed: () => _deleteRole(role),
|
||||
)
|
||||
: Icon(Icons.lock, color: Colors.grey.shade400),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text('Aucun rôle, permission ou menu trouvé'),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.inbox, size: 64, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucune donnée disponible',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Rôles: ${roles.length} | Permissions: ${permissions.length} | Menus: ${menus.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _initData,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Actualiser'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -207,4 +460,34 @@ class _HandleUserRoleState extends State<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/appDrawer.dart';
|
||||
import 'package:youmazgestion/Models/client.dart';
|
||||
import 'package:youmazgestion/Models/produit.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
|
||||
class HistoriquePage extends StatefulWidget {
|
||||
@ -612,7 +611,7 @@ class _HistoriquePageState extends State<HistoriquePage> {
|
||||
),
|
||||
),
|
||||
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 = [];
|
||||
Users? _selectedCommercialUser;
|
||||
|
||||
// Variables pour les suggestions clients
|
||||
List<Client> _clientSuggestions = [];
|
||||
bool _showNomSuggestions = false;
|
||||
bool _showTelephoneSuggestions = false;
|
||||
GlobalKey _nomFieldKey = GlobalKey();
|
||||
GlobalKey _telephoneFieldKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -142,6 +149,123 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
_searchNameController.addListener(_filterProducts);
|
||||
_searchImeiController.addListener(_filterProducts);
|
||||
_searchReferenceController.addListener(_filterProducts);
|
||||
|
||||
// Listeners pour l'autocomplétion client
|
||||
_nomController.addListener(() {
|
||||
if (_nomController.text.length >= 3) {
|
||||
_showClientSuggestions(_nomController.text, isNom: true);
|
||||
} else {
|
||||
_hideNomSuggestions();
|
||||
}
|
||||
});
|
||||
|
||||
_telephoneController.addListener(() {
|
||||
if (_telephoneController.text.length >= 3) {
|
||||
_showClientSuggestions(_telephoneController.text, isNom: false);
|
||||
} else {
|
||||
_hideTelephoneSuggestions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Méthode pour vider complètement le formulaire et le panier
|
||||
void _clearFormAndCart() {
|
||||
setState(() {
|
||||
// Vider les contrôleurs client
|
||||
_nomController.clear();
|
||||
_prenomController.clear();
|
||||
_emailController.clear();
|
||||
_telephoneController.clear();
|
||||
_adresseController.clear();
|
||||
|
||||
// Vider le panier
|
||||
_quantites.clear();
|
||||
|
||||
// Réinitialiser le commercial au premier de la liste
|
||||
if (_commercialUsers.isNotEmpty) {
|
||||
_selectedCommercialUser = _commercialUsers.first;
|
||||
}
|
||||
|
||||
// Masquer toutes les suggestions
|
||||
_hideAllSuggestions();
|
||||
|
||||
// Réinitialiser l'état de chargement
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<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 {
|
||||
@ -391,34 +515,10 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
return Scaffold(
|
||||
floatingActionButton: _buildFloatingCartButton(),
|
||||
drawer: isMobile ? CustomDrawer() : null,
|
||||
body: Column(
|
||||
body: GestureDetector(
|
||||
onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs
|
||||
child: Column(
|
||||
children: [
|
||||
// Bouton client - version compacte pour mobile
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: isMobile ? 12 : 16
|
||||
),
|
||||
backgroundColor: Colors.blue.shade800,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: _showClientFormDialog,
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: Text(
|
||||
isMobile ? 'Client' : 'Ajouter les informations client',
|
||||
style: TextStyle(fontSize: isMobile ? 14 : 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Section des filtres - adaptée comme dans HistoriquePage
|
||||
if (!isMobile)
|
||||
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() {
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
final cartItemCount = _quantites.values.where((q) => q > 0).length;
|
||||
@ -508,7 +668,24 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
void _showClientFormDialog() {
|
||||
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(
|
||||
StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return Stack(
|
||||
children: [
|
||||
AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
@ -541,19 +718,63 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTextFormField(
|
||||
// Champ Nom avec suggestions (SANS bouton recherche)
|
||||
_buildTextFormFieldWithKey(
|
||||
key: nomFieldKey,
|
||||
controller: _nomController,
|
||||
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),
|
||||
_buildTextFormField(
|
||||
|
||||
// Champ Prénom avec suggestions (SANS bouton recherche)
|
||||
_buildTextFormFieldWithKey(
|
||||
key: prenomFieldKey,
|
||||
controller: _prenomController,
|
||||
label: 'Prénom',
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
|
||||
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),
|
||||
_buildTextFormField(
|
||||
|
||||
// Champ Email avec suggestions (SANS bouton recherche)
|
||||
_buildTextFormFieldWithKey(
|
||||
key: emailFieldKey,
|
||||
controller: _emailController,
|
||||
label: 'Email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
@ -564,20 +785,60 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
}
|
||||
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),
|
||||
_buildTextFormField(
|
||||
|
||||
// Champ Téléphone avec suggestions (SANS bouton recherche)
|
||||
_buildTextFormFieldWithKey(
|
||||
key: telephoneFieldKey,
|
||||
controller: _telephoneController,
|
||||
label: 'Téléphone',
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
|
||||
validator: (value) => value?.isEmpty ?? true
|
||||
? 'Veuillez entrer un téléphone' : null,
|
||||
onChanged: (value) async {
|
||||
if (value.length >= 3) {
|
||||
final suggestions = await _appDatabase.suggestClients(value);
|
||||
setDialogState(() {
|
||||
localClientSuggestions = suggestions;
|
||||
showTelephoneSuggestions = suggestions.isNotEmpty;
|
||||
showNomSuggestions = false;
|
||||
showPrenomSuggestions = false;
|
||||
showEmailSuggestions = false;
|
||||
});
|
||||
} else {
|
||||
setDialogState(() {
|
||||
showTelephoneSuggestions = false;
|
||||
localClientSuggestions = [];
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_buildTextFormField(
|
||||
controller: _adresseController,
|
||||
label: 'Adresse',
|
||||
maxLines: 2,
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
|
||||
validator: (value) => value?.isEmpty ?? true
|
||||
? 'Veuillez entrer une adresse' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCommercialDropdown(),
|
||||
@ -602,6 +863,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Fermer toutes les suggestions avant de soumettre
|
||||
setDialogState(() {
|
||||
showNomSuggestions = false;
|
||||
showPrenomSuggestions = false;
|
||||
showEmailSuggestions = false;
|
||||
showTelephoneSuggestions = false;
|
||||
localClientSuggestions = [];
|
||||
});
|
||||
Get.back();
|
||||
_submitOrder();
|
||||
}
|
||||
@ -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({
|
||||
required TextEditingController controller,
|
||||
@ -622,6 +1135,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
TextInputType? keyboardType,
|
||||
String? Function(String?)? validator,
|
||||
int? maxLines,
|
||||
void Function(String)? onChanged,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
@ -629,19 +1143,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
labelText: label,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade400),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade400),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
keyboardType: keyboardType,
|
||||
validator: validator,
|
||||
maxLines: maxLines,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1137,11 +1646,15 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
try {
|
||||
await _appDatabase.createCommandeComplete(client, commande, details);
|
||||
|
||||
// Fermer le panier avant d'afficher la confirmation
|
||||
Get.back();
|
||||
|
||||
// Afficher le dialogue de confirmation - adapté pour mobile
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false, // Empêcher la fermeture accidentelle
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
@ -1182,16 +1695,8 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
// Réinitialiser le formulaire
|
||||
_nomController.clear();
|
||||
_prenomController.clear();
|
||||
_emailController.clear();
|
||||
_telephoneController.clear();
|
||||
_adresseController.clear();
|
||||
setState(() {
|
||||
_quantites.clear();
|
||||
_isLoading = false;
|
||||
});
|
||||
// Vider complètement le formulaire et le panier
|
||||
_clearFormAndCart();
|
||||
// Recharger les produits pour mettre à jour le stock
|
||||
_loadProducts();
|
||||
},
|
||||
@ -1222,6 +1727,10 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Nettoyer les suggestions
|
||||
_hideAllSuggestions();
|
||||
|
||||
// Disposer les contrôleurs
|
||||
_nomController.dispose();
|
||||
_prenomController.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:get/get.dart';
|
||||
//import 'package:youmazgestion/Services/app_database.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
//import 'Services/productDatabase.dart';
|
||||
import 'my_app.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
@ -11,31 +9,117 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
try {
|
||||
// Initialiser les bases de données une seule fois
|
||||
print("Initialisation de l'application...");
|
||||
|
||||
// Pour le développement : supprimer toutes les tables (équivalent à deleteDatabaseFile)
|
||||
// ATTENTION: Décommentez seulement si vous voulez réinitialiser la base
|
||||
// await AppDatabase.instance.deleteDatabaseFile();
|
||||
// await ProductDatabase.instance.deleteDatabaseFile();
|
||||
|
||||
// await ProductDatabase.instance.initDatabase();
|
||||
// Initialiser la base de données MySQL
|
||||
print("Connexion à la base de données MySQL...");
|
||||
await AppDatabase.instance.initDatabase();
|
||||
|
||||
print("Base de données initialisée avec succès !");
|
||||
|
||||
// Afficher les informations de la base (pour debug)
|
||||
// await AppDatabase.instance.printDatabaseInfo();
|
||||
Get.put(
|
||||
UserController()); // Ajoute ce code AVANT tout accès au UserController
|
||||
await AppDatabase.instance.printDatabaseInfo();
|
||||
|
||||
// Initialiser le contrôleur utilisateur
|
||||
Get.put(UserController());
|
||||
print("Contrôleur utilisateur initialisé");
|
||||
|
||||
// Configurer le logger
|
||||
setupLogger();
|
||||
|
||||
print("Lancement de l'application...");
|
||||
runApp(const GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: MyApp(),
|
||||
));
|
||||
} catch (e) {
|
||||
print('Erreur lors de l\'initialisation: $e');
|
||||
// Vous pourriez vouloir afficher une page d'erreur ici
|
||||
|
||||
// Afficher une page d'erreur avec plus de détails
|
||||
runApp(MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Text('Erreur d\'initialisation: $e'),
|
||||
backgroundColor: Colors.red[50],
|
||||
appBar: AppBar(
|
||||
title: const Text('Erreur d\'initialisation'),
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error,
|
||||
color: Colors.red,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Erreur de connexion à la base de données',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Vérifiez que :',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('• XAMPP est démarré'),
|
||||
const Text('• MySQL est en cours d\'exécution'),
|
||||
const Text('• La base de données "guycom_databse" existe'),
|
||||
const Text('• Les paramètres de connexion sont corrects'),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Détails de l\'erreur :',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
e.toString(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Relancer l'application
|
||||
main();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
@ -11,7 +11,6 @@ import mobile_scanner
|
||||
import open_file_mac
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@ -21,6 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
48
pubspec.lock
48
pubspec.lock
@ -632,6 +632,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
mysql1:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mysql1
|
||||
sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -832,6 +840,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -981,22 +997,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1013,22 +1013,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -35,7 +35,8 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
get: ^4.6.5
|
||||
sqflite: ^2.2.8+4
|
||||
# sqflite: ^2.2.8+4
|
||||
mysql1: ^0.20.0
|
||||
|
||||
flutter_dropzone: ^4.2.1
|
||||
image_picker: ^0.8.7+5
|
||||
|
||||
Loading…
Reference in New Issue
Block a user