Alignement design systeme OK

This commit is contained in:
DahoudG
2025-09-20 03:56:11 +00:00
parent a1214bc116
commit 96a17eadbd
34 changed files with 11720 additions and 766 deletions

View File

@@ -15,7 +15,7 @@ import 'keycloak_webview_auth_service.dart';
/// Configuration Keycloak pour votre instance
class KeycloakConfig {
/// URL de base de votre Keycloak
static const String baseUrl = 'http://192.168.1.145:8180';
static const String baseUrl = 'http://192.168.1.11:8180';
/// Realm UnionFlow
static const String realm = 'unionflow';
@@ -193,9 +193,9 @@ class KeycloakAuthService {
lastName: lastName,
primaryRole: primaryRole,
organizationContexts: [], // À implémenter selon vos besoins
organizationContexts: const [], // À implémenter selon vos besoins
additionalPermissions: permissions,
revokedPermissions: [],
revokedPermissions: const [],
preferences: const UserPreferences(),
lastLoginAt: DateTime.now(),
createdAt: DateTime.now(), // À récupérer depuis Keycloak si disponible

View File

@@ -27,7 +27,7 @@ import 'keycloak_role_mapper.dart';
/// Configuration Keycloak pour l'authentification WebView
class KeycloakWebViewConfig {
/// URL de base de l'instance Keycloak
static const String baseUrl = 'http://192.168.1.145:8180';
static const String baseUrl = 'http://192.168.1.11:8180';
/// Realm UnionFlow
static const String realm = 'unionflow';
@@ -273,7 +273,7 @@ class KeycloakWebViewAuthService {
},
body: body,
)
.timeout(Duration(seconds: KeycloakWebViewConfig.httpTimeoutSeconds));
.timeout(const Duration(seconds: KeycloakWebViewConfig.httpTimeoutSeconds));
debugPrint('📡 Réponse token endpoint: ${response.statusCode}');
@@ -371,7 +371,7 @@ class KeycloakWebViewAuthService {
}
// Vérifier l'issuer
final String expectedIssuer = '${KeycloakWebViewConfig.baseUrl}/realms/${KeycloakWebViewConfig.realm}';
const String expectedIssuer = '${KeycloakWebViewConfig.baseUrl}/realms/${KeycloakWebViewConfig.realm}';
if (payload['iss'] != expectedIssuer) {
throw KeycloakWebViewAuthException(
'Token JWT invalide: issuer incorrect (attendu: $expectedIssuer, reçu: ${payload['iss']})',

View File

@@ -0,0 +1,328 @@
/// Modèle pour les critères de recherche avancée des membres
/// Correspond au DTO Java MembreSearchCriteria
class MembreSearchCriteria {
/// Terme de recherche général (nom, prénom, email)
final String? query;
/// Recherche par nom exact ou partiel
final String? nom;
/// Recherche par prénom exact ou partiel
final String? prenom;
/// Recherche par email exact ou partiel
final String? email;
/// Filtre par numéro de téléphone
final String? telephone;
/// Liste des IDs d'organisations
final List<String>? organisationIds;
/// Liste des rôles à rechercher
final List<String>? roles;
/// Filtre par statut d'activité
final String? statut;
/// Date d'adhésion minimum (format ISO 8601)
final String? dateAdhesionMin;
/// Date d'adhésion maximum (format ISO 8601)
final String? dateAdhesionMax;
/// Âge minimum
final int? ageMin;
/// Âge maximum
final int? ageMax;
/// Filtre par région
final String? region;
/// Filtre par ville
final String? ville;
/// Filtre par profession
final String? profession;
/// Filtre par nationalité
final String? nationalite;
/// Filtre membres du bureau uniquement
final bool? membreBureau;
/// Filtre responsables uniquement
final bool? responsable;
/// Inclure les membres inactifs dans la recherche
final bool includeInactifs;
const MembreSearchCriteria({
this.query,
this.nom,
this.prenom,
this.email,
this.telephone,
this.organisationIds,
this.roles,
this.statut,
this.dateAdhesionMin,
this.dateAdhesionMax,
this.ageMin,
this.ageMax,
this.region,
this.ville,
this.profession,
this.nationalite,
this.membreBureau,
this.responsable,
this.includeInactifs = false,
});
/// Factory constructor pour créer depuis JSON
factory MembreSearchCriteria.fromJson(Map<String, dynamic> json) {
return MembreSearchCriteria(
query: json['query'] as String?,
nom: json['nom'] as String?,
prenom: json['prenom'] as String?,
email: json['email'] as String?,
telephone: json['telephone'] as String?,
organisationIds: (json['organisationIds'] as List<dynamic>?)?.cast<String>(),
roles: (json['roles'] as List<dynamic>?)?.cast<String>(),
statut: json['statut'] as String?,
dateAdhesionMin: json['dateAdhesionMin'] as String?,
dateAdhesionMax: json['dateAdhesionMax'] as String?,
ageMin: json['ageMin'] as int?,
ageMax: json['ageMax'] as int?,
region: json['region'] as String?,
ville: json['ville'] as String?,
profession: json['profession'] as String?,
nationalite: json['nationalite'] as String?,
membreBureau: json['membreBureau'] as bool?,
responsable: json['responsable'] as bool?,
includeInactifs: json['includeInactifs'] as bool? ?? false,
);
}
/// Convertit vers JSON
Map<String, dynamic> toJson() {
return {
'query': query,
'nom': nom,
'prenom': prenom,
'email': email,
'telephone': telephone,
'organisationIds': organisationIds,
'roles': roles,
'statut': statut,
'dateAdhesionMin': dateAdhesionMin,
'dateAdhesionMax': dateAdhesionMax,
'ageMin': ageMin,
'ageMax': ageMax,
'region': region,
'ville': ville,
'profession': profession,
'nationalite': nationalite,
'membreBureau': membreBureau,
'responsable': responsable,
'includeInactifs': includeInactifs,
};
}
/// Vérifie si au moins un critère de recherche est défini
bool get hasAnyCriteria {
return query?.isNotEmpty == true ||
nom?.isNotEmpty == true ||
prenom?.isNotEmpty == true ||
email?.isNotEmpty == true ||
telephone?.isNotEmpty == true ||
organisationIds?.isNotEmpty == true ||
roles?.isNotEmpty == true ||
statut?.isNotEmpty == true ||
dateAdhesionMin?.isNotEmpty == true ||
dateAdhesionMax?.isNotEmpty == true ||
ageMin != null ||
ageMax != null ||
region?.isNotEmpty == true ||
ville?.isNotEmpty == true ||
profession?.isNotEmpty == true ||
nationalite?.isNotEmpty == true ||
membreBureau != null ||
responsable != null;
}
/// Valide la cohérence des critères de recherche
bool get isValid {
// Validation des âges
if (ageMin != null && ageMax != null) {
if (ageMin! > ageMax!) {
return false;
}
}
// Validation des dates (si implémentée)
if (dateAdhesionMin != null && dateAdhesionMax != null) {
try {
final dateMin = DateTime.parse(dateAdhesionMin!);
final dateMax = DateTime.parse(dateAdhesionMax!);
if (dateMin.isAfter(dateMax)) {
return false;
}
} catch (e) {
return false;
}
}
return true;
}
/// Retourne une description textuelle des critères actifs
String get description {
final parts = <String>[];
if (query?.isNotEmpty == true) parts.add("Recherche: '$query'");
if (nom?.isNotEmpty == true) parts.add("Nom: '$nom'");
if (prenom?.isNotEmpty == true) parts.add("Prénom: '$prenom'");
if (email?.isNotEmpty == true) parts.add("Email: '$email'");
if (statut?.isNotEmpty == true) parts.add("Statut: $statut");
if (organisationIds?.isNotEmpty == true) {
parts.add("Organisations: ${organisationIds!.length}");
}
if (roles?.isNotEmpty == true) {
parts.add("Rôles: ${roles!.join(', ')}");
}
if (dateAdhesionMin?.isNotEmpty == true) {
parts.add("Adhésion >= $dateAdhesionMin");
}
if (dateAdhesionMax?.isNotEmpty == true) {
parts.add("Adhésion <= $dateAdhesionMax");
}
if (ageMin != null) parts.add("Âge >= $ageMin");
if (ageMax != null) parts.add("Âge <= $ageMax");
if (region?.isNotEmpty == true) parts.add("Région: '$region'");
if (ville?.isNotEmpty == true) parts.add("Ville: '$ville'");
if (profession?.isNotEmpty == true) parts.add("Profession: '$profession'");
if (nationalite?.isNotEmpty == true) parts.add("Nationalité: '$nationalite'");
if (membreBureau == true) parts.add("Membre bureau");
if (responsable == true) parts.add("Responsable");
return parts.join('');
}
/// Crée une copie avec des modifications
MembreSearchCriteria copyWith({
String? query,
String? nom,
String? prenom,
String? email,
String? telephone,
List<String>? organisationIds,
List<String>? roles,
String? statut,
String? dateAdhesionMin,
String? dateAdhesionMax,
int? ageMin,
int? ageMax,
String? region,
String? ville,
String? profession,
String? nationalite,
bool? membreBureau,
bool? responsable,
bool? includeInactifs,
}) {
return MembreSearchCriteria(
query: query ?? this.query,
nom: nom ?? this.nom,
prenom: prenom ?? this.prenom,
email: email ?? this.email,
telephone: telephone ?? this.telephone,
organisationIds: organisationIds ?? this.organisationIds,
roles: roles ?? this.roles,
statut: statut ?? this.statut,
dateAdhesionMin: dateAdhesionMin ?? this.dateAdhesionMin,
dateAdhesionMax: dateAdhesionMax ?? this.dateAdhesionMax,
ageMin: ageMin ?? this.ageMin,
ageMax: ageMax ?? this.ageMax,
region: region ?? this.region,
ville: ville ?? this.ville,
profession: profession ?? this.profession,
nationalite: nationalite ?? this.nationalite,
membreBureau: membreBureau ?? this.membreBureau,
responsable: responsable ?? this.responsable,
includeInactifs: includeInactifs ?? this.includeInactifs,
);
}
/// Critères vides
static const empty = MembreSearchCriteria();
/// Critères pour recherche rapide par nom/prénom
static MembreSearchCriteria quickSearch(String query) {
return MembreSearchCriteria(query: query);
}
/// Critères pour membres actifs uniquement
static const activeMembers = MembreSearchCriteria(
statut: 'ACTIF',
includeInactifs: false,
);
/// Critères pour membres du bureau
static const bureauMembers = MembreSearchCriteria(
membreBureau: true,
statut: 'ACTIF',
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MembreSearchCriteria &&
runtimeType == other.runtimeType &&
query == other.query &&
nom == other.nom &&
prenom == other.prenom &&
email == other.email &&
telephone == other.telephone &&
organisationIds == other.organisationIds &&
roles == other.roles &&
statut == other.statut &&
dateAdhesionMin == other.dateAdhesionMin &&
dateAdhesionMax == other.dateAdhesionMax &&
ageMin == other.ageMin &&
ageMax == other.ageMax &&
region == other.region &&
ville == other.ville &&
profession == other.profession &&
nationalite == other.nationalite &&
membreBureau == other.membreBureau &&
responsable == other.responsable &&
includeInactifs == other.includeInactifs;
@override
int get hashCode => Object.hashAll([
query,
nom,
prenom,
email,
telephone,
organisationIds,
roles,
statut,
dateAdhesionMin,
dateAdhesionMax,
ageMin,
ageMax,
region,
ville,
profession,
nationalite,
membreBureau,
responsable,
includeInactifs,
]);
@override
String toString() => 'MembreSearchCriteria(${description.isNotEmpty ? description : 'empty'})';
}

View File

@@ -0,0 +1,269 @@
import 'membre_search_criteria.dart';
import '../../features/members/data/models/membre_model.dart';
/// Modèle pour les résultats de recherche avancée des membres
/// Correspond au DTO Java MembreSearchResultDTO
class MembreSearchResult {
/// Liste des membres trouvés
final List<MembreModel> membres;
/// Nombre total de résultats (toutes pages confondues)
final int totalElements;
/// Nombre total de pages
final int totalPages;
/// Numéro de la page actuelle (0-based)
final int currentPage;
/// Taille de la page
final int pageSize;
/// Nombre d'éléments sur la page actuelle
final int numberOfElements;
/// Indique s'il y a une page suivante
final bool hasNext;
/// Indique s'il y a une page précédente
final bool hasPrevious;
/// Indique si c'est la première page
final bool isFirst;
/// Indique si c'est la dernière page
final bool isLast;
/// Critères de recherche utilisés
final MembreSearchCriteria criteria;
/// Temps d'exécution de la recherche en millisecondes
final int executionTimeMs;
/// Statistiques de recherche
final SearchStatistics? statistics;
const MembreSearchResult({
required this.membres,
required this.totalElements,
required this.totalPages,
required this.currentPage,
required this.pageSize,
required this.numberOfElements,
required this.hasNext,
required this.hasPrevious,
required this.isFirst,
required this.isLast,
required this.criteria,
required this.executionTimeMs,
this.statistics,
});
/// Factory constructor pour créer depuis JSON
factory MembreSearchResult.fromJson(Map<String, dynamic> json) {
return MembreSearchResult(
membres: (json['membres'] as List<dynamic>?)
?.map((e) => MembreModel.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
totalElements: json['totalElements'] as int? ?? 0,
totalPages: json['totalPages'] as int? ?? 0,
currentPage: json['currentPage'] as int? ?? 0,
pageSize: json['pageSize'] as int? ?? 20,
numberOfElements: json['numberOfElements'] as int? ?? 0,
hasNext: json['hasNext'] as bool? ?? false,
hasPrevious: json['hasPrevious'] as bool? ?? false,
isFirst: json['isFirst'] as bool? ?? true,
isLast: json['isLast'] as bool? ?? true,
criteria: MembreSearchCriteria.fromJson(json['criteria'] as Map<String, dynamic>? ?? {}),
executionTimeMs: json['executionTimeMs'] as int? ?? 0,
statistics: json['statistics'] != null
? SearchStatistics.fromJson(json['statistics'] as Map<String, dynamic>)
: null,
);
}
/// Convertit vers JSON
Map<String, dynamic> toJson() {
return {
'membres': membres.map((e) => e.toJson()).toList(),
'totalElements': totalElements,
'totalPages': totalPages,
'currentPage': currentPage,
'pageSize': pageSize,
'numberOfElements': numberOfElements,
'hasNext': hasNext,
'hasPrevious': hasPrevious,
'isFirst': isFirst,
'isLast': isLast,
'criteria': criteria.toJson(),
'executionTimeMs': executionTimeMs,
'statistics': statistics?.toJson(),
};
}
/// Vérifie si les résultats sont vides
bool get isEmpty => membres.isEmpty;
/// Vérifie si les résultats ne sont pas vides
bool get isNotEmpty => membres.isNotEmpty;
/// Retourne le numéro de la page suivante (1-based pour affichage)
int get nextPageNumber => hasNext ? currentPage + 2 : -1;
/// Retourne le numéro de la page précédente (1-based pour affichage)
int get previousPageNumber => hasPrevious ? currentPage : -1;
/// Retourne une description textuelle des résultats
String get resultDescription {
if (isEmpty) {
return 'Aucun membre trouvé';
}
if (totalElements == 1) {
return '1 membre trouvé';
}
if (totalPages == 1) {
return '$totalElements membres trouvés';
}
final startElement = currentPage * pageSize + 1;
final endElement = (startElement + numberOfElements - 1).clamp(1, totalElements);
return 'Membres $startElement-$endElement sur $totalElements (page ${currentPage + 1}/$totalPages)';
}
/// Résultat vide
static MembreSearchResult empty(MembreSearchCriteria criteria) {
return MembreSearchResult(
membres: const [],
totalElements: 0,
totalPages: 0,
currentPage: 0,
pageSize: 20,
numberOfElements: 0,
hasNext: false,
hasPrevious: false,
isFirst: true,
isLast: true,
criteria: criteria,
executionTimeMs: 0,
);
}
@override
String toString() => 'MembreSearchResult($resultDescription, ${executionTimeMs}ms)';
}
/// Statistiques sur les résultats de recherche
class SearchStatistics {
/// Nombre de membres actifs dans les résultats
final int membresActifs;
/// Nombre de membres inactifs dans les résultats
final int membresInactifs;
/// Âge moyen des membres trouvés
final double ageMoyen;
/// Âge minimum des membres trouvés
final int ageMin;
/// Âge maximum des membres trouvés
final int ageMax;
/// Nombre d'organisations représentées
final int nombreOrganisations;
/// Nombre de régions représentées
final int nombreRegions;
/// Ancienneté moyenne en années
final double ancienneteMoyenne;
const SearchStatistics({
required this.membresActifs,
required this.membresInactifs,
required this.ageMoyen,
required this.ageMin,
required this.ageMax,
required this.nombreOrganisations,
required this.nombreRegions,
required this.ancienneteMoyenne,
});
/// Factory constructor pour créer depuis JSON
factory SearchStatistics.fromJson(Map<String, dynamic> json) {
return SearchStatistics(
membresActifs: json['membresActifs'] as int? ?? 0,
membresInactifs: json['membresInactifs'] as int? ?? 0,
ageMoyen: (json['ageMoyen'] as num?)?.toDouble() ?? 0.0,
ageMin: json['ageMin'] as int? ?? 0,
ageMax: json['ageMax'] as int? ?? 0,
nombreOrganisations: json['nombreOrganisations'] as int? ?? 0,
nombreRegions: json['nombreRegions'] as int? ?? 0,
ancienneteMoyenne: (json['ancienneteMoyenne'] as num?)?.toDouble() ?? 0.0,
);
}
/// Convertit vers JSON
Map<String, dynamic> toJson() {
return {
'membresActifs': membresActifs,
'membresInactifs': membresInactifs,
'ageMoyen': ageMoyen,
'ageMin': ageMin,
'ageMax': ageMax,
'nombreOrganisations': nombreOrganisations,
'nombreRegions': nombreRegions,
'ancienneteMoyenne': ancienneteMoyenne,
};
}
/// Nombre total de membres
int get totalMembres => membresActifs + membresInactifs;
/// Pourcentage de membres actifs
double get pourcentageActifs {
if (totalMembres == 0) return 0.0;
return (membresActifs / totalMembres) * 100;
}
/// Pourcentage de membres inactifs
double get pourcentageInactifs {
if (totalMembres == 0) return 0.0;
return (membresInactifs / totalMembres) * 100;
}
/// Tranche d'âge
String get trancheAge {
if (ageMin == ageMax) return '$ageMin ans';
return '$ageMin-$ageMax ans';
}
/// Description textuelle des statistiques
String get description {
final parts = <String>[];
if (totalMembres > 0) {
parts.add('$totalMembres membres');
if (membresActifs > 0) {
parts.add('${pourcentageActifs.toStringAsFixed(1)}% actifs');
}
if (ageMoyen > 0) {
parts.add('âge moyen: ${ageMoyen.toStringAsFixed(1)} ans');
}
if (nombreOrganisations > 0) {
parts.add('$nombreOrganisations organisations');
}
if (ancienneteMoyenne > 0) {
parts.add('ancienneté: ${ancienneteMoyenne.toStringAsFixed(1)} ans');
}
}
return parts.join('');
}
@override
String toString() => 'SearchStatistics($description)';
}

View File

@@ -0,0 +1,43 @@
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../auth/bloc/auth_bloc.dart';
import '../../features/auth/presentation/pages/login_page.dart';
import 'main_navigation_layout.dart';
/// Configuration du routeur principal de l'application
class AppRouter {
static final GoRouter router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final authState = context.read<AuthBloc>().state;
final isAuthenticated = authState is AuthAuthenticated;
final isOnLoginPage = state.matchedLocation == '/login';
// Si pas authentifié et pas sur la page de login, rediriger vers login
if (!isAuthenticated && !isOnLoginPage) {
return '/login';
}
// Si authentifié et sur la page de login, rediriger vers dashboard
if (isAuthenticated && isOnLoginPage) {
return '/';
}
return null; // Pas de redirection
},
routes: [
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: '/',
name: 'main',
builder: (context, state) => const MainNavigationLayout(),
),
],
);
}

View File

@@ -0,0 +1,419 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../auth/bloc/auth_bloc.dart';
import '../auth/models/user_role.dart';
import '../design_system/tokens/tokens.dart';
import '../../features/dashboard/presentation/pages/role_dashboards/role_dashboards.dart';
import '../../features/members/presentation/pages/members_page.dart';
import '../../features/events/presentation/pages/events_page.dart';
/// Layout principal avec navigation hybride
/// Bottom Navigation pour les sections principales + Drawer pour fonctions avancées
class MainNavigationLayout extends StatefulWidget {
const MainNavigationLayout({super.key});
@override
State<MainNavigationLayout> createState() => _MainNavigationLayoutState();
}
class _MainNavigationLayoutState extends State<MainNavigationLayout> {
int _selectedIndex = 0;
/// Obtient le dashboard approprié selon le rôle de l'utilisateur
Widget _getDashboardForRole(UserRole role) {
switch (role) {
case UserRole.superAdmin:
return const SuperAdminDashboard();
case UserRole.orgAdmin:
return const OrgAdminDashboard();
case UserRole.moderator:
return const ModeratorDashboard();
case UserRole.activeMember:
return const ActiveMemberDashboard();
case UserRole.simpleMember:
return const SimpleMemberDashboard();
case UserRole.visitor:
return const VisitorDashboard();
}
}
List<Widget> _getPages(UserRole role) {
return [
_getDashboardForRole(role),
const MembersPage(),
const EventsPage(),
const MorePage(), // Page "Plus" qui affiche les options avancées
];
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is! AuthAuthenticated) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _getPages(state.effectiveRole),
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
selectedItemColor: ColorTokens.primary,
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.dashboard),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
label: 'Membres',
),
BottomNavigationBarItem(
icon: Icon(Icons.event),
label: 'Événements',
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz),
label: 'Plus',
),
],
),
);
},
);
}
}
/// Page "Plus" avec les fonctions avancées selon le rôle
class MorePage extends StatelessWidget {
const MorePage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is! AuthAuthenticated) {
return Container(
color: const Color(0xFFF8F9FA),
child: const Center(child: CircularProgressIndicator()),
);
}
return Container(
color: const Color(0xFFF8F9FA),
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de la section
const Text(
'Plus d\'Options',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
fontSize: 20,
),
),
const SizedBox(height: 12),
// Profil utilisateur
_buildUserProfile(state),
const SizedBox(height: 16),
// Options selon le rôle
..._buildRoleBasedOptions(state),
const SizedBox(height: 16),
// Options communes
..._buildCommonOptions(context),
],
),
),
);
},
);
}
Widget _buildUserProfile(AuthAuthenticated state) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7),
borderRadius: BorderRadius.circular(25),
),
child: Center(
child: Text(
state.user.firstName.isNotEmpty ? state.user.firstName[0].toUpperCase() : 'U',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${state.user.firstName} ${state.user.lastName}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF374151),
),
),
const SizedBox(height: 2),
Text(
state.effectiveRole.displayName,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF6C5CE7),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
state.user.email,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
),
],
),
);
}
List<Widget> _buildRoleBasedOptions(AuthAuthenticated state) {
final options = <Widget>[];
// Options Super Admin
if (state.effectiveRole == UserRole.superAdmin) {
options.addAll([
_buildSectionTitle('Administration Système'),
_buildOptionTile(
icon: Icons.settings,
title: 'Paramètres Système',
subtitle: 'Configuration globale',
onTap: () {},
),
_buildOptionTile(
icon: Icons.backup,
title: 'Sauvegarde',
subtitle: 'Gestion des sauvegardes',
onTap: () {},
),
_buildOptionTile(
icon: Icons.analytics,
title: 'Logs Système',
subtitle: 'Surveillance et logs',
onTap: () {},
),
]);
}
// Options Admin Organisation
if (state.effectiveRole == UserRole.orgAdmin || state.effectiveRole == UserRole.superAdmin) {
options.addAll([
_buildSectionTitle('Administration'),
_buildOptionTile(
icon: Icons.business,
title: 'Gestion Organisation',
subtitle: 'Paramètres organisation',
onTap: () {},
),
_buildOptionTile(
icon: Icons.assessment,
title: 'Rapports',
subtitle: 'Rapports et statistiques',
onTap: () {},
),
]);
}
// Options RH
if (state.effectiveRole == UserRole.moderator || state.effectiveRole == UserRole.superAdmin) {
options.addAll([
_buildSectionTitle('Ressources Humaines'),
_buildOptionTile(
icon: Icons.people_alt,
title: 'Gestion RH',
subtitle: 'Outils RH avancés',
onTap: () {},
),
]);
}
return options;
}
List<Widget> _buildCommonOptions(BuildContext context) {
return [
_buildSectionTitle('Général'),
_buildOptionTile(
icon: Icons.person,
title: 'Mon Profil',
subtitle: 'Modifier mes informations',
onTap: () {},
),
_buildOptionTile(
icon: Icons.notifications,
title: 'Notifications',
subtitle: 'Gérer les notifications',
onTap: () {},
),
_buildOptionTile(
icon: Icons.help,
title: 'Aide & Support',
subtitle: 'Documentation et support',
onTap: () {},
),
_buildOptionTile(
icon: Icons.info,
title: 'À propos',
subtitle: 'Version et informations',
onTap: () {},
),
const SizedBox(height: 16),
_buildOptionTile(
icon: Icons.logout,
title: 'Déconnexion',
subtitle: 'Se déconnecter de l\'application',
color: Colors.red,
onTap: () {
context.read<AuthBloc>().add(AuthLogoutRequested());
},
),
];
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
bottom: 8,
),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
);
}
Widget _buildOptionTile({
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
Color? color,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: (color ?? const Color(0xFF6C5CE7)).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
icon,
color: color ?? const Color(0xFF6C5CE7),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: color ?? const Color(0xFF374151),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
),
const Icon(
Icons.chevron_right,
color: Color(0xFF6B7280),
size: 16,
),
],
),
),
),
);
}
}

