From 55f84da49a07a500f960eb4fcf49f1119c19f1b3 Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:13:50 +0000 Subject: [PATCH] feat(ui): RefreshIndicator + AlwaysScrollable + dark mode sur 14 pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RefreshIndicator ajouté (dispatche les events BLoC appropriés) : - adhesion_detail, adhesions_page, demande_aide_detail, demandes_aide_page - event_detail, organization_detail, org_selector, org_types - user_management_detail, reports (TabBarView), logs (Dashboard tab) - profile (onglet Perso), backup (3 onglets), notifications Fixes associés : - AlwaysScrollableScrollPhysics sur tous les scroll widgets (permet pull-to-refresh même si contenu < écran) - Empty states des listes : wrappés dans SingleChildScrollView pour refresh - Dark mode adaptatif sur textes/surfaces/borders hardcodés - backup_page : bouton retour ajouté dans le header gradient - org_types : chevron/star/border adaptatifs - reports : couleurs placeholders graphique + chevrons --- .../pages/adhesion_detail_page.dart | 38 +- .../presentation/pages/adhesions_page.dart | 37 +- .../pages/user_management_detail_page.dart | 213 ++- .../presentation/pages/backup_page.dart | 183 ++- .../presentation/pages/event_detail_page.dart | 61 +- .../logs/presentation/pages/logs_page.dart | 1152 ++++++++--------- .../pages/notifications_page.dart | 230 ++-- .../presentation/pages/org_selector_page.dart | 96 +- .../presentation/pages/org_types_page.dart | 95 +- .../pages/organization_detail_page.dart | 271 ++-- .../presentation/pages/profile_page.dart | 337 ++--- .../presentation/pages/reports_page.dart | 323 ++--- .../pages/demande_aide_detail_page.dart | 34 +- .../pages/demandes_aide_page.dart | 33 +- 14 files changed, 1565 insertions(+), 1538 deletions(-) diff --git a/lib/features/adhesions/presentation/pages/adhesion_detail_page.dart b/lib/features/adhesions/presentation/pages/adhesion_detail_page.dart index 21201ce..78f5f6d 100644 --- a/lib/features/adhesions/presentation/pages/adhesion_detail_page.dart +++ b/lib/features/adhesions/presentation/pages/adhesion_detail_page.dart @@ -31,13 +31,14 @@ class _AdhesionDetailPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.background, - appBar: const UFAppBar( - title: 'DÉTAIL ADHÉSION', - backgroundColor: AppColors.surface, - foregroundColor: AppColors.textPrimaryLight, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: UFAppBar( + title: 'Détail Adhésion', + moduleGradient: ModuleColors.adhesionsGradient, ), - body: BlocConsumer( + body: SafeArea( + top: false, + child: BlocConsumer( listenWhen: (prev, curr) => prev.status != curr.status, listener: (context, state) { if (state.status == AdhesionsStatus.error && state.message != null) { @@ -58,7 +59,7 @@ class _AdhesionDetailPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.error_outline, size: 64, color: AppColors.textSecondaryLight), + Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( 'Adhésion introuvable', @@ -68,7 +69,12 @@ class _AdhesionDetailPageState extends State { ), ); } - return SingleChildScrollView( + return RefreshIndicator( + color: ModuleColors.adhesions, + onRefresh: () async => + context.read().add(LoadAdhesionById(widget.adhesionId)), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -118,10 +124,12 @@ class _AdhesionDetailPageState extends State { _ActionsSection(adhesion: a, currencyFormat: _currencyFormat, isGestionnaire: _isGestionnaire()), ], ), - ); + ), // SingleChildScrollView + ); // RefreshIndicator }, - ), - ); + ), // BlocConsumer + ), // SafeArea + ); // Scaffold } bool _isGestionnaire() { @@ -157,7 +165,7 @@ class _InfoCard extends StatelessWidget { style: AppTypography.subtitleSmall.copyWith( fontWeight: FontWeight.bold, fontSize: 9, - color: AppColors.textSecondaryLight, + color: AppColors.textSecondary, ), ), const SizedBox(height: 2), @@ -187,13 +195,13 @@ Widget _buildStatutBadge(String? statut) { color = AppColors.error; break; case 'EN_ATTENTE': - color = AppColors.brandGreenLight; + color = AppColors.primaryLight; break; case 'EN_PAIEMENT': color = AppColors.warning; break; default: - color = AppColors.textSecondaryLight; + color = AppColors.textSecondary; } return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color); } @@ -307,7 +315,7 @@ class _ActionsSection extends StatelessWidget { ); }, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, + backgroundColor: AppColors.primary, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), diff --git a/lib/features/adhesions/presentation/pages/adhesions_page.dart b/lib/features/adhesions/presentation/pages/adhesions_page.dart index 8f1839e..7e7b9c6 100644 --- a/lib/features/adhesions/presentation/pages/adhesions_page.dart +++ b/lib/features/adhesions/presentation/pages/adhesions_page.dart @@ -89,11 +89,10 @@ class _AdhesionsPageState extends State } }, child: Scaffold( - backgroundColor: AppColors.background, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: UFAppBar( - title: 'ADHÉSIONS', - backgroundColor: AppColors.surface, - foregroundColor: AppColors.textPrimaryLight, + title: 'Adhésions', + moduleGradient: ModuleColors.adhesionsGradient, actions: [ IconButton( icon: const Icon(Icons.add, size: 20), @@ -105,9 +104,9 @@ class _AdhesionsPageState extends State controller: _tabController, onTap: _loadTab, isScrollable: true, - labelColor: AppColors.primaryGreen, - unselectedLabelColor: AppColors.textSecondaryLight, - indicatorColor: AppColors.primaryGreen, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, indicatorSize: TabBarIndicatorSize.label, labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), tabs: const [ @@ -148,11 +147,17 @@ class _AdhesionsPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.assignment_outlined, size: 40, color: AppColors.textSecondaryLight), + Icon( + Icons.assignment_outlined, + size: 40, + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.textSecondaryDark + : AppColors.textSecondary, + ), const SizedBox(height: 8), Text( 'Aucune demande d\'adhésion', - style: AppTypography.bodyTextSmall.copyWith(fontSize: 16, color: AppColors.textSecondaryLight), + style: AppTypography.bodyTextSmall.copyWith(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), TextButton.icon( @@ -249,7 +254,7 @@ class _AdhesionCard extends StatelessWidget { ], ), ), - _buildStatutBadge(adhesion.statut), + _buildStatutBadge(context, adhesion.statut), ], ), const SizedBox(height: 12), @@ -262,7 +267,7 @@ class _AdhesionCard extends StatelessWidget { Text('FRAIS D\'ADHÉSION', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)), Text( adhesion.fraisAdhesion != null ? currencyFormat.format(adhesion.fraisAdhesion) : '—', - style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primaryGreen), + style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primary), ), ], ), @@ -283,7 +288,7 @@ class _AdhesionCard extends StatelessWidget { const SizedBox(height: 8), Text( 'MEMBRE : ${adhesion.nomMembreComplet.toUpperCase()}', - style: AppTypography.subtitleSmall.copyWith(fontSize: 8, color: AppColors.textSecondaryLight), + style: AppTypography.subtitleSmall.copyWith(fontSize: 8, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ], @@ -291,7 +296,7 @@ class _AdhesionCard extends StatelessWidget { ); } - Widget _buildStatutBadge(String? statut) { + Widget _buildStatutBadge(BuildContext context, String? statut) { Color color; switch (statut) { case 'APPROUVEE': @@ -303,13 +308,15 @@ class _AdhesionCard extends StatelessWidget { color = AppColors.error; break; case 'EN_ATTENTE': - color = AppColors.brandGreenLight; + color = AppColors.primaryLight; break; case 'EN_PAIEMENT': color = AppColors.warning; break; default: - color = AppColors.textSecondaryLight; + color = Theme.of(context).brightness == Brightness.dark + ? AppColors.textSecondaryDark + : AppColors.textSecondary; } return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color); } diff --git a/lib/features/admin/presentation/pages/user_management_detail_page.dart b/lib/features/admin/presentation/pages/user_management_detail_page.dart index 1fea58e..1f84a72 100644 --- a/lib/features/admin/presentation/pages/user_management_detail_page.dart +++ b/lib/features/admin/presentation/pages/user_management_detail_page.dart @@ -21,40 +21,52 @@ class UserManagementDetailPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.background, - appBar: const UFAppBar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: UFAppBar( title: 'Détail utilisateur', + moduleGradient: ModuleColors.systemeGradient, ), - body: BlocBuilder( - builder: (context, state) { - if (state is AdminUsersLoading) { - return const Center(child: CircularProgressIndicator()); - } - if (state is AdminUsersError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(state.message), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => context.read().add(AdminUserDetailRequested(userId)), - child: const Text('Réessayer'), - ), - ], - ), - ); - } - if (state is AdminUserDetailLoaded) { - return _UserDetailContent( - user: state.user, - userRoles: state.userRoles, - allRoles: state.allRoles, - userId: userId, - ); - } - return const SizedBox(); - }, + body: SafeArea( + top: false, + child: BlocBuilder( + builder: (context, state) { + if (state is AdminUsersLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is AdminUsersError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: AppColors.error), + const SizedBox(height: 12), + Text(state.message, textAlign: TextAlign.center), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () => context.read() + .add(AdminUserDetailWithRolesRequested(userId)), + icon: const Icon(Icons.refresh, size: 16), + label: const Text('Réessayer'), + ), + ], + ), + ); + } + if (state is AdminUserDetailLoaded) { + return _UserDetailContent( + user: state.user, + userRoles: state.userRoles, + allRoles: state.allRoles, + userId: userId, + ); + } + // AdminUserRolesUpdated : rechargement en cours — garder un indicateur léger + if (state is AdminUserRolesUpdated) { + return const Center(child: CircularProgressIndicator()); + } + return const SizedBox(); + }, + ), ), ); } @@ -96,7 +108,13 @@ class _UserDetailContentState extends State<_UserDetailContent> { ); } }, - child: SingleChildScrollView( + child: RefreshIndicator( + color: ModuleColors.systeme, + onRefresh: () async => context + .read() + .add(AdminUserDetailWithRolesRequested(widget.userId)), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -122,42 +140,96 @@ class _UserDetailContentState extends State<_UserDetailContent> { ), ), const SizedBox(height: 8), - Text( - 'RÔLES (SÉLECTION)', - style: AppTypography.subtitleSmall.copyWith( - fontWeight: FontWeight.bold, - letterSpacing: 1.1, + CoreCard( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.manage_accounts_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, size: 16), + const SizedBox(width: 8), + Text( + 'RÔLES', + style: AppTypography.subtitleSmall.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: 1.1, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const Spacer(), + Text( + '${_selectedRoleNames.length} sélectionné(s)', + style: AppTypography.subtitleSmall.copyWith( + fontSize: 10, + color: AppColors.primary, + ), + ), + ], + ), + const SizedBox(height: 8), + if (widget.allRoles.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + const Icon(Icons.warning_amber_outlined, + size: 16, color: AppColors.error), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Impossible de charger les rôles disponibles.', + style: AppTypography.bodyTextSmall.copyWith( + color: AppColors.error), + ), + ), + TextButton.icon( + onPressed: () => context.read() + .add(AdminUserDetailWithRolesRequested(widget.userId)), + icon: const Icon(Icons.refresh, size: 14), + label: const Text('Recharger'), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ], + ), + ) + else + ...widget.allRoles.map((role) { + final selected = _selectedRoleNames.contains(role.name); + return CheckboxListTile( + title: Text(role.name, style: AppTypography.bodyTextSmall), + subtitle: role.description != null && role.description!.isNotEmpty + ? Text(role.description!, + style: AppTypography.subtitleSmall.copyWith(fontSize: 10)) + : null, + activeColor: AppColors.primary, + contentPadding: const EdgeInsets.symmetric(horizontal: 4), + dense: true, + value: selected, + onChanged: (v) => setState(() { + if (v == true) { + _selectedRoleNames.add(role.name); + } else { + _selectedRoleNames.remove(role.name); + } + }), + ); + }), + ], ), ), const SizedBox(height: 8), - ...widget.allRoles.map((role) { - final selected = _selectedRoleNames.contains(role.name); - return CheckboxListTile( - title: Text(role.name, style: AppTypography.bodyTextSmall), - activeColor: AppColors.primaryGreen, - contentPadding: EdgeInsets.zero, - dense: true, - value: selected, - onChanged: (v) { - setState(() { - if (v == true) { - _selectedRoleNames.add(role.name); - } else { - _selectedRoleNames.remove(role.name); - } - }); - }, - ); - }), - const SizedBox(height: 12), - UFPrimaryButton( - label: 'Enregistrer les rôles', - onPressed: () { - context.read().add( - AdminUserRolesUpdateRequested(widget.userId, _selectedRoleNames.toList()), - ); - }, - ), + if (widget.allRoles.isNotEmpty) + UFPrimaryButton( + label: 'Enregistrer les rôles', + onPressed: () => context.read().add( + AdminUserRolesUpdateRequested( + widget.userId, _selectedRoleNames.toList())), + ), const SizedBox(height: 12), const Divider(height: 1), const SizedBox(height: 8), @@ -171,7 +243,7 @@ class _UserDetailContentState extends State<_UserDetailContent> { const SizedBox(height: 8), Text( 'Permet à cet utilisateur (ex. admin d\'organisation) de voir « Mes organisations » et d\'accéder au dashboard de l\'organisation.', - style: AppTypography.bodyTextSmall.copyWith(color: AppColors.textSecondaryLight), + style: AppTypography.bodyTextSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 12), OutlinedButton.icon( @@ -181,13 +253,14 @@ class _UserDetailContentState extends State<_UserDetailContent> { icon: const Icon(Icons.business, size: 18), label: const Text('Associer à une organisation'), style: OutlinedButton.styleFrom( - foregroundColor: AppColors.primaryGreen, - side: const BorderSide(color: AppColors.primaryGreen), + foregroundColor: AppColors.primary, + side: const BorderSide(color: AppColors.primary), ), ), ], ), - ), + ), // SingleChildScrollView + ), // RefreshIndicator ); } diff --git a/lib/features/backup/presentation/pages/backup_page.dart b/lib/features/backup/presentation/pages/backup_page.dart index 89e45d2..c1b41d4 100644 --- a/lib/features/backup/presentation/pages/backup_page.dart +++ b/lib/features/backup/presentation/pages/backup_page.dart @@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:share_plus/share_plus.dart'; import '../../../../shared/design_system/tokens/app_colors.dart'; import '../../../../shared/design_system/tokens/color_tokens.dart'; +import '../../../../shared/design_system/tokens/module_colors.dart'; import '../../../../shared/design_system/tokens/spacing_tokens.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../core/utils/logger.dart'; @@ -82,8 +83,10 @@ class _BackupPageState extends State }, builder: (context, state) { return Scaffold( - backgroundColor: AppColors.lightBackground, - body: Column( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: SafeArea( + top: false, + child: Column( children: [ _buildHeader(), _buildTabBar(), @@ -99,27 +102,28 @@ class _BackupPageState extends State ), ], ), + ), ); }, ), ); } - /// Header harmonisé + /// Header harmonisé avec bouton retour Widget _buildHeader() { return Container( margin: const EdgeInsets.symmetric(horizontal: SpacingTokens.sm, vertical: SpacingTokens.xs), padding: const EdgeInsets.all(SpacingTokens.md), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: ColorTokens.primaryGradient, + gradient: LinearGradient( + colors: ModuleColors.backupGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), boxShadow: [ BoxShadow( - color: ColorTokens.primary.withOpacity(0.3), + color: ModuleColors.backup.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 8), ), @@ -129,6 +133,19 @@ class _BackupPageState extends State children: [ Row( children: [ + // Bouton retour + GestureDetector( + onTap: () => Navigator.of(context).maybePop(), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.arrow_back_rounded, color: Colors.white, size: 18), + ), + ), + const SizedBox(width: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( @@ -137,7 +154,7 @@ class _BackupPageState extends State ), child: const Icon( Icons.backup, - color: Colors.white, + color: AppColors.onGradient, size: 20, ), ), @@ -151,14 +168,14 @@ class _BackupPageState extends State style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.onGradient, ), ), Text( 'Gestion des sauvegardes système', style: TextStyle( fontSize: 14, - color: Colors.white.withOpacity(0.8), + color: AppColors.onGradient.withOpacity(0.8), ), ), ], @@ -173,7 +190,7 @@ class _BackupPageState extends State onPressed: () => _createBackupNow(), icon: const Icon( Icons.save, - color: Colors.white, + color: AppColors.onGradient, ), tooltip: 'Sauvegarde immédiate', ), @@ -275,21 +292,21 @@ class _BackupPageState extends State ), child: Column( children: [ - Icon(icon, color: Colors.white, size: 20), + Icon(icon, color: AppColors.onGradient, size: 20), const SizedBox(height: 4), Text( value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.onGradient, ), ), Text( label, style: TextStyle( fontSize: 10, - color: Colors.white.withOpacity(0.8), + color: AppColors.onGradient.withOpacity(0.8), ), textAlign: TextAlign.center, ), @@ -303,11 +320,11 @@ class _BackupPageState extends State return Container( margin: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: AppColors.shadow, blurRadius: 10, offset: const Offset(0, 2), ), @@ -315,9 +332,9 @@ class _BackupPageState extends State ), child: TabBar( controller: _tabController, - labelColor: AppColors.primaryGreen, - unselectedLabelColor: Colors.grey[600], - indicatorColor: AppColors.primaryGreen, + labelColor: AppColors.primary, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + indicatorColor: AppColors.primary, indicatorWeight: 3, labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12), tabs: const [ @@ -331,16 +348,25 @@ class _BackupPageState extends State /// Onglet sauvegardes Widget _buildBackupsTab(BackupState state) { - return SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - const SizedBox(height: 8), - state is BackupLoading - ? const Center(child: CircularProgressIndicator()) - : _buildBackupsList(state is BackupsLoaded ? state.backups : (_cachedBackups ?? [])), - const SizedBox(height: 80), - ], + return RefreshIndicator( + color: ModuleColors.backup, + onRefresh: () async { + context.read() + ..add(LoadBackups()) + ..add(LoadBackupConfig()); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(12), + child: Column( + children: [ + const SizedBox(height: 8), + state is BackupLoading + ? const Center(child: CircularProgressIndicator()) + : _buildBackupsList(state is BackupsLoaded ? state.backups : (_cachedBackups ?? [])), + const SizedBox(height: 80), + ], + ), ), ); } @@ -358,11 +384,11 @@ class _BackupPageState extends State return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: AppColors.shadow, blurRadius: 10, offset: const Offset(0, 2), ), @@ -373,14 +399,14 @@ class _BackupPageState extends State children: [ Row( children: [ - const Icon(Icons.folder, color: AppColors.primaryGreen, size: 20), + const Icon(Icons.folder, color: AppColors.primary, size: 20), const SizedBox(width: 8), Text( 'Sauvegardes disponibles', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: Theme.of(context).colorScheme.onSurface, ), ), ], @@ -398,14 +424,14 @@ class _BackupPageState extends State margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon( backup['type'] == 'Auto' ? Icons.schedule : Icons.touch_app, - color: backup['type'] == 'Auto' ? AppColors.primaryGreen : AppColors.success, + color: backup['type'] == 'Auto' ? AppColors.primary : AppColors.success, size: 20, ), const SizedBox(width: 12), @@ -415,17 +441,19 @@ class _BackupPageState extends State children: [ Text( backup['name']!, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.textPrimaryLight, + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.textPrimaryDark + : AppColors.textPrimary, ), ), Text( '${backup['date']} • ${backup['size']}', style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -438,7 +466,12 @@ class _BackupPageState extends State const PopupMenuItem(value: 'download', child: Text('Télécharger')), const PopupMenuItem(value: 'delete', child: Text('Supprimer')), ], - child: const Icon(Icons.more_vert, color: Colors.grey), + child: Icon( + Icons.more_vert, + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.textSecondaryDark + : AppColors.textTertiary, + ), ), ], ), @@ -447,14 +480,20 @@ class _BackupPageState extends State /// Onglet planification Widget _buildScheduleTab() { - return SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - const SizedBox(height: 16), - _buildScheduleSettings(), - const SizedBox(height: 80), - ], + return RefreshIndicator( + color: ModuleColors.backup, + onRefresh: () async => + context.read().add(LoadBackupConfig()), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(12), + child: Column( + children: [ + const SizedBox(height: 16), + _buildScheduleSettings(), + const SizedBox(height: 80), + ], + ), ), ); } @@ -464,11 +503,11 @@ class _BackupPageState extends State return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: AppColors.shadow, blurRadius: 10, offset: const Offset(0, 2), ), @@ -479,14 +518,14 @@ class _BackupPageState extends State children: [ Row( children: [ - const Icon(Icons.schedule, color: AppColors.primaryGreen, size: 20), + const Icon(Icons.schedule, color: AppColors.primary, size: 20), const SizedBox(width: 8), Text( 'Configuration automatique', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: Theme.of(context).colorScheme.onSurface, ), ), ], @@ -519,14 +558,20 @@ class _BackupPageState extends State /// Onglet restauration Widget _buildRestoreTab() { - return SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - const SizedBox(height: 16), - _buildRestoreOptions(), - const SizedBox(height: 80), - ], + return RefreshIndicator( + color: ModuleColors.backup, + onRefresh: () async => + context.read().add(LoadBackups()), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(12), + child: Column( + children: [ + const SizedBox(height: 16), + _buildRestoreOptions(), + const SizedBox(height: 80), + ], + ), ), ); } @@ -536,11 +581,11 @@ class _BackupPageState extends State return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: AppColors.shadow, blurRadius: 10, offset: const Offset(0, 2), ), @@ -551,14 +596,14 @@ class _BackupPageState extends State children: [ Row( children: [ - const Icon(Icons.restore, color: AppColors.primaryGreen, size: 20), + const Icon(Icons.restore, color: AppColors.primary, size: 20), const SizedBox(width: 8), Text( 'Options de restauration', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: Theme.of(context).colorScheme.onSurface, ), ), ], @@ -568,7 +613,7 @@ class _BackupPageState extends State 'Restaurer depuis un fichier', 'Importer une sauvegarde externe', Icons.file_upload, - AppColors.primaryGreen, + AppColors.primary, () => _restoreFromFile(), ), const SizedBox(height: 12), @@ -601,11 +646,11 @@ class _BackupPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), - Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[600])), + Text(subtitle, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ), ), - Switch(value: value, onChanged: onChanged, activeColor: AppColors.primaryGreen), + Switch(value: value, onChanged: onChanged, activeColor: AppColors.primary), ], ); } @@ -619,9 +664,9 @@ class _BackupPageState extends State Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: Colors.grey[50], + color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey[300]!), + border: Border.all(color: Theme.of(context).colorScheme.outline), ), child: DropdownButtonHideUnderline( child: DropdownButton( @@ -656,11 +701,11 @@ class _BackupPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: color)), - Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[600])), + Text(subtitle, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ), ), - Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16), + Icon(Icons.arrow_forward_ios, color: Theme.of(context).colorScheme.onSurfaceVariant, size: 16), ], ), ), diff --git a/lib/features/events/presentation/pages/event_detail_page.dart b/lib/features/events/presentation/pages/event_detail_page.dart index 714703a..986cf6a 100644 --- a/lib/features/events/presentation/pages/event_detail_page.dart +++ b/lib/features/events/presentation/pages/event_detail_page.dart @@ -2,7 +2,10 @@ library event_detail_page; import 'package:flutter/material.dart'; +import '../../../../shared/design_system/tokens/module_colors.dart'; +import '../../../../shared/design_system/tokens/color_tokens.dart'; import '../../../../shared/design_system/tokens/app_colors.dart'; +import '../../../../shared/design_system/components/uf_app_bar.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/di/injection.dart'; import '../../bloc/evenements_bloc.dart'; @@ -51,10 +54,9 @@ class _EventDetailPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Détails de l\'événement'), - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, + appBar: UFAppBar( + title: "Détails de l'événement", + moduleGradient: ModuleColors.evenementsGradient, actions: [ IconButton( icon: const Icon(Icons.edit), @@ -62,10 +64,16 @@ class _EventDetailPageState extends State { ), ], ), - body: BlocBuilder( - builder: (context, state) { - return SingleChildScrollView( - child: Column( + body: SafeArea( + top: false, + child: BlocBuilder( + builder: (context, state) { + return RefreshIndicator( + color: ModuleColors.evenements, + onRefresh: _loadInscriptionStatus, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), @@ -76,8 +84,10 @@ class _EventDetailPageState extends State { const SizedBox(height: 80), // Espace pour le bouton flottant ], ), - ); + ), // SingleChildScrollView + ); // RefreshIndicator }, + ), ), floatingActionButton: _buildInscriptionButton(context), ); @@ -87,12 +97,9 @@ class _EventDetailPageState extends State { return Container( width: double.infinity, padding: const EdgeInsets.all(16), - decoration: BoxDecoration( + decoration: const BoxDecoration( gradient: LinearGradient( - colors: [ - AppColors.primaryGreen, - AppColors.primaryGreen.withOpacity(0.8), - ], + colors: ModuleColors.evenementsGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -191,7 +198,7 @@ class _EventDetailPageState extends State { padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ - Icon(icon, color: AppColors.primaryGreen, size: 20), + Icon(icon, color: ModuleColors.evenements, size: 20), const SizedBox(width: 12), Expanded( child: Column( @@ -201,7 +208,7 @@ class _EventDetailPageState extends State { label, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), @@ -261,7 +268,7 @@ class _EventDetailPageState extends State { const SizedBox(height: 8), Row( children: [ - const Icon(Icons.location_on, color: AppColors.primaryGreen), + const Icon(Icons.location_on, color: ModuleColors.evenements), const SizedBox(width: 8), Expanded( child: Text( @@ -296,7 +303,7 @@ class _EventDetailPageState extends State { '${widget.evenement.participantsActuels} inscrits', style: TextStyle( fontSize: 14, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -305,7 +312,7 @@ class _EventDetailPageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[100], + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: const Row( @@ -335,14 +342,14 @@ class _EventDetailPageState extends State { if (!isComplet) { return FloatingActionButton.extended( onPressed: () => _showInscriptionDialog(context, isInscrit), - backgroundColor: AppColors.primaryGreen, + backgroundColor: ModuleColors.evenements, icon: const Icon(Icons.check), label: const Text('S\'inscrire'), ); } else { return const FloatingActionButton.extended( onPressed: null, - backgroundColor: Colors.grey, + backgroundColor: AppColors.textTertiary, icon: Icon(Icons.block), label: Text('Complet'), ); @@ -425,17 +432,17 @@ class _EventDetailPageState extends State { Color _getStatutColor(StatutEvenement statut) { switch (statut) { case StatutEvenement.planifie: - return AppColors.info; + return ColorTokens.info; case StatutEvenement.confirme: - return AppColors.success; + return ColorTokens.success; case StatutEvenement.enCours: - return AppColors.warning; + return ColorTokens.warningLight; case StatutEvenement.termine: - return AppColors.textSecondaryLight; + return ColorTokens.textSecondary; case StatutEvenement.annule: - return AppColors.error; + return ColorTokens.error; case StatutEvenement.reporte: - return AppColors.brandGreen; + return ColorTokens.warning; } } } diff --git a/lib/features/logs/presentation/pages/logs_page.dart b/lib/features/logs/presentation/pages/logs_page.dart index 6b831e3..97efb76 100644 --- a/lib/features/logs/presentation/pages/logs_page.dart +++ b/lib/features/logs/presentation/pages/logs_page.dart @@ -1,31 +1,38 @@ -import 'package:flutter/material.dart'; import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../../shared/design_system/tokens/color_tokens.dart'; -import '../../../../shared/design_system/tokens/spacing_tokens.dart'; -import '../../../../shared/design_system/tokens/typography_tokens.dart'; -import '../../../../shared/design_system/components/cards/uf_metric_card.dart'; -import '../../../../shared/design_system/components/cards/uf_info_card.dart'; -import '../../../../shared/design_system/components/inputs/uf_switch_tile.dart'; -import '../../../../shared/design_system/components/inputs/uf_dropdown_tile.dart'; +import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../core/di/injection_container.dart'; import '../bloc/logs_monitoring_bloc.dart'; +import '../../data/models/system_log_model.dart'; +import '../../data/models/system_alert_model.dart'; /// Page Logs & Monitoring - UnionFlow Mobile /// /// Page complète de consultation des logs système avec monitoring en temps réel, /// alertes, métriques système et gestion avancée des journaux. -class LogsPage extends StatefulWidget { +class LogsPage extends StatelessWidget { const LogsPage({super.key}); @override - State createState() => _LogsPageState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => sl()..add(LoadMetrics()), + child: const _LogsView(), + ); + } } -class _LogsPageState extends State - with TickerProviderStateMixin { +class _LogsView extends StatefulWidget { + const _LogsView(); + + @override + State<_LogsView> createState() => _LogsViewState(); +} + +class _LogsViewState extends State<_LogsView> with TickerProviderStateMixin { late TabController _tabController; - late Timer _refreshTimer; + Timer? _refreshTimer; final TextEditingController _searchController = TextEditingController(); // États de filtrage @@ -36,6 +43,11 @@ class _LogsPageState extends State bool _autoRefresh = true; bool _isLiveMode = false; + // Données réelles du BLoC + List _currentLogs = []; + List _currentAlerts = []; + bool _isLoadingLogs = false; + // Données de configuration final List _levels = ['Tous', 'CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE']; final List _timeRanges = ['Temps réel', 'Dernière heure', 'Dernières 24h', 'Dernière semaine', 'Dernier mois']; @@ -53,100 +65,105 @@ class _LogsPageState extends State 'uptime': '15j 7h 23m', }; - // Alertes actives - final List> _activeAlerts = [ - { - 'id': 'alert_001', - 'level': 'WARNING', - 'title': 'CPU élevé', - 'message': 'Utilisation CPU > 80% pendant 5 minutes', - 'timestamp': DateTime.now().subtract(const Duration(minutes: 12)), - 'acknowledged': false, - }, - { - 'id': 'alert_002', - 'level': 'INFO', - 'title': 'Sauvegarde terminée', - 'message': 'Sauvegarde automatique réussie (2.3 GB)', - 'timestamp': DateTime.now().subtract(const Duration(hours: 2)), - 'acknowledged': true, - }, - ]; - @override void initState() { super.initState(); - _tabController = TabController(length: 5, vsync: this); + _tabController = TabController(length: 4, vsync: this); _startAutoRefresh(); + // Charger les données initiales après le premier frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + context.read() + ..add(SearchLogs()) + ..add(LoadAlerts()); + } + }); } @override void dispose() { _tabController.dispose(); _searchController.dispose(); - if (_autoRefresh) { - _refreshTimer.cancel(); - } + _refreshTimer?.cancel(); super.dispose(); } void _startAutoRefresh() { + _refreshTimer?.cancel(); if (_autoRefresh) { - _refreshTimer = Timer.periodic(const Duration(seconds: 5), (timer) { - if (mounted && _isLiveMode) { - setState(() { - // Simuler l'arrivée de nouveaux logs - _updateSystemMetrics(); - }); + _refreshTimer = Timer.periodic(const Duration(seconds: 10), (timer) { + if (!mounted) return; + context.read().add(LoadMetrics()); + context.read().add(LoadAlerts()); + if (_isLiveMode) { + _dispatchSearchLogs(); } }); } } - void _updateSystemMetrics() { - setState(() { - _systemMetrics['cpu'] = 20 + (DateTime.now().millisecond % 40); - _systemMetrics['memory'] = 60 + (DateTime.now().millisecond % 20); - _systemMetrics['network'] = 10 + (DateTime.now().millisecond % 15); - _systemMetrics['activeConnections'] = 1200 + (DateTime.now().millisecond % 100); - }); - } - @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => sl()..add(LoadMetrics()), - child: BlocConsumer( - listener: (context, state) { - if (state is LogsMonitoringSuccess) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: ColorTokens.success, - behavior: SnackBarBehavior.floating, + return BlocConsumer( + listener: (context, state) { + if (state is MetricsLoaded) { + // setState doit être appelé dans le listener, jamais dans builder + _updateSystemMetricsFromState(state.metrics); + } else if (state is LogsLoaded) { + setState(() { + _currentLogs = state.logs; + _isLoadingLogs = false; + }); + } else if (state is AlertsLoaded) { + setState(() { + _currentAlerts = state.alerts; + }); + } else if (state is LogsMonitoringSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: ColorTokens.success, + behavior: SnackBarBehavior.floating, + ), + ); + } else if (state is LogsMonitoringError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: ColorTokens.error, + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + builder: (context, state) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: UFAppBar( + title: 'Logs & Monitoring', + moduleGradient: ModuleColors.logsGradient, + automaticallyImplyLeading: true, + actions: [ + IconButton( + onPressed: () => _toggleLiveMode(), + icon: Icon( + _isLiveMode ? Icons.stop_circle : Icons.play_circle, + color: _isLiveMode ? AppColors.successUI : AppColors.onGradient, + ), + tooltip: _isLiveMode ? 'Arrêter le mode temps réel' : 'Mode temps réel', ), - ); - } else if (state is LogsMonitoringError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: ColorTokens.error, - behavior: SnackBarBehavior.floating, + IconButton( + onPressed: () => _showExportDialog(), + icon: const Icon(Icons.download, color: AppColors.onGradient), + tooltip: 'Exporter les données', ), - ); - } - }, - builder: (context, state) { - // Mettre à jour les métriques avec les données du state - if (state is MetricsLoaded) { - _updateSystemMetricsFromState(state.metrics); - } - - return Scaffold( - backgroundColor: ColorTokens.background, - body: Column( + ], + ), + body: SafeArea( + top: false, + child: Column( children: [ - _buildHeader(), + _buildSystemIndicators(), _buildTabBar(), Expanded( child: TabBarView( @@ -156,159 +173,106 @@ class _LogsPageState extends State _buildLogsTab(), _buildAlertsTab(), _buildMetricsTab(), - _buildSettingsTab(), ], ), ), ], ), - ); - }, + ), + ); + }, + ); + } + + /// Indicateurs système KPI (remplace le header custom) + Widget _buildSystemIndicators() { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Container( + margin: const EdgeInsets.fromLTRB(SpacingTokens.lg, SpacingTokens.md, SpacingTokens.lg, 0), + padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.md), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), + border: Border.all(color: isDark ? AppColors.borderDark : AppColors.border), + boxShadow: const [BoxShadow(color: AppColors.shadow, blurRadius: 8, offset: Offset(0, 2))], + ), + child: Row( + children: [ + Expanded(child: _buildKpiIndicator('CPU', '${(_systemMetrics['cpu'] as num).toDouble().toStringAsFixed(0)}%', Icons.memory, _getCpuColor())), + Container(width: 1, height: 36, color: isDark ? AppColors.borderDark : AppColors.border), + Expanded(child: _buildKpiIndicator('RAM', '${(_systemMetrics['memory'] as num).toDouble().toStringAsFixed(0)}%', Icons.storage, _getMemoryColor())), + Container(width: 1, height: 36, color: isDark ? AppColors.borderDark : AppColors.border), + Expanded(child: _buildKpiIndicator('Disque', '${(_systemMetrics['disk'] as num).toDouble().toStringAsFixed(0)}%', Icons.sd_storage, AppColors.warning)), + Container(width: 1, height: 36, color: isDark ? AppColors.borderDark : AppColors.border), + Expanded(child: _buildKpiIndicator('Uptime', _systemMetrics['uptime'] as String, Icons.schedule, AppColors.success)), + ], ), ); } - /// Header avec métriques système en temps réel - Widget _buildHeader() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: SpacingTokens.sm, vertical: SpacingTokens.xs), - padding: const EdgeInsets.all(SpacingTokens.md), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: ColorTokens.primaryGradient, - begin: Alignment.topLeft, - end: Alignment.bottomRight, + Widget _buildKpiIndicator(String label, String value, IconData icon, Color color) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(SpacingTokens.sm), + decoration: BoxDecoration(color: color.withOpacity(0.1), shape: BoxShape.circle), + child: Icon(icon, color: color, size: 16), ), - borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), - boxShadow: [ - BoxShadow( - color: ColorTokens.primary.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 8), + const SizedBox(height: SpacingTokens.xs), + Text( + value, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimary, ), - ], - ), - child: Column( - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(SpacingTokens.sm), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), - ), - child: const Icon(Icons.monitor_heart, color: Colors.white, size: 20), - ), - const SizedBox(width: SpacingTokens.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Logs & Monitoring', - style: TypographyTokens.headlineSmall.copyWith(color: Colors.white), - ), - Text( - 'Surveillance système en temps réel', - style: TypographyTokens.bodyMedium.copyWith(color: Colors.white.withOpacity(0.8)), - ), - ], - ), - ), - Row( - children: [ - Container( - decoration: BoxDecoration( - color: _isLiveMode ? ColorTokens.success.withOpacity(0.3) : Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), - ), - child: IconButton( - onPressed: () => _toggleLiveMode(), - icon: Icon( - _isLiveMode ? Icons.stop : Icons.play_arrow, - color: Colors.white, - ), - tooltip: _isLiveMode ? 'Arrêter le mode temps réel' : 'Mode temps réel', - ), - ), - const SizedBox(width: SpacingTokens.md), - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), - ), - child: IconButton( - onPressed: () => _showExportDialog(), - icon: const Icon(Icons.download, color: Colors.white), - tooltip: 'Exporter les données', - ), - ), - ], - ), - ], + ), + Text( + label, + style: TextStyle( + fontSize: 10, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary, ), - const SizedBox(height: SpacingTokens.sm), - // Métriques système en temps réel - Row( - children: [ - Expanded(child: UFMetricCard(label: 'CPU', value: '${_systemMetrics['cpu']?.toStringAsFixed(1)}%', icon: Icons.memory, color: _getCpuColor())), - const SizedBox(width: SpacingTokens.md), - Expanded(child: UFMetricCard(label: 'RAM', value: '${_systemMetrics['memory']?.toStringAsFixed(1)}%', icon: Icons.storage, color: _getMemoryColor())), - const SizedBox(width: SpacingTokens.md), - Expanded(child: UFMetricCard(label: 'Réseau', value: '${_systemMetrics['network']?.toStringAsFixed(1)} MB/s', icon: Icons.network_check, color: ColorTokens.info)), - ], - ), - const SizedBox(height: SpacingTokens.sm), - Row( - children: [ - Expanded(child: UFMetricCard(label: 'Connexions', value: '${_systemMetrics['activeConnections']}', icon: Icons.people, color: ColorTokens.success)), - const SizedBox(width: SpacingTokens.md), - Expanded(child: UFMetricCard(label: 'Erreurs/min', value: '${(_systemMetrics['errorRate']! * 100).toStringAsFixed(1)}', icon: Icons.error, color: ColorTokens.error)), - const SizedBox(width: SpacingTokens.md), - Expanded(child: UFMetricCard(label: 'Uptime', value: _systemMetrics['uptime'], icon: Icons.schedule, color: ColorTokens.secondary)), - ], - ), - ], - ), + ), + ], ); } Color _getCpuColor() { - final cpu = _systemMetrics['cpu'] as double; + final cpu = (_systemMetrics['cpu'] as num).toDouble(); if (cpu > 80) return ColorTokens.error; if (cpu > 60) return ColorTokens.warning; return ColorTokens.success; } Color _getMemoryColor() { - final memory = _systemMetrics['memory'] as double; + final memory = (_systemMetrics['memory'] as num).toDouble(); if (memory > 85) return ColorTokens.error; if (memory > 70) return ColorTokens.warning; return ColorTokens.success; } - - /// Barre d'onglets réorganisée Widget _buildTabBar() { return Container( - margin: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg), + margin: const EdgeInsets.fromLTRB(SpacingTokens.lg, SpacingTokens.md, SpacingTokens.lg, 0), decoration: BoxDecoration( - color: ColorTokens.surface, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), - boxShadow: [ + boxShadow: const [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: AppColors.shadow, blurRadius: 10, - offset: const Offset(0, 2), + offset: Offset(0, 2), ), ], ), child: TabBar( controller: _tabController, labelColor: ColorTokens.primary, - unselectedLabelColor: ColorTokens.onSurfaceVariant, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, indicatorColor: ColorTokens.primary, indicatorWeight: 3, labelStyle: TypographyTokens.labelSmall.copyWith(fontWeight: FontWeight.w600), @@ -318,7 +282,6 @@ class _LogsPageState extends State Tab(icon: Icon(Icons.list_alt, size: 16), text: 'Logs'), Tab(icon: Icon(Icons.notification_important, size: 16), text: 'Alertes'), Tab(icon: Icon(Icons.analytics, size: 16), text: 'Métriques'), - Tab(icon: Icon(Icons.settings, size: 16), text: 'Config'), ], ), ); @@ -326,6 +289,16 @@ class _LogsPageState extends State // ==================== MÉTHODES D'ACTION ==================== + void _dispatchSearchLogs() { + setState(() => _isLoadingLogs = true); + context.read().add(SearchLogs( + level: _selectedLevel == 'Tous' ? null : _selectedLevel, + source: _selectedSource == 'Tous' ? null : _selectedSource, + searchQuery: _searchQuery.isEmpty ? null : _searchQuery, + timeRange: _selectedTimeRange == 'Temps réel' ? null : _selectedTimeRange, + )); + } + void _toggleLiveMode() { setState(() { _isLiveMode = !_isLiveMode; @@ -333,9 +306,7 @@ class _LogsPageState extends State _selectedTimeRange = 'Temps réel'; _startAutoRefresh(); } else { - if (_autoRefresh) { - _refreshTimer.cancel(); - } + _refreshTimer?.cancel(); } }); _showSuccessSnackBar(_isLiveMode ? 'Mode temps réel activé' : 'Mode temps réel désactivé'); @@ -350,7 +321,7 @@ class _LogsPageState extends State mainAxisSize: MainAxisSize.min, children: [ const Text('Sélectionnez les données à exporter :'), - const SizedBox(height: 16), + const SizedBox(height: SpacingTokens.xl), CheckboxListTile( title: const Text('Logs système'), value: true, @@ -380,7 +351,7 @@ class _LogsPageState extends State }, style: ElevatedButton.styleFrom( backgroundColor: ColorTokens.primary, - foregroundColor: Colors.white, + foregroundColor: AppColors.onPrimary, ), child: const Text('Exporter'), ), @@ -395,22 +366,31 @@ class _LogsPageState extends State // ==================== ONGLETS PRINCIPAUX ==================== - /// Onglet Dashboard - Vue d'ensemble + /// Onglet 1 — Dashboard (vue d'ensemble non-redondante) Widget _buildDashboardTab() { - return SingleChildScrollView( + return RefreshIndicator( + color: ModuleColors.logs, + onRefresh: () async { + context.read() + ..add(LoadMetrics()) + ..add(LoadAlerts()); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( children: [ const SizedBox(height: SpacingTokens.xl), _buildSystemStatus(), const SizedBox(height: SpacingTokens.xl), - _buildQuickStats(), + _buildLogSummary24h(), const SizedBox(height: SpacingTokens.xl), - _buildRecentAlerts(), + _buildAlertsSummaryCard(), const SizedBox(height: 80), ], ), - ); + ), // SingleChildScrollView + ); // RefreshIndicator } /// Statut système @@ -427,7 +407,7 @@ class _LogsPageState extends State child: Text( 'OPÉRATIONNEL', style: TypographyTokens.labelSmall.copyWith( - color: Colors.white, + color: AppColors.onPrimary, fontWeight: FontWeight.w600, ), ), @@ -477,7 +457,7 @@ class _LogsPageState extends State service, style: TypographyTokens.bodySmall.copyWith( fontWeight: FontWeight.w600, - color: ColorTokens.onSurface, + color: Theme.of(context).colorScheme.onSurface, ), ), ), @@ -493,26 +473,34 @@ class _LogsPageState extends State ); } - /// Statistiques rapides - Widget _buildQuickStats() { + /// Activité — Dernières 24h (remplace _buildQuickStats) + Widget _buildLogSummary24h() { return UFInfoCard( - title: 'Statistiques (dernières 24h)', - icon: Icons.speed, + title: 'Activité — Dernières 24h', + icon: Icons.bar_chart, child: Column( children: [ Row( children: [ - Expanded(child: _buildStatItem('Logs totaux', '15,247', Icons.list_alt, ColorTokens.info)), + Expanded(child: _buildStatItem('Logs totaux', '${_systemMetrics['totalLogs'] ?? 15247}', Icons.list_alt, AppColors.info)), const SizedBox(width: SpacingTokens.lg), - Expanded(child: _buildStatItem('Erreurs', '23', Icons.error, ColorTokens.error)), + Expanded(child: _buildStatItem('Erreurs', '${_systemMetrics['errors'] ?? 23}', Icons.error_outline, AppColors.error)), ], ), const SizedBox(height: SpacingTokens.lg), Row( children: [ - Expanded(child: _buildStatItem('Warnings', '156', Icons.warning, ColorTokens.warning)), + Expanded(child: _buildStatItem('Warnings', '${_systemMetrics['warnings'] ?? 156}', Icons.warning_amber, AppColors.warning)), const SizedBox(width: SpacingTokens.lg), - Expanded(child: _buildStatItem('Temps réponse', '127ms', Icons.timer, ColorTokens.success)), + Expanded(child: _buildStatItem('Connexions', '${_systemMetrics['activeConnections'] ?? 1247}', Icons.people_outline, AppColors.success)), + ], + ), + const SizedBox(height: SpacingTokens.lg), + Row( + children: [ + Expanded(child: _buildStatItem('Taux erreur', '${(((_systemMetrics['errorRate'] as num?)?.toDouble() ?? 0.02) * 100).toStringAsFixed(1)}%', Icons.percent, AppColors.warning)), + const SizedBox(width: SpacingTokens.lg), + Expanded(child: _buildStatItem('Tps réponse', '${_systemMetrics['responseTime'] ?? 127}ms', Icons.timer_outlined, AppColors.primaryDark)), ], ), ], @@ -542,7 +530,7 @@ class _LogsPageState extends State Text( label, style: TypographyTokens.bodySmall.copyWith( - color: ColorTokens.onSurfaceVariant, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), @@ -551,23 +539,64 @@ class _LogsPageState extends State ); } - /// Alertes récentes - Widget _buildRecentAlerts() { + /// Résumé alertes dashboard (remplace _buildRecentAlerts) + Widget _buildAlertsSummaryCard() { + final unacknowledged = _currentAlerts.where((a) => a.acknowledged == false).toList(); return UFInfoCard( - title: 'Alertes récentes', - icon: Icons.notification_important, - trailing: TextButton( + title: 'Alertes', + icon: Icons.notifications_outlined, + trailing: TextButton.icon( onPressed: () => _tabController.animateTo(2), - child: const Text('Voir tout'), - ), - child: Column( - children: _activeAlerts.take(3).map((alert) => _buildAlertItem(alert)).toList(), + icon: const Icon(Icons.arrow_forward, size: 14), + label: const Text('Gérer'), ), + child: unacknowledged.isEmpty + ? Row( + children: [ + Container( + padding: const EdgeInsets.all(SpacingTokens.md), + decoration: BoxDecoration( + color: AppColors.successContainer, + shape: BoxShape.circle, + ), + child: Icon(Icons.check_circle, color: AppColors.success, size: 20), + ), + const SizedBox(width: SpacingTokens.lg), + Text( + 'Aucune alerte active', + style: TextStyle( + color: AppColors.success, + fontWeight: FontWeight.w600, + ), + ), + ], + ) + : Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg, vertical: SpacingTokens.sm), + decoration: BoxDecoration( + color: AppColors.errorContainer, + borderRadius: BorderRadius.circular(SpacingTokens.radiusCircular), + ), + child: Text( + '${unacknowledged.length} non acquittée${unacknowledged.length > 1 ? 's' : ''}', + style: TextStyle(color: AppColors.error, fontWeight: FontWeight.bold, fontSize: 12), + ), + ), + ], + ), + const SizedBox(height: SpacingTokens.lg), + ...unacknowledged.take(2).map((alert) => _buildAlertItem(alert)), + ], + ), ); } - Widget _buildAlertItem(Map alert) { - final color = _getAlertColor(alert['level']); + Widget _buildAlertItem(SystemAlertModel alert) { + final color = _getAlertColor(alert.level ?? 'INFO'); return Container( margin: const EdgeInsets.only(bottom: SpacingTokens.lg), padding: const EdgeInsets.all(SpacingTokens.lg), @@ -578,38 +607,19 @@ class _LogsPageState extends State ), child: Row( children: [ - Icon( - _getAlertIcon(alert['level']), - color: color, - size: 20, - ), + Icon(_getAlertIcon(alert.level ?? 'INFO'), color: color, size: 20), const SizedBox(width: SpacingTokens.lg), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - alert['title'], - style: TypographyTokens.bodyMedium.copyWith( - fontWeight: FontWeight.w600, - color: color, - ), - ), - Text( - alert['message'], - style: TypographyTokens.bodySmall.copyWith( - color: ColorTokens.onSurfaceVariant, - ), - ), + Text(alert.title ?? '', style: TypographyTokens.bodyMedium.copyWith(fontWeight: FontWeight.w600, color: color)), + Text(alert.message ?? '', style: TypographyTokens.bodySmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ), ), - Text( - _formatTimestamp(alert['timestamp']), - style: TypographyTokens.labelSmall.copyWith( - color: ColorTokens.onSurfaceVariant, - ), - ), + if (alert.timestamp != null) + Text(_formatTimestamp(alert.timestamp!), style: TypographyTokens.labelSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ), ); @@ -630,11 +640,11 @@ class _LogsPageState extends State margin: const EdgeInsets.all(SpacingTokens.lg), padding: const EdgeInsets.all(SpacingTokens.xl), decoration: BoxDecoration( - color: ColorTokens.surface, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: AppColors.shadow, blurRadius: 10, offset: const Offset(0, 2), ), @@ -647,38 +657,41 @@ class _LogsPageState extends State Expanded( child: Container( decoration: BoxDecoration( - color: Colors.grey[50], - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[300]!), + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), + border: Border.all(color: Theme.of(context).colorScheme.outline), ), child: TextField( controller: _searchController, - onChanged: (value) => setState(() => _searchQuery = value), + onChanged: (value) { + setState(() => _searchQuery = value); + _dispatchSearchLogs(); + }, decoration: const InputDecoration( hintText: 'Rechercher dans les logs...', prefixIcon: Icon(Icons.search), border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: EdgeInsets.symmetric(horizontal: SpacingTokens.xl, vertical: SpacingTokens.lg), ), ), ), ), - const SizedBox(width: 12), - _buildFilterChip(_selectedLevel, _levels), + const SizedBox(width: SpacingTokens.lg), + Expanded(child: _buildFilterChip(_selectedLevel, _levels)), ], ), - const SizedBox(height: 12), + const SizedBox(height: SpacingTokens.lg), Row( children: [ Expanded(child: _buildFilterChip(_selectedTimeRange, _timeRanges)), - const SizedBox(width: 12), + const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildFilterChip(_selectedSource, _sources)), - const SizedBox(width: 12), + const SizedBox(width: SpacingTokens.lg), ElevatedButton.icon( onPressed: () => _clearFilters(), style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[100], - foregroundColor: Colors.grey[700], + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, elevation: 0, ), icon: const Icon(Icons.clear, size: 16), @@ -693,11 +706,11 @@ class _LogsPageState extends State Widget _buildFilterChip(String value, List options) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg), decoration: BoxDecoration( - color: Colors.grey[50], - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey[300]!), + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), + border: Border.all(color: Theme.of(context).colorScheme.outline), ), child: DropdownButtonHideUnderline( child: DropdownButton( @@ -709,6 +722,7 @@ class _LogsPageState extends State if (options == _timeRanges) _selectedTimeRange = newValue!; if (options == _sources) _selectedSource = newValue!; }); + _dispatchSearchLogs(); }, items: options.map((option) => DropdownMenuItem(value: option, child: Text(option))).toList(), ), @@ -717,30 +731,54 @@ class _LogsPageState extends State } Widget _buildLogsList() { - final logs = _getFilteredLogs(); + if (_isLoadingLogs) { + return const Center( + child: Padding( + padding: EdgeInsets.all(SpacingTokens.xxxl), + child: CircularProgressIndicator(), + ), + ); + } + if (_currentLogs.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(SpacingTokens.xxxl), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.list_alt, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4)), + const SizedBox(height: SpacingTokens.lg), + Text( + 'Aucun log correspondant aux filtres', + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 12), - itemCount: logs.length, - itemBuilder: (context, index) => _buildLogEntry(logs[index]), + padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg), + itemCount: _currentLogs.length, + itemBuilder: (context, index) => _buildLogEntry(_currentLogs[index]), ); } - Widget _buildLogEntry(Map log) { - final color = _getLogColor(log['level']); + Widget _buildLogEntry(SystemLogModel log) { + final color = _getLogColor(log.level ?? 'INFO'); + final ts = log.timestamp; + final timeStr = ts != null + ? '${ts.hour.toString().padLeft(2, '0')}:${ts.minute.toString().padLeft(2, '0')}:${ts.second.toString().padLeft(2, '0')}' + : '--:--:--'; return Container( margin: const EdgeInsets.only(bottom: SpacingTokens.md), padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( - color: ColorTokens.surface, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all(color: color.withOpacity(0.2)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + boxShadow: const [BoxShadow(color: AppColors.shadow, blurRadius: 4, offset: Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -749,49 +787,48 @@ class _LogsPageState extends State children: [ Container( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.sm), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), - ), + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(SpacingTokens.radiusLg)), child: Text( - log['level'], - style: TypographyTokens.labelSmall.copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, - ), + log.level ?? 'INFO', + style: TypographyTokens.labelSmall.copyWith(color: AppColors.onPrimary, fontWeight: FontWeight.w600), ), ), const SizedBox(width: SpacingTokens.md), - Text( - log['timestamp'], - style: TypographyTokens.bodySmall.copyWith( - color: ColorTokens.onSurfaceVariant, - ), - ), + Text(timeStr, style: TypographyTokens.bodySmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), const Spacer(), - Text( - log['source'], - style: TypographyTokens.bodySmall.copyWith( - color: ColorTokens.onSurfaceVariant, - fontWeight: FontWeight.w600, + if (log.source != null) + Container( + padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.xs), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(SpacingTokens.radiusSm), + ), + child: Text(log.source!, style: TypographyTokens.labelSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600)), ), - ), ], ), const SizedBox(height: SpacingTokens.md), - Text( - log['message'], - style: TypographyTokens.bodyMedium.copyWith( - color: ColorTokens.onSurface, - ), - ), - if (log['details'] != null) ...[ + Text(log.message ?? '', style: TypographyTokens.bodyMedium.copyWith(color: Theme.of(context).colorScheme.onSurface)), + if (log.details != null && log.details!.isNotEmpty) ...[ const SizedBox(height: SpacingTokens.sm), - Text( - log['details'], - style: TypographyTokens.bodySmall.copyWith( - color: ColorTokens.onSurfaceVariant, - ), + Text(log.details!, style: TypographyTokens.bodySmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), + ], + if (log.username != null || log.ipAddress != null) ...[ + const SizedBox(height: SpacingTokens.sm), + Row( + children: [ + if (log.username != null) ...[ + Icon(Icons.person_outline, size: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + const SizedBox(width: SpacingTokens.xs), + Text(log.username!, style: TypographyTokens.labelSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), + const SizedBox(width: SpacingTokens.lg), + ], + if (log.ipAddress != null) ...[ + Icon(Icons.lan_outlined, size: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + const SizedBox(width: SpacingTokens.xs), + Text(log.ipAddress!, style: TypographyTokens.labelSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), + ], + ], ), ], ], @@ -799,164 +836,187 @@ class _LogsPageState extends State ); } - /// Onglet Alertes + /// Onglet 3 — Alertes (gestion uniquement, config déplacée dans Config) Widget _buildAlertsTab() { return SingleChildScrollView( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( children: [ - const SizedBox(height: 16), - _buildAlertsConfiguration(), - const SizedBox(height: 16), + const SizedBox(height: SpacingTokens.xl), _buildActiveAlerts(), + const SizedBox(height: SpacingTokens.xl), + _buildAlertHistoryInfo(), const SizedBox(height: 80), ], ), ); } - Widget _buildAlertsConfiguration() { - return UFInfoCard( - title: 'Configuration des alertes', - icon: Icons.tune, - child: Column( - children: [ - UFSwitchTile( - title: 'CPU élevé', - subtitle: 'Alerte si CPU > 80% pendant 5 min', - value: true, - onChanged: (value) => _showSuccessSnackBar('Alerte ${value ? 'activée' : 'désactivée'}'), - ), - UFSwitchTile( - title: 'Mémoire faible', - subtitle: 'Alerte si RAM < 20%', - value: true, - onChanged: (value) => _showSuccessSnackBar('Alerte ${value ? 'activée' : 'désactivée'}'), - ), - UFSwitchTile( - title: 'Erreurs critiques', - subtitle: 'Alerte pour toute erreur CRITICAL', - value: true, - onChanged: (value) => _showSuccessSnackBar('Alerte ${value ? 'activée' : 'désactivée'}'), - ), - UFSwitchTile( - title: 'Connexions échouées', - subtitle: 'Alerte si > 100 échecs/min', - value: false, - onChanged: (value) => _showSuccessSnackBar('Alerte ${value ? 'activée' : 'désactivée'}'), - ), - ], - ), - ); - } - - - Widget _buildActiveAlerts() { + if (_currentAlerts.isEmpty) { + return UFInfoCard( + title: 'Alertes actives', + icon: Icons.notifications_active, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: SpacingTokens.xl), + child: Column( + children: [ + Icon(Icons.check_circle_outline, size: 40, color: AppColors.success), + const SizedBox(height: SpacingTokens.lg), + Text('Aucune alerte active', style: TextStyle(color: AppColors.success, fontWeight: FontWeight.w600)), + ], + ), + ), + ); + } return UFInfoCard( - title: 'Alertes actives', + title: 'Alertes actives (${_currentAlerts.length})', icon: Icons.notifications_active, child: Column( - children: _activeAlerts.map((alert) => _buildDetailedAlertItem(alert)).toList(), + children: _currentAlerts.map((alert) => _buildDetailedAlertItem(alert)).toList(), ), ); } - Widget _buildDetailedAlertItem(Map alert) { - final color = _getAlertColor(alert['level']); + Widget _buildDetailedAlertItem(SystemAlertModel alert) { + final color = _getAlertColor(alert.level ?? 'INFO'); + final isAcknowledged = alert.acknowledged ?? false; return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: SpacingTokens.lg), + padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: color.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.2)), + borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), + border: Border.all(color: isAcknowledged ? Theme.of(context).colorScheme.outlineVariant : color.withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(_getAlertIcon(alert['level']), color: color, size: 20), - const SizedBox(width: 8), + Icon(_getAlertIcon(alert.level ?? 'INFO'), color: isAcknowledged ? Theme.of(context).colorScheme.onSurfaceVariant : color, size: 20), + const SizedBox(width: SpacingTokens.md), Expanded( child: Text( - alert['title'], - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: color, - ), + alert.title ?? '', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: isAcknowledged ? Theme.of(context).colorScheme.onSurfaceVariant : color), ), ), - if (!alert['acknowledged']) + if (!isAcknowledged && alert.id != null) ElevatedButton( - onPressed: () => _acknowledgeAlert(alert['id']), + onPressed: () => _acknowledgeAlert(alert.id!), style: ElevatedButton.styleFrom( backgroundColor: color, - foregroundColor: Colors.white, + foregroundColor: AppColors.onPrimary, minimumSize: const Size(80, 32), + padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg), ), child: const Text('Acquitter', style: TextStyle(fontSize: 12)), + ) + else if (isAcknowledged) + Container( + padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.xs), + decoration: BoxDecoration( + color: AppColors.successContainer, + borderRadius: BorderRadius.circular(SpacingTokens.radiusSm), + ), + child: Text('Acquittée', style: TextStyle(fontSize: 11, color: AppColors.success, fontWeight: FontWeight.w600)), ), ], ), - const SizedBox(height: 8), - Text( - alert['message'], - style: TextStyle( - fontSize: 12, - color: Colors.grey[700], + const SizedBox(height: SpacingTokens.md), + Text(alert.message ?? '', style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), + if (alert.timestamp != null) ...[ + const SizedBox(height: SpacingTokens.sm), + Text( + 'Il y a ${_formatTimestamp(alert.timestamp!)}', + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), - ), - const SizedBox(height: 4), - Text( - 'Il y a ${_formatTimestamp(alert['timestamp'])}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[500], + ], + if (alert.currentValue != null && alert.thresholdValue != null) ...[ + const SizedBox(height: SpacingTokens.sm), + Text( + 'Valeur: ${alert.currentValue!.toStringAsFixed(1)} / Seuil: ${alert.thresholdValue!.toStringAsFixed(1)}', + style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w600), ), - ), + ], ], ), ); } - /// Onglet Métriques + Widget _buildAlertHistoryInfo() { + return UFInfoCard( + title: 'Informations', + icon: Icons.info_outline, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow(Icons.tune, 'Configuration des alertes', 'Disponible dans l\'onglet Config', onTap: () => _tabController.animateTo(4)), + const SizedBox(height: SpacingTokens.md), + _buildInfoRow(Icons.history, 'Historique complet', 'Consultez l\'onglet Logs pour filtrer par niveau'), + ], + ), + ); + } + + Widget _buildInfoRow(IconData icon, String title, String subtitle, {VoidCallback? onTap}) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), + child: Row( + children: [ + Icon(icon, size: 18, color: Theme.of(context).colorScheme.onSurfaceVariant), + const SizedBox(width: SpacingTokens.lg), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Theme.of(context).colorScheme.onSurface)), + Text(subtitle, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), + ], + ), + ), + if (onTap != null) Icon(Icons.chevron_right, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), + ], + ), + ), + ); + } + + /// Onglet 4 — Métriques (réseau, perf applicative, base de données) Widget _buildMetricsTab() { return SingleChildScrollView( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( children: [ - const SizedBox(height: 16), - _buildSystemMetrics(), - const SizedBox(height: 16), - _buildPerformanceMetrics(), + const SizedBox(height: SpacingTokens.xl), + _buildNetworkMetrics(), + const SizedBox(height: SpacingTokens.xl), + _buildApplicationMetrics(), + const SizedBox(height: SpacingTokens.xl), + _buildDatabaseMetrics(), const SizedBox(height: 80), ], ), ); } - Widget _buildSystemMetrics() { + Widget _buildNetworkMetrics() { return UFInfoCard( - title: 'Métriques système', - icon: Icons.computer, + title: 'Réseau & Connexions', + icon: Icons.network_check, child: Column( children: [ + _buildMetricProgress('Trafic réseau', (_systemMetrics['network'] as num?)?.toDouble() ?? 0.0, ' MB/s', AppColors.info, maxValue: 100), + const SizedBox(height: SpacingTokens.xl), Row( children: [ - Expanded(child: _buildMetricProgress('CPU', _systemMetrics['cpu'], '%', _getCpuColor())), + Expanded(child: _buildMetricValueCard('Connexions actives', '${_systemMetrics['activeConnections'] ?? 1247}', Icons.people_outline, AppColors.success)), const SizedBox(width: SpacingTokens.lg), - Expanded(child: _buildMetricProgress('Mémoire', _systemMetrics['memory'], '%', _getMemoryColor())), - ], - ), - const SizedBox(height: SpacingTokens.lg), - Row( - children: [ - Expanded(child: _buildMetricProgress('Disque', _systemMetrics['disk'], '%', ColorTokens.warning)), - const SizedBox(width: SpacingTokens.lg), - Expanded(child: _buildMetricValue('Uptime', _systemMetrics['uptime'], '', ColorTokens.secondary)), + Expanded(child: _buildMetricValueCard('Taux erreur', '${(((_systemMetrics['errorRate'] as num?)?.toDouble() ?? 0.02) * 100).toStringAsFixed(2)}%', Icons.error_outline, AppColors.error)), ], ), ], @@ -964,7 +1024,42 @@ class _LogsPageState extends State ); } - Widget _buildMetricProgress(String label, double value, String unit, Color color) { + Widget _buildApplicationMetrics() { + return UFInfoCard( + title: 'Performance applicative', + icon: Icons.speed, + child: Column( + children: [ + _buildMetricProgress('Temps de réponse', (_systemMetrics['responseTime'] as num? ?? 127).toDouble(), 'ms', AppColors.primaryDark, maxValue: 500), + const SizedBox(height: SpacingTokens.xl), + _buildMetricProgress('Taux d\'erreur', (((_systemMetrics['errorRate'] as num?)?.toDouble() ?? 0.02) * 100), '%', AppColors.warning, maxValue: 10), + ], + ), + ); + } + + Widget _buildDatabaseMetrics() { + return UFInfoCard( + title: 'Base de données', + icon: Icons.storage, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded(child: _buildMetricValueCard('Pool connexions', 'Actif', Icons.check_circle_outline, AppColors.success)), + const SizedBox(width: SpacingTokens.lg), + Expanded(child: _buildMetricValueCard('Statut', 'OK', Icons.verified_outlined, AppColors.success)), + ], + ), + const SizedBox(height: SpacingTokens.xl), + _buildMetricProgress('Utilisation disque', (_systemMetrics['disk'] as num?)?.toDouble() ?? 45.8, '%', AppColors.warning, maxValue: 100), + ], + ), + ); + } + + Widget _buildMetricProgress(String label, double value, String unit, Color color, {double maxValue = 100}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -973,223 +1068,63 @@ class _LogsPageState extends State children: [ Text( label, - style: TypographyTokens.bodyMedium.copyWith( - fontWeight: FontWeight.w600, - color: ColorTokens.onSurface, - ), + style: TextStyle(fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface, fontSize: 13), ), Text( '${value.toStringAsFixed(1)}$unit', - style: TypographyTokens.bodyMedium.copyWith( - fontWeight: FontWeight.w600, - color: color, - ), + style: TextStyle(fontWeight: FontWeight.w600, color: color, fontSize: 13), ), ], ), const SizedBox(height: SpacingTokens.md), - LinearProgressIndicator( - value: value / 100, - backgroundColor: ColorTokens.surfaceVariant, - valueColor: AlwaysStoppedAnimation(color), - ), - ], - ); - } - - Widget _buildMetricValue(String label, dynamic value, String unit, Color color) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TypographyTokens.bodyMedium.copyWith( - fontWeight: FontWeight.w600, - color: ColorTokens.onSurface, - ), - ), - const SizedBox(height: SpacingTokens.md), - Text( - '$value$unit', - style: TypographyTokens.headlineSmall.copyWith( - fontWeight: FontWeight.bold, - color: color, + ClipRRect( + borderRadius: BorderRadius.circular(SpacingTokens.radiusSm), + child: LinearProgressIndicator( + value: (value / maxValue).clamp(0.0, 1.0), + backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + valueColor: AlwaysStoppedAnimation(color), + minHeight: 6, ), ), ], ); } - Widget _buildPerformanceMetrics() { - return UFInfoCard( - title: 'Métriques de performance', - icon: Icons.speed, + Widget _buildMetricValueCard(String label, String value, IconData icon, Color color) { + return Container( + padding: const EdgeInsets.all(SpacingTokens.lg), + decoration: BoxDecoration( + color: color.withOpacity(0.06), + borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), + border: Border.all(color: color.withOpacity(0.15)), + ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded(child: _buildMetricValue('Connexions actives', _systemMetrics['activeConnections'], '', ColorTokens.success)), - const SizedBox(width: SpacingTokens.lg), - Expanded(child: _buildMetricValue('Temps de réponse', '${_systemMetrics['responseTime']}', 'ms', ColorTokens.info)), - ], + Icon(icon, color: color, size: 18), + const SizedBox(height: SpacingTokens.sm), + Text( + value, + style: TextStyle(fontWeight: FontWeight.bold, color: color, fontSize: 16), ), - const SizedBox(height: SpacingTokens.lg), - Row( - children: [ - Expanded(child: _buildMetricValue('Taux d\'erreur', '${(_systemMetrics['errorRate']! * 100).toStringAsFixed(2)}', '%', ColorTokens.error)), - const SizedBox(width: SpacingTokens.lg), - Expanded(child: _buildMetricValue('Réseau', '${_systemMetrics['network']?.toStringAsFixed(1)}', ' MB/s', ColorTokens.warning)), - ], + Text( + label, + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), ); } - /// Onglet Configuration - Widget _buildSettingsTab() { - return SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - const SizedBox(height: 16), - _buildLoggingSettings(), - const SizedBox(height: 16), - _buildMonitoringSettings(), - const SizedBox(height: 80), - ], - ), - ); - } - - Widget _buildLoggingSettings() { - return UFInfoCard( - title: 'Configuration des logs', - icon: Icons.settings, - child: Column( - children: [ - UFDropdownTile( - title: 'Niveau de log', - value: 'INFO', - items: const ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'], - onChanged: (value) => _showSuccessSnackBar('Paramètre mis à jour'), - ), - UFDropdownTile( - title: 'Rétention', - value: '30 jours', - items: const ['7 jours', '30 jours', '90 jours', '1 an'], - onChanged: (value) => _showSuccessSnackBar('Paramètre mis à jour'), - ), - UFDropdownTile( - title: 'Format', - value: 'JSON', - items: const ['JSON', 'Plain Text', 'Structured'], - onChanged: (value) => _showSuccessSnackBar('Paramètre mis à jour'), - ), - UFSwitchTile( - title: 'Logs détaillés', - subtitle: 'Inclure les stack traces', - value: true, - onChanged: (value) => _showSuccessSnackBar('Paramètre ${value ? 'activé' : 'désactivé'}'), - ), - UFSwitchTile( - title: 'Compression', - subtitle: 'Compresser les anciens logs', - value: true, - onChanged: (value) => _showSuccessSnackBar('Paramètre ${value ? 'activé' : 'désactivé'}'), - ), - ], - ), - ); - } - - Widget _buildMonitoringSettings() { - return UFInfoCard( - title: 'Configuration du monitoring', - icon: Icons.monitor, - child: Column( - children: [ - UFDropdownTile( - title: 'Intervalle de collecte', - value: '5 secondes', - items: const ['1 seconde', '5 secondes', '30 secondes', '1 minute'], - onChanged: (value) => _showSuccessSnackBar('Paramètre mis à jour'), - ), - UFDropdownTile( - title: 'Historique des métriques', - value: '7 jours', - items: const ['1 jour', '7 jours', '30 jours', '90 jours'], - onChanged: (value) => _showSuccessSnackBar('Paramètre mis à jour'), - ), - UFSwitchTile( - title: 'Monitoring temps réel', - subtitle: 'Mise à jour automatique', - value: _autoRefresh, - onChanged: (value) { - setState(() => _autoRefresh = value); - if (value) { - _startAutoRefresh(); - } else { - _refreshTimer.cancel(); - } - _showSuccessSnackBar('Paramètre ${value ? 'activé' : 'désactivé'}'); - }, - ), - UFSwitchTile( - title: 'Alertes email', - subtitle: 'Notifications par email', - value: true, - onChanged: (value) => _showSuccessSnackBar('Paramètre ${value ? 'activé' : 'désactivé'}'), - ), - UFSwitchTile( - title: 'Alertes push', - subtitle: 'Notifications push mobile', - value: false, - onChanged: (value) => _showSuccessSnackBar('Paramètre ${value ? 'activé' : 'désactivé'}'), - ), - ], - ), - ); - } - - + // Onglet "Config" retiré : toute la configuration (logs, alertes, monitoring) + // est désormais dans Paramètres Système (Drawer → Système → Paramètres Système). + // Logs & Monitoring est strictement orienté CONSULTATION (read-only). // ==================== MÉTHODES UTILITAIRES ==================== - List> _getFilteredLogs() { - List> allLogs = [ - {'level': 'CRITICAL', 'timestamp': '16:15:42', 'source': 'Database', 'message': 'Connexion à la base de données perdue', 'details': 'Pool de connexions épuisé'}, - {'level': 'ERROR', 'timestamp': '16:12:33', 'source': 'API', 'message': 'Erreur 500 sur /api/members', 'details': 'NullPointerException dans MemberService.findAll()'}, - {'level': 'WARN', 'timestamp': '16:10:15', 'source': 'Auth', 'message': 'Tentative de connexion avec mot de passe incorrect', 'details': 'IP: 192.168.1.100 - Utilisateur: admin@test.com'}, - {'level': 'INFO', 'timestamp': '16:08:22', 'source': 'System', 'message': 'Sauvegarde automatique terminée', 'details': 'Taille: 2.3 GB - Durée: 45 secondes'}, - {'level': 'DEBUG', 'timestamp': '16:05:45', 'source': 'Cache', 'message': 'Cache invalidé pour user_sessions', 'details': 'Raison: Expiration automatique'}, - {'level': 'TRACE', 'timestamp': '16:03:12', 'source': 'Performance', 'message': 'Requête SQL exécutée', 'details': 'SELECT * FROM members WHERE active = true - Durée: 23ms'}, - ]; - - // Filtrage par niveau - if (_selectedLevel != 'Tous') { - allLogs = allLogs.where((log) => log['level'] == _selectedLevel).toList(); - } - - // Filtrage par source - if (_selectedSource != 'Tous') { - allLogs = allLogs.where((log) => log['source'] == _selectedSource).toList(); - } - - // Filtrage par recherche - if (_searchQuery.isNotEmpty) { - allLogs = allLogs.where((log) => - log['message'].toLowerCase().contains(_searchQuery.toLowerCase()) || - log['source'].toLowerCase().contains(_searchQuery.toLowerCase()) - ).toList(); - } - - return allLogs; - } - Color _getLogColor(String level) { switch (level) { - case 'CRITICAL': return ColorTokens.secondary; + case 'CRITICAL': return AppColors.accentDark; case 'ERROR': return ColorTokens.error; case 'WARN': return ColorTokens.warning; case 'INFO': return ColorTokens.info; @@ -1201,7 +1136,7 @@ class _LogsPageState extends State Color _getAlertColor(String level) { switch (level) { - case 'CRITICAL': return ColorTokens.secondary; + case 'CRITICAL': return AppColors.accentDark; case 'ERROR': return ColorTokens.error; case 'WARNING': return ColorTokens.warning; case 'INFO': return ColorTokens.info; @@ -1233,11 +1168,7 @@ class _LogsPageState extends State } void _acknowledgeAlert(String alertId) { - setState(() { - final alert = _activeAlerts.firstWhere((a) => a['id'] == alertId); - alert['acknowledged'] = true; - }); - _showSuccessSnackBar('Alerte acquittée'); + context.read().add(AcknowledgeAlert(alertId)); } void _clearFilters() { @@ -1249,6 +1180,7 @@ class _LogsPageState extends State _searchController.clear(); }); _showSuccessSnackBar('Filtres réinitialisés'); + _dispatchSearchLogs(); } void _updateSystemMetricsFromState(dynamic metrics) { @@ -1262,6 +1194,10 @@ class _LogsPageState extends State _systemMetrics['errorRate'] = metrics.errorRate ?? 0.02; _systemMetrics['responseTime'] = metrics.averageResponseTimeMs ?? 127; _systemMetrics['uptime'] = metrics.uptimeFormatted ?? '15j 7h 23m'; + // Nouvelles clés 24h — champs réels du SystemMetricsModel + if (metrics.totalLogs24h != null) _systemMetrics['totalLogs'] = metrics.totalLogs24h; + if (metrics.totalErrors24h != null) _systemMetrics['errors'] = metrics.totalErrors24h; + if (metrics.totalWarnings24h != null) _systemMetrics['warnings'] = metrics.totalWarnings24h; }); } diff --git a/lib/features/notifications/presentation/pages/notifications_page.dart b/lib/features/notifications/presentation/pages/notifications_page.dart index 6e97b61..602ddb1 100644 --- a/lib/features/notifications/presentation/pages/notifications_page.dart +++ b/lib/features/notifications/presentation/pages/notifications_page.dart @@ -71,126 +71,54 @@ class _NotificationsPageState extends State } if (state is NotificationsError) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.message), backgroundColor: Colors.red), + SnackBar(content: Text(state.message), backgroundColor: AppColors.error), ); } }, builder: (context, state) { return Scaffold( - backgroundColor: AppColors.lightBackground, - body: Column( - children: [ - _buildHeader(), - _buildTabBar(), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildNotificationsTab(), - _buildPreferencesTab(), - ], - ), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: UFAppBar( + title: 'Notifications', + moduleGradient: ModuleColors.notificationsGradient, + actions: [ + IconButton( + onPressed: () => _markAllAsRead(), + icon: const Icon(Icons.done_all, size: 20), + tooltip: 'Tout marquer comme lu', ), ], + bottom: TabBar( + controller: _tabController, + isScrollable: true, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, + indicatorSize: TabBarIndicatorSize.label, + labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), + tabs: const [ + Tab(child: Text('FLUX')), + Tab(child: Text('RÉGLAGES')), + ], + ), + ), + body: SafeArea( + top: false, + child: TabBarView( + controller: _tabController, + children: [ + _buildNotificationsTab(), + _buildPreferencesTab(), + ], + ), ), ); }, ); } - /// Header harmonisé avec le design system - Widget _buildHeader() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [AppColors.brandGreen, AppColors.primaryGreen], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - color: Color(0x1A000000), - blurRadius: 10, - offset: Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.notifications_none, - color: Colors.white, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'NOTIFICATIONS', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: 1.1, - ), - ), - Text( - 'Restez connecté à votre réseau', - style: TextStyle( - fontSize: 11, - color: Colors.white.withOpacity(0.9), - ), - ), - ], - ), - ), - IconButton( - onPressed: () => _markAllAsRead(), - icon: const Icon(Icons.done_all, color: Colors.white, size: 20), - tooltip: 'Tout marquer comme lu', - ), - ], - ), - ); - } - - /// Barre d'onglets - Widget _buildTabBar() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: AppColors.lightSurface, - borderRadius: BorderRadius.circular(8), - ), - child: TabBar( - controller: _tabController, - labelColor: AppColors.primaryGreen, - unselectedLabelColor: AppColors.textSecondaryLight, - indicator: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: AppColors.primaryGreen.withOpacity(0.1), - ), - labelStyle: AppTypography.actionText.copyWith(fontSize: 12), - unselectedLabelStyle: AppTypography.bodyTextSmall.copyWith(fontSize: 12), - tabs: const [ - Tab(text: 'FLUX'), - Tab(text: 'RÉGLAGES'), - ], - ), - ); - } + // _buildHeader() et _buildTabBar() supprimés : gradient + TabBar migrés + // dans UFAppBar + UFAppBar.bottom (pattern Adhésions). /// Onglet des notifications Widget _buildNotificationsTab() { @@ -236,7 +164,7 @@ class _NotificationsPageState extends State child: Switch( value: _showOnlyUnread, onChanged: (value) => setState(() => _showOnlyUnread = value), - activeColor: AppColors.primaryGreen, + activeColor: AppColors.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), @@ -262,22 +190,25 @@ class _NotificationsPageState extends State /// Chip de filtre Widget _buildFilterChip(String label, bool isSelected) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final borderInactive= isDark ? AppColors.borderDark : AppColors.border; + final textInactive = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; return InkWell( onTap: () => setState(() => _selectedFilter = label), borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( - color: isSelected ? AppColors.primaryGreen.withOpacity(0.1) : Colors.transparent, + color: isSelected ? AppColors.primary.withOpacity(isDark ? 0.2 : 0.1) : Colors.transparent, borderRadius: BorderRadius.circular(4), border: Border.all( - color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder, + color: isSelected ? AppColors.primary : borderInactive, ), ), child: Text( label.toUpperCase(), style: AppTypography.badgeText.copyWith( - color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight, + color: isSelected ? AppColors.primary : textInactive, fontSize: 9, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), @@ -290,17 +221,24 @@ class _NotificationsPageState extends State Widget _buildNotificationsList() { final notifications = _getFilteredNotifications(); - if (notifications.isEmpty) { - return _buildEmptyState(); - } - - return ListView.builder( - padding: const EdgeInsets.all(12), - itemCount: notifications.length, - itemBuilder: (context, index) { - final notification = notifications[index]; - return _buildNotificationCard(notification); - }, + return RefreshIndicator( + color: ModuleColors.notifications, + onRefresh: () async => + context.read().add(const LoadNotifications()), + child: notifications.isEmpty + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: _buildEmptyState(), + ) + : ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(12), + itemCount: notifications.length, + itemBuilder: (context, index) { + final notification = notifications[index]; + return _buildNotificationCard(notification); + }, + ), ); } @@ -310,11 +248,11 @@ class _NotificationsPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.notifications_none_outlined, size: 40, - color: AppColors.textSecondaryLight, - ), + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), const SizedBox(height: 12), Text( 'AUCUNE NOTIFICATION', @@ -338,6 +276,10 @@ class _NotificationsPageState extends State final isRead = notification['isRead'] as bool; final type = notification['type'] as String; final color = _getNotificationColor(type); + final isDark = Theme.of(context).brightness == Brightness.dark; + final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary; + final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; + final bgRead = isDark ? AppColors.surfaceVariantDark : AppColors.surface; return CoreCard( margin: const EdgeInsets.only(bottom: 8), @@ -349,8 +291,8 @@ class _NotificationsPageState extends State MiniAvatar( fallbackText: _getNotificationIconSource(type), size: 32, - backgroundColor: isRead ? AppColors.lightSurface : color.withOpacity(0.1), - iconColor: isRead ? AppColors.textSecondaryLight : color, + backgroundColor: isRead ? bgRead : color.withOpacity(isDark ? 0.2 : 0.1), + iconColor: isRead ? textSecondary : color, isIcon: true, ), const SizedBox(width: 12), @@ -366,7 +308,7 @@ class _NotificationsPageState extends State notification['title'].toString().toUpperCase(), style: AppTypography.actionText.copyWith( fontSize: 11, - color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight, + color: isRead ? textSecondary : textPrimary, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -382,7 +324,7 @@ class _NotificationsPageState extends State Text( notification['message'], style: AppTypography.bodyTextSmall.copyWith( - color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight, + color: isRead ? textSecondary : textPrimary, fontWeight: isRead ? FontWeight.normal : FontWeight.w500, ), maxLines: 2, @@ -392,8 +334,8 @@ class _NotificationsPageState extends State const SizedBox(height: 4), InfoBadge( text: 'NOUVEAU', - backgroundColor: AppColors.primaryGreen.withOpacity(0.1), - textColor: AppColors.primaryGreen, + backgroundColor: AppColors.primary.withOpacity(0.1), + textColor: AppColors.primary, ), ], ], @@ -413,7 +355,7 @@ class _NotificationsPageState extends State child: Text('Supprimer', style: AppTypography.bodyTextSmall.copyWith(color: AppColors.error)), ), ], - child: const Icon(Icons.more_vert, size: 14, color: AppColors.textSecondaryLight), + child: Icon(Icons.more_vert, size: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), @@ -545,7 +487,7 @@ class _NotificationsPageState extends State children: [ Icon( icon, - color: AppColors.primaryGreen, + color: AppColors.primary, size: 18, ), const SizedBox(width: 10), @@ -604,7 +546,7 @@ class _NotificationsPageState extends State child: Switch( value: value, onChanged: onChanged, - activeColor: AppColors.primaryGreen, + activeColor: AppColors.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), @@ -659,15 +601,15 @@ class _NotificationsPageState extends State Color _getNotificationColor(String type) { switch (type) { case 'Membres': - return AppColors.primaryGreen; + return AppColors.primary; case 'Événements': return AppColors.success; case 'Organisations': - return AppColors.primaryGreen; + return AppColors.primary; case 'Système': return AppColors.warning; default: - return AppColors.textSecondaryLight; + return AppColors.textSecondary; } } @@ -756,7 +698,7 @@ class _NotificationsPageState extends State actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: AppColors.textSecondaryLight)), + child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), ), TextButton( onPressed: () { @@ -769,7 +711,7 @@ class _NotificationsPageState extends State }); _showSuccessSnackBar('Flux marqué comme lu'); }, - child: Text('CONFIRMER', style: AppTypography.actionText.copyWith(color: AppColors.primaryGreen)), + child: Text('CONFIRMER', style: AppTypography.actionText.copyWith(color: AppColors.primary)), ), ], ), @@ -797,8 +739,8 @@ class _NotificationsPageState extends State _tabController.animateTo(1); // Aller à l'onglet Préférences }, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, + backgroundColor: AppColors.primary, + foregroundColor: AppColors.onPrimary, ), child: const Text('Voir les préférences'), ), @@ -831,8 +773,8 @@ class _NotificationsPageState extends State _showSuccessSnackBar('Notification supprimée'); }, style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, + backgroundColor: AppColors.error, + foregroundColor: AppColors.onError, ), child: const Text('Supprimer'), ), @@ -884,7 +826,7 @@ class _NotificationsPageState extends State }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.warning, - foregroundColor: Colors.white, + foregroundColor: AppColors.onPrimary, ), child: Text(notification['actionText']), ), diff --git a/lib/features/organizations/presentation/pages/org_selector_page.dart b/lib/features/organizations/presentation/pages/org_selector_page.dart index 25b9b9d..13574e5 100644 --- a/lib/features/organizations/presentation/pages/org_selector_page.dart +++ b/lib/features/organizations/presentation/pages/org_selector_page.dart @@ -5,6 +5,9 @@ library org_selector_page; import 'package:flutter/material.dart'; +import '../../../../shared/design_system/tokens/module_colors.dart'; +import '../../../../shared/design_system/tokens/app_colors.dart'; +import '../../../../shared/design_system/components/uf_app_bar.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../bloc/org_switcher_bloc.dart'; import '../../data/models/org_switcher_entry.dart'; @@ -30,46 +33,49 @@ class _OrgSelectorPageState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( - appBar: AppBar( - title: const Text('Choisir une organisation'), + appBar: UFAppBar( + title: 'Choisir une organisation', + moduleGradient: ModuleColors.organisationsGradient, automaticallyImplyLeading: !widget.required, - elevation: 0, ), - body: BlocConsumer( - listener: (context, state) { - if (state is OrgSwitcherLoaded && widget.required && state.active != null) { - // Une org a été auto-sélectionnée, on peut continuer - } - }, - builder: (context, state) { - if (state is OrgSwitcherLoading) { - return const Center(child: CircularProgressIndicator()); - } - if (state is OrgSwitcherError) { - return _ErrorView( - message: state.message, - onRetry: () => context - .read() - .add(const OrgSwitcherLoadRequested()), - ); - } - if (state is OrgSwitcherLoaded) { - if (state.organisations.isEmpty) { - return const _EmptyView(); + body: SafeArea( + top: false, + child: BlocConsumer( + listener: (context, state) { + if (state is OrgSwitcherLoaded && widget.required && state.active != null) { + // Une org a été auto-sélectionnée, on peut continuer } - return _OrgList( - organisations: state.organisations, - active: state.active, - onSelect: (org) { - context + }, + builder: (context, state) { + if (state is OrgSwitcherLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is OrgSwitcherError) { + return _ErrorView( + message: state.message, + onRetry: () => context .read() - .add(OrgSwitcherSelectRequested(org)); - Navigator.of(context).pop(org); - }, - ); - } - return const SizedBox.shrink(); - }, + .add(const OrgSwitcherLoadRequested()), + ); + } + if (state is OrgSwitcherLoaded) { + if (state.organisations.isEmpty) { + return const _EmptyView(); + } + return _OrgList( + organisations: state.organisations, + active: state.active, + onSelect: (org) { + context + .read() + .add(OrgSwitcherSelectRequested(org)); + Navigator.of(context).pop(org); + }, + ); + } + return const SizedBox.shrink(); + }, + ), ), ); } @@ -103,7 +109,12 @@ class _OrgList extends StatelessWidget { ), ), Expanded( - child: ListView.separated( + child: RefreshIndicator( + color: ModuleColors.organisations, + onRefresh: () async => + context.read().add(const OrgSwitcherLoadRequested()), + child: ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: organisations.length, separatorBuilder: (_, __) => const SizedBox(height: 8), @@ -116,7 +127,8 @@ class _OrgList extends StatelessWidget { onTap: () => onSelect(org), ); }, - ), + ), // ListView.separated + ), // RefreshIndicator ), ], ); @@ -199,8 +211,8 @@ class _OrgCard extends StatelessWidget { _Chip( org.statutMembre!, color: org.statutMembre == 'ACTIF' - ? Colors.green.shade700 - : Colors.orange.shade700, + ? AppColors.success + : AppColors.warning, ), ], ], @@ -268,7 +280,7 @@ class _ErrorView extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.cloud_off, size: 56, color: Colors.grey), + Icon(Icons.cloud_off, size: 56, color: AppColors.textTertiary), const SizedBox(height: 16), Text(message, textAlign: TextAlign.center), const SizedBox(height: 16), @@ -295,7 +307,7 @@ class _EmptyView extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.business_outlined, size: 56, color: Colors.grey), + Icon(Icons.business_outlined, size: 56, color: AppColors.textTertiary), SizedBox(height: 16), Text( 'Vous n\'êtes membre d\'aucune organisation active.', diff --git a/lib/features/organizations/presentation/pages/org_types_page.dart b/lib/features/organizations/presentation/pages/org_types_page.dart index fcf589a..60d2fb2 100644 --- a/lib/features/organizations/presentation/pages/org_types_page.dart +++ b/lib/features/organizations/presentation/pages/org_types_page.dart @@ -30,9 +30,10 @@ class _OrgTypesViewState extends State<_OrgTypesView> { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.lightBackground, - appBar: const UFAppBar( - title: 'TYPES D\'ORGANISATIONS', + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: UFAppBar( + title: "Types d'Organisations", + moduleGradient: ModuleColors.organisationsGradient, automaticallyImplyLeading: true, ), body: BlocConsumer( @@ -41,7 +42,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), - backgroundColor: Colors.green, + backgroundColor: AppColors.success, duration: const Duration(seconds: 2), ), ); @@ -49,7 +50,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), - backgroundColor: Colors.red, + backgroundColor: AppColors.error, duration: const Duration(seconds: 3), ), ); @@ -86,7 +87,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> { if (!isSuperAdmin) return const SizedBox.shrink(); return FloatingActionButton.small( onPressed: () => _showTypeForm(context, null), - backgroundColor: AppColors.primaryGreen, + backgroundColor: AppColors.primary, child: const Icon(Icons.add, color: Colors.white), ); }), @@ -103,10 +104,13 @@ class _OrgTypesViewState extends State<_OrgTypesView> { Widget _buildTypeCard(BuildContext context, TypeReferenceEntity type, OrgTypesState state) { final isOperating = state is OrgTypeOperating; - final color = _parseColor(type.couleur) ?? AppColors.primaryGreen; + final color = _parseColor(type.couleur) ?? AppColors.primary; final authState = context.read().state; final isSuperAdmin = authState is AuthAuthenticated && authState.effectiveRole == UserRole.superAdmin; + final isDark = Theme.of(context).brightness == Brightness.dark; + final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary; + final textSecondary= isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; return Opacity( opacity: isOperating ? 0.6 : 1.0, @@ -145,28 +149,28 @@ class _OrgTypesViewState extends State<_OrgTypesView> { ), if (type.estDefaut) ...[ const SizedBox(width: 6), - const Icon(Icons.star_rounded, size: 13, color: Color(0xFFF59E0B)), + const Icon(Icons.star_rounded, size: 13, color: AppColors.warning), ], if (type.estSysteme) ...[ const SizedBox(width: 6), - Icon(Icons.lock_outline, size: 12, color: Colors.grey[500]), + Icon(Icons.lock_outline, size: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), ], ], ), const SizedBox(height: 4), Text( type.libelle, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, - color: AppColors.textPrimaryLight, + color: textPrimary, ), ), if (type.description != null && type.description!.isNotEmpty) ...[ const SizedBox(height: 2), Text( type.description!, - style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight), + style: TextStyle(fontSize: 11, color: textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -177,14 +181,14 @@ class _OrgTypesViewState extends State<_OrgTypesView> { if (isSuperAdmin && !type.estSysteme && !isOperating) ...[ IconButton( icon: const Icon(Icons.edit_outlined, size: 16), - color: AppColors.textSecondaryLight, + color: AppColors.textSecondary, onPressed: () => _showTypeForm(context, type), padding: const EdgeInsets.all(4), constraints: const BoxConstraints(), ), IconButton( icon: const Icon(Icons.delete_outline, size: 16), - color: Colors.red[400], + color: AppColors.error, onPressed: () => _confirmDelete(context, type), padding: const EdgeInsets.all(4), constraints: const BoxConstraints(), @@ -208,18 +212,24 @@ class _OrgTypesViewState extends State<_OrgTypesView> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.category_outlined, size: 48, color: Colors.grey[400]), + Icon(Icons.category_outlined, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 12), - const Text( + Text( 'Aucun type défini', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.textPrimaryLight), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.textPrimaryDark + : AppColors.textPrimary, + ), ), const SizedBox(height: 6), Text( isSuperAdmin ? 'Créez votre premier type d\'organisation' : 'Aucun type d\'organisation disponible', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), if (isSuperAdmin) ...[ @@ -229,8 +239,8 @@ class _OrgTypesViewState extends State<_OrgTypesView> { icon: const Icon(Icons.add, size: 16), label: const Text('Créer un type'), style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, + backgroundColor: AppColors.primary, + foregroundColor: AppColors.onPrimary, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), @@ -249,17 +259,26 @@ class _OrgTypesViewState extends State<_OrgTypesView> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 40, color: Colors.red[400]), + Icon(Icons.error_outline, size: 40, color: AppColors.error), const SizedBox(height: 12), const Text('Erreur de chargement', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700)), const SizedBox(height: 6), - Text(message, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight), textAlign: TextAlign.center), + Text( + message, + style: TextStyle( + fontSize: 11, + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.textSecondaryDark + : AppColors.textSecondary, + ), + textAlign: TextAlign.center, + ), const SizedBox(height: 16), ElevatedButton( onPressed: () => context.read().add(const LoadOrgTypes()), style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, + backgroundColor: AppColors.primary, + foregroundColor: AppColors.onPrimary, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), @@ -303,8 +322,8 @@ class _OrgTypesViewState extends State<_OrgTypesView> { bloc.add(DeleteOrgTypeEvent(type.id)); }, style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, + backgroundColor: AppColors.error, + foregroundColor: AppColors.onError, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), @@ -369,13 +388,17 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> { @override Widget build(BuildContext context) { - final isEdit = widget.existing != null; + final isEdit = widget.existing != null; + final isDark = Theme.of(context).brightness == Brightness.dark; + final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary; + final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; + final onSurface = Theme.of(context).colorScheme.onSurface; return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), child: Form( @@ -389,13 +412,13 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> { child: Container( width: 36, height: 4, - decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)), + decoration: BoxDecoration(color: Theme.of(context).colorScheme.outline, borderRadius: BorderRadius.circular(2)), ), ), const SizedBox(height: 14), Text( isEdit ? 'Modifier le type' : 'Nouveau type d\'organisation', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: AppColors.textPrimaryLight), + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: textPrimary), ), const SizedBox(height: 14), @@ -446,7 +469,7 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> { const SizedBox(height: 12), // Color picker - const Text('Couleur', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textSecondaryLight)), + Text('Couleur', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: textSecondary)), const SizedBox(height: 6), Wrap( spacing: 8, @@ -462,7 +485,7 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> { decoration: BoxDecoration( color: color, shape: BoxShape.circle, - border: selected ? Border.all(color: Colors.black, width: 2) : null, + border: selected ? Border.all(color: onSurface, width: 2) : null, ), child: selected ? const Icon(Icons.check, size: 14, color: Colors.white) : null, ), @@ -477,8 +500,8 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> { child: ElevatedButton( onPressed: _submit, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, + backgroundColor: AppColors.primary, + foregroundColor: AppColors.onPrimary, padding: const EdgeInsets.symmetric(vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), diff --git a/lib/features/organizations/presentation/pages/organization_detail_page.dart b/lib/features/organizations/presentation/pages/organization_detail_page.dart index 7b19b32..8ca2f24 100644 --- a/lib/features/organizations/presentation/pages/organization_detail_page.dart +++ b/lib/features/organizations/presentation/pages/organization_detail_page.dart @@ -11,10 +11,11 @@ import '../../bloc/organizations_event.dart'; import '../../bloc/organizations_state.dart'; import '../../domain/repositories/organization_repository.dart'; import 'edit_organization_page.dart'; -import '../../../../shared/design_system/tokens/app_colors.dart'; +import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../features/authentication/presentation/bloc/auth_bloc.dart'; import '../../../../features/authentication/data/models/user_role.dart'; + class OrganizationDetailPage extends StatefulWidget { final String organizationId; @@ -44,12 +45,10 @@ class _OrganizationDetailPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.lightBackground, - appBar: AppBar( - backgroundColor: AppColors.brandGreen, - foregroundColor: Colors.white, - title: const Text('Détail Organisation'), - elevation: 0, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: UFAppBar( + title: 'Détail Organisation', + moduleGradient: ModuleColors.organisationsGradient, actions: [ Builder(builder: (ctx) { final authState = ctx.read().state; @@ -67,90 +66,100 @@ class _OrganizationDetailPageState extends State { return PopupMenuButton( onSelected: _handleMenuAction, itemBuilder: (context) => [ - const PopupMenuItem(value: 'activate', child: Row(children: [Icon(Icons.check_circle, color: AppColors.success), SizedBox(width: 8), Text('Activer')])), - const PopupMenuItem(value: 'deactivate', child: Row(children: [Icon(Icons.pause_circle, color: AppColors.textSecondaryLight), SizedBox(width: 8), Text('Désactiver')])), - const PopupMenuItem(value: 'delete', child: Row(children: [Icon(Icons.delete, color: Colors.red), SizedBox(width: 8), Text('Supprimer')])), + const PopupMenuItem(value: 'activate', child: Row(children: [Icon(Icons.check_circle, color: AppColors.success), SizedBox(width: SpacingTokens.md), Text('Activer')])), + const PopupMenuItem(value: 'deactivate', child: Row(children: [Icon(Icons.pause_circle, color: AppColors.textSecondary), SizedBox(width: SpacingTokens.md), Text('Désactiver')])), + const PopupMenuItem(value: 'delete', child: Row(children: [Icon(Icons.delete, color: AppColors.error), SizedBox(width: SpacingTokens.md), Text('Supprimer')])), ], ); }), ], ), - body: BlocBuilder( - builder: (context, state) { - if (state is OrganizationLoading) return _buildLoading(); - if (state is OrganizationLoaded) return _buildContent(state.organization); - if (state is OrganizationsError) return _buildError(state); - return _buildEmpty(); - }, + body: SafeArea( + top: false, + child: BlocBuilder( + builder: (context, state) { + if (state is OrganizationLoading) return _buildLoading(); + if (state is OrganizationLoaded) return _buildContent(state.organization); + if (state is OrganizationsError) return _buildError(state); + return _buildEmpty(); + }, + ), ), ); } Widget _buildContent(OrganizationModel org) { - return SingleChildScrollView( - padding: const EdgeInsets.all(12), + return RefreshIndicator( + color: ModuleColors.organisations, + onRefresh: () async => context + .read() + .add(LoadOrganizationById(widget.organizationId)), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeaderCard(org), - const SizedBox(height: 8), + const SizedBox(height: SpacingTokens.md), _buildInfoCard(org), - const SizedBox(height: 8), + const SizedBox(height: SpacingTokens.md), _buildContactCard(org), - if (_hasAddress(org)) ...[const SizedBox(height: 8), _buildAddressCard(org)], - const SizedBox(height: 8), + if (_hasAddress(org)) ...[const SizedBox(height: SpacingTokens.md), _buildAddressCard(org)], + const SizedBox(height: SpacingTokens.md), _buildStatsCard(org), - if (_hasFinances(org)) ...[const SizedBox(height: 8), _buildFinancesCard(org)], - if (_hasMission(org)) ...[const SizedBox(height: 8), _buildMissionCard(org)], - if (_hasSupplementary(org)) ...[const SizedBox(height: 8), _buildSupplementaryCard(org)], - if (org.notes?.isNotEmpty == true) ...[const SizedBox(height: 8), _buildNotesCard(org)], - const SizedBox(height: 8), + if (_hasFinances(org)) ...[const SizedBox(height: SpacingTokens.md), _buildFinancesCard(org)], + if (_hasMission(org)) ...[const SizedBox(height: SpacingTokens.md), _buildMissionCard(org)], + if (_hasSupplementary(org)) ...[const SizedBox(height: SpacingTokens.md), _buildSupplementaryCard(org)], + if (org.notes?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildNotesCard(org)], + const SizedBox(height: SpacingTokens.md), _buildActionsCard(org), ], - ), - ); + ), // Column + ), // SingleChildScrollView + ); // RefreshIndicator } // ── Header ───────────────────────────────────────────────────────────────── Widget _buildHeaderCard(OrganizationModel org) { return Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( - gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.brandGreen, AppColors.primaryGreen]), - borderRadius: BorderRadius.circular(10), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 2))], + gradient: const LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ModuleColors.organisationsDark, ModuleColors.organisations]), + borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), + boxShadow: [BoxShadow(color: AppColors.shadowMedium, blurRadius: 8, offset: const Offset(0, 2))], ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.all(SpacingTokens.lg), + decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd)), child: const Icon(Icons.business_outlined, size: 24, color: Colors.white), ), - const SizedBox(width: 16), + const SizedBox(width: SpacingTokens.xl), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(org.nom, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), if (org.nomCourt?.isNotEmpty == true) ...[ - const SizedBox(height: 2), + const SizedBox(height: SpacingTokens.xs), Text(org.nomCourt!, style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(0.9))), ], - const SizedBox(height: 6), + const SizedBox(height: SpacingTokens.md), Row(children: [ _buildWhiteBadge(org.typeOrganisationLibelle ?? org.typeOrganisation), - const SizedBox(width: 6), + const SizedBox(width: SpacingTokens.md), _buildWhiteBadge(org.statutLibelle ?? org.statut.displayName), ]), ])), ]), if (org.description?.isNotEmpty == true) ...[ - const SizedBox(height: 12), + const SizedBox(height: SpacingTokens.lg), Text(org.description!, style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(0.9), height: 1.4)), ], - const SizedBox(height: 10), + const SizedBox(height: SpacingTokens.md), Row(children: [ _buildBoolBadge(Icons.public, 'Public', org.organisationPublique), - const SizedBox(width: 8), + const SizedBox(width: SpacingTokens.md), _buildBoolBadge(Icons.person_add, 'Ouvert', org.accepteNouveauxMembres), ]), ]), @@ -159,17 +168,19 @@ class _OrganizationDetailPageState extends State { Widget _buildWhiteBadge(String text) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: 3), + decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg)), child: Text(text, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white)), ); } Widget _buildBoolBadge(IconData icon, String label, bool value) { + // Sur gradient : vert clair lisible pour TRUE, blanc translucide pour FALSE + final color = value ? AppColors.successUI : Colors.white.withOpacity(0.6); return Row(mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 12, color: value ? Colors.greenAccent : Colors.white60), - const SizedBox(width: 4), - Text('$label: ${value ? 'Oui' : 'Non'}', style: TextStyle(fontSize: 11, color: value ? Colors.greenAccent : Colors.white60)), + Icon(icon, size: 12, color: color), + const SizedBox(width: SpacingTokens.sm), + Text('$label: ${value ? 'Oui' : 'Non'}', style: TextStyle(fontSize: 11, color: color)), ]); } @@ -178,14 +189,14 @@ class _OrganizationDetailPageState extends State { Widget _buildInfoCard(OrganizationModel org) { return _buildCard('Informations générales', Icons.info_outline, [ if (org.dateFondation != null) _buildInfoRow(Icons.cake, 'Date de fondation', _formatDate(org.dateFondation)), - if (org.dateFondation != null) const SizedBox(height: 10), + if (org.dateFondation != null) const SizedBox(height: SpacingTokens.md), if (org.ancienneteAnnees > 0) _buildInfoRow(Icons.access_time, 'Ancienneté', '${org.ancienneteAnnees} an(s)'), - if (org.ancienneteAnnees > 0) const SizedBox(height: 10), + if (org.ancienneteAnnees > 0) const SizedBox(height: SpacingTokens.md), if (org.numeroEnregistrement?.isNotEmpty == true) _buildInfoRow(Icons.assignment, 'N° d\'enregistrement', org.numeroEnregistrement!), - if (org.numeroEnregistrement?.isNotEmpty == true) const SizedBox(height: 10), + if (org.numeroEnregistrement?.isNotEmpty == true) const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.calendar_today, 'Créé dans le système', _formatDate(org.dateCreation)), if (org.dateModification != null) ...[ - const SizedBox(height: 10), + const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.edit_calendar, 'Dernière modification', _formatDate(org.dateModification)), ], ]); @@ -204,11 +215,11 @@ class _OrganizationDetailPageState extends State { return _buildCard('Contact', Icons.contact_phone, [ if (org.email?.isNotEmpty == true) _buildContactRow(Icons.email, 'Email', org.email!, onTap: () => _launchEmail(org.email!)), - if (org.emailSecondaire?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildContactRow(Icons.alternate_email, 'Email secondaire', org.emailSecondaire!, onTap: () => _launchEmail(org.emailSecondaire!))], - if (org.telephone?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildContactRow(Icons.phone, 'Téléphone', org.telephone!, onTap: () => _launchPhone(org.telephone!))], - if (org.telephoneSecondaire?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildContactRow(Icons.phone_forwarded, 'Téléphone secondaire', org.telephoneSecondaire!, onTap: () => _launchPhone(org.telephoneSecondaire!))], - if (org.siteWeb?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildContactRow(Icons.web, 'Site web', org.siteWeb!, onTap: () => _launchWebsite(org.siteWeb!))], - if (org.reseauxSociaux?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildInfoRow(Icons.share, 'Réseaux sociaux', org.reseauxSociaux!)], + if (org.emailSecondaire?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildContactRow(Icons.alternate_email, 'Email secondaire', org.emailSecondaire!, onTap: () => _launchEmail(org.emailSecondaire!))], + if (org.telephone?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildContactRow(Icons.phone, 'Téléphone', org.telephone!, onTap: () => _launchPhone(org.telephone!))], + if (org.telephoneSecondaire?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildContactRow(Icons.phone_forwarded, 'Téléphone secondaire', org.telephoneSecondaire!, onTap: () => _launchPhone(org.telephoneSecondaire!))], + if (org.siteWeb?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildContactRow(Icons.web, 'Site web', org.siteWeb!, onTap: () => _launchWebsite(org.siteWeb!))], + if (org.reseauxSociaux?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.share, 'Réseaux sociaux', org.reseauxSociaux!)], ]); } @@ -221,10 +232,10 @@ class _OrganizationDetailPageState extends State { Widget _buildAddressCard(OrganizationModel org) { return _buildCard('Localisation', Icons.location_on, [ if (org.adresse?.isNotEmpty == true) _buildInfoRow(Icons.location_on, 'Adresse', org.adresse!), - if (org.adresse?.isNotEmpty == true && (org.ville?.isNotEmpty == true)) const SizedBox(height: 10), + if (org.adresse?.isNotEmpty == true && (org.ville?.isNotEmpty == true)) const SizedBox(height: SpacingTokens.md), if (org.ville?.isNotEmpty == true) _buildInfoRow(Icons.location_city, 'Ville', '${org.ville!}${org.codePostal?.isNotEmpty == true ? ' — ${org.codePostal}' : ''}'), - if (org.region?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildInfoRow(Icons.map, 'Région', org.region!)], - if (org.pays?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildInfoRow(Icons.flag, 'Pays', org.pays!)], + if (org.region?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.map, 'Région', org.region!)], + if (org.pays?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.flag, 'Pays', org.pays!)], ]); } @@ -233,10 +244,10 @@ class _OrganizationDetailPageState extends State { Widget _buildStatsCard(OrganizationModel org) { return _buildCard('Statistiques', Icons.bar_chart, [ Row(children: [ - Expanded(child: _buildStatItem(Icons.people, 'Membres', (_memberCount ?? org.nombreMembres).toString(), AppColors.primaryGreen)), - const SizedBox(width: 10), - Expanded(child: _buildStatItem(Icons.admin_panel_settings, 'Admins', org.nombreAdministrateurs.toString(), AppColors.brandGreen)), - const SizedBox(width: 10), + Expanded(child: _buildStatItem(Icons.people, 'Membres', (_memberCount ?? org.nombreMembres).toString(), ModuleColors.organisations)), + const SizedBox(width: SpacingTokens.md), + Expanded(child: _buildStatItem(Icons.admin_panel_settings, 'Admins', org.nombreAdministrateurs.toString(), ModuleColors.organisationsDark)), + const SizedBox(width: SpacingTokens.md), Expanded(child: _buildStatItem(Icons.event, 'Événements', (org.nombreEvenements ?? 0).toString(), AppColors.success)), ]), ]); @@ -244,11 +255,11 @@ class _OrganizationDetailPageState extends State { Widget _buildStatItem(IconData icon, String label, String value, Color color) { return Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration(color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withOpacity(0.15))), + padding: const EdgeInsets.all(SpacingTokens.md), + decoration: BoxDecoration(color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), border: Border.all(color: color.withOpacity(0.15))), child: Column(children: [ Icon(icon, size: 20, color: color), - const SizedBox(height: 4), + const SizedBox(height: SpacingTokens.sm), Text(value, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: color)), Text(label, style: TextStyle(fontSize: 11, color: color)), ]), @@ -263,10 +274,10 @@ class _OrganizationDetailPageState extends State { Widget _buildFinancesCard(OrganizationModel org) { return _buildCard('Finances', Icons.account_balance_wallet, [ _buildInfoRow(Icons.currency_exchange, 'Devise', org.devise), - if (org.budgetAnnuel != null) ...[const SizedBox(height: 10), _buildInfoRow(Icons.account_balance, 'Budget annuel', '${_formatMontant(org.budgetAnnuel)} ${org.devise}')], - const SizedBox(height: 10), + if (org.budgetAnnuel != null) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.account_balance, 'Budget annuel', '${_formatMontant(org.budgetAnnuel)} ${org.devise}')], + const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.payments, 'Cotisation', org.cotisationObligatoire ? 'Obligatoire' : 'Facultative'), - if (org.cotisationObligatoire && org.montantCotisationAnnuelle != null) ...[const SizedBox(height: 10), _buildInfoRow(Icons.money, 'Montant annuel', '${_formatMontant(org.montantCotisationAnnuelle)} ${org.devise}')], + if (org.cotisationObligatoire && org.montantCotisationAnnuelle != null) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.money, 'Montant annuel', '${_formatMontant(org.montantCotisationAnnuelle)} ${org.devise}')], ]); } @@ -278,7 +289,7 @@ class _OrganizationDetailPageState extends State { Widget _buildMissionCard(OrganizationModel org) { return _buildCard('Mission & Activités', Icons.flag, [ if (org.objectifs?.isNotEmpty == true) _buildTextBlock(Icons.track_changes, 'Objectifs', org.objectifs!), - if (org.objectifs?.isNotEmpty == true && org.activitesPrincipales?.isNotEmpty == true) const SizedBox(height: 12), + if (org.objectifs?.isNotEmpty == true && org.activitesPrincipales?.isNotEmpty == true) const SizedBox(height: SpacingTokens.lg), if (org.activitesPrincipales?.isNotEmpty == true) _buildTextBlock(Icons.work, 'Activités principales', org.activitesPrincipales!), ]); } @@ -291,7 +302,7 @@ class _OrganizationDetailPageState extends State { Widget _buildSupplementaryCard(OrganizationModel org) { return _buildCard('Informations complémentaires', Icons.info, [ if (org.certifications?.isNotEmpty == true) _buildTextBlock(Icons.verified, 'Certifications / Agréments', org.certifications!), - if (org.certifications?.isNotEmpty == true && org.partenaires?.isNotEmpty == true) const SizedBox(height: 12), + if (org.certifications?.isNotEmpty == true && org.partenaires?.isNotEmpty == true) const SizedBox(height: SpacingTokens.lg), if (org.partenaires?.isNotEmpty == true) _buildTextBlock(Icons.handshake, 'Partenaires', org.partenaires!), ]); } @@ -299,20 +310,34 @@ class _OrganizationDetailPageState extends State { // ── Notes internes ────────────────────────────────────────────────────────── Widget _buildNotesCard(OrganizationModel org) { + final isDark = Theme.of(context).brightness == Brightness.dark; return Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( - color: const Color(0xFFFFFBEB), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFFCD34D), width: 1), + // Fond warning adaptatif (jaune clair light / surface sombre dark) + color: isDark + ? AppColors.surfaceVariantDark + : AppColors.warningContainer, + borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), + border: Border.all(color: AppColors.warningUI, width: 1), ), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.sticky_note_2, size: 18, color: Color(0xFFF59E0B)), - const SizedBox(width: 10), + const Icon(Icons.sticky_note_2, size: 18, color: AppColors.warningUI), + const SizedBox(width: SpacingTokens.md), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Notes internes', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFFF59E0B))), - const SizedBox(height: 4), - Text(org.notes!, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, height: 1.4)), + Text('Notes internes', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isDark ? AppColors.warningUI : AppColors.warning, + )), + const SizedBox(height: SpacingTokens.sm), + Text(org.notes!, + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurface, + height: 1.4, + )), ])), ]), ); @@ -337,15 +362,15 @@ class _OrganizationDetailPageState extends State { onPressed: _showEditPage, icon: const Icon(Icons.edit), label: const Text('Modifier'), - style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white), + style: ElevatedButton.styleFrom(backgroundColor: ModuleColors.organisations, foregroundColor: AppColors.onPrimary), )), if (isSuperAdmin) ...[ - const SizedBox(width: 12), + const SizedBox(width: SpacingTokens.lg), Expanded(child: OutlinedButton.icon( onPressed: () => _showDeleteConfirmation(org), icon: const Icon(Icons.delete), label: const Text('Supprimer'), - style: OutlinedButton.styleFrom(foregroundColor: Colors.red, side: const BorderSide(color: Colors.red)), + style: OutlinedButton.styleFrom(foregroundColor: AppColors.error, side: const BorderSide(color: AppColors.error)), )), ], ]), @@ -356,15 +381,19 @@ class _OrganizationDetailPageState extends State { Widget _buildCard(String title, IconData icon, List children) { return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.all(SpacingTokens.lg), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), + border: Border.all(color: Theme.of(context).colorScheme.outlineVariant, width: 1), + ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ - Icon(icon, size: 15, color: AppColors.primaryGreen), - const SizedBox(width: 6), - Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primaryGreen)), + Icon(icon, size: 15, color: ModuleColors.organisations), + const SizedBox(width: SpacingTokens.md), + Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: ModuleColors.organisations)), ]), - const SizedBox(height: 10), + const SizedBox(height: SpacingTokens.md), ...children, ]), ); @@ -372,12 +401,12 @@ class _OrganizationDetailPageState extends State { Widget _buildInfoRow(IconData icon, String label, String value) { return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, size: 18, color: AppColors.primaryGreen), - const SizedBox(width: 10), + Icon(icon, size: 18, color: ModuleColors.organisations), + const SizedBox(width: SpacingTokens.md), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight, fontWeight: FontWeight.w500)), - const SizedBox(height: 2), - Text(value, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, fontWeight: FontWeight.w600)), + Text(label, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500)), + const SizedBox(height: SpacingTokens.xs), + Text(value, style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600)), ])), ]); } @@ -385,29 +414,29 @@ class _OrganizationDetailPageState extends State { Widget _buildTextBlock(IconData icon, String label, String value) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ - Icon(icon, size: 15, color: AppColors.primaryGreen), - const SizedBox(width: 6), - Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondaryLight, fontWeight: FontWeight.w600)), + Icon(icon, size: 15, color: ModuleColors.organisations), + const SizedBox(width: SpacingTokens.md), + Text(label, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600)), ]), - const SizedBox(height: 4), - Text(value, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, height: 1.5)), + const SizedBox(height: SpacingTokens.sm), + Text(value, style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurface, height: 1.5)), ]); } Widget _buildContactRow(IconData icon, String label, String value, {VoidCallback? onTap}) { return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row(children: [ - Icon(icon, size: 18, color: AppColors.primaryGreen), - const SizedBox(width: 10), + Icon(icon, size: 18, color: ModuleColors.organisations), + const SizedBox(width: SpacingTokens.md), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight)), - Text(value, style: TextStyle(fontSize: 13, color: onTap != null ? AppColors.primaryGreen : AppColors.textPrimaryLight, fontWeight: FontWeight.w600, decoration: onTap != null ? TextDecoration.underline : null)), + Text(label, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant)), + Text(value, style: TextStyle(fontSize: 13, color: onTap != null ? ModuleColors.organisations : Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600, decoration: onTap != null ? TextDecoration.underline : null)), ])), - if (onTap != null) const Icon(Icons.open_in_new, size: 14, color: AppColors.primaryGreen), + if (onTap != null) const Icon(Icons.open_in_new, size: 14, color: ModuleColors.organisations), ]), ), ); @@ -416,28 +445,28 @@ class _OrganizationDetailPageState extends State { // ── États ──────────────────────────────────────────────────────────────────── Widget _buildLoading() => const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(AppColors.primaryGreen)), - SizedBox(height: 16), - Text('Chargement...', style: TextStyle(color: AppColors.textSecondaryLight)), + CircularProgressIndicator(color: ModuleColors.organisations), + SizedBox(height: SpacingTokens.xl), + Text('Chargement...'), ])); Widget _buildError(OrganizationsError state) => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 64, color: Colors.red.shade400), - const SizedBox(height: 16), - Text(state.message, textAlign: TextAlign.center, style: const TextStyle(color: AppColors.textSecondaryLight)), + Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.error), + const SizedBox(height: SpacingTokens.xl), + Text(state.message, textAlign: TextAlign.center, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)), const SizedBox(height: 24), - ElevatedButton.icon( + FilledButton.icon( onPressed: () => context.read().add(LoadOrganizationById(widget.organizationId)), - icon: const Icon(Icons.refresh), + icon: const Icon(Icons.refresh, size: 18), label: const Text('Réessayer'), - style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white), + style: FilledButton.styleFrom(backgroundColor: ModuleColors.organisations), ), ])); - Widget _buildEmpty() => const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.business_outlined, size: 64, color: AppColors.textSecondaryLight), - SizedBox(height: 16), - Text('Organisation non trouvée', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimaryLight)), + Widget _buildEmpty() => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.business_outlined, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), + const SizedBox(height: SpacingTokens.xl), + Text('Organisation non trouvée', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)), ])); // ── Actions ────────────────────────────────────────────────────────────────── @@ -475,8 +504,8 @@ class _OrganizationDetailPageState extends State { TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Annuler')), ElevatedButton( onPressed: () { Navigator.of(ctx).pop(); bloc.add(DeleteOrganization(widget.organizationId)); nav.pop(); }, - style: ElevatedButton.styleFrom(backgroundColor: Colors.red), - child: const Text('Supprimer', style: TextStyle(color: Colors.white)), + style: ElevatedButton.styleFrom(backgroundColor: AppColors.error), + child: const Text('Supprimer', style: TextStyle(color: AppColors.onPrimary)), ), ], ), diff --git a/lib/features/profile/presentation/pages/profile_page.dart b/lib/features/profile/presentation/pages/profile_page.dart index 858f6f7..64a6406 100644 --- a/lib/features/profile/presentation/pages/profile_page.dart +++ b/lib/features/profile/presentation/pages/profile_page.dart @@ -10,9 +10,11 @@ import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; +import '../../../../shared/design_system/tokens/color_tokens.dart'; import '../../../../shared/widgets/core_card.dart'; import '../../../../shared/widgets/info_badge.dart'; import '../../../../shared/widgets/mini_avatar.dart'; +import '../../../../core/config/environment.dart'; import '../../../../core/l10n/locale_provider.dart'; import '../../../../core/theme/theme_provider.dart'; import '../../../authentication/presentation/bloc/auth_bloc.dart'; @@ -26,7 +28,7 @@ import '../widgets/kyc_status_widget.dart'; import '../bloc/profile_bloc.dart'; /// Page Mon Profil - UnionFlow Mobile -/// +/// /// Page complète de gestion du profil utilisateur avec informations personnelles, /// préférences, sécurité, et paramètres avancés. class ProfilePage extends StatefulWidget { @@ -40,7 +42,7 @@ class _ProfilePageState extends State with TickerProviderStateMixin { late TabController _tabController; final _formKey = GlobalKey(); - + // Contrôleurs pour les champs de texte final _firstNameController = TextEditingController(); final _lastNameController = TextEditingController(); @@ -50,7 +52,7 @@ class _ProfilePageState extends State final _cityController = TextEditingController(); final _postalCodeController = TextEditingController(); final _bioController = TextEditingController(); - + // État du profil bool _isEditing = false; bool _isLoading = false; @@ -66,15 +68,22 @@ class _ProfilePageState extends State // Infos app (chargées via package_info_plus) String _appVersion = '...'; String _platformInfo = '...'; + String _osVersion = '...'; + String _envInfo = '...'; // Tailles stockage (calculées au chargement) String _cacheSize = '...'; String _imagesSize = '...'; String _offlineSize = '...'; + // Options développeur (persistées via SharedPreferences) + bool _devMode = false; + bool _detailedLogs = false; final List _themes = ['Système', 'Clair', 'Sombre']; static const _keyNotifPush = 'notif_push'; static const _keyNotifEmail = 'notif_email'; static const _keyNotifSon = 'notif_son'; + static const _keyDevMode = 'dev_mode'; + static const _keyDetailedLogs = 'detailed_logs'; @override void initState() { @@ -136,37 +145,51 @@ class _ProfilePageState extends State child: Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: UFAppBar( - title: 'MON PROFIL', - backgroundColor: Theme.of(context).cardColor, - foregroundColor: Theme.of(context).brightness == Brightness.dark - ? AppColors.textPrimaryDark - : AppColors.textPrimaryLight, + title: 'Mon Profil', + moduleGradient: ModuleColors.profilGradient, actions: [ IconButton( icon: Icon(_isEditing ? Icons.save_outlined : Icons.edit_outlined, size: 20), onPressed: () => _isEditing ? _saveProfile() : _startEditing(), ), ], + bottom: TabBar( + controller: _tabController, + isScrollable: true, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, + indicatorSize: TabBarIndicatorSize.label, + labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), + tabs: const [ + Tab(child: Text('PERSO')), + Tab(child: Text('PRÉFÉRENCES')), + Tab(child: Text('SÉCURITÉ')), + Tab(child: Text('AVANCÉ')), + ], + ), ), - body: ListView( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - children: [ - _buildHeader(), - const SizedBox(height: 8), - _buildTabBar(), - SizedBox( - height: 600, // Ajuster selon contenu ou utiliser NestedScrollView - child: TabBarView( - controller: _tabController, - children: [ - _buildPersonalInfoTab(), - _buildPreferencesTab(), - _buildSecurityTab(), - _buildAdvancedTab(), - ], + body: SafeArea( + top: false, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: _buildHeader(), ), - ), - ], + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPersonalInfoTab(), + _buildPreferencesTab(), + _buildSecurityTab(), + _buildAdvancedTab(), + ], + ), + ), + ], + ), ), ), ); @@ -176,6 +199,7 @@ class _ProfilePageState extends State Widget _buildHeader() { return BlocBuilder( builder: (context, state) { + final scheme = Theme.of(context).colorScheme; final membre = (state is ProfileLoaded) ? state.membre : (state is ProfileUpdated ? state.membre @@ -189,39 +213,39 @@ class _ProfilePageState extends State Color badgeColor; if (isLoading) { badgeText = 'CHARGEMENT...'; - badgeColor = AppColors.textSecondaryLight; + badgeColor = ColorTokens.textSecondary; } else if (hasError) { badgeText = 'ERREUR CHARGEMENT'; - badgeColor = AppColors.error; + badgeColor = ColorTokens.error; } else if (membre != null) { // Priorité au rôle s'il est disponible if (membre.role != null && membre.role!.isNotEmpty) { badgeText = membre.role!.replaceAll('_', ' '); - badgeColor = AppColors.primaryGreen; + badgeColor = ModuleColors.profil; } else { // Fallback sur le statut switch (membre.statut) { case StatutMembre.actif: badgeText = membre.cotisationAJour ? 'MEMBRE ACTIF' : 'COTISATION EN RETARD'; - badgeColor = membre.cotisationAJour ? AppColors.success : AppColors.warning; + badgeColor = membre.cotisationAJour ? ColorTokens.success : ColorTokens.warning; break; case StatutMembre.inactif: badgeText = 'INACTIF'; - badgeColor = AppColors.textSecondaryLight; + badgeColor = ColorTokens.textSecondary; break; case StatutMembre.suspendu: badgeText = 'SUSPENDU'; - badgeColor = AppColors.error; + badgeColor = ColorTokens.error; break; case StatutMembre.enAttente: badgeText = 'EN ATTENTE'; - badgeColor = AppColors.warning; + badgeColor = ColorTokens.warning; break; } } } else { badgeText = 'CHARGEMENT...'; - badgeColor = AppColors.textSecondaryLight; + badgeColor = ColorTokens.textSecondary; } // Ancienneté réelle @@ -274,7 +298,7 @@ class _ProfilePageState extends State ), Text( _emailController.text.toLowerCase(), - style: AppTypography.subtitleSmall.copyWith(fontSize: 11), + style: AppTypography.subtitleSmall.copyWith(fontSize: 11, color: scheme.onSurfaceVariant), ), const SizedBox(height: 8), if (isLoading) @@ -291,7 +315,7 @@ class _ProfilePageState extends State child: Text( 'Réessayer', style: AppTypography.subtitleSmall.copyWith( - color: AppColors.primaryGreen, + color: ModuleColors.profil, fontSize: 10, decoration: TextDecoration.underline, ), @@ -344,10 +368,11 @@ class _ProfilePageState extends State } Widget _buildStatItem(String label, String value) { + final scheme = Theme.of(context).colorScheme; return Column( children: [ Text(value, style: AppTypography.headerSmall.copyWith(fontSize: 14)), - Text(label, style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)), + Text(label, style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold, color: scheme.onSurfaceVariant)), ], ); } @@ -388,35 +413,16 @@ class _ProfilePageState extends State ); } - /// Barre d'onglets - Widget _buildTabBar() { - final isDark = Theme.of(context).brightness == Brightness.dark; - return Container( - decoration: BoxDecoration( - color: isDark ? AppColors.darkSurface : AppColors.lightSurface, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), - ), - child: TabBar( - controller: _tabController, - labelColor: AppColors.primaryGreen, - unselectedLabelColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, - indicatorColor: AppColors.primaryGreen, - indicatorSize: TabBarIndicatorSize.label, - labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), - tabs: const [ - Tab(text: 'PERSO'), - Tab(text: 'PRÉF'), - Tab(text: 'SÉCU'), - Tab(text: 'AVANCÉ'), - ], - ), - ); - } + // _buildTabBar() supprimé : migré dans UFAppBar.bottom (pattern Adhésions) /// Onglet informations personnelles Widget _buildPersonalInfoTab() { - return SingleChildScrollView( + return RefreshIndicator( + color: ModuleColors.profil, + onRefresh: () async => + context.read().add(const LoadMe()), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), child: Column( children: [ @@ -533,8 +539,9 @@ class _ProfilePageState extends State const SizedBox(height: 80), ], - ), - ); + ), // Column + ), // SingleChildScrollView + ); // RefreshIndicator } /// Section d'informations @@ -551,7 +558,7 @@ class _ProfilePageState extends State children: [ Row( children: [ - Icon(icon, color: AppColors.primaryGreen, size: 16), + Icon(icon, color: ModuleColors.profil, size: 16), const SizedBox(width: 8), Text( title.toUpperCase(), @@ -579,7 +586,7 @@ class _ProfilePageState extends State int maxLines = 1, String? hintText, }) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final scheme = Theme.of(context).colorScheme; return TextFormField( controller: controller, enabled: enabled, @@ -587,35 +594,34 @@ class _ProfilePageState extends State maxLines: maxLines, style: AppTypography.bodyTextSmall.copyWith( fontSize: 12, - color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight, ), decoration: InputDecoration( labelText: label.toUpperCase(), labelStyle: AppTypography.subtitleSmall.copyWith( fontSize: 9, fontWeight: FontWeight.bold, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + color: scheme.onSurfaceVariant, ), hintText: hintText, - prefixIcon: Icon(icon, color: enabled ? AppColors.primaryGreen : AppColors.textSecondaryLight, size: 16), + prefixIcon: Icon(icon, color: enabled ? ModuleColors.profil : ColorTokens.textSecondary, size: 16), filled: true, - fillColor: isDark ? AppColors.darkSurface : AppColors.lightSurface, + fillColor: scheme.surface, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), border: OutlineInputBorder( borderRadius: BorderRadius.circular(4), - borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder), + borderSide: BorderSide(color: scheme.outlineVariant), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), - borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder), + borderSide: BorderSide(color: scheme.outlineVariant), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), - borderSide: const BorderSide(color: AppColors.primaryGreen, width: 1), + borderSide: BorderSide(color: ModuleColors.profil, width: 1), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), - borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), + borderSide: BorderSide(color: scheme.outlineVariant, width: 0.5), ), ), validator: (value) { @@ -631,15 +637,15 @@ class _ProfilePageState extends State /// Boutons d'action Widget _buildActionButtons() { - final isDark = Theme.of(context).brightness == Brightness.dark; + final scheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: isDark ? AppColors.darkSurface : Colors.white, + color: scheme.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: AppColors.shadow, blurRadius: 10, offset: const Offset(0, 2), ), @@ -652,8 +658,8 @@ class _ProfilePageState extends State child: ElevatedButton.icon( onPressed: _isLoading ? null : _cancelEditing, style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[100], - foregroundColor: Colors.grey[700], + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + foregroundColor: AppColors.textSecondary, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), ), @@ -666,7 +672,7 @@ class _ProfilePageState extends State child: ElevatedButton.icon( onPressed: _isLoading ? null : _saveProfile, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, + backgroundColor: ModuleColors.profil, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), @@ -689,7 +695,7 @@ class _ProfilePageState extends State child: ElevatedButton.icon( onPressed: _startEditing, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, + backgroundColor: ModuleColors.profil, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), @@ -720,9 +726,7 @@ class _ProfilePageState extends State [ Builder( builder: (ctx) { - final isDark = Theme.of(ctx).brightness == Brightness.dark; - final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; - final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; + final scheme = Theme.of(ctx).colorScheme; return InkWell( onTap: () async { await Navigator.of(context).push( @@ -738,18 +742,18 @@ class _ProfilePageState extends State padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ - Icon(Icons.language, color: textSecondary, size: 22), + Icon(Icons.language, color: scheme.onSurfaceVariant, size: 22), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), - Text('Actuellement : $_selectedLanguage', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)), + Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)), + Text('Actuellement : $_selectedLanguage', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)), ], ), ), - Icon(Icons.chevron_right, color: textSecondary, size: 20), + Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20), ], ), ), @@ -823,9 +827,7 @@ class _ProfilePageState extends State [ Builder( builder: (ctx) { - final isDark = Theme.of(ctx).brightness == Brightness.dark; - final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; - final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; + final scheme = Theme.of(ctx).colorScheme; return InkWell( onTap: () { Navigator.of(context).push( @@ -837,18 +839,18 @@ class _ProfilePageState extends State padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ - Icon(Icons.privacy_tip, color: textSecondary, size: 22), + Icon(Icons.privacy_tip, color: scheme.onSurfaceVariant, size: 22), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Gérer la confidentialité', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), - Text('Visibilité, partage de données, suppression de compte', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)), + Text('Gérer la confidentialité', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)), + Text('Visibilité, partage de données, suppression de compte', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)), ], ), ), - Icon(Icons.chevron_right, color: textSecondary, size: 20), + Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20), ], ), ), @@ -950,21 +952,21 @@ class _ProfilePageState extends State 'Télécharger mes données', 'Exporter toutes vos données personnelles', Icons.download, - AppColors.primaryGreen, + ModuleColors.profil, () => _exportUserData(), ), _buildActionItem( 'Déconnecter tous les appareils', 'Fermer toutes les sessions actives', Icons.logout, - AppColors.warning, + ColorTokens.warning, () => _logoutAllDevices(), ), _buildActionItem( 'Supprimer mon compte', 'Action irréversible - toutes les données seront perdues', Icons.delete_forever, - Colors.red, + ColorTokens.error, () => _showDeleteAccountDialog(), ), ], @@ -1019,14 +1021,22 @@ class _ProfilePageState extends State _buildSwitchPreference( 'Mode développeur', 'Afficher les options de débogage', - false, - (value) => _showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}'), + _devMode, + (value) async { + setState(() => _devMode = value); + await _savePreference(_keyDevMode, value); + _showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}'); + }, ), _buildSwitchPreference( 'Logs détaillés', - 'Enregistrer plus d\'informations de débogage', - false, - (value) => _showSuccessSnackBar('Logs détaillés ${value ? 'activés' : 'désactivés'}'), + 'Logs réseau et erreurs — actif au prochain démarrage', + _detailedLogs, + (value) async { + setState(() => _detailedLogs = value); + await _savePreference(_keyDetailedLogs, value); + _showSuccessSnackBar('Logs détaillés ${value ? 'activés' : 'désactivés'} (prochain démarrage)'); + }, ), ], ), @@ -1041,6 +1051,9 @@ class _ProfilePageState extends State [ _buildInfoItem('Version de l\'app', _appVersion), _buildInfoItem('Plateforme', _platformInfo), + _buildInfoItem('Système', _osVersion), + _buildInfoItem('Environnement', _envInfo), + if (_devMode) _buildInfoItem('API URL', AppConfig.apiBaseUrl), ], ), @@ -1054,9 +1067,7 @@ class _ProfilePageState extends State [ Builder( builder: (ctx) { - final isDark = Theme.of(ctx).brightness == Brightness.dark; - final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; - final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; + final scheme = Theme.of(ctx).colorScheme; return InkWell( onTap: () { Navigator.of(context).push( @@ -1068,18 +1079,18 @@ class _ProfilePageState extends State padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ - Icon(Icons.edit_note, color: textSecondary, size: 22), + Icon(Icons.edit_note, color: scheme.onSurfaceVariant, size: 22), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Envoyer des commentaires', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), - Text('Suggestions, bugs ou idées d\'amélioration', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)), + Text('Envoyer des commentaires', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)), + Text('Suggestions, bugs ou idées d\'amélioration', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)), ], ), ), - Icon(Icons.chevron_right, color: textSecondary, size: 20), + Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20), ], ), ), @@ -1113,7 +1124,7 @@ class _ProfilePageState extends State children: [ Row( children: [ - Icon(icon, color: AppColors.primaryGreen, size: 16), + Icon(icon, color: ModuleColors.profil, size: 16), const SizedBox(width: 8), Text( title.toUpperCase(), @@ -1159,7 +1170,7 @@ class _ProfilePageState extends State List options, Function(String?) onChanged, ) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final scheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1171,21 +1182,31 @@ class _ProfilePageState extends State Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: isDark ? AppColors.darkSurface : AppColors.lightSurface, + color: scheme.surface, borderRadius: BorderRadius.circular(4), - border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), + border: Border.all(color: scheme.outlineVariant, width: 0.5), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isExpanded: true, onChanged: onChanged, - icon: const Icon(Icons.arrow_drop_down, color: AppColors.primaryGreen, size: 18), - style: AppTypography.bodyTextSmall.copyWith(fontSize: 12), + dropdownColor: scheme.surface, + icon: Icon(Icons.arrow_drop_down, color: ModuleColors.profil, size: 18), + style: AppTypography.bodyTextSmall.copyWith( + fontSize: 12, + color: scheme.onSurface, + ), items: options.map((option) { return DropdownMenuItem( value: option, - child: Text(option), + child: Text( + option, + style: AppTypography.bodyTextSmall.copyWith( + fontSize: 12, + color: scheme.onSurface, + ), + ), ); }).toList(), ), @@ -1202,7 +1223,7 @@ class _ProfilePageState extends State bool value, Function(bool) onChanged, ) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final scheme = Theme.of(context).colorScheme; return Row( children: [ Expanded( @@ -1217,7 +1238,7 @@ class _ProfilePageState extends State subtitle, style: AppTypography.bodyTextSmall.copyWith( fontSize: 10, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + color: scheme.onSurfaceVariant, ), ), ], @@ -1228,7 +1249,7 @@ class _ProfilePageState extends State child: Switch( value: value, onChanged: onChanged, - activeColor: AppColors.primaryGreen, + activeColor: ModuleColors.profil, ), ), ], @@ -1242,21 +1263,20 @@ class _ProfilePageState extends State IconData icon, VoidCallback onTap, ) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; + final scheme = Theme.of(context).colorScheme; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( - color: isDark ? AppColors.darkSurface : AppColors.lightSurface, + color: scheme.surface, borderRadius: BorderRadius.circular(4), - border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), + border: Border.all(color: scheme.outlineVariant, width: 0.5), ), child: Row( children: [ - Icon(icon, color: AppColors.primaryGreen, size: 16), + Icon(icon, color: ModuleColors.profil, size: 16), const SizedBox(width: 12), Expanded( child: Column( @@ -1268,12 +1288,12 @@ class _ProfilePageState extends State ), Text( subtitle, - style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: textSecondary), + style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant), ), ], ), ), - Icon(Icons.arrow_forward_ios, color: textSecondary, size: 12), + Icon(Icons.arrow_forward_ios, color: scheme.onSurfaceVariant, size: 12), ], ), ), @@ -1287,19 +1307,18 @@ class _ProfilePageState extends State IconData icon, bool isCurrentDevice, ) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; + final scheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isCurrentDevice - ? AppColors.primaryGreen.withOpacity(0.05) - : (isDark ? AppColors.darkSurface : AppColors.lightSurface), + ? ModuleColors.profil.withOpacity(0.05) + : scheme.surface, borderRadius: BorderRadius.circular(4), border: Border.all( color: isCurrentDevice - ? AppColors.primaryGreen.withOpacity(0.3) - : (isDark ? AppColors.darkBorder : AppColors.lightBorder), + ? ModuleColors.profil.withOpacity(0.3) + : scheme.outlineVariant, width: 0.5, ), ), @@ -1307,7 +1326,7 @@ class _ProfilePageState extends State children: [ Icon( icon, - color: isCurrentDevice ? AppColors.primaryGreen : textSecondary, + color: isCurrentDevice ? ModuleColors.profil : scheme.onSurfaceVariant, size: 16, ), const SizedBox(width: 12), @@ -1323,13 +1342,13 @@ class _ProfilePageState extends State ), if (isCurrentDevice) ...[ const SizedBox(width: 8), - const InfoBadge(text: 'ACTUEL', backgroundColor: AppColors.primaryGreen), + InfoBadge(text: 'ACTUEL', backgroundColor: ModuleColors.profil), ], ], ), Text( subtitle, - style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: textSecondary), + style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant), ), ], ), @@ -1347,7 +1366,7 @@ class _ProfilePageState extends State Color color, VoidCallback onTap, ) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final scheme = Theme.of(context).colorScheme; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(4), @@ -1374,7 +1393,7 @@ class _ProfilePageState extends State subtitle, style: AppTypography.bodyTextSmall.copyWith( fontSize: 10, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + color: scheme.onSurfaceVariant, ), ), ], @@ -1389,20 +1408,20 @@ class _ProfilePageState extends State /// Élément de stockage Widget _buildStorageItem(String title, String size, VoidCallback onTap) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final scheme = Theme.of(context).colorScheme; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( - color: isDark ? AppColors.darkSurface : AppColors.lightSurface, + color: scheme.surface, borderRadius: BorderRadius.circular(4), - border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), + border: Border.all(color: scheme.outlineVariant, width: 0.5), ), child: Row( children: [ - const Icon(Icons.folder_outlined, color: AppColors.primaryGreen, size: 16), + Icon(Icons.folder_outlined, color: ModuleColors.profil, size: 16), const SizedBox(width: 12), Expanded( child: Text( @@ -1412,10 +1431,10 @@ class _ProfilePageState extends State ), Text( size, - style: AppTypography.subtitleSmall.copyWith(fontSize: 10), + style: AppTypography.subtitleSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant), ), const SizedBox(width: 8), - const Icon(Icons.clear, color: AppColors.error, size: 14), + Icon(Icons.clear, color: ColorTokens.error, size: 14), ], ), ), @@ -1424,20 +1443,20 @@ class _ProfilePageState extends State /// Élément d'information Widget _buildInfoItem(String title, String value) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final scheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: isDark ? AppColors.darkSurface : AppColors.lightSurface, + color: scheme.surface, borderRadius: BorderRadius.circular(4), - border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), + border: Border.all(color: scheme.outlineVariant, width: 0.5), ), child: Row( children: [ Expanded( child: Text( title.toUpperCase(), - style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold), + style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold, color: scheme.onSurfaceVariant), ), ), Text( @@ -1565,7 +1584,7 @@ class _ProfilePageState extends State ); } - /// Charger les préférences notifications depuis SharedPreferences + /// Charger les préférences depuis SharedPreferences Future _loadPreferences() async { final prefs = await SharedPreferences.getInstance(); if (mounted) { @@ -1573,6 +1592,8 @@ class _ProfilePageState extends State _notifPush = prefs.getBool(_keyNotifPush) ?? true; _notifEmail = prefs.getBool(_keyNotifEmail) ?? false; _notifSon = prefs.getBool(_keyNotifSon) ?? true; + _devMode = prefs.getBool(_keyDevMode) ?? AppConfig.enableDebugMode; + _detailedLogs = prefs.getBool(_keyDetailedLogs) ?? AppConfig.enableLogging; }); } } @@ -1588,7 +1609,7 @@ class _ProfilePageState extends State final info = await PackageInfo.fromPlatform(); if (mounted) { setState(() { - _appVersion = '${info.version} (Build ${info.buildNumber})'; + _appVersion = '${info.version} (build ${info.buildNumber})'; final os = kIsWeb ? 'Web' : Platform.isAndroid @@ -1597,6 +1618,10 @@ class _ProfilePageState extends State ? 'iOS' : Platform.operatingSystem; _platformInfo = '$os · ${info.appName}'; + _osVersion = kIsWeb + ? 'Navigateur web' + : Platform.operatingSystemVersion; + _envInfo = AppConfig.environment.name.toUpperCase(); }); } } @@ -1684,7 +1709,7 @@ class _ProfilePageState extends State confirmPassCtrl.dispose(); }, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, + backgroundColor: ModuleColors.profil, foregroundColor: Colors.white, ), child: const Text('Modifier'), @@ -1782,7 +1807,7 @@ class _ProfilePageState extends State _showFinalDeleteConfirmation(); }, style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, + backgroundColor: ColorTokens.error, foregroundColor: Colors.white, ), child: const Text('Continuer'), @@ -1836,7 +1861,7 @@ class _ProfilePageState extends State confirmCtrl.dispose(); }, style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, + backgroundColor: ColorTokens.error, foregroundColor: Colors.white, ), child: const Text('SUPPRIMER DÉFINITIVEMENT'), @@ -1953,7 +1978,7 @@ class _ProfilePageState extends State message.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), ), - backgroundColor: AppColors.success, + backgroundColor: ColorTokens.success, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), ), @@ -1967,7 +1992,7 @@ class _ProfilePageState extends State message.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), ), - backgroundColor: AppColors.error, + backgroundColor: ColorTokens.error, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), ), diff --git a/lib/features/reports/presentation/pages/reports_page.dart b/lib/features/reports/presentation/pages/reports_page.dart index cd3c20b..729537e 100644 --- a/lib/features/reports/presentation/pages/reports_page.dart +++ b/lib/features/reports/presentation/pages/reports_page.dart @@ -58,7 +58,7 @@ class _ReportsPageState extends State } if (state is ReportsError) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.message), backgroundColor: Colors.orange), + SnackBar(content: Text(state.message), backgroundColor: AppColors.warning), ); } if (state is ReportScheduled) { @@ -74,184 +74,75 @@ class _ReportsPageState extends State }, builder: (context, state) { return Scaffold( - backgroundColor: AppColors.lightBackground, - body: Column( - children: [ - _buildHeader(), - _buildTabBar(), - if (state is ReportsLoading) - const LinearProgressIndicator( - minHeight: 2, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation(AppColors.primaryGreen), - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildOverviewTab(), - _buildMembersTab(), - _buildOrganizationsTab(), - _buildEventsTab(), - ], - ), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: UFAppBar( + title: 'Rapports & Analytics', + moduleGradient: ModuleColors.rapportsGradient, + actions: [ + IconButton( + onPressed: () => _showExportDialog(), + icon: const Icon(Icons.file_download_outlined, size: 20), + tooltip: 'Exporter', ), ], + bottom: TabBar( + controller: _tabController, + isScrollable: true, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, + indicatorSize: TabBarIndicatorSize.label, + labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), + tabs: const [ + Tab(child: Text('GLOBAL')), + Tab(child: Text('MEMBRES')), + Tab(child: Text('ORGS')), + Tab(child: Text('EVENTS')), + ], + ), + ), + body: SafeArea( + top: false, + child: Column( + children: [ + if (state is ReportsLoading) + const LinearProgressIndicator( + minHeight: 2, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation(AppColors.primary), + ), + Expanded( + child: RefreshIndicator( + color: ModuleColors.rapports, + onRefresh: () async => context + .read() + .add(const LoadDashboardReports()), + child: TabBarView( + controller: _tabController, + children: [ + _buildOverviewTab(), + _buildMembersTab(), + _buildOrganizationsTab(), + _buildEventsTab(), + ], + ), + ), + ), + ], + ), ), ); }, ); } - Widget _buildHeader() { - return Container( - width: double.infinity, - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + 12, - bottom: 16, - left: 12, - right: 12, - ), - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppColors.primaryGreen, - AppColors.brandGreen, - ], - ), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(32), - bottomRight: Radius.circular(32), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'UnionFlow Analytics'.toUpperCase(), - style: AppTypography.subtitleSmall.copyWith( - color: Colors.white.withOpacity(0.8), - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - ), - const SizedBox(height: 4), - const Text( - 'Rapports & Insights', - style: AppTypography.headerSmall, - ), - ], - ), - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: IconButton( - onPressed: () => _showExportDialog(), - icon: const Icon(Icons.file_download_outlined, color: Colors.white), - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildHeaderStat( - 'Membres', - _statsMembres['total']?.toString() ?? '...', - Icons.people_outline, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildHeaderStat( - 'Organisations', - _statsMembres['totalOrganisations']?.toString() ?? '...', - Icons.business_outlined, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildHeaderStat( - 'Événements', - _statsEvenements['total']?.toString() ?? '...', - Icons.event_outlined, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildHeaderStat(String label, String value, IconData icon) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white.withOpacity(0.2)), - ), - child: Column( - children: [ - Icon(icon, color: Colors.white, size: 18), - const SizedBox(height: 8), - Text( - value, - style: AppTypography.headerSmall.copyWith(fontSize: 18), - ), - Text( - label.toUpperCase(), - style: AppTypography.subtitleSmall.copyWith( - color: Colors.white.withOpacity(0.7), - fontSize: 8, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } - - Widget _buildTabBar() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: AppColors.lightBackground, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)), - ), - child: TabBar( - controller: _tabController, - labelColor: AppColors.primaryGreen, - unselectedLabelColor: AppColors.textSecondaryLight, - indicatorColor: AppColors.primaryGreen, - indicatorSize: TabBarIndicatorSize.label, - dividerColor: Colors.transparent, - labelStyle: AppTypography.badgeText.copyWith(fontWeight: FontWeight.bold), - tabs: const [ - Tab(text: 'GLOBAL'), - Tab(text: 'MEMBRES'), - Tab(text: 'ORGS'), - Tab(text: 'EVENTS'), - ], - ), - ); - } + // _buildHeader(), _buildHeaderStat() et _buildTabBar() supprimés : + // titre + action Export migrés dans UFAppBar, TabBar dans UFAppBar.bottom. + // Les KPIs sont toujours disponibles dans _buildKPICards() (onglet Global). Widget _buildOverviewTab() { return ListView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), children: [ _buildKPICards(), @@ -282,7 +173,7 @@ class _ReportsPageState extends State const SizedBox(height: 8), Row( children: [ - Expanded(child: _buildKPICard('Cotisations', totalCotisations, Icons.payments_outlined, AppColors.brandGreen)), + Expanded(child: _buildKPICard('Cotisations', totalCotisations, Icons.payments_outlined, AppColors.primaryDark)), const SizedBox(width: 8), Expanded(child: _buildKPICard('Événements', totalEvenements, Icons.event_available_outlined, AppColors.warning)), ], @@ -332,7 +223,7 @@ class _ReportsPageState extends State children: [ Row( children: [ - const Icon(Icons.analytics_outlined, color: AppColors.primaryGreen, size: 20), + const Icon(Icons.analytics_outlined, color: AppColors.primary, size: 20), const SizedBox(width: 8), Text( 'Évolution de l\'Activité'.toUpperCase(), @@ -341,26 +232,29 @@ class _ReportsPageState extends State ], ), const SizedBox(height: 10), - Container( - height: 180, - decoration: BoxDecoration( - color: AppColors.lightBorder.withOpacity(0.1), - borderRadius: BorderRadius.circular(16), - ), - child: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.auto_graph_outlined, color: AppColors.textSecondaryLight, size: 40), - SizedBox(height: 12), - Text( - 'Visualisation graphique en préparation', - style: AppTypography.subtitleSmall, - ), - ], + Builder(builder: (context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Container( + height: 180, + decoration: BoxDecoration( + color: (isDark ? AppColors.borderDark : AppColors.border).withOpacity(0.15), + borderRadius: BorderRadius.circular(16), ), - ), - ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.auto_graph_outlined, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary, + size: 40), + const SizedBox(height: 12), + Text('Visualisation graphique en préparation', + style: AppTypography.subtitleSmall), + ], + ), + ), + ); + }), ], ), ); @@ -400,19 +294,19 @@ class _ReportsPageState extends State child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: AppColors.lightBorder.withOpacity(0.05), + color: AppColors.border.withOpacity(0.05), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)), + border: Border.all(color: AppColors.border.withOpacity(0.1)), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: AppColors.primaryGreen.withOpacity(0.1), + color: AppColors.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), - child: Icon(icon, color: AppColors.primaryGreen, size: 20), + child: Icon(icon, color: AppColors.primary, size: 20), ), const SizedBox(width: 16), Expanded( @@ -425,7 +319,11 @@ class _ReportsPageState extends State ], ), ), - const Icon(Icons.chevron_right_outlined, color: AppColors.textSecondaryLight, size: 20), + Icon(Icons.chevron_right_outlined, + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.textSecondaryDark + : AppColors.textSecondary, + size: 20), ], ), ), @@ -436,6 +334,7 @@ class _ReportsPageState extends State /// Onglet membres Widget _buildMembersTab() { return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), child: Column( children: [ @@ -473,9 +372,9 @@ class _ReportsPageState extends State Row( children: [ Expanded(child: _buildStatItem('Total', total)), - Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)), + Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)), Expanded(child: _buildStatItem('Nouveaux', nouveaux)), - Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)), + Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)), Expanded(child: _buildStatItem('Actifs %', actifs)), ], ), @@ -512,6 +411,7 @@ class _ReportsPageState extends State /// Onglet organisations Widget _buildOrganizationsTab() { return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), child: Column( children: [ @@ -537,7 +437,7 @@ class _ReportsPageState extends State children: [ Row( children: [ - const Icon(Icons.business_center_outlined, color: AppColors.primaryGreen, size: 20), + const Icon(Icons.business_center_outlined, color: AppColors.primary, size: 20), const SizedBox(width: 12), Text( 'Indicateurs Organisations'.toUpperCase(), @@ -549,9 +449,9 @@ class _ReportsPageState extends State Row( children: [ Expanded(child: _buildStatItem('Total', total)), - Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)), + Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)), Expanded(child: _buildStatItem('Actives', actives)), - Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)), + Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)), Expanded(child: _buildStatItem('Membres moy.', moy)), ], ), @@ -568,7 +468,7 @@ class _ReportsPageState extends State children: [ Row( children: [ - const Icon(Icons.folder_shared_outlined, color: AppColors.primaryGreen, size: 20), + const Icon(Icons.folder_shared_outlined, color: AppColors.primary, size: 20), const SizedBox(width: 12), Text( 'Rapports Structures'.toUpperCase(), @@ -588,6 +488,7 @@ class _ReportsPageState extends State /// Onglet événements Widget _buildEventsTab() { return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), child: Column( children: [ @@ -625,9 +526,9 @@ class _ReportsPageState extends State Row( children: [ Expanded(child: _buildStatItem('Total', total)), - Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)), + Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)), Expanded(child: _buildStatItem('À Venir', venir)), - Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)), + Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)), Expanded(child: _buildStatItem('Part. moyenne', participation)), ], ), @@ -667,7 +568,7 @@ class _ReportsPageState extends State children: [ Text( value, - style: AppTypography.headerSmall.copyWith(color: AppColors.primaryGreen, fontWeight: FontWeight.bold), + style: AppTypography.headerSmall.copyWith(color: AppColors.primary, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( @@ -688,19 +589,19 @@ class _ReportsPageState extends State child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppColors.lightBorder.withOpacity(0.05), + color: AppColors.border.withOpacity(0.05), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)), + border: Border.all(color: AppColors.border.withOpacity(0.1)), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: AppColors.primaryGreen.withOpacity(0.1), + color: AppColors.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), - child: Icon(icon, color: AppColors.primaryGreen, size: 20), + child: Icon(icon, color: AppColors.primary, size: 20), ), const SizedBox(width: 16), Expanded( @@ -713,7 +614,11 @@ class _ReportsPageState extends State ], ), ), - const Icon(Icons.file_download_outlined, color: AppColors.textSecondaryLight, size: 20), + Icon(Icons.file_download_outlined, + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.textSecondaryDark + : AppColors.textSecondary, + size: 20), ], ), ), @@ -752,7 +657,7 @@ class _ReportsPageState extends State Navigator.of(context).pop(); context.read().add(GenerateReportRequested('export', format: _selectedFormat)); }, - style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white), + style: ElevatedButton.styleFrom(backgroundColor: AppColors.primary, foregroundColor: AppColors.onPrimary), child: const Text('Exporter'), ), ], diff --git a/lib/features/solidarity/presentation/pages/demande_aide_detail_page.dart b/lib/features/solidarity/presentation/pages/demande_aide_detail_page.dart index 476ebd8..eb906b5 100644 --- a/lib/features/solidarity/presentation/pages/demande_aide_detail_page.dart +++ b/lib/features/solidarity/presentation/pages/demande_aide_detail_page.dart @@ -28,18 +28,19 @@ class _DemandeAideDetailPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.background, - appBar: const UFAppBar( - title: 'DÉTAIL DEMANDE', - backgroundColor: AppColors.surface, - foregroundColor: AppColors.textPrimaryLight, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: UFAppBar( + title: 'Détail Demande', + moduleGradient: ModuleColors.solidariteGradient, ), - body: BlocConsumer( + body: SafeArea( + top: false, + child: BlocConsumer( listenWhen: (prev, curr) => prev.status != curr.status, listener: (context, state) { if (state.status == SolidarityStatus.error && state.message != null) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.message!), backgroundColor: Colors.red), + SnackBar(content: Text(state.message!), backgroundColor: AppColors.error), ); } }, @@ -55,7 +56,7 @@ class _DemandeAideDetailPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.error_outline, size: 64, color: Colors.grey), + const Icon(Icons.error_outline, size: 64, color: AppColors.textTertiary), const SizedBox(height: 16), Text( 'Demande introuvable', @@ -65,7 +66,12 @@ class _DemandeAideDetailPageState extends State { ), ); } - return SingleChildScrollView( + return RefreshIndicator( + color: ModuleColors.solidarite, + onRefresh: () async => + context.read().add(LoadDemandeAideById(widget.demandeId)), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -97,10 +103,12 @@ class _DemandeAideDetailPageState extends State { _ActionsSection(demande: d, isGestionnaire: _isGestionnaire()), ], ), - ); + ), // SingleChildScrollView + ); // RefreshIndicator }, - ), - ); + ), // BlocConsumer + ), // SafeArea + ); // Scaffold } bool _isGestionnaire() { @@ -136,7 +144,7 @@ class _InfoCard extends StatelessWidget { style: AppTypography.subtitleSmall.copyWith( fontWeight: FontWeight.bold, fontSize: 9, - color: AppColors.textSecondaryLight, + color: AppColors.textSecondary, ), ), const SizedBox(height: 2), diff --git a/lib/features/solidarity/presentation/pages/demandes_aide_page.dart b/lib/features/solidarity/presentation/pages/demandes_aide_page.dart index 9a403e0..7d484ba 100644 --- a/lib/features/solidarity/presentation/pages/demandes_aide_page.dart +++ b/lib/features/solidarity/presentation/pages/demandes_aide_page.dart @@ -76,17 +76,16 @@ class _DemandesAidePageState extends State } }, child: Scaffold( - backgroundColor: AppColors.background, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: UFAppBar( - title: 'SOLIDARITÉ', - backgroundColor: AppColors.surface, - foregroundColor: AppColors.textPrimaryLight, + title: 'Solidarité', + moduleGradient: ModuleColors.solidariteGradient, bottom: TabBar( controller: _tabController, onTap: _loadTab, - labelColor: AppColors.primaryGreen, - unselectedLabelColor: AppColors.textSecondaryLight, - indicatorColor: AppColors.primaryGreen, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, indicatorSize: TabBarIndicatorSize.label, labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), tabs: const [ @@ -125,7 +124,13 @@ class _DemandesAidePageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.volunteer_activism_outlined, size: 32, color: AppColors.lightBorder), + Icon( + Icons.volunteer_activism_outlined, + size: 32, + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.borderDark + : AppColors.border, + ), const SizedBox(height: 12), Text('Aucune demande', style: AppTypography.subtitleSmall), ], @@ -204,7 +209,7 @@ class _DemandeCard extends StatelessWidget { ], ), ), - _buildStatutBadge(demande.statut), + _buildStatutBadge(context, demande.statut), ], ), const SizedBox(height: 12), @@ -217,7 +222,7 @@ class _DemandeCard extends StatelessWidget { Text('MONTANT DEMANDÉ', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)), Text( currencyFormat.format(demande.montantDemande ?? 0), - style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primaryGreen), + style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primary), ), ], ), @@ -235,7 +240,7 @@ class _DemandeCard extends StatelessWidget { ); } - Widget _buildStatutBadge(String? statut) { + Widget _buildStatutBadge(BuildContext context, String? statut) { Color color; switch (statut) { case 'APPROUVEE': @@ -246,10 +251,12 @@ class _DemandeCard extends StatelessWidget { break; case 'EN_ATTENTE': case 'SOUMISE': - color = AppColors.brandGreenLight; + color = AppColors.primaryLight; break; default: - color = AppColors.textSecondaryLight; + color = Theme.of(context).brightness == Brightness.dark + ? AppColors.textSecondaryDark + : AppColors.textSecondary; } return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color); }