Refactoring - Version OK

This commit is contained in:
dahoud
2025-11-17 16:02:04 +00:00
parent 3f00a26308
commit 3b9ffac8cd
198 changed files with 18010 additions and 11383 deletions

View File

@@ -1,468 +0,0 @@
/// BLoC d'authentification Keycloak adaptatif avec gestion des rôles
/// Support Keycloak avec contextes multi-organisations et états sophistiqués
library auth_bloc;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import '../models/user.dart';
import '../models/user_role.dart';
import '../services/permission_engine.dart';
import '../services/keycloak_auth_service.dart';
import '../../cache/dashboard_cache_manager.dart';
// === ÉVÉNEMENTS ===
/// Événements d'authentification
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
/// Événement de connexion Keycloak
class AuthLoginRequested extends AuthEvent {
const AuthLoginRequested();
}
/// Événement de déconnexion
class AuthLogoutRequested extends AuthEvent {
const AuthLogoutRequested();
}
/// Événement de changement de contexte organisationnel
class AuthOrganizationContextChanged extends AuthEvent {
final String organizationId;
const AuthOrganizationContextChanged(this.organizationId);
@override
List<Object?> get props => [organizationId];
}
/// Événement de rafraîchissement du token
class AuthTokenRefreshRequested extends AuthEvent {
const AuthTokenRefreshRequested();
}
/// Événement de vérification de l'état d'authentification
class AuthStatusChecked extends AuthEvent {
const AuthStatusChecked();
}
/// Événement de mise à jour du profil utilisateur
class AuthUserProfileUpdated extends AuthEvent {
final User updatedUser;
const AuthUserProfileUpdated(this.updatedUser);
@override
List<Object?> get props => [updatedUser];
}
/// Événement de callback WebView
class AuthWebViewCallback extends AuthEvent {
final String callbackUrl;
final User? user;
const AuthWebViewCallback(this.callbackUrl, {this.user});
@override
List<Object?> get props => [callbackUrl, user];
}
// === ÉTATS ===
/// États d'authentification
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> get props => [];
}
/// État initial
class AuthInitial extends AuthState {
const AuthInitial();
}
/// État de chargement
class AuthLoading extends AuthState {
const AuthLoading();
}
/// État authentifié avec contexte riche
class AuthAuthenticated extends AuthState {
final User user;
final String? currentOrganizationId;
final UserRole effectiveRole;
final List<String> effectivePermissions;
final DateTime authenticatedAt;
final String? accessToken;
const AuthAuthenticated({
required this.user,
this.currentOrganizationId,
required this.effectiveRole,
required this.effectivePermissions,
required this.authenticatedAt,
this.accessToken,
});
/// Vérifie si l'utilisateur a une permission
bool hasPermission(String permission) {
return effectivePermissions.contains(permission);
}
/// Vérifie si l'utilisateur peut effectuer une action
bool canPerformAction(String domain, String action, {String scope = 'own'}) {
final permission = '$domain.$action.$scope';
return hasPermission(permission);
}
/// Obtient le contexte organisationnel actuel
UserOrganizationContext? get currentOrganizationContext {
if (currentOrganizationId == null) return null;
return user.getOrganizationContext(currentOrganizationId!);
}
/// Crée une copie avec des modifications
AuthAuthenticated copyWith({
User? user,
String? currentOrganizationId,
UserRole? effectiveRole,
List<String>? effectivePermissions,
DateTime? authenticatedAt,
String? accessToken,
}) {
return AuthAuthenticated(
user: user ?? this.user,
currentOrganizationId: currentOrganizationId ?? this.currentOrganizationId,
effectiveRole: effectiveRole ?? this.effectiveRole,
effectivePermissions: effectivePermissions ?? this.effectivePermissions,
authenticatedAt: authenticatedAt ?? this.authenticatedAt,
accessToken: accessToken ?? this.accessToken,
);
}
@override
List<Object?> get props => [
user,
currentOrganizationId,
effectiveRole,
effectivePermissions,
authenticatedAt,
accessToken,
];
}
/// État non authentifié
class AuthUnauthenticated extends AuthState {
final String? message;
const AuthUnauthenticated({this.message});
@override
List<Object?> get props => [message];
}
/// État d'erreur
class AuthError extends AuthState {
final String message;
final String? errorCode;
const AuthError({
required this.message,
this.errorCode,
});
@override
List<Object?> get props => [message, errorCode];
}
/// État indiquant qu'une WebView d'authentification est requise
class AuthWebViewRequired extends AuthState {
final String authUrl;
final String state;
final String codeVerifier;
const AuthWebViewRequired({
required this.authUrl,
required this.state,
required this.codeVerifier,
});
@override
List<Object?> get props => [authUrl, state, codeVerifier];
}
// === BLOC ===
/// BLoC d'authentification adaptatif
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(const AuthInitial()) {
on<AuthLoginRequested>(_onLoginRequested);
on<AuthLogoutRequested>(_onLogoutRequested);
on<AuthOrganizationContextChanged>(_onOrganizationContextChanged);
on<AuthTokenRefreshRequested>(_onTokenRefreshRequested);
on<AuthStatusChecked>(_onStatusChecked);
on<AuthUserProfileUpdated>(_onUserProfileUpdated);
on<AuthWebViewCallback>(_onWebViewCallback);
}
/// Gère la demande de connexion Keycloak via WebView
///
/// Cette méthode prépare l'authentification WebView et émet un état spécial
/// pour indiquer qu'une WebView doit être ouverte
Future<void> _onLoginRequested(
AuthLoginRequested event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
try {
debugPrint('🔐 Préparation authentification Keycloak WebView...');
// Préparer l'authentification WebView
final Map<String, String> authParams = await KeycloakAuthService.prepareWebViewAuthentication();
debugPrint('✅ Authentification WebView préparée');
// Émettre un état spécial pour indiquer qu'une WebView doit être ouverte
debugPrint('🚀 Émission de l\'état AuthWebViewRequired...');
emit(AuthWebViewRequired(
authUrl: authParams['url']!,
state: authParams['state']!,
codeVerifier: authParams['code_verifier']!,
));
debugPrint('✅ État AuthWebViewRequired émis');
} catch (e, stackTrace) {
debugPrint('💥 Erreur préparation authentification Keycloak: $e');
debugPrint('Stack trace: $stackTrace');
emit(AuthError(message: 'Erreur de préparation: $e'));
}
}
/// Traite le callback WebView et finalise l'authentification
Future<void> _onWebViewCallback(
AuthWebViewCallback event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
try {
debugPrint('🔄 Traitement callback WebView...');
// Utiliser l'utilisateur fourni ou traiter le callback
final User user;
if (event.user != null) {
debugPrint('👤 Utilisation des données utilisateur fournies: ${event.user!.fullName}');
user = event.user!;
} else {
debugPrint('🔄 Traitement du callback URL: ${event.callbackUrl}');
user = await KeycloakAuthService.handleWebViewCallback(event.callbackUrl);
}
debugPrint('👤 Utilisateur authentifié: ${user.fullName} (${user.primaryRole.displayName})');
// Calculer les permissions effectives
debugPrint('🔐 Calcul des permissions effectives...');
final effectivePermissions = await PermissionEngine.getEffectivePermissions(user);
debugPrint('✅ Permissions effectives calculées: ${effectivePermissions.length} permissions');
// Invalider le cache pour forcer le rechargement
debugPrint('🧹 Invalidation du cache pour le rôle ${user.primaryRole.displayName}...');
await DashboardCacheManager.invalidateForRole(user.primaryRole);
debugPrint('✅ Cache invalidé');
emit(AuthAuthenticated(
user: user,
currentOrganizationId: null, // À implémenter selon vos besoins
effectiveRole: user.primaryRole,
effectivePermissions: effectivePermissions,
authenticatedAt: DateTime.now(),
accessToken: '', // Token géré par KeycloakWebViewAuthService
));
debugPrint('🎉 Authentification complète réussie - navigation vers dashboard');
} catch (e, stackTrace) {
debugPrint('💥 Erreur authentification: $e');
debugPrint('Stack trace: $stackTrace');
emit(AuthError(message: 'Erreur de connexion: $e'));
}
}
/// Gère la demande de déconnexion Keycloak
Future<void> _onLogoutRequested(
AuthLogoutRequested event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
try {
debugPrint('🚪 Démarrage déconnexion Keycloak...');
// Déconnexion Keycloak
final logoutSuccess = await KeycloakAuthService.logout();
if (!logoutSuccess) {
debugPrint('⚠️ Déconnexion Keycloak partielle');
}
// Nettoyer le cache local
await DashboardCacheManager.clear();
// Invalider le cache des permissions
if (state is AuthAuthenticated) {
final authState = state as AuthAuthenticated;
PermissionEngine.invalidateUserCache(authState.user.id);
}
debugPrint('✅ Déconnexion complète réussie');
emit(const AuthUnauthenticated(message: 'Déconnexion réussie'));
} catch (e, stackTrace) {
debugPrint('💥 Erreur déconnexion: $e');
debugPrint('Stack trace: $stackTrace');
emit(AuthError(message: 'Erreur de déconnexion: $e'));
}
}
/// Gère le changement de contexte organisationnel
Future<void> _onOrganizationContextChanged(
AuthOrganizationContextChanged event,
Emitter<AuthState> emit,
) async {
if (state is! AuthAuthenticated) return;
final currentState = state as AuthAuthenticated;
emit(const AuthLoading());
try {
// Recalculer le rôle effectif et les permissions
final effectiveRole = currentState.user.getRoleInOrganization(event.organizationId);
final effectivePermissions = await PermissionEngine.getEffectivePermissions(
currentState.user,
organizationId: event.organizationId,
);
// Invalider le cache pour le nouveau contexte
PermissionEngine.invalidateUserCache(currentState.user.id);
emit(currentState.copyWith(
currentOrganizationId: event.organizationId,
effectiveRole: effectiveRole,
effectivePermissions: effectivePermissions,
));
} catch (e) {
emit(AuthError(message: 'Erreur de changement de contexte: $e'));
}
}
/// Gère le rafraîchissement du token
Future<void> _onTokenRefreshRequested(
AuthTokenRefreshRequested event,
Emitter<AuthState> emit,
) async {
if (state is! AuthAuthenticated) return;
final currentState = state as AuthAuthenticated;
try {
// Simulation du rafraîchissement (à remplacer par l'API réelle)
await Future.delayed(const Duration(seconds: 1));
final newToken = 'refreshed_token_${DateTime.now().millisecondsSinceEpoch}';
emit(currentState.copyWith(accessToken: newToken));
} catch (e) {
emit(AuthError(message: 'Erreur de rafraîchissement: $e'));
}
}
/// Vérifie l'état d'authentification Keycloak
Future<void> _onStatusChecked(
AuthStatusChecked event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
try {
debugPrint('🔍 Vérification état authentification Keycloak...');
// Vérifier si l'utilisateur est authentifié avec Keycloak
final bool isAuthenticated = await KeycloakAuthService.isAuthenticated();
if (!isAuthenticated) {
debugPrint('❌ Utilisateur non authentifié');
emit(const AuthUnauthenticated());
return;
}
// Récupérer l'utilisateur actuel
final User? user = await KeycloakAuthService.getCurrentUser();
if (user == null) {
debugPrint('❌ Impossible de récupérer l\'utilisateur');
emit(const AuthUnauthenticated());
return;
}
// Calculer les permissions effectives
final effectivePermissions = await PermissionEngine.getEffectivePermissions(user);
// Récupérer le token d'accès
final String? accessToken = await KeycloakAuthService.getAccessToken();
debugPrint('✅ Utilisateur authentifié: ${user.fullName}');
emit(AuthAuthenticated(
user: user,
currentOrganizationId: null, // À implémenter selon vos besoins
effectiveRole: user.primaryRole,
effectivePermissions: effectivePermissions,
authenticatedAt: DateTime.now(),
accessToken: accessToken ?? '',
));
} catch (e, stackTrace) {
debugPrint('💥 Erreur vérification authentification: $e');
debugPrint('Stack trace: $stackTrace');
emit(AuthError(message: 'Erreur de vérification: $e'));
}
}
/// Met à jour le profil utilisateur
Future<void> _onUserProfileUpdated(
AuthUserProfileUpdated event,
Emitter<AuthState> emit,
) async {
if (state is! AuthAuthenticated) return;
final currentState = state as AuthAuthenticated;
try {
// Recalculer les permissions si nécessaire
final effectivePermissions = await PermissionEngine.getEffectivePermissions(
event.updatedUser,
organizationId: currentState.currentOrganizationId,
);
emit(currentState.copyWith(
user: event.updatedUser,
effectivePermissions: effectivePermissions,
));
} catch (e) {
emit(AuthError(message: 'Erreur de mise à jour: $e'));
}
}
}

View File

@@ -1,212 +0,0 @@
/// Système de permissions granulaires ultra-sophistiqué
/// Plus de 50 permissions atomiques avec héritage intelligent
library permission_matrix;
/// Matrice de permissions atomiques pour contrôle granulaire
///
/// Chaque permission suit la convention : `domain.action.scope`
/// Exemples : `members.edit.own`, `finances.view.all`, `system.admin.global`
class PermissionMatrix {
// === PERMISSIONS SYSTÈME ===
static const String SYSTEM_ADMIN = 'system.admin.global';
static const String SYSTEM_CONFIG = 'system.config.global';
static const String SYSTEM_MONITORING = 'system.monitoring.view';
static const String SYSTEM_BACKUP = 'system.backup.manage';
static const String SYSTEM_SECURITY = 'system.security.manage';
static const String SYSTEM_AUDIT = 'system.audit.view';
static const String SYSTEM_LOGS = 'system.logs.view';
static const String SYSTEM_MAINTENANCE = 'system.maintenance.execute';
// === PERMISSIONS ORGANISATION ===
static const String ORG_CREATE = 'organization.create.global';
static const String ORG_DELETE = 'organization.delete.own';
static const String ORG_CONFIG = 'organization.config.own';
static const String ORG_BRANDING = 'organization.branding.manage';
static const String ORG_SETTINGS = 'organization.settings.manage';
static const String ORG_PERMISSIONS = 'organization.permissions.manage';
static const String ORG_WORKFLOWS = 'organization.workflows.manage';
static const String ORG_INTEGRATIONS = 'organization.integrations.manage';
// === PERMISSIONS DASHBOARD ===
static const String DASHBOARD_VIEW = 'dashboard.view.own';
static const String DASHBOARD_ADMIN = 'dashboard.admin.view';
static const String DASHBOARD_ANALYTICS = 'dashboard.analytics.view';
static const String DASHBOARD_REPORTS = 'dashboard.reports.generate';
static const String DASHBOARD_EXPORT = 'dashboard.export.data';
static const String DASHBOARD_CUSTOMIZE = 'dashboard.customize.layout';
// === PERMISSIONS MEMBRES ===
static const String MEMBERS_VIEW_ALL = 'members.view.all';
static const String MEMBERS_VIEW_OWN = 'members.view.own';
static const String MEMBERS_CREATE = 'members.create.organization';
static const String MEMBERS_EDIT_ALL = 'members.edit.all';
static const String MEMBERS_EDIT_OWN = 'members.edit.own';
static const String MEMBERS_EDIT_BASIC = 'members.edit.basic';
static const String MEMBERS_DELETE = 'members.delete.organization';
static const String MEMBERS_DELETE_ALL = 'members.delete.all';
static const String MEMBERS_APPROVE = 'members.approve.requests';
static const String MEMBERS_SUSPEND = 'members.suspend.organization';
static const String MEMBERS_EXPORT = 'members.export.data';
static const String MEMBERS_IMPORT = 'members.import.data';
static const String MEMBERS_COMMUNICATE = 'members.communicate.all';
// === PERMISSIONS FINANCES ===
static const String FINANCES_VIEW_ALL = 'finances.view.all';
static const String FINANCES_VIEW_OWN = 'finances.view.own';
static const String FINANCES_EDIT_ALL = 'finances.edit.all';
static const String FINANCES_MANAGE = 'finances.manage.organization';
static const String FINANCES_APPROVE = 'finances.approve.transactions';
static const String FINANCES_REPORTS = 'finances.reports.generate';
static const String FINANCES_BUDGET = 'finances.budget.manage';
static const String FINANCES_AUDIT = 'finances.audit.access';
// === PERMISSIONS ÉVÉNEMENTS ===
static const String EVENTS_VIEW_ALL = 'events.view.all';
static const String EVENTS_VIEW_PUBLIC = 'events.view.public';
static const String EVENTS_CREATE = 'events.create.organization';
static const String EVENTS_EDIT_ALL = 'events.edit.all';
static const String EVENTS_EDIT_OWN = 'events.edit.own';
static const String EVENTS_DELETE = 'events.delete.organization';
static const String EVENTS_PARTICIPATE = 'events.participate.public';
static const String EVENTS_MODERATE = 'events.moderate.organization';
static const String EVENTS_ANALYTICS = 'events.analytics.view';
// === PERMISSIONS SOLIDARITÉ ===
static const String SOLIDARITY_VIEW_ALL = 'solidarity.view.all';
static const String SOLIDARITY_VIEW_OWN = 'solidarity.view.own';
static const String SOLIDARITY_VIEW_PUBLIC = 'solidarity.view.public';
static const String SOLIDARITY_CREATE = 'solidarity.create.request';
static const String SOLIDARITY_EDIT_ALL = 'solidarity.edit.all';
static const String SOLIDARITY_APPROVE = 'solidarity.approve.requests';
static const String SOLIDARITY_PARTICIPATE = 'solidarity.participate.actions';
static const String SOLIDARITY_MANAGE = 'solidarity.manage.organization';
static const String SOLIDARITY_FUND = 'solidarity.fund.manage';
// === PERMISSIONS COMMUNICATION ===
static const String COMM_SEND_ALL = 'communication.send.all';
static const String COMM_SEND_MEMBERS = 'communication.send.members';
static const String COMM_MODERATE = 'communication.moderate.organization';
static const String COMM_BROADCAST = 'communication.broadcast.organization';
static const String COMM_TEMPLATES = 'communication.templates.manage';
// === PERMISSIONS RAPPORTS ===
static const String REPORTS_VIEW_ALL = 'reports.view.all';
static const String REPORTS_GENERATE = 'reports.generate.organization';
static const String REPORTS_EXPORT = 'reports.export.data';
static const String REPORTS_SCHEDULE = 'reports.schedule.automated';
// === PERMISSIONS MODÉRATION ===
static const String MODERATION_CONTENT = 'moderation.content.manage';
static const String MODERATION_USERS = 'moderation.users.manage';
static const String MODERATION_REPORTS = 'moderation.reports.handle';
/// Toutes les permissions disponibles dans le système
static const List<String> ALL_PERMISSIONS = [
// Système
SYSTEM_ADMIN, SYSTEM_CONFIG, SYSTEM_MONITORING, SYSTEM_BACKUP,
SYSTEM_SECURITY, SYSTEM_AUDIT, SYSTEM_LOGS, SYSTEM_MAINTENANCE,
// Organisation
ORG_CREATE, ORG_DELETE, ORG_CONFIG, ORG_BRANDING, ORG_SETTINGS,
ORG_PERMISSIONS, ORG_WORKFLOWS, ORG_INTEGRATIONS,
// Dashboard
DASHBOARD_VIEW, DASHBOARD_ADMIN, DASHBOARD_ANALYTICS, DASHBOARD_REPORTS,
DASHBOARD_EXPORT, DASHBOARD_CUSTOMIZE,
// Membres
MEMBERS_VIEW_ALL, MEMBERS_VIEW_OWN, MEMBERS_CREATE, MEMBERS_EDIT_ALL,
MEMBERS_EDIT_OWN, MEMBERS_DELETE, MEMBERS_APPROVE, MEMBERS_SUSPEND,
MEMBERS_EXPORT, MEMBERS_IMPORT, MEMBERS_COMMUNICATE,
// Finances
FINANCES_VIEW_ALL, FINANCES_VIEW_OWN, FINANCES_MANAGE, FINANCES_APPROVE,
FINANCES_REPORTS, FINANCES_BUDGET, FINANCES_AUDIT,
// Événements
EVENTS_VIEW_ALL, EVENTS_VIEW_PUBLIC, EVENTS_CREATE, EVENTS_EDIT_ALL,
EVENTS_EDIT_OWN, EVENTS_DELETE, EVENTS_MODERATE, EVENTS_ANALYTICS,
// Solidarité
SOLIDARITY_VIEW_ALL, SOLIDARITY_VIEW_OWN, SOLIDARITY_CREATE,
SOLIDARITY_APPROVE, SOLIDARITY_MANAGE, SOLIDARITY_FUND,
// Communication
COMM_SEND_ALL, COMM_SEND_MEMBERS, COMM_MODERATE, COMM_BROADCAST,
COMM_TEMPLATES,
// Rapports
REPORTS_VIEW_ALL, REPORTS_GENERATE, REPORTS_EXPORT, REPORTS_SCHEDULE,
// Modération
MODERATION_CONTENT, MODERATION_USERS, MODERATION_REPORTS,
];
/// Permissions publiques (accessibles sans authentification)
static const List<String> PUBLIC_PERMISSIONS = [
EVENTS_VIEW_PUBLIC,
];
/// Vérifie si une permission est publique
static bool isPublicPermission(String permission) {
return PUBLIC_PERMISSIONS.contains(permission);
}
/// Obtient le domaine d'une permission (partie avant le premier point)
static String getDomain(String permission) {
return permission.split('.').first;
}
/// Obtient l'action d'une permission (partie du milieu)
static String getAction(String permission) {
final parts = permission.split('.');
return parts.length > 1 ? parts[1] : '';
}
/// Obtient la portée d'une permission (partie après le dernier point)
static String getScope(String permission) {
return permission.split('.').last;
}
/// Vérifie si une permission implique une autre (héritage)
static bool implies(String higherPermission, String lowerPermission) {
// Exemple : 'members.edit.all' implique 'members.view.all'
final higherParts = higherPermission.split('.');
final lowerParts = lowerPermission.split('.');
if (higherParts.length != 3 || lowerParts.length != 3) return false;
// Même domaine requis
if (higherParts[0] != lowerParts[0]) return false;
// Vérification des implications d'actions
return _actionImplies(higherParts[1], lowerParts[1]) &&
_scopeImplies(higherParts[2], lowerParts[2]);
}
/// Vérifie si une action implique une autre
static bool _actionImplies(String higherAction, String lowerAction) {
const actionHierarchy = {
'admin': ['manage', 'edit', 'create', 'delete', 'view'],
'manage': ['edit', 'create', 'delete', 'view'],
'edit': ['view'],
'create': ['view'],
'delete': ['view'],
};
return actionHierarchy[higherAction]?.contains(lowerAction) ??
higherAction == lowerAction;
}
/// Vérifie si une portée implique une autre
static bool _scopeImplies(String higherScope, String lowerScope) {
const scopeHierarchy = {
'global': ['all', 'organization', 'own'],
'all': ['organization', 'own'],
'organization': ['own'],
};
return scopeHierarchy[higherScope]?.contains(lowerScope) ??
higherScope == lowerScope;
}
}

View File