View File

@@ -1,178 +0,0 @@
/// Dashboard Page Stable - Redirecteur vers Dashboard Adaptatif
/// Redirige automatiquement vers le nouveau système de dashboard adaptatif
library dashboard_page_stable;
import 'package:flutter/material.dart';
import 'adaptive_dashboard_page.dart';
/// Page Dashboard Stable - Maintenant un redirecteur
///
/// Cette page redirige automatiquement vers le nouveau système
/// de dashboard adaptatif basé sur les rôles utilisateurs.
class DashboardPageStable extends StatefulWidget {
const DashboardPageStable({super.key});
@override
State<DashboardPageStable> createState() => _DashboardPageStableState();
}
class _DashboardPageStableState extends State<DashboardPageStable> {
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
appBar: AppBar(
title: Text(
'UnionFlow Dashboard',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
color: ColorTokens.onSurface,
),
),
backgroundColor: ColorTokens.surface,
elevation: 0,
actions: [
IconButton(
onPressed: () => _showNotifications(),
icon: const Icon(Icons.notifications_outlined),
tooltip: 'Notifications',
),
IconButton(
onPressed: () => _showSettings(),
icon: const Icon(Icons.settings_outlined),
tooltip: 'Paramètres',
),
],
),
drawer: DashboardDrawer(
onNavigate: _onNavigate,
onLogout: _onLogout,
),
body: RefreshIndicator(
key: _refreshKey,
onRefresh: _refreshData,
child: SingleChildScrollView(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Message de bienvenue
DashboardWelcomeSection(
title: 'Bienvenue sur UnionFlow',
subtitle: 'Votre plateforme de gestion d\'union familiale',
),
const SizedBox(height: SpacingTokens.xl),
// Statistiques
DashboardStatsGrid(
onStatTap: _onStatTap,
),
const SizedBox(height: SpacingTokens.xl),
// Actions rapides
DashboardQuickActionsGrid(
onActionTap: _onActionTap,
),
const SizedBox(height: SpacingTokens.xl),
// Activité récente
DashboardRecentActivitySection(
onActivityTap: _onActivityTap,
),
const SizedBox(height: SpacingTokens.xl),
// Insights
DashboardInsightsSection(
onMetricTap: _onMetricTap,
),
],
),
),
),
);
}
// === CALLBACKS POUR LES WIDGETS MODULAIRES ===
/// Callback pour les actions sur les statistiques
void _onStatTap(String statType) {
debugPrint('Statistique tapée: $statType');
// TODO: Implémenter la navigation vers les détails
}
/// Callback pour les actions rapides
void _onActionTap(String actionType) {
debugPrint('Action rapide: $actionType');
// TODO: Implémenter les actions spécifiques
}
/// Callback pour les activités récentes
void _onActivityTap(String activityId) {
debugPrint('Activité tapée: $activityId');
// TODO: Implémenter la navigation vers les détails
}
/// Callback pour les métriques d'insights
void _onMetricTap(String metricType) {
debugPrint('Métrique tapée: $metricType');
// TODO: Implémenter la navigation vers les rapports
}
/// Callback pour la navigation du drawer
void _onNavigate(String route) {
Navigator.of(context).pop(); // Fermer le drawer
debugPrint('Navigation vers: $route');
// TODO: Implémenter la navigation
}
/// Callback pour la déconnexion
void _onLogout() {
Navigator.of(context).pop(); // Fermer le drawer
debugPrint('Déconnexion demandée');
// TODO: Implémenter la déconnexion
}
// === MÉTHODES UTILITAIRES ===
/// Actualise les données du dashboard
Future<void> _refreshData() async {
setState(() {
_isLoading = true;
});
// Simulation d'un appel API
await Future.delayed(const Duration(seconds: 1));
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Données actualisées'),
duration: Duration(seconds: 2),
),
);
}
}
/// Affiche les notifications
void _showNotifications() {
debugPrint('Afficher les notifications');
// TODO: Implémenter l'affichage des notifications
}
/// Affiche les paramètres
void _showSettings() {
debugPrint('Afficher les paramètres');
// TODO: Implémenter l'affichage des paramètres
}
}

