Clean project: remove test files, debug logs, and add documentation
This commit is contained in:
@@ -4,7 +4,6 @@ library user_models;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'user_role.dart';
|
||||
import 'permission_matrix.dart';
|
||||
|
||||
/// Modèle utilisateur principal avec contexte multi-organisations
|
||||
///
|
||||
|
||||
312
unionflow-mobile-apps/lib/core/constants/app_constants.dart
Normal file
312
unionflow-mobile-apps/lib/core/constants/app_constants.dart
Normal file
@@ -0,0 +1,312 @@
|
||||
/// Constantes globales de l'application
|
||||
library app_constants;
|
||||
|
||||
/// Constantes de l'application UnionFlow
|
||||
class AppConstants {
|
||||
// Empêcher l'instanciation
|
||||
AppConstants._();
|
||||
|
||||
// ============================================================================
|
||||
// API & BACKEND
|
||||
// ============================================================================
|
||||
|
||||
/// URL de base de l'API backend
|
||||
static const String baseUrl = 'http://192.168.1.11:8080';
|
||||
|
||||
/// URL de base de Keycloak
|
||||
static const String keycloakUrl = 'http://192.168.1.11:8180';
|
||||
|
||||
/// Realm Keycloak
|
||||
static const String keycloakRealm = 'unionflow';
|
||||
|
||||
/// Client ID Keycloak
|
||||
static const String keycloakClientId = 'unionflow-mobile';
|
||||
|
||||
/// Redirect URI pour l'authentification
|
||||
static const String redirectUri = 'dev.lions.unionflow-mobile://auth/callback';
|
||||
|
||||
// ============================================================================
|
||||
// PAGINATION
|
||||
// ============================================================================
|
||||
|
||||
/// Taille de page par défaut pour les listes paginées
|
||||
static const int defaultPageSize = 20;
|
||||
|
||||
/// Taille de page maximale
|
||||
static const int maxPageSize = 100;
|
||||
|
||||
/// Taille de page minimale
|
||||
static const int minPageSize = 5;
|
||||
|
||||
/// Page initiale
|
||||
static const int initialPage = 0;
|
||||
|
||||
// ============================================================================
|
||||
// TIMEOUTS
|
||||
// ============================================================================
|
||||
|
||||
/// Timeout de connexion (en secondes)
|
||||
static const Duration connectTimeout = Duration(seconds: 30);
|
||||
|
||||
/// Timeout d'envoi (en secondes)
|
||||
static const Duration sendTimeout = Duration(seconds: 30);
|
||||
|
||||
/// Timeout de réception (en secondes)
|
||||
static const Duration receiveTimeout = Duration(seconds: 30);
|
||||
|
||||
// ============================================================================
|
||||
// CACHE
|
||||
// ============================================================================
|
||||
|
||||
/// Durée d'expiration du cache (en heures)
|
||||
static const Duration cacheExpiration = Duration(hours: 1);
|
||||
|
||||
/// Durée d'expiration du cache pour les données statiques (en jours)
|
||||
static const Duration staticCacheExpiration = Duration(days: 7);
|
||||
|
||||
/// Taille maximale du cache (en MB)
|
||||
static const int maxCacheSize = 100;
|
||||
|
||||
// ============================================================================
|
||||
// UI & DESIGN
|
||||
// ============================================================================
|
||||
|
||||
/// Padding par défaut
|
||||
static const double defaultPadding = 16.0;
|
||||
|
||||
/// Padding petit
|
||||
static const double smallPadding = 8.0;
|
||||
|
||||
/// Padding large
|
||||
static const double largePadding = 24.0;
|
||||
|
||||
/// Padding extra large
|
||||
static const double extraLargePadding = 32.0;
|
||||
|
||||
/// Rayon de bordure par défaut
|
||||
static const double defaultRadius = 8.0;
|
||||
|
||||
/// Rayon de bordure petit
|
||||
static const double smallRadius = 4.0;
|
||||
|
||||
/// Rayon de bordure large
|
||||
static const double largeRadius = 12.0;
|
||||
|
||||
/// Rayon de bordure extra large
|
||||
static const double extraLargeRadius = 16.0;
|
||||
|
||||
/// Hauteur de l'AppBar
|
||||
static const double appBarHeight = 56.0;
|
||||
|
||||
/// Hauteur du BottomNavigationBar
|
||||
static const double bottomNavBarHeight = 60.0;
|
||||
|
||||
/// Largeur maximale pour les écrans larges (tablettes, desktop)
|
||||
static const double maxContentWidth = 1200.0;
|
||||
|
||||
// ============================================================================
|
||||
// ANIMATIONS
|
||||
// ============================================================================
|
||||
|
||||
/// Durée d'animation par défaut
|
||||
static const Duration defaultAnimationDuration = Duration(milliseconds: 300);
|
||||
|
||||
/// Durée d'animation rapide
|
||||
static const Duration fastAnimationDuration = Duration(milliseconds: 150);
|
||||
|
||||
/// Durée d'animation lente
|
||||
static const Duration slowAnimationDuration = Duration(milliseconds: 500);
|
||||
|
||||
// ============================================================================
|
||||
// VALIDATION
|
||||
// ============================================================================
|
||||
|
||||
/// Longueur minimale du mot de passe
|
||||
static const int minPasswordLength = 8;
|
||||
|
||||
/// Longueur maximale du mot de passe
|
||||
static const int maxPasswordLength = 128;
|
||||
|
||||
/// Longueur minimale du nom
|
||||
static const int minNameLength = 2;
|
||||
|
||||
/// Longueur maximale du nom
|
||||
static const int maxNameLength = 100;
|
||||
|
||||
/// Longueur maximale de la description
|
||||
static const int maxDescriptionLength = 1000;
|
||||
|
||||
/// Longueur maximale du titre
|
||||
static const int maxTitleLength = 200;
|
||||
|
||||
// ============================================================================
|
||||
// FORMATS
|
||||
// ============================================================================
|
||||
|
||||
/// Format de date par défaut (dd/MM/yyyy)
|
||||
static const String defaultDateFormat = 'dd/MM/yyyy';
|
||||
|
||||
/// Format de date et heure (dd/MM/yyyy HH:mm)
|
||||
static const String defaultDateTimeFormat = 'dd/MM/yyyy HH:mm';
|
||||
|
||||
/// Format de date court (dd/MM/yy)
|
||||
static const String shortDateFormat = 'dd/MM/yy';
|
||||
|
||||
/// Format de date long (EEEE dd MMMM yyyy)
|
||||
static const String longDateFormat = 'EEEE dd MMMM yyyy';
|
||||
|
||||
/// Format d'heure (HH:mm)
|
||||
static const String timeFormat = 'HH:mm';
|
||||
|
||||
/// Format d'heure avec secondes (HH:mm:ss)
|
||||
static const String timeWithSecondsFormat = 'HH:mm:ss';
|
||||
|
||||
// ============================================================================
|
||||
// DEVISE
|
||||
// ============================================================================
|
||||
|
||||
/// Devise par défaut
|
||||
static const String defaultCurrency = 'EUR';
|
||||
|
||||
/// Symbole de la devise par défaut
|
||||
static const String defaultCurrencySymbol = '€';
|
||||
|
||||
// ============================================================================
|
||||
// IMAGES
|
||||
// ============================================================================
|
||||
|
||||
/// Taille maximale d'upload d'image (en MB)
|
||||
static const int maxImageUploadSize = 5;
|
||||
|
||||
/// Formats d'image acceptés
|
||||
static const List<String> acceptedImageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
/// Qualité de compression d'image (0-100)
|
||||
static const int imageCompressionQuality = 85;
|
||||
|
||||
// ============================================================================
|
||||
// DOCUMENTS
|
||||
// ============================================================================
|
||||
|
||||
/// Taille maximale d'upload de document (en MB)
|
||||
static const int maxDocumentUploadSize = 10;
|
||||
|
||||
/// Formats de document acceptés
|
||||
static const List<String> acceptedDocumentFormats = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'];
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATIONS
|
||||
// ============================================================================
|
||||
|
||||
/// Durée d'affichage des snackbars (en secondes)
|
||||
static const Duration snackbarDuration = Duration(seconds: 3);
|
||||
|
||||
/// Durée d'affichage des snackbars d'erreur (en secondes)
|
||||
static const Duration errorSnackbarDuration = Duration(seconds: 5);
|
||||
|
||||
/// Durée d'affichage des snackbars de succès (en secondes)
|
||||
static const Duration successSnackbarDuration = Duration(seconds: 2);
|
||||
|
||||
// ============================================================================
|
||||
// RECHERCHE
|
||||
// ============================================================================
|
||||
|
||||
/// Délai de debounce pour la recherche (en millisecondes)
|
||||
static const Duration searchDebounce = Duration(milliseconds: 500);
|
||||
|
||||
/// Nombre minimum de caractères pour déclencher une recherche
|
||||
static const int minSearchLength = 2;
|
||||
|
||||
// ============================================================================
|
||||
// REFRESH
|
||||
// ============================================================================
|
||||
|
||||
/// Intervalle de rafraîchissement automatique (en minutes)
|
||||
static const Duration autoRefreshInterval = Duration(minutes: 5);
|
||||
|
||||
// ============================================================================
|
||||
// STORAGE KEYS
|
||||
// ============================================================================
|
||||
|
||||
/// Clé pour le token d'accès
|
||||
static const String accessTokenKey = 'access_token';
|
||||
|
||||
/// Clé pour le refresh token
|
||||
static const String refreshTokenKey = 'refresh_token';
|
||||
|
||||
/// Clé pour l'ID token
|
||||
static const String idTokenKey = 'id_token';
|
||||
|
||||
/// Clé pour les données utilisateur
|
||||
static const String userDataKey = 'user_data';
|
||||
|
||||
/// Clé pour les préférences de thème
|
||||
static const String themePreferenceKey = 'theme_preference';
|
||||
|
||||
/// Clé pour les préférences de langue
|
||||
static const String languagePreferenceKey = 'language_preference';
|
||||
|
||||
/// Clé pour le mode hors ligne
|
||||
static const String offlineModeKey = 'offline_mode';
|
||||
|
||||
// ============================================================================
|
||||
// REGEX PATTERNS
|
||||
// ============================================================================
|
||||
|
||||
/// Pattern pour valider un email
|
||||
static const String emailPattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$';
|
||||
|
||||
/// Pattern pour valider un numéro de téléphone français
|
||||
static const String phonePattern = r'^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$';
|
||||
|
||||
/// Pattern pour valider un code postal français
|
||||
static const String postalCodePattern = r'^\d{5}$';
|
||||
|
||||
/// Pattern pour valider une URL
|
||||
static const String urlPattern = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$';
|
||||
|
||||
// ============================================================================
|
||||
// FEATURES FLAGS
|
||||
// ============================================================================
|
||||
|
||||
/// Activer le mode debug
|
||||
static const bool enableDebugMode = true;
|
||||
|
||||
/// Activer les logs
|
||||
static const bool enableLogging = true;
|
||||
|
||||
/// Activer le mode offline
|
||||
static const bool enableOfflineMode = false;
|
||||
|
||||
/// Activer les analytics
|
||||
static const bool enableAnalytics = false;
|
||||
|
||||
/// Activer le crash reporting
|
||||
static const bool enableCrashReporting = false;
|
||||
|
||||
// ============================================================================
|
||||
// APP INFO
|
||||
// ============================================================================
|
||||
|
||||
/// Nom de l'application
|
||||
static const String appName = 'UnionFlow';
|
||||
|
||||
/// Version de l'application
|
||||
static const String appVersion = '1.0.0';
|
||||
|
||||
/// Build number
|
||||
static const String buildNumber = '1';
|
||||
|
||||
/// Email de support
|
||||
static const String supportEmail = 'support@unionflow.com';
|
||||
|
||||
/// URL du site web
|
||||
static const String websiteUrl = 'https://unionflow.com';
|
||||
|
||||
/// URL des conditions d'utilisation
|
||||
static const String termsOfServiceUrl = 'https://unionflow.com/terms';
|
||||
|
||||
/// URL de la politique de confidentialité
|
||||
static const String privacyPolicyUrl = 'https://unionflow.com/privacy';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
189
unionflow-mobile-apps/lib/core/design_system/design_tokens.dart
Normal file
189
unionflow-mobile-apps/lib/core/design_system/design_tokens.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Design System UnionFlow - Tokens de design centralisés
|
||||
///
|
||||
/// Ce fichier centralise tous les tokens de design pour garantir
|
||||
/// la cohérence visuelle dans toute l'application UnionFlow.
|
||||
class UnionFlowDesignTokens {
|
||||
// ==================== COULEURS ====================
|
||||
|
||||
/// Couleurs primaires
|
||||
static const Color primaryColor = Color(0xFF6C5CE7);
|
||||
static const Color primaryDark = Color(0xFF5A4FCF);
|
||||
static const Color primaryLight = Color(0xFF8B7EE8);
|
||||
|
||||
/// Couleurs secondaires
|
||||
static const Color secondaryColor = Color(0xFF0984E3);
|
||||
static const Color secondaryDark = Color(0xFF0770C2);
|
||||
static const Color secondaryLight = Color(0xFF3498E8);
|
||||
|
||||
/// Couleurs de statut
|
||||
static const Color successColor = Color(0xFF00B894);
|
||||
static const Color warningColor = Color(0xFFE17055);
|
||||
static const Color errorColor = Color(0xFFE74C3C);
|
||||
static const Color infoColor = Color(0xFF00CEC9);
|
||||
|
||||
/// Couleurs neutres
|
||||
static const Color backgroundColor = Color(0xFFF8F9FA);
|
||||
static const Color surfaceColor = Colors.white;
|
||||
static const Color cardColor = Colors.white;
|
||||
|
||||
/// Couleurs de texte
|
||||
static const Color textPrimary = Color(0xFF1F2937);
|
||||
static const Color textSecondary = Color(0xFF6B7280);
|
||||
static const Color textTertiary = Color(0xFF9CA3AF);
|
||||
static const Color textOnPrimary = Colors.white;
|
||||
|
||||
/// Couleurs de bordure
|
||||
static const Color borderLight = Color(0xFFE5E7EB);
|
||||
static const Color borderMedium = Color(0xFFD1D5DB);
|
||||
static const Color borderDark = Color(0xFF9CA3AF);
|
||||
|
||||
// ==================== TYPOGRAPHIE ====================
|
||||
|
||||
/// Tailles de police
|
||||
static const double fontSizeXS = 10.0;
|
||||
static const double fontSizeSM = 12.0;
|
||||
static const double fontSizeBase = 14.0;
|
||||
static const double fontSizeLG = 16.0;
|
||||
static const double fontSizeXL = 18.0;
|
||||
static const double fontSize2XL = 20.0;
|
||||
static const double fontSize3XL = 24.0;
|
||||
static const double fontSize4XL = 28.0;
|
||||
|
||||
/// Poids de police
|
||||
static const FontWeight fontWeightNormal = FontWeight.w400;
|
||||
static const FontWeight fontWeightMedium = FontWeight.w500;
|
||||
static const FontWeight fontWeightSemiBold = FontWeight.w600;
|
||||
static const FontWeight fontWeightBold = FontWeight.w700;
|
||||
|
||||
// ==================== ESPACEMENT ====================
|
||||
|
||||
/// Espacements
|
||||
static const double spaceXS = 4.0;
|
||||
static const double spaceSM = 8.0;
|
||||
static const double spaceBase = 12.0;
|
||||
static const double spaceMD = 16.0;
|
||||
static const double spaceLG = 20.0;
|
||||
static const double spaceXL = 24.0;
|
||||
static const double space2XL = 32.0;
|
||||
static const double space3XL = 48.0;
|
||||
|
||||
// ==================== RAYONS DE BORDURE ====================
|
||||
|
||||
/// Rayons de bordure
|
||||
static const double radiusXS = 4.0;
|
||||
static const double radiusSM = 8.0;
|
||||
static const double radiusBase = 12.0;
|
||||
static const double radiusLG = 16.0;
|
||||
static const double radiusXL = 20.0;
|
||||
static const double radiusFull = 999.0;
|
||||
|
||||
// ==================== OMBRES ====================
|
||||
|
||||
/// Ombres prédéfinies
|
||||
static List<BoxShadow> get shadowSM => [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
];
|
||||
|
||||
static List<BoxShadow> get shadowBase => [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
];
|
||||
|
||||
static List<BoxShadow> get shadowLG => [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
];
|
||||
|
||||
static List<BoxShadow> get shadowXL => [
|
||||
BoxShadow(
|
||||
color: primaryColor.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
];
|
||||
|
||||
// ==================== GRADIENTS ====================
|
||||
|
||||
/// Gradients prédéfinis
|
||||
static const LinearGradient primaryGradient = LinearGradient(
|
||||
colors: [primaryColor, primaryDark],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
static const LinearGradient secondaryGradient = LinearGradient(
|
||||
colors: [secondaryColor, secondaryDark],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
// ==================== STYLES DE TEXTE ====================
|
||||
|
||||
/// Styles de texte prédéfinis
|
||||
static const TextStyle headingXL = TextStyle(
|
||||
fontSize: fontSize3XL,
|
||||
fontWeight: fontWeightBold,
|
||||
color: textPrimary,
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
static const TextStyle headingLG = TextStyle(
|
||||
fontSize: fontSize2XL,
|
||||
fontWeight: fontWeightBold,
|
||||
color: textPrimary,
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
static const TextStyle headingMD = TextStyle(
|
||||
fontSize: fontSizeXL,
|
||||
fontWeight: fontWeightSemiBold,
|
||||
color: textPrimary,
|
||||
height: 1.4,
|
||||
);
|
||||
|
||||
static const TextStyle bodySM = TextStyle(
|
||||
fontSize: fontSizeSM,
|
||||
fontWeight: fontWeightNormal,
|
||||
color: textSecondary,
|
||||
height: 1.5,
|
||||
);
|
||||
|
||||
static const TextStyle bodyBase = TextStyle(
|
||||
fontSize: fontSizeBase,
|
||||
fontWeight: fontWeightNormal,
|
||||
color: textPrimary,
|
||||
height: 1.5,
|
||||
);
|
||||
|
||||
static const TextStyle bodyLG = TextStyle(
|
||||
fontSize: fontSizeLG,
|
||||
fontWeight: fontWeightNormal,
|
||||
color: textPrimary,
|
||||
height: 1.5,
|
||||
);
|
||||
|
||||
static const TextStyle caption = TextStyle(
|
||||
fontSize: fontSizeXS,
|
||||
fontWeight: fontWeightNormal,
|
||||
color: textTertiary,
|
||||
height: 1.4,
|
||||
);
|
||||
|
||||
static const TextStyle buttonText = TextStyle(
|
||||
fontSize: fontSizeBase,
|
||||
fontWeight: fontWeightSemiBold,
|
||||
color: textOnPrimary,
|
||||
);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ class AppThemeSophisticated {
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
|
||||
// Configuration des extensions
|
||||
extensions: [
|
||||
extensions: const [
|
||||
_customColors,
|
||||
_customSpacing,
|
||||
],
|
||||
@@ -117,7 +117,7 @@ class AppThemeSophisticated {
|
||||
// Couleurs de surface
|
||||
surface: ColorTokens.surface,
|
||||
onSurface: ColorTokens.onSurface,
|
||||
surfaceVariant: ColorTokens.surfaceVariant,
|
||||
surfaceContainerHighest: ColorTokens.surfaceVariant,
|
||||
onSurfaceVariant: ColorTokens.onSurfaceVariant,
|
||||
|
||||
// Couleurs de contour
|
||||
@@ -184,7 +184,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des cartes sophistiquées
|
||||
static CardTheme _cardTheme = CardTheme(
|
||||
static final CardTheme _cardTheme = CardTheme(
|
||||
elevation: SpacingTokens.elevationSm,
|
||||
shadowColor: ColorTokens.shadow,
|
||||
surfaceTintColor: ColorTokens.surfaceContainer,
|
||||
@@ -195,7 +195,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des boutons élevés
|
||||
static ElevatedButtonThemeData _elevatedButtonTheme = ElevatedButtonThemeData(
|
||||
static final ElevatedButtonThemeData _elevatedButtonTheme = ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: SpacingTokens.elevationSm,
|
||||
shadowColor: ColorTokens.shadow,
|
||||
@@ -217,7 +217,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des boutons remplis
|
||||
static FilledButtonThemeData _filledButtonTheme = FilledButtonThemeData(
|
||||
static final FilledButtonThemeData _filledButtonTheme = FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: ColorTokens.primary,
|
||||
foregroundColor: ColorTokens.onPrimary,
|
||||
@@ -237,7 +237,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des boutons avec contour
|
||||
static OutlinedButtonThemeData _outlinedButtonTheme = OutlinedButtonThemeData(
|
||||
static final OutlinedButtonThemeData _outlinedButtonTheme = OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: ColorTokens.primary,
|
||||
textStyle: TypographyTokens.buttonMedium,
|
||||
@@ -260,7 +260,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des boutons texte
|
||||
static TextButtonThemeData _textButtonTheme = TextButtonThemeData(
|
||||
static final TextButtonThemeData _textButtonTheme = TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: ColorTokens.primary,
|
||||
textStyle: TypographyTokens.buttonMedium,
|
||||
@@ -279,7 +279,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des champs de saisie
|
||||
static InputDecorationTheme _inputDecorationTheme = InputDecorationTheme(
|
||||
static final InputDecorationTheme _inputDecorationTheme = InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: ColorTokens.surfaceContainer,
|
||||
labelStyle: TypographyTokens.inputLabel,
|
||||
@@ -304,7 +304,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration de la barre de navigation
|
||||
static NavigationBarThemeData _navigationBarTheme = NavigationBarThemeData(
|
||||
static final NavigationBarThemeData _navigationBarTheme = NavigationBarThemeData(
|
||||
backgroundColor: ColorTokens.navigationBackground,
|
||||
indicatorColor: ColorTokens.navigationIndicator,
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
@@ -322,7 +322,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration du drawer de navigation
|
||||
static NavigationDrawerThemeData _navigationDrawerTheme = NavigationDrawerThemeData(
|
||||
static final NavigationDrawerThemeData _navigationDrawerTheme = NavigationDrawerThemeData(
|
||||
backgroundColor: ColorTokens.surfaceContainer,
|
||||
elevation: SpacingTokens.elevationMd,
|
||||
shadowColor: ColorTokens.shadow,
|
||||
@@ -337,7 +337,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des dialogues
|
||||
static DialogTheme _dialogTheme = DialogTheme(
|
||||
static final DialogTheme _dialogTheme = DialogTheme(
|
||||
backgroundColor: ColorTokens.surfaceContainer,
|
||||
elevation: SpacingTokens.elevationLg,
|
||||
shadowColor: ColorTokens.shadow,
|
||||
@@ -350,7 +350,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des snackbars
|
||||
static SnackBarThemeData _snackBarTheme = SnackBarThemeData(
|
||||
static final SnackBarThemeData _snackBarTheme = SnackBarThemeData(
|
||||
backgroundColor: ColorTokens.onSurface,
|
||||
contentTextStyle: TypographyTokens.bodyMedium.copyWith(
|
||||
color: ColorTokens.surface,
|
||||
@@ -362,7 +362,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des puces
|
||||
static ChipThemeData _chipTheme = ChipThemeData(
|
||||
static final ChipThemeData _chipTheme = ChipThemeData(
|
||||
backgroundColor: ColorTokens.surfaceVariant,
|
||||
selectedColor: ColorTokens.primaryContainer,
|
||||
labelStyle: TypographyTokens.labelMedium,
|
||||
@@ -376,8 +376,8 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des éléments de liste
|
||||
static ListTileThemeData _listTileTheme = ListTileThemeData(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
static const ListTileThemeData _listTileTheme = ListTileThemeData(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.xl,
|
||||
vertical: SpacingTokens.md,
|
||||
),
|
||||
@@ -388,7 +388,7 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des onglets
|
||||
static TabBarTheme _tabBarTheme = TabBarTheme(
|
||||
static final TabBarTheme _tabBarTheme = TabBarTheme(
|
||||
labelColor: ColorTokens.primary,
|
||||
unselectedLabelColor: ColorTokens.onSurfaceVariant,
|
||||
labelStyle: TypographyTokens.titleSmall,
|
||||
@@ -403,20 +403,20 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Configuration des dividers
|
||||
static DividerThemeData _dividerTheme = DividerThemeData(
|
||||
static const DividerThemeData _dividerTheme = DividerThemeData(
|
||||
color: ColorTokens.outline,
|
||||
thickness: 1.0,
|
||||
space: SpacingTokens.md,
|
||||
);
|
||||
|
||||
/// Configuration des icônes
|
||||
static IconThemeData _iconTheme = IconThemeData(
|
||||
static const IconThemeData _iconTheme = IconThemeData(
|
||||
color: ColorTokens.onSurfaceVariant,
|
||||
size: 24.0,
|
||||
);
|
||||
|
||||
/// Configuration des transitions de page
|
||||
static PageTransitionsTheme _pageTransitionsTheme = PageTransitionsTheme(
|
||||
static const PageTransitionsTheme _pageTransitionsTheme = PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
@@ -424,10 +424,10 @@ class AppThemeSophisticated {
|
||||
);
|
||||
|
||||
/// Extensions personnalisées - Couleurs
|
||||
static CustomColors _customColors = CustomColors();
|
||||
static const CustomColors _customColors = CustomColors();
|
||||
|
||||
/// Extensions personnalisées - Espacements
|
||||
static CustomSpacing _customSpacing = CustomSpacing();
|
||||
static const CustomSpacing _customSpacing = CustomSpacing();
|
||||
}
|
||||
|
||||
/// Extension de couleurs personnalisées
|
||||
|
||||
79
unionflow-mobile-apps/lib/core/di/app_di.dart
Normal file
79
unionflow-mobile-apps/lib/core/di/app_di.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
/// Configuration globale de l'injection de dépendances
|
||||
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 '../../features/members/di/membres_di.dart';
|
||||
import '../../features/events/di/evenements_di.dart';
|
||||
import '../../features/cotisations/di/cotisations_di.dart';
|
||||
|
||||
/// Gestionnaire global des dépendances
|
||||
class AppDI {
|
||||
static final GetIt _getIt = GetIt.instance;
|
||||
|
||||
/// Initialise toutes les dépendances de l'application
|
||||
static Future<void> initialize() async {
|
||||
// Configuration du client HTTP
|
||||
await _setupNetworking();
|
||||
|
||||
// Configuration des modules
|
||||
await _setupModules();
|
||||
}
|
||||
|
||||
/// Configure les services réseau
|
||||
static Future<void> _setupNetworking() async {
|
||||
// Client Dio
|
||||
final dioClient = DioClient();
|
||||
_getIt.registerSingleton<DioClient>(dioClient);
|
||||
_getIt.registerSingleton<Dio>(dioClient.dio);
|
||||
}
|
||||
|
||||
/// Configure tous les modules de l'application
|
||||
static Future<void> _setupModules() async {
|
||||
// Module Organisations
|
||||
OrganisationsDI.registerDependencies();
|
||||
|
||||
// Module Membres
|
||||
MembresDI.register();
|
||||
|
||||
// Module Événements
|
||||
EvenementsDI.register();
|
||||
|
||||
// Module Cotisations
|
||||
registerCotisationsDependencies(_getIt);
|
||||
|
||||
// TODO: Ajouter d'autres modules ici
|
||||
// SolidariteDI.registerDependencies();
|
||||
// RapportsDI.registerDependencies();
|
||||
}
|
||||
|
||||
/// Nettoie toutes les dépendances
|
||||
static Future<void> dispose() async {
|
||||
// Nettoyer les modules
|
||||
OrganisationsDI.unregisterDependencies();
|
||||
MembresDI.unregister();
|
||||
EvenementsDI.unregister();
|
||||
|
||||
// Nettoyer les services globaux
|
||||
if (_getIt.isRegistered<Dio>()) {
|
||||
_getIt.unregister<Dio>();
|
||||
}
|
||||
if (_getIt.isRegistered<DioClient>()) {
|
||||
_getIt.unregister<DioClient>();
|
||||
}
|
||||
|
||||
// Reset complet
|
||||
await _getIt.reset();
|
||||
}
|
||||
|
||||
/// Obtient l'instance GetIt
|
||||
static GetIt get instance => _getIt;
|
||||
|
||||
/// Obtient le client Dio
|
||||
static Dio get dio => _getIt<Dio>();
|
||||
|
||||
/// Obtient le client Dio wrapper
|
||||
static DioClient get dioClient => _getIt<DioClient>();
|
||||
}
|
||||
192
unionflow-mobile-apps/lib/core/error/error_handler.dart
Normal file
192
unionflow-mobile-apps/lib/core/error/error_handler.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
/// Gestionnaire d'erreurs global pour l'application
|
||||
library error_handler;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
/// Classe utilitaire pour gérer les erreurs de manière centralisée
|
||||
class ErrorHandler {
|
||||
/// Convertit une erreur en message utilisateur lisible
|
||||
static String getErrorMessage(dynamic error) {
|
||||
if (error is DioException) {
|
||||
return _handleDioError(error);
|
||||
} else if (error is String) {
|
||||
return error;
|
||||
} else if (error is Exception) {
|
||||
return error.toString().replaceAll('Exception: ', '');
|
||||
}
|
||||
return 'Une erreur inattendue s\'est produite.';
|
||||
}
|
||||
|
||||
/// Gère les erreurs Dio spécifiques
|
||||
static String _handleDioError(DioException error) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
return 'Délai de connexion dépassé.\nVérifiez votre connexion internet.';
|
||||
|
||||
case DioExceptionType.sendTimeout:
|
||||
return 'Délai d\'envoi dépassé.\nVérifiez votre connexion internet.';
|
||||
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return 'Délai de réception dépassé.\nLe serveur met trop de temps à répondre.';
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
return _handleBadResponse(error.response);
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
return 'Requête annulée.';
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
return 'Erreur de connexion.\nVérifiez votre connexion internet.';
|
||||
|
||||
case DioExceptionType.badCertificate:
|
||||
return 'Erreur de certificat SSL.\nLa connexion n\'est pas sécurisée.';
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
if (error.message?.contains('SocketException') ?? false) {
|
||||
return 'Impossible de se connecter au serveur.\nVérifiez votre connexion internet.';
|
||||
}
|
||||
return 'Erreur de connexion.\nVeuillez réessayer.';
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les réponses HTTP avec erreur
|
||||
static String _handleBadResponse(Response? response) {
|
||||
if (response == null) {
|
||||
return 'Erreur serveur inconnue.';
|
||||
}
|
||||
|
||||
// Essayer d'extraire le message d'erreur du body
|
||||
String? errorMessage;
|
||||
if (response.data is Map) {
|
||||
errorMessage = response.data['message'] ??
|
||||
response.data['error'] ??
|
||||
response.data['details'];
|
||||
}
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 400:
|
||||
return errorMessage ?? 'Requête invalide.\nVérifiez les données saisies.';
|
||||
|
||||
case 401:
|
||||
return errorMessage ?? 'Non authentifié.\nVeuillez vous reconnecter.';
|
||||
|
||||
case 403:
|
||||
return errorMessage ?? 'Accès refusé.\nVous n\'avez pas les permissions nécessaires.';
|
||||
|
||||
case 404:
|
||||
return errorMessage ?? 'Ressource non trouvée.';
|
||||
|
||||
case 409:
|
||||
return errorMessage ?? 'Conflit.\nCette ressource existe déjà.';
|
||||
|
||||
case 422:
|
||||
return errorMessage ?? 'Données invalides.\nVérifiez les informations saisies.';
|
||||
|
||||
case 429:
|
||||
return 'Trop de requêtes.\nVeuillez patienter quelques instants.';
|
||||
|
||||
case 500:
|
||||
return errorMessage ?? 'Erreur serveur interne.\nVeuillez réessayer plus tard.';
|
||||
|
||||
case 502:
|
||||
return 'Passerelle incorrecte.\nLe serveur est temporairement indisponible.';
|
||||
|
||||
case 503:
|
||||
return 'Service temporairement indisponible.\nVeuillez réessayer plus tard.';
|
||||
|
||||
case 504:
|
||||
return 'Délai d\'attente de la passerelle dépassé.\nLe serveur met trop de temps à répondre.';
|
||||
|
||||
default:
|
||||
return errorMessage ?? 'Erreur serveur (${response.statusCode}).\nVeuillez réessayer.';
|
||||
}
|
||||
}
|
||||
|
||||
/// Détermine si l'erreur est une erreur réseau
|
||||
static bool isNetworkError(dynamic error) {
|
||||
if (error is DioException) {
|
||||
return error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.sendTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout ||
|
||||
error.type == DioExceptionType.connectionError ||
|
||||
(error.message?.contains('SocketException') ?? false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Détermine si l'erreur est une erreur d'authentification
|
||||
static bool isAuthError(dynamic error) {
|
||||
if (error is DioException && error.response != null) {
|
||||
return error.response!.statusCode == 401;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Détermine si l'erreur est une erreur de permissions
|
||||
static bool isPermissionError(dynamic error) {
|
||||
if (error is DioException && error.response != null) {
|
||||
return error.response!.statusCode == 403;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Détermine si l'erreur est une erreur de validation
|
||||
static bool isValidationError(dynamic error) {
|
||||
if (error is DioException && error.response != null) {
|
||||
return error.response!.statusCode == 400 ||
|
||||
error.response!.statusCode == 422;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Détermine si l'erreur est une erreur serveur
|
||||
static bool isServerError(dynamic error) {
|
||||
if (error is DioException && error.response != null) {
|
||||
final statusCode = error.response!.statusCode ?? 0;
|
||||
return statusCode >= 500 && statusCode < 600;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Extrait les détails de validation d'une erreur
|
||||
static Map<String, dynamic>? getValidationErrors(dynamic error) {
|
||||
if (error is DioException &&
|
||||
error.response != null &&
|
||||
error.response!.data is Map) {
|
||||
final data = error.response!.data as Map;
|
||||
if (data.containsKey('errors')) {
|
||||
return data['errors'] as Map<String, dynamic>?;
|
||||
}
|
||||
if (data.containsKey('validationErrors')) {
|
||||
return data['validationErrors'] as Map<String, dynamic>?;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour faciliter l'utilisation de ErrorHandler
|
||||
extension ErrorHandlerExtension on Object {
|
||||
/// Convertit l'objet en message d'erreur lisible
|
||||
String toErrorMessage() => ErrorHandler.getErrorMessage(this);
|
||||
|
||||
/// Vérifie si c'est une erreur réseau
|
||||
bool get isNetworkError => ErrorHandler.isNetworkError(this);
|
||||
|
||||
/// Vérifie si c'est une erreur d'authentification
|
||||
bool get isAuthError => ErrorHandler.isAuthError(this);
|
||||
|
||||
/// Vérifie si c'est une erreur de permissions
|
||||
bool get isPermissionError => ErrorHandler.isPermissionError(this);
|
||||
|
||||
/// Vérifie si c'est une erreur de validation
|
||||
bool get isValidationError => ErrorHandler.isValidationError(this);
|
||||
|
||||
/// Vérifie si c'est une erreur serveur
|
||||
bool get isServerError => ErrorHandler.isServerError(this);
|
||||
|
||||
/// Récupère les erreurs de validation
|
||||
Map<String, dynamic>? get validationErrors => ErrorHandler.getValidationErrors(this);
|
||||
}
|
||||
|
||||
102
unionflow-mobile-apps/lib/core/l10n/locale_provider.dart
Normal file
102
unionflow-mobile-apps/lib/core/l10n/locale_provider.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
/// Provider pour gérer la locale de l'application
|
||||
library locale_provider;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../utils/logger.dart';
|
||||
|
||||
/// Provider pour la gestion de la locale
|
||||
class LocaleProvider extends ChangeNotifier {
|
||||
static const String _localeKey = 'app_locale';
|
||||
|
||||
Locale _locale = const Locale('fr');
|
||||
|
||||
/// Locale actuelle
|
||||
Locale get locale => _locale;
|
||||
|
||||
/// Locales supportées
|
||||
static const List<Locale> supportedLocales = [
|
||||
Locale('fr'),
|
||||
Locale('en'),
|
||||
];
|
||||
|
||||
/// Initialiser la locale depuis les préférences
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final localeCode = prefs.getString(_localeKey);
|
||||
|
||||
if (localeCode != null) {
|
||||
_locale = Locale(localeCode);
|
||||
AppLogger.info('Locale chargée: $localeCode');
|
||||
} else {
|
||||
AppLogger.info('Locale par défaut: fr');
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors du chargement de la locale',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Changer la locale
|
||||
Future<void> setLocale(Locale locale) async {
|
||||
if (!supportedLocales.contains(locale)) {
|
||||
AppLogger.warning('Locale non supportée: ${locale.languageCode}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_locale == locale) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_locale = locale;
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_localeKey, locale.languageCode);
|
||||
|
||||
AppLogger.info('Locale changée: ${locale.languageCode}');
|
||||
AppLogger.userAction('Change language', data: {'locale': locale.languageCode});
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors du changement de locale',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Basculer entre FR et EN
|
||||
Future<void> toggleLocale() async {
|
||||
final newLocale = _locale.languageCode == 'fr'
|
||||
? const Locale('en')
|
||||
: const Locale('fr');
|
||||
await setLocale(newLocale);
|
||||
}
|
||||
|
||||
/// Obtenir le nom de la langue actuelle
|
||||
String get currentLanguageName {
|
||||
switch (_locale.languageCode) {
|
||||
case 'fr':
|
||||
return 'Français';
|
||||
case 'en':
|
||||
return 'English';
|
||||
default:
|
||||
return 'Français';
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifier si la locale est française
|
||||
bool get isFrench => _locale.languageCode == 'fr';
|
||||
|
||||
/// Vérifier si la locale est anglaise
|
||||
bool get isEnglish => _locale.languageCode == 'en';
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'membre_search_criteria.dart';
|
||||
import '../../features/members/data/models/membre_model.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<MembreModel> membres;
|
||||
final List<MembreCompletModel> membres;
|
||||
|
||||
/// Nombre total de résultats (toutes pages confondues)
|
||||
final int totalElements;
|
||||
@@ -63,7 +63,7 @@ class MembreSearchResult {
|
||||
factory MembreSearchResult.fromJson(Map<String, dynamic> json) {
|
||||
return MembreSearchResult(
|
||||
membres: (json['membres'] as List<dynamic>?)
|
||||
?.map((e) => MembreModel.fromJson(e as Map<String, dynamic>))
|
||||
?.map((e) => MembreCompletModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
totalElements: json['totalElements'] as int? ?? 0,
|
||||
totalPages: json['totalPages'] as int? ?? 0,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../auth/bloc/auth_bloc.dart';
|
||||
|
||||
@@ -6,8 +6,18 @@ import '../auth/models/user_role.dart';
|
||||
|
||||
import '../design_system/tokens/tokens.dart';
|
||||
import '../../features/dashboard/presentation/pages/role_dashboards/role_dashboards.dart';
|
||||
import '../../features/members/presentation/pages/members_page.dart';
|
||||
import '../../features/events/presentation/pages/events_page.dart';
|
||||
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/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/backup/presentation/pages/backup_page.dart';
|
||||
import '../../features/logs/presentation/pages/logs_page.dart';
|
||||
import '../../features/reports/presentation/pages/reports_page.dart';
|
||||
|
||||
/// Layout principal avec navigation hybride
|
||||
/// Bottom Navigation pour les sections principales + Drawer pour fonctions avancées
|
||||
@@ -42,8 +52,8 @@ class _MainNavigationLayoutState extends State<MainNavigationLayout> {
|
||||
List<Widget> _getPages(UserRole role) {
|
||||
return [
|
||||
_getDashboardForRole(role),
|
||||
const MembersPage(),
|
||||
const EventsPage(),
|
||||
const MembersPageWrapper(), // Wrapper BLoC pour connexion API
|
||||
const EventsPageWrapper(), // Wrapper BLoC pour connexion API
|
||||
const MorePage(), // Page "Plus" qui affiche les options avancées
|
||||
];
|
||||
}
|
||||
@@ -136,7 +146,7 @@ class MorePage extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Options selon le rôle
|
||||
..._buildRoleBasedOptions(state),
|
||||
..._buildRoleBasedOptions(context, state),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -222,10 +232,10 @@ class MorePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildRoleBasedOptions(AuthAuthenticated state) {
|
||||
List<Widget> _buildRoleBasedOptions(BuildContext context, AuthAuthenticated state) {
|
||||
final options = <Widget>[];
|
||||
|
||||
// Options Super Admin
|
||||
|
||||
// Options Super Admin uniquement
|
||||
if (state.effectiveRole == UserRole.superAdmin) {
|
||||
options.addAll([
|
||||
_buildSectionTitle('Administration Système'),
|
||||
@@ -233,84 +243,125 @@ class MorePage extends StatelessWidget {
|
||||
icon: Icons.settings,
|
||||
title: 'Paramètres Système',
|
||||
subtitle: 'Configuration globale',
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SystemSettingsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildOptionTile(
|
||||
icon: Icons.backup,
|
||||
title: 'Sauvegarde',
|
||||
title: 'Sauvegarde & Restauration',
|
||||
subtitle: 'Gestion des sauvegardes',
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const BackupPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildOptionTile(
|
||||
icon: Icons.analytics,
|
||||
title: 'Logs Système',
|
||||
subtitle: 'Surveillance et logs',
|
||||
onTap: () {},
|
||||
icon: Icons.article,
|
||||
title: 'Logs & Monitoring',
|
||||
subtitle: 'Surveillance et journaux',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const LogsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
// Options Admin Organisation
|
||||
|
||||
// Options Admin+ (Admin Organisation et Super Admin)
|
||||
if (state.effectiveRole == UserRole.orgAdmin || state.effectiveRole == UserRole.superAdmin) {
|
||||
options.addAll([
|
||||
_buildSectionTitle('Administration'),
|
||||
_buildOptionTile(
|
||||
icon: Icons.business,
|
||||
title: 'Gestion Organisation',
|
||||
subtitle: 'Paramètres organisation',
|
||||
onTap: () {},
|
||||
),
|
||||
_buildSectionTitle('Rapports & Analytics'),
|
||||
_buildOptionTile(
|
||||
icon: Icons.assessment,
|
||||
title: 'Rapports',
|
||||
subtitle: 'Rapports et statistiques',
|
||||
onTap: () {},
|
||||
title: 'Rapports & Analytics',
|
||||
subtitle: 'Statistiques détaillées',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ReportsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
// Options RH
|
||||
if (state.effectiveRole == UserRole.moderator || state.effectiveRole == UserRole.superAdmin) {
|
||||
options.addAll([
|
||||
_buildSectionTitle('Ressources Humaines'),
|
||||
_buildOptionTile(
|
||||
icon: Icons.people_alt,
|
||||
title: 'Gestion RH',
|
||||
subtitle: 'Outils RH avancés',
|
||||
onTap: () {},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
List<Widget> _buildCommonOptions(BuildContext context) {
|
||||
return [
|
||||
_buildSectionTitle('Général'),
|
||||
_buildOptionTile(
|
||||
icon: Icons.payment,
|
||||
title: 'Cotisations',
|
||||
subtitle: 'Gérer les cotisations',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CotisationsPageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildOptionTile(
|
||||
icon: Icons.person,
|
||||
title: 'Mon Profil',
|
||||
subtitle: 'Modifier mes informations',
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ProfilePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildOptionTile(
|
||||
icon: Icons.notifications,
|
||||
title: 'Notifications',
|
||||
subtitle: 'Gérer les notifications',
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NotificationsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildOptionTile(
|
||||
icon: Icons.help,
|
||||
title: 'Aide & Support',
|
||||
subtitle: 'Documentation et support',
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const HelpSupportPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildOptionTile(
|
||||
icon: Icons.info,
|
||||
title: 'À propos',
|
||||
subtitle: 'Version et informations',
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AboutPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildOptionTile(
|
||||
@@ -319,7 +370,7 @@ class MorePage extends StatelessWidget {
|
||||
subtitle: 'Se déconnecter de l\'application',
|
||||
color: Colors.red,
|
||||
onTap: () {
|
||||
context.read<AuthBloc>().add(AuthLogoutRequested());
|
||||
context.read<AuthBloc>().add(const AuthLogoutRequested());
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
212
unionflow-mobile-apps/lib/core/network/dio_client.dart
Normal file
212
unionflow-mobile-apps/lib/core/network/dio_client.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
/// Client HTTP Dio configuré pour l'API UnionFlow
|
||||
library dio_client;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
/// Configuration du client HTTP Dio
|
||||
class DioClient {
|
||||
static const String _baseUrl = 'http://192.168.1.11:8080'; // URL du backend UnionFlow
|
||||
static const int _connectTimeout = 30000; // 30 secondes
|
||||
static const int _receiveTimeout = 30000; // 30 secondes
|
||||
static const int _sendTimeout = 30000; // 30 secondes
|
||||
|
||||
late final Dio _dio;
|
||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||
|
||||
DioClient() {
|
||||
_dio = Dio();
|
||||
_configureDio();
|
||||
}
|
||||
|
||||
/// Configuration du client Dio
|
||||
void _configureDio() {
|
||||
// Configuration de base
|
||||
_dio.options = BaseOptions(
|
||||
baseUrl: _baseUrl,
|
||||
connectTimeout: const Duration(milliseconds: _connectTimeout),
|
||||
receiveTimeout: const Duration(milliseconds: _receiveTimeout),
|
||||
sendTimeout: const Duration(milliseconds: _sendTimeout),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
// Intercepteur d'authentification
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// Ajouter le token d'authentification si disponible
|
||||
final token = await _secureStorage.read(key: 'keycloak_webview_access_token');
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (error, handler) async {
|
||||
// Gestion des erreurs d'authentification
|
||||
if (error.response?.statusCode == 401) {
|
||||
// Token expiré, essayer de le rafraîchir
|
||||
final refreshed = await _refreshToken();
|
||||
if (refreshed) {
|
||||
// Réessayer la requête avec le nouveau token
|
||||
final token = await _secureStorage.read(key: 'keycloak_webview_access_token');
|
||||
if (token != null) {
|
||||
error.requestOptions.headers['Authorization'] = 'Bearer $token';
|
||||
final response = await _dio.fetch(error.requestOptions);
|
||||
handler.resolve(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
|
||||
// Logger pour le développement (désactivé en production)
|
||||
// _dio.interceptors.add(
|
||||
// LogInterceptor(
|
||||
// requestHeader: true,
|
||||
// requestBody: true,
|
||||
// responseBody: true,
|
||||
// responseHeader: false,
|
||||
// error: true,
|
||||
// logPrint: (obj) => print('DIO: $obj'),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
|
||||
/// Rafraîchit le token d'authentification
|
||||
Future<bool> _refreshToken() async {
|
||||
try {
|
||||
final refreshToken = await _secureStorage.read(key: 'keycloak_webview_refresh_token');
|
||||
if (refreshToken == null) return false;
|
||||
|
||||
final response = await Dio().post(
|
||||
'http://192.168.1.11:8180/realms/unionflow/protocol/openid-connect/token',
|
||||
data: {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refreshToken,
|
||||
'client_id': 'unionflow-mobile',
|
||||
},
|
||||
options: Options(
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
await _secureStorage.write(key: 'keycloak_webview_access_token', value: data['access_token']);
|
||||
if (data['refresh_token'] != null) {
|
||||
await _secureStorage.write(key: 'keycloak_webview_refresh_token', value: data['refresh_token']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Erreur lors du rafraîchissement, l'utilisateur devra se reconnecter
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Obtient l'instance Dio configurée
|
||||
Dio get dio => _dio;
|
||||
|
||||
/// Méthodes de convenance pour les requêtes HTTP
|
||||
|
||||
/// GET request
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return _dio.get<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// POST request
|
||||
Future<Response<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return _dio.post<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// PUT request
|
||||
Future<Response<T>> put<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return _dio.put<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// DELETE request
|
||||
Future<Response<T>> delete<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _dio.delete<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// PATCH request
|
||||
Future<Response<T>> patch<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return _dio.patch<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
}
|
||||
301
unionflow-mobile-apps/lib/core/utils/logger.dart
Normal file
301
unionflow-mobile-apps/lib/core/utils/logger.dart
Normal file
@@ -0,0 +1,301 @@
|
||||
/// Logger centralisé pour l'application
|
||||
library logger;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
|
||||
/// Niveaux de log
|
||||
enum LogLevel {
|
||||
debug,
|
||||
info,
|
||||
warning,
|
||||
error,
|
||||
fatal,
|
||||
}
|
||||
|
||||
/// Logger centralisé pour toute l'application
|
||||
class AppLogger {
|
||||
// Empêcher l'instanciation
|
||||
AppLogger._();
|
||||
|
||||
/// Couleurs ANSI pour les logs en console
|
||||
static const String _reset = '\x1B[0m';
|
||||
static const String _red = '\x1B[31m';
|
||||
static const String _green = '\x1B[32m';
|
||||
static const String _yellow = '\x1B[33m';
|
||||
static const String _blue = '\x1B[34m';
|
||||
static const String _magenta = '\x1B[35m';
|
||||
static const String _cyan = '\x1B[36m';
|
||||
static const String _white = '\x1B[37m';
|
||||
|
||||
/// Log de niveau DEBUG (bleu)
|
||||
static void debug(String message, {String? tag}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
_log(LogLevel.debug, message, tag: tag, color: _blue);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de niveau INFO (vert)
|
||||
static void info(String message, {String? tag}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
_log(LogLevel.info, message, tag: tag, color: _green);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de niveau WARNING (jaune)
|
||||
static void warning(String message, {String? tag}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
_log(LogLevel.warning, message, tag: tag, color: _yellow);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de niveau ERROR (rouge)
|
||||
static void error(
|
||||
String message, {
|
||||
String? tag,
|
||||
dynamic error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
if (AppConstants.enableLogging) {
|
||||
_log(LogLevel.error, message, tag: tag, color: _red);
|
||||
|
||||
if (error != null) {
|
||||
_log(LogLevel.error, 'Error: $error', tag: tag, color: _red);
|
||||
}
|
||||
|
||||
if (stackTrace != null) {
|
||||
_log(LogLevel.error, 'StackTrace:\n$stackTrace', tag: tag, color: _red);
|
||||
}
|
||||
|
||||
// TODO: Envoyer à un service de monitoring (Sentry, Firebase Crashlytics)
|
||||
if (AppConstants.enableCrashReporting) {
|
||||
_sendToMonitoring(message, error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de niveau FATAL (magenta)
|
||||
static void fatal(
|
||||
String message, {
|
||||
String? tag,
|
||||
dynamic error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
if (AppConstants.enableLogging) {
|
||||
_log(LogLevel.fatal, message, tag: tag, color: _magenta);
|
||||
|
||||
if (error != null) {
|
||||
_log(LogLevel.fatal, 'Error: $error', tag: tag, color: _magenta);
|
||||
}
|
||||
|
||||
if (stackTrace != null) {
|
||||
_log(LogLevel.fatal, 'StackTrace:\n$stackTrace', tag: tag, color: _magenta);
|
||||
}
|
||||
|
||||
// TODO: Envoyer à un service de monitoring (Sentry, Firebase Crashlytics)
|
||||
if (AppConstants.enableCrashReporting) {
|
||||
_sendToMonitoring(message, error, stackTrace, isFatal: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'une requête HTTP
|
||||
static void httpRequest({
|
||||
required String method,
|
||||
required String url,
|
||||
Map<String, dynamic>? headers,
|
||||
dynamic body,
|
||||
}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('┌─────────────────────────────────────────────────');
|
||||
buffer.writeln('│ HTTP REQUEST');
|
||||
buffer.writeln('├─────────────────────────────────────────────────');
|
||||
buffer.writeln('│ Method: $method');
|
||||
buffer.writeln('│ URL: $url');
|
||||
|
||||
if (headers != null && headers.isNotEmpty) {
|
||||
buffer.writeln('│ Headers:');
|
||||
headers.forEach((key, value) {
|
||||
buffer.writeln('│ $key: $value');
|
||||
});
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
buffer.writeln('│ Body: $body');
|
||||
}
|
||||
|
||||
buffer.writeln('└─────────────────────────────────────────────────');
|
||||
|
||||
_log(LogLevel.debug, buffer.toString(), color: _cyan);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'une réponse HTTP
|
||||
static void httpResponse({
|
||||
required int statusCode,
|
||||
required String url,
|
||||
Map<String, dynamic>? headers,
|
||||
dynamic body,
|
||||
Duration? duration,
|
||||
}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('┌─────────────────────────────────────────────────');
|
||||
buffer.writeln('│ HTTP RESPONSE');
|
||||
buffer.writeln('├─────────────────────────────────────────────────');
|
||||
buffer.writeln('│ Status: $statusCode');
|
||||
buffer.writeln('│ URL: $url');
|
||||
|
||||
if (duration != null) {
|
||||
buffer.writeln('│ Duration: ${duration.inMilliseconds}ms');
|
||||
}
|
||||
|
||||
if (headers != null && headers.isNotEmpty) {
|
||||
buffer.writeln('│ Headers:');
|
||||
headers.forEach((key, value) {
|
||||
buffer.writeln('│ $key: $value');
|
||||
});
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
buffer.writeln('│ Body: $body');
|
||||
}
|
||||
|
||||
buffer.writeln('└─────────────────────────────────────────────────');
|
||||
|
||||
final color = statusCode >= 200 && statusCode < 300 ? _green : _red;
|
||||
_log(LogLevel.debug, buffer.toString(), color: color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'un événement BLoC
|
||||
static void blocEvent(String blocName, String eventName, {dynamic data}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
final message = data != null
|
||||
? '[$blocName] Event: $eventName | Data: $data'
|
||||
: '[$blocName] Event: $eventName';
|
||||
_log(LogLevel.debug, message, color: _cyan);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'un changement d'état BLoC
|
||||
static void blocState(String blocName, String stateName, {dynamic data}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
final message = data != null
|
||||
? '[$blocName] State: $stateName | Data: $data'
|
||||
: '[$blocName] State: $stateName';
|
||||
_log(LogLevel.debug, message, color: _magenta);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'une navigation
|
||||
static void navigation(String from, String to) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
_log(LogLevel.debug, 'Navigation: $from → $to', color: _yellow);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'une action utilisateur
|
||||
static void userAction(String action, {Map<String, dynamic>? data}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
final message = data != null
|
||||
? 'User Action: $action | Data: $data'
|
||||
: 'User Action: $action';
|
||||
_log(LogLevel.info, message, color: _green);
|
||||
}
|
||||
|
||||
// TODO: Envoyer à un service d'analytics
|
||||
if (AppConstants.enableAnalytics) {
|
||||
_sendToAnalytics(action, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthode privée pour logger avec formatage
|
||||
static void _log(
|
||||
LogLevel level,
|
||||
String message, {
|
||||
String? tag,
|
||||
String color = _white,
|
||||
}) {
|
||||
final timestamp = DateTime.now().toIso8601String();
|
||||
final levelStr = level.name.toUpperCase().padRight(7);
|
||||
final tagStr = tag != null ? '[$tag] ' : '';
|
||||
|
||||
if (kDebugMode) {
|
||||
// En mode debug, utiliser les couleurs
|
||||
debugPrint('$color$timestamp | $levelStr | $tagStr$message$_reset');
|
||||
} else {
|
||||
// En mode release, pas de couleurs
|
||||
debugPrint('$timestamp | $levelStr | $tagStr$message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Envoyer les erreurs à un service de monitoring
|
||||
static void _sendToMonitoring(
|
||||
String message,
|
||||
dynamic error,
|
||||
StackTrace? stackTrace, {
|
||||
bool isFatal = false,
|
||||
}) {
|
||||
// TODO: Implémenter l'envoi à Sentry, Firebase Crashlytics, etc.
|
||||
// Exemple avec Sentry:
|
||||
// Sentry.captureException(
|
||||
// error,
|
||||
// stackTrace: stackTrace,
|
||||
// hint: Hint.withMap({'message': message}),
|
||||
// );
|
||||
}
|
||||
|
||||
/// Envoyer les événements à un service d'analytics
|
||||
static void _sendToAnalytics(String action, Map<String, dynamic>? data) {
|
||||
// TODO: Implémenter l'envoi à Firebase Analytics, Mixpanel, etc.
|
||||
// Exemple avec Firebase Analytics:
|
||||
// FirebaseAnalytics.instance.logEvent(
|
||||
// name: action,
|
||||
// parameters: data,
|
||||
// );
|
||||
}
|
||||
|
||||
/// Divider pour séparer visuellement les logs
|
||||
static void divider({String? title}) {
|
||||
if (AppConstants.enableLogging && kDebugMode) {
|
||||
if (title != null) {
|
||||
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
|
||||
debugPrint('$_cyan $title$_reset');
|
||||
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
|
||||
} else {
|
||||
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour faciliter le logging depuis n'importe où
|
||||
extension LoggerExtension on Object {
|
||||
/// Log debug
|
||||
void logDebug(String message) {
|
||||
AppLogger.debug(message, tag: runtimeType.toString());
|
||||
}
|
||||
|
||||
/// Log info
|
||||
void logInfo(String message) {
|
||||
AppLogger.info(message, tag: runtimeType.toString());
|
||||
}
|
||||
|
||||
/// Log warning
|
||||
void logWarning(String message) {
|
||||
AppLogger.warning(message, tag: runtimeType.toString());
|
||||
}
|
||||
|
||||
/// Log error
|
||||
void logError(String message, {dynamic error, StackTrace? stackTrace}) {
|
||||
AppLogger.error(
|
||||
message,
|
||||
tag: runtimeType.toString(),
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +172,7 @@ class _AdaptiveWidgetState extends State<AdaptiveWidget>
|
||||
// Trouver le widget approprié
|
||||
Widget? widget = _findWidgetForRole(role);
|
||||
|
||||
if (widget == null) {
|
||||
widget = this.widget.fallbackWidget ?? _buildUnsupportedRoleWidget(role);
|
||||
}
|
||||
widget ??= this.widget.fallbackWidget ?? _buildUnsupportedRoleWidget(role);
|
||||
|
||||
// Mettre en cache
|
||||
_widgetCache[role] = widget;
|
||||
|
||||
292
unionflow-mobile-apps/lib/core/widgets/confirmation_dialog.dart
Normal file
292
unionflow-mobile-apps/lib/core/widgets/confirmation_dialog.dart
Normal file
@@ -0,0 +1,292 @@
|
||||
/// 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;
|
||||
}
|
||||
|
||||
168
unionflow-mobile-apps/lib/core/widgets/error_widget.dart
Normal file
168
unionflow-mobile-apps/lib/core/widgets/error_widget.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
/// 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!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
244
unionflow-mobile-apps/lib/core/widgets/loading_widget.dart
Normal file
244
unionflow-mobile-apps/lib/core/widgets/loading_widget.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
/// 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,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user