refactoring
This commit is contained in:
@@ -6,6 +6,8 @@ import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'membres_event.dart';
|
||||
import 'membres_state.dart';
|
||||
import '../../../shared/models/membre_search_criteria.dart';
|
||||
import '../../../shared/models/membre_search_result.dart';
|
||||
import '../domain/usecases/get_members.dart';
|
||||
import '../domain/usecases/get_member_by_id.dart';
|
||||
import '../domain/usecases/create_member.dart' as uc;
|
||||
@@ -64,11 +66,25 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
emit(const MembresLoading());
|
||||
}
|
||||
|
||||
final result = await _getMembers(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
recherche: event.recherche,
|
||||
);
|
||||
final MembreSearchResult result;
|
||||
if (event.organisationId != null) {
|
||||
// OrgAdmin : scope la requête à son organisation via la recherche avancée
|
||||
result = await _searchMembers(
|
||||
criteria: MembreSearchCriteria(
|
||||
organisationIds: [event.organisationId!],
|
||||
query: event.recherche?.isNotEmpty == true ? event.recherche : null,
|
||||
),
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
);
|
||||
} else {
|
||||
// SuperAdmin et autres rôles : accès global sans filtre org
|
||||
result = await _getMembers(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
recherche: event.recherche,
|
||||
);
|
||||
}
|
||||
|
||||
emit(MembresLoaded(
|
||||
membres: result.membres,
|
||||
@@ -76,16 +92,19 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
currentPage: result.currentPage,
|
||||
pageSize: result.pageSize,
|
||||
totalPages: result.totalPages,
|
||||
organisationId: event.organisationId,
|
||||
));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur inattendue lors du chargement des membres: $e',
|
||||
message: 'Erreur lors du chargement des membres. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -110,14 +129,16 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
));
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors du chargement du membre: $e',
|
||||
message: 'Erreur lors du chargement du membre. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -135,6 +156,7 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
|
||||
emit(MembreCreated(membre));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
if (e.response?.statusCode == 400) {
|
||||
// Erreur de validation
|
||||
final errors = _extractValidationErrors(e.response?.data);
|
||||
@@ -151,8 +173,9 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors de la création du membre: $e',
|
||||
message: 'Erreur lors de la création du membre. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -170,6 +193,7 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
|
||||
emit(MembreUpdated(membre));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
if (e.response?.statusCode == 400) {
|
||||
final errors = _extractValidationErrors(e.response?.data);
|
||||
emit(MembresValidationError(
|
||||
@@ -185,8 +209,9 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors de la mise à jour du membre: $e',
|
||||
message: 'Erreur lors de la mise à jour du membre. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -204,14 +229,16 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
|
||||
emit(MembreDeleted(event.id));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors de la suppression du membre: $e',
|
||||
message: 'Erreur lors de la suppression du membre. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -229,14 +256,16 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
|
||||
emit(MembreActivated(membre));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors de l\'activation du membre: $e',
|
||||
message: 'Erreur lors de l\'activation du membre. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -254,14 +283,16 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
|
||||
emit(MembreDeactivated(membre));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors de la désactivation du membre: $e',
|
||||
message: 'Erreur lors de la désactivation du membre. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -289,14 +320,16 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
totalPages: result.totalPages,
|
||||
));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors de la recherche de membres: $e',
|
||||
message: 'Erreur lors de la recherche de membres. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -323,14 +356,16 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
totalPages: result.totalPages,
|
||||
));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors du chargement des membres actifs: $e',
|
||||
message: 'Erreur lors du chargement des membres actifs. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -357,14 +392,16 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
totalPages: result.totalPages,
|
||||
));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors du chargement des membres du bureau: $e',
|
||||
message: 'Erreur lors du chargement des membres du bureau. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
@@ -382,14 +419,16 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
|
||||
emit(MembresStatsLoaded(stats));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors du chargement des statistiques: $e',
|
||||
message: 'Erreur lors du chargement des statistiques. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -19,16 +19,18 @@ class LoadMembres extends MembresEvent {
|
||||
final int size;
|
||||
final String? recherche;
|
||||
final bool refresh;
|
||||
final String? organisationId;
|
||||
|
||||
const LoadMembres({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.recherche,
|
||||
this.refresh = false,
|
||||
this.organisationId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, recherche, refresh];
|
||||
List<Object?> get props => [page, size, recherche, refresh, organisationId];
|
||||
}
|
||||
|
||||
/// Événement pour charger un membre par ID
|
||||
|
||||
@@ -40,6 +40,7 @@ class MembresLoaded extends MembresState {
|
||||
final int pageSize;
|
||||
final int totalPages;
|
||||
final bool hasMore;
|
||||
final String? organisationId;
|
||||
|
||||
const MembresLoaded({
|
||||
required this.membres,
|
||||
@@ -47,10 +48,19 @@ class MembresLoaded extends MembresState {
|
||||
this.currentPage = 0,
|
||||
this.pageSize = 20,
|
||||
required this.totalPages,
|
||||
this.organisationId,
|
||||
}) : hasMore = currentPage < totalPages - 1;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membres, totalElements, currentPage, pageSize, totalPages, hasMore];
|
||||
List<Object?> get props => [
|
||||
membres,
|
||||
totalElements,
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
hasMore,
|
||||
organisationId,
|
||||
];
|
||||
|
||||
MembresLoaded copyWith({
|
||||
List<MembreCompletModel>? membres,
|
||||
@@ -58,6 +68,7 @@ class MembresLoaded extends MembresState {
|
||||
int? currentPage,
|
||||
int? pageSize,
|
||||
int? totalPages,
|
||||
String? organisationId,
|
||||
}) {
|
||||
return MembresLoaded(
|
||||
membres: membres ?? this.membres,
|
||||
@@ -65,6 +76,7 @@ class MembresLoaded extends MembresState {
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
pageSize: pageSize ?? this.pageSize,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,10 @@ class MembreCompletModel extends Equatable {
|
||||
@JsonKey(name: 'dateVerificationIdentite')
|
||||
final DateTime? dateVerificationIdentite;
|
||||
|
||||
/// Mot de passe temporaire (retourné une seule fois à la création, null sinon)
|
||||
@JsonKey(name: 'motDePasseTemporaire')
|
||||
final String? motDePasseTemporaire;
|
||||
|
||||
const MembreCompletModel({
|
||||
this.id,
|
||||
required this.nom,
|
||||
@@ -210,6 +214,7 @@ class MembreCompletModel extends Equatable {
|
||||
this.niveauVigilanceKyc,
|
||||
this.statutKyc,
|
||||
this.dateVerificationIdentite,
|
||||
this.motDePasseTemporaire,
|
||||
});
|
||||
|
||||
/// Création depuis JSON
|
||||
@@ -256,6 +261,7 @@ class MembreCompletModel extends Equatable {
|
||||
NiveauVigilanceKyc? niveauVigilanceKyc,
|
||||
StatutKyc? statutKyc,
|
||||
DateTime? dateVerificationIdentite,
|
||||
String? motDePasseTemporaire,
|
||||
}) {
|
||||
return MembreCompletModel(
|
||||
id: id ?? this.id,
|
||||
@@ -293,6 +299,7 @@ class MembreCompletModel extends Equatable {
|
||||
niveauVigilanceKyc: niveauVigilanceKyc ?? this.niveauVigilanceKyc,
|
||||
statutKyc: statutKyc ?? this.statutKyc,
|
||||
dateVerificationIdentite: dateVerificationIdentite ?? this.dateVerificationIdentite,
|
||||
motDePasseTemporaire: motDePasseTemporaire ?? this.motDePasseTemporaire,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ MembreCompletModel _$MembreCompletModelFromJson(Map<String, dynamic> json) =>
|
||||
dateVerificationIdentite: json['dateVerificationIdentite'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateVerificationIdentite'] as String),
|
||||
motDePasseTemporaire: json['motDePasseTemporaire'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MembreCompletModelToJson(MembreCompletModel instance) =>
|
||||
@@ -101,6 +102,7 @@ Map<String, dynamic> _$MembreCompletModelToJson(MembreCompletModel instance) =>
|
||||
'statutKyc': _$StatutKycEnumMap[instance.statutKyc],
|
||||
'dateVerificationIdentite':
|
||||
instance.dateVerificationIdentite?.toIso8601String(),
|
||||
'motDePasseTemporaire': instance.motDePasseTemporaire,
|
||||
};
|
||||
|
||||
const _$GenreEnumMap = {
|
||||
|
||||
@@ -49,10 +49,10 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
);
|
||||
|
||||
return _parseMembreSearchResult(response, page, size, const MembreSearchCriteria());
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la récupération des membres: ${e.message}');
|
||||
} on DioException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la récupération des membres: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +64,29 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
if (map.containsKey('organisationId') && map['organisationId'] != null && map['organisationId'] is! String) {
|
||||
map['organisationId'] = map['organisationId'].toString();
|
||||
}
|
||||
// Mapping statutCompte → statut avec normalisation des valeurs backend
|
||||
if (map.containsKey('statutCompte') && !map.containsKey('statut')) {
|
||||
map['statut'] = map['statutCompte'];
|
||||
final sc = (map['statutCompte'] as String? ?? '').toUpperCase();
|
||||
if (sc == 'ACTIF') {
|
||||
map['statut'] = 'ACTIF';
|
||||
} else if (sc == 'INACTIF') {
|
||||
map['statut'] = 'INACTIF';
|
||||
} else if (sc == 'SUSPENDU') {
|
||||
map['statut'] = 'SUSPENDU';
|
||||
} else {
|
||||
map['statut'] = 'EN_ATTENTE';
|
||||
}
|
||||
}
|
||||
if (map.containsKey('photoUrl') && !map.containsKey('photo')) {
|
||||
map['photo'] = map['photoUrl'];
|
||||
}
|
||||
// roles (List) → role (premier rôle)
|
||||
if (map.containsKey('roles') && !map.containsKey('role')) {
|
||||
final roles = map['roles'];
|
||||
if (roles is List && roles.isNotEmpty) {
|
||||
map['role'] = roles.first?.toString();
|
||||
}
|
||||
}
|
||||
if (map['id'] != null && map['id'] is! String) {
|
||||
map['id'] = map['id'].toString();
|
||||
}
|
||||
@@ -155,12 +172,13 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
throw Exception('Erreur lors de la récupération du membre: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 404) {
|
||||
return null;
|
||||
}
|
||||
throw Exception('Erreur réseau lors de la récupération du membre: ${e.message}');
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la récupération du membre: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,10 +195,10 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
} else {
|
||||
throw Exception('Erreur lors de la création du membre: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la création du membre: ${e.message}');
|
||||
} on DioException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la création du membre: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,10 +215,10 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
} else {
|
||||
throw Exception('Erreur lors de la mise à jour du membre: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la mise à jour du membre: ${e.message}');
|
||||
} on DioException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la mise à jour du membre: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,10 +230,10 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
if (response.statusCode != 204 && response.statusCode != 200) {
|
||||
throw Exception('Erreur lors de la suppression du membre: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la suppression du membre: ${e.message}');
|
||||
} on DioException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la suppression du membre: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,10 +247,10 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
} else {
|
||||
throw Exception('Erreur lors de l\'activation du membre: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de l\'activation du membre: ${e.message}');
|
||||
} on DioException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de l\'activation du membre: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +264,10 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
} else {
|
||||
throw Exception('Erreur lors de la désactivation du membre: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la désactivation du membre: ${e.message}');
|
||||
} on DioException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la désactivation du membre: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,10 +290,10 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
);
|
||||
|
||||
return _parseMembreSearchResult(response, page, size, criteria);
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la recherche de membres: ${e.message}');
|
||||
} on DioException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la recherche de membres: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,10 +333,10 @@ class MembreRepositoryImpl implements IMembreRepository {
|
||||
} else {
|
||||
throw Exception('Erreur lors de la récupération des statistiques: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la récupération des statistiques: ${e.message}');
|
||||
} on DioException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la récupération des statistiques: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../../../shared/models/membre_search_criteria.dart';
|
||||
import '../../../../shared/models/membre_search_result.dart';
|
||||
import '../../../organizations/domain/repositories/organization_repository.dart';
|
||||
@@ -115,7 +116,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Recherche Avancée'),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
bottom: TabBar(
|
||||
@@ -178,7 +179,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.flash_on, color: Theme.of(context).primaryColor),
|
||||
const Icon(Icons.flash_on, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Recherche Rapide',
|
||||
@@ -233,7 +234,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.tune, color: Theme.of(context).primaryColor),
|
||||
const Icon(Icons.tune, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Critères Détaillés',
|
||||
@@ -324,7 +325,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.filter_alt, color: Theme.of(context).primaryColor),
|
||||
const Icon(Icons.filter_alt, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Filtres Avancés',
|
||||
@@ -499,7 +500,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error, size: 64, color: Colors.red),
|
||||
const Icon(Icons.error, size: 64, color: AppColors.error),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Erreur de recherche',
|
||||
@@ -509,7 +510,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
Text(
|
||||
_errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: const TextStyle(color: AppColors.error),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
@@ -573,8 +574,8 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
return ActionChip(
|
||||
label: Text(label),
|
||||
onPressed: onTap,
|
||||
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
labelStyle: TextStyle(color: Theme.of(context).primaryColor),
|
||||
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
|
||||
labelStyle: const TextStyle(color: AppColors.primaryGreen),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -620,7 +621,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Veuillez spécifier au moins un critère de recherche'),
|
||||
backgroundColor: Colors.orange,
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -649,7 +650,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(result.resultDescription),
|
||||
backgroundColor: Colors.green,
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -661,7 +662,7 @@ class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur de recherche: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../../features/authentication/data/models/user_role.dart';
|
||||
@@ -174,13 +175,13 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
builder: (context, state) {
|
||||
if (state is! AuthAuthenticated) {
|
||||
return Container(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
color: AppColors.lightBackground,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
color: AppColors.lightBackground,
|
||||
child: _buildMembersContent(state),
|
||||
);
|
||||
},
|
||||
@@ -196,19 +197,19 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
children: [
|
||||
// Header avec titre et actions
|
||||
_buildMembersHeader(state),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Statistiques et métriques
|
||||
_buildMembersMetrics(),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Barre de recherche et filtres
|
||||
_buildSearchAndFilters(),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Onglets de catégories
|
||||
_buildCategoryTabs(),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Liste/Grille des membres
|
||||
_buildMembersDisplay(),
|
||||
@@ -265,8 +266,8 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
'Métriques & Statistiques',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7),
|
||||
fontSize: 18,
|
||||
color: AppColors.primaryGreen,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -280,7 +281,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
totalMembers.toString(),
|
||||
'+$newThisMonth ce mois',
|
||||
Icons.people,
|
||||
const Color(0xFF6C5CE7),
|
||||
AppColors.primaryGreen,
|
||||
trend: newThisMonth > 0 ? 'up' : 'stable',
|
||||
),
|
||||
),
|
||||
@@ -291,7 +292,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
activeMembers.toString(),
|
||||
'${((activeMembers / totalMembers) * 100).toStringAsFixed(1)}%',
|
||||
Icons.check_circle,
|
||||
const Color(0xFF00B894),
|
||||
AppColors.success,
|
||||
trend: 'up',
|
||||
),
|
||||
),
|
||||
@@ -308,7 +309,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
avgContribution.toStringAsFixed(0),
|
||||
'Contribution',
|
||||
Icons.trending_up,
|
||||
const Color(0xFF0984E3),
|
||||
AppColors.brandGreenLight,
|
||||
trend: 'up',
|
||||
),
|
||||
),
|
||||
@@ -319,7 +320,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
newThisMonth.toString(),
|
||||
'Ce mois',
|
||||
Icons.new_releases,
|
||||
const Color(0xFFF39C12),
|
||||
AppColors.warning,
|
||||
trend: newThisMonth > 0 ? 'up' : 'stable',
|
||||
),
|
||||
),
|
||||
@@ -388,7 +389,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
@@ -396,7 +397,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF9CA3AF),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -424,7 +425,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search, color: Color(0xFF6B7280)),
|
||||
const Icon(Icons.search, color: AppColors.textSecondaryLight),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
@@ -432,7 +433,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Rechercher par nom, email, département...',
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(color: Color(0xFF9CA3AF)),
|
||||
hintStyle: TextStyle(color: AppColors.textSecondaryLight),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@@ -449,13 +450,13 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
_searchQuery = '';
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.clear, color: Color(0xFF6B7280)),
|
||||
icon: const Icon(Icons.clear, color: AppColors.textSecondaryLight),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
height: 32,
|
||||
width: 1,
|
||||
color: const Color(0xFFE5E7EB),
|
||||
color: AppColors.lightBorder,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
@@ -466,7 +467,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
},
|
||||
icon: Icon(
|
||||
_showAdvancedFilters ? Icons.filter_list_off : Icons.filter_list,
|
||||
color: _showAdvancedFilters ? const Color(0xFF6C5CE7) : const Color(0xFF6B7280),
|
||||
color: _showAdvancedFilters ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
),
|
||||
tooltip: 'Filtres avancés',
|
||||
),
|
||||
@@ -478,7 +479,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
},
|
||||
icon: Icon(
|
||||
_isGridView ? Icons.view_list : Icons.grid_view,
|
||||
color: const Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
tooltip: _isGridView ? 'Vue liste' : 'Vue grille',
|
||||
),
|
||||
@@ -526,14 +527,14 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
});
|
||||
},
|
||||
backgroundColor: Colors.white,
|
||||
selectedColor: const Color(0xFF6C5CE7).withOpacity(0.1),
|
||||
checkmarkColor: const Color(0xFF6C5CE7),
|
||||
selectedColor: AppColors.primaryGreen.withOpacity(0.1),
|
||||
checkmarkColor: AppColors.primaryGreen,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFF6B7280),
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFFE5E7EB),
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -549,7 +550,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||||
border: Border.all(color: AppColors.lightBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -558,7 +559,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
'Filtres Avancés',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF374151),
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -589,14 +590,14 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
});
|
||||
},
|
||||
backgroundColor: Colors.grey[50],
|
||||
selectedColor: const Color(0xFF6C5CE7).withOpacity(0.1),
|
||||
checkmarkColor: const Color(0xFF6C5CE7),
|
||||
selectedColor: AppColors.primaryGreen.withOpacity(0.1),
|
||||
checkmarkColor: AppColors.primaryGreen,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFF6B7280),
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
fontSize: 12,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFFE5E7EB),
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -616,14 +617,14 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
icon: const Icon(Icons.clear_all, size: 16),
|
||||
label: const Text('Réinitialiser'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF6B7280),
|
||||
foregroundColor: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${_getFilteredMembers().length} résultat(s)',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
@@ -656,9 +657,9 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
Tab(text: 'Équipes', icon: Icon(Icons.groups, size: 18)),
|
||||
Tab(text: 'Analytics', icon: Icon(Icons.analytics, size: 18)),
|
||||
],
|
||||
labelColor: const Color(0xFF6C5CE7),
|
||||
unselectedLabelColor: const Color(0xFF6B7280),
|
||||
indicatorColor: const Color(0xFF6C5CE7),
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
indicatorWeight: 3,
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
@@ -804,7 +805,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: Color(0xFF1F2937),
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -829,7 +830,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
Text(
|
||||
member['email'],
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
@@ -844,24 +845,24 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
member['department'],
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
size: 12,
|
||||
color: Colors.grey[500],
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
member['location'],
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -903,7 +904,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
'Rejoint ${_formatDate(joinDate)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF9CA3AF),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -911,7 +912,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
'Actif ${_formatRelativeTime(lastActivity)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF9CA3AF),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -974,7 +975,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
child: const Icon(
|
||||
Icons.more_vert,
|
||||
size: 16,
|
||||
color: Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1040,15 +1041,15 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status) {
|
||||
case 'Actif':
|
||||
return const Color(0xFF10B981);
|
||||
return AppColors.success;
|
||||
case 'Inactif':
|
||||
return const Color(0xFF6B7280);
|
||||
return AppColors.textSecondaryLight;
|
||||
case 'Suspendu':
|
||||
return const Color(0xFFDC2626);
|
||||
return AppColors.error;
|
||||
case 'En attente':
|
||||
return const Color(0xFFF59E0B);
|
||||
return AppColors.warning;
|
||||
default:
|
||||
return const Color(0xFF6B7280);
|
||||
return AppColors.textSecondaryLight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1056,30 +1057,30 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
Color _getRoleColor(String role) {
|
||||
switch (role) {
|
||||
case 'Super Administrateur':
|
||||
return const Color(0xFF7C3AED);
|
||||
return AppColors.brandGreen;
|
||||
case 'Administrateur Org':
|
||||
return const Color(0xFF6366F1);
|
||||
return AppColors.primaryGreen;
|
||||
case 'Gestionnaire RH':
|
||||
return const Color(0xFF0EA5E9);
|
||||
return AppColors.info;
|
||||
case 'Modérateur':
|
||||
return const Color(0xFF059669);
|
||||
return AppColors.brandGreenLight;
|
||||
case 'Membre Actif':
|
||||
return const Color(0xFF6C5CE7);
|
||||
return AppColors.primaryGreen;
|
||||
case 'Consultant':
|
||||
return const Color(0xFFF59E0B);
|
||||
return AppColors.warning;
|
||||
case 'Membre Simple':
|
||||
return const Color(0xFF6B7280);
|
||||
return AppColors.textSecondaryLight;
|
||||
default:
|
||||
return const Color(0xFF6B7280);
|
||||
return AppColors.textSecondaryLight;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la couleur selon le score de contribution
|
||||
Color _getScoreColor(int score) {
|
||||
if (score >= 90) return const Color(0xFF10B981);
|
||||
if (score >= 70) return const Color(0xFF0EA5E9);
|
||||
if (score >= 50) return const Color(0xFFF59E0B);
|
||||
return const Color(0xFFDC2626);
|
||||
if (score >= 90) return AppColors.success;
|
||||
if (score >= 70) return AppColors.brandGreenLight;
|
||||
if (score >= 50) return AppColors.warning;
|
||||
return AppColors.error;
|
||||
}
|
||||
|
||||
/// Formate une date
|
||||
@@ -1174,7 +1175,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Messagerie groupée à venir. Utilisez l\'action « Message » sur un membre.'),
|
||||
backgroundColor: Color(0xFF6C5CE7),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1190,7 +1191,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Export des membres en cours...'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1234,7 +1235,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Message à ${member['name']} à implémenter'),
|
||||
backgroundColor: const Color(0xFF0EA5E9),
|
||||
backgroundColor: AppColors.info,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1257,7 +1258,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${member['name']} supprimé'),
|
||||
backgroundColor: const Color(0xFFDC2626),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1413,13 +1414,13 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.1),
|
||||
color: AppColors.primaryGreen.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.people_outline,
|
||||
size: 48,
|
||||
color: Color(0xFF6C5CE7),
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -1428,7 +1429,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF374151),
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -1437,7 +1438,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
? 'Aucun membre ne correspond à votre recherche'
|
||||
: 'Aucun membre ne correspond aux filtres sélectionnés',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -1454,7 +1455,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réinitialiser les filtres'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6C5CE7),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
@@ -1667,7 +1668,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: index < 3 ? const Color(0xFFF59E0B) : const Color(0xFF6B7280),
|
||||
color: index < 3 ? AppColors.warning : AppColors.textSecondaryLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
@@ -1707,7 +1708,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
member['role'],
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1890,7 +1891,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Modifier'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6C5CE7),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
@@ -1929,7 +1930,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF374151),
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1948,7 +1949,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: const Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -1959,7 +1960,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -1967,7 +1968,7 @@ class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF374151),
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -79,7 +79,7 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
final pendingCount = widget.members.where((m) => m['status'] == 'En attente').length;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
border: Border(
|
||||
@@ -100,10 +100,10 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
|
||||
Widget _buildStatBadge(String label, String value, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
@@ -118,7 +118,7 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
|
||||
Widget _buildSearchAndFilters() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
border: Border(bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)),
|
||||
@@ -192,10 +192,10 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _filterStatus = label),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? UnionFlowColors.unionGreen : UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: isSelected ? UnionFlowColors.unionGreen : UnionFlowColors.border, width: 1),
|
||||
),
|
||||
child: Text(
|
||||
@@ -225,9 +225,9 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
onRefresh: () async => widget.onRefresh(),
|
||||
color: UnionFlowColors.unionGreen,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: filtered.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||
itemBuilder: (context, index) => _buildMemberCard(filtered[index]),
|
||||
),
|
||||
);
|
||||
@@ -237,23 +237,22 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
return GestureDetector(
|
||||
onTap: () => _showMemberDetails(member),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: UnionFlowColors.border.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
width: 32,
|
||||
height: 32,
|
||||
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: 18),
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -322,14 +321,14 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(color: UnionFlowColors.unionGreenPale, shape: BoxShape.circle),
|
||||
child: const Icon(Icons.people_outline, size: 64, color: UnionFlowColors.unionGreen),
|
||||
child: const Icon(Icons.people_outline, size: 40, color: UnionFlowColors.unionGreen),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Aucun membre trouvé',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary),
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
@@ -389,40 +388,40 @@ class _MembersPageWithDataAndPaginationState extends State<MembersPageWithDataAn
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
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: 32),
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 22),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
member['name'] ?? '',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary),
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
member['role'] ?? '',
|
||||
style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
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: 24),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -22,21 +22,23 @@ final _getIt = GetIt.instance;
|
||||
|
||||
/// Wrapper qui fournit le BLoC à la page des membres
|
||||
class MembersPageWrapper extends StatelessWidget {
|
||||
const MembersPageWrapper({super.key});
|
||||
final String? organisationId;
|
||||
|
||||
const MembersPageWrapper({super.key, this.organisationId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppLogger.info('MembersPageWrapper: Création du BlocProvider');
|
||||
|
||||
|
||||
return BlocProvider<MembresBloc>(
|
||||
create: (context) {
|
||||
AppLogger.info('MembresPageWrapper: Initialisation du MembresBloc');
|
||||
final bloc = _getIt<MembresBloc>();
|
||||
// Charger les membres au démarrage
|
||||
bloc.add(const LoadMembres());
|
||||
bloc.add(LoadMembres(organisationId: organisationId));
|
||||
return bloc;
|
||||
},
|
||||
child: const MembersPageConnected(),
|
||||
child: MembersPageConnected(organisationId: organisationId),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,15 +47,59 @@ class MembersPageWrapper extends StatelessWidget {
|
||||
///
|
||||
/// Cette page gère les états du BLoC et affiche l'UI appropriée
|
||||
class MembersPageConnected extends StatelessWidget {
|
||||
const MembersPageConnected({super.key});
|
||||
final String? organisationId;
|
||||
|
||||
const MembersPageConnected({super.key, this.organisationId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<MembresBloc, MembresState>(
|
||||
listener: (context, state) {
|
||||
// Après création : recharger la liste
|
||||
// Après création : afficher le mot de passe temporaire si disponible, puis recharger
|
||||
if (state is MembreCreated) {
|
||||
context.read<MembresBloc>().add(const LoadMembres(refresh: true));
|
||||
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
|
||||
@@ -67,7 +113,7 @@ class MembersPageConnected extends StatelessWidget {
|
||||
label: 'Réessayer',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
context.read<MembresBloc>().add(const LoadMembres());
|
||||
context.read<MembresBloc>().add(LoadMembres(organisationId: organisationId));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -134,19 +180,23 @@ class MembersPageConnected extends StatelessWidget {
|
||||
totalPages: state.totalPages,
|
||||
onPageChanged: (newPage, recherche) {
|
||||
AppLogger.userAction('Load page', data: {'page': newPage});
|
||||
context.read<MembresBloc>().add(LoadMembres(page: newPage, recherche: recherche));
|
||||
context.read<MembresBloc>().add(LoadMembres(page: newPage, recherche: recherche, organisationId: organisationId));
|
||||
},
|
||||
onRefresh: () {
|
||||
AppLogger.userAction('Refresh membres');
|
||||
context.read<MembresBloc>().add(const LoadMembres(refresh: true));
|
||||
context.read<MembresBloc>().add(LoadMembres(refresh: true, organisationId: organisationId));
|
||||
},
|
||||
onSearch: (query) {
|
||||
context.read<MembresBloc>().add(LoadMembres(page: 0, recherche: 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: (_) => const AddMemberDialog(),
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: const AddMemberDialog(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -160,7 +210,7 @@ class MembersPageConnected extends StatelessWidget {
|
||||
child: NetworkErrorWidget(
|
||||
onRetry: () {
|
||||
AppLogger.userAction('Retry load membres after network error');
|
||||
context.read<MembresBloc>().add(const LoadMembres());
|
||||
context.read<MembresBloc>().add(LoadMembres(organisationId: organisationId));
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -175,7 +225,7 @@ class MembersPageConnected extends StatelessWidget {
|
||||
message: state.message,
|
||||
onRetry: () {
|
||||
AppLogger.userAction('Retry load membres after error');
|
||||
context.read<MembresBloc>().add(const LoadMembres());
|
||||
context.read<MembresBloc>().add(LoadMembres(organisationId: organisationId));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user