View File

@@ -1,322 +1,275 @@
/// Dashboard Membre Actif - Activity Center Personnalisé
/// Interface personnalisée pour participation active
library active_member_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../widgets/widgets.dart';
/// Dashboard Activity Center pour Membre Actif
/// Dashboard simple pour Membre Actif
class ActiveMemberDashboard extends StatelessWidget {
const ActiveMemberDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
body: CustomScrollView(
slivers: [
// App Bar Membre Actif
SliverAppBar(
expandedHeight: 160,
floating: false,
pinned: true,
backgroundColor: const Color(0xFF00B894), // Vert communauté
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Activity Center',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00A085)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Center(
child: Icon(Icons.groups, color: Colors.white, size: 60),
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête de bienvenue
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00CEC9)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Bienvenue personnalisé
_buildPersonalizedWelcome(),
const SizedBox(height: SpacingTokens.xl),
// Mes statistiques
_buildMyStats(),
const SizedBox(height: SpacingTokens.xl),
// Actions membres
_buildMemberActions(),
const SizedBox(height: SpacingTokens.xl),
// Événements à venir
_buildUpcomingEvents(),
const SizedBox(height: SpacingTokens.xl),
// Mon activité
_buildMyActivity(),
Text(
'Bonjour !',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'Bienvenue sur votre espace membre',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
],
),
),
),
],
),
);
}
Widget _buildPersonalizedWelcome() {
return Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00CEC9)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(RadiusTokens.lg),
),
child: Row(
children: [
const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(Icons.person, color: Color(0xFF00B894), size: 30),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 24),
// Statistiques rapides
const Text(
'Mes Statistiques',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 1.2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
Text(
'Bonjour, Marie !',
style: TypographyTokens.headlineMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
_buildStatCard(
icon: Icons.event_available,
value: '12',
title: 'Événements',
color: const Color(0xFF00B894),
),
Text(
'Membre depuis 2 ans • Niveau Actif',
style: TypographyTokens.bodyMedium.copyWith(
color: Colors.white.withOpacity(0.9),
),
_buildStatCard(
icon: Icons.volunteer_activism,
value: '3',
title: 'Solidarité',
color: const Color(0xFF00CEC9),
),
_buildStatCard(
icon: Icons.payment,
value: 'À jour',
title: 'Cotisations',
color: const Color(0xFF0984E3),
),
_buildStatCard(
icon: Icons.star,
value: '4.8',
title: 'Engagement',
color: const Color(0xFFE17055),
),
],
),
),
],
const SizedBox(height: 24),
// Actions rapides
const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 1.5,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildActionCard(
icon: Icons.event,
title: 'Créer Événement',
color: const Color(0xFF00B894),
onTap: () {},
),
_buildActionCard(
icon: Icons.volunteer_activism,
title: 'Demande Aide',
color: const Color(0xFF00CEC9),
onTap: () {},
),
_buildActionCard(
icon: Icons.account_circle,
title: 'Mon Profil',
color: const Color(0xFF0984E3),
onTap: () {},
),
_buildActionCard(
icon: Icons.message,
title: 'Contacter',
color: const Color(0xFFE17055),
onTap: () {},
),
],
),
const SizedBox(height: 24),
// Activités récentes
const Text(
'Activités Récentes',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Card(
child: Column(
children: [
_buildActivityItem(
icon: Icons.check_circle,
title: 'Participation confirmée',
subtitle: 'Assemblée Générale - Il y a 2h',
color: const Color(0xFF00B894),
),
const Divider(height: 1),
_buildActivityItem(
icon: Icons.payment,
title: 'Cotisation payée',
subtitle: 'Décembre 2024 - Il y a 1j',
color: const Color(0xFF0984E3),
),
const Divider(height: 1),
_buildActivityItem(
icon: Icons.event,
title: 'Événement créé',
subtitle: 'Sortie ski de fond - Il y a 3j',
color: const Color(0xFF00CEC9),
),
],
),
),
],
),
);
}
Widget _buildStatCard({
required IconData icon,
required String value,
required String title,
required Color color,
}) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildMyStats() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mes Statistiques',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardStatsGrid(
stats: [
DashboardStat(
icon: Icons.event_available,
value: '12',
title: 'Événements',
color: const Color(0xFF00B894),
onTap: () {},
),
DashboardStat(
icon: Icons.volunteer_activism,
value: '3',
title: 'Solidarité',
color: const Color(0xFF00CEC9),
onTap: () {},
),
DashboardStat(
icon: Icons.payment,
value: 'À jour',
title: 'Cotisations',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardStat(
icon: Icons.star,
value: '4.8',
title: 'Engagement',
color: const Color(0xFFE17055),
onTap: () {},
),
],
onStatTap: (type) {},
),
],
);
}
Widget _buildMemberActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions Rapides',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
actions: [
DashboardQuickAction(
icon: Icons.event,
title: 'Créer Événement',
subtitle: 'Organiser activité',
color: const Color(0xFF00B894),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.volunteer_activism,
title: 'Demande Aide',
subtitle: 'Solidarité',
color: const Color(0xFF00CEC9),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.account_circle,
title: 'Mon Profil',
subtitle: 'Modifier infos',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.message,
title: 'Contacter',
subtitle: 'Support',
color: const Color(0xFFE17055),
onTap: () {},
),
],
onActionTap: (type) {},
),
],
);
}
Widget _buildUpcomingEvents() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Événements à Venir',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const Spacer(),
TextButton(
onPressed: () {},
child: const Text('Voir tout'),
),
],
),
const SizedBox(height: SpacingTokens.md),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
Widget _buildActionCard({
required IconData icon,
required String title,
required Color color,
required VoidCallback onTap,
}) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('15', style: TextStyle(fontWeight: FontWeight.bold)),
Text('DÉC', style: TextStyle(fontSize: 10)),
],
),
Icon(icon, color: color, size: 28),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
title: const Text('Assemblée Générale'),
subtitle: const Text('Salle communale • 19h00'),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
),
const Divider(height: 1),
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF00CEC9).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('22', style: TextStyle(fontWeight: FontWeight.bold)),
Text('DÉC', style: TextStyle(fontSize: 10)),
],
),
),
title: const Text('Soirée de Noël'),
subtitle: const Text('Restaurant Le Gourmet • 20h00'),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
textAlign: TextAlign.center,
),
],
),
),
],
),
);
}
Widget _buildMyActivity() {
return DashboardRecentActivitySection(
activities: [
DashboardActivity(
title: 'Participation confirmée',
subtitle: 'Assemblée Générale',
icon: Icons.check_circle,
color: const Color(0xFF00B894),
time: 'Il y a 2h',
),
DashboardActivity(
title: 'Cotisation payée',
subtitle: 'Décembre 2024',
icon: Icons.payment,
color: const Color(0xFF0984E3),
time: 'Il y a 1j',
),
DashboardActivity(
title: 'Événement créé',
subtitle: 'Sortie ski de fond',
icon: Icons.event,
color: const Color(0xFF00CEC9),
time: 'Il y a 3j',
),
],
onActivityTap: (id) {},
Widget _buildActivityItem({
required IconData icon,
required String title,
required String subtitle,
required Color color,
}) {
return ListTile(
leading: CircleAvatar(
backgroundColor: color.withOpacity(0.1),
child: Icon(icon, color: color, size: 20),
),
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(subtitle),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
);
}
}

View File

@@ -0,0 +1,725 @@
/// Dashboard Consultant - Interface Limitée
/// Interface spécialisée pour consultants externes
library consultant_dashboard;
import 'package:flutter/material.dart';
/// Dashboard pour Consultant Externe
class ConsultantDashboard extends StatefulWidget {
const ConsultantDashboard({super.key});
@override
State<ConsultantDashboard> createState() => _ConsultantDashboardState();
}
class _ConsultantDashboardState extends State<ConsultantDashboard> {
int _selectedIndex = 0;
final List<String> _consultantSections = [
'Mes Projets',
'Contacts',
'Profil',
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
title: Text(
'Consultant - ${_consultantSections[_selectedIndex]}',
style: const TextStyle(
color: Color(0xFF6C5CE7),
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: Colors.white,
elevation: 2,
centerTitle: false,
actions: [
// Notifications consultant
IconButton(
icon: const Icon(Icons.notifications_outlined, color: Color(0xFF6C5CE7)),
onPressed: () => _showConsultantNotifications(),
tooltip: 'Mes notifications',
),
// Menu consultant
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Color(0xFF6C5CE7)),
onSelected: (value) {
switch (value) {
case 'profile':
_editProfile();
break;
case 'contact':
_contactSupport();
break;
case 'help':
_showHelp();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'profile',
child: Row(
children: [
Icon(Icons.person, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Mon Profil'),
],
),
),
const PopupMenuItem(
value: 'contact',
child: Row(
children: [
Icon(Icons.support_agent, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Support'),
],
),
),
const PopupMenuItem(
value: 'help',
child: Row(
children: [
Icon(Icons.help, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Aide'),
],
),
),
],
),
],
),
drawer: _buildConsultantDrawer(),
body: Stack(
children: [
_buildSelectedContent(),
// Navigation rapide consultant
Positioned(
bottom: 20,
left: 20,
right: 20,
child: _buildConsultantQuickNavigation(),
),
],
),
);
}
/// Drawer de navigation consultant
Widget _buildConsultantDrawer() {
return Drawer(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF6C5CE7),
Color(0xFF5A4FCF),
Color(0xFF4834D4),
],
),
),
child: Column(
children: [
// Header consultant
Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.business_center,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sophie Martin',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Consultant IT',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
// Menu de navigation
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _consultantSections.length,
itemBuilder: (context, index) {
final isSelected = _selectedIndex == index;
return Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(
_getConsultantSectionIcon(index),
color: Colors.white,
size: 22,
),
title: Text(
_consultantSections[index],
style: TextStyle(
color: Colors.white,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
onTap: () {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
),
);
},
),
),
// Footer avec déconnexion
Container(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
// TODO: Implémenter la déconnexion
},
icon: const Icon(Icons.logout, size: 16),
label: const Text('Déconnexion'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.2),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 40),
),
),
),
],
),
),
);
}
/// Icône pour chaque section consultant
IconData _getConsultantSectionIcon(int index) {
switch (index) {
case 0: return Icons.work;
case 1: return Icons.contacts;
case 2: return Icons.person;
default: return Icons.work;
}
}
/// Contenu de la section sélectionnée
Widget _buildSelectedContent() {
switch (_selectedIndex) {
case 0:
return _buildProjectsContent();
case 1:
return _buildContactsContent();
case 2:
return _buildProfileContent();
default:
return _buildProjectsContent();
}
}
/// Mes Projets - Vue des projets assignés
Widget _buildProjectsContent() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header projets
_buildProjectsHeader(),
const SizedBox(height: 20),
// Projets actifs
_buildActiveProjects(),
const SizedBox(height: 20),
// Tâches en cours
_buildCurrentTasks(),
const SizedBox(height: 20),
// Statistiques consultant
_buildConsultantStats(),
],
),
);
}
/// Placeholder pour les autres sections
Widget _buildContactsContent() {
return const Center(
child: Text(
'Contacts\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildProfileContent() {
return const Center(
child: Text(
'Mon Profil\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
/// Header projets
Widget _buildProjectsHeader() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const Row(
children: [
Icon(Icons.work, color: Color(0xFF6C5CE7), size: 24),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mes Projets Assignés',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'3 projets actifs',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
],
),
),
],
),
);
}
/// Projets actifs
Widget _buildActiveProjects() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Projets Actifs',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildProjectCard(
'Refonte Site Web',
'Développement frontend',
'75%',
const Color(0xFF00B894),
),
const SizedBox(height: 8),
_buildProjectCard(
'App Mobile',
'Interface utilisateur',
'45%',
const Color(0xFF0984E3),
),
const SizedBox(height: 8),
_buildProjectCard(
'API Backend',
'Architecture serveur',
'90%',
const Color(0xFFE17055),
),
],
);
}
/// Widget pour une carte de projet
Widget _buildProjectCard(String title, String description, String progress, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.folder, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
Text(
description,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
Text(
progress,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
LinearProgressIndicator(
value: double.parse(progress.replaceAll('%', '')) / 100,
backgroundColor: color.withOpacity(0.2),
valueColor: AlwaysStoppedAnimation<Color>(color),
),
],
),
);
}
/// Tâches en cours
Widget _buildCurrentTasks() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tâches du Jour',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildTaskItem('Révision code frontend', true),
const SizedBox(height: 8),
_buildTaskItem('Réunion client 15h', false),
const SizedBox(height: 8),
_buildTaskItem('Tests unitaires', false),
],
),
),
],
);
}
/// Widget pour un élément de tâche
Widget _buildTaskItem(String task, bool completed) {
return Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: completed ? const Color(0xFF6C5CE7) : Colors.transparent,
border: Border.all(color: const Color(0xFF6C5CE7), width: 2),
borderRadius: BorderRadius.circular(4),
),
child: completed
? const Icon(Icons.check, color: Colors.white, size: 14)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
task,
style: TextStyle(
fontSize: 14,
decoration: completed ? TextDecoration.lineThrough : null,
color: completed ? Colors.grey[600] : Colors.black,
),
),
),
],
);
}
/// Statistiques consultant
Widget _buildConsultantStats() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Mes Statistiques',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard('Projets', '3', Icons.work, const Color(0xFF6C5CE7)),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard('Tâches', '12', Icons.task, const Color(0xFF00B894)),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard('Heures', '156h', Icons.schedule, const Color(0xFF0984E3)),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard('Éval.', '4.8/5', Icons.star, const Color(0xFFFDAB00)),
),
],
),
],
);
}
/// Widget pour une carte de statistique
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
title,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
);
}
/// Navigation rapide consultant
Widget _buildConsultantQuickNavigation() {
return Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(Icons.work, 'Projets', 0),
_buildNavItem(Icons.contacts, 'Contacts', 1),
_buildNavItem(Icons.person, 'Profil', 2),
],
),
);
}
/// Widget pour un élément de navigation
Widget _buildNavItem(IconData icon, String label, int index) {
final isSelected = _selectedIndex == index;
return GestureDetector(
onTap: () => setState(() => _selectedIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF6C5CE7).withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
size: 18,
color: isSelected
? const Color(0xFF6C5CE7)
: Colors.grey[600],
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 9,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? const Color(0xFF6C5CE7)
: Colors.grey[600],
),
),
],
),
),
);
}
// Méthodes d'action
void _showConsultantNotifications() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notifications consultant - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF6C5CE7),
),
);
}
void _editProfile() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Éditer profil - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF6C5CE7),
),
);
}
void _contactSupport() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Contacter support - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF6C5CE7),
),
);
}
void _showHelp() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Aide - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF6C5CE7),
),
);
}
}

