feat(#95): implémenter la gestion Relais admin et le rattachement gestionnaire

Ajoute la section Paramètres territoriaux avec CRUD Relais, modale de saisie structurée, états visuels harmonisés, et rattachement d'un relais principal aux gestionnaires via l'API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-21 20:06:17 +01:00
co-authored by Cursor
parent 135c7c2255
commit fbafef8f2c
10 changed files with 1652 additions and 68 deletions
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/relais_model.dart';
import 'package:p_tits_pas/models/user.dart';
import 'package:p_tits_pas/services/relais_service.dart';
import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/gestionnaire_card.dart';
class GestionnaireManagementWidget extends StatefulWidget {
const GestionnaireManagementWidget({Key? key}) : super(key: key);
@@ -16,6 +17,7 @@ class _GestionnaireManagementWidgetState
bool _isLoading = false;
String? _error;
List<AppUser> _gestionnaires = [];
List<RelaisModel> _relais = [];
List<AppUser> _filteredGestionnaires = [];
final TextEditingController _searchController = TextEditingController();
@@ -38,11 +40,19 @@ class _GestionnaireManagementWidgetState
_error = null;
});
try {
final list = await UserService.getGestionnaires();
final gestionnaires = await UserService.getGestionnaires();
List<RelaisModel> relais = [];
try {
relais = await RelaisService.getRelais();
} catch (_) {
// L'ecran reste utilisable meme si la route Relais n'est pas disponible.
}
if (!mounted) return;
setState(() {
_gestionnaires = list;
_filteredGestionnaires = list;
_gestionnaires = gestionnaires;
_relais = relais;
_filteredGestionnaires = gestionnaires;
_isLoading = false;
});
} catch (e) {
@@ -65,6 +75,84 @@ class _GestionnaireManagementWidgetState
});
}
Future<void> _openRelaisAssignmentDialog(AppUser user) async {
String? selectedRelaisId = user.relaisId;
final saved = await showDialog<bool>(
context: context,
builder: (ctx) {
return StatefulBuilder(
builder: (context, setStateDialog) {
return AlertDialog(
title: Text(
'Rattacher ${user.fullName.isEmpty ? user.email : user.fullName}',
),
content: DropdownButtonFormField<String?>(
value: selectedRelaisId,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Relais principal',
border: OutlineInputBorder(),
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('Aucun relais'),
),
..._relais.map(
(relais) => DropdownMenuItem<String?>(
value: relais.id,
child: Text(relais.nom),
),
),
],
onChanged: (value) {
setStateDialog(() {
selectedRelaisId = value;
});
},
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Enregistrer'),
),
],
);
},
);
},
);
if (saved != true) return;
try {
await UserService.updateGestionnaireRelais(
gestionnaireId: user.id,
relaisId: selectedRelaisId,
);
if (!mounted) return;
await _loadGestionnaires();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Rattachement relais mis a jour.')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
e.toString().replaceAll('Exception: ', ''),
),
backgroundColor: Colors.red.shade600,
),
);
}
}
@override
Widget build(BuildContext context) {
return Padding(
@@ -72,14 +160,13 @@ class _GestionnaireManagementWidgetState
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 🔹 Barre du haut avec bouton
Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: "Rechercher un gestionnaire...",
hintText: 'Rechercher un gestionnaire...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
@@ -88,35 +175,70 @@ class _GestionnaireManagementWidgetState
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: () {
// TODO: Rediriger vers la page de création
// TODO: Rediriger vers la page de creation.
},
icon: const Icon(Icons.add),
label: const Text("Créer un gestionnaire"),
label: const Text('Creer un gestionnaire'),
),
],
),
const SizedBox(height: 24),
// 🔹 Liste des gestionnaires
if (_isLoading)
const Center(child: CircularProgressIndicator())
else if (_error != null)
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
Center(
child: Text(
'Erreur: $_error',
style: const TextStyle(color: Colors.red),
),
)
else if (_filteredGestionnaires.isEmpty)
const Center(child: Text("Aucun gestionnaire trouvé."))
const Center(child: Text('Aucun gestionnaire trouve.'))
else
Expanded(
child: ListView.builder(
itemCount: _filteredGestionnaires.length,
itemBuilder: (context, index) {
final user = _filteredGestionnaires[index];
return GestionnaireCard(
name: user.fullName.isNotEmpty ? user.fullName : "Sans nom",
email: user.email,
return Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: CircleAvatar(
child: Text(
(user.prenom?.isNotEmpty == true
? user.prenom!.substring(0, 1)
: user.email.substring(0, 1))
.toUpperCase(),
),
),
title: Text(
user.fullName.isNotEmpty ? user.fullName : 'Sans nom',
),
subtitle: Text(
'${user.email} • Relais: ${user.relaisNom ?? 'Non rattache'}',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.location_city_outlined),
tooltip: 'Rattacher un relais',
onPressed: () => _openRelaisAssignmentDialog(user),
),
IconButton(
icon: const Icon(Icons.edit),
tooltip: 'Modifier',
onPressed: () {
// TODO: Modifier gestionnaire.
},
),
],
),
),
);
},
),
)
),
],
),
);