Files
geo/app/lib/presentation/dialogs/sector_dialog.dart
pierre 599b9fcda0 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>
2025-08-07 11:01:45 +02:00

423 lines
15 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/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'),
),
],
);
}
}