View File

@@ -0,0 +1,914 @@
/// Dashboard Gestionnaire RH - Interface Ressources Humaines
/// Outils spécialisés pour la gestion des employés et RH
library hr_manager_dashboard;
import 'package:flutter/material.dart';
/// Dashboard spécialisé pour Gestionnaire RH
///
/// Fonctionnalités RH :
/// - Gestion des employés
/// - Recrutement et onboarding
/// - Évaluations de performance
/// - Congés et absences
/// - Reporting RH
/// - Formation et développement
class HRManagerDashboard extends StatefulWidget {
const HRManagerDashboard({super.key});
@override
State<HRManagerDashboard> createState() => _HRManagerDashboardState();
}
class _HRManagerDashboardState extends State<HRManagerDashboard>
with TickerProviderStateMixin {
late TabController _tabController;
int _selectedIndex = 0;
final List<String> _hrSections = [
'Vue d\'ensemble',
'Employés',
'Recrutement',
'Évaluations',
'Congés',
'Formation',
];
@override
void initState() {
super.initState();
_tabController = TabController(length: _hrSections.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
title: Text(
'RH Manager - ${_hrSections[_selectedIndex]}',
style: const TextStyle(
color: Color(0xFF00B894),
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: Colors.white,
elevation: 2,
centerTitle: false,
actions: [
// Recherche employés
IconButton(
icon: const Icon(Icons.search, color: Color(0xFF00B894)),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recherche avancée - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF00B894),
),
);
},
tooltip: 'Rechercher employés',
),
// Notifications RH
IconButton(
icon: const Icon(Icons.notifications_outlined, color: Color(0xFF00B894)),
onPressed: () => _showHRNotifications(),
tooltip: 'Notifications RH',
),
// Menu RH
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Color(0xFF00B894)),
onSelected: (value) {
switch (value) {
case 'reports':
_generateHRReports();
break;
case 'settings':
_openHRSettings();
break;
case 'export':
_exportHRData();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'reports',
child: Row(
children: [
Icon(Icons.assessment, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Rapports RH'),
],
),
),
const PopupMenuItem(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Paramètres RH'),
],
),
),
const PopupMenuItem(
value: 'export',
child: Row(
children: [
Icon(Icons.download, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Exporter données'),
],
),
),
],
),
],
),
drawer: _buildHRDrawer(),
body: Stack(
children: [
_buildSelectedContent(),
// Navigation rapide RH
Positioned(
bottom: 20,
left: 20,
right: 20,
child: _buildHRQuickNavigation(),
),
],
),
);
}
/// Drawer de navigation RH
Widget _buildHRDrawer() {
return Drawer(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF00B894),
Color(0xFF00A085),
Color(0xFF008B75),
],
),
),
child: Column(
children: [
// Header RH
Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.people,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestionnaire RH',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Ressources Humaines',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
// Menu de navigation
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _hrSections.length,
itemBuilder: (context, index) {
final isSelected = _selectedIndex == index;
return Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(
_getHRSectionIcon(index),
color: Colors.white,
size: 22,
),
title: Text(
_hrSections[index],
style: TextStyle(
color: Colors.white,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
onTap: () {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
),
);
},
),
),
// Footer avec déconnexion
Container(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
// TODO: Implémenter la déconnexion
},
icon: const Icon(Icons.logout, size: 16),
label: const Text('Déconnexion'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.2),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 40),
),
),
),
],
),
),
);
}
/// Icône pour chaque section RH
IconData _getHRSectionIcon(int index) {
switch (index) {
case 0: return Icons.dashboard;
case 1: return Icons.people;
case 2: return Icons.person_add;
case 3: return Icons.star_rate;
case 4: return Icons.event_busy;
case 5: return Icons.school;
default: return Icons.dashboard;
}
}
/// Contenu de la section sélectionnée
Widget _buildSelectedContent() {
switch (_selectedIndex) {
case 0:
return _buildOverviewContent();
case 1:
return _buildEmployeesContent();
case 2:
return _buildRecruitmentContent();
case 3:
return _buildEvaluationsContent();
case 4:
return _buildLeavesContent();
case 5:
return _buildTrainingContent();
default:
return _buildOverviewContent();
}
}
/// Vue d'ensemble RH
Widget _buildOverviewContent() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec statut RH
_buildHRStatusHeader(),
const SizedBox(height: 20),
// KPIs RH
_buildHRKPIsSection(),
const SizedBox(height: 20),
// Actions rapides RH
_buildHRQuickActions(),
const SizedBox(height: 20),
// Alertes RH importantes
_buildHRAlerts(),
],
),
);
}
/// Placeholder pour les autres sections
Widget _buildEmployeesContent() {
return const Center(
child: Text(
'Gestion des Employés\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildRecruitmentContent() {
return const Center(
child: Text(
'Recrutement\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildEvaluationsContent() {
return const Center(
child: Text(
'Évaluations\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildLeavesContent() {
return const Center(
child: Text(
'Congés et Absences\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildTrainingContent() {
return const Center(
child: Text(
'Formation\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
/// Header avec statut RH
Widget _buildHRStatusHeader() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00A085)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF00B894).withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Département RH Actif',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Dernière sync: ${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')}',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.people,
color: Colors.white,
size: 28,
),
),
],
),
);
}
/// Section KPIs RH
Widget _buildHRKPIsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Indicateurs RH',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildHRKPICard(
'Employés Actifs',
'247',
'+12 ce mois',
Icons.people,
const Color(0xFF00B894),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHRKPICard(
'Candidatures',
'34',
'+8 cette semaine',
Icons.person_add,
const Color(0xFF0984E3),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildHRKPICard(
'En Congé',
'18',
'7.3% de l\'effectif',
Icons.event_busy,
const Color(0xFFFDAB00),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHRKPICard(
'Évaluations',
'156',
'89% complétées',
Icons.star_rate,
const Color(0xFFE17055),
),
),
],
),
],
);
}
/// Widget pour une carte KPI RH
Widget _buildHRKPICard(
String title,
String value,
String subtitle,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 20,
),
),
const Spacer(),
],
),
const SizedBox(height: 12),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
],
),
);
}
/// Actions rapides RH
Widget _buildHRQuickActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.5,
children: [
_buildHRActionCard(
'Nouveau Employé',
Icons.person_add,
const Color(0xFF00B894),
() => _addNewEmployee(),
),
_buildHRActionCard(
'Demandes Congés',
Icons.event_busy,
const Color(0xFFFDAB00),
() => _viewLeaveRequests(),
),
_buildHRActionCard(
'Évaluations',
Icons.star_rate,
const Color(0xFFE17055),
() => _viewEvaluations(),
),
_buildHRActionCard(
'Recrutement',
Icons.work,
const Color(0xFF0984E3),
() => _viewRecruitment(),
),
],
),
],
);
}
/// Widget pour une action RH
Widget _buildHRActionCard(
String title,
IconData icon,
Color color,
VoidCallback onTap,
) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Alertes RH importantes
Widget _buildHRAlerts() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Alertes Importantes',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildHRAlertItem(
'Évaluations en retard',
'12 évaluations annuelles à finaliser',
Icons.warning,
const Color(0xFFE17055),
),
const SizedBox(height: 8),
_buildHRAlertItem(
'Congés à approuver',
'5 demandes de congé en attente',
Icons.pending_actions,
const Color(0xFFFDAB00),
),
const SizedBox(height: 8),
_buildHRAlertItem(
'Nouveaux candidats',
'8 candidatures reçues cette semaine',
Icons.person_add,
const Color(0xFF0984E3),
),
],
);
}
/// Widget pour un élément d'alerte RH
Widget _buildHRAlertItem(
String title,
String message,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: color,
),
),
Text(
message,
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
),
),
],
),
),
],
),
);
}
/// Navigation rapide RH
Widget _buildHRQuickNavigation() {
return Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildHRNavItem(Icons.dashboard, 'Vue', 0),
_buildHRNavItem(Icons.people, 'Employés', 1),
_buildHRNavItem(Icons.person_add, 'Recrutement', 2),
_buildHRNavItem(Icons.star_rate, 'Évaluations', 3),
_buildHRNavItem(Icons.event_busy, 'Congés', 4),
],
),
);
}
/// Widget pour un élément de navigation RH
Widget _buildHRNavItem(IconData icon, String label, int index) {
final isSelected = _selectedIndex == index;
return GestureDetector(
onTap: () => setState(() => _selectedIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF00B894).withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
size: 18,
color: isSelected
? const Color(0xFF00B894)
: Colors.grey[600],
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 9,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? const Color(0xFF00B894)
: Colors.grey[600],
),
),
],
),
),
);
}
// Méthodes d'action
void _showHRNotifications() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notifications RH - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF00B894),
),
);
}
void _generateHRReports() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Rapports RH - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF00B894),
),
);
}
void _openHRSettings() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Paramètres RH - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF00B894),
),
);
}
void _exportHRData() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Export données RH - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF00B894),
),
);
}
void _addNewEmployee() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ajouter employé - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF00B894),
),
);
}
void _viewLeaveRequests() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Demandes de congé - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFFFDAB00),
),
);
}
void _viewEvaluations() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Évaluations - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFFE17055),
),
);
}
void _viewRecruitment() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recrutement - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF0984E3),
),
);
}
}

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../widgets/widgets.dart';
/// Dashboard Control Panel pour Administrateur d'Organisation
///
/// Fonctionnalités exclusives :
@@ -34,6 +35,89 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
floating: false,
pinned: true,
backgroundColor: const Color(0xFF0984E3), // Bleu corporate
actions: [
// Recherche des membres
IconButton(
icon: const Icon(Icons.search, color: Colors.white),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recherche avancée - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF0984E3),
),
);
},
tooltip: 'Rechercher des membres',
),
// Notifications organisation
IconButton(
icon: const Icon(Icons.notifications_outlined, color: Colors.white),
onPressed: () => _showOrgNotifications(),
tooltip: 'Notifications organisation',
),
// Menu d'options
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
onSelected: (value) {
switch (value) {
case 'settings':
_openOrgSettings();
break;
case 'reports':
_generateReports();
break;
case 'export':
_exportOrgData();
break;
case 'backup':
_backupOrgData();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings, size: 20, color: Color(0xFF0984E3)),
SizedBox(width: 12),
Text('Paramètres Org'),
],
),
),
const PopupMenuItem(
value: 'reports',
child: Row(
children: [
Icon(Icons.assessment, size: 20, color: Color(0xFF0984E3)),
SizedBox(width: 12),
Text('Rapports'),
],
),
),
const PopupMenuItem(
value: 'export',
child: Row(
children: [
Icon(Icons.download, size: 20, color: Color(0xFF0984E3)),
SizedBox(width: 12),
Text('Exporter données'),
],
),
),
const PopupMenuItem(
value: 'backup',
child: Row(
children: [
Icon(Icons.backup, size: 20, color: Color(0xFF0984E3)),
SizedBox(width: 12),
Text('Sauvegarde'),
],
),
),
],
),
],
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Control Panel',
@@ -419,7 +503,7 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
),
),
const SizedBox(height: 2),
Icon(
const Icon(
Icons.arrow_forward_ios,
size: 12,
color: ColorTokens.textSecondary,
@@ -443,25 +527,25 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
),
const SizedBox(height: SpacingTokens.md),
DashboardInsightsSection(
const DashboardInsightsSection(
metrics: [
DashboardMetric(
label: 'Cotisations collectées',
value: '89%',
progress: 0.89,
color: const Color(0xFF00B894),
color: Color(0xFF00B894),
),
DashboardMetric(
label: 'Budget utilisé',
value: '67%',
progress: 0.67,
color: const Color(0xFF0984E3),
color: Color(0xFF0984E3),
),
DashboardMetric(
label: 'Objectif annuel',
value: '78%',
progress: 0.78,
color: const Color(0xFFE17055),
color: Color(0xFFE17055),
),
],
),
@@ -483,26 +567,26 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
const SizedBox(height: SpacingTokens.md),
DashboardRecentActivitySection(
activities: [
activities: const [
DashboardActivity(
title: 'Nouveau membre approuvé',
subtitle: 'Sophie Laurent rejoint l\'organisation',
icon: Icons.person_add,
color: const Color(0xFF00B894),
color: Color(0xFF00B894),
time: 'Il y a 2h',
),
DashboardActivity(
title: 'Budget mis à jour',
subtitle: 'Allocation événements modifiée',
icon: Icons.account_balance_wallet,
color: const Color(0xFF0984E3),
color: Color(0xFF0984E3),
time: 'Il y a 4h',
),
DashboardActivity(
title: 'Rapport généré',
subtitle: 'Rapport mensuel d\'activité',
icon: Icons.assessment,
color: const Color(0xFF6C5CE7),
color: Color(0xFF6C5CE7),
time: 'Il y a 1j',
),
],
@@ -533,6 +617,319 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
void _onActivityTap(String activityId) {
// Navigation vers les détails de l'activité
}
/// Afficher les notifications de l'organisation
void _showOrgNotifications() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFF0984E3),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Row(
children: [
const Icon(Icons.business, color: Colors.white),
const SizedBox(width: 12),
const Text(
'Notifications Organisation',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, color: Colors.white),
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildOrgNotificationItem(
'Nouveau membre',
'Marie Dubois a rejoint le département Marketing',
Icons.person_add,
const Color(0xFF00B894),
'10 min',
),
_buildOrgNotificationItem(
'Budget dépassé',
'Le département IT a dépassé son budget mensuel',
Icons.warning,
const Color(0xFFE17055),
'1h',
),
_buildOrgNotificationItem(
'Rapport mensuel',
'Le rapport d\'activité de mars est disponible',
Icons.assessment,
const Color(0xFF0984E3),
'2h',
),
_buildOrgNotificationItem(
'Demande de congé',
'3 nouvelles demandes de congé en attente',
Icons.event_busy,
const Color(0xFFFDAB00),
'3h',
),
],
),
),
],
),
),
);
}
/// Widget pour un élément de notification organisation
Widget _buildOrgNotificationItem(
String title,
String message,
IconData icon,
Color color,
String time,
) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
message,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
Text(
time,
style: TextStyle(
color: Colors.grey[500],
fontSize: 11,
),
),
],
),
);
}
/// Ouvrir les paramètres de l'organisation
void _openOrgSettings() {
// TODO: Naviguer vers la page des paramètres organisation
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Paramètres Organisation - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF0984E3),
),
);
}
/// Générer des rapports
void _generateReports() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Générer un rapport'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Sélectionnez le type de rapport :'),
const SizedBox(height: 16),
ListTile(
leading: const Icon(Icons.people, color: Color(0xFF0984E3)),
title: const Text('Rapport Membres'),
onTap: () {
Navigator.pop(context);
_generateMemberReport();
},
),
ListTile(
leading: const Icon(Icons.attach_money, color: Color(0xFF00B894)),
title: const Text('Rapport Financier'),
onTap: () {
Navigator.pop(context);
_generateFinancialReport();
},
),
ListTile(
leading: const Icon(Icons.analytics, color: Color(0xFFE17055)),
title: const Text('Rapport d\'Activité'),
onTap: () {
Navigator.pop(context);
_generateActivityReport();
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
],
),
);
}
/// Générer rapport des membres
void _generateMemberReport() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Génération du rapport membres en cours...'),
backgroundColor: Color(0xFF0984E3),
),
);
}
/// Générer rapport financier
void _generateFinancialReport() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Génération du rapport financier en cours...'),
backgroundColor: Color(0xFF00B894),
),
);
}
/// Générer rapport d'activité
void _generateActivityReport() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Génération du rapport d\'activité en cours...'),
backgroundColor: Color(0xFFE17055),
),
);
}
/// Exporter les données de l'organisation
void _exportOrgData() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Exporter les données'),
content: const Text(
'Sélectionnez le format d\'export souhaité :',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Export CSV en cours...'),
backgroundColor: Color(0xFF00B894),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0984E3),
),
child: const Text('CSV', style: TextStyle(color: Colors.white)),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Export Excel en cours...'),
backgroundColor: Color(0xFF00B894),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0984E3),
),
child: const Text('Excel', style: TextStyle(color: Colors.white)),
),
],
),
);
}
/// Sauvegarder les données de l'organisation
void _backupOrgData() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Sauvegarde Organisation'),
content: const Text(
'Voulez-vous créer une sauvegarde complète des données de l\'organisation ?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sauvegarde en cours...'),
backgroundColor: Color(0xFF0984E3),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0984E3),
),
child: const Text('Confirmer', style: TextStyle(color: Colors.white)),
),
],
),
);
}
}
/// Painter pour le motif corporate de l'en-tête

View File

@@ -3,9 +3,7 @@
library dashboard_quick_action_button;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Modèle de données pour une action rapide
class DashboardQuickAction {

View File

@@ -0,0 +1,297 @@
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/tokens.dart';
/// Widget pour afficher une grille d'actions rapides
class DashboardQuickActionsGrid extends StatelessWidget {
final List<Widget> children;
final int crossAxisCount;
const DashboardQuickActionsGrid({
super.key,
required this.children,
this.crossAxisCount = 2,
});
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossAxisCount,
childAspectRatio: 1.2,
crossAxisSpacing: SpacingTokens.md,
mainAxisSpacing: SpacingTokens.md,
children: children,
);
}
}
/// Widget pour une action rapide
class DashboardQuickAction extends StatelessWidget {
final String title;
final IconData icon;
final Color? color;
final VoidCallback? onTap;
const DashboardQuickAction({
super.key,
required this.title,
required this.icon,
this.color,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RadiusTokens.md),
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 32,
color: color ?? ColorTokens.primary,
),
const SizedBox(height: SpacingTokens.sm),
Text(
title,
style: TypographyTokens.bodyMedium,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
}
/// Widget pour afficher une section d'activité récente
class DashboardRecentActivitySection extends StatelessWidget {
final List<Widget> children;
final String title;
const DashboardRecentActivitySection({
super.key,
required this.children,
this.title = 'Activité Récente',
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.headlineSmall,
),
const SizedBox(height: SpacingTokens.md),
...children,
],
);
}
}
/// Widget pour une activité
class DashboardActivity extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Color? color;
const DashboardActivity({
super.key,
required this.title,
required this.subtitle,
required this.icon,
this.color,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: SpacingTokens.sm),
child: ListTile(
leading: CircleAvatar(
backgroundColor: color ?? ColorTokens.primary,
child: Icon(icon, color: Colors.white),
),
title: Text(title),
subtitle: Text(subtitle),
),
);
}
}
/// Widget pour une section d'insights
class DashboardInsightsSection extends StatelessWidget {
final List<Widget> children;
const DashboardInsightsSection({
super.key,
required this.children,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Insights',
style: TypographyTokens.headlineSmall,
),
const SizedBox(height: SpacingTokens.md),
...children,
],
);
}
}
/// Widget pour une statistique
class DashboardStat extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color? color;
final VoidCallback? onTap;
const DashboardStat({
super.key,
required this.title,
required this.value,
required this.icon,
this.color,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RadiusTokens.md),
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 32,
color: color ?? ColorTokens.primary,
),
const SizedBox(height: SpacingTokens.sm),
Text(
value,
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.bold,
color: color ?? ColorTokens.primary,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
title,
style: TypographyTokens.bodySmall,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
}
/// Widget pour la grille de statistiques
class DashboardStatsGrid extends StatelessWidget {
final List<Widget> children;
final int crossAxisCount;
const DashboardStatsGrid({
super.key,
required this.children,
this.crossAxisCount = 2,
});
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossAxisCount,
childAspectRatio: 1.2,
crossAxisSpacing: SpacingTokens.md,
mainAxisSpacing: SpacingTokens.md,
children: children,
);
}
}
/// Widget pour le drawer du dashboard
class DashboardDrawer extends StatelessWidget {
const DashboardDrawer({super.key});
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(
color: ColorTokens.primary,
),
child: Text(
'UnionFlow',
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
ListTile(
leading: const Icon(Icons.dashboard),
title: const Text('Dashboard'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.people),
title: const Text('Membres'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.event),
title: const Text('Événements'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Paramètres'),
onTap: () {
Navigator.pop(context);
},
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
/// Modèle de données pour un membre
class MembreModel {
final String id;
final String nom;
final String prenom;
final String email;
final String? telephone;
final String? statut;
final String? role;
final OrganisationModel? organisation;
const MembreModel({
required this.id,
required this.nom,
required this.prenom,
required this.email,
this.telephone,
this.statut,
this.role,
this.organisation,
});
factory MembreModel.fromJson(Map<String, dynamic> json) {
return MembreModel(
id: json['id'] as String,
nom: json['nom'] as String,
prenom: json['prenom'] as String,
email: json['email'] as String,
telephone: json['telephone'] as String?,
statut: json['statut'] as String?,
role: json['role'] as String?,
organisation: json['organisation'] != null
? OrganisationModel.fromJson(json['organisation'] as Map<String, dynamic>)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'nom': nom,
'prenom': prenom,
'email': email,
'telephone': telephone,
'statut': statut,
'role': role,
'organisation': organisation?.toJson(),
};
}
}
/// Modèle pour une organisation
class OrganisationModel {
final String? nom;
const OrganisationModel({this.nom});
factory OrganisationModel.fromJson(Map<String, dynamic> json) {
return OrganisationModel(
nom: json['nom'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'nom': nom,
};
}
}

View File

@@ -0,0 +1,278 @@
import 'package:dio/dio.dart';
import '../../../../core/models/membre_search_criteria.dart';
import '../../../../core/models/membre_search_result.dart';
/// Service pour la recherche avancée de membres
/// Gère les appels API vers l'endpoint de recherche sophistiquée
class MembreSearchService {
final Dio _dio;
MembreSearchService(this._dio);
/// Effectue une recherche avancée de membres
///
/// [criteria] Critères de recherche
/// [page] Numéro de page (0-based)
/// [size] Taille de la page
/// [sortField] Champ de tri
/// [sortDirection] Direction du tri (asc/desc)
///
/// Returns [MembreSearchResult] avec les résultats paginés
Future<MembreSearchResult> searchMembresAdvanced({
required MembreSearchCriteria criteria,
int page = 0,
int size = 20,
String sortField = 'nom',
String sortDirection = 'asc',
}) async {
print('Recherche avancée de membres: ${criteria.description}');
try {
// Validation des critères
if (!criteria.hasAnyCriteria) {
throw Exception('Au moins un critère de recherche doit être spécifié');
}
if (!criteria.isValid) {
throw Exception('Critères de recherche invalides');
}
// Préparation des paramètres de requête
final queryParams = {
'page': page.toString(),
'size': size.toString(),
'sort': sortField,
'direction': sortDirection,
};
// Appel API
final response = await _dio.post(
'/api/membres/search/advanced',
data: criteria.toJson(),
queryParameters: queryParams,
);
// Parsing de la réponse
final result = MembreSearchResult.fromJson(response.data);
print('Recherche terminée: ${result.totalElements} résultats en ${result.executionTimeMs}ms');
return result;
} on DioException catch (e) {
print('Erreur lors de la recherche avancée: $e');
rethrow;
} catch (e) {
print('Erreur inattendue lors de la recherche: $e');
rethrow;
}
}
/// Recherche rapide par terme général
///
/// [query] Terme de recherche
/// [page] Numéro de page
/// [size] Taille de la page
///
/// Returns [MembreSearchResult] avec les résultats
Future<MembreSearchResult> quickSearch({
required String query,
int page = 0,
int size = 20,
}) async {
final criteria = MembreSearchCriteria.quickSearch(query);
return searchMembresAdvanced(
criteria: criteria,
page: page,
size: size,
);
}
/// Recherche des membres actifs uniquement
///
/// [page] Numéro de page
/// [size] Taille de la page
///
/// Returns [MembreSearchResult] avec les membres actifs
Future<MembreSearchResult> searchActiveMembers({
int page = 0,
int size = 20,
}) async {
return searchMembresAdvanced(
criteria: MembreSearchCriteria.activeMembers,
page: page,
size: size,
);
}
/// Recherche des membres du bureau
///
/// [page] Numéro de page
/// [size] Taille de la page
///
/// Returns [MembreSearchResult] avec les membres du bureau
Future<MembreSearchResult> searchBureauMembers({
int page = 0,
int size = 20,
}) async {
return searchMembresAdvanced(
criteria: MembreSearchCriteria.bureauMembers,
page: page,
size: size,
);
}
/// Recherche par organisation
///
/// [organisationIds] Liste des IDs d'organisations
/// [page] Numéro de page
/// [size] Taille de la page
///
/// Returns [MembreSearchResult] avec les membres des organisations
Future<MembreSearchResult> searchByOrganisations({
required List<String> organisationIds,
int page = 0,
int size = 20,
}) async {
final criteria = MembreSearchCriteria(
organisationIds: organisationIds,
statut: 'ACTIF',
);
return searchMembresAdvanced(
criteria: criteria,
page: page,
size: size,
);
}
/// Recherche par tranche d'âge
///
/// [ageMin] Âge minimum
/// [ageMax] Âge maximum
/// [page] Numéro de page
/// [size] Taille de la page
///
/// Returns [MembreSearchResult] avec les membres dans la tranche d'âge
Future<MembreSearchResult> searchByAgeRange({
int? ageMin,
int? ageMax,
int page = 0,
int size = 20,
}) async {
final criteria = MembreSearchCriteria(
ageMin: ageMin,
ageMax: ageMax,
statut: 'ACTIF',
);
return searchMembresAdvanced(
criteria: criteria,
page: page,
size: size,
);
}
/// Recherche par région
///
/// [region] Nom de la région
/// [page] Numéro de page
/// [size] Taille de la page
///
/// Returns [MembreSearchResult] avec les membres de la région
Future<MembreSearchResult> searchByRegion({
required String region,
int page = 0,
int size = 20,
}) async {
final criteria = MembreSearchCriteria(
region: region,
statut: 'ACTIF',
);
return searchMembresAdvanced(
criteria: criteria,
page: page,
size: size,
);
}
/// Recherche par rôles
///
/// [roles] Liste des rôles
/// [page] Numéro de page
/// [size] Taille de la page
///
/// Returns [MembreSearchResult] avec les membres ayant ces rôles
Future<MembreSearchResult> searchByRoles({
required List<String> roles,
int page = 0,
int size = 20,
}) async {
final criteria = MembreSearchCriteria(
roles: roles,
statut: 'ACTIF',
);
return searchMembresAdvanced(
criteria: criteria,
page: page,
size: size,
);
}
/// Recherche par période d'adhésion
///
/// [dateMin] Date minimum (ISO 8601)
/// [dateMax] Date maximum (ISO 8601)
/// [page] Numéro de page
/// [size] Taille de la page
///
/// Returns [MembreSearchResult] avec les membres adhérés dans la période
Future<MembreSearchResult> searchByAdhesionPeriod({
String? dateMin,
String? dateMax,
int page = 0,
int size = 20,
}) async {
final criteria = MembreSearchCriteria(
dateAdhesionMin: dateMin,
dateAdhesionMax: dateMax,
statut: 'ACTIF',
);
return searchMembresAdvanced(
criteria: criteria,
page: page,
size: size,
);
}
/// Valide les critères de recherche avant envoi
bool validateCriteria(MembreSearchCriteria criteria) {
if (!criteria.hasAnyCriteria) {
print('Aucun critère de recherche spécifié');
return false;
}
if (!criteria.isValid) {
print('Critères de recherche invalides: ${criteria.description}');
return false;
}
return true;
}
/// Estime le temps de recherche basé sur les critères
Duration estimateSearchTime(MembreSearchCriteria criteria) {
// Estimation basique - peut être améliorée avec des métriques réelles
int complexityScore = 0;
if (criteria.query?.isNotEmpty == true) complexityScore += 2;
if (criteria.organisationIds?.isNotEmpty == true) complexityScore += 1;
if (criteria.roles?.isNotEmpty == true) complexityScore += 1;
if (criteria.ageMin != null || criteria.ageMax != null) complexityScore += 1;
if (criteria.dateAdhesionMin != null || criteria.dateAdhesionMax != null) complexityScore += 1;
// Temps de base + complexité
final baseTime = 100; // 100ms de base
final additionalTime = complexityScore * 50; // 50ms par critère
return Duration(milliseconds: baseTime + additionalTime);
}
}

View File

@@ -0,0 +1,579 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/models/membre_search_criteria.dart';
import '../../../../core/models/membre_search_result.dart';
import '../../../dashboard/presentation/widgets/dashboard_activity_tile.dart';
import '../widgets/membre_search_form.dart';
import '../widgets/membre_search_results.dart';
import '../widgets/search_statistics_card.dart';
/// Page de recherche avancée des membres
/// Interface complète pour la recherche sophistiquée avec filtres multiples
class AdvancedSearchPage extends StatefulWidget {
const AdvancedSearchPage({super.key});
@override
State<AdvancedSearchPage> createState() => _AdvancedSearchPageState();
}
class _AdvancedSearchPageState extends State<AdvancedSearchPage>
with TickerProviderStateMixin {
late TabController _tabController;
MembreSearchCriteria _currentCriteria = MembreSearchCriteria.empty;
MembreSearchResult? _currentResult;
bool _isSearching = false;
String? _errorMessage;
// Contrôleurs pour les champs de recherche
final _queryController = TextEditingController();
final _nomController = TextEditingController();
final _prenomController = TextEditingController();
final _emailController = TextEditingController();
final _telephoneController = TextEditingController();
final _regionController = TextEditingController();
final _villeController = TextEditingController();
final _professionController = TextEditingController();
// Valeurs pour les filtres
String? _selectedStatut;
List<String> _selectedRoles = [];
List<String> _selectedOrganisations = [];
RangeValues _ageRange = const RangeValues(18, 65);
DateTimeRange? _adhesionDateRange;
bool _includeInactifs = false;
bool _membreBureau = false;
bool _responsable = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
_queryController.dispose();
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_regionController.dispose();
_villeController.dispose();
_professionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Recherche Avancée'),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
elevation: 0,
bottom: TabBar(
controller: _tabController,
indicatorColor: Colors.white,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
tabs: const [
Tab(icon: Icon(Icons.search), text: 'Critères'),
Tab(icon: Icon(Icons.list), text: 'Résultats'),
Tab(icon: Icon(Icons.analytics), text: 'Statistiques'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildSearchCriteriaTab(),
_buildSearchResultsTab(),
_buildStatisticsTab(),
],
),
floatingActionButton: _buildSearchFab(),
);
}
/// Onglet des critères de recherche
Widget _buildSearchCriteriaTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Recherche rapide
_buildQuickSearchSection(),
const SizedBox(height: 24),
// Critères détaillés
_buildDetailedCriteriaSection(),
const SizedBox(height: 24),
// Filtres avancés
_buildAdvancedFiltersSection(),
const SizedBox(height: 24),
// Boutons d'action
_buildActionButtons(),
],
),
);
}
/// Section de recherche rapide
Widget _buildQuickSearchSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.flash_on, color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
'Recherche Rapide',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _queryController,
decoration: const InputDecoration(
labelText: 'Rechercher un membre',
hintText: 'Nom, prénom ou email...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onSubmitted: (_) => _performQuickSearch(),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
_buildQuickFilterChip('Membres actifs', () {
_selectedStatut = 'ACTIF';
_includeInactifs = false;
}),
_buildQuickFilterChip('Membres bureau', () {
_membreBureau = true;
_selectedStatut = 'ACTIF';
}),
_buildQuickFilterChip('Responsables', () {
_responsable = true;
_selectedStatut = 'ACTIF';
}),
],
),
],
),
),
);
}
/// Section des critères détaillés
Widget _buildDetailedCriteriaSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.tune, color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
'Critères Détaillés',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _prenomController,
decoration: const InputDecoration(
labelText: 'Prénom',
border: OutlineInputBorder(),
),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'exemple@unionflow.com',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _telephoneController,
decoration: const InputDecoration(
labelText: 'Téléphone',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 12),
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedStatut,
decoration: const InputDecoration(
labelText: 'Statut',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(value: null, child: Text('Tous')),
DropdownMenuItem(value: 'ACTIF', child: Text('Actif')),
DropdownMenuItem(value: 'INACTIF', child: Text('Inactif')),
DropdownMenuItem(value: 'SUSPENDU', child: Text('Suspendu')),
DropdownMenuItem(value: 'RADIE', child: Text('Radié')),
],
onChanged: (value) => setState(() => _selectedStatut = value),
),
),
],
),
],
),
),
);
}
/// Section des filtres avancés
Widget _buildAdvancedFiltersSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.filter_alt, color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
'Filtres Avancés',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
// Tranche d'âge
Text('Tranche d\'âge: ${_ageRange.start.round()}-${_ageRange.end.round()} ans'),
RangeSlider(
values: _ageRange,
min: 18,
max: 80,
divisions: 62,
labels: RangeLabels(
'${_ageRange.start.round()}',
'${_ageRange.end.round()}',
),
onChanged: (values) => setState(() => _ageRange = values),
),
const SizedBox(height: 16),
// Options booléennes
CheckboxListTile(
title: const Text('Inclure les membres inactifs'),
value: _includeInactifs,
onChanged: (value) => setState(() => _includeInactifs = value ?? false),
),
CheckboxListTile(
title: const Text('Membres du bureau uniquement'),
value: _membreBureau,
onChanged: (value) => setState(() => _membreBureau = value ?? false),
),
CheckboxListTile(
title: const Text('Responsables uniquement'),
value: _responsable,
onChanged: (value) => setState(() => _responsable = value ?? false),
),
],
),
),
);
}
/// Boutons d'action
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _clearCriteria,
icon: const Icon(Icons.clear),
label: const Text('Effacer'),
),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: ElevatedButton.icon(
onPressed: _isSearching ? null : _performAdvancedSearch,
icon: _isSearching
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.search),
label: Text(_isSearching ? 'Recherche...' : 'Rechercher'),
),
),
],
);
}
/// Onglet des résultats
Widget _buildSearchResultsTab() {
if (_currentResult == null) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucune recherche effectuée',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'Utilisez l\'onglet Critères pour lancer une recherche',
style: TextStyle(color: Colors.grey),
),
],
),
);
}
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Erreur de recherche',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => setState(() => _errorMessage = null),
child: const Text('Réessayer'),
),
],
),
);
}
return MembreSearchResults(result: _currentResult!);
}
/// Onglet des statistiques
Widget _buildStatisticsTab() {
if (_currentResult?.statistics == null) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.analytics, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucune statistique disponible',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'Effectuez une recherche pour voir les statistiques',
style: TextStyle(color: Colors.grey),
),
],
),
);
}
return SearchStatisticsCard(statistics: _currentResult!.statistics!);
}
/// FAB de recherche
Widget _buildSearchFab() {
return FloatingActionButton.extended(
onPressed: _isSearching ? null : _performAdvancedSearch,
icon: _isSearching
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.search),
label: Text(_isSearching ? 'Recherche...' : 'Rechercher'),
);
}
/// Chip de filtre rapide
Widget _buildQuickFilterChip(String label, VoidCallback onTap) {
return ActionChip(
label: Text(label),
onPressed: onTap,
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
labelStyle: TextStyle(color: Theme.of(context).primaryColor),
);
}
/// Effectue une recherche rapide
void _performQuickSearch() {
if (_queryController.text.trim().isEmpty) return;
final criteria = MembreSearchCriteria.quickSearch(_queryController.text.trim());
_performSearch(criteria);
}
/// Effectue une recherche avancée
void _performAdvancedSearch() {
final criteria = _buildSearchCriteria();
_performSearch(criteria);
}
/// Construit les critères de recherche à partir des champs
MembreSearchCriteria _buildSearchCriteria() {
return MembreSearchCriteria(
query: _queryController.text.trim().isEmpty ? null : _queryController.text.trim(),
nom: _nomController.text.trim().isEmpty ? null : _nomController.text.trim(),
prenom: _prenomController.text.trim().isEmpty ? null : _prenomController.text.trim(),
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
telephone: _telephoneController.text.trim().isEmpty ? null : _telephoneController.text.trim(),
statut: _selectedStatut,
ageMin: _ageRange.start.round(),
ageMax: _ageRange.end.round(),
region: _regionController.text.trim().isEmpty ? null : _regionController.text.trim(),
ville: _villeController.text.trim().isEmpty ? null : _villeController.text.trim(),
profession: _professionController.text.trim().isEmpty ? null : _professionController.text.trim(),
organisationIds: _selectedOrganisations.isEmpty ? null : _selectedOrganisations,
roles: _selectedRoles.isEmpty ? null : _selectedRoles,
membreBureau: _membreBureau ? true : null,
responsable: _responsable ? true : null,
includeInactifs: _includeInactifs,
);
}
/// Effectue la recherche
void _performSearch(MembreSearchCriteria criteria) async {
if (!criteria.hasAnyCriteria) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez spécifier au moins un critère de recherche'),
backgroundColor: Colors.orange,
),
);
return;
}
setState(() {
_isSearching = true;
_errorMessage = null;
_currentCriteria = criteria;
});
try {
// TODO: Appeler le service de recherche
// final result = await _searchService.searchMembresAdvanced(criteria: criteria);
// Simulation pour l'instant
await Future.delayed(const Duration(seconds: 2));
final result = MembreSearchResult.empty(criteria);
setState(() {
_currentResult = result;
_isSearching = false;
});
// Basculer vers l'onglet des résultats
_tabController.animateTo(1);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.resultDescription),
backgroundColor: Colors.green,
),
);
} catch (e) {
setState(() {
_errorMessage = e.toString();
_isSearching = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur de recherche: $e'),
backgroundColor: Colors.red,
),
);
}
}
/// Efface tous les critères
void _clearCriteria() {
setState(() {
_queryController.clear();
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_regionController.clear();
_villeController.clear();
_professionController.clear();
_selectedStatut = null;
_selectedRoles.clear();
_selectedOrganisations.clear();
_ageRange = const RangeValues(18, 65);
_adhesionDateRange = null;
_includeInactifs = false;
_membreBureau = false;
_responsable = false;
_currentResult = null;
_errorMessage = null;
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,433 @@
import 'package:flutter/material.dart';
import '../../../../core/models/membre_search_criteria.dart';
/// Formulaire de recherche de membres
/// Widget réutilisable pour la saisie des critères de recherche
class MembreSearchForm extends StatefulWidget {
final MembreSearchCriteria initialCriteria;
final Function(MembreSearchCriteria) onCriteriaChanged;
final VoidCallback? onSearch;
final VoidCallback? onClear;
final bool isCompact;
const MembreSearchForm({
super.key,
this.initialCriteria = MembreSearchCriteria.empty,
required this.onCriteriaChanged,
this.onSearch,
this.onClear,
this.isCompact = false,
});
@override
State<MembreSearchForm> createState() => _MembreSearchFormState();
}
class _MembreSearchFormState extends State<MembreSearchForm> {
late TextEditingController _queryController;
late TextEditingController _nomController;
late TextEditingController _prenomController;
late TextEditingController _emailController;
late TextEditingController _telephoneController;
String? _selectedStatut;
List<String> _selectedRoles = [];
RangeValues _ageRange = const RangeValues(18, 65);
bool _includeInactifs = false;
bool _membreBureau = false;
bool _responsable = false;
@override
void initState() {
super.initState();
_initializeControllers();
_loadInitialCriteria();
}
@override
void dispose() {
_queryController.dispose();
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
super.dispose();
}
void _initializeControllers() {
_queryController = TextEditingController();
_nomController = TextEditingController();
_prenomController = TextEditingController();
_emailController = TextEditingController();
_telephoneController = TextEditingController();
// Écouter les changements pour mettre à jour les critères
_queryController.addListener(_updateCriteria);
_nomController.addListener(_updateCriteria);
_prenomController.addListener(_updateCriteria);
_emailController.addListener(_updateCriteria);
_telephoneController.addListener(_updateCriteria);
}
void _loadInitialCriteria() {
final criteria = widget.initialCriteria;
_queryController.text = criteria.query ?? '';
_nomController.text = criteria.nom ?? '';
_prenomController.text = criteria.prenom ?? '';
_emailController.text = criteria.email ?? '';
_telephoneController.text = criteria.telephone ?? '';
_selectedStatut = criteria.statut;
_selectedRoles = criteria.roles ?? [];
_ageRange = RangeValues(
criteria.ageMin?.toDouble() ?? 18,
criteria.ageMax?.toDouble() ?? 65,
);
_includeInactifs = criteria.includeInactifs;
_membreBureau = criteria.membreBureau ?? false;
_responsable = criteria.responsable ?? false;
}
@override
Widget build(BuildContext context) {
if (widget.isCompact) {
return _buildCompactForm();
}
return _buildFullForm();
}
/// Formulaire compact pour recherche rapide
Widget _buildCompactForm() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _queryController,
decoration: InputDecoration(
labelText: 'Rechercher un membre',
hintText: 'Nom, prénom ou email...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _queryController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_queryController.clear();
_updateCriteria();
},
)
: null,
border: const OutlineInputBorder(),
),
onSubmitted: (_) => widget.onSearch?.call(),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedStatut,
decoration: const InputDecoration(
labelText: 'Statut',
border: OutlineInputBorder(),
isDense: true,
),
items: const [
DropdownMenuItem(value: null, child: Text('Tous')),
DropdownMenuItem(value: 'ACTIF', child: Text('Actif')),
DropdownMenuItem(value: 'INACTIF', child: Text('Inactif')),
],
onChanged: (value) {
setState(() => _selectedStatut = value);
_updateCriteria();
},
),
),
const SizedBox(width: 12),
if (widget.onSearch != null)
ElevatedButton.icon(
onPressed: widget.onSearch,
icon: const Icon(Icons.search),
label: const Text('Rechercher'),
),
],
),
],
),
),
);
}
/// Formulaire complet avec tous les critères
Widget _buildFullForm() {
return Column(
children: [
// Recherche générale
_buildGeneralSearchSection(),
const SizedBox(height: 16),
// Critères détaillés
_buildDetailedCriteriaSection(),
const SizedBox(height: 16),
// Filtres avancés
_buildAdvancedFiltersSection(),
const SizedBox(height: 16),
// Boutons d'action
_buildActionButtons(),
],
);
}
/// Section de recherche générale
Widget _buildGeneralSearchSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recherche Générale',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
controller: _queryController,
decoration: const InputDecoration(
labelText: 'Terme de recherche',
hintText: 'Nom, prénom, email...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
),
],
),
),
);
}
/// Section des critères détaillés
Widget _buildDetailedCriteriaSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Critères Détaillés',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _prenomController,
decoration: const InputDecoration(
labelText: 'Prénom',
border: OutlineInputBorder(),
),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _telephoneController,
decoration: const InputDecoration(
labelText: 'Téléphone',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 12),
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedStatut,
decoration: const InputDecoration(
labelText: 'Statut',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(value: null, child: Text('Tous')),
DropdownMenuItem(value: 'ACTIF', child: Text('Actif')),
DropdownMenuItem(value: 'INACTIF', child: Text('Inactif')),
DropdownMenuItem(value: 'SUSPENDU', child: Text('Suspendu')),
DropdownMenuItem(value: 'RADIE', child: Text('Radié')),
],
onChanged: (value) {
setState(() => _selectedStatut = value);
_updateCriteria();
},
),
),
],
),
],
),
),
);
}
/// Section des filtres avancés
Widget _buildAdvancedFiltersSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filtres Avancés',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Tranche d'âge
Text('Tranche d\'âge: ${_ageRange.start.round()}-${_ageRange.end.round()} ans'),
RangeSlider(
values: _ageRange,
min: 18,
max: 80,
divisions: 62,
labels: RangeLabels(
'${_ageRange.start.round()}',
'${_ageRange.end.round()}',
),
onChanged: (values) {
setState(() => _ageRange = values);
_updateCriteria();
},
),
const SizedBox(height: 16),
// Options booléennes
CheckboxListTile(
title: const Text('Inclure les membres inactifs'),
value: _includeInactifs,
onChanged: (value) {
setState(() => _includeInactifs = value ?? false);
_updateCriteria();
},
),
CheckboxListTile(
title: const Text('Membres du bureau uniquement'),
value: _membreBureau,
onChanged: (value) {
setState(() => _membreBureau = value ?? false);
_updateCriteria();
},
),
CheckboxListTile(
title: const Text('Responsables uniquement'),
value: _responsable,
onChanged: (value) {
setState(() => _responsable = value ?? false);
_updateCriteria();
},
),
],
),
),
);
}
/// Boutons d'action
Widget _buildActionButtons() {
return Row(
children: [
if (widget.onClear != null)
Expanded(
child: OutlinedButton.icon(
onPressed: () {
_clearForm();
widget.onClear?.call();
},
icon: const Icon(Icons.clear),
label: const Text('Effacer'),
),
),
if (widget.onClear != null && widget.onSearch != null)
const SizedBox(width: 16),
if (widget.onSearch != null)
Expanded(
flex: widget.onClear != null ? 2 : 1,
child: ElevatedButton.icon(
onPressed: widget.onSearch,
icon: const Icon(Icons.search),
label: const Text('Rechercher'),
),
),
],
);
}
/// Met à jour les critères de recherche
void _updateCriteria() {
final criteria = MembreSearchCriteria(
query: _queryController.text.trim().isEmpty ? null : _queryController.text.trim(),
nom: _nomController.text.trim().isEmpty ? null : _nomController.text.trim(),
prenom: _prenomController.text.trim().isEmpty ? null : _prenomController.text.trim(),
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
telephone: _telephoneController.text.trim().isEmpty ? null : _telephoneController.text.trim(),
statut: _selectedStatut,
roles: _selectedRoles.isEmpty ? null : _selectedRoles,
ageMin: _ageRange.start.round(),
ageMax: _ageRange.end.round(),
membreBureau: _membreBureau ? true : null,
responsable: _responsable ? true : null,
includeInactifs: _includeInactifs,
);
widget.onCriteriaChanged(criteria);
}
/// Efface le formulaire
void _clearForm() {
setState(() {
_queryController.clear();
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_selectedStatut = null;
_selectedRoles.clear();
_ageRange = const RangeValues(18, 65);
_includeInactifs = false;
_membreBureau = false;
_responsable = false;
});
_updateCriteria();
}
}

View File

@@ -0,0 +1,388 @@
import 'package:flutter/material.dart';
import '../../../../core/models/membre_search_result.dart' as search_model;
import '../../data/models/membre_model.dart' as member_model;
/// Widget d'affichage des résultats de recherche de membres
/// Gère la pagination, le tri et l'affichage des membres trouvés
class MembreSearchResults extends StatefulWidget {
final search_model.MembreSearchResult result;
final Function(member_model.MembreModel)? onMembreSelected;
final bool showPagination;
const MembreSearchResults({
super.key,
required this.result,
this.onMembreSelected,
this.showPagination = true,
});
@override
State<MembreSearchResults> createState() => _MembreSearchResultsState();
}
class _MembreSearchResultsState extends State<MembreSearchResults> {
@override
Widget build(BuildContext context) {
if (widget.result.isEmpty) {
return _buildEmptyState();
}
return Column(
children: [
// En-tête avec informations sur les résultats
_buildResultsHeader(),
// Liste des membres
Expanded(
child: _buildMembersList(),
),
// Pagination si activée
if (widget.showPagination && widget.result.totalPages > 1)
_buildPagination(),
],
);
}
/// État vide quand aucun résultat
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Aucun membre trouvé',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Essayez de modifier vos critères de recherche',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[500],
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.tune),
label: const Text('Modifier les critères'),
),
],
),
);
}
/// En-tête avec informations sur les résultats
Widget _buildResultsHeader() {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.search,
color: Theme.of(context).primaryColor,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.result.resultDescription,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
Chip(
label: Text('${widget.result.executionTimeMs}ms'),
backgroundColor: Colors.green.withOpacity(0.1),
labelStyle: const TextStyle(
color: Colors.green,
fontSize: 12,
),
),
],
),
if (widget.result.criteria.description.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Critères: ${widget.result.criteria.description}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
);
}
/// Liste des membres trouvés
Widget _buildMembersList() {
return ListView.builder(
itemCount: widget.result.membres.length,
itemBuilder: (context, index) {
final membre = widget.result.membres[index];
return _buildMembreCard(membre, index);
},
);
}
/// Carte d'affichage d'un membre
Widget _buildMembreCard(member_model.MembreModel membre, int index) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: _getStatusColor(membre.statut ?? 'ACTIF'),
child: Text(
_getInitials(membre.nom, membre.prenom),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
'${membre.prenom} ${membre.nom}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (membre.email.isNotEmpty)
Row(
children: [
const Icon(Icons.email, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
membre.email,
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
),
if (membre.telephone?.isNotEmpty == true)
Row(
children: [
const Icon(Icons.phone, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Text(
membre.telephone!,
style: const TextStyle(fontSize: 12),
),
],
),
if (membre.organisation?.nom?.isNotEmpty == true)
Row(
children: [
const Icon(Icons.business, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
membre.organisation!.nom!,
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatusChip(membre.statut ?? 'ACTIF'),
if (membre.role?.isNotEmpty == true) ...[
const SizedBox(height: 4),
Text(
_formatRoles(membre.role!),
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
],
],
),
onTap: widget.onMembreSelected != null
? () => widget.onMembreSelected!(membre)
: null,
),
);
}
/// Pagination
Widget _buildPagination() {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border(
top: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Bouton page précédente
ElevatedButton.icon(
onPressed: widget.result.hasPrevious ? _goToPreviousPage : null,
icon: const Icon(Icons.chevron_left),
label: const Text('Précédent'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[100],
foregroundColor: Colors.grey[700],
),
),
// Indicateur de page
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Page ${widget.result.currentPage + 1} / ${widget.result.totalPages}',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
// Bouton page suivante
ElevatedButton.icon(
onPressed: widget.result.hasNext ? _goToNextPage : null,
icon: const Icon(Icons.chevron_right),
label: const Text('Suivant'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
),
),
],
),
);
}
/// Chip de statut
Widget _buildStatusChip(String statut) {
final color = _getStatusColor(statut);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color, width: 1),
),
child: Text(
_getStatusLabel(statut),
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
);
}
/// Obtient la couleur du statut
Color _getStatusColor(String statut) {
switch (statut.toUpperCase()) {
case 'ACTIF':
return Colors.green;
case 'INACTIF':
return Colors.orange;
case 'SUSPENDU':
return Colors.red;
case 'RADIE':
return Colors.grey;
default:
return Colors.grey;
}
}
/// Obtient le libellé du statut
String _getStatusLabel(String statut) {
switch (statut.toUpperCase()) {
case 'ACTIF':
return 'Actif';
case 'INACTIF':
return 'Inactif';
case 'SUSPENDU':
return 'Suspendu';
case 'RADIE':
return 'Radié';
default:
return statut;
}
}
/// Obtient les initiales d'un membre
String _getInitials(String nom, String prenom) {
final nomInitial = nom.isNotEmpty ? nom[0].toUpperCase() : '';
final prenomInitial = prenom.isNotEmpty ? prenom[0].toUpperCase() : '';
return '$prenomInitial$nomInitial';
}
/// Formate les rôles pour l'affichage
String _formatRoles(String roles) {
final rolesList = roles.split(',').map((r) => r.trim()).toList();
if (rolesList.length <= 2) {
return rolesList.join(', ');
}
return '${rolesList.take(2).join(', ')}...';
}
/// Navigation vers la page précédente
void _goToPreviousPage() {
// TODO: Implémenter la navigation vers la page précédente
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Navigation vers la page précédente'),
duration: Duration(seconds: 1),
),
);
}
/// Navigation vers la page suivante
void _goToNextPage() {
// TODO: Implémenter la navigation vers la page suivante
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Navigation vers la page suivante'),
duration: Duration(seconds: 1),
),
);
}
}

View File

@@ -0,0 +1,416 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../../core/models/membre_search_result.dart';
/// Widget d'affichage des statistiques de recherche
/// Présente les métriques et graphiques des résultats de recherche
class SearchStatisticsCard extends StatelessWidget {
final SearchStatistics statistics;
const SearchStatisticsCard({
super.key,
required this.statistics,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre
Row(
children: [
Icon(
Icons.analytics,
color: Theme.of(context).primaryColor,
size: 24,
),
const SizedBox(width: 8),
Text(
'Statistiques de Recherche',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 24),
// Métriques principales
_buildMainMetrics(context),
const SizedBox(height: 24),
// Graphique de répartition actifs/inactifs
_buildStatusChart(context),
const SizedBox(height: 24),
// Métriques détaillées
_buildDetailedMetrics(context),
const SizedBox(height: 24),
// Informations complémentaires
_buildAdditionalInfo(context),
],
),
);
}
/// Métriques principales
Widget _buildMainMetrics(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Vue d\'ensemble',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildMetricCard(
context,
'Total Membres',
statistics.totalMembres.toString(),
Icons.people,
Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildMetricCard(
context,
'Membres Actifs',
statistics.membresActifs.toString(),
Icons.person,
Colors.green,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildMetricCard(
context,
'Âge Moyen',
'${statistics.ageMoyen.toStringAsFixed(1)} ans',
Icons.cake,
Colors.orange,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildMetricCard(
context,
'Ancienneté',
'${statistics.ancienneteMoyenne.toStringAsFixed(1)} ans',
Icons.schedule,
Colors.purple,
),
),
],
),
],
),
),
);
}
/// Carte de métrique individuelle
Widget _buildMetricCard(
BuildContext context,
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
);
}
/// Graphique de répartition des statuts
Widget _buildStatusChart(BuildContext context) {
if (statistics.totalMembres == 0) {
return const SizedBox.shrink();
}
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Répartition par Statut',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: Row(
children: [
// Graphique en secteurs
Expanded(
flex: 2,
child: PieChart(
PieChartData(
sections: [
PieChartSectionData(
value: statistics.membresActifs.toDouble(),
title: '${statistics.pourcentageActifs.toStringAsFixed(1)}%',
color: Colors.green,
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (statistics.membresInactifs > 0)
PieChartSectionData(
value: statistics.membresInactifs.toDouble(),
title: '${statistics.pourcentageInactifs.toStringAsFixed(1)}%',
color: Colors.orange,
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
sectionsSpace: 2,
centerSpaceRadius: 40,
),
),
),
// Légende
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLegendItem(
'Actifs',
statistics.membresActifs,
Colors.green,
),
const SizedBox(height: 8),
if (statistics.membresInactifs > 0)
_buildLegendItem(
'Inactifs',
statistics.membresInactifs,
Colors.orange,
),
],
),
),
],
),
),
],
),
),
);
}
/// Item de légende
Widget _buildLegendItem(String label, int count, Color color) {
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'$label ($count)',
style: const TextStyle(fontSize: 12),
),
),
],
);
}
/// Métriques détaillées
Widget _buildDetailedMetrics(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Détails Démographiques',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildDetailRow(
context,
'Tranche d\'âge',
statistics.trancheAge,
Icons.calendar_today,
),
_buildDetailRow(
context,
'Organisations',
'${statistics.nombreOrganisations} représentées',
Icons.business,
),
_buildDetailRow(
context,
'Régions',
'${statistics.nombreRegions} représentées',
Icons.location_on,
),
_buildDetailRow(
context,
'Taux d\'activité',
'${statistics.pourcentageActifs.toStringAsFixed(1)}%',
Icons.trending_up,
),
],
),
),
);
}
/// Ligne de détail
Widget _buildDetailRow(
BuildContext context,
String label,
String value,
IconData icon,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(
icon,
size: 20,
color: Colors.grey[600],
),
const SizedBox(width: 12),
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium,
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
);
}
/// Informations complémentaires
Widget _buildAdditionalInfo(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).primaryColor,
),
const SizedBox(width: 8),
Text(
'Informations Complémentaires',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Text(
statistics.description,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[700],
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Row(
children: [
const Icon(Icons.lightbulb, color: Colors.blue),
const SizedBox(width: 8),
Expanded(
child: Text(
'Ces statistiques sont calculées en temps réel sur les résultats de votre recherche.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.blue[700],
),
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
/// Page de recherche avancée des membres
class AdvancedSearchPage extends StatefulWidget {
const AdvancedSearchPage({super.key});
@override
State<AdvancedSearchPage> createState() => _AdvancedSearchPageState();
}
class _AdvancedSearchPageState extends State<AdvancedSearchPage> {
final _formKey = GlobalKey<FormState>();
final _queryController = TextEditingController();
final _nomController = TextEditingController();
final _prenomController = TextEditingController();
final _emailController = TextEditingController();
@override
void dispose() {
_queryController.dispose();
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Recherche Avancée'),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
),
body: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
controller: _queryController,
decoration: const InputDecoration(
labelText: 'Recherche générale',
hintText: 'Nom, prénom, email...',
prefixIcon: Icon(Icons.search),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom',
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _prenomController,
decoration: const InputDecoration(
labelText: 'Prénom',
prefixIcon: Icon(Icons.person_outline),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _performSearch,
child: const Text('Rechercher'),
),
),
const SizedBox(width: 16),
Expanded(
child: OutlinedButton(
onPressed: _clearForm,
child: const Text('Effacer'),
),
),
],
),
],
),
),
),
);
}
void _performSearch() {
if (_formKey.currentState!.validate()) {
// TODO: Implémenter la recherche
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recherche en cours...'),
),
);
}
}
void _clearForm() {
_queryController.clear();
_nomController.clear();
_prenomController.clear();
_emailController.clear();
}
}

View File

@@ -12,7 +12,7 @@ import 'core/design_system/theme/app_theme_sophisticated.dart';
import 'core/auth/bloc/auth_bloc.dart';
import 'core/cache/dashboard_cache_manager.dart';
import 'features/auth/presentation/pages/login_page.dart';
import 'features/dashboard/presentation/pages/adaptive_dashboard_page.dart';
import 'core/navigation/main_navigation_layout.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -85,13 +85,13 @@ class UnionFlowApp extends StatelessWidget {
),
);
} else if (state is AuthAuthenticated) {
return const AdaptiveDashboardPage();
return const MainNavigationLayout();
} else {
return const LoginPage();
}
},
),
'/dashboard': (context) => const AdaptiveDashboardPage(),
'/dashboard': (context) => const MainNavigationLayout(),
'/login': (context) => const LoginPage(),
},