feat: Gestion des secteurs et migration v3.0.4+304
- Ajout système complet de gestion des secteurs avec contours géographiques - Import des contours départementaux depuis GeoJSON - API REST pour la gestion des secteurs (/api/sectors) - Service de géolocalisation pour déterminer les secteurs - Migration base de données avec tables x_departements_contours et sectors_adresses - Interface Flutter pour visualisation et gestion des secteurs - Ajout thème sombre dans l'application - Corrections diverses et optimisations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
348
app/lib/presentation/dialogs/sector_action_result_dialog.dart
Normal file
348
app/lib/presentation/dialogs/sector_action_result_dialog.dart
Normal file
@@ -0,0 +1,348 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum SectorActionType {
|
||||
create,
|
||||
update,
|
||||
delete,
|
||||
}
|
||||
|
||||
class SectorActionResultDialog extends StatelessWidget {
|
||||
final SectorActionType actionType;
|
||||
final String sectorName;
|
||||
final Map<String, dynamic> statistics;
|
||||
final Map<String, dynamic>? departmentWarning;
|
||||
final VoidCallback? onConfirm;
|
||||
|
||||
const SectorActionResultDialog({
|
||||
super.key,
|
||||
required this.actionType,
|
||||
required this.sectorName,
|
||||
required this.statistics,
|
||||
this.departmentWarning,
|
||||
this.onConfirm,
|
||||
});
|
||||
|
||||
String get _actionTitle {
|
||||
switch (actionType) {
|
||||
case SectorActionType.create:
|
||||
return 'Secteur créé';
|
||||
case SectorActionType.update:
|
||||
return 'Secteur modifié';
|
||||
case SectorActionType.delete:
|
||||
return 'Secteur supprimé';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get _actionIcon {
|
||||
switch (actionType) {
|
||||
case SectorActionType.create:
|
||||
return Icons.add_location_alt;
|
||||
case SectorActionType.update:
|
||||
return Icons.edit_location_alt;
|
||||
case SectorActionType.delete:
|
||||
return Icons.delete_forever;
|
||||
}
|
||||
}
|
||||
|
||||
Color get _actionColor {
|
||||
switch (actionType) {
|
||||
case SectorActionType.create:
|
||||
return Colors.green;
|
||||
case SectorActionType.update:
|
||||
return Colors.orange;
|
||||
case SectorActionType.delete:
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(_actionIcon, color: _actionColor, size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_actionTitle,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
sectorName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Warning départemental si présent
|
||||
if (departmentWarning != null && departmentWarning!['intersecting_departments'] != null) ...[
|
||||
_buildDepartmentWarning(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Statistiques des passages
|
||||
_buildStatisticsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onConfirm?.call();
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDepartmentWarning() {
|
||||
final departments = departmentWarning!['intersecting_departments'] as List<dynamic>;
|
||||
final isMultipleDepartments = departments.length > 1;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.orange[700], size: 24),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isMultipleDepartments
|
||||
? 'Secteur à cheval sur plusieurs départements'
|
||||
: 'Secteur sur un autre département',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange[900],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...departments.map((dept) {
|
||||
final percentage = dept['percentage_overlap'] as num;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 32, top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.location_on, size: 16, color: Colors.orange[700]),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${dept['nom_dept']} (${dept['code_dept']})',
|
||||
style: TextStyle(color: Colors.orange[900]),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${percentage.toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange[900],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatisticsSection() {
|
||||
final List<Widget> statisticWidgets = [];
|
||||
|
||||
// Titre de la section
|
||||
statisticWidgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(
|
||||
'Statistiques des passages',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// CREATE : passages créés et intégrés
|
||||
if (actionType == SectorActionType.create) {
|
||||
final passagesCreated = statistics['passages_created'] ?? 0;
|
||||
final passagesIntegrated = statistics['passages_integrated'] ?? 0;
|
||||
final totalPassages = passagesCreated + passagesIntegrated;
|
||||
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.add_circle_outline,
|
||||
label: 'Nouveaux passages créés',
|
||||
value: passagesCreated,
|
||||
color: Colors.green,
|
||||
));
|
||||
|
||||
if (passagesIntegrated > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.merge_type,
|
||||
label: 'Passages orphelins intégrés',
|
||||
value: passagesIntegrated,
|
||||
color: Colors.blue,
|
||||
));
|
||||
}
|
||||
|
||||
statisticWidgets.add(const Divider(height: 24));
|
||||
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.functions,
|
||||
label: 'Total des passages du secteur',
|
||||
value: totalPassages,
|
||||
color: Colors.indigo,
|
||||
isBold: true,
|
||||
));
|
||||
}
|
||||
|
||||
// UPDATE : passages créés, mis à jour, orphelins, total
|
||||
else if (actionType == SectorActionType.update) {
|
||||
final passagesCreated = statistics['passages_created'] ?? 0;
|
||||
final passagesUpdated = statistics['passages_updated'] ?? 0;
|
||||
final passagesOrphaned = statistics['passages_orphaned'] ?? 0;
|
||||
final passagesTotal = statistics['passages_total'] ?? 0;
|
||||
|
||||
if (passagesCreated > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.add_circle_outline,
|
||||
label: 'Nouveaux passages créés',
|
||||
value: passagesCreated,
|
||||
color: Colors.green,
|
||||
));
|
||||
}
|
||||
|
||||
if (passagesUpdated > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.update,
|
||||
label: 'Passages mis à jour',
|
||||
value: passagesUpdated,
|
||||
color: Colors.blue,
|
||||
));
|
||||
}
|
||||
|
||||
if (passagesOrphaned > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.remove_circle_outline,
|
||||
label: 'Passages mis en orphelin',
|
||||
value: passagesOrphaned,
|
||||
color: Colors.orange,
|
||||
));
|
||||
}
|
||||
|
||||
statisticWidgets.add(const Divider(height: 24));
|
||||
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.functions,
|
||||
label: 'Total des passages du secteur',
|
||||
value: passagesTotal,
|
||||
color: Colors.indigo,
|
||||
isBold: true,
|
||||
));
|
||||
}
|
||||
|
||||
// DELETE : passages supprimés et conservés
|
||||
else if (actionType == SectorActionType.delete) {
|
||||
final passagesDeleted = statistics['passages_deleted'] ?? 0;
|
||||
final passagesReassigned = statistics['passages_reassigned'] ?? 0;
|
||||
|
||||
if (passagesDeleted > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.delete_outline,
|
||||
label: 'Passages supprimés',
|
||||
value: passagesDeleted,
|
||||
color: Colors.red,
|
||||
));
|
||||
}
|
||||
|
||||
if (passagesReassigned > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.bookmark_border,
|
||||
label: 'Passages conservés (orphelins)',
|
||||
value: passagesReassigned,
|
||||
color: Colors.orange,
|
||||
));
|
||||
}
|
||||
|
||||
if (passagesDeleted == 0 && passagesReassigned == 0) {
|
||||
statisticWidgets.add(
|
||||
Text(
|
||||
'Aucun passage dans ce secteur',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: statisticWidgets,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required int value,
|
||||
required Color color,
|
||||
bool isBold = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value.toString(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
fontSize: isBold ? 18 : 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
423
app/lib/presentation/dialogs/sector_dialog.dart
Normal file
423
app/lib/presentation/dialogs/sector_dialog.dart
Normal file
@@ -0,0 +1,423 @@
|
||||
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/repositories/membre_repository.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) 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();
|
||||
Color _selectedColor = Colors.blue;
|
||||
final List<int> _selectedMemberIds = [];
|
||||
bool _isLoading = false;
|
||||
bool _membersLoaded = false;
|
||||
|
||||
@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]: membreId=${us.id}, 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
|
||||
setState(() {
|
||||
_selectedMemberIds.clear();
|
||||
for (final userSector in userSectors) {
|
||||
// userSector.id est l'ID du membre (pas de l'utilisateur)
|
||||
_selectedMemberIds.add(userSector.id);
|
||||
debugPrint('Membre présélectionné: ${userSector.firstName} ${userSector.name} (membreId: ${userSector.id}, 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(() {
|
||||
_membersLoaded = true;
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du chargement des membres du secteur: $e');
|
||||
setState(() {
|
||||
_membersLoaded = true; // Même en cas d'erreur
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_nameFocusNode.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()}';
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
// 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() {
|
||||
// Liste de couleurs prédéfinies
|
||||
final List<Color> colors = [
|
||||
Colors.red,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
Colors.indigo,
|
||||
Colors.blue,
|
||||
Colors.lightBlue,
|
||||
Colors.cyan,
|
||||
Colors.teal,
|
||||
Colors.green,
|
||||
Colors.lightGreen,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
const Color(0xFF1E88E5), // Bleu personnalisé
|
||||
const Color(0xFF43A047), // Vert personnalisé
|
||||
const Color(0xFFE53935), // Rouge personnalisé
|
||||
const Color(0xFFFFB300), // Ambre personnalisé
|
||||
const Color(0xFF8E24AA), // Violet personnalisé
|
||||
];
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Choisir une couleur'),
|
||||
content: Container(
|
||||
width: double.maxFinite,
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: colors.length,
|
||||
itemBuilder: (context, index) {
|
||||
final color = colors[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedColor = color;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(
|
||||
color: _selectedColor == color ? Colors.black : Colors.grey,
|
||||
width: _selectedColor == color ? 3 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentAmicale = CurrentAmicaleService.instance.currentAmicale;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(widget.existingSector == null ? 'Nouveau secteur' : 'Modifier le secteur'),
|
||||
content: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 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),
|
||||
|
||||
// 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),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentAmicale != null)
|
||||
ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: Hive.box<MembreModel>(AppKeys.membresBoxName).listenable(),
|
||||
builder: (context, box, _) {
|
||||
debugPrint('=== Build liste membres - IDs présélectionnés: $_selectedMemberIds ===');
|
||||
|
||||
final membres = box.values
|
||||
.where((m) => m.fkEntite == currentAmicale.id)
|
||||
.toList();
|
||||
|
||||
if (membres.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Aucun membre disponible'),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: membres.length,
|
||||
itemBuilder: (context, index) {
|
||||
final membre = membres[index];
|
||||
final isSelected = _selectedMemberIds.contains(membre.id);
|
||||
|
||||
// Log pour debug
|
||||
if (index < 3) { // Limiter les logs aux 3 premiers membres
|
||||
debugPrint('Membre ${index}: ${membre.firstName} ${membre.name} (ID: ${membre.id}) - isSelected: $isSelected');
|
||||
}
|
||||
|
||||
return CheckboxListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0),
|
||||
title: Text(
|
||||
'${membre.firstName} ${membre.name}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
value: isSelected,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedMemberIds.add(membre.id);
|
||||
} else {
|
||||
_selectedMemberIds.remove(membre.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user