fix(mobile): URL changement mdp corrigée + v3.0 — multi-org, AppAuth, sécurité prod
Auth: - profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password Multi-org (Phase 3): - OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry - org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role Navigation: - MorePage: navigation conditionnelle par typeOrganisation - Suppression adaptive_navigation (remplacé par main_navigation_layout) Auth AppAuth: - keycloak_webview_auth_service: fixes AppAuth Android - AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet Onboarding: - Nouveaux états: payment_method_page, onboarding_shared_widgets - SouscriptionStatusModel mis à jour StatutValidationSouscription Android: - build.gradle: ProGuard/R8, network_security_config - Gradle wrapper mis à jour
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_v2.dart';
|
||||
import '../../../../shared/design_system/components/uf_app_bar.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
import '../../../../features/organizations/domain/repositories/organization_repository.dart';
|
||||
|
||||
/// Annuaire des Membres - Design UnionFlow
|
||||
class MembersPageWithDataAndPagination extends StatefulWidget {
|
||||
@@ -14,6 +16,16 @@ class MembersPageWithDataAndPagination extends StatefulWidget {
|
||||
final VoidCallback onRefresh;
|
||||
final void Function(String? query)? onSearch;
|
||||
final VoidCallback? onAddMember;
|
||||
/// null = SUPER_ADMIN (vue globale, affiche l'organisation sur chaque carte)
|
||||
final String? organisationId;
|
||||
/// Callback déclenché quand l'admin active un membre en attente
|
||||
final void Function(String memberId)? onActivateMember;
|
||||
/// Callback déclenché quand l'admin réinitialise le mot de passe d'un membre
|
||||
final void Function(String memberId)? onResetPassword;
|
||||
/// Callback déclenché quand le superadmin affecte un membre à une organisation
|
||||
final void Function(String memberId, String organisationId)? onAffecterOrganisation;
|
||||
/// Callback pour les actions de cycle de vie adhésion (admin org)
|
||||
final void Function(String memberId, String action, String? motif)? onLifecycleAction;
|
||||
|
||||
const MembersPageWithDataAndPagination({
|
||||
super.key,
|
||||
@@ -25,6 +37,11 @@ class MembersPageWithDataAndPagination extends StatefulWidget {
|
||||
required this.onRefresh,
|
||||
this.onSearch,
|
||||
this.onAddMember,
|
||||
this.organisationId,
|
||||
this.onActivateMember,
|
||||
this.onResetPassword,
|
||||
this.onAffecterOrganisation,
|
||||
this.onLifecycleAction,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -37,6 +54,11 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
String _filterStatus = 'Tous';
|
||||
Timer? _searchDebounce;
|
||||
|
||||
// Organisations pour le picker d'affectation (superadmin)
|
||||
List<Map<String, String>> _organisationsPicker = [];
|
||||
|
||||
bool get _isSuperAdmin => widget.organisationId == null;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchDebounce?.cancel();
|
||||
@@ -74,51 +96,55 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
);
|
||||
}
|
||||
|
||||
// ── Header ────────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildHeader() {
|
||||
final activeCount = widget.members.where((m) => m['status'] == 'Actif').length;
|
||||
final pendingCount = widget.members.where((m) => m['status'] == 'En attente').length;
|
||||
final pageMembers = widget.members;
|
||||
final activeCount = pageMembers.where((m) => m['status'] == 'Actif').length;
|
||||
final pendingCount = pageMembers.where((m) => m['status'] == 'En attente').length;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1),
|
||||
),
|
||||
border: Border(bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _buildStatBadge('Total', widget.totalCount.toString(), UnionFlowColors.unionGreen)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildStatBadge('Actifs', activeCount.toString(), UnionFlowColors.success)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildStatBadge('Attente', pendingCount.toString(), UnionFlowColors.warning)),
|
||||
Expanded(child: _buildStatBadge('Total', widget.totalCount.toString(), UnionFlowColors.unionGreen, subtitle: 'global')),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _buildStatBadge('Actifs', activeCount.toString(), UnionFlowColors.success, subtitle: 'cette page')),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _buildStatBadge('Attente', pendingCount.toString(), UnionFlowColors.warning, subtitle: 'cette page')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatBadge(String label, String value, Color color) {
|
||||
Widget _buildStatBadge(String label, String value, Color color, {String? subtitle}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
color: color.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 1),
|
||||
border: Border.all(color: color.withOpacity(0.25), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: color)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: color)),
|
||||
Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color)),
|
||||
if (subtitle != null)
|
||||
Text(subtitle, style: TextStyle(fontSize: 9, color: color.withOpacity(0.6))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Recherche + Filtres ────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildSearchAndFilters() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
border: Border(bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)),
|
||||
@@ -136,7 +162,7 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
},
|
||||
style: const TextStyle(fontSize: 14, color: UnionFlowColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher un membre...',
|
||||
hintText: 'Nom, email, numéro membre...',
|
||||
hintStyle: const TextStyle(fontSize: 13, color: UnionFlowColors.textTertiary),
|
||||
prefixIcon: const Icon(Icons.search, size: 20, color: UnionFlowColors.textSecondary),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
@@ -150,24 +176,15 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: UnionFlowColors.border.withOpacity(0.3)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: UnionFlowColors.border.withOpacity(0.3)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: UnionFlowColors.unionGreen, width: 1.5),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 11, horizontal: 14),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: UnionFlowColors.border.withOpacity(0.3))),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: UnionFlowColors.border.withOpacity(0.3))),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: UnionFlowColors.unionGreen, width: 1.5)),
|
||||
filled: true,
|
||||
fillColor: UnionFlowColors.surfaceVariant.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 10),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
@@ -191,11 +208,12 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
final isSelected = _filterStatus == label;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _filterStatus = label),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? UnionFlowColors.unionGreen : UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: isSelected ? UnionFlowColors.unionGreen : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: isSelected ? UnionFlowColors.unionGreen : UnionFlowColors.border, width: 1),
|
||||
),
|
||||
child: Text(
|
||||
@@ -210,11 +228,14 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
);
|
||||
}
|
||||
|
||||
// ── Liste ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildMembersList() {
|
||||
final filtered = widget.members.where((m) {
|
||||
final matchesSearch = _searchQuery.isEmpty ||
|
||||
m['name']!.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
(m['email']?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false);
|
||||
(m['name'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
(m['email'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
(m['numeroMembre'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
final matchesStatus = _filterStatus == 'Tous' || m['status'] == _filterStatus;
|
||||
return matchesSearch && matchesStatus;
|
||||
}).toList();
|
||||
@@ -225,7 +246,7 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
onRefresh: () async => widget.onRefresh(),
|
||||
color: UnionFlowColors.unionGreen,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
|
||||
itemCount: filtered.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||
itemBuilder: (context, index) => _buildMemberCard(filtered[index]),
|
||||
@@ -233,11 +254,17 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
);
|
||||
}
|
||||
|
||||
// ── Carte membre ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildMemberCard(Map<String, dynamic> member) {
|
||||
final String? orgName = member['organisationNom'] as String?;
|
||||
final String? numero = member['numeroMembre'] as String?;
|
||||
final String status = member['status'] as String? ?? '?';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showMemberDetails(member),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
@@ -245,104 +272,183 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Avatar
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient, shape: BoxShape.circle),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
member['initiales'] ?? '??',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13),
|
||||
member['initiales'] as String? ?? '??',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Infos principales
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
member['name'] ?? 'Inconnu',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: UnionFlowColors.textPrimary),
|
||||
// Nom + numéro
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
member['name'] as String? ?? 'Inconnu',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: UnionFlowColors.textPrimary),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (numero != null && numero.isNotEmpty) ...[
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
numero,
|
||||
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: UnionFlowColors.textTertiary),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Rôle
|
||||
Text(
|
||||
member['role'] ?? 'Membre',
|
||||
member['role'] as String? ?? 'Membre',
|
||||
style: const TextStyle(fontSize: 12, color: UnionFlowColors.textSecondary),
|
||||
),
|
||||
if (member['email'] != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
// Organisation (SUPER_ADMIN uniquement)
|
||||
if (_isSuperAdmin && orgName != null && orgName.isNotEmpty) ...[
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.email_outlined, size: 12, color: UnionFlowColors.textTertiary),
|
||||
const Icon(Icons.business_outlined, size: 11, color: UnionFlowColors.unionGreen),
|
||||
const SizedBox(width: 4),
|
||||
Text(member['email']!, style: const TextStyle(fontSize: 11, color: UnionFlowColors.textTertiary)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
orgName,
|
||||
style: const TextStyle(fontSize: 11, color: UnionFlowColors.unionGreen, fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
// Email
|
||||
if (member['email'] != null) ...[
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.email_outlined, size: 11, color: UnionFlowColors.textTertiary),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
member['email'] as String,
|
||||
style: const TextStyle(fontSize: 11, color: UnionFlowColors.textTertiary),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStatusBadge(member['status']),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatusBadge(status),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(String? status) {
|
||||
Color color;
|
||||
switch (status) {
|
||||
case 'Actif':
|
||||
color = UnionFlowColors.success;
|
||||
break;
|
||||
case 'Inactif':
|
||||
color = UnionFlowColors.error;
|
||||
break;
|
||||
case 'En attente':
|
||||
color = UnionFlowColors.warning;
|
||||
break;
|
||||
default:
|
||||
color = UnionFlowColors.textSecondary;
|
||||
}
|
||||
Widget _buildStatusBadge(String status) {
|
||||
final (Color color, IconData icon) = switch (status) {
|
||||
'Actif' => (UnionFlowColors.success, Icons.check_circle_outline),
|
||||
'Inactif' => (UnionFlowColors.error, Icons.cancel_outlined),
|
||||
'En attente' => (UnionFlowColors.warning, Icons.schedule_outlined),
|
||||
'Suspendu' => (const Color(0xFF9E9E9E), Icons.block_outlined),
|
||||
_ => (UnionFlowColors.textSecondary, Icons.help_outline),
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Text(status ?? '?', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(color: UnionFlowColors.unionGreenPale, shape: BoxShape.circle),
|
||||
child: const Icon(Icons.people_outline, size: 40, color: UnionFlowColors.unionGreen),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Aucun membre trouvé',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_searchQuery.isEmpty ? 'Changez vos filtres' : 'Essayez une autre recherche',
|
||||
style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary),
|
||||
),
|
||||
Icon(icon, size: 11, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(status, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── État vide ─────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
final hasActiveFilters = _filterStatus != 'Tous' || _searchQuery.isNotEmpty;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: const BoxDecoration(color: UnionFlowColors.unionGreenPale, shape: BoxShape.circle),
|
||||
child: Icon(
|
||||
hasActiveFilters ? Icons.filter_list_off : Icons.people_outline,
|
||||
size: 40,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
hasActiveFilters ? 'Aucun résultat' : 'Aucun membre',
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
hasActiveFilters
|
||||
? 'Modifiez la recherche ou le filtre de statut'
|
||||
: 'Aucun membre enregistré pour le moment',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary),
|
||||
),
|
||||
if (hasActiveFilters) ...[
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_filterStatus = 'Tous';
|
||||
_searchQuery = '';
|
||||
_searchController.clear();
|
||||
});
|
||||
widget.onSearch?.call(null);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UnionFlowColors.unionGreen),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text('Effacer les filtres', style: TextStyle(fontSize: 13, color: UnionFlowColors.unionGreen, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Pagination ────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildPagination() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
border: Border(top: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)),
|
||||
@@ -354,10 +460,7 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
icon: const Icon(Icons.chevron_left, size: 24),
|
||||
color: widget.currentPage > 0 ? UnionFlowColors.unionGreen : UnionFlowColors.textTertiary,
|
||||
onPressed: widget.currentPage > 0
|
||||
? () => widget.onPageChanged(
|
||||
widget.currentPage - 1,
|
||||
_searchQuery.isEmpty ? null : _searchQuery,
|
||||
)
|
||||
? () => widget.onPageChanged(widget.currentPage - 1, _searchQuery.isEmpty ? null : _searchQuery)
|
||||
: null,
|
||||
),
|
||||
Container(
|
||||
@@ -372,10 +475,7 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
icon: const Icon(Icons.chevron_right, size: 24),
|
||||
color: widget.currentPage < widget.totalPages - 1 ? UnionFlowColors.unionGreen : UnionFlowColors.textTertiary,
|
||||
onPressed: widget.currentPage < widget.totalPages - 1
|
||||
? () => widget.onPageChanged(
|
||||
widget.currentPage + 1,
|
||||
_searchQuery.isEmpty ? null : _searchQuery,
|
||||
)
|
||||
? () => widget.onPageChanged(widget.currentPage + 1, _searchQuery.isEmpty ? null : _searchQuery)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
@@ -383,61 +483,434 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
);
|
||||
}
|
||||
|
||||
// ── Sheet détail membre ───────────────────────────────────────────────────
|
||||
|
||||
void _showMemberDetails(Map<String, dynamic> member) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient, shape: BoxShape.circle),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
member['initiales'] ?? '??',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 22),
|
||||
isScrollControlled: true,
|
||||
builder: (context) => DraggableScrollableSheet(
|
||||
initialChildSize: 0.55,
|
||||
minChildSize: 0.4,
|
||||
maxChildSize: 0.85,
|
||||
expand: false,
|
||||
builder: (context, scrollController) => Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Drag handle
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(color: UnionFlowColors.border, borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avatar + nom + statut
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient, shape: BoxShape.circle),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
member['initiales'] as String? ?? '??',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 22),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
member['name'] as String? ?? '',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
member['role'] as String? ?? 'Membre',
|
||||
style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary),
|
||||
),
|
||||
if (member['numeroMembre'] != null) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
member['numeroMembre'] as String,
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: UnionFlowColors.textTertiary),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStatusBadge(member['status'] as String? ?? '?'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 12),
|
||||
// Infos contact
|
||||
_buildSectionTitle('Contact'),
|
||||
_buildDetailRow(Icons.email_outlined, 'Email', member['email'] as String? ?? '—'),
|
||||
if ((member['phone'] as String? ?? '').isNotEmpty)
|
||||
_buildDetailRow(Icons.phone_outlined, 'Téléphone', member['phone'] as String),
|
||||
const SizedBox(height: 12),
|
||||
// Infos organisation
|
||||
if (member['organisationNom'] != null || member['organisationId'] != null) ...[
|
||||
_buildSectionTitle('Organisation'),
|
||||
if (member['organisationNom'] != null)
|
||||
_buildDetailRow(Icons.business_outlined, 'Organisation', member['organisationNom'] as String),
|
||||
if (member['dateAdhesion'] != null)
|
||||
_buildDetailRow(Icons.calendar_today_outlined, 'Adhésion', _formatDate(member['dateAdhesion'])),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
// Infos pro
|
||||
if ((member['department'] as String? ?? '').isNotEmpty) ...[
|
||||
_buildSectionTitle('Profil'),
|
||||
_buildDetailRow(Icons.work_outline, 'Profession', member['department'] as String),
|
||||
if ((member['nationalite'] as String? ?? '').isNotEmpty)
|
||||
_buildDetailRow(Icons.flag_outlined, 'Nationalité', member['nationalite'] as String),
|
||||
if ((member['location'] as String? ?? ', ').trim() != ',')
|
||||
_buildDetailRow(Icons.location_on_outlined, 'Localisation', member['location'] as String),
|
||||
],
|
||||
// Bouton d'activation (ADMIN_ORGANISATION uniquement, membre en attente)
|
||||
if (member['status'] == 'En attente' && widget.onActivateMember != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onActivateMember!(member['id'] as String);
|
||||
},
|
||||
icon: const Icon(Icons.check_circle_outline, size: 18),
|
||||
label: const Text('Activer le membre', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UnionFlowColors.success,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// Bouton reset mot de passe (tous membres avec compte Keycloak)
|
||||
if (widget.onResetPassword != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onResetPassword!(member['id'] as String);
|
||||
},
|
||||
icon: const Icon(Icons.lock_reset_outlined, size: 18),
|
||||
label: const Text('Réinitialiser le mot de passe', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UnionFlowColors.warning,
|
||||
side: const BorderSide(color: UnionFlowColors.warning),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// Bouton affectation organisation (superadmin, membre sans organisation)
|
||||
if (_isSuperAdmin &&
|
||||
widget.onAffecterOrganisation != null &&
|
||||
(member['organisationId'] == null || (member['organisationId'] as String).isEmpty)) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _showAffecterOrganisationDialog(context, member),
|
||||
icon: const Icon(Icons.business_outlined, size: 18),
|
||||
label: const Text('Affecter à une organisation', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UnionFlowColors.unionGreen,
|
||||
side: const BorderSide(color: UnionFlowColors.unionGreen),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// ── Boutons cycle de vie adhésion (ADMIN_ORGANISATION) ──
|
||||
if (!_isSuperAdmin && widget.onLifecycleAction != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 8),
|
||||
_buildLifecycleActionsSection(context, member),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLifecycleActionsSection(BuildContext context, Map<String, dynamic> member) {
|
||||
final memberId = member['id'] as String? ?? '';
|
||||
final statut = member['statutMembre'] as String? ?? member['status'] as String? ?? '';
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Text('Actions adhésion', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.grey)),
|
||||
),
|
||||
// Activer (INVITE ou EN_ATTENTE_VALIDATION)
|
||||
if (statut == 'INVITE' || statut == 'EN_ATTENTE_VALIDATION' || statut == 'En attente')
|
||||
_lifecycleButton(
|
||||
context: context,
|
||||
label: 'Activer l\'adhésion',
|
||||
icon: Icons.check_circle_outline,
|
||||
color: Colors.green,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onLifecycleAction!(memberId, 'activer', null);
|
||||
},
|
||||
),
|
||||
// Suspendre (ACTIF)
|
||||
if (statut == 'ACTIF' || statut == 'Actif')
|
||||
_lifecycleButton(
|
||||
context: context,
|
||||
label: 'Suspendre l\'adhésion',
|
||||
icon: Icons.pause_circle_outline,
|
||||
color: Colors.orange,
|
||||
outlined: true,
|
||||
onPressed: () => _showMotifDialog(
|
||||
context,
|
||||
'Suspendre l\'adhésion',
|
||||
onConfirm: (motif) => widget.onLifecycleAction!(memberId, 'suspendre', motif),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
member['name'] ?? '',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary),
|
||||
),
|
||||
// Réactiver (SUSPENDU)
|
||||
if (statut == 'SUSPENDU' || statut == 'Suspendu')
|
||||
_lifecycleButton(
|
||||
context: context,
|
||||
label: 'Réactiver l\'adhésion',
|
||||
icon: Icons.play_circle_outline,
|
||||
color: Colors.blue,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onLifecycleAction!(memberId, 'activer', null);
|
||||
},
|
||||
),
|
||||
// Radier (tout statut actif)
|
||||
if (statut != 'RADIE' && statut != 'ARCHIVE' && statut.isNotEmpty)
|
||||
_lifecycleButton(
|
||||
context: context,
|
||||
label: 'Radier de l\'organisation',
|
||||
icon: Icons.block_outlined,
|
||||
color: Colors.red,
|
||||
outlined: true,
|
||||
onPressed: () => _showMotifDialog(
|
||||
context,
|
||||
'Radier le membre',
|
||||
onConfirm: (motif) => widget.onLifecycleAction!(memberId, 'radier', motif),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
member['role'] ?? '',
|
||||
style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _lifecycleButton({
|
||||
required BuildContext context,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
bool outlined = false,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: outlined
|
||||
? OutlinedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: 18),
|
||||
label: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: color,
|
||||
side: BorderSide(color: color),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
)
|
||||
: ElevatedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: 18),
|
||||
label: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: color,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMotifDialog(
|
||||
BuildContext context,
|
||||
String titre, {
|
||||
required void Function(String? motif) onConfirm,
|
||||
}) {
|
||||
final motifCtrl = TextEditingController();
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(titre),
|
||||
content: TextField(
|
||||
controller: motifCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Motif (optionnel)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx); // ferme dialog motif
|
||||
Navigator.pop(context); // ferme bottom sheet
|
||||
onConfirm(motifCtrl.text.isNotEmpty ? motifCtrl.text : null);
|
||||
},
|
||||
child: const Text('Confirmer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showAffecterOrganisationDialog(
|
||||
BuildContext ctx,
|
||||
Map<String, dynamic> member,
|
||||
) async {
|
||||
// Charger les organisations si pas encore fait
|
||||
if (_organisationsPicker.isEmpty) {
|
||||
try {
|
||||
final repo = GetIt.instance<IOrganizationRepository>();
|
||||
final orgs = await repo.getOrganizations(page: 0, size: 100);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_organisationsPicker = orgs
|
||||
.where((o) => o.id != null && o.id!.isNotEmpty)
|
||||
.map((o) => {'id': o.id!, 'nom': o.nomAffichage})
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
String? selectedOrgId;
|
||||
|
||||
await showDialog<void>(
|
||||
context: ctx,
|
||||
builder: (dialogCtx) => StatefulBuilder(
|
||||
builder: (dialogCtx, setDialogState) => AlertDialog(
|
||||
title: const Text('Affecter à une organisation'),
|
||||
content: _organisationsPicker.isEmpty
|
||||
? const Text('Aucune organisation disponible.')
|
||||
: DropdownButtonFormField<String>(
|
||||
decoration: const InputDecoration(labelText: 'Organisation'),
|
||||
items: _organisationsPicker
|
||||
.map((o) => DropdownMenuItem<String>(
|
||||
value: o['id'],
|
||||
child: Text(o['nom']!),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (v) => setDialogState(() => selectedOrgId = v),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogCtx),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: selectedOrgId == null
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(dialogCtx);
|
||||
Navigator.pop(ctx); // ferme le bottom sheet
|
||||
widget.onAffecterOrganisation!(
|
||||
member['id'] as String,
|
||||
selectedOrgId!,
|
||||
);
|
||||
},
|
||||
child: const Text('Confirmer'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(Icons.email_outlined, member['email'] ?? 'Non fourni'),
|
||||
_buildInfoRow(Icons.phone_outlined, member['phone'] ?? 'Non fourni'),
|
||||
_buildInfoRow(Icons.location_on_outlined, member['location'] ?? 'Non renseigné'),
|
||||
_buildInfoRow(Icons.work_outline, member['department'] ?? 'Aucun département'),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(IconData icon, String text) {
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: UnionFlowColors.textTertiary, letterSpacing: 0.8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(IconData icon, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: UnionFlowColors.unionGreen),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: UnionFlowColors.textPrimary))),
|
||||
Icon(icon, size: 16, color: UnionFlowColors.unionGreen),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 11, color: UnionFlowColors.textTertiary)),
|
||||
const SizedBox(height: 1),
|
||||
Text(value, style: const TextStyle(fontSize: 13, color: UnionFlowColors.textPrimary, fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(dynamic date) {
|
||||
if (date == null) return '—';
|
||||
if (date is DateTime) return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
||||
return date.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import '../../bloc/membres_bloc.dart';
|
||||
import '../../bloc/membres_event.dart';
|
||||
import '../../bloc/membres_state.dart';
|
||||
import '../../data/models/membre_complete_model.dart';
|
||||
import '../widgets/add_member_dialog.dart';
|
||||
import '../widgets/add_member_dialog.dart' show showAddMemberSheet, showCredentialsDialog;
|
||||
import 'members_page_connected.dart';
|
||||
|
||||
final _getIt = GetIt.instance;
|
||||
@@ -55,55 +55,14 @@ class MembersPageConnected extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<MembresBloc, MembresState>(
|
||||
listener: (context, state) {
|
||||
// Après création : afficher le mot de passe temporaire si disponible, puis recharger
|
||||
// Après création : recharger la liste (la dialog mot de passe est gérée dans AddMemberDialog)
|
||||
if (state is MembreCreated) {
|
||||
final motDePasse = state.membre.motDePasseTemporaire;
|
||||
if (motDePasse != null && motDePasse.isNotEmpty) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Compte créé avec succès'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Le membre ${state.membre.nomComplet} a été créé.'),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Mot de passe temporaire :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(
|
||||
motDePasse,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Communiquez ce mot de passe au membre. Il devra le changer à sa première connexion.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(_).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
context.read<MembresBloc>().add(LoadMembres(refresh: true, organisationId: organisationId));
|
||||
}
|
||||
|
||||
// Gestion des erreurs avec SnackBar
|
||||
if (state is MembresError) {
|
||||
final bloc = context.read<MembresBloc>();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
@@ -113,12 +72,67 @@ class MembersPageConnected extends StatelessWidget {
|
||||
label: 'Réessayer',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
context.read<MembresBloc>().add(LoadMembres(organisationId: organisationId));
|
||||
bloc.add(LoadMembres(organisationId: organisationId));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Après activation : succès + rechargement
|
||||
if (state is MembreActivated) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Membre activé avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
context.read<MembresBloc>().add(LoadMembres(refresh: true, organisationId: organisationId));
|
||||
}
|
||||
|
||||
// Après reset mot de passe : afficher le dialog credentials
|
||||
if (state is MotDePasseReinitialise) {
|
||||
showCredentialsDialog(context, state.membre);
|
||||
}
|
||||
|
||||
// Après affectation à une organisation : succès + rechargement
|
||||
if (state is MembreAffecte) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Membre affecté à l\'organisation avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
context.read<MembresBloc>().add(LoadMembres(refresh: true, organisationId: organisationId));
|
||||
}
|
||||
|
||||
// Lifecycle adhésion : succès + rechargement
|
||||
if (state is MembreInvite) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Invitation envoyée'), backgroundColor: Colors.blue, duration: Duration(seconds: 3)),
|
||||
);
|
||||
context.read<MembresBloc>().add(LoadMembres(refresh: true, organisationId: organisationId));
|
||||
}
|
||||
if (state is AdhesionActivee) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Adhésion activée'), backgroundColor: Colors.green, duration: Duration(seconds: 3)),
|
||||
);
|
||||
context.read<MembresBloc>().add(LoadMembres(refresh: true, organisationId: organisationId));
|
||||
}
|
||||
if (state is AdhesionSuspendue) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Adhésion suspendue'), backgroundColor: Colors.orange, duration: Duration(seconds: 3)),
|
||||
);
|
||||
context.read<MembresBloc>().add(LoadMembres(refresh: true, organisationId: organisationId));
|
||||
}
|
||||
if (state is MembreRadie) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Membre radié de l\'organisation'), backgroundColor: Colors.red, duration: Duration(seconds: 3)),
|
||||
);
|
||||
context.read<MembresBloc>().add(LoadMembres(refresh: true, organisationId: organisationId));
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
@@ -178,6 +192,7 @@ class MembersPageConnected extends StatelessWidget {
|
||||
totalCount: state.totalElements,
|
||||
currentPage: state.currentPage,
|
||||
totalPages: state.totalPages,
|
||||
organisationId: organisationId,
|
||||
onPageChanged: (newPage, recherche) {
|
||||
AppLogger.userAction('Load page', data: {'page': newPage});
|
||||
context.read<MembresBloc>().add(LoadMembres(page: newPage, recherche: recherche, organisationId: organisationId));
|
||||
@@ -189,16 +204,37 @@ class MembersPageConnected extends StatelessWidget {
|
||||
onSearch: (query) {
|
||||
context.read<MembresBloc>().add(LoadMembres(page: 0, recherche: query, organisationId: organisationId));
|
||||
},
|
||||
onAddMember: () async {
|
||||
final bloc = context.read<MembresBloc>();
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: const AddMemberDialog(),
|
||||
),
|
||||
);
|
||||
onAddMember: () => showAddMemberSheet(context),
|
||||
onActivateMember: (memberId) {
|
||||
context.read<MembresBloc>().add(ActivateMembre(memberId));
|
||||
},
|
||||
onResetPassword: (memberId) {
|
||||
context.read<MembresBloc>().add(ResetMotDePasse(memberId));
|
||||
},
|
||||
onAffecterOrganisation: organisationId == null
|
||||
? (memberId, orgId) {
|
||||
context.read<MembresBloc>().add(AffecterOrganisation(memberId, orgId));
|
||||
}
|
||||
: null,
|
||||
onLifecycleAction: organisationId != null
|
||||
? (memberId, action, motif) {
|
||||
final bloc = context.read<MembresBloc>();
|
||||
switch (action) {
|
||||
case 'inviter':
|
||||
bloc.add(InviterMembre(membreId: memberId, organisationId: organisationId!));
|
||||
break;
|
||||
case 'activer':
|
||||
bloc.add(ActiverAdhesion(membreId: memberId, organisationId: organisationId!, motif: motif));
|
||||
break;
|
||||
case 'suspendre':
|
||||
bloc.add(SuspendrAdhesion(membreId: memberId, organisationId: organisationId!, motif: motif));
|
||||
break;
|
||||
case 'radier':
|
||||
bloc.add(RadierAdhesion(membreId: memberId, organisationId: organisationId!, motif: motif));
|
||||
break;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user