@@ -1,359 +0,0 @@
/// Modèles de données utilisateur avec contexte et permissions
/// Support des relations multi-organisations et permissions contextuelles
library user_models;
import 'package:equatable/equatable.dart';
import 'user_role.dart';
/// Modèle utilisateur principal avec contexte multi-organisations
///
/// Supporte les utilisateurs ayant des rôles différents dans plusieurs organisations
/// avec des permissions contextuelles et des préférences personnalisées
class User extends Equatable {
/// Identifiant unique de l'utilisateur
final String id;
/// Informations personnelles
final String email;
final String firstName;
final String lastName;
final String? avatar;
final String? phone;
/// Rôle principal de l'utilisateur (le plus élevé)
final UserRole primaryRole;
/// Contextes organisationnels (rôles dans différentes organisations)
final List<UserOrganizationContext> organizationContexts;
/// Permissions supplémentaires accordées spécifiquement
final List<String> additionalPermissions;
/// Permissions révoquées spécifiquement
final List<String> revokedPermissions;
/// Préférences utilisateur
final UserPreferences preferences;
/// Métadonnées
final DateTime createdAt;
final DateTime lastLoginAt;
final bool isActive;
final bool isVerified;
/// Constructeur du modèle utilisateur
const User({
required this.id,
required this.email,
required this.firstName,
required this.lastName,
required this.primaryRole,
this.avatar,
this.phone,
this.organizationContexts = const [],
this.additionalPermissions = const [],
this.revokedPermissions = const [],
this.preferences = const UserPreferences(),
required this.createdAt,
required this.lastLoginAt,
this.isActive = true,
this.isVerified = false,
});
/// Nom complet de l'utilisateur
String get fullName => '$firstName $lastName';
/// Initiales de l'utilisateur
String get initials => '${firstName[0]}${lastName[0]}'.toUpperCase();
/// Vérifie si l'utilisateur a une permission dans le contexte actuel
bool hasPermission(String permission, {String? organizationId}) {
// Vérification des permissions révoquées
if (revokedPermissions.contains(permission)) return false;
// Vérification des permissions additionnelles
if (additionalPermissions.contains(permission)) return true;
// Vérification du rôle principal
if (primaryRole.hasPermission(permission)) return true;
// Vérification dans le contexte organisationnel spécifique
if (organizationId != null) {
final context = getOrganizationContext(organizationId);
if (context?.role.hasPermission(permission) == true) return true;
}
// Vérification dans tous les contextes organisationnels
return organizationContexts.any((context) =>
context.role.hasPermission(permission));
}
/// Obtient le contexte organisationnel pour une organisation
UserOrganizationContext? getOrganizationContext(String organizationId) {
try {
return organizationContexts.firstWhere(
(context) => context.organizationId == organizationId,
);
} catch (e) {
return null;
}
}
/// Obtient le rôle dans une organisation spécifique
UserRole getRoleInOrganization(String organizationId) {
final context = getOrganizationContext(organizationId);
return context?.role ?? primaryRole;
}
/// Vérifie si l'utilisateur est membre d'une organisation
bool isMemberOfOrganization(String organizationId) {
return organizationContexts.any(
(context) => context.organizationId == organizationId,
);
}
/// Obtient toutes les permissions effectives de l'utilisateur
List<String> getEffectivePermissions({String? organizationId}) {
final permissions = <String>{};
// Permissions du rôle principal
permissions.addAll(primaryRole.getEffectivePermissions());
// Permissions des contextes organisationnels
if (organizationId != null) {
final context = getOrganizationContext(organizationId);
if (context != null) {
permissions.addAll(context.role.getEffectivePermissions());
}
} else {
for (final context in organizationContexts) {
permissions.addAll(context.role.getEffectivePermissions());
}
}
// Permissions additionnelles
permissions.addAll(additionalPermissions);
// Retirer les permissions révoquées
permissions.removeAll(revokedPermissions);
return permissions.toList()..sort();
}
/// Crée une copie de l'utilisateur avec des modifications
User copyWith({
String? email,
String? firstName,
String? lastName,
String? avatar,
String? phone,
UserRole? primaryRole,
List<UserOrganizationContext>? organizationContexts,
List<String>? additionalPermissions,
List<String>? revokedPermissions,
UserPreferences? preferences,
DateTime? lastLoginAt,
bool? isActive,
bool? isVerified,
}) {
return User(
id: id,
email: email ?? this.email,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
avatar: avatar ?? this.avatar,
phone: phone ?? this.phone,
primaryRole: primaryRole ?? this.primaryRole,
organizationContexts: organizationContexts ?? this.organizationContexts,
additionalPermissions: additionalPermissions ?? this.additionalPermissions,
revokedPermissions: revokedPermissions ?? this.revokedPermissions,
preferences: preferences ?? this.preferences,
createdAt: createdAt,
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
isActive: isActive ?? this.isActive,
isVerified: isVerified ?? this.isVerified,
);
}
/// Conversion vers Map pour sérialisation
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'firstName': firstName,
'lastName': lastName,
'avatar': avatar,
'phone': phone,
'primaryRole': primaryRole.name,
'organizationContexts': organizationContexts.map((c) => c.toJson()).toList(),
'additionalPermissions': additionalPermissions,
'revokedPermissions': revokedPermissions,
'preferences': preferences.toJson(),
'createdAt': createdAt.toIso8601String(),
'lastLoginAt': lastLoginAt.toIso8601String(),
'isActive': isActive,
'isVerified': isVerified,
};
}
/// Création depuis Map pour désérialisation
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
email: json['email'],
firstName: json['firstName'],
lastName: json['lastName'],
avatar: json['avatar'],
phone: json['phone'],
primaryRole: UserRole.fromString(json['primaryRole']) ?? UserRole.visitor,
organizationContexts: (json['organizationContexts'] as List?)
?.map((c) => UserOrganizationContext.fromJson(c))
.toList() ?? [],
additionalPermissions: List<String>.from(json['additionalPermissions'] ?? []),
revokedPermissions: List<String>.from(json['revokedPermissions'] ?? []),
preferences: UserPreferences.fromJson(json['preferences'] ?? {}),
createdAt: DateTime.parse(json['createdAt']),
lastLoginAt: DateTime.parse(json['lastLoginAt']),
isActive: json['isActive'] ?? true,
isVerified: json['isVerified'] ?? false,
);
}
@override
List<Object?> get props => [
id, email, firstName, lastName, avatar, phone, primaryRole,
organizationContexts, additionalPermissions, revokedPermissions,
preferences, createdAt, lastLoginAt, isActive, isVerified,
];
}
/// Contexte organisationnel d'un utilisateur
///
/// Définit le rôle et les permissions spécifiques dans une organisation
class UserOrganizationContext extends Equatable {
/// Identifiant de l'organisation
final String organizationId;
/// Nom de l'organisation
final String organizationName;
/// Rôle de l'utilisateur dans cette organisation
final UserRole role;
/// Permissions spécifiques dans cette organisation
final List<String> specificPermissions;
/// Date d'adhésion à l'organisation
final DateTime joinedAt;
/// Statut dans l'organisation
final bool isActive;
/// Constructeur du contexte organisationnel
const UserOrganizationContext({
required this.organizationId,
required this.organizationName,
required this.role,
this.specificPermissions = const [],
required this.joinedAt,
this.isActive = true,
});
/// Conversion vers Map
Map<String, dynamic> toJson() {
return {
'organizationId': organizationId,
'organizationName': organizationName,
'role': role.name,
'specificPermissions': specificPermissions,
'joinedAt': joinedAt.toIso8601String(),
'isActive': isActive,
};
}
/// Création depuis Map
factory UserOrganizationContext.fromJson(Map<String, dynamic> json) {
return UserOrganizationContext(
organizationId: json['organizationId'],
organizationName: json['organizationName'],
role: UserRole.fromString(json['role']) ?? UserRole.visitor,
specificPermissions: List<String>.from(json['specificPermissions'] ?? []),
joinedAt: DateTime.parse(json['joinedAt']),
isActive: json['isActive'] ?? true,
);
}
@override
List<Object?> get props => [
organizationId, organizationName, role, specificPermissions, joinedAt, isActive,
];
}
/// Préférences utilisateur personnalisables
class UserPreferences extends Equatable {
/// Langue préférée
final String language;
/// Thème préféré
final String theme;
/// Notifications activées
final bool notificationsEnabled;
/// Notifications par email
final bool emailNotifications;
/// Notifications push
final bool pushNotifications;
/// Layout du dashboard préféré
final String dashboardLayout;
/// Timezone
final String timezone;
/// Constructeur des préférences
const UserPreferences({
this.language = 'fr',
this.theme = 'system',
this.notificationsEnabled = true,
this.emailNotifications = true,
this.pushNotifications = true,
this.dashboardLayout = 'default',
this.timezone = 'Europe/Paris',
});
/// Conversion vers Map
Map<String, dynamic> toJson() {
return {
'language': language,
'theme': theme,
'notificationsEnabled': notificationsEnabled,
'emailNotifications': emailNotifications,
'pushNotifications': pushNotifications,
'dashboardLayout': dashboardLayout,
'timezone': timezone,
};
}
/// Création depuis Map
factory UserPreferences.fromJson(Map<String, dynamic> json) {
return UserPreferences(
language: json['language'] ?? 'fr',
theme: json['theme'] ?? 'system',
notificationsEnabled: json['notificationsEnabled'] ?? true,
emailNotifications: json['emailNotifications'] ?? true,
pushNotifications: json['pushNotifications'] ?? true,
dashboardLayout: json['dashboardLayout'] ?? 'default',
timezone: json['timezone'] ?? 'Europe/Paris',
);
}
@override
List<Object?> get props => [
language, theme, notificationsEnabled, emailNotifications,
pushNotifications, dashboardLayout, timezone,
];
}

View File

@@ -1,319 +0,0 @@
/// Système de rôles utilisateurs avec hiérarchie intelligente
/// 6 niveaux de rôles avec permissions héritées et contextuelles
library user_role;
import 'permission_matrix.dart';
/// Énumération des rôles utilisateurs avec hiérarchie et permissions
///
/// Chaque rôle a un niveau numérique pour faciliter les comparaisons
/// et une liste de permissions spécifiques avec héritage intelligent
enum UserRole {
/// Super Administrateur - Niveau système (100)
/// Accès complet à toutes les fonctionnalités multi-organisations
superAdmin(
level: 100,
displayName: 'Super Administrateur',
description: 'Accès complet système et multi-organisations',
color: 0xFF6C5CE7, // Violet sophistiqué
permissions: _superAdminPermissions,
),
/// Administrateur d'Organisation - Niveau organisation (80)
/// Gestion complète de son organisation uniquement
orgAdmin(
level: 80,
displayName: 'Administrateur',
description: 'Gestion complète de l\'organisation',
color: 0xFF0984E3, // Bleu corporate
permissions: _orgAdminPermissions,
),
/// Modérateur/Gestionnaire - Niveau intermédiaire (60)
/// Gestion partielle selon permissions accordées
moderator(
level: 60,
displayName: 'Modérateur',
description: 'Gestion partielle et modération',
color: 0xFFE17055, // Orange focus
permissions: _moderatorPermissions,
),
/// Membre Actif - Niveau utilisateur (40)
/// Accès aux fonctionnalités membres avec participation active
activeMember(
level: 40,
displayName: 'Membre Actif',
description: 'Participation active aux activités',
color: 0xFF00B894, // Vert communauté
permissions: _activeMemberPermissions,
),
/// Membre Simple - Niveau basique (20)
/// Accès limité aux informations personnelles
simpleMember(
level: 20,
displayName: 'Membre',
description: 'Accès aux informations de base',
color: 0xFF00CEC9, // Teal simple
permissions: _simpleMemberPermissions,
),
/// Visiteur/Invité - Niveau public (0)
/// Accès aux informations publiques uniquement
visitor(
level: 0,
displayName: 'Visiteur',
description: 'Accès aux informations publiques',
color: 0xFF6C5CE7, // Indigo accueillant
permissions: _visitorPermissions,
);
/// Constructeur du rôle avec toutes ses propriétés
const UserRole({
required this.level,
required this.displayName,
required this.description,
required this.color,
required this.permissions,
});
/// Niveau numérique du rôle (0-100)
final int level;
/// Nom d'affichage du rôle
final String displayName;
/// Description détaillée du rôle
final String description;
/// Couleur thématique du rôle (format 0xFFRRGGBB)
final int color;
/// Liste des permissions spécifiques au rôle
final List<String> permissions;
/// Vérifie si ce rôle a un niveau supérieur ou égal à un autre
bool hasLevelOrAbove(UserRole other) => level >= other.level;
/// Vérifie si ce rôle a un niveau strictement supérieur à un autre
bool hasLevelAbove(UserRole other) => level > other.level;
/// Vérifie si ce rôle possède une permission spécifique
bool hasPermission(String permission) {
// Vérification directe
if (permissions.contains(permission)) return true;
// Vérification par héritage (permissions impliquées)
return permissions.any((p) => PermissionMatrix.implies(p, permission));
}
/// Obtient toutes les permissions effectives (directes + héritées)
List<String> getEffectivePermissions() {
final effective = <String>{};
// Ajouter les permissions directes
effective.addAll(permissions);
// Ajouter les permissions impliquées
for (final permission in permissions) {
for (final allPermission in PermissionMatrix.ALL_PERMISSIONS) {
if (PermissionMatrix.implies(permission, allPermission)) {
effective.add(allPermission);
}
}
}
return effective.toList()..sort();
}
/// Vérifie si ce rôle peut effectuer une action sur un domaine
bool canPerformAction(String domain, String action, {String scope = 'own'}) {
final permission = '$domain.$action.$scope';
return hasPermission(permission);
}
/// Obtient le rôle à partir de son nom
static UserRole? fromString(String roleName) {
return UserRole.values.firstWhere(
(role) => role.name == roleName,
orElse: () => UserRole.visitor,
);
}
/// Obtient tous les rôles avec un niveau inférieur ou égal
List<UserRole> getSubordinateRoles() {
return UserRole.values.where((role) => role.level < level).toList();
}
/// Obtient tous les rôles avec un niveau supérieur ou égal
List<UserRole> getSuperiorRoles() {
return UserRole.values.where((role) => role.level >= level).toList();
}
}
// === DÉFINITIONS DES PERMISSIONS PAR RÔLE ===
/// Permissions du Super Administrateur (accès complet)
const List<String> _superAdminPermissions = [
// Toutes les permissions système
PermissionMatrix.SYSTEM_ADMIN,
PermissionMatrix.SYSTEM_CONFIG,
PermissionMatrix.SYSTEM_MONITORING,
PermissionMatrix.SYSTEM_BACKUP,
PermissionMatrix.SYSTEM_SECURITY,
PermissionMatrix.SYSTEM_AUDIT,
PermissionMatrix.SYSTEM_LOGS,
PermissionMatrix.SYSTEM_MAINTENANCE,
// Gestion globale des organisations
PermissionMatrix.ORG_CREATE,
PermissionMatrix.ORG_DELETE,
PermissionMatrix.ORG_CONFIG,
// Accès complet aux dashboards
PermissionMatrix.DASHBOARD_ADMIN,
PermissionMatrix.DASHBOARD_ANALYTICS,
PermissionMatrix.DASHBOARD_REPORTS,
PermissionMatrix.DASHBOARD_EXPORT,
// Gestion complète des membres
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.MEMBERS_DELETE,
PermissionMatrix.MEMBERS_EXPORT,
PermissionMatrix.MEMBERS_IMPORT,
// Accès complet aux finances
PermissionMatrix.FINANCES_VIEW_ALL,
PermissionMatrix.FINANCES_MANAGE,
PermissionMatrix.FINANCES_AUDIT,
// Tous les rapports
PermissionMatrix.REPORTS_VIEW_ALL,
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.REPORTS_EXPORT,
PermissionMatrix.REPORTS_SCHEDULE,
];
/// Permissions de l'Administrateur d'Organisation
const List<String> _orgAdminPermissions = [
// Configuration organisation
PermissionMatrix.ORG_CONFIG,
PermissionMatrix.ORG_BRANDING,
PermissionMatrix.ORG_SETTINGS,
PermissionMatrix.ORG_PERMISSIONS,
PermissionMatrix.ORG_WORKFLOWS,
// Dashboard organisation
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.DASHBOARD_ANALYTICS,
PermissionMatrix.DASHBOARD_REPORTS,
PermissionMatrix.DASHBOARD_CUSTOMIZE,
// Gestion des membres
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_CREATE,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.MEMBERS_APPROVE,
PermissionMatrix.MEMBERS_SUSPEND,
PermissionMatrix.MEMBERS_COMMUNICATE,
// Gestion financière
PermissionMatrix.FINANCES_VIEW_ALL,
PermissionMatrix.FINANCES_MANAGE,
PermissionMatrix.FINANCES_REPORTS,
PermissionMatrix.FINANCES_BUDGET,
// Gestion des événements
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_CREATE,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.EVENTS_DELETE,
PermissionMatrix.EVENTS_ANALYTICS,
// Gestion de la solidarité
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_APPROVE,
PermissionMatrix.SOLIDARITY_MANAGE,
PermissionMatrix.SOLIDARITY_FUND,
// Communication
PermissionMatrix.COMM_SEND_ALL,
PermissionMatrix.COMM_BROADCAST,
PermissionMatrix.COMM_TEMPLATES,
// Rapports organisation
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.REPORTS_EXPORT,
];
/// Permissions du Modérateur
const List<String> _moderatorPermissions = [
// Dashboard limité
PermissionMatrix.DASHBOARD_VIEW,
// Modération des membres
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_APPROVE,
PermissionMatrix.MODERATION_USERS,
// Modération du contenu
PermissionMatrix.MODERATION_CONTENT,
PermissionMatrix.MODERATION_REPORTS,
// Événements limités
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_MODERATE,
// Communication modérée
PermissionMatrix.COMM_MODERATE,
PermissionMatrix.COMM_SEND_MEMBERS,
];
/// Permissions du Membre Actif
const List<String> _activeMemberPermissions = [
// Dashboard personnel
PermissionMatrix.DASHBOARD_VIEW,
// Profil personnel
PermissionMatrix.MEMBERS_VIEW_OWN,
PermissionMatrix.MEMBERS_EDIT_OWN,
// Finances personnelles
PermissionMatrix.FINANCES_VIEW_OWN,
// Événements
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_CREATE,
PermissionMatrix.EVENTS_EDIT_OWN,
// Solidarité
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_CREATE,
];
/// Permissions du Membre Simple
const List<String> _simpleMemberPermissions = [
// Dashboard basique
PermissionMatrix.DASHBOARD_VIEW,
// Profil personnel uniquement
PermissionMatrix.MEMBERS_VIEW_OWN,
PermissionMatrix.MEMBERS_EDIT_OWN,
// Finances personnelles
PermissionMatrix.FINANCES_VIEW_OWN,
// Événements publics
PermissionMatrix.EVENTS_VIEW_PUBLIC,
// Solidarité consultation
PermissionMatrix.SOLIDARITY_VIEW_OWN,
];
/// Permissions du Visiteur
const List<String> _visitorPermissions = [
// Événements publics uniquement
PermissionMatrix.EVENTS_VIEW_PUBLIC,
];

View File

@@ -1,418 +0,0 @@
/// Service d'Authentification Keycloak
/// Gère l'authentification avec votre instance Keycloak sur port 8180
library keycloak_auth_service;
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import '../models/user.dart';
import '../models/user_role.dart';
import 'keycloak_role_mapper.dart';
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.11:8180';
/// Realm UnionFlow
static const String realm = 'unionflow';
/// Client ID pour l'application mobile
static const String clientId = 'unionflow-mobile';
/// URL de redirection après authentification
static const String redirectUrl = 'dev.lions.unionflow-mobile://auth/callback';
/// Scopes demandés
static const List<String> scopes = ['openid', 'profile', 'email', 'roles'];
/// Endpoints calculés
static String get authorizationEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/auth';
static String get tokenEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/token';
static String get userInfoEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/userinfo';
static String get logoutEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/logout';
}
/// Service d'authentification Keycloak ultra-sophistiqué
class KeycloakAuthService {
static const FlutterAppAuth _appAuth = FlutterAppAuth();
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
// Clés de stockage sécurisé
static const String _accessTokenKey = 'keycloak_access_token';
static const String _refreshTokenKey = 'keycloak_refresh_token';
static const String _idTokenKey = 'keycloak_id_token';
static const String _userInfoKey = 'keycloak_user_info';
/// Authentification avec Keycloak via WebView (solution HTTP compatible)
///
/// Cette méthode utilise maintenant KeycloakWebViewAuthService pour contourner
/// les limitations HTTPS de flutter_appauth
static Future<AuthorizationTokenResponse?> authenticate() async {
try {
debugPrint('🔐 Démarrage authentification Keycloak via WebView...');
// Utiliser le service WebView pour l'authentification
// Cette méthode retourne null car l'authentification WebView
// est gérée différemment (via callback)
debugPrint('💡 Authentification WebView - utilisez authenticateWithWebView() à la place');
return null;
} catch (e, stackTrace) {
debugPrint('💥 Erreur authentification Keycloak: $e');
debugPrint('Stack trace: $stackTrace');
return null;
}
}
/// Rafraîchit le token d'accès
static Future<TokenResponse?> refreshToken() async {
try {
final String? refreshToken = await _secureStorage.read(
key: _refreshTokenKey,
);
if (refreshToken == null) {
debugPrint('❌ Aucun refresh token disponible');
return null;
}
debugPrint('🔄 Rafraîchissement du token...');
final TokenRequest request = TokenRequest(
KeycloakConfig.clientId,
KeycloakConfig.redirectUrl,
refreshToken: refreshToken,
serviceConfiguration: AuthorizationServiceConfiguration(
authorizationEndpoint: KeycloakConfig.authorizationEndpoint,
tokenEndpoint: KeycloakConfig.tokenEndpoint,
),
);
final TokenResponse? result = await _appAuth.token(request);
if (result != null) {
await _storeTokens(result);
debugPrint('✅ Token rafraîchi avec succès');
return result;
}
debugPrint('❌ Échec du rafraîchissement du token');
return null;
} catch (e, stackTrace) {
debugPrint('💥 Erreur rafraîchissement token: $e');
debugPrint('Stack trace: $stackTrace');
return null;
}
}
/// Récupère l'utilisateur authentifié depuis les tokens
static Future<User?> getCurrentUser() async {
try {
final String? accessToken = await _secureStorage.read(
key: _accessTokenKey,
);
final String? idToken = await _secureStorage.read(
key: _idTokenKey,
);
if (accessToken == null || idToken == null) {
debugPrint('❌ Tokens manquants');
return null;
}
// Vérifier si les tokens sont expirés
if (JwtDecoder.isExpired(accessToken)) {
debugPrint('⏰ Access token expiré, tentative de rafraîchissement...');
final TokenResponse? refreshResult = await refreshToken();
if (refreshResult == null) {
debugPrint('❌ Impossible de rafraîchir le token');
return null;
}
}
// Décoder les tokens JWT
final Map<String, dynamic> accessTokenPayload =
JwtDecoder.decode(accessToken);
final Map<String, dynamic> idTokenPayload =
JwtDecoder.decode(idToken);
debugPrint('🔍 Payload Access Token: $accessTokenPayload');
debugPrint('🔍 Payload ID Token: $idTokenPayload');
// Extraire les informations utilisateur
final String userId = idTokenPayload['sub'] ?? '';
final String email = idTokenPayload['email'] ?? '';
final String firstName = idTokenPayload['given_name'] ?? '';
final String lastName = idTokenPayload['family_name'] ?? '';
final String fullName = idTokenPayload['name'] ?? '$firstName $lastName';
// Extraire les rôles Keycloak
final List<String> keycloakRoles = _extractKeycloakRoles(accessTokenPayload);
debugPrint('🎭 Rôles Keycloak extraits: $keycloakRoles');
// Si aucun rôle, assigner un rôle par défaut
if (keycloakRoles.isEmpty) {
debugPrint('⚠️ Aucun rôle trouvé, assignation du rôle MEMBER par défaut');
keycloakRoles.add('member');
}
// Mapper vers notre système de rôles
final UserRole primaryRole = KeycloakRoleMapper.mapToUserRole(keycloakRoles);
final List<String> permissions = KeycloakRoleMapper.mapToPermissions(keycloakRoles);
debugPrint('🎯 Rôle principal mappé: ${primaryRole.displayName}');
debugPrint('🔐 Permissions mappées: ${permissions.length} permissions');
debugPrint('📋 Permissions détaillées: $permissions');
// Créer l'utilisateur
final User user = User(
id: userId,
email: email,
firstName: firstName,
lastName: lastName,
primaryRole: primaryRole,
organizationContexts: const [], // À implémenter selon vos besoins
additionalPermissions: permissions,
revokedPermissions: const [],
preferences: const UserPreferences(),
lastLoginAt: DateTime.now(),
createdAt: DateTime.now(), // À récupérer depuis Keycloak si disponible
isActive: true,
);
// Stocker les informations utilisateur
await _secureStorage.write(
key: _userInfoKey,
value: jsonEncode(user.toJson()),
);
debugPrint('✅ Utilisateur récupéré: ${user.fullName} (${user.primaryRole.displayName})');
return user;
} catch (e, stackTrace) {
debugPrint('💥 Erreur récupération utilisateur: $e');
debugPrint('Stack trace: $stackTrace');
return null;
}
}
/// Déconnexion complète
static Future<bool> logout() async {
try {
debugPrint('🚪 Déconnexion Keycloak...');
final String? idToken = await _secureStorage.read(key: _idTokenKey);
// Déconnexion côté Keycloak si possible
if (idToken != null) {
try {
final EndSessionRequest request = EndSessionRequest(
idTokenHint: idToken,
postLogoutRedirectUrl: KeycloakConfig.redirectUrl,
serviceConfiguration: AuthorizationServiceConfiguration(
authorizationEndpoint: KeycloakConfig.authorizationEndpoint,
tokenEndpoint: KeycloakConfig.tokenEndpoint,
endSessionEndpoint: KeycloakConfig.logoutEndpoint,
),
);
await _appAuth.endSession(request);
debugPrint('✅ Déconnexion Keycloak côté serveur réussie');
} catch (e) {
debugPrint('⚠️ Déconnexion côté serveur échouée: $e');
// Continue quand même avec la déconnexion locale
}
}
// Nettoyage local des tokens
await _clearTokens();
debugPrint('✅ Déconnexion locale terminée');
return true;
} catch (e, stackTrace) {
debugPrint('💥 Erreur déconnexion: $e');
debugPrint('Stack trace: $stackTrace');
return false;
}
}
/// Vérifie si l'utilisateur est authentifié
static Future<bool> isAuthenticated() async {
try {
final String? accessToken = await _secureStorage.read(
key: _accessTokenKey,
);
if (accessToken == null) {
return false;
}
// Vérifier si le token est expiré
if (JwtDecoder.isExpired(accessToken)) {
// Tenter de rafraîchir
final TokenResponse? refreshResult = await refreshToken();
return refreshResult != null;
}
return true;
} catch (e) {
debugPrint('💥 Erreur vérification authentification: $e');
return false;
}
}
/// Stocke les tokens de manière sécurisée
static Future<void> _storeTokens(TokenResponse tokenResponse) async {
if (tokenResponse.accessToken != null) {
await _secureStorage.write(
key: _accessTokenKey,
value: tokenResponse.accessToken!,
);
}
if (tokenResponse.refreshToken != null) {
await _secureStorage.write(
key: _refreshTokenKey,
value: tokenResponse.refreshToken!,
);
}
if (tokenResponse.idToken != null) {
await _secureStorage.write(
key: _idTokenKey,
value: tokenResponse.idToken!,
);
}
debugPrint('🔒 Tokens stockés de manière sécurisée');
}
/// Nettoie tous les tokens stockés
static Future<void> _clearTokens() async {
await _secureStorage.delete(key: _accessTokenKey);
await _secureStorage.delete(key: _refreshTokenKey);
await _secureStorage.delete(key: _idTokenKey);
await _secureStorage.delete(key: _userInfoKey);
debugPrint('🧹 Tokens nettoyés');
}
/// Extrait les rôles depuis le payload JWT Keycloak
static List<String> _extractKeycloakRoles(Map<String, dynamic> payload) {
final List<String> roles = [];
try {
// Rôles du realm
final Map<String, dynamic>? realmAccess = payload['realm_access'];
if (realmAccess != null && realmAccess['roles'] is List) {
final List<dynamic> realmRoles = realmAccess['roles'];
roles.addAll(realmRoles.cast<String>());
}
// Rôles des clients
final Map<String, dynamic>? resourceAccess = payload['resource_access'];
if (resourceAccess != null) {
resourceAccess.forEach((clientId, clientData) {
if (clientData is Map<String, dynamic> && clientData['roles'] is List) {
final List<dynamic> clientRoles = clientData['roles'];
roles.addAll(clientRoles.cast<String>());
}
});
}
// Filtrer les rôles système Keycloak
return roles.where((role) =>
!role.startsWith('default-roles-') &&
role != 'offline_access' &&
role != 'uma_authorization'
).toList();
} catch (e) {
debugPrint('💥 Erreur extraction rôles: $e');
return [];
}
}
/// Récupère le token d'accès actuel
static Future<String?> getAccessToken() async {
try {
final String? accessToken = await _secureStorage.read(
key: _accessTokenKey,
);
if (accessToken != null && !JwtDecoder.isExpired(accessToken)) {
return accessToken;
}
// Token expiré, tenter de rafraîchir
final TokenResponse? refreshResult = await refreshToken();
return refreshResult?.accessToken;
} catch (e) {
debugPrint('💥 Erreur récupération access token: $e');
return null;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES WEBVIEW - Délégation vers KeycloakWebViewAuthService
// ═══════════════════════════════════════════════════════════════════════════
/// Prépare l'authentification WebView
///
/// Retourne les paramètres nécessaires pour lancer la WebView d'authentification
static Future<Map<String, String>> prepareWebViewAuthentication() async {
return KeycloakWebViewAuthService.prepareAuthentication();
}
/// Traite le callback WebView et finalise l'authentification
///
/// Cette méthode doit être appelée quand l'URL de callback est interceptée
static Future<User> handleWebViewCallback(String callbackUrl) async {
return KeycloakWebViewAuthService.handleAuthCallback(callbackUrl);
}
/// Vérifie si l'utilisateur est authentifié (compatible WebView)
static Future<bool> isWebViewAuthenticated() async {
return KeycloakWebViewAuthService.isAuthenticated();
}
/// Récupère l'utilisateur authentifié (compatible WebView)
static Future<User?> getCurrentWebViewUser() async {
return KeycloakWebViewAuthService.getCurrentUser();
}
/// Déconnecte l'utilisateur (compatible WebView)
static Future<bool> logoutWebView() async {
return KeycloakWebViewAuthService.logout();
}
/// Nettoie les données d'authentification WebView
static Future<void> clearWebViewAuthData() async {
return KeycloakWebViewAuthService.clearAuthData();
}
}

View File

@@ -1,344 +0,0 @@
/// Mapper de Rôles Keycloak vers UserRole
/// Convertit les rôles Keycloak existants vers notre système de rôles sophistiqué
library keycloak_role_mapper;
import '../models/user_role.dart';
import '../models/permission_matrix.dart';
/// Service de mapping des rôles Keycloak
class KeycloakRoleMapper {
/// Mapping des rôles Keycloak vers UserRole
static const Map<String, UserRole> _keycloakToUserRole = {
// Rôles administratifs
'SUPER_ADMINISTRATEUR': UserRole.superAdmin,
'ADMIN': UserRole.superAdmin,
'ADMINISTRATEUR_ORGANISATION': UserRole.orgAdmin,
'PRESIDENT': UserRole.orgAdmin,
// Rôles de gestion
'RESPONSABLE_TECHNIQUE': UserRole.moderator,
'RESPONSABLE_MEMBRES': UserRole.moderator,
'TRESORIER': UserRole.moderator,
'SECRETAIRE': UserRole.moderator,
'GESTIONNAIRE_MEMBRE': UserRole.moderator,
'ORGANISATEUR_EVENEMENT': UserRole.moderator,
// Rôles membres
'MEMBRE_ACTIF': UserRole.activeMember,
'MEMBRE_SIMPLE': UserRole.simpleMember,
'MEMBRE': UserRole.activeMember,
};
/// Mapping des rôles Keycloak vers permissions spécifiques
static const Map<String, List<String>> _keycloakToPermissions = {
'SUPER_ADMINISTRATEUR': [
// Permissions Super Admin - Accès total
PermissionMatrix.SYSTEM_ADMIN,
PermissionMatrix.SYSTEM_CONFIG,
PermissionMatrix.SYSTEM_SECURITY,
PermissionMatrix.ORG_CREATE,
PermissionMatrix.ORG_DELETE,
PermissionMatrix.ORG_CONFIG,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.MEMBERS_DELETE_ALL,
PermissionMatrix.FINANCES_VIEW_ALL,
PermissionMatrix.FINANCES_EDIT_ALL,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_EDIT_ALL,
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.DASHBOARD_ANALYTICS,
],
'ADMIN': [
// Permissions Super Admin - Accès total (compatibilité)
PermissionMatrix.SYSTEM_ADMIN,
PermissionMatrix.SYSTEM_CONFIG,
PermissionMatrix.SYSTEM_SECURITY,
PermissionMatrix.ORG_CREATE,
PermissionMatrix.ORG_DELETE,
PermissionMatrix.ORG_CONFIG,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.MEMBERS_DELETE_ALL,
PermissionMatrix.FINANCES_VIEW_ALL,
PermissionMatrix.FINANCES_EDIT_ALL,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_EDIT_ALL,
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.DASHBOARD_ANALYTICS,
],
'ADMINISTRATEUR_ORGANISATION': [
// Permissions Admin Organisation
PermissionMatrix.ORG_CONFIG,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.FINANCES_VIEW_ALL,
PermissionMatrix.FINANCES_EDIT_ALL,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_EDIT_ALL,
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.DASHBOARD_ANALYTICS,
],
'PRESIDENT': [
// Permissions Président - Gestion organisation
PermissionMatrix.ORG_CONFIG,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.FINANCES_VIEW_ALL,
PermissionMatrix.FINANCES_EDIT_ALL,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_EDIT_ALL,
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.DASHBOARD_ANALYTICS,
PermissionMatrix.COMM_SEND_ALL,
],
'RESPONSABLE_TECHNIQUE': [
// Permissions Responsable Technique
PermissionMatrix.SYSTEM_MONITORING,
PermissionMatrix.SYSTEM_MAINTENANCE,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_BASIC,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.REPORTS_GENERATE,
],
'RESPONSABLE_MEMBRES': [
// Permissions Responsable Membres
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.MEMBERS_DELETE_ALL,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_EDIT_ALL,
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.REPORTS_GENERATE,
],
'TRESORIER': [
// Permissions Trésorier - Focus finances
PermissionMatrix.FINANCES_VIEW_ALL,
PermissionMatrix.FINANCES_EDIT_ALL,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_BASIC,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.DASHBOARD_VIEW,
],
'SECRETAIRE': [
// Permissions Secrétaire - Communication et membres
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_BASIC,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.COMM_SEND_ALL,
PermissionMatrix.COMM_MODERATE,
PermissionMatrix.DASHBOARD_VIEW,
],
'GESTIONNAIRE_MEMBRE': [
// Permissions Gestionnaire de Membres
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.MEMBERS_CREATE,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.COMM_SEND_MEMBERS,
],
'ORGANISATEUR_EVENEMENT': [
// Permissions Organisateur d'Événements
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.EVENTS_CREATE,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.COMM_SEND_MEMBERS,
],
'MEMBRE_ACTIF': [
// Permissions Membre Actif
PermissionMatrix.MEMBERS_VIEW_OWN,
PermissionMatrix.MEMBERS_EDIT_OWN,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_PARTICIPATE,
PermissionMatrix.EVENTS_CREATE,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_PARTICIPATE,
PermissionMatrix.SOLIDARITY_CREATE,
PermissionMatrix.FINANCES_VIEW_OWN,
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.COMM_SEND_MEMBERS,
],
'MEMBRE_SIMPLE': [
// Permissions Membre Simple
PermissionMatrix.MEMBERS_VIEW_OWN,
PermissionMatrix.MEMBERS_EDIT_OWN,
PermissionMatrix.EVENTS_VIEW_PUBLIC,
PermissionMatrix.EVENTS_PARTICIPATE,
PermissionMatrix.SOLIDARITY_VIEW_PUBLIC,
PermissionMatrix.SOLIDARITY_PARTICIPATE,
PermissionMatrix.FINANCES_VIEW_OWN,
PermissionMatrix.DASHBOARD_VIEW,
],
'MEMBRE': [
// Permissions Membre Standard (compatibilité)
PermissionMatrix.MEMBERS_VIEW_OWN,
PermissionMatrix.MEMBERS_EDIT_OWN,
PermissionMatrix.EVENTS_VIEW_PUBLIC,
PermissionMatrix.EVENTS_PARTICIPATE,
PermissionMatrix.SOLIDARITY_VIEW_PUBLIC,
PermissionMatrix.SOLIDARITY_PARTICIPATE,
PermissionMatrix.FINANCES_VIEW_OWN,
PermissionMatrix.DASHBOARD_VIEW,
],
};
/// Mappe une liste de rôles Keycloak vers le UserRole principal
static UserRole mapToUserRole(List<String> keycloakRoles) {
// Priorité des rôles (du plus élevé au plus bas)
const List<String> rolePriority = [
'SUPER_ADMINISTRATEUR',
'ADMIN',
'ADMINISTRATEUR_ORGANISATION',
'PRESIDENT',
'RESPONSABLE_TECHNIQUE',
'RESPONSABLE_MEMBRES',
'TRESORIER',
'SECRETAIRE',
'GESTIONNAIRE_MEMBRE',
'ORGANISATEUR_EVENEMENT',
'MEMBRE_ACTIF',
'MEMBRE_SIMPLE',
'MEMBRE',
];
// Trouver le rôle avec la priorité la plus élevée
for (final String priorityRole in rolePriority) {
if (keycloakRoles.contains(priorityRole)) {
return _keycloakToUserRole[priorityRole] ?? UserRole.simpleMember;
}
}
// Par défaut, visiteur si aucun rôle reconnu
return UserRole.visitor;
}
/// Mappe une liste de rôles Keycloak vers les permissions
static List<String> mapToPermissions(List<String> keycloakRoles) {
final Set<String> permissions = <String>{};
// Ajouter les permissions pour chaque rôle
for (final String role in keycloakRoles) {
final List<String>? rolePermissions = _keycloakToPermissions[role];
if (rolePermissions != null) {
permissions.addAll(rolePermissions);
}
}
// Ajouter les permissions de base pour tous les utilisateurs authentifiés
permissions.add(PermissionMatrix.DASHBOARD_VIEW);
permissions.add(PermissionMatrix.MEMBERS_VIEW_OWN);
return permissions.toList();
}
/// Vérifie si un rôle Keycloak est reconnu
static bool isValidKeycloakRole(String role) {
return _keycloakToUserRole.containsKey(role);
}
/// Récupère tous les rôles Keycloak supportés
static List<String> getSupportedKeycloakRoles() {
return _keycloakToUserRole.keys.toList();
}
/// Récupère le UserRole correspondant à un rôle Keycloak spécifique
static UserRole? getUserRoleForKeycloakRole(String keycloakRole) {
return _keycloakToUserRole[keycloakRole];
}
/// Récupère les permissions pour un rôle Keycloak spécifique
static List<String> getPermissionsForKeycloakRole(String keycloakRole) {
return _keycloakToPermissions[keycloakRole] ?? [];
}
/// Analyse détaillée du mapping des rôles
static Map<String, dynamic> analyzeRoleMapping(List<String> keycloakRoles) {
final UserRole primaryRole = mapToUserRole(keycloakRoles);
final List<String> permissions = mapToPermissions(keycloakRoles);
final Map<String, List<String>> roleBreakdown = {};
for (final String role in keycloakRoles) {
if (isValidKeycloakRole(role)) {
roleBreakdown[role] = getPermissionsForKeycloakRole(role);
}
}
return {
'keycloakRoles': keycloakRoles,
'primaryRole': primaryRole.name,
'primaryRoleDisplayName': primaryRole.displayName,
'totalPermissions': permissions.length,
'permissions': permissions,
'roleBreakdown': roleBreakdown,
'unrecognizedRoles': keycloakRoles
.where((role) => !isValidKeycloakRole(role))
.toList(),
};
}
/// Suggestions d'amélioration du mapping
static Map<String, dynamic> getMappingSuggestions(List<String> keycloakRoles) {
final List<String> unrecognized = keycloakRoles
.where((role) => !isValidKeycloakRole(role))
.toList();
final List<String> suggestions = [];
if (unrecognized.isNotEmpty) {
suggestions.add(
'Rôles non reconnus détectés: ${unrecognized.join(", ")}. '
'Considérez ajouter ces rôles au mapping ou les ignorer.',
);
}
if (keycloakRoles.isEmpty) {
suggestions.add(
'Aucun rôle Keycloak détecté. L\'utilisateur sera traité comme visiteur.',
);
}
final UserRole primaryRole = mapToUserRole(keycloakRoles);
if (primaryRole == UserRole.visitor && keycloakRoles.isNotEmpty) {
suggestions.add(
'L\'utilisateur a des rôles Keycloak mais est mappé comme visiteur. '
'Vérifiez la configuration du mapping.',
);
}
return {
'unrecognizedRoles': unrecognized,
'suggestions': suggestions,
'mappingHealth': suggestions.isEmpty ? 'excellent' : 'needs_attention',
};
}
}

View File

@@ -1,671 +0,0 @@
/// Service d'Authentification Keycloak via WebView
///
/// Implémentation professionnelle et sécurisée de l'authentification OAuth2/OIDC
/// avec Keycloak utilisant WebView pour contourner les limitations HTTPS de flutter_appauth.
///
/// Fonctionnalités :
/// - Flow OAuth2 Authorization Code avec PKCE
/// - Gestion sécurisée des tokens JWT
/// - Support HTTP/HTTPS
/// - Gestion complète des erreurs et timeouts
/// - Validation rigoureuse des paramètres
/// - Logging détaillé pour le debugging
library keycloak_webview_auth_service;
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:jwt_decoder/jwt_decoder.dart';
import '../models/user.dart';
import '../models/user_role.dart';
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.11:8180';
/// Realm UnionFlow
static const String realm = 'unionflow';
/// Client ID pour l'application mobile
static const String clientId = 'unionflow-mobile';
/// URL de redirection après authentification
static const String redirectUrl = 'dev.lions.unionflow-mobile://auth/callback';
/// Scopes OAuth2 demandés
static const List<String> scopes = ['openid', 'profile', 'email', 'roles'];
/// Timeout pour les requêtes HTTP (en secondes)
static const int httpTimeoutSeconds = 30;
/// Timeout pour l'authentification WebView (en secondes)
static const int authTimeoutSeconds = 300; // 5 minutes
/// Endpoints calculés
static String get authorizationEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/auth';
static String get tokenEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/token';
static String get userInfoEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/userinfo';
static String get logoutEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/logout';
static String get jwksEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/certs';
}
/// Résultat de l'authentification WebView
class WebViewAuthResult {
final String accessToken;
final String idToken;
final String? refreshToken;
final int expiresIn;
final String tokenType;
final List<String> scopes;
const WebViewAuthResult({
required this.accessToken,
required this.idToken,
this.refreshToken,
required this.expiresIn,
required this.tokenType,
required this.scopes,
});
/// Création depuis la réponse token de Keycloak
factory WebViewAuthResult.fromTokenResponse(Map<String, dynamic> response) {
return WebViewAuthResult(
accessToken: response['access_token'] ?? '',
idToken: response['id_token'] ?? '',
refreshToken: response['refresh_token'],
expiresIn: response['expires_in'] ?? 3600,
tokenType: response['token_type'] ?? 'Bearer',
scopes: (response['scope'] as String?)?.split(' ') ?? [],
);
}
}
/// Exceptions spécifiques à l'authentification WebView
class KeycloakWebViewAuthException implements Exception {
final String message;
final String? code;
final dynamic originalError;
const KeycloakWebViewAuthException(
this.message, {
this.code,
this.originalError,
});
@override
String toString() => 'KeycloakWebViewAuthException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Service d'authentification Keycloak via WebView
///
/// Implémentation complète et sécurisée du flow OAuth2 Authorization Code avec PKCE
class KeycloakWebViewAuthService {
// Stockage sécurisé des tokens
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
// Clés de stockage sécurisé
static const String _accessTokenKey = 'keycloak_webview_access_token';
static const String _idTokenKey = 'keycloak_webview_id_token';
static const String _refreshTokenKey = 'keycloak_webview_refresh_token';
static const String _userInfoKey = 'keycloak_webview_user_info';
static const String _authStateKey = 'keycloak_webview_auth_state';
// Client HTTP avec timeout configuré
static final http.Client _httpClient = http.Client();
/// Génère un code verifier PKCE sécurisé
static String _generateCodeVerifier() {
const String charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
final Random random = Random.secure();
return List.generate(128, (i) => charset[random.nextInt(charset.length)]).join();
}
/// Génère le code challenge PKCE à partir du verifier
static String _generateCodeChallenge(String verifier) {
final List<int> bytes = utf8.encode(verifier);
final Digest digest = sha256.convert(bytes);
return base64Url.encode(digest.bytes).replaceAll('=', '');
}
/// Génère un state sécurisé pour la protection CSRF
static String _generateState() {
final Random random = Random.secure();
final List<int> bytes = List.generate(32, (i) => random.nextInt(256));
return base64Url.encode(bytes).replaceAll('=', '');
}
/// Construit l'URL d'autorisation Keycloak avec tous les paramètres
static Future<Map<String, String>> _buildAuthorizationUrl() async {
final String codeVerifier = _generateCodeVerifier();
final String codeChallenge = _generateCodeChallenge(codeVerifier);
final String state = _generateState();
// Stocker les paramètres pour la validation ultérieure
await _secureStorage.write(
key: _authStateKey,
value: jsonEncode({
'code_verifier': codeVerifier,
'state': state,
'timestamp': DateTime.now().millisecondsSinceEpoch,
}),
);
final Map<String, String> params = {
'response_type': 'code',
'client_id': KeycloakWebViewConfig.clientId,
'redirect_uri': KeycloakWebViewConfig.redirectUrl,
'scope': KeycloakWebViewConfig.scopes.join(' '),
'state': state,
'code_challenge': codeChallenge,
'code_challenge_method': 'S256',
'kc_locale': 'fr',
'prompt': 'login',
};
final String queryString = params.entries
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
.join('&');
final String authUrl = '${KeycloakWebViewConfig.authorizationEndpoint}?$queryString';
debugPrint('🔐 URL d\'autorisation générée: $authUrl');
return {
'url': authUrl,
'state': state,
'code_verifier': codeVerifier,
};
}
/// Valide la réponse de redirection et extrait le code d'autorisation
static Future<String> _validateCallbackAndExtractCode(
String callbackUrl,
String expectedState,
) async {
debugPrint('🔍 Validation du callback: $callbackUrl');
final Uri uri = Uri.parse(callbackUrl);
// Vérifier que c'est bien notre URL de redirection
if (!callbackUrl.startsWith(KeycloakWebViewConfig.redirectUrl)) {
throw const KeycloakWebViewAuthException(
'URL de callback invalide',
code: 'INVALID_CALLBACK_URL',
);
}
// Vérifier la présence d'erreurs
final String? error = uri.queryParameters['error'];
if (error != null) {
final String? errorDescription = uri.queryParameters['error_description'];
throw KeycloakWebViewAuthException(
'Erreur d\'authentification: ${errorDescription ?? error}',
code: error,
);
}
// Valider le state pour la protection CSRF
final String? receivedState = uri.queryParameters['state'];
if (receivedState != expectedState) {
throw const KeycloakWebViewAuthException(
'State invalide - possible attaque CSRF',
code: 'INVALID_STATE',
);
}
// Extraire le code d'autorisation
final String? code = uri.queryParameters['code'];
if (code == null || code.isEmpty) {
throw const KeycloakWebViewAuthException(
'Code d\'autorisation manquant',
code: 'MISSING_AUTH_CODE',
);
}
debugPrint('✅ Code d\'autorisation extrait avec succès');
return code;
}
/// Échange le code d'autorisation contre des tokens
static Future<WebViewAuthResult> _exchangeCodeForTokens(
String authCode,
String codeVerifier,
) async {
debugPrint('🔄 Échange du code d\'autorisation contre les tokens...');
try {
final Map<String, String> body = {
'grant_type': 'authorization_code',
'client_id': KeycloakWebViewConfig.clientId,
'code': authCode,
'redirect_uri': KeycloakWebViewConfig.redirectUrl,
'code_verifier': codeVerifier,
};
final http.Response response = await _httpClient
.post(
Uri.parse(KeycloakWebViewConfig.tokenEndpoint),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
body: body,
)
.timeout(const Duration(seconds: KeycloakWebViewConfig.httpTimeoutSeconds));
debugPrint('📡 Réponse token endpoint: ${response.statusCode}');
if (response.statusCode != 200) {
final String errorBody = response.body;
debugPrint('❌ Erreur échange tokens: $errorBody');
Map<String, dynamic>? errorJson;
try {
errorJson = jsonDecode(errorBody);
} catch (e) {
// Ignore JSON parsing errors
}
final String errorMessage = errorJson?['error_description'] ??
errorJson?['error'] ??
'Erreur HTTP ${response.statusCode}';
throw KeycloakWebViewAuthException(
'Échec de l\'échange de tokens: $errorMessage',
code: errorJson?['error'],
);
}
final Map<String, dynamic> tokenResponse = jsonDecode(response.body);
// Valider la présence des tokens requis
if (!tokenResponse.containsKey('access_token') ||
!tokenResponse.containsKey('id_token')) {
throw const KeycloakWebViewAuthException(
'Tokens manquants dans la réponse',
code: 'MISSING_TOKENS',
);
}
debugPrint('✅ Tokens reçus avec succès');
return WebViewAuthResult.fromTokenResponse(tokenResponse);
} on TimeoutException {
throw const KeycloakWebViewAuthException(
'Timeout lors de l\'échange des tokens',
code: 'TIMEOUT',
);
} catch (e) {
if (e is KeycloakWebViewAuthException) rethrow;
throw KeycloakWebViewAuthException(
'Erreur lors de l\'échange des tokens: $e',
originalError: e,
);
}
}
/// Stocke les tokens de manière sécurisée
static Future<void> _storeTokens(WebViewAuthResult authResult) async {
debugPrint('💾 Stockage sécurisé des tokens...');
try {
await Future.wait([
_secureStorage.write(key: _accessTokenKey, value: authResult.accessToken),
_secureStorage.write(key: _idTokenKey, value: authResult.idToken),
if (authResult.refreshToken != null)
_secureStorage.write(key: _refreshTokenKey, value: authResult.refreshToken!),
]);
debugPrint('✅ Tokens stockés avec succès');
} catch (e) {
throw KeycloakWebViewAuthException(
'Erreur lors du stockage des tokens: $e',
originalError: e,
);
}
}
/// Valide et parse un token JWT
static Map<String, dynamic> _parseAndValidateJWT(String token, String tokenType) {
try {
// Vérifier l'expiration
if (JwtDecoder.isExpired(token)) {
throw KeycloakWebViewAuthException(
'$tokenType expiré',
code: 'TOKEN_EXPIRED',
);
}
// Parser le payload
final Map<String, dynamic> payload = JwtDecoder.decode(token);
// Validations de base
if (payload['iss'] == null) {
throw const KeycloakWebViewAuthException(
'Token JWT invalide: issuer manquant',
code: 'INVALID_JWT',
);
}
// Vérifier l'issuer
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']})',
code: 'INVALID_ISSUER',
);
}
debugPrint('$tokenType validé avec succès');
return payload;
} catch (e) {
if (e is KeycloakWebViewAuthException) rethrow;
throw KeycloakWebViewAuthException(
'Erreur lors de la validation du $tokenType: $e',
originalError: e,
);
}
}
/// Méthode principale d'authentification
///
/// Retourne les paramètres nécessaires pour lancer la WebView d'authentification
static Future<Map<String, String>> prepareAuthentication() async {
debugPrint('🚀 Préparation de l\'authentification WebView...');
try {
// Nettoyer les données d'authentification précédentes
await clearAuthData();
// Générer l'URL d'autorisation avec PKCE
final Map<String, String> authParams = await _buildAuthorizationUrl();
debugPrint('✅ Authentification préparée avec succès');
return authParams;
} catch (e) {
throw KeycloakWebViewAuthException(
'Erreur lors de la préparation de l\'authentification: $e',
originalError: e,
);
}
}
/// Traite le callback de redirection et finalise l'authentification
static Future<User> handleAuthCallback(String callbackUrl) async {
debugPrint('🔄 Traitement du callback d\'authentification...');
debugPrint('📋 URL de callback: $callbackUrl');
try {
// Récupérer les paramètres d'authentification stockés
debugPrint('🔍 Récupération de l\'état d\'authentification...');
final String? authStateJson = await _secureStorage.read(key: _authStateKey);
if (authStateJson == null) {
debugPrint('❌ État d\'authentification manquant');
throw const KeycloakWebViewAuthException(
'État d\'authentification manquant',
code: 'MISSING_AUTH_STATE',
);
}
final Map<String, dynamic> authState = jsonDecode(authStateJson);
final String expectedState = authState['state'];
final String codeVerifier = authState['code_verifier'];
debugPrint('✅ État d\'authentification récupéré');
// Valider le callback et extraire le code
debugPrint('🔍 Validation du callback...');
final String authCode = await _validateCallbackAndExtractCode(
callbackUrl,
expectedState,
);
debugPrint('✅ Code d\'autorisation extrait: ${authCode.substring(0, 10)}...');
// Échanger le code contre des tokens
debugPrint('🔄 Échange du code contre les tokens...');
final WebViewAuthResult authResult = await _exchangeCodeForTokens(
authCode,
codeVerifier,
);
debugPrint('✅ Tokens reçus avec succès');
// Stocker les tokens
debugPrint('💾 Stockage des tokens...');
await _storeTokens(authResult);
debugPrint('✅ Tokens stockés');
// Créer l'utilisateur depuis les tokens
debugPrint('👤 Création de l\'utilisateur...');
final User user = await _createUserFromTokens(authResult);
debugPrint('✅ Utilisateur créé: ${user.fullName}');
// Nettoyer l'état d'authentification temporaire
await _secureStorage.delete(key: _authStateKey);
debugPrint('🎉 Authentification WebView terminée avec succès');
return user;
} catch (e, stackTrace) {
debugPrint('💥 Erreur lors du traitement du callback: $e');
debugPrint('📋 Stack trace: $stackTrace');
// Nettoyer en cas d'erreur
await _secureStorage.delete(key: _authStateKey);
if (e is KeycloakWebViewAuthException) rethrow;
throw KeycloakWebViewAuthException(
'Erreur lors du traitement du callback: $e',
originalError: e,
);
}
}
/// Crée un utilisateur depuis les tokens JWT
static Future<User> _createUserFromTokens(WebViewAuthResult authResult) async {
debugPrint('👤 Création de l\'utilisateur depuis les tokens...');
try {
// Parser et valider les tokens
final Map<String, dynamic> accessTokenPayload = _parseAndValidateJWT(
authResult.accessToken,
'Access Token',
);
final Map<String, dynamic> idTokenPayload = _parseAndValidateJWT(
authResult.idToken,
'ID Token',
);
// Extraire les informations utilisateur
final String userId = idTokenPayload['sub'] ?? '';
final String email = idTokenPayload['email'] ?? '';
final String firstName = idTokenPayload['given_name'] ?? '';
final String lastName = idTokenPayload['family_name'] ?? '';
if (userId.isEmpty || email.isEmpty) {
throw const KeycloakWebViewAuthException(
'Informations utilisateur manquantes dans les tokens',
code: 'MISSING_USER_INFO',
);
}
// Extraire les rôles Keycloak
final List<String> keycloakRoles = _extractKeycloakRoles(accessTokenPayload);
// Mapper vers notre système de rôles
final UserRole primaryRole = KeycloakRoleMapper.mapToUserRole(keycloakRoles);
final List<String> permissions = KeycloakRoleMapper.mapToPermissions(keycloakRoles);
// Créer l'utilisateur
final User user = User(
id: userId,
email: email,
firstName: firstName,
lastName: lastName,
primaryRole: primaryRole,
organizationContexts: const [],
additionalPermissions: permissions,
revokedPermissions: const [],
preferences: const UserPreferences(
language: 'fr',
theme: 'system',
notificationsEnabled: true,
emailNotifications: true,
pushNotifications: true,
dashboardLayout: 'adaptive',
timezone: 'Europe/Paris',
),
lastLoginAt: DateTime.now(),
createdAt: DateTime.now(),
isActive: true,
);
// Stocker les informations utilisateur
await _secureStorage.write(
key: _userInfoKey,
value: jsonEncode(user.toJson()),
);
debugPrint('✅ Utilisateur créé: ${user.fullName} (${user.primaryRole.displayName})');
return user;
} catch (e) {
if (e is KeycloakWebViewAuthException) rethrow;
throw KeycloakWebViewAuthException(
'Erreur lors de la création de l\'utilisateur: $e',
originalError: e,
);
}
}
/// Extrait les rôles Keycloak depuis le payload du token
static List<String> _extractKeycloakRoles(Map<String, dynamic> tokenPayload) {
try {
final List<String> roles = <String>[];
// Rôles realm
final Map<String, dynamic>? realmAccess = tokenPayload['realm_access'];
if (realmAccess != null && realmAccess['roles'] is List) {
roles.addAll(List<String>.from(realmAccess['roles']));
}
// Rôles client
final Map<String, dynamic>? resourceAccess = tokenPayload['resource_access'];
if (resourceAccess != null) {
final Map<String, dynamic>? clientAccess = resourceAccess[KeycloakWebViewConfig.clientId];
if (clientAccess != null && clientAccess['roles'] is List) {
roles.addAll(List<String>.from(clientAccess['roles']));
}
}
// Filtrer les rôles système
return roles.where((role) =>
!role.startsWith('default-roles-') &&
role != 'offline_access' &&
role != 'uma_authorization'
).toList();
} catch (e) {
debugPrint('💥 Erreur extraction rôles: $e');
return [];
}
}
/// Nettoie toutes les données d'authentification
static Future<void> clearAuthData() async {
debugPrint('🧹 Nettoyage des données d\'authentification...');
try {
await Future.wait([
_secureStorage.delete(key: _accessTokenKey),
_secureStorage.delete(key: _idTokenKey),
_secureStorage.delete(key: _refreshTokenKey),
_secureStorage.delete(key: _userInfoKey),
_secureStorage.delete(key: _authStateKey),
]);
debugPrint('✅ Données d\'authentification nettoyées');
} catch (e) {
debugPrint('⚠️ Erreur lors du nettoyage: $e');
}
}
/// Vérifie si l'utilisateur est authentifié
static Future<bool> isAuthenticated() async {
try {
final String? accessToken = await _secureStorage.read(key: _accessTokenKey);
if (accessToken == null) {
return false;
}
// Vérifier si le token est expiré
return !JwtDecoder.isExpired(accessToken);
} catch (e) {
debugPrint('💥 Erreur vérification authentification: $e');
return false;
}
}
/// Récupère l'utilisateur authentifié
static Future<User?> getCurrentUser() async {
try {
final String? userInfoJson = await _secureStorage.read(key: _userInfoKey);
if (userInfoJson == null) {
return null;
}
final Map<String, dynamic> userJson = jsonDecode(userInfoJson);
return User.fromJson(userJson);
} catch (e) {
debugPrint('💥 Erreur récupération utilisateur: $e');
return null;
}
}
/// Déconnecte l'utilisateur
static Future<bool> logout() async {
debugPrint('🚪 Déconnexion de l\'utilisateur...');
try {
// Nettoyer les données locales
await clearAuthData();
debugPrint('✅ Déconnexion réussie');
return true;
} catch (e) {
debugPrint('💥 Erreur déconnexion: $e');
return false;
}
}
}

View File

@@ -1,375 +0,0 @@
/// Moteur de permissions ultra-performant avec cache intelligent
/// Vérifications contextuelles et audit trail intégré
library permission_engine;
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/user.dart';
import '../models/user_role.dart';
import '../models/permission_matrix.dart';
/// Moteur de permissions haute performance avec cache multi-niveaux
///
/// Fonctionnalités :
/// - Cache mémoire ultra-rapide avec TTL
/// - Vérifications contextuelles avancées
/// - Audit trail automatique
/// - Support des permissions héritées
/// - Invalidation intelligente du cache
class PermissionEngine {
static final PermissionEngine _instance = PermissionEngine._internal();
factory PermissionEngine() => _instance;
PermissionEngine._internal();
/// Cache mémoire des permissions avec TTL
static final Map<String, _CachedPermission> _permissionCache = {};
/// Cache des permissions effectives par utilisateur
static final Map<String, _CachedUserPermissions> _userPermissionsCache = {};
/// Durée de vie du cache (5 minutes par défaut)
static const Duration _defaultCacheTTL = Duration(minutes: 5);
/// Durée de vie du cache pour les super admins (plus long)
static const Duration _superAdminCacheTTL = Duration(minutes: 15);
/// Compteur de hits/miss du cache pour monitoring
static int _cacheHits = 0;
static int _cacheMisses = 0;
/// Stream pour les événements d'audit
static final StreamController<PermissionAuditEvent> _auditController =
StreamController<PermissionAuditEvent>.broadcast();
/// Stream des événements d'audit
static Stream<PermissionAuditEvent> get auditStream => _auditController.stream;
/// Vérifie si un utilisateur a une permission spécifique
///
/// [user] - Utilisateur à vérifier
/// [permission] - Permission à vérifier
/// [organizationId] - Contexte organisationnel optionnel
/// [auditLog] - Activer l'audit trail (défaut: true)
static Future<bool> hasPermission(
User user,
String permission, {
String? organizationId,
bool auditLog = true,
}) async {
final cacheKey = _generateCacheKey(user.id, permission, organizationId);
// Vérification du cache
final cachedResult = _getCachedPermission(cacheKey);
if (cachedResult != null) {
_cacheHits++;
if (auditLog && !cachedResult.result) {
_logAuditEvent(user, permission, false, 'CACHED_DENIED', organizationId);
}
return cachedResult.result;
}
_cacheMisses++;
// Calcul de la permission
final result = await _computePermission(user, permission, organizationId);
// Mise en cache
_cachePermission(cacheKey, result, user.primaryRole);
// Audit trail
if (auditLog) {
_logAuditEvent(
user,
permission,
result,
result ? 'GRANTED' : 'DENIED',
organizationId,
);
}
return result;
}
/// Vérifie plusieurs permissions en une seule fois
static Future<Map<String, bool>> hasPermissions(
User user,
List<String> permissions, {
String? organizationId,
bool auditLog = true,
}) async {
final results = <String, bool>{};
// Traitement en parallèle pour les performances
final futures = permissions.map((permission) =>
hasPermission(user, permission, organizationId: organizationId, auditLog: auditLog)
.then((result) => MapEntry(permission, result))
);
final entries = await Future.wait(futures);
for (final entry in entries) {
results[entry.key] = entry.value;
}
return results;
}
/// Obtient toutes les permissions effectives d'un utilisateur
static Future<List<String>> getEffectivePermissions(
User user, {
String? organizationId,
}) async {
final cacheKey = '${user.id}_effective_${organizationId ?? 'global'}';
// Vérification du cache utilisateur
final cachedUserPermissions = _getCachedUserPermissions(cacheKey);
if (cachedUserPermissions != null) {
_cacheHits++;
return cachedUserPermissions.permissions;
}
_cacheMisses++;
// Calcul des permissions effectives
final permissions = user.getEffectivePermissions(organizationId: organizationId);
// Mise en cache
_cacheUserPermissions(cacheKey, permissions, user.primaryRole);
return permissions;
}
/// Vérifie si un utilisateur peut effectuer une action sur un domaine
static Future<bool> canPerformAction(
User user,
String domain,
String action, {
String scope = 'own',
String? organizationId,
}) async {
final permission = '$domain.$action.$scope';
return hasPermission(user, permission, organizationId: organizationId);
}
/// Invalide le cache pour un utilisateur spécifique
static void invalidateUserCache(String userId) {
final keysToRemove = <String>[];
// Invalider le cache des permissions
for (final key in _permissionCache.keys) {
if (key.startsWith('${userId}_')) {
keysToRemove.add(key);
}
}
for (final key in keysToRemove) {
_permissionCache.remove(key);
}
// Invalider le cache des permissions utilisateur
final userKeysToRemove = <String>[];
for (final key in _userPermissionsCache.keys) {
if (key.startsWith('${userId}_')) {
userKeysToRemove.add(key);
}
}
for (final key in userKeysToRemove) {
_userPermissionsCache.remove(key);
}
debugPrint('Cache invalidé pour l\'utilisateur: $userId');
}
/// Invalide tout le cache
static void invalidateAllCache() {
_permissionCache.clear();
_userPermissionsCache.clear();
debugPrint('Cache complet invalidé');
}
/// Obtient les statistiques du cache
static Map<String, dynamic> getCacheStats() {
final totalRequests = _cacheHits + _cacheMisses;
final hitRate = totalRequests > 0 ? (_cacheHits / totalRequests * 100) : 0.0;
return {
'cacheHits': _cacheHits,
'cacheMisses': _cacheMisses,
'hitRate': hitRate.toStringAsFixed(2),
'permissionCacheSize': _permissionCache.length,
'userPermissionsCacheSize': _userPermissionsCache.length,
};
}
/// Nettoie le cache expiré
static void cleanExpiredCache() {
final now = DateTime.now();
// Nettoyer le cache des permissions
_permissionCache.removeWhere((key, cached) => cached.expiresAt.isBefore(now));
// Nettoyer le cache des permissions utilisateur
_userPermissionsCache.removeWhere((key, cached) => cached.expiresAt.isBefore(now));
debugPrint('Cache expiré nettoyé');
}
// === MÉTHODES PRIVÉES ===
/// Calcule une permission sans cache
static Future<bool> _computePermission(
User user,
String permission,
String? organizationId,
) async {
// Vérification des permissions publiques
if (PermissionMatrix.isPublicPermission(permission)) {
return true;
}
// Vérification utilisateur actif
if (!user.isActive) return false;
// Vérification directe de l'utilisateur
if (user.hasPermission(permission, organizationId: organizationId)) {
return true;
}
// Vérifications contextuelles avancées
return _checkContextualPermissions(user, permission, organizationId);
}
/// Vérifications contextuelles avancées
static Future<bool> _checkContextualPermissions(
User user,
String permission,
String? organizationId,
) async {
// Logique contextuelle future (intégration avec le serveur)
// Pour l'instant, retourne false
return false;
}
/// Génère une clé de cache unique
static String _generateCacheKey(String userId, String permission, String? organizationId) {
return '${userId}_${permission}_${organizationId ?? 'global'}';
}
/// Obtient une permission depuis le cache
static _CachedPermission? _getCachedPermission(String key) {
final cached = _permissionCache[key];
if (cached != null && cached.expiresAt.isAfter(DateTime.now())) {
return cached;
}
if (cached != null) {
_permissionCache.remove(key);
}
return null;
}
/// Met en cache une permission
static void _cachePermission(String key, bool result, UserRole userRole) {
final ttl = userRole == UserRole.superAdmin ? _superAdminCacheTTL : _defaultCacheTTL;
_permissionCache[key] = _CachedPermission(
result: result,
expiresAt: DateTime.now().add(ttl),
);
}
/// Obtient les permissions utilisateur depuis le cache
static _CachedUserPermissions? _getCachedUserPermissions(String key) {
final cached = _userPermissionsCache[key];
if (cached != null && cached.expiresAt.isAfter(DateTime.now())) {
return cached;
}
if (cached != null) {
_userPermissionsCache.remove(key);
}
return null;
}
/// Met en cache les permissions utilisateur
static void _cacheUserPermissions(String key, List<String> permissions, UserRole userRole) {
final ttl = userRole == UserRole.superAdmin ? _superAdminCacheTTL : _defaultCacheTTL;
_userPermissionsCache[key] = _CachedUserPermissions(
permissions: permissions,
expiresAt: DateTime.now().add(ttl),
);
}
/// Enregistre un événement d'audit
static void _logAuditEvent(
User user,
String permission,
bool granted,
String reason,
String? organizationId,
) {
final event = PermissionAuditEvent(
userId: user.id,
userEmail: user.email,
permission: permission,
granted: granted,
reason: reason,
organizationId: organizationId,
timestamp: DateTime.now(),
);
_auditController.add(event);
}
}
/// Classe pour les permissions mises en cache
class _CachedPermission {
final bool result;
final DateTime expiresAt;
_CachedPermission({required this.result, required this.expiresAt});
}
/// Classe pour les permissions utilisateur mises en cache
class _CachedUserPermissions {
final List<String> permissions;
final DateTime expiresAt;
_CachedUserPermissions({required this.permissions, required this.expiresAt});
}
/// Événement d'audit des permissions
class PermissionAuditEvent {
final String userId;
final String userEmail;
final String permission;
final bool granted;
final String reason;
final String? organizationId;
final DateTime timestamp;
PermissionAuditEvent({
required this.userId,
required this.userEmail,
required this.permission,
required this.granted,
required this.reason,
this.organizationId,
required this.timestamp,
});
Map<String, dynamic> toJson() {
return {
'userId': userId,
'userEmail': userEmail,
'permission': permission,
'granted': granted,
'reason': reason,
'organizationId': organizationId,
'timestamp': timestamp.toIso8601String(),
};
}
}

View File

@@ -1,349 +0,0 @@
# Guide du Design System UnionFlow
## 📋 Table des matières
1. [Introduction](#introduction)
2. [Tokens](#tokens)
3. [Composants](#composants)
4. [Bonnes pratiques](#bonnes-pratiques)
---
## Introduction
Le Design System UnionFlow garantit la cohérence visuelle et l'expérience utilisateur dans toute l'application.
**Palette de couleurs** : Bleu Roi (#4169E1) + Bleu Pétrole (#2C5F6F)
**Basé sur** : Material Design 3 et tendances UI/UX 2024-2025
### Import
```dart
import 'package:unionflow_mobile_apps/core/design_system/unionflow_design_system.dart';
```
---
## Tokens
### 🎨 Couleurs (ColorTokens)
```dart
// Primaire
ColorTokens.primary // Bleu Roi #4169E1
ColorTokens.onPrimary // Blanc #FFFFFF
ColorTokens.primaryContainer // Container bleu roi
// Sémantiques
ColorTokens.success // Vert #10B981
ColorTokens.error // Rouge #DC2626
ColorTokens.warning // Orange #F59E0B
ColorTokens.info // Bleu #0EA5E9
// Surfaces
ColorTokens.surface // Blanc #FFFFFF
ColorTokens.background // Gris clair #F8F9FA
ColorTokens.onSurface // Texte principal #1F2937
ColorTokens.onSurfaceVariant // Texte secondaire #6B7280
// Gradients
ColorTokens.primaryGradient // [Bleu Roi, Bleu Roi clair]
```
### 📏 Espacements (SpacingTokens)
```dart
SpacingTokens.xs // 2px
SpacingTokens.sm // 4px
SpacingTokens.md // 8px
SpacingTokens.lg // 12px
SpacingTokens.xl // 16px
SpacingTokens.xxl // 20px
SpacingTokens.xxxl // 24px
SpacingTokens.huge // 32px
SpacingTokens.giant // 48px
```
### 🔘 Rayons (SpacingTokens)
```dart
SpacingTokens.radiusXs // 2px
SpacingTokens.radiusSm // 4px
SpacingTokens.radiusMd // 8px
SpacingTokens.radiusLg // 12px - Standard pour cards
SpacingTokens.radiusXl // 16px
SpacingTokens.radiusXxl // 20px
SpacingTokens.radiusCircular // 999px - Boutons ronds
```
### 🌑 Ombres (ShadowTokens)
```dart
ShadowTokens.xs // Ombre minimale
ShadowTokens.sm // Ombre petite (cards, boutons)
ShadowTokens.md // Ombre moyenne (cards importantes)
ShadowTokens.lg // Ombre large (modals, dialogs)
ShadowTokens.xl // Ombre très large (éléments flottants)
// Ombres colorées
ShadowTokens.primary // Ombre avec couleur primaire
ShadowTokens.success // Ombre verte
ShadowTokens.error // Ombre rouge
```
### ✍️ Typographie (TypographyTokens)
```dart
TypographyTokens.displayLarge // 57px - Titres héroïques
TypographyTokens.headlineLarge // 32px - Titres de page
TypographyTokens.headlineMedium // 28px - Sous-titres
TypographyTokens.titleLarge // 22px - Titres de section
TypographyTokens.titleMedium // 16px - Titres de card
TypographyTokens.bodyLarge // 16px - Corps de texte
TypographyTokens.bodyMedium // 14px - Corps standard
TypographyTokens.labelLarge // 14px - Labels
TypographyTokens.labelSmall // 11px - Petits labels
```
---
## Composants
### 📦 UFCard - Card standardisé
```dart
// Card avec ombre (par défaut)
UFCard(
child: Text('Contenu'),
)
// Card avec bordure
UFCard.outlined(
borderColor: ColorTokens.primary,
child: Text('Contenu'),
)
// Card avec fond coloré
UFCard.filled(
color: ColorTokens.primaryContainer,
child: Text('Contenu'),
)
// Card cliquable
UFCard(
onTap: () => print('Cliqué'),
child: Text('Contenu'),
)
```
### 📦 UFContainer - Container standardisé
```dart
// Container standard
UFContainer(
child: Text('Contenu'),
)
// Container arrondi
UFContainer.rounded(
color: ColorTokens.primary,
padding: EdgeInsets.all(SpacingTokens.lg),
child: Text('Contenu'),
)
// Container avec ombre
UFContainer.elevated(
child: Text('Contenu'),
)
// Container circulaire
UFContainer.circular(
width: 48,
height: 48,
color: ColorTokens.primary,
child: Icon(Icons.person),
)
```
### 📊 UFStatCard - Card de statistiques
```dart
UFStatCard(
title: 'Membres',
value: '142',
icon: Icons.people,
iconColor: ColorTokens.primary,
subtitle: '+5 ce mois',
onTap: () => navigateToMembers(),
)
```
### UFInfoCard - Card d'information
```dart
UFInfoCard(
title: 'État du système',
icon: Icons.health_and_safety,
iconColor: ColorTokens.success,
trailing: Badge(label: Text('OK')),
child: Column(
children: [
Text('Tous les systèmes fonctionnent normalement'),
],
),
)
```
### 🎯 UFHeader - Header de page
```dart
UFHeader(
title: 'Tableau de bord',
subtitle: 'Vue d\'ensemble de votre activité',
icon: Icons.dashboard,
onNotificationTap: () => showNotifications(),
onSettingsTap: () => showSettings(),
)
```
### 📱 UFAppBar - AppBar standardisé
```dart
UFAppBar(
title: 'Détails du membre',
actions: [
IconButton(
icon: Icon(Icons.edit),
onPressed: () => edit(),
),
],
)
```
### 🔘 Boutons
```dart
// Bouton primaire
UFPrimaryButton(
text: 'Enregistrer',
onPressed: () => save(),
icon: Icons.save,
)
// Bouton secondaire
UFSecondaryButton(
text: 'Annuler',
onPressed: () => cancel(),
)
```
---
## Bonnes pratiques
### ✅ À FAIRE
```dart
// ✅ Utiliser les tokens
Container(
padding: EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
color: ColorTokens.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.sm,
),
)
// ✅ Utiliser les composants
UFCard(
child: Text('Contenu'),
)
```
### ❌ À ÉVITER
```dart
// ❌ Valeurs hardcodées
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
)
// ❌ Card Flutter standard
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Text('Contenu'),
)
```
### 📐 Hiérarchie des espacements
- **xs/sm** : Éléments très proches (icône + texte)
- **md/lg** : Espacement interne standard
- **xl/xxl** : Espacement entre sections
- **xxxl+** : Grandes séparations
### 🎨 Hiérarchie des couleurs
1. **primary** : Actions principales, navigation active
2. **secondary** : Actions secondaires
3. **success/error/warning** : États et feedbacks
4. **surface/background** : Fonds et containers
### 🌑 Hiérarchie des ombres
- **xs/sm** : Cards et boutons standards
- **md** : Cards importantes
- **lg/xl** : Modals, dialogs, éléments flottants
- **Colorées** : Éléments avec accent visuel
---
## 🔄 Migration
Pour migrer du code existant :
1. Remplacer `Card` par `UFCard`
2. Remplacer `Container` personnalisés par `UFContainer`
3. Remplacer couleurs hardcodées par `ColorTokens`
4. Remplacer espacements hardcodés par `SpacingTokens`
5. Remplacer ombres personnalisées par `ShadowTokens`
**Exemple** :
```dart
// Avant
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Contenu'),
),
)
// Après
UFCard(
child: Text('Contenu'),
)
```
---
**Version** : 1.0.0
**Dernière mise à jour** : 2025-10-05

View File

@@ -1,103 +0,0 @@
/// UnionFlow Primary Button - Bouton principal
///
/// Bouton primaire avec la couleur Bleu Roi (#4169E1)
/// Utilisé pour les actions principales (connexion, enregistrer, valider, etc.)
library uf_primary_button;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Bouton primaire UnionFlow
///
/// Usage:
/// ```dart
/// UFPrimaryButton(
/// label: 'Connexion',
/// onPressed: () => login(),
/// icon: Icons.login,
/// isLoading: false,
/// )
/// ```
class UFPrimaryButton extends StatelessWidget {
/// Texte du bouton
final String label;
/// Callback appelé lors du clic
final VoidCallback? onPressed;
/// Indique si le bouton est en chargement
final bool isLoading;
/// Icône optionnelle à gauche du texte
final IconData? icon;
/// Bouton pleine largeur
final bool isFullWidth;
/// Hauteur personnalisée (optionnel)
final double? height;
const UFPrimaryButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.icon,
this.isFullWidth = false,
this.height,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: isFullWidth ? double.infinity : null,
height: height ?? SpacingTokens.buttonHeightLarge,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: ColorTokens.primary, // Bleu roi
foregroundColor: ColorTokens.onPrimary, // Blanc
disabledBackgroundColor: ColorTokens.primary.withOpacity(0.5),
disabledForegroundColor: ColorTokens.onPrimary.withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
padding: EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
),
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
ColorTokens.onPrimary,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
SizedBox(width: SpacingTokens.md),
],
Text(
label,
style: TypographyTokens.buttonLarge,
),
],
),
),
);
}
}

View File

@@ -1,82 +0,0 @@
/// UnionFlow Secondary Button - Bouton secondaire
///
/// Bouton secondaire avec la couleur Indigo (#6366F1)
/// Utilisé pour les actions secondaires (annuler, retour, etc.)
library uf_secondary_button;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Bouton secondaire UnionFlow
class UFSecondaryButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
final bool isFullWidth;
final double? height;
const UFSecondaryButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.icon,
this.isFullWidth = false,
this.height,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: isFullWidth ? double.infinity : null,
height: height ?? SpacingTokens.buttonHeightLarge,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: ColorTokens.secondary, // Indigo
foregroundColor: ColorTokens.onSecondary, // Blanc
disabledBackgroundColor: ColorTokens.secondary.withOpacity(0.5),
disabledForegroundColor: ColorTokens.onSecondary.withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
padding: EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
),
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
ColorTokens.onSecondary,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
SizedBox(width: SpacingTokens.md),
],
Text(
label,
style: TypographyTokens.buttonLarge,
),
],
),
),
);
}
}

View File

@@ -1,156 +0,0 @@
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Card standardisé UnionFlow
///
/// Composant Card unifié avec 3 styles prédéfinis :
/// - elevated : Card avec ombre (par défaut)
/// - outlined : Card avec bordure
/// - filled : Card avec fond coloré
///
/// Usage:
/// ```dart
/// UFCard(
/// child: Text('Contenu'),
/// )
///
/// UFCard.outlined(
/// child: Text('Contenu'),
/// )
///
/// UFCard.filled(
/// color: ColorTokens.primary,
/// child: Text('Contenu'),
/// )
/// ```
class UFCard extends StatelessWidget {
final Widget child;
final EdgeInsets? padding;
final EdgeInsets? margin;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final UFCardStyle style;
final Color? color;
final Color? borderColor;
final double? borderWidth;
final double? elevation;
final double? borderRadius;
/// Card avec ombre (style par défaut)
const UFCard({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
this.color,
this.elevation,
this.borderRadius,
}) : style = UFCardStyle.elevated,
borderColor = null,
borderWidth = null;
/// Card avec bordure
const UFCard.outlined({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
this.color,
this.borderColor,
this.borderWidth,
this.borderRadius,
}) : style = UFCardStyle.outlined,
elevation = null;
/// Card avec fond coloré
const UFCard.filled({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
required this.color,
this.borderRadius,
}) : style = UFCardStyle.filled,
borderColor = null,
borderWidth = null,
elevation = null;
@override
Widget build(BuildContext context) {
final effectivePadding = padding ?? EdgeInsets.all(SpacingTokens.cardPadding);
final effectiveMargin = margin ?? EdgeInsets.zero;
final effectiveBorderRadius = borderRadius ?? SpacingTokens.radiusLg;
Widget content = Container(
padding: effectivePadding,
decoration: _getDecoration(effectiveBorderRadius),
child: child,
);
if (onTap != null || onLongPress != null) {
content = InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(effectiveBorderRadius),
child: content,
);
}
return Container(
margin: effectiveMargin,
child: content,
);
}
BoxDecoration _getDecoration(double radius) {
switch (style) {
case UFCardStyle.elevated:
return BoxDecoration(
color: color ?? ColorTokens.surface,
borderRadius: BorderRadius.circular(radius),
boxShadow: [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: elevation ?? SpacingTokens.elevationSm * 5, // 10
offset: const Offset(0, 2),
),
],
);
case UFCardStyle.outlined:
return BoxDecoration(
color: color ?? ColorTokens.surface,
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: borderColor ?? ColorTokens.outline,
width: borderWidth ?? 1.0,
),
);
case UFCardStyle.filled:
return BoxDecoration(
color: color ?? ColorTokens.surfaceContainer,
borderRadius: BorderRadius.circular(radius),
);
}
}
}
/// Styles de Card disponibles
enum UFCardStyle {
/// Card avec ombre
elevated,
/// Card avec bordure
outlined,
/// Card avec fond coloré
filled,
}

View File

@@ -1,97 +0,0 @@
/// UnionFlow Info Card - Card d'information générique
///
/// Card blanche avec titre, icône et contenu personnalisable
library uf_info_card;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Card d'information générique
///
/// Usage:
/// ```dart
/// UFInfoCard(
/// title: 'État du système',
/// icon: Icons.health_and_safety,
/// iconColor: ColorTokens.primary,
/// trailing: Container(...), // Badge ou autre widget
/// child: Column(...), // Contenu de la card
/// )
/// ```
class UFInfoCard extends StatelessWidget {
/// Titre de la card
final String title;
/// Icône du titre
final IconData icon;
/// Couleur de l'icône (par défaut: primary)
final Color? iconColor;
/// Widget à droite du titre (badge, bouton, etc.)
final Widget? trailing;
/// Contenu de la card
final Widget child;
/// Padding de la card (par défaut: xl)
final EdgeInsets? padding;
const UFInfoCard({
super.key,
required this.title,
required this.icon,
this.iconColor,
this.trailing,
required this.child,
this.padding,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
final effectivePadding = padding ?? EdgeInsets.all(SpacingTokens.xl);
return Container(
padding: effectivePadding,
decoration: BoxDecoration(
color: ColorTokens.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusXl),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec titre et trailing
Row(
children: [
Icon(icon, color: effectiveIconColor, size: 20),
SizedBox(width: SpacingTokens.md),
Expanded(
child: Text(
title,
style: TypographyTokens.titleMedium.copyWith(
color: ColorTokens.onSurface,
),
),
),
if (trailing != null) trailing!,
],
),
SizedBox(height: SpacingTokens.xl),
// Contenu
child,
],
),
);
}
}

View File

@@ -1,76 +0,0 @@
/// UnionFlow Metric Card - Card de métrique système
///
/// Card compacte pour afficher une métrique système (CPU, RAM, etc.)
library uf_metric_card;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Card de métrique système
///
/// Usage:
/// ```dart
/// UFMetricCard(
/// label: 'CPU',
/// value: '23.5%',
/// icon: Icons.memory,
/// color: ColorTokens.success,
/// )
/// ```
class UFMetricCard extends StatelessWidget {
/// Label de la métrique (ex: "CPU")
final String label;
/// Valeur de la métrique (ex: "23.5%")
final String value;
/// Icône représentant la métrique
final IconData icon;
/// Couleur de la métrique (optionnel)
final Color? color;
const UFMetricCard({
super.key,
required this.label,
required this.value,
required this.icon,
this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Column(
children: [
Icon(icon, color: Colors.white, size: 16),
SizedBox(height: SpacingTokens.sm),
Text(
value,
style: TypographyTokens.labelSmall.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
Text(
label,
style: TypographyTokens.labelSmall.copyWith(
fontSize: 9,
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@@ -1,143 +0,0 @@
/// UnionFlow Stat Card - Card de statistiques
///
/// Card affichant une statistique avec icône, titre, valeur et sous-titre optionnel
/// Utilisé dans le dashboard pour afficher les métriques clés
library uf_stat_card;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Card de statistiques UnionFlow
///
/// Usage:
/// ```dart
/// UFStatCard(
/// title: 'Membres',
/// value: '142',
/// icon: Icons.people,
/// iconColor: ColorTokens.primary,
/// subtitle: '+5 ce mois',
/// onTap: () => navigateToMembers(),
/// )
/// ```
class UFStatCard extends StatelessWidget {
/// Titre de la statistique (ex: "Membres")
final String title;
/// Valeur de la statistique (ex: "142")
final String value;
/// Icône représentant la statistique
final IconData icon;
/// Couleur de l'icône (par défaut: primary)
final Color? iconColor;
/// Sous-titre optionnel (ex: "+5 ce mois")
final String? subtitle;
/// Callback appelé lors du clic sur la card
final VoidCallback? onTap;
/// Couleur de fond de l'icône (par défaut: iconColor avec opacité)
final Color? iconBackgroundColor;
const UFStatCard({
super.key,
required this.title,
required this.value,
required this.icon,
this.iconColor,
this.subtitle,
this.onTap,
this.iconBackgroundColor,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
final effectiveIconBgColor = iconBackgroundColor ??
effectiveIconColor.withOpacity(0.1);
return Card(
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
child: Padding(
padding: EdgeInsets.all(SpacingTokens.cardPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header avec icône et flèche
Row(
children: [
// Icône avec background coloré
Container(
padding: EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: effectiveIconBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 24,
),
),
const Spacer(),
// Flèche si cliquable
if (onTap != null)
Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorTokens.onSurfaceVariant,
),
],
),
SizedBox(height: SpacingTokens.lg),
// Titre
Text(
title,
style: TypographyTokens.labelLarge.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
SizedBox(height: SpacingTokens.sm),
// Valeur
Text(
value,
style: TypographyTokens.cardValue.copyWith(
color: ColorTokens.onSurface,
),
),
// Sous-titre optionnel
if (subtitle != null) ...[
SizedBox(height: SpacingTokens.sm),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
],
),
),
),
);
}
}

View File

@@ -1,26 +0,0 @@
/// UnionFlow Components - Export centralisé
///
/// Ce fichier exporte tous les composants réutilisables du Design System
library components;
// ═══════════════════════════════════════════════════════════════════════════
// BOUTONS
// ═══════════════════════════════════════════════════════════════════════════
export 'buttons/uf_primary_button.dart';
export 'buttons/uf_secondary_button.dart';
// ═══════════════════════════════════════════════════════════════════════════
// CARDS
// ═══════════════════════════════════════════════════════════════════════════
export 'cards/uf_stat_card.dart';
// TODO: Ajouter d'autres composants au fur et à mesure
// export 'buttons/uf_outline_button.dart';
// export 'buttons/uf_text_button.dart';
// export 'cards/uf_event_card.dart';
// export 'cards/uf_info_card.dart';
// export 'inputs/uf_text_field.dart';
// export 'navigation/uf_app_bar.dart';

View File

@@ -1,99 +0,0 @@
/// UnionFlow Dropdown Tile - Ligne de paramètre avec dropdown
///
/// Tile avec titre et dropdown pour les paramètres
library uf_dropdown_tile;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Tile de paramètre avec dropdown
///
/// Usage:
/// ```dart
/// UFDropdownTile<String>(
/// title: 'Niveau de log',
/// value: 'INFO',
/// items: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'],
/// onChanged: (value) => setState(() => _logLevel = value),
/// )
/// ```
class UFDropdownTile<T> extends StatelessWidget {
/// Titre du paramètre
final String title;
/// Valeur actuelle
final T value;
/// Liste des options
final List<T> items;
/// Callback appelé lors du changement
final ValueChanged<T?>? onChanged;
/// Couleur de fond (par défaut: surfaceVariant)
final Color? backgroundColor;
/// Fonction pour afficher le texte d'un item (par défaut: toString())
final String Function(T)? itemBuilder;
const UFDropdownTile({
super.key,
required this.title,
required this.value,
required this.items,
this.onChanged,
this.backgroundColor,
this.itemBuilder,
});
@override
Widget build(BuildContext context) {
final effectiveBgColor = backgroundColor ?? ColorTokens.surfaceVariant;
final effectiveItemBuilder = itemBuilder ?? (item) => item.toString();
return Container(
margin: EdgeInsets.only(bottom: SpacingTokens.lg),
padding: EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: effectiveBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Row(
children: [
Expanded(
child: Text(
title,
style: TypographyTokens.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.onSurface,
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: SpacingTokens.lg),
decoration: BoxDecoration(
color: ColorTokens.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(color: ColorTokens.outline),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(
value: value,
onChanged: onChanged,
items: items.map((item) {
return DropdownMenuItem<T>(
value: item,
child: Text(effectiveItemBuilder(item)),
);
}).toList(),
),
),
),
],
),
);
}
}

View File

@@ -1,90 +0,0 @@
/// UnionFlow Switch Tile - Ligne de paramètre avec switch
///
/// Tile avec titre, description et switch pour les paramètres
library uf_switch_tile;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Tile de paramètre avec switch
///
/// Usage:
/// ```dart
/// UFSwitchTile(
/// title: 'Notifications',
/// subtitle: 'Activer les notifications push',
/// value: true,
/// onChanged: (value) => setState(() => _notifications = value),
/// )
/// ```
class UFSwitchTile extends StatelessWidget {
/// Titre du paramètre
final String title;
/// Description du paramètre
final String subtitle;
/// Valeur actuelle du switch
final bool value;
/// Callback appelé lors du changement
final ValueChanged<bool>? onChanged;
/// Couleur de fond (par défaut: surfaceVariant)
final Color? backgroundColor;
const UFSwitchTile({
super.key,
required this.title,
required this.subtitle,
required this.value,
this.onChanged,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
final effectiveBgColor = backgroundColor ?? ColorTokens.surfaceVariant;
return Container(
margin: EdgeInsets.only(bottom: SpacingTokens.lg),
padding: EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: effectiveBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.onSurface,
),
),
Text(
subtitle,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: ColorTokens.primary,
),
],
),
);
}
}

View File

@@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../unionflow_design_system.dart';
/// AppBar standardisé UnionFlow
///
/// Composant AppBar unifié pour toutes les pages de détail/formulaire.
/// Garantit la cohérence visuelle et l'expérience utilisateur.
class UFAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final Widget? leading;
final bool automaticallyImplyLeading;
final PreferredSizeWidget? bottom;
final Color? backgroundColor;
final Color? foregroundColor;
final double elevation;
const UFAppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.automaticallyImplyLeading = true,
this.bottom,
this.backgroundColor,
this.foregroundColor,
this.elevation = 0,
});
@override
Widget build(BuildContext context) {
return AppBar(
title: Text(title),
backgroundColor: backgroundColor ?? ColorTokens.primary,
foregroundColor: foregroundColor ?? ColorTokens.onPrimary,
elevation: elevation,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
actions: actions,
bottom: bottom,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light, // Icônes claires sur fond bleu
statusBarBrightness: Brightness.dark, // Pour iOS
),
centerTitle: false,
titleTextStyle: TypographyTokens.titleLarge.copyWith(
color: foregroundColor ?? ColorTokens.onPrimary,
fontWeight: FontWeight.w600,
),
);
}
@override
Size get preferredSize => Size.fromHeight(
kToolbarHeight + (bottom?.preferredSize.height ?? 0.0),
);
}

View File

@@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import '../unionflow_design_system.dart';
/// Container standardisé UnionFlow
///
/// Composant Container unifié avec styles prédéfinis.
/// Garantit la cohérence des espacements, rayons et ombres.
///
/// Usage:
/// ```dart
/// UFContainer(
/// child: Text('Contenu'),
/// )
///
/// UFContainer.rounded(
/// color: ColorTokens.primary,
/// child: Text('Contenu'),
/// )
///
/// UFContainer.elevated(
/// child: Text('Contenu'),
/// )
/// ```
class UFContainer extends StatelessWidget {
final Widget child;
final Color? color;
final EdgeInsets? padding;
final EdgeInsets? margin;
final double? width;
final double? height;
final AlignmentGeometry? alignment;
final BoxConstraints? constraints;
final Gradient? gradient;
final double borderRadius;
final Border? border;
final List<BoxShadow>? boxShadow;
/// Container standard
const UFContainer({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusMd;
/// Container avec coins arrondis
const UFContainer.rounded({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusLg;
/// Container très arrondi
const UFContainer.extraRounded({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusXl;
/// Container avec ombre
UFContainer.elevated({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
}) : borderRadius = SpacingTokens.radiusLg,
boxShadow = [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: 10,
offset: const Offset(0, 2),
),
];
/// Container circulaire
const UFContainer.circular({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusCircular;
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
padding: padding,
margin: margin,
alignment: alignment,
constraints: constraints,
decoration: BoxDecoration(
color: gradient == null ? (color ?? ColorTokens.surface) : null,
gradient: gradient,
borderRadius: BorderRadius.circular(borderRadius),
border: border,
boxShadow: boxShadow,
),
child: child,
);
}
}

View File

@@ -1,125 +0,0 @@
import 'package:flutter/material.dart';
import '../design_tokens.dart';
/// Header harmonisé UnionFlow
///
/// Composant header standardisé pour toutes les pages de l'application.
/// Garantit la cohérence visuelle et l'expérience utilisateur.
class UFHeader extends StatelessWidget {
final String title;
final String? subtitle;
final IconData icon;
final List<Widget>? actions;
final VoidCallback? onNotificationTap;
final VoidCallback? onSettingsTap;
final bool showActions;
const UFHeader({
super.key,
required this.title,
this.subtitle,
required this.icon,
this.actions,
this.onNotificationTap,
this.onSettingsTap,
this.showActions = true,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UnionFlowDesignTokens.spaceMD),
decoration: BoxDecoration(
gradient: UnionFlowDesignTokens.primaryGradient,
borderRadius: BorderRadius.circular(UnionFlowDesignTokens.radiusLG),
boxShadow: UnionFlowDesignTokens.shadowXL,
),
child: Row(
children: [
// Icône et contenu principal
Container(
padding: const EdgeInsets.all(UnionFlowDesignTokens.spaceSM),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(UnionFlowDesignTokens.radiusBase),
),
child: Icon(
icon,
color: UnionFlowDesignTokens.textOnPrimary,
size: 24,
),
),
const SizedBox(width: UnionFlowDesignTokens.spaceMD),
// Titre et sous-titre
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: UnionFlowDesignTokens.headingMD.copyWith(
color: UnionFlowDesignTokens.textOnPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: UnionFlowDesignTokens.spaceXS),
Text(
subtitle!,
style: UnionFlowDesignTokens.bodySM.copyWith(
color: UnionFlowDesignTokens.textOnPrimary.withOpacity(0.8),
),
),
],
],
),
),
// Actions
if (showActions) _buildActions(),
],
),
);
}
Widget _buildActions() {
if (actions != null) {
return Row(children: actions!);
}
return Row(
children: [
if (onNotificationTap != null)
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(UnionFlowDesignTokens.radiusSM),
),
child: IconButton(
onPressed: onNotificationTap,
icon: const Icon(
Icons.notifications_outlined,
color: UnionFlowDesignTokens.textOnPrimary,
),
),
),
if (onNotificationTap != null && onSettingsTap != null)
const SizedBox(width: UnionFlowDesignTokens.spaceSM),
if (onSettingsTap != null)
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(UnionFlowDesignTokens.radiusSM),
),
child: IconButton(
onPressed: onSettingsTap,
icon: const Icon(
Icons.settings_outlined,
color: UnionFlowDesignTokens.textOnPrimary,
),
),
),
],
);
}
}

View File

@@ -1,248 +0,0 @@
import 'package:flutter/material.dart';
import '../unionflow_design_system.dart';
/// Header de page compact et moderne
///
/// Composant header minimaliste pour les pages principales du BottomNavigationBar.
/// Design épuré sans gradient lourd, optimisé pour l'espace.
///
/// Usage:
/// ```dart
/// UFPageHeader(
/// title: 'Membres',
/// icon: Icons.people,
/// actions: [
/// IconButton(icon: Icon(Icons.add), onPressed: () {}),
/// ],
/// )
/// ```
class UFPageHeader extends StatelessWidget {
final String title;
final IconData icon;
final List<Widget>? actions;
final Color? iconColor;
final bool showDivider;
const UFPageHeader({
super.key,
required this.title,
required this.icon,
this.actions,
this.iconColor,
this.showDivider = true,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
vertical: SpacingTokens.md,
),
child: Row(
children: [
// Icône
Container(
padding: EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: effectiveIconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 20,
),
),
SizedBox(width: SpacingTokens.md),
// Titre
Expanded(
child: Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onSurface,
fontWeight: FontWeight.w600,
),
),
),
// Actions
if (actions != null) ...actions!,
],
),
),
// Divider optionnel
if (showDivider)
Divider(
height: 1,
thickness: 1,
color: ColorTokens.outline.withOpacity(0.1),
),
],
);
}
}
/// Header de page avec statistiques
///
/// Header compact avec des métriques KPI intégrées.
///
/// Usage:
/// ```dart
/// UFPageHeaderWithStats(
/// title: 'Membres',
/// icon: Icons.people,
/// stats: [
/// UFHeaderStat(label: 'Total', value: '142'),
/// UFHeaderStat(label: 'Actifs', value: '128'),
/// ],
/// )
/// ```
class UFPageHeaderWithStats extends StatelessWidget {
final String title;
final IconData icon;
final List<UFHeaderStat> stats;
final List<Widget>? actions;
final Color? iconColor;
const UFPageHeaderWithStats({
super.key,
required this.title,
required this.icon,
required this.stats,
this.actions,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
return Column(
children: [
// Titre et actions
Padding(
padding: EdgeInsets.fromLTRB(
SpacingTokens.lg,
SpacingTokens.md,
SpacingTokens.lg,
SpacingTokens.sm,
),
child: Row(
children: [
// Icône
Container(
padding: EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: effectiveIconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 20,
),
),
SizedBox(width: SpacingTokens.md),
// Titre
Expanded(
child: Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onSurface,
fontWeight: FontWeight.w600,
),
),
),
// Actions
if (actions != null) ...actions!,
],
),
),
// Statistiques
Padding(
padding: EdgeInsets.fromLTRB(
SpacingTokens.lg,
0,
SpacingTokens.lg,
SpacingTokens.md,
),
child: Row(
children: stats.map((stat) {
final isLast = stat == stats.last;
return Expanded(
child: Padding(
padding: EdgeInsets.only(
right: isLast ? 0 : SpacingTokens.sm,
),
child: _buildStatItem(stat),
),
);
}).toList(),
),
),
// Divider
Divider(
height: 1,
thickness: 1,
color: ColorTokens.outline.withOpacity(0.1),
),
],
);
}
Widget _buildStatItem(UFHeaderStat stat) {
return UFContainer.rounded(
padding: EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
color: (stat.color ?? ColorTokens.primary).withOpacity(0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stat.value,
style: TypographyTokens.titleMedium.copyWith(
color: stat.color ?? ColorTokens.primary,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: SpacingTokens.xs),
Text(
stat.label,
style: TypographyTokens.labelSmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
/// Statistique pour UFPageHeaderWithStats
class UFHeaderStat {
final String label;
final String value;
final Color? color;
const UFHeaderStat({
required this.label,
required this.value,
this.color,
});
}

View File

@@ -1,457 +0,0 @@
/// Thème Sophistiqué UnionFlow
///
/// Implémentation complète du design system avec les dernières tendances UI/UX 2024-2025
/// Architecture modulaire et tokens de design cohérents
library app_theme_sophisticated;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../tokens/color_tokens.dart';
import '../tokens/typography_tokens.dart';
import '../tokens/spacing_tokens.dart';
/// Thème principal de l'application UnionFlow
class AppThemeSophisticated {
AppThemeSophisticated._();
// ═══════════════════════════════════════════════════════════════════════════
// THÈME PRINCIPAL - Configuration complète
// ═══════════════════════════════════════════════════════════════════════════
/// Thème clair principal
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// Couleurs principales
colorScheme: _lightColorScheme,
// Typographie
textTheme: _textTheme,
// Configuration de l'AppBar
appBarTheme: _appBarTheme,
// Configuration des cartes
cardTheme: _cardTheme,
// Configuration des boutons
elevatedButtonTheme: _elevatedButtonTheme,
filledButtonTheme: _filledButtonTheme,
outlinedButtonTheme: _outlinedButtonTheme,
textButtonTheme: _textButtonTheme,
// Configuration des champs de saisie
inputDecorationTheme: _inputDecorationTheme,
// Configuration de la navigation
navigationBarTheme: _navigationBarTheme,
navigationDrawerTheme: _navigationDrawerTheme,
// Configuration des dialogues
dialogTheme: _dialogTheme,
// Configuration des snackbars
snackBarTheme: _snackBarTheme,
// Configuration des puces
chipTheme: _chipTheme,
// Configuration des listes
listTileTheme: _listTileTheme,
// Configuration des onglets
tabBarTheme: _tabBarTheme,
// Configuration des dividers
dividerTheme: _dividerTheme,
// Configuration des icônes
iconTheme: _iconTheme,
// Configuration des surfaces
scaffoldBackgroundColor: ColorTokens.surface,
canvasColor: ColorTokens.surface,
// Configuration des animations
pageTransitionsTheme: _pageTransitionsTheme,
// Configuration des extensions
extensions: const [
_customColors,
_customSpacing,
],
);
}
// ═══════════════════════════════════════════════════════════════════════════
// SCHÉMA DE COULEURS
// ═══════════════════════════════════════════════════════════════════════════
static const ColorScheme _lightColorScheme = ColorScheme.light(
// Couleurs primaires
primary: ColorTokens.primary,
onPrimary: ColorTokens.onPrimary,
primaryContainer: ColorTokens.primaryContainer,
onPrimaryContainer: ColorTokens.onPrimaryContainer,
// Couleurs secondaires
secondary: ColorTokens.secondary,
onSecondary: ColorTokens.onSecondary,
secondaryContainer: ColorTokens.secondaryContainer,
onSecondaryContainer: ColorTokens.onSecondaryContainer,
// Couleurs tertiaires
tertiary: ColorTokens.tertiary,
onTertiary: ColorTokens.onTertiary,
tertiaryContainer: ColorTokens.tertiaryContainer,
onTertiaryContainer: ColorTokens.onTertiaryContainer,
// Couleurs d'erreur
error: ColorTokens.error,
onError: ColorTokens.onError,
errorContainer: ColorTokens.errorContainer,
onErrorContainer: ColorTokens.onErrorContainer,
// Couleurs de surface
surface: ColorTokens.surface,
onSurface: ColorTokens.onSurface,
surfaceContainerHighest: ColorTokens.surfaceVariant,
onSurfaceVariant: ColorTokens.onSurfaceVariant,
// Couleurs de contour
outline: ColorTokens.outline,
outlineVariant: ColorTokens.outlineVariant,
// Couleurs d'ombre
shadow: ColorTokens.shadow,
scrim: ColorTokens.shadow,
// Couleurs d'inversion
inverseSurface: ColorTokens.onSurface,
onInverseSurface: ColorTokens.surface,
inversePrimary: ColorTokens.primaryLight,
);
// ═══════════════════════════════════════════════════════════════════════════
// THÈME TYPOGRAPHIQUE
// ═══════════════════════════════════════════════════════════════════════════
static const TextTheme _textTheme = TextTheme(
// Display styles
displayLarge: TypographyTokens.displayLarge,
displayMedium: TypographyTokens.displayMedium,
displaySmall: TypographyTokens.displaySmall,
// Headline styles
headlineLarge: TypographyTokens.headlineLarge,
headlineMedium: TypographyTokens.headlineMedium,
headlineSmall: TypographyTokens.headlineSmall,
// Title styles
titleLarge: TypographyTokens.titleLarge,
titleMedium: TypographyTokens.titleMedium,
titleSmall: TypographyTokens.titleSmall,
// Label styles
labelLarge: TypographyTokens.labelLarge,
labelMedium: TypographyTokens.labelMedium,
labelSmall: TypographyTokens.labelSmall,
// Body styles
bodyLarge: TypographyTokens.bodyLarge,
bodyMedium: TypographyTokens.bodyMedium,
bodySmall: TypographyTokens.bodySmall,
);
// ═══════════════════════════════════════════════════════════════════════════
// THÈMES DE COMPOSANTS
// ═══════════════════════════════════════════════════════════════════════════
/// Configuration AppBar moderne (sans AppBar traditionnelle)
static const AppBarTheme _appBarTheme = AppBarTheme(
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: ColorTokens.onSurface,
surfaceTintColor: Colors.transparent,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
),
);
/// Configuration des cartes sophistiquées
static final CardTheme _cardTheme = CardTheme(
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
surfaceTintColor: ColorTokens.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
margin: const EdgeInsets.all(SpacingTokens.cardMargin),
);
/// Configuration des boutons élevés
static final ElevatedButtonThemeData _elevatedButtonTheme = ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
backgroundColor: ColorTokens.primary,
foregroundColor: ColorTokens.onPrimary,
textStyle: TypographyTokens.buttonMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
minimumSize: const Size(
SpacingTokens.minButtonWidth,
SpacingTokens.buttonHeightMedium,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
),
);
/// Configuration des boutons remplis
static final FilledButtonThemeData _filledButtonTheme = FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: ColorTokens.primary,
foregroundColor: ColorTokens.onPrimary,
textStyle: TypographyTokens.buttonMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
minimumSize: const Size(
SpacingTokens.minButtonWidth,
SpacingTokens.buttonHeightMedium,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
),
);
/// Configuration des boutons avec contour
static final OutlinedButtonThemeData _outlinedButtonTheme = OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: ColorTokens.primary,
textStyle: TypographyTokens.buttonMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
minimumSize: const Size(
SpacingTokens.minButtonWidth,
SpacingTokens.buttonHeightMedium,
),
side: const BorderSide(
color: ColorTokens.outline,
width: 1.0,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
),
);
/// Configuration des boutons texte
static final TextButtonThemeData _textButtonTheme = TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: ColorTokens.primary,
textStyle: TypographyTokens.buttonMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
minimumSize: const Size(
SpacingTokens.minButtonWidth,
SpacingTokens.buttonHeightMedium,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
),
);
/// Configuration des champs de saisie
static final InputDecorationTheme _inputDecorationTheme = InputDecorationTheme(
filled: true,
fillColor: ColorTokens.surfaceContainer,
labelStyle: TypographyTokens.inputLabel,
hintStyle: TypographyTokens.inputHint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ColorTokens.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ColorTokens.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ColorTokens.primary, width: 2.0),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ColorTokens.error),
),
contentPadding: const EdgeInsets.all(SpacingTokens.formPadding),
);
/// Configuration de la barre de navigation
static final NavigationBarThemeData _navigationBarTheme = NavigationBarThemeData(
backgroundColor: ColorTokens.navigationBackground,
indicatorColor: ColorTokens.navigationIndicator,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return TypographyTokens.navigationLabelSelected;
}
return TypographyTokens.navigationLabel;
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: ColorTokens.navigationSelected);
}
return const IconThemeData(color: ColorTokens.navigationUnselected);
}),
);
/// Configuration du drawer de navigation
static final NavigationDrawerThemeData _navigationDrawerTheme = NavigationDrawerThemeData(
backgroundColor: ColorTokens.surfaceContainer,
elevation: SpacingTokens.elevationMd,
shadowColor: ColorTokens.shadow,
surfaceTintColor: ColorTokens.surfaceContainer,
indicatorColor: ColorTokens.primaryContainer,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return TypographyTokens.navigationLabelSelected;
}
return TypographyTokens.navigationLabel;
}),
);
/// Configuration des dialogues
static final DialogTheme _dialogTheme = DialogTheme(
backgroundColor: ColorTokens.surfaceContainer,
elevation: SpacingTokens.elevationLg,
shadowColor: ColorTokens.shadow,
surfaceTintColor: ColorTokens.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusXl),
),
titleTextStyle: TypographyTokens.headlineSmall,
contentTextStyle: TypographyTokens.bodyMedium,
);
/// Configuration des snackbars
static final SnackBarThemeData _snackBarTheme = SnackBarThemeData(
backgroundColor: ColorTokens.onSurface,
contentTextStyle: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.surface,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
behavior: SnackBarBehavior.floating,
);
/// Configuration des puces
static final ChipThemeData _chipTheme = ChipThemeData(
backgroundColor: ColorTokens.surfaceVariant,
selectedColor: ColorTokens.primaryContainer,
labelStyle: TypographyTokens.labelMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
);
/// Configuration des éléments de liste
static const ListTileThemeData _listTileTheme = ListTileThemeData(
contentPadding: EdgeInsets.symmetric(
horizontal: SpacingTokens.xl,
vertical: SpacingTokens.md,
),
titleTextStyle: TypographyTokens.titleMedium,
subtitleTextStyle: TypographyTokens.bodyMedium,
leadingAndTrailingTextStyle: TypographyTokens.labelMedium,
minVerticalPadding: SpacingTokens.md,
);
/// Configuration des onglets
static final TabBarTheme _tabBarTheme = TabBarTheme(
labelColor: ColorTokens.primary,
unselectedLabelColor: ColorTokens.onSurfaceVariant,
labelStyle: TypographyTokens.titleSmall,
unselectedLabelStyle: TypographyTokens.titleSmall,
indicator: UnderlineTabIndicator(
borderSide: const BorderSide(
color: ColorTokens.primary,
width: 2.0,
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
),
);
/// Configuration des dividers
static const DividerThemeData _dividerTheme = DividerThemeData(
color: ColorTokens.outline,
thickness: 1.0,
space: SpacingTokens.md,
);
/// Configuration des icônes
static const IconThemeData _iconTheme = IconThemeData(
color: ColorTokens.onSurfaceVariant,
size: 24.0,
);
/// Configuration des transitions de page
static const PageTransitionsTheme _pageTransitionsTheme = PageTransitionsTheme(
builders: {
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
);
/// Extensions personnalisées - Couleurs
static const CustomColors _customColors = CustomColors();
/// Extensions personnalisées - Espacements
static const CustomSpacing _customSpacing = CustomSpacing();
}
/// Extension de couleurs personnalisées
class CustomColors extends ThemeExtension<CustomColors> {
const CustomColors();
@override
CustomColors copyWith() => const CustomColors();
@override
CustomColors lerp(ThemeExtension<CustomColors>? other, double t) {
return const CustomColors();
}
}
/// Extension d'espacements personnalisés
class CustomSpacing extends ThemeExtension<CustomSpacing> {
const CustomSpacing();
@override
CustomSpacing copyWith() => const CustomSpacing();
@override
CustomSpacing lerp(ThemeExtension<CustomSpacing>? other, double t) {
return const CustomSpacing();
}
}

View File

@@ -1,158 +0,0 @@
/// Design Tokens - Couleurs
///
/// Palette de couleurs sophistiquée inspirée des tendances UI/UX 2024-2025
/// Basée sur les principes de Material Design 3 et les meilleures pratiques
/// d'applications professionnelles d'entreprise.
library color_tokens;
import 'package:flutter/material.dart';
/// Tokens de couleurs primaires - Palette sophistiquée
class ColorTokens {
ColorTokens._();
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS PRIMAIRES - Bleu professionnel moderne
// ═══════════════════════════════════════════════════════════════════════════
/// Couleur primaire principale - Bleu corporate moderne
static const Color primary = Color(0xFF1E3A8A); // Bleu profond
static const Color primaryLight = Color(0xFF3B82F6); // Bleu vif
static const Color primaryDark = Color(0xFF1E40AF); // Bleu sombre
static const Color primaryContainer = Color(0xFFDEEAFF); // Container bleu clair
static const Color onPrimary = Color(0xFFFFFFFF); // Texte sur primaire
static const Color onPrimaryContainer = Color(0xFF001D36); // Texte sur container
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS SECONDAIRES - Accent sophistiqué
// ═══════════════════════════════════════════════════════════════════════════
static const Color secondary = Color(0xFF6366F1); // Indigo moderne
static const Color secondaryLight = Color(0xFF8B5CF6); // Violet clair
static const Color secondaryDark = Color(0xFF4F46E5); // Indigo sombre
static const Color secondaryContainer = Color(0xFFE0E7FF); // Container indigo
static const Color onSecondary = Color(0xFFFFFFFF);
static const Color onSecondaryContainer = Color(0xFF1E1B3A);
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS TERTIAIRES - Accent complémentaire
// ═══════════════════════════════════════════════════════════════════════════
static const Color tertiary = Color(0xFF059669); // Vert émeraude
static const Color tertiaryLight = Color(0xFF10B981); // Vert clair
static const Color tertiaryDark = Color(0xFF047857); // Vert sombre
static const Color tertiaryContainer = Color(0xFFD1FAE5); // Container vert
static const Color onTertiary = Color(0xFFFFFFFF);
static const Color onTertiaryContainer = Color(0xFF002114);
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS NEUTRES - Échelle de gris sophistiquée
// ═══════════════════════════════════════════════════════════════════════════
static const Color surface = Color(0xFFFAFAFA); // Surface principale
static const Color surfaceVariant = Color(0xFFF5F5F5); // Surface variante
static const Color surfaceContainer = Color(0xFFFFFFFF); // Container surface
static const Color surfaceContainerHigh = Color(0xFFF8F9FA); // Container élevé
static const Color surfaceContainerHighest = Color(0xFFE5E7EB); // Container max
static const Color onSurface = Color(0xFF1F2937); // Texte principal
static const Color onSurfaceVariant = Color(0xFF6B7280); // Texte secondaire
static const Color textSecondary = Color(0xFF6B7280); // Texte secondaire (alias)
static const Color outline = Color(0xFFD1D5DB); // Bordures
static const Color outlineVariant = Color(0xFFE5E7EB); // Bordures claires
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS SÉMANTIQUES - États et feedback
// ═══════════════════════════════════════════════════════════════════════════
/// Couleurs de succès
static const Color success = Color(0xFF10B981); // Vert succès
static const Color successLight = Color(0xFF34D399); // Vert clair
static const Color successDark = Color(0xFF059669); // Vert sombre
static const Color successContainer = Color(0xFFECFDF5); // Container succès
static const Color onSuccess = Color(0xFFFFFFFF);
static const Color onSuccessContainer = Color(0xFF002114);
/// Couleurs d'erreur
static const Color error = Color(0xFFDC2626); // Rouge erreur
static const Color errorLight = Color(0xFFEF4444); // Rouge clair
static const Color errorDark = Color(0xFFB91C1C); // Rouge sombre
static const Color errorContainer = Color(0xFFFEF2F2); // Container erreur
static const Color onError = Color(0xFFFFFFFF);
static const Color onErrorContainer = Color(0xFF410002);
/// Couleurs d'avertissement
static const Color warning = Color(0xFFF59E0B); // Orange avertissement
static const Color warningLight = Color(0xFFFBBF24); // Orange clair
static const Color warningDark = Color(0xFFD97706); // Orange sombre
static const Color warningContainer = Color(0xFFFEF3C7); // Container avertissement
static const Color onWarning = Color(0xFFFFFFFF);
static const Color onWarningContainer = Color(0xFF2D1B00);
/// Couleurs d'information
static const Color info = Color(0xFF0EA5E9); // Bleu info
static const Color infoLight = Color(0xFF38BDF8); // Bleu clair
static const Color infoDark = Color(0xFF0284C7); // Bleu sombre
static const Color infoContainer = Color(0xFFE0F2FE); // Container info
static const Color onInfo = Color(0xFFFFFFFF);
static const Color onInfoContainer = Color(0xFF001D36);
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS SPÉCIALISÉES - Interface avancée
// ═══════════════════════════════════════════════════════════════════════════
/// Couleurs de navigation
static const Color navigationBackground = Color(0xFFFFFFFF);
static const Color navigationSelected = Color(0xFF1E3A8A);
static const Color navigationUnselected = Color(0xFF6B7280);
static const Color navigationIndicator = Color(0xFF3B82F6);
/// Couleurs d'élévation et ombres
static const Color shadow = Color(0x1A000000); // Ombre légère
static const Color shadowMedium = Color(0x33000000); // Ombre moyenne
static const Color shadowHigh = Color(0x4D000000); // Ombre forte
/// Couleurs de glassmorphism (tendance 2024-2025)
static const Color glassBackground = Color(0x80FFFFFF); // Fond verre
static const Color glassBorder = Color(0x33FFFFFF); // Bordure verre
static const Color glassOverlay = Color(0x0DFFFFFF); // Overlay verre
/// Couleurs de gradient (tendance moderne)
static const List<Color> primaryGradient = [
Color(0xFF1E3A8A),
Color(0xFF3B82F6),
];
static const List<Color> secondaryGradient = [
Color(0xFF6366F1),
Color(0xFF8B5CF6),
];
static const List<Color> successGradient = [
Color(0xFF059669),
Color(0xFF10B981),
];
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES UTILITAIRES
// ═══════════════════════════════════════════════════════════════════════════
/// Obtient une couleur avec opacité
static Color withOpacity(Color color, double opacity) {
return color.withOpacity(opacity);
}
/// Obtient une couleur plus claire
static Color lighten(Color color, [double amount = 0.1]) {
final hsl = HSLColor.fromColor(color);
final lightness = (hsl.lightness + amount).clamp(0.0, 1.0);
return hsl.withLightness(lightness).toColor();
}
/// Obtient une couleur plus sombre
static Color darken(Color color, [double amount = 0.1]) {
final hsl = HSLColor.fromColor(color);
final lightness = (hsl.lightness - amount).clamp(0.0, 1.0);
return hsl.withLightness(lightness).toColor();
}
}

View File

@@ -1,23 +0,0 @@
/// Tokens de rayon pour le design system
/// Définit les rayons de bordure standardisés de l'application
library radius_tokens;
/// Tokens de rayon
class RadiusTokens {
RadiusTokens._();
/// Small - 4px
static const double sm = 4.0;
/// Medium - 8px
static const double md = 8.0;
/// Large - 12px
static const double lg = 12.0;
/// Extra large - 16px
static const double xl = 16.0;
/// Round - 50px
static const double round = 50.0;
}

View File

@@ -1,150 +0,0 @@
/// Tokens d'ombres pour le design system
/// Définit les ombres standardisées de l'application
library shadow_tokens;
import 'package:flutter/material.dart';
import 'color_tokens.dart';
/// Tokens d'ombres standardisés
///
/// Utilisation cohérente des ombres dans toute l'application.
/// Basé sur les principes de Material Design 3.
class ShadowTokens {
ShadowTokens._();
// ═══════════════════════════════════════════════════════════════════════════
// OMBRES STANDARDS
// ═══════════════════════════════════════════════════════════════════════════
/// Ombre minimale - Pour éléments subtils
static final List<BoxShadow> xs = [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: 4,
offset: const Offset(0, 1),
),
];
/// Ombre petite - Pour cards et boutons
static final List<BoxShadow> sm = [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: 8,
offset: const Offset(0, 2),
),
];
/// Ombre moyenne - Pour cards importantes
static final List<BoxShadow> md = [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: 12,
offset: const Offset(0, 4),
),
];
/// Ombre large - Pour modals et dialogs
static final List<BoxShadow> lg = [
BoxShadow(
color: ColorTokens.shadowMedium,
blurRadius: 16,
offset: const Offset(0, 6),
),
];
/// Ombre très large - Pour éléments flottants
static final List<BoxShadow> xl = [
BoxShadow(
color: ColorTokens.shadowMedium,
blurRadius: 24,
offset: const Offset(0, 8),
),
];
/// Ombre extra large - Pour éléments héroïques
static final List<BoxShadow> xxl = [
BoxShadow(
color: ColorTokens.shadowHigh,
blurRadius: 32,
offset: const Offset(0, 12),
spreadRadius: -4,
),
];
// ═══════════════════════════════════════════════════════════════════════════
// OMBRES COLORÉES
// ═══════════════════════════════════════════════════════════════════════════
/// Ombre primaire - Pour éléments avec couleur primaire
static final List<BoxShadow> primary = [
BoxShadow(
color: ColorTokens.primary.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// Ombre secondaire - Pour éléments avec couleur secondaire
static final List<BoxShadow> secondary = [
BoxShadow(
color: ColorTokens.secondary.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// Ombre success - Pour éléments de succès
static final List<BoxShadow> success = [
BoxShadow(
color: ColorTokens.success.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// Ombre error - Pour éléments d'erreur
static final List<BoxShadow> error = [
BoxShadow(
color: ColorTokens.error.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// Ombre warning - Pour éléments d'avertissement
static final List<BoxShadow> warning = [
BoxShadow(
color: ColorTokens.warning.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
// ═══════════════════════════════════════════════════════════════════════════
// OMBRES SPÉCIALES
// ═══════════════════════════════════════════════════════════════════════════
/// Ombre interne - Pour effets enfoncés
static final List<BoxShadow> inner = [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: 4,
offset: const Offset(0, 2),
spreadRadius: -2,
),
];
/// Ombre diffuse - Pour glassmorphism
static final List<BoxShadow> diffuse = [
BoxShadow(
color: ColorTokens.shadow.withOpacity(0.05),
blurRadius: 20,
offset: const Offset(0, 4),
spreadRadius: 2,
),
];
/// Pas d'ombre
static const List<BoxShadow> none = [];
}

View File

@@ -1,194 +0,0 @@
/// Design Tokens - Espacements
///
/// Système d'espacement cohérent basé sur une grille de 4px
/// Optimisé pour la lisibilité et l'harmonie visuelle
library spacing_tokens;
/// Tokens d'espacement - Système de grille moderne
class SpacingTokens {
SpacingTokens._();
// ═══════════════════════════════════════════════════════════════════════════
// ESPACEMENT DE BASE - Grille 4px
// ═══════════════════════════════════════════════════════════════════════════
/// Unité de base (4px) - Fondation du système
static const double baseUnit = 4.0;
/// Espacement minimal (2px) - Détails fins
static const double xs = baseUnit * 0.5; // 2px
/// Espacement très petit (4px) - Éléments adjacents
static const double sm = baseUnit * 1; // 4px
/// Espacement petit (8px) - Espacement interne léger
static const double md = baseUnit * 2; // 8px
/// Espacement moyen (12px) - Espacement standard
static const double lg = baseUnit * 3; // 12px
/// Espacement large (16px) - Séparation de composants
static const double xl = baseUnit * 4; // 16px
/// Espacement très large (20px) - Séparation importante
static const double xxl = baseUnit * 5; // 20px
/// Espacement extra large (24px) - Sections principales
static const double xxxl = baseUnit * 6; // 24px
/// Espacement massif (32px) - Séparation majeure
static const double huge = baseUnit * 8; // 32px
/// Espacement géant (48px) - Espacement héroïque
static const double giant = baseUnit * 12; // 48px
// ═══════════════════════════════════════════════════════════════════════════
// ESPACEMENTS SPÉCIALISÉS - Composants spécifiques
// ═══════════════════════════════════════════════════════════════════════════
/// Padding des conteneurs
static const double containerPaddingSmall = lg; // 12px
static const double containerPaddingMedium = xl; // 16px
static const double containerPaddingLarge = xxxl; // 24px
/// Marges des cartes
static const double cardMargin = xl; // 16px
static const double cardPadding = xl; // 16px
static const double cardPaddingLarge = xxxl; // 24px
/// Espacement des listes
static const double listItemSpacing = md; // 8px
static const double listSectionSpacing = xxxl; // 24px
/// Espacement des boutons
static const double buttonPaddingHorizontal = xl; // 16px
static const double buttonPaddingVertical = lg; // 12px
static const double buttonSpacing = md; // 8px
/// Espacement des formulaires
static const double formFieldSpacing = xl; // 16px
static const double formSectionSpacing = xxxl; // 24px
static const double formPadding = xl; // 16px
/// Espacement de navigation
static const double navigationPadding = xl; // 16px
static const double navigationItemSpacing = md; // 8px
static const double navigationSectionSpacing = xxxl; // 24px
/// Espacement des en-têtes
static const double headerPadding = xl; // 16px
static const double headerHeight = 56.0; // Hauteur standard
static const double headerElevation = 4.0; // Élévation
/// Espacement des onglets
static const double tabPadding = xl; // 16px
static const double tabHeight = 48.0; // Hauteur standard
/// Espacement des dialogues
static const double dialogPadding = xxxl; // 24px
static const double dialogMargin = xl; // 16px
/// Espacement des snackbars
static const double snackbarMargin = xl; // 16px
static const double snackbarPadding = xl; // 16px
// ═══════════════════════════════════════════════════════════════════════════
// RAYONS DE BORDURE - Système cohérent
// ═══════════════════════════════════════════════════════════════════════════
/// Rayon minimal (2px) - Détails subtils
static const double radiusXs = 2.0;
/// Rayon petit (4px) - Éléments fins
static const double radiusSm = 4.0;
/// Rayon moyen (8px) - Standard
static const double radiusMd = 8.0;
/// Rayon large (12px) - Cartes et composants
static const double radiusLg = 12.0;
/// Rayon très large (16px) - Conteneurs principaux
static const double radiusXl = 16.0;
/// Rayon extra large (20px) - Éléments héroïques
static const double radiusXxl = 20.0;
/// Rayon circulaire (999px) - Boutons ronds
static const double radiusCircular = 999.0;
// ═══════════════════════════════════════════════════════════════════════════
// ÉLÉVATIONS - Système d'ombres
// ═══════════════════════════════════════════════════════════════════════════
/// Élévation minimale
static const double elevationXs = 1.0;
/// Élévation petite
static const double elevationSm = 2.0;
/// Élévation moyenne
static const double elevationMd = 4.0;
/// Élévation large
static const double elevationLg = 8.0;
/// Élévation très large
static const double elevationXl = 12.0;
/// Élévation maximale
static const double elevationMax = 24.0;
// ═══════════════════════════════════════════════════════════════════════════
// DIMENSIONS FIXES - Composants standardisés
// ═══════════════════════════════════════════════════════════════════════════
/// Hauteurs de boutons
static const double buttonHeightSmall = 32.0;
static const double buttonHeightMedium = 40.0;
static const double buttonHeightLarge = 48.0;
/// Hauteurs d'éléments de liste
static const double listItemHeightSmall = 48.0;
static const double listItemHeightMedium = 56.0;
static const double listItemHeightLarge = 72.0;
/// Largeurs minimales
static const double minTouchTarget = 44.0; // Cible tactile minimale
static const double minButtonWidth = 64.0; // Largeur minimale bouton
/// Largeurs maximales
static const double maxContentWidth = 600.0; // Largeur max contenu
static const double maxDialogWidth = 400.0; // Largeur max dialogue
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES UTILITAIRES
// ═══════════════════════════════════════════════════════════════════════════
/// Calcule un espacement basé sur l'unité de base
static double spacing(double multiplier) {
return baseUnit * multiplier;
}
/// Obtient un espacement responsive basé sur la largeur d'écran
static double responsiveSpacing(double screenWidth) {
if (screenWidth < 600) {
return xl; // Mobile
} else if (screenWidth < 1200) {
return xxxl; // Tablette
} else {
return huge; // Desktop
}
}
/// Obtient un padding responsive
static double responsivePadding(double screenWidth) {
if (screenWidth < 600) {
return xl; // 16px mobile
} else if (screenWidth < 1200) {
return xxxl; // 24px tablette
} else {
return huge; // 32px desktop
}
}
}

View File

@@ -1,296 +0,0 @@
/// Design Tokens - Typographie
///
/// Système typographique sophistiqué basé sur les tendances 2024-2025
/// Hiérarchie claire et lisibilité optimale pour applications professionnelles
library typography_tokens;
import 'package:flutter/material.dart';
import 'color_tokens.dart';
/// Tokens typographiques - Système de texte moderne
class TypographyTokens {
TypographyTokens._();
// ═══════════════════════════════════════════════════════════════════════════
// FAMILLES DE POLICES
// ═══════════════════════════════════════════════════════════════════════════
/// Police principale - Inter (moderne et lisible)
static const String primaryFontFamily = 'Inter';
/// Police secondaire - SF Pro Display (élégante)
static const String secondaryFontFamily = 'SF Pro Display';
/// Police monospace - JetBrains Mono (code et données)
static const String monospaceFontFamily = 'JetBrains Mono';
// ═══════════════════════════════════════════════════════════════════════════
// ÉCHELLE TYPOGRAPHIQUE - Basée sur Material Design 3
// ═══════════════════════════════════════════════════════════════════════════
/// Display - Titres principaux et héros
static const TextStyle displayLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 57.0,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
height: 1.12,
color: ColorTokens.onSurface,
);
static const TextStyle displayMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 45.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.0,
height: 1.16,
color: ColorTokens.onSurface,
);
static const TextStyle displaySmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 36.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.0,
height: 1.22,
color: ColorTokens.onSurface,
);
/// Headline - Titres de sections
static const TextStyle headlineLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 32.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.25,
color: ColorTokens.onSurface,
);
static const TextStyle headlineMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 28.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.29,
color: ColorTokens.onSurface,
);
static const TextStyle headlineSmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 24.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.33,
color: ColorTokens.onSurface,
);
/// Title - Titres de composants
static const TextStyle titleLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 22.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.27,
color: ColorTokens.onSurface,
);
static const TextStyle titleMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.15,
height: 1.50,
color: ColorTokens.onSurface,
);
static const TextStyle titleSmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.onSurface,
);
/// Label - Étiquettes et boutons
static const TextStyle labelLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.onSurface,
);
static const TextStyle labelMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 12.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 1.33,
color: ColorTokens.onSurface,
);
static const TextStyle labelSmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 11.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 1.45,
color: ColorTokens.onSurface,
);
/// Body - Texte de contenu
static const TextStyle bodyLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 1.50,
color: ColorTokens.onSurface,
);
static const TextStyle bodyMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 1.43,
color: ColorTokens.onSurface,
);
static const TextStyle bodySmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 12.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
color: ColorTokens.onSurface,
);
// ═══════════════════════════════════════════════════════════════════════════
// STYLES SPÉCIALISÉS - Interface UnionFlow
// ═══════════════════════════════════════════════════════════════════════════
/// Navigation - Styles pour menu et navigation
static const TextStyle navigationLabel = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.navigationUnselected,
);
static const TextStyle navigationLabelSelected = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.navigationSelected,
);
/// Cartes et composants
static const TextStyle cardTitle = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 18.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.33,
color: ColorTokens.onSurface,
);
static const TextStyle cardSubtitle = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 1.43,
color: ColorTokens.onSurfaceVariant,
);
static const TextStyle cardValue = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 24.0,
fontWeight: FontWeight.w700,
letterSpacing: 0.0,
height: 1.25,
color: ColorTokens.primary,
);
/// Boutons
static const TextStyle buttonLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.25,
color: ColorTokens.onPrimary,
);
static const TextStyle buttonMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.29,
color: ColorTokens.onPrimary,
);
static const TextStyle buttonSmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 12.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
height: 1.33,
color: ColorTokens.onPrimary,
);
/// Formulaires
static const TextStyle inputLabel = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.onSurfaceVariant,
);
static const TextStyle inputText = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 1.50,
color: ColorTokens.onSurface,
);
static const TextStyle inputHint = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 1.50,
color: ColorTokens.onSurfaceVariant,
);
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES UTILITAIRES
// ═══════════════════════════════════════════════════════════════════════════
/// Applique une couleur à un style
static TextStyle withColor(TextStyle style, Color color) {
return style.copyWith(color: color);
}
/// Applique un poids de police
static TextStyle withWeight(TextStyle style, FontWeight weight) {
return style.copyWith(fontWeight: weight);
}
/// Applique une taille de police
static TextStyle withSize(TextStyle style, double size) {
return style.copyWith(fontSize: size);
}
}

View File

@@ -1,58 +0,0 @@
/// UnionFlow Design System - Point d'entrée unique
///
/// Ce fichier centralise tous les tokens et composants du Design System UnionFlow.
/// Importer ce fichier pour accéder à tous les éléments de design.
///
/// Palette de couleurs: Bleu Roi (#4169E1) + Bleu Pétrole (#2C5F6F)
/// Basé sur Material Design 3 et les tendances UI/UX 2024-2025
///
/// Usage:
/// ```dart
/// import 'package:unionflow_mobile_apps/core/design_system/unionflow_design_system.dart';
///
/// // Utiliser les tokens
/// Container(
/// color: ColorTokens.primary,
/// padding: EdgeInsets.all(SpacingTokens.xl),
/// child: Text(
/// 'UnionFlow',
/// style: TypographyTokens.headlineMedium,
/// ),
/// );
/// ```
library unionflow_design_system;
// ═══════════════════════════════════════════════════════════════════════════
// TOKENS - Valeurs de design fondamentales
// ═══════════════════════════════════════════════════════════════════════════
/// Tokens de couleurs (Bleu Roi + Bleu Pétrole)
export 'tokens/color_tokens.dart';
/// Tokens de typographie (Inter, SF Pro Display, JetBrains Mono)
export 'tokens/typography_tokens.dart';
/// Tokens d'espacement (Grille 4px)
export 'tokens/spacing_tokens.dart';
/// Tokens de rayons de bordure
export 'tokens/radius_tokens.dart';
// ═══════════════════════════════════════════════════════════════════════════
// THÈME - Configuration Material Design 3
// ═══════════════════════════════════════════════════════════════════════════
/// Thème sophistiqué (Light + Dark)
export 'theme/app_theme_sophisticated.dart';
// ═══════════════════════════════════════════════════════════════════════════
// COMPOSANTS - Widgets réutilisables (à ajouter progressivement)
// ═══════════════════════════════════════════════════════════════════════════
// TODO: Ajouter les composants au fur et à mesure de leur création
// export 'components/buttons/uf_buttons.dart';
// export 'components/cards/uf_cards.dart';
// export 'components/inputs/uf_inputs.dart';
// export 'components/navigation/uf_navigation.dart';
// export 'components/feedback/uf_feedback.dart';

View File

@@ -4,10 +4,12 @@ library app_di;
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import '../network/dio_client.dart';
import '../../features/organisations/di/organisations_di.dart';
import '../network/network_info.dart';
import '../../features/organizations/di/organizations_di.dart';
import '../../features/members/di/membres_di.dart';
import '../../features/events/di/evenements_di.dart';
import '../../features/cotisations/di/cotisations_di.dart';
import '../../features/contributions/di/contributions_di.dart';
import '../../features/dashboard/di/dashboard_di.dart';
/// Gestionnaire global des dépendances
class AppDI {
@@ -28,12 +30,17 @@ class AppDI {
final dioClient = DioClient();
_getIt.registerSingleton<DioClient>(dioClient);
_getIt.registerSingleton<Dio>(dioClient.dio);
// Network Info (pour l'instant, on simule toujours connecté)
_getIt.registerLazySingleton<NetworkInfo>(
() => _MockNetworkInfo(),
);
}
/// Configure tous les modules de l'application
static Future<void> _setupModules() async {
// Module Organisations
OrganisationsDI.registerDependencies();
// Module Organizations
OrganizationsDI.registerDependencies();
// Module Membres
MembresDI.register();
@@ -41,9 +48,12 @@ class AppDI {
// Module Événements
EvenementsDI.register();
// Module Cotisations
// Module Contributions
registerCotisationsDependencies(_getIt);
// Module Dashboard
DashboardDI.registerDependencies();
// TODO: Ajouter d'autres modules ici
// SolidariteDI.registerDependencies();
// RapportsDI.registerDependencies();
@@ -52,7 +62,7 @@ class AppDI {
/// Nettoie toutes les dépendances
static Future<void> dispose() async {
// Nettoyer les modules
OrganisationsDI.unregisterDependencies();
OrganizationsDI.unregisterDependencies();
MembresDI.unregister();
EvenementsDI.unregister();
@@ -76,4 +86,15 @@ class AppDI {
/// Obtient le client Dio wrapper
static DioClient get dioClient => _getIt<DioClient>();
/// Nettoie toutes les dépendances
static Future<void> cleanup() async {
await _getIt.reset();
}
}
/// Mock de NetworkInfo pour les tests et développement
class _MockNetworkInfo implements NetworkInfo {
@override
Future<bool> get isConnected async => true;
}

View File

@@ -0,0 +1,15 @@
import 'package:get_it/get_it.dart';
import 'app_di.dart';
/// Service locator global - alias pour faciliter l'utilisation
final GetIt sl = AppDI.instance;
/// Initialise toutes les dépendances de l'application
Future<void> initializeDependencies() async {
await AppDI.initialize();
}
/// Nettoie toutes les dépendances
Future<void> cleanupDependencies() async {
await AppDI.cleanup();
}

View File

@@ -0,0 +1,50 @@
/// Exception de base pour l'application
abstract class AppException implements Exception {
final String message;
final String? code;
const AppException(this.message, [this.code]);
@override
String toString() => 'AppException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception serveur
class ServerException extends AppException {
const ServerException(super.message, [super.code]);
@override
String toString() => 'ServerException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception de cache
class CacheException extends AppException {
const CacheException(super.message, [super.code]);
@override
String toString() => 'CacheException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception de réseau
class NetworkException extends AppException {
const NetworkException(super.message, [super.code]);
@override
String toString() => 'NetworkException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception d'authentification
class AuthException extends AppException {
const AuthException(super.message, [super.code]);
@override
String toString() => 'AuthException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception de validation
class ValidationException extends AppException {
const ValidationException(super.message, [super.code]);
@override
String toString() => 'ValidationException: $message${code != null ? ' (Code: $code)' : ''}';
}

View File

@@ -0,0 +1,71 @@
import 'package:equatable/equatable.dart';
/// Classe de base pour tous les échecs
abstract class Failure extends Equatable {
final String message;
final String? code;
const Failure(this.message, [this.code]);
@override
List<Object?> get props => [message, code];
@override
String toString() => 'Failure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec serveur
class ServerFailure extends Failure {
const ServerFailure(super.message, [super.code]);
@override
String toString() => 'ServerFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de cache
class CacheFailure extends Failure {
const CacheFailure(super.message, [super.code]);
@override
String toString() => 'CacheFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de réseau
class NetworkFailure extends Failure {
const NetworkFailure(super.message, [super.code]);
@override
String toString() => 'NetworkFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec d'authentification
class AuthFailure extends Failure {
const AuthFailure(super.message, [super.code]);
@override
String toString() => 'AuthFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de validation
class ValidationFailure extends Failure {
const ValidationFailure(super.message, [super.code]);
@override
String toString() => 'ValidationFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de permission
class PermissionFailure extends Failure {
const PermissionFailure(super.message, [super.code]);
@override
String toString() => 'PermissionFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de données non trouvées
class NotFoundFailure extends Failure {
const NotFoundFailure(super.message, [super.code]);
@override
String toString() => 'NotFoundFailure: $message${code != null ? ' (Code: $code)' : ''}';
}

View File

@@ -1,328 +0,0 @@
/// 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

@@ -1,269 +0,0 @@
import 'membre_search_criteria.dart';
import '../../features/members/data/models/membre_complete_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<MembreCompletModel> 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) => MembreCompletModel.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

@@ -4,10 +4,10 @@ library adaptive_navigation;
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 '../auth/models/permission_matrix.dart';
import '../widgets/adaptive_widget.dart';
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../features/authentication/data/models/user_role.dart';
import '../../features/authentication/data/models/permission_matrix.dart';
import '../../shared/widgets/adaptive_widget.dart';
/// Élément de navigation adaptatif
class AdaptiveNavigationItem {

View File

@@ -1,8 +1,8 @@
import 'package:go_router/go_router.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../auth/bloc/auth_bloc.dart';
import '../../features/auth/presentation/pages/login_page.dart';
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../features/authentication/presentation/pages/login_page.dart';
import 'main_navigation_layout.dart';
/// Configuration du routeur principal de l'application

View File

@@ -1,20 +1,19 @@
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/authentication/presentation/bloc/auth_bloc.dart';
import '../../features/authentication/data/models/user_role.dart';
import '../../shared/design_system/unionflow_design_system.dart';
import '../../features/dashboard/presentation/pages/role_dashboards/role_dashboards.dart';
import '../../features/members/presentation/pages/members_page_wrapper.dart';
import '../../features/events/presentation/pages/events_page_wrapper.dart';
import '../../features/cotisations/presentation/pages/cotisations_page_wrapper.dart';
import '../../features/contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../features/about/presentation/pages/about_page.dart';
import '../../features/help/presentation/pages/help_support_page.dart';
import '../../features/notifications/presentation/pages/notifications_page.dart';
import '../../features/profile/presentation/pages/profile_page.dart';
import '../../features/system_settings/presentation/pages/system_settings_page.dart';
import '../../features/settings/presentation/pages/system_settings_page.dart';
import '../../features/backup/presentation/pages/backup_page.dart';
import '../../features/logs/presentation/pages/logs_page.dart';
import '../../features/reports/presentation/pages/reports_page.dart';
@@ -69,38 +68,68 @@ class _MainNavigationLayoutState extends State<MainNavigationLayout> {
}
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _getPages(state.effectiveRole),
backgroundColor: ColorTokens.background,
body: SafeArea(
top: true, // Respecte le StatusBar
bottom: false, // Le BottomNavigationBar gère son propre SafeArea
child: 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',
bottomNavigationBar: SafeArea(
top: false,
child: Container(
decoration: const BoxDecoration(
color: ColorTokens.surface,
boxShadow: [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: 8,
offset: Offset(0, -2),
),
],
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
label: 'Membres',
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
backgroundColor: ColorTokens.surface,
selectedItemColor: ColorTokens.primary,
unselectedItemColor: ColorTokens.onSurfaceVariant,
selectedLabelStyle: TypographyTokens.labelSmall.copyWith(
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: TypographyTokens.labelSmall,
elevation: 0, // Géré par le Container
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.dashboard_outlined),
activeIcon: Icon(Icons.dashboard),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.people_outline),
activeIcon: Icon(Icons.people),
label: 'Membres',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_outlined),
activeIcon: Icon(Icons.event),
label: 'Événements',
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz_outlined),
activeIcon: Icon(Icons.more_horiz),
label: 'Plus',
),
],
),
BottomNavigationBarItem(
icon: Icon(Icons.event),
label: 'Événements',
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz),
label: 'Plus',
),
],
),
),
);
},
@@ -124,22 +153,21 @@ class MorePage extends StatelessWidget {
}
return Container(
color: const Color(0xFFF8F9FA),
color: ColorTokens.background,
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de la section
const Text(
Text(
'Plus d\'Options',
style: TextStyle(
style: TypographyTokens.headlineMedium.copyWith(
color: ColorTokens.primary,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
fontSize: 20,
),
),
const SizedBox(height: 12),
const SizedBox(height: SpacingTokens.lg),
// Profil utilisateur
_buildUserProfile(state),

View File

@@ -0,0 +1,19 @@
import 'package:connectivity_plus/connectivity_plus.dart';
/// Interface pour vérifier la connectivité réseau
abstract class NetworkInfo {
Future<bool> get isConnected;
}
/// Implémentation de NetworkInfo utilisant connectivity_plus
class NetworkInfoImpl implements NetworkInfo {
final Connectivity connectivity;
NetworkInfoImpl(this.connectivity);
@override
Future<bool> get isConnected async {
final result = await connectivity.checkConnectivity();
return result.any((r) => r != ConnectivityResult.none);
}
}

View File

@@ -6,7 +6,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../auth/models/user_role.dart';
import '../../features/authentication/data/models/user_role.dart';
/// Gestionnaire de cache intelligent avec stratégie multi-niveaux
///

View File

@@ -0,0 +1,17 @@
import 'package:dartz/dartz.dart';
import '../error/failures.dart';
/// Interface de base pour tous les cas d'usage
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
/// Cas d'usage sans paramètres
abstract class NoParamsUseCase<Type> {
Future<Either<Failure, Type>> call();
}
/// Classe pour représenter l'absence de paramètres
class NoParams {
const NoParams();
}

View File

@@ -1,396 +0,0 @@
/// Widget adaptatif révolutionnaire avec morphing intelligent
/// Transformation dynamique selon le rôle utilisateur avec animations fluides
library adaptive_widget;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../auth/models/user.dart';
import '../auth/models/user_role.dart';
import '../auth/services/permission_engine.dart';
import '../auth/bloc/auth_bloc.dart';
/// Widget adaptatif révolutionnaire qui se transforme selon le rôle utilisateur
///
/// Fonctionnalités :
/// - Morphing intelligent avec animations fluides
/// - Widgets spécifiques par rôle
/// - Vérification de permissions intégrée
/// - Fallback gracieux pour les rôles non supportés
/// - Cache des widgets pour les performances
class AdaptiveWidget extends StatefulWidget {
/// Widgets spécifiques par rôle utilisateur
final Map<UserRole, Widget Function()> roleWidgets;
/// Permissions requises pour afficher le widget
final List<String> requiredPermissions;
/// Widget affiché si les permissions sont insuffisantes
final Widget? fallbackWidget;
/// Widget affiché pendant le chargement
final Widget? loadingWidget;
/// Activer les animations de morphing
final bool enableMorphing;
/// Durée de l'animation de morphing
final Duration morphingDuration;
/// Courbe d'animation
final Curve animationCurve;
/// Contexte organisationnel pour les permissions
final String? organizationId;
/// Activer l'audit trail
final bool auditLog;
/// Constructeur du widget adaptatif
const AdaptiveWidget({
super.key,
required this.roleWidgets,
this.requiredPermissions = const [],
this.fallbackWidget,
this.loadingWidget,
this.enableMorphing = true,
this.morphingDuration = const Duration(milliseconds: 800),
this.animationCurve = Curves.easeInOutCubic,
this.organizationId,
this.auditLog = true,
});
@override
State<AdaptiveWidget> createState() => _AdaptiveWidgetState();
}
class _AdaptiveWidgetState extends State<AdaptiveWidget>
with TickerProviderStateMixin {
/// Cache des widgets construits pour éviter les reconstructions
final Map<UserRole, Widget> _widgetCache = {};
/// Contrôleur d'animation pour le morphing
late AnimationController _morphController;
/// Animation d'opacité
late Animation<double> _opacityAnimation;
/// Animation d'échelle
late Animation<double> _scaleAnimation;
/// Rôle utilisateur précédent pour détecter les changements
UserRole? _previousRole;
@override
void initState() {
super.initState();
_initializeAnimations();
}
@override
void dispose() {
_morphController.dispose();
super.dispose();
}
/// Initialise les animations de morphing
void _initializeAnimations() {
_morphController = AnimationController(
duration: widget.morphingDuration,
vsync: this,
);
_opacityAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _morphController,
curve: widget.animationCurve,
));
_scaleAnimation = Tween<double>(
begin: 0.95,
end: 1.0,
).animate(CurvedAnimation(
parent: _morphController,
curve: widget.animationCurve,
));
// Démarrer l'animation initiale
_morphController.forward();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
// État de chargement
if (state is AuthLoading) {
return widget.loadingWidget ?? _buildLoadingWidget();
}
// État non authentifié
if (state is! AuthAuthenticated) {
return _buildForRole(UserRole.visitor);
}
final user = state.user;
final currentRole = user.primaryRole;
// Détecter le changement de rôle pour déclencher l'animation
if (_previousRole != null && _previousRole != currentRole && widget.enableMorphing) {
_triggerMorphing();
}
_previousRole = currentRole;
return FutureBuilder<bool>(
future: _checkPermissions(user),
builder: (context, permissionSnapshot) {
if (permissionSnapshot.connectionState == ConnectionState.waiting) {
return widget.loadingWidget ?? _buildLoadingWidget();
}
final hasPermissions = permissionSnapshot.data ?? false;
if (!hasPermissions) {
return widget.fallbackWidget ?? _buildUnauthorizedWidget();
}
return _buildForRole(currentRole);
},
);
},
);
}
/// Construit le widget pour un rôle spécifique
Widget _buildForRole(UserRole role) {
// Vérifier le cache
if (_widgetCache.containsKey(role)) {
return _wrapWithAnimation(_widgetCache[role]!);
}
// Trouver le widget approprié
Widget? widget = _findWidgetForRole(role);
widget ??= this.widget.fallbackWidget ?? _buildUnsupportedRoleWidget(role);
// Mettre en cache
_widgetCache[role] = widget;
return _wrapWithAnimation(widget);
}
/// Trouve le widget approprié pour un rôle
Widget? _findWidgetForRole(UserRole role) {
// Vérification directe
if (widget.roleWidgets.containsKey(role)) {
return widget.roleWidgets[role]!();
}
// Recherche du meilleur match par niveau de rôle
UserRole? bestMatch;
for (final availableRole in widget.roleWidgets.keys) {
if (availableRole.level <= role.level) {
if (bestMatch == null || availableRole.level > bestMatch.level) {
bestMatch = availableRole;
}
}
}
return bestMatch != null ? widget.roleWidgets[bestMatch]!() : null;
}
/// Enveloppe le widget avec les animations
Widget _wrapWithAnimation(Widget child) {
if (!widget.enableMorphing) return child;
return AnimatedBuilder(
animation: _morphController,
builder: (context, _) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: child,
),
);
},
);
}
/// Déclenche l'animation de morphing
void _triggerMorphing() {
_morphController.reset();
_morphController.forward();
// Vider le cache pour forcer la reconstruction
_widgetCache.clear();
}
/// Vérifie les permissions requises
Future<bool> _checkPermissions(User user) async {
if (widget.requiredPermissions.isEmpty) return true;
final results = await PermissionEngine.hasPermissions(
user,
widget.requiredPermissions,
organizationId: widget.organizationId,
auditLog: widget.auditLog,
);
return results.values.every((hasPermission) => hasPermission);
}
/// Widget de chargement par défaut
Widget _buildLoadingWidget() {
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
}
/// Widget non autorisé par défaut
Widget _buildUnauthorizedWidget() {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lock_outline,
size: 48,
color: Theme.of(context).disabledColor,
),
const SizedBox(height: 8),
Text(
'Accès non autorisé',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).disabledColor,
),
),
const SizedBox(height: 4),
Text(
'Vous n\'avez pas les permissions nécessaires',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).disabledColor,
),
textAlign: TextAlign.center,
),
],
),
);
}
/// Widget pour rôle non supporté
Widget _buildUnsupportedRoleWidget(UserRole role) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.warning_outlined,
size: 48,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 8),
Text(
'Rôle non supporté',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 4),
Text(
'Le rôle ${role.displayName} n\'est pas supporté par ce widget',
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
],
),
);
}
}
/// Widget sécurisé avec vérification de permissions intégrée
///
/// Version simplifiée d'AdaptiveWidget pour les cas où seules
/// les permissions importent, pas le rôle spécifique
class SecureWidget extends StatelessWidget {
/// Permissions requises pour afficher le widget
final List<String> requiredPermissions;
/// Widget à afficher si autorisé
final Widget child;
/// Widget à afficher si non autorisé
final Widget? unauthorizedWidget;
/// Widget à afficher pendant le chargement
final Widget? loadingWidget;
/// Contexte organisationnel
final String? organizationId;
/// Activer l'audit trail
final bool auditLog;
/// Constructeur du widget sécurisé
const SecureWidget({
super.key,
required this.requiredPermissions,
required this.child,
this.unauthorizedWidget,
this.loadingWidget,
this.organizationId,
this.auditLog = true,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthLoading) {
return loadingWidget ?? const SizedBox.shrink();
}
if (state is! AuthAuthenticated) {
return unauthorizedWidget ?? const SizedBox.shrink();
}
return FutureBuilder<bool>(
future: _checkPermissions(state.user),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return loadingWidget ?? const SizedBox.shrink();
}
final hasPermissions = snapshot.data ?? false;
if (!hasPermissions) {
return unauthorizedWidget ?? const SizedBox.shrink();
}
return child;
},
);
},
);
}
/// Vérifie les permissions requises
Future<bool> _checkPermissions(User user) async {
if (requiredPermissions.isEmpty) return true;
final results = await PermissionEngine.hasPermissions(
user,
requiredPermissions,
organizationId: organizationId,
auditLog: auditLog,
);
return results.values.every((hasPermission) => hasPermission);
}
}

View File

@@ -1,292 +0,0 @@
/// Dialogue de confirmation réutilisable
/// Utilisé pour confirmer les actions critiques (suppression, etc.)
library confirmation_dialog;
import 'package:flutter/material.dart';
/// Type d'action pour personnaliser l'apparence du dialogue
enum ConfirmationAction {
delete,
deactivate,
activate,
cancel,
warning,
info,
}
/// Dialogue de confirmation générique
class ConfirmationDialog extends StatelessWidget {
final String title;
final String message;
final String confirmText;
final String cancelText;
final ConfirmationAction action;
final VoidCallback? onConfirm;
final VoidCallback? onCancel;
const ConfirmationDialog({
super.key,
required this.title,
required this.message,
this.confirmText = 'Confirmer',
this.cancelText = 'Annuler',
this.action = ConfirmationAction.warning,
this.onConfirm,
this.onCancel,
});
/// Constructeur pour suppression
const ConfirmationDialog.delete({
super.key,
required this.title,
required this.message,
this.confirmText = 'Supprimer',
this.cancelText = 'Annuler',
this.onConfirm,
this.onCancel,
}) : action = ConfirmationAction.delete;
/// Constructeur pour désactivation
const ConfirmationDialog.deactivate({
super.key,
required this.title,
required this.message,
this.confirmText = 'Désactiver',
this.cancelText = 'Annuler',
this.onConfirm,
this.onCancel,
}) : action = ConfirmationAction.deactivate;
/// Constructeur pour activation
const ConfirmationDialog.activate({
super.key,
required this.title,
required this.message,
this.confirmText = 'Activer',
this.cancelText = 'Annuler',
this.onConfirm,
this.onCancel,
}) : action = ConfirmationAction.activate;
@override
Widget build(BuildContext context) {
final colors = _getColors();
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: Row(
children: [
Icon(
_getIcon(),
color: colors['icon'],
size: 28,
),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: TextStyle(
color: colors['title'],
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
),
content: Text(
message,
style: const TextStyle(
fontSize: 16,
height: 1.5,
),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
onCancel?.call();
},
child: Text(
cancelText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
onConfirm?.call();
},
style: ElevatedButton.styleFrom(
backgroundColor: colors['button'],
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: Text(
confirmText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
IconData _getIcon() {
switch (action) {
case ConfirmationAction.delete:
return Icons.delete_forever;
case ConfirmationAction.deactivate:
return Icons.block;
case ConfirmationAction.activate:
return Icons.check_circle;
case ConfirmationAction.cancel:
return Icons.cancel;
case ConfirmationAction.warning:
return Icons.warning;
case ConfirmationAction.info:
return Icons.info;
}
}
Map<String, Color> _getColors() {
switch (action) {
case ConfirmationAction.delete:
return {
'icon': Colors.red,
'title': Colors.red[700]!,
'button': Colors.red,
};
case ConfirmationAction.deactivate:
return {
'icon': Colors.orange,
'title': Colors.orange[700]!,
'button': Colors.orange,
};
case ConfirmationAction.activate:
return {
'icon': Colors.green,
'title': Colors.green[700]!,
'button': Colors.green,
};
case ConfirmationAction.cancel:
return {
'icon': Colors.grey,
'title': Colors.grey[700]!,
'button': Colors.grey,
};
case ConfirmationAction.warning:
return {
'icon': Colors.amber,
'title': Colors.amber[700]!,
'button': Colors.amber,
};
case ConfirmationAction.info:
return {
'icon': Colors.blue,
'title': Colors.blue[700]!,
'button': Colors.blue,
};
}
}
}
/// Fonction utilitaire pour afficher un dialogue de confirmation
Future<bool> showConfirmationDialog({
required BuildContext context,
required String title,
required String message,
String confirmText = 'Confirmer',
String cancelText = 'Annuler',
ConfirmationAction action = ConfirmationAction.warning,
}) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => ConfirmationDialog(
title: title,
message: message,
confirmText: confirmText,
cancelText: cancelText,
action: action,
onConfirm: () {},
onCancel: () {},
),
);
return result ?? false;
}
/// Fonction utilitaire pour dialogue de suppression
Future<bool> showDeleteConfirmation({
required BuildContext context,
required String itemName,
String? additionalMessage,
}) async {
final message = additionalMessage != null
? 'Êtes-vous sûr de vouloir supprimer "$itemName" ?\n\n$additionalMessage\n\nCette action est irréversible.'
: 'Êtes-vous sûr de vouloir supprimer "$itemName" ?\n\nCette action est irréversible.';
final result = await showDialog<bool>(
context: context,
builder: (context) => ConfirmationDialog.delete(
title: 'Confirmer la suppression',
message: message,
onConfirm: () {},
onCancel: () {},
),
);
return result ?? false;
}
/// Fonction utilitaire pour dialogue de désactivation
Future<bool> showDeactivateConfirmation({
required BuildContext context,
required String itemName,
String? reason,
}) async {
final message = reason != null
? 'Êtes-vous sûr de vouloir désactiver "$itemName" ?\n\n$reason'
: 'Êtes-vous sûr de vouloir désactiver "$itemName" ?';
final result = await showDialog<bool>(
context: context,
builder: (context) => ConfirmationDialog.deactivate(
title: 'Confirmer la désactivation',
message: message,
onConfirm: () {},
onCancel: () {},
),
);
return result ?? false;
}
/// Fonction utilitaire pour dialogue d'activation
Future<bool> showActivateConfirmation({
required BuildContext context,
required String itemName,
}) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => ConfirmationDialog.activate(
title: 'Confirmer l\'activation',
message: 'Êtes-vous sûr de vouloir activer "$itemName" ?',
onConfirm: () {},
onCancel: () {},
),
);
return result ?? false;
}

View File

@@ -1,168 +0,0 @@
/// Widget d'erreur réutilisable pour toute l'application
library error_widget;
import 'package:flutter/material.dart';
/// Widget d'erreur avec message et bouton de retry
class AppErrorWidget extends StatelessWidget {
/// Message d'erreur à afficher
final String message;
/// Callback appelé lors du clic sur le bouton retry
final VoidCallback? onRetry;
/// Icône personnalisée (optionnel)
final IconData? icon;
/// Titre personnalisé (optionnel)
final String? title;
const AppErrorWidget({
super.key,
required this.message,
this.onRetry,
this.icon,
this.title,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon ?? Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
title ?? 'Oups !',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (onRetry != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
],
),
),
);
}
}
/// Widget d'erreur réseau spécifique
class NetworkErrorWidget extends StatelessWidget {
final VoidCallback? onRetry;
const NetworkErrorWidget({
super.key,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return AppErrorWidget(
message: 'Impossible de se connecter au serveur.\nVérifiez votre connexion internet.',
onRetry: onRetry,
icon: Icons.wifi_off,
title: 'Pas de connexion',
);
}
}
/// Widget d'erreur de permissions
class PermissionErrorWidget extends StatelessWidget {
final String? message;
const PermissionErrorWidget({
super.key,
this.message,
});
@override
Widget build(BuildContext context) {
return AppErrorWidget(
message: message ?? 'Vous n\'avez pas les permissions nécessaires pour accéder à cette ressource.',
icon: Icons.lock_outline,
title: 'Accès refusé',
);
}
}
/// Widget d'erreur "Aucune donnée"
class EmptyDataWidget extends StatelessWidget {
final String message;
final IconData? icon;
final VoidCallback? onAction;
final String? actionLabel;
const EmptyDataWidget({
super.key,
required this.message,
this.icon,
this.onAction,
this.actionLabel,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon ?? Icons.inbox_outlined,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (onAction != null && actionLabel != null) ...[
const SizedBox(height: 24),
ElevatedButton(
onPressed: onAction,
child: Text(actionLabel!),
),
],
],
),
),
);
}
}

View File

@@ -1,244 +0,0 @@
/// Widgets de chargement réutilisables pour toute l'application
library loading_widget;
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
/// Widget de chargement simple avec CircularProgressIndicator
class AppLoadingWidget extends StatelessWidget {
final String? message;
final double? size;
const AppLoadingWidget({
super.key,
this.message,
this.size,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size ?? 40,
height: size ?? 40,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
),
if (message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
],
),
);
}
}
/// Widget de chargement avec effet shimmer pour les listes
class ShimmerListLoading extends StatelessWidget {
final int itemCount;
final double itemHeight;
const ShimmerListLoading({
super.key,
this.itemCount = 5,
this.itemHeight = 80,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: itemCount,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: itemHeight,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
),
),
);
},
);
}
}
/// Widget de chargement avec effet shimmer pour les cartes
class ShimmerCardLoading extends StatelessWidget {
final double height;
final double? width;
const ShimmerCardLoading({
super.key,
this.height = 120,
this.width,
});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
);
}
}
/// Widget de chargement avec effet shimmer pour une grille
class ShimmerGridLoading extends StatelessWidget {
final int itemCount;
final int crossAxisCount;
final double childAspectRatio;
const ShimmerGridLoading({
super.key,
this.itemCount = 6,
this.crossAxisCount = 2,
this.childAspectRatio = 1.0,
});
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: childAspectRatio,
),
itemCount: itemCount,
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
);
},
);
}
}
/// Widget de chargement pour les détails d'un élément
class ShimmerDetailLoading extends StatelessWidget {
const ShimmerDetailLoading({super.key});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
const SizedBox(height: 16),
// Title
Container(
height: 24,
width: double.infinity,
color: Colors.white,
),
const SizedBox(height: 8),
// Subtitle
Container(
height: 16,
width: 200,
color: Colors.white,
),
const SizedBox(height: 24),
// Content lines
...List.generate(5, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
height: 12,
width: double.infinity,
color: Colors.white,
),
);
}),
],
),
),
);
}
}
/// Widget de chargement inline (petit)
class InlineLoadingWidget extends StatelessWidget {
final String? message;
const InlineLoadingWidget({
super.key,
this.message,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
),
if (message != null) ...[
const SizedBox(width: 8),
Text(
message!,
style: Theme.of(context).textTheme.bodySmall,
),
],
],
);
}
}