- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD) * DEV: Clés TEST Pierre (mode test) * REC: Clés TEST Client (mode test) * PROD: Clés LIVE Client (mode live) - Ajout de la gestion des bases de données immeubles/bâtiments * Configuration buildings_database pour DEV/REC/PROD * Service BuildingService pour enrichissement des adresses - Optimisations pages et améliorations ergonomie - Mises à jour des dépendances Composer - Nettoyage des fichiers obsolètes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
765 lines
29 KiB
Dart
765 lines
29 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:geosector_app/core/data/models/sector_model.dart';
|
|
import 'package:geosector_app/core/data/models/membre_model.dart';
|
|
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
|
import 'package:geosector_app/core/services/current_amicale_service.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
|
|
|
class SectorDialog extends StatefulWidget {
|
|
final SectorModel? existingSector;
|
|
final List<List<double>> coordinates;
|
|
final Future<void> Function(String name, String color, List<int> memberIds, bool updatePassages) onSave;
|
|
|
|
const SectorDialog({
|
|
super.key,
|
|
this.existingSector,
|
|
required this.coordinates,
|
|
required this.onSave,
|
|
});
|
|
|
|
@override
|
|
State<SectorDialog> createState() => _SectorDialogState();
|
|
}
|
|
|
|
class _SectorDialogState extends State<SectorDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _nameController = TextEditingController();
|
|
final _nameFocusNode = FocusNode();
|
|
final _searchController = TextEditingController();
|
|
Color _selectedColor = Colors.blue;
|
|
final List<int> _selectedMemberIds = [];
|
|
bool _isLoading = false;
|
|
String _searchQuery = '';
|
|
bool _updatePassages = true; // Par défaut activé
|
|
bool _initialUpdatePassages = true; // Valeur initiale pour détecter les changements
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.existingSector != null) {
|
|
_nameController.text = widget.existingSector!.libelle;
|
|
_selectedColor = _hexToColor(widget.existingSector!.color);
|
|
// Charger les membres affectés au secteur
|
|
_loadSectorMembers();
|
|
}
|
|
|
|
// Donner le focus au champ nom après que le dialog soit construit
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_nameFocusNode.requestFocus();
|
|
});
|
|
}
|
|
|
|
// Charger les membres actuellement affectés au secteur
|
|
void _loadSectorMembers() {
|
|
if (widget.existingSector == null) return;
|
|
|
|
debugPrint('=== Début chargement membres pour secteur ${widget.existingSector!.id} - ${widget.existingSector!.libelle} ===');
|
|
|
|
try {
|
|
// Vérifier si la box UserSector est ouverte
|
|
if (!Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
|
|
debugPrint('Box UserSector non ouverte');
|
|
return;
|
|
}
|
|
|
|
final userSectorBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
|
debugPrint('Box UserSector contient ${userSectorBox.length} entrées au total');
|
|
|
|
// Afficher toutes les entrées pour debug
|
|
for (var i = 0; i < userSectorBox.length; i++) {
|
|
final us = userSectorBox.getAt(i);
|
|
if (us != null) {
|
|
debugPrint(' - UserSector[$i]: userId=${us.userId}, opeUserId=${us.opeUserId}, fkSector=${us.fkSector}, name="${us.firstName} ${us.name}"');
|
|
}
|
|
}
|
|
|
|
// Récupérer tous les UserSectorModel pour ce secteur
|
|
final userSectors = userSectorBox.values
|
|
.where((us) => us.fkSector == widget.existingSector!.id)
|
|
.toList();
|
|
|
|
debugPrint('Trouvé ${userSectors.length} UserSectorModel pour le secteur ${widget.existingSector!.id}');
|
|
|
|
// Pré-sélectionner les IDs des membres affectés (ope_users.id)
|
|
setState(() {
|
|
_selectedMemberIds.clear();
|
|
for (final userSector in userSectors) {
|
|
// Utiliser opeUserId (ope_users.id) pour la sélection
|
|
_selectedMemberIds.add(userSector.opeUserId);
|
|
debugPrint('Membre présélectionné: ${userSector.firstName} ${userSector.name} (userId: ${userSector.userId}, opeUserId: ${userSector.opeUserId}, fkSector: ${userSector.fkSector})');
|
|
}
|
|
});
|
|
|
|
debugPrint('=== Fin chargement: ${_selectedMemberIds.length} membres présélectionnés ===');
|
|
debugPrint('IDs présélectionnés: $_selectedMemberIds');
|
|
|
|
// Marquer le chargement comme terminé
|
|
setState(() {
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des membres du secteur: $e');
|
|
setState(() {
|
|
// Marquer comme terminé même en cas d'erreur
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
_nameFocusNode.dispose();
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Color _hexToColor(String hexColor) {
|
|
final String colorStr = hexColor.startsWith('#') ? hexColor.substring(1) : hexColor;
|
|
final String fullColorStr = colorStr.length == 6 ? 'FF$colorStr' : colorStr;
|
|
return Color(int.parse(fullColorStr, radix: 16));
|
|
}
|
|
|
|
String _colorToHex(Color color) {
|
|
return '#${color.value.toRadixString(16).substring(2).toUpperCase()}';
|
|
}
|
|
|
|
// Dialogue de confirmation pour le changement du switch
|
|
Future<bool> _showUpdatePassagesConfirmation(bool newValue) async {
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.warning_amber_rounded,
|
|
color: Colors.orange,
|
|
size: 28,
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Text('Confirmation'),
|
|
],
|
|
),
|
|
content: Text(
|
|
newValue
|
|
? 'Êtes-vous sûr de vouloir recalculer les passages ?\n\n'
|
|
'Tous les passages du secteur seront réaffectés selon les nouvelles limites.'
|
|
: 'Êtes-vous sûr de vouloir conserver les passages existants ?\n\n'
|
|
'Les passages actuels ne seront pas modifiés même si les limites du secteur changent.',
|
|
style: const TextStyle(fontSize: 15),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Confirmer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
return result ?? false;
|
|
}
|
|
|
|
void _handleSave() async {
|
|
if (_formKey.currentState!.validate()) {
|
|
// Vérifier qu'au moins un membre est sélectionné
|
|
if (_selectedMemberIds.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Veuillez sélectionner au moins un membre'),
|
|
backgroundColor: Colors.orange,
|
|
duration: Duration(seconds: 3),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Indiquer que nous sommes en train de sauvegarder
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
// Appeler le callback onSave et attendre sa résolution
|
|
await widget.onSave(
|
|
_nameController.text.trim(),
|
|
_colorToHex(_selectedColor),
|
|
_selectedMemberIds,
|
|
_updatePassages, // Passer le paramètre updatePassages
|
|
);
|
|
|
|
// Si tout s'est bien passé, fermer le dialog
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
} catch (e) {
|
|
// En cas d'erreur, réactiver le bouton
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
// L'erreur sera gérée par le callback onSave
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showColorPicker() {
|
|
// Grille 6x6 de couleurs suivant le spectre
|
|
// 6 colonnes: Rouge, Orange, Jaune, Vert, Bleu, Violet
|
|
// 6 lignes: variations de luminosité/saturation
|
|
final List<Color> colors = _generateSpectralColorGrid();
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Choisir une couleur'),
|
|
contentPadding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
|
|
content: Container(
|
|
width: 280, // Largeur fixe pour contrôler la taille
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: Colors.grey.shade50,
|
|
),
|
|
child: GridView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 6,
|
|
mainAxisSpacing: 4,
|
|
crossAxisSpacing: 4,
|
|
childAspectRatio: 1.0,
|
|
),
|
|
itemCount: colors.length,
|
|
itemBuilder: (context, index) {
|
|
final color = colors[index];
|
|
final isSelected = _selectedColor.value == color.value;
|
|
|
|
return InkWell(
|
|
onTap: () {
|
|
setState(() {
|
|
_selectedColor = color;
|
|
});
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Container(
|
|
width: 35,
|
|
height: 35,
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(
|
|
color: isSelected ? Colors.black87 : Colors.grey.shade400,
|
|
width: isSelected ? 2.5 : 0.5,
|
|
),
|
|
boxShadow: isSelected
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: isSelected
|
|
? const Icon(
|
|
Icons.check,
|
|
color: Colors.white,
|
|
size: 18,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.black,
|
|
blurRadius: 2,
|
|
),
|
|
],
|
|
)
|
|
: null,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Affichage de la couleur sélectionnée
|
|
Container(
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: _selectedColor,
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: Colors.grey.shade400),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'Couleur sélectionnée',
|
|
style: TextStyle(
|
|
color: _selectedColor.computeLuminance() > 0.5
|
|
? Colors.black87
|
|
: Colors.white,
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Annuler'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Mettre en surbrillance les termes recherchés dans le texte
|
|
List<TextSpan> _highlightSearchTerms(String text) {
|
|
if (_searchQuery.isEmpty) {
|
|
return [TextSpan(text: text)];
|
|
}
|
|
|
|
final List<TextSpan> spans = [];
|
|
final lowerText = text.toLowerCase();
|
|
int start = 0;
|
|
int index = lowerText.indexOf(_searchQuery, start);
|
|
|
|
while (index != -1) {
|
|
// Ajouter le texte avant le terme trouvé
|
|
if (index > start) {
|
|
spans.add(TextSpan(
|
|
text: text.substring(start, index),
|
|
));
|
|
}
|
|
|
|
// Ajouter le terme trouvé en surbrillance
|
|
spans.add(TextSpan(
|
|
text: text.substring(index, index + _searchQuery.length),
|
|
style: const TextStyle(
|
|
backgroundColor: Colors.yellow,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
));
|
|
|
|
start = index + _searchQuery.length;
|
|
index = lowerText.indexOf(_searchQuery, start);
|
|
}
|
|
|
|
// Ajouter le reste du texte
|
|
if (start < text.length) {
|
|
spans.add(TextSpan(
|
|
text: text.substring(start),
|
|
));
|
|
}
|
|
|
|
return spans;
|
|
}
|
|
|
|
// Générer une grille 6x6 de couleurs spectrales
|
|
List<Color> _generateSpectralColorGrid() {
|
|
final List<Color> colors = [];
|
|
|
|
// 6 teintes de base (colonnes)
|
|
final List<double> hues = [
|
|
0, // Rouge
|
|
30, // Orange
|
|
60, // Jaune
|
|
120, // Vert
|
|
210, // Bleu
|
|
270, // Violet
|
|
];
|
|
|
|
// 6 variations de luminosité/saturation (lignes)
|
|
// Du plus clair au plus foncé
|
|
final List<Map<String, double>> variations = [
|
|
{'saturation': 0.3, 'lightness': 0.85}, // Très clair
|
|
{'saturation': 0.5, 'lightness': 0.70}, // Clair
|
|
{'saturation': 0.7, 'lightness': 0.55}, // Moyen clair
|
|
{'saturation': 0.85, 'lightness': 0.45}, // Moyen foncé
|
|
{'saturation': 0.95, 'lightness': 0.35}, // Foncé
|
|
{'saturation': 1.0, 'lightness': 0.25}, // Très foncé
|
|
];
|
|
|
|
// Générer la grille ligne par ligne
|
|
for (final variation in variations) {
|
|
for (final hue in hues) {
|
|
colors.add(
|
|
HSLColor.fromAHSL(
|
|
1.0,
|
|
hue,
|
|
variation['saturation']!,
|
|
variation['lightness']!,
|
|
).toColor(),
|
|
);
|
|
}
|
|
}
|
|
|
|
return colors;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final currentAmicale = CurrentAmicaleService.instance.currentAmicale;
|
|
final screenHeight = MediaQuery.of(context).size.height;
|
|
final dialogHeight = (screenHeight * 0.8).clamp(0.0, 800.0); // 80% de l'écran avec max 800px
|
|
|
|
return AlertDialog(
|
|
title: Text(widget.existingSector == null ? 'Nouveau secteur' : 'Modifier le secteur'),
|
|
content: Container(
|
|
width: 450, // Largeur fixe pour la dialog
|
|
height: dialogHeight, // Hauteur avec maximum de 800px
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Section scrollable pour nom et couleur
|
|
// Nom du secteur
|
|
TextFormField(
|
|
controller: _nameController,
|
|
focusNode: _nameFocusNode,
|
|
decoration: InputDecoration(
|
|
labelText: 'Nom du secteur',
|
|
labelStyle: TextStyle(color: Colors.black),
|
|
label: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('Nom du secteur'),
|
|
Text(
|
|
' *',
|
|
style: TextStyle(color: Colors.red),
|
|
),
|
|
],
|
|
),
|
|
hintText: 'Ex: Centre-ville',
|
|
prefixIcon: Icon(Icons.location_on),
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Veuillez entrer un nom';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Couleur du secteur
|
|
const Text(
|
|
'Couleur du secteur',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 10),
|
|
InkWell(
|
|
onTap: () {
|
|
_showColorPicker();
|
|
},
|
|
child: Container(
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
color: _selectedColor,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'Toucher pour changer',
|
|
style: TextStyle(
|
|
color: _selectedColor.computeLuminance() > 0.5
|
|
? Colors.black
|
|
: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Switch pour la mise à jour des passages (uniquement en mode édition)
|
|
if (widget.existingSector != null) ...[
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 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: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Mise à jour des passages',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 15,
|
|
color: Colors.blue.shade900,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_updatePassages
|
|
? 'Les passages seront recalculés et réaffectés'
|
|
: 'Les passages existants ne seront pas modifiés',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade700,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Switch(
|
|
value: _updatePassages,
|
|
onChanged: (value) async {
|
|
// Afficher confirmation uniquement si la valeur change par rapport à l'initiale
|
|
if (value != _initialUpdatePassages) {
|
|
final confirmed = await _showUpdatePassagesConfirmation(value);
|
|
if (confirmed) {
|
|
setState(() {
|
|
_updatePassages = value;
|
|
});
|
|
}
|
|
} else {
|
|
setState(() {
|
|
_updatePassages = value;
|
|
});
|
|
}
|
|
},
|
|
activeColor: Colors.blue,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
|
|
// Sélection des membres
|
|
Row(
|
|
children: [
|
|
const Text(
|
|
'Membres affectés',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'*',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// Champ de recherche pour filtrer les membres
|
|
TextField(
|
|
controller: _searchController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Rechercher par prénom, nom ou nom de tournée...',
|
|
prefixIcon: const Icon(Icons.search),
|
|
suffixIcon: _searchQuery.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
setState(() {
|
|
_searchController.clear();
|
|
_searchQuery = '';
|
|
});
|
|
},
|
|
)
|
|
: null,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_searchQuery = value.toLowerCase();
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
if (_selectedMemberIds.isEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
child: Text(
|
|
'Sélectionnez au moins un membre',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Liste des membres avec scrolling et filtre
|
|
if (currentAmicale != null)
|
|
Expanded(
|
|
child: ValueListenableBuilder<Box<MembreModel>>(
|
|
valueListenable: Hive.box<MembreModel>(AppKeys.membresBoxName).listenable(),
|
|
builder: (context, box, _) {
|
|
debugPrint('=== Build liste membres - IDs présélectionnés: $_selectedMemberIds ===');
|
|
|
|
// Récupérer tous les membres (déjà uniques dans la box)
|
|
// Filtrer uniquement ceux qui ont un opeUserId (dans l'opération courante)
|
|
var membres = box.values
|
|
.whereType<MembreModel>()
|
|
.where((membre) => membre.opeUserId != null)
|
|
.toList();
|
|
|
|
// Appliquer le filtre de recherche
|
|
if (_searchQuery.isNotEmpty) {
|
|
membres = membres.where((membre) {
|
|
final firstName = membre.firstName?.toLowerCase() ?? '';
|
|
final lastName = membre.name?.toLowerCase() ?? '';
|
|
final sectName = membre.sectName?.toLowerCase() ?? '';
|
|
|
|
return firstName.contains(_searchQuery) ||
|
|
lastName.contains(_searchQuery) ||
|
|
sectName.contains(_searchQuery);
|
|
}).toList();
|
|
}
|
|
|
|
// Trier : membres affectés en premier, puis les autres
|
|
membres.sort((a, b) {
|
|
final aSelected = _selectedMemberIds.contains(a.opeUserId);
|
|
final bSelected = _selectedMemberIds.contains(b.opeUserId);
|
|
|
|
// Si l'un est sélectionné et pas l'autre, le mettre en premier
|
|
if (aSelected && !bSelected) return -1;
|
|
if (!aSelected && bSelected) return 1;
|
|
|
|
// Sinon, trier alphabétiquement par prénom puis nom
|
|
final firstNameCompare = (a.firstName ?? '').compareTo(b.firstName ?? '');
|
|
if (firstNameCompare != 0) return firstNameCompare;
|
|
return (a.name ?? '').compareTo(b.name ?? '');
|
|
});
|
|
|
|
if (membres.isEmpty) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Text(
|
|
_searchQuery.isNotEmpty
|
|
? 'Aucun membre trouvé pour "$_searchQuery"'
|
|
: 'Aucun membre disponible pour cette opération',
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Afficher le nombre de résultats
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Text(
|
|
'${membres.length} membre${membres.length > 1 ? 's' : ''} ${_searchQuery.isNotEmpty ? 'trouvé${membres.length > 1 ? 's' : ''}' : 'disponible${membres.length > 1 ? 's' : ''}'}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: ListView.builder(
|
|
itemCount: membres.length,
|
|
itemBuilder: (context, index) {
|
|
final membre = membres[index];
|
|
final isSelected = _selectedMemberIds.contains(membre.opeUserId);
|
|
|
|
// Log pour debug
|
|
if (index < 3) { // Limiter les logs aux 3 premiers membres
|
|
debugPrint('Membre ${index}: ${membre.firstName} ${membre.name} (userId: ${membre.id}, opeUserId: ${membre.opeUserId}) - isSelected: $isSelected');
|
|
}
|
|
|
|
return CheckboxListTile(
|
|
dense: true,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0),
|
|
title: RichText(
|
|
text: TextSpan(
|
|
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
|
children: _highlightSearchTerms(
|
|
'${membre.firstName} ${membre.name}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}',
|
|
),
|
|
),
|
|
),
|
|
value: isSelected,
|
|
onChanged: (bool? value) {
|
|
setState(() {
|
|
if (value == true) {
|
|
// opeUserId ne peut pas être null grâce au filtre ligne 517
|
|
_selectedMemberIds.add(membre.opeUserId!);
|
|
} else {
|
|
_selectedMemberIds.remove(membre.opeUserId!);
|
|
}
|
|
});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: _isLoading ? null : _handleSave,
|
|
child: _isLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: Text(widget.existingSector == null ? 'Créer' : 'Modifier'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |