You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
471 lines
15 KiB
471 lines
15 KiB
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'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|