feat(ui): RefreshIndicator + AlwaysScrollable + dark mode sur 14 pages

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
This commit is contained in:
dahoud
2026-04-15 20:13:50 +00:00
parent f78892e5f6
commit 55f84da49a
14 changed files with 1565 additions and 1538 deletions

View File

@@ -31,13 +31,14 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: const UFAppBar( appBar: UFAppBar(
title: 'DÉTAIL ADHÉSION', title: 'Détail Adhésion',
backgroundColor: AppColors.surface, moduleGradient: ModuleColors.adhesionsGradient,
foregroundColor: AppColors.textPrimaryLight,
), ),
body: BlocConsumer<AdhesionsBloc, AdhesionsState>( body: SafeArea(
top: false,
child: BlocConsumer<AdhesionsBloc, AdhesionsState>(
listenWhen: (prev, curr) => prev.status != curr.status, listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) { listener: (context, state) {
if (state.status == AdhesionsStatus.error && state.message != null) { if (state.status == AdhesionsStatus.error && state.message != null) {
@@ -58,7 +59,7 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 16),
Text( Text(
'Adhésion introuvable', 'Adhésion introuvable',
@@ -68,7 +69,12 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
), ),
); );
} }
return SingleChildScrollView( return RefreshIndicator(
color: ModuleColors.adhesions,
onRefresh: () async =>
context.read<AdhesionsBloc>().add(LoadAdhesionById(widget.adhesionId)),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -118,10 +124,12 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
_ActionsSection(adhesion: a, currencyFormat: _currencyFormat, isGestionnaire: _isGestionnaire()), _ActionsSection(adhesion: a, currencyFormat: _currencyFormat, isGestionnaire: _isGestionnaire()),
], ],
), ),
); ), // SingleChildScrollView
); // RefreshIndicator
}, },
), ), // BlocConsumer
); ), // SafeArea
); // Scaffold
} }
bool _isGestionnaire() { bool _isGestionnaire() {
@@ -157,7 +165,7 @@ class _InfoCard extends StatelessWidget {
style: AppTypography.subtitleSmall.copyWith( style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 9, fontSize: 9,
color: AppColors.textSecondaryLight, color: AppColors.textSecondary,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -187,13 +195,13 @@ Widget _buildStatutBadge(String? statut) {
color = AppColors.error; color = AppColors.error;
break; break;
case 'EN_ATTENTE': case 'EN_ATTENTE':
color = AppColors.brandGreenLight; color = AppColors.primaryLight;
break; break;
case 'EN_PAIEMENT': case 'EN_PAIEMENT':
color = AppColors.warning; color = AppColors.warning;
break; break;
default: default:
color = AppColors.textSecondaryLight; color = AppColors.textSecondary;
} }
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color); return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
} }
@@ -307,7 +315,7 @@ class _ActionsSection extends StatelessWidget {
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen, backgroundColor: AppColors.primary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),

View File

@@ -89,11 +89,10 @@ class _AdhesionsPageState extends State<AdhesionsPage>
} }
}, },
child: Scaffold( child: Scaffold(
backgroundColor: AppColors.background, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar( appBar: UFAppBar(
title: 'ADHÉSIONS', title: 'Adhésions',
backgroundColor: AppColors.surface, moduleGradient: ModuleColors.adhesionsGradient,
foregroundColor: AppColors.textPrimaryLight,
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.add, size: 20), icon: const Icon(Icons.add, size: 20),
@@ -105,9 +104,9 @@ class _AdhesionsPageState extends State<AdhesionsPage>
controller: _tabController, controller: _tabController,
onTap: _loadTab, onTap: _loadTab,
isScrollable: true, isScrollable: true,
labelColor: AppColors.primaryGreen, labelColor: Colors.white,
unselectedLabelColor: AppColors.textSecondaryLight, unselectedLabelColor: Colors.white70,
indicatorColor: AppColors.primaryGreen, indicatorColor: Colors.white,
indicatorSize: TabBarIndicatorSize.label, indicatorSize: TabBarIndicatorSize.label,
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
tabs: const [ tabs: const [
@@ -148,11 +147,17 @@ class _AdhesionsPageState extends State<AdhesionsPage>
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 8),
Text( Text(
'Aucune demande d\'adhésion', '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), const SizedBox(height: 8),
TextButton.icon( TextButton.icon(
@@ -249,7 +254,7 @@ class _AdhesionCard extends StatelessWidget {
], ],
), ),
), ),
_buildStatutBadge(adhesion.statut), _buildStatutBadge(context, adhesion.statut),
], ],
), ),
const SizedBox(height: 12), 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('FRAIS D\'ADHÉSION', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
Text( Text(
adhesion.fraisAdhesion != null ? currencyFormat.format(adhesion.fraisAdhesion) : '', 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), const SizedBox(height: 8),
Text( Text(
'MEMBRE : ${adhesion.nomMembreComplet.toUpperCase()}', '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; Color color;
switch (statut) { switch (statut) {
case 'APPROUVEE': case 'APPROUVEE':
@@ -303,13 +308,15 @@ class _AdhesionCard extends StatelessWidget {
color = AppColors.error; color = AppColors.error;
break; break;
case 'EN_ATTENTE': case 'EN_ATTENTE':
color = AppColors.brandGreenLight; color = AppColors.primaryLight;
break; break;
case 'EN_PAIEMENT': case 'EN_PAIEMENT':
color = AppColors.warning; color = AppColors.warning;
break; break;
default: default:
color = AppColors.textSecondaryLight; color = Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary;
} }
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color); return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
} }

View File

@@ -21,11 +21,14 @@ class UserManagementDetailPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: const UFAppBar( appBar: UFAppBar(
title: 'Détail utilisateur', title: 'Détail utilisateur',
moduleGradient: ModuleColors.systemeGradient,
), ),
body: BlocBuilder<AdminUsersBloc, AdminUsersState>( body: SafeArea(
top: false,
child: BlocBuilder<AdminUsersBloc, AdminUsersState>(
builder: (context, state) { builder: (context, state) {
if (state is AdminUsersLoading) { if (state is AdminUsersLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@@ -35,11 +38,15 @@ class UserManagementDetailPage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(state.message), const Icon(Icons.error_outline, size: 48, color: AppColors.error),
const SizedBox(height: 12),
Text(state.message, textAlign: TextAlign.center),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton( ElevatedButton.icon(
onPressed: () => context.read<AdminUsersBloc>().add(AdminUserDetailRequested(userId)), onPressed: () => context.read<AdminUsersBloc>()
child: const Text('Réessayer'), .add(AdminUserDetailWithRolesRequested(userId)),
icon: const Icon(Icons.refresh, size: 16),
label: const Text('Réessayer'),
), ),
], ],
), ),
@@ -53,9 +60,14 @@ class UserManagementDetailPage extends StatelessWidget {
userId: userId, userId: userId,
); );
} }
// AdminUserRolesUpdated : rechargement en cours — garder un indicateur léger
if (state is AdminUserRolesUpdated) {
return const Center(child: CircularProgressIndicator());
}
return const SizedBox(); return const SizedBox();
}, },
), ),
),
); );
} }
} }
@@ -96,7 +108,13 @@ class _UserDetailContentState extends State<_UserDetailContent> {
); );
} }
}, },
child: RefreshIndicator(
color: ModuleColors.systeme,
onRefresh: () async => context
.read<AdminUsersBloc>()
.add(AdminUserDetailWithRolesRequested(widget.userId)),
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -122,41 +140,95 @@ class _UserDetailContentState extends State<_UserDetailContent> {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
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( Text(
'RÔLES (SÉLECTION)', 'RÔLES',
style: AppTypography.subtitleSmall.copyWith( style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
letterSpacing: 1.1, 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), 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<AdminUsersBloc>()
.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) { ...widget.allRoles.map((role) {
final selected = _selectedRoleNames.contains(role.name); final selected = _selectedRoleNames.contains(role.name);
return CheckboxListTile( return CheckboxListTile(
title: Text(role.name, style: AppTypography.bodyTextSmall), title: Text(role.name, style: AppTypography.bodyTextSmall),
activeColor: AppColors.primaryGreen, subtitle: role.description != null && role.description!.isNotEmpty
contentPadding: EdgeInsets.zero, ? Text(role.description!,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10))
: null,
activeColor: AppColors.primary,
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
dense: true, dense: true,
value: selected, value: selected,
onChanged: (v) { onChanged: (v) => setState(() {
setState(() {
if (v == true) { if (v == true) {
_selectedRoleNames.add(role.name); _selectedRoleNames.add(role.name);
} else { } else {
_selectedRoleNames.remove(role.name); _selectedRoleNames.remove(role.name);
} }
}); }),
},
); );
}), }),
const SizedBox(height: 12), ],
),
),
const SizedBox(height: 8),
if (widget.allRoles.isNotEmpty)
UFPrimaryButton( UFPrimaryButton(
label: 'Enregistrer les rôles', label: 'Enregistrer les rôles',
onPressed: () { onPressed: () => context.read<AdminUsersBloc>().add(
context.read<AdminUsersBloc>().add( AdminUserRolesUpdateRequested(
AdminUserRolesUpdateRequested(widget.userId, _selectedRoleNames.toList()), widget.userId, _selectedRoleNames.toList())),
);
},
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
const Divider(height: 1), const Divider(height: 1),
@@ -171,7 +243,7 @@ class _UserDetailContentState extends State<_UserDetailContent> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Permet à cet utilisateur (ex. admin d\'organisation) de voir « Mes organisations » et d\'accéder au dashboard de l\'organisation.', '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), const SizedBox(height: 12),
OutlinedButton.icon( OutlinedButton.icon(
@@ -181,13 +253,14 @@ class _UserDetailContentState extends State<_UserDetailContent> {
icon: const Icon(Icons.business, size: 18), icon: const Icon(Icons.business, size: 18),
label: const Text('Associer à une organisation'), label: const Text('Associer à une organisation'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryGreen, foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primaryGreen), side: const BorderSide(color: AppColors.primary),
), ),
), ),
], ],
), ),
), ), // SingleChildScrollView
), // RefreshIndicator
); );
} }

View File

@@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../../shared/design_system/tokens/app_colors.dart'; import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../shared/design_system/tokens/color_tokens.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 '../../../../shared/design_system/tokens/spacing_tokens.dart';
import '../../../../core/di/injection_container.dart'; import '../../../../core/di/injection_container.dart';
import '../../../../core/utils/logger.dart'; import '../../../../core/utils/logger.dart';
@@ -82,8 +83,10 @@ class _BackupPageState extends State<BackupPage>
}, },
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.lightBackground, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column( body: SafeArea(
top: false,
child: Column(
children: [ children: [
_buildHeader(), _buildHeader(),
_buildTabBar(), _buildTabBar(),
@@ -99,27 +102,28 @@ class _BackupPageState extends State<BackupPage>
), ),
], ],
), ),
),
); );
}, },
), ),
); );
} }
/// Header harmonisé /// Header harmonisé avec bouton retour
Widget _buildHeader() { Widget _buildHeader() {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: SpacingTokens.sm, vertical: SpacingTokens.xs), margin: const EdgeInsets.symmetric(horizontal: SpacingTokens.sm, vertical: SpacingTokens.xs),
padding: const EdgeInsets.all(SpacingTokens.md), padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: const LinearGradient( gradient: LinearGradient(
colors: ColorTokens.primaryGradient, colors: ModuleColors.backupGradient,
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), borderRadius: BorderRadius.circular(SpacingTokens.radiusXl),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: ColorTokens.primary.withOpacity(0.3), color: ModuleColors.backup.withOpacity(0.3),
blurRadius: 20, blurRadius: 20,
offset: const Offset(0, 8), offset: const Offset(0, 8),
), ),
@@ -129,6 +133,19 @@ class _BackupPageState extends State<BackupPage>
children: [ children: [
Row( Row(
children: [ 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( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -137,7 +154,7 @@ class _BackupPageState extends State<BackupPage>
), ),
child: const Icon( child: const Icon(
Icons.backup, Icons.backup,
color: Colors.white, color: AppColors.onGradient,
size: 20, size: 20,
), ),
), ),
@@ -151,14 +168,14 @@ class _BackupPageState extends State<BackupPage>
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: AppColors.onGradient,
), ),
), ),
Text( Text(
'Gestion des sauvegardes système', 'Gestion des sauvegardes système',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.white.withOpacity(0.8), color: AppColors.onGradient.withOpacity(0.8),
), ),
), ),
], ],
@@ -173,7 +190,7 @@ class _BackupPageState extends State<BackupPage>
onPressed: () => _createBackupNow(), onPressed: () => _createBackupNow(),
icon: const Icon( icon: const Icon(
Icons.save, Icons.save,
color: Colors.white, color: AppColors.onGradient,
), ),
tooltip: 'Sauvegarde immédiate', tooltip: 'Sauvegarde immédiate',
), ),
@@ -275,21 +292,21 @@ class _BackupPageState extends State<BackupPage>
), ),
child: Column( child: Column(
children: [ children: [
Icon(icon, color: Colors.white, size: 20), Icon(icon, color: AppColors.onGradient, size: 20),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
value, value,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: AppColors.onGradient,
), ),
), ),
Text( Text(
label, label,
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white.withOpacity(0.8), color: AppColors.onGradient.withOpacity(0.8),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -303,11 +320,11 @@ class _BackupPageState extends State<BackupPage>
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 12), margin: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: AppColors.shadow,
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -315,9 +332,9 @@ class _BackupPageState extends State<BackupPage>
), ),
child: TabBar( child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: AppColors.primaryGreen, labelColor: AppColors.primary,
unselectedLabelColor: Colors.grey[600], unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
indicatorColor: AppColors.primaryGreen, indicatorColor: AppColors.primary,
indicatorWeight: 3, indicatorWeight: 3,
labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12), labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12),
tabs: const [ tabs: const [
@@ -331,7 +348,15 @@ class _BackupPageState extends State<BackupPage>
/// Onglet sauvegardes /// Onglet sauvegardes
Widget _buildBackupsTab(BackupState state) { Widget _buildBackupsTab(BackupState state) {
return SingleChildScrollView( return RefreshIndicator(
color: ModuleColors.backup,
onRefresh: () async {
context.read<BackupBloc>()
..add(LoadBackups())
..add(LoadBackupConfig());
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
@@ -342,6 +367,7 @@ class _BackupPageState extends State<BackupPage>
const SizedBox(height: 80), const SizedBox(height: 80),
], ],
), ),
),
); );
} }
@@ -358,11 +384,11 @@ class _BackupPageState extends State<BackupPage>
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: AppColors.shadow,
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -373,14 +399,14 @@ class _BackupPageState extends State<BackupPage>
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.folder, color: AppColors.primaryGreen, size: 20), const Icon(Icons.folder, color: AppColors.primary, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Sauvegardes disponibles', 'Sauvegardes disponibles',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey[800], color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
], ],
@@ -398,14 +424,14 @@ class _BackupPageState extends State<BackupPage>
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[50], color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
children: [ children: [
Icon( Icon(
backup['type'] == 'Auto' ? Icons.schedule : Icons.touch_app, 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, size: 20,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -415,17 +441,19 @@ class _BackupPageState extends State<BackupPage>
children: [ children: [
Text( Text(
backup['name']!, backup['name']!,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimaryLight, color: Theme.of(context).brightness == Brightness.dark
? AppColors.textPrimaryDark
: AppColors.textPrimary,
), ),
), ),
Text( Text(
'${backup['date']}${backup['size']}', '${backup['date']}${backup['size']}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -438,7 +466,12 @@ class _BackupPageState extends State<BackupPage>
const PopupMenuItem(value: 'download', child: Text('Télécharger')), const PopupMenuItem(value: 'download', child: Text('Télécharger')),
const PopupMenuItem(value: 'delete', child: Text('Supprimer')), 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,7 +480,12 @@ class _BackupPageState extends State<BackupPage>
/// Onglet planification /// Onglet planification
Widget _buildScheduleTab() { Widget _buildScheduleTab() {
return SingleChildScrollView( return RefreshIndicator(
color: ModuleColors.backup,
onRefresh: () async =>
context.read<BackupBloc>().add(LoadBackupConfig()),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
@@ -456,6 +494,7 @@ class _BackupPageState extends State<BackupPage>
const SizedBox(height: 80), const SizedBox(height: 80),
], ],
), ),
),
); );
} }
@@ -464,11 +503,11 @@ class _BackupPageState extends State<BackupPage>
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: AppColors.shadow,
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -479,14 +518,14 @@ class _BackupPageState extends State<BackupPage>
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.schedule, color: AppColors.primaryGreen, size: 20), const Icon(Icons.schedule, color: AppColors.primary, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Configuration automatique', 'Configuration automatique',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey[800], color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
], ],
@@ -519,7 +558,12 @@ class _BackupPageState extends State<BackupPage>
/// Onglet restauration /// Onglet restauration
Widget _buildRestoreTab() { Widget _buildRestoreTab() {
return SingleChildScrollView( return RefreshIndicator(
color: ModuleColors.backup,
onRefresh: () async =>
context.read<BackupBloc>().add(LoadBackups()),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
@@ -528,6 +572,7 @@ class _BackupPageState extends State<BackupPage>
const SizedBox(height: 80), const SizedBox(height: 80),
], ],
), ),
),
); );
} }
@@ -536,11 +581,11 @@ class _BackupPageState extends State<BackupPage>
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: AppColors.shadow,
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -551,14 +596,14 @@ class _BackupPageState extends State<BackupPage>
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.restore, color: AppColors.primaryGreen, size: 20), const Icon(Icons.restore, color: AppColors.primary, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Options de restauration', 'Options de restauration',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey[800], color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
], ],
@@ -568,7 +613,7 @@ class _BackupPageState extends State<BackupPage>
'Restaurer depuis un fichier', 'Restaurer depuis un fichier',
'Importer une sauvegarde externe', 'Importer une sauvegarde externe',
Icons.file_upload, Icons.file_upload,
AppColors.primaryGreen, AppColors.primary,
() => _restoreFromFile(), () => _restoreFromFile(),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -601,11 +646,11 @@ class _BackupPageState extends State<BackupPage>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), 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<BackupPage>
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[50], color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!), border: Border.all(color: Theme.of(context).colorScheme.outline),
), ),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
@@ -656,11 +701,11 @@ class _BackupPageState extends State<BackupPage>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: color)), 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),
], ],
), ),
), ),

View File

@@ -2,7 +2,10 @@
library event_detail_page; library event_detail_page;
import 'package:flutter/material.dart'; 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/tokens/app_colors.dart';
import '../../../../shared/design_system/components/uf_app_bar.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/di/injection.dart'; import '../../../../core/di/injection.dart';
import '../../bloc/evenements_bloc.dart'; import '../../bloc/evenements_bloc.dart';
@@ -51,10 +54,9 @@ class _EventDetailPageState extends State<EventDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: UFAppBar(
title: const Text('Détails de l\'événement'), title: "Détails de l'événement",
backgroundColor: AppColors.primaryGreen, moduleGradient: ModuleColors.evenementsGradient,
foregroundColor: Colors.white,
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
@@ -62,9 +64,15 @@ class _EventDetailPageState extends State<EventDetailPage> {
), ),
], ],
), ),
body: BlocBuilder<EvenementsBloc, EvenementsState>( body: SafeArea(
top: false,
child: BlocBuilder<EvenementsBloc, EvenementsState>(
builder: (context, state) { builder: (context, state) {
return SingleChildScrollView( return RefreshIndicator(
color: ModuleColors.evenements,
onRefresh: _loadInscriptionStatus,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -76,9 +84,11 @@ class _EventDetailPageState extends State<EventDetailPage> {
const SizedBox(height: 80), // Espace pour le bouton flottant const SizedBox(height: 80), // Espace pour le bouton flottant
], ],
), ),
); ), // SingleChildScrollView
); // RefreshIndicator
}, },
), ),
),
floatingActionButton: _buildInscriptionButton(context), floatingActionButton: _buildInscriptionButton(context),
); );
} }
@@ -87,12 +97,9 @@ class _EventDetailPageState extends State<EventDetailPage> {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: const BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: ModuleColors.evenementsGradient,
AppColors.primaryGreen,
AppColors.primaryGreen.withOpacity(0.8),
],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
@@ -191,7 +198,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Row(
children: [ children: [
Icon(icon, color: AppColors.primaryGreen, size: 20), Icon(icon, color: ModuleColors.evenements, size: 20),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
@@ -201,7 +208,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
label, label,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -261,7 +268,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
const Icon(Icons.location_on, color: AppColors.primaryGreen), const Icon(Icons.location_on, color: ModuleColors.evenements),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
@@ -296,7 +303,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
'${widget.evenement.participantsActuels} inscrits', '${widget.evenement.participantsActuels} inscrits',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -305,7 +312,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Row( child: const Row(
@@ -335,14 +342,14 @@ class _EventDetailPageState extends State<EventDetailPage> {
if (!isComplet) { if (!isComplet) {
return FloatingActionButton.extended( return FloatingActionButton.extended(
onPressed: () => _showInscriptionDialog(context, isInscrit), onPressed: () => _showInscriptionDialog(context, isInscrit),
backgroundColor: AppColors.primaryGreen, backgroundColor: ModuleColors.evenements,
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
label: const Text('S\'inscrire'), label: const Text('S\'inscrire'),
); );
} else { } else {
return const FloatingActionButton.extended( return const FloatingActionButton.extended(
onPressed: null, onPressed: null,
backgroundColor: Colors.grey, backgroundColor: AppColors.textTertiary,
icon: Icon(Icons.block), icon: Icon(Icons.block),
label: Text('Complet'), label: Text('Complet'),
); );
@@ -425,17 +432,17 @@ class _EventDetailPageState extends State<EventDetailPage> {
Color _getStatutColor(StatutEvenement statut) { Color _getStatutColor(StatutEvenement statut) {
switch (statut) { switch (statut) {
case StatutEvenement.planifie: case StatutEvenement.planifie:
return AppColors.info; return ColorTokens.info;
case StatutEvenement.confirme: case StatutEvenement.confirme:
return AppColors.success; return ColorTokens.success;
case StatutEvenement.enCours: case StatutEvenement.enCours:
return AppColors.warning; return ColorTokens.warningLight;
case StatutEvenement.termine: case StatutEvenement.termine:
return AppColors.textSecondaryLight; return ColorTokens.textSecondary;
case StatutEvenement.annule: case StatutEvenement.annule:
return AppColors.error; return ColorTokens.error;
case StatutEvenement.reporte: case StatutEvenement.reporte:
return AppColors.brandGreen; return ColorTokens.warning;
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -71,18 +71,39 @@ class _NotificationsPageState extends State<NotificationsPage>
} }
if (state is NotificationsError) { if (state is NotificationsError) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.red), SnackBar(content: Text(state.message), backgroundColor: AppColors.error),
); );
} }
}, },
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.lightBackground, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column( appBar: UFAppBar(
children: [ title: 'Notifications',
_buildHeader(), moduleGradient: ModuleColors.notificationsGradient,
_buildTabBar(), actions: [
Expanded( 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( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
@@ -91,106 +112,13 @@ class _NotificationsPageState extends State<NotificationsPage>
], ],
), ),
), ),
],
),
); );
}, },
); );
} }
/// Header harmonisé avec le design system // _buildHeader() et _buildTabBar() supprimés : gradient + TabBar migrés
Widget _buildHeader() { // dans UFAppBar + UFAppBar.bottom (pattern Adhésions).
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'),
],
),
);
}
/// Onglet des notifications /// Onglet des notifications
Widget _buildNotificationsTab() { Widget _buildNotificationsTab() {
@@ -236,7 +164,7 @@ class _NotificationsPageState extends State<NotificationsPage>
child: Switch( child: Switch(
value: _showOnlyUnread, value: _showOnlyUnread,
onChanged: (value) => setState(() => _showOnlyUnread = value), onChanged: (value) => setState(() => _showOnlyUnread = value),
activeColor: AppColors.primaryGreen, activeColor: AppColors.primary,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
), ),
@@ -262,22 +190,25 @@ class _NotificationsPageState extends State<NotificationsPage>
/// Chip de filtre /// Chip de filtre
Widget _buildFilterChip(String label, bool isSelected) { 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( return InkWell(
onTap: () => setState(() => _selectedFilter = label), onTap: () => setState(() => _selectedFilter = label),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( 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), borderRadius: BorderRadius.circular(4),
border: Border.all( border: Border.all(
color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder, color: isSelected ? AppColors.primary : borderInactive,
), ),
), ),
child: Text( child: Text(
label.toUpperCase(), label.toUpperCase(),
style: AppTypography.badgeText.copyWith( style: AppTypography.badgeText.copyWith(
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight, color: isSelected ? AppColors.primary : textInactive,
fontSize: 9, fontSize: 9,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
), ),
@@ -290,17 +221,24 @@ class _NotificationsPageState extends State<NotificationsPage>
Widget _buildNotificationsList() { Widget _buildNotificationsList() {
final notifications = _getFilteredNotifications(); final notifications = _getFilteredNotifications();
if (notifications.isEmpty) { return RefreshIndicator(
return _buildEmptyState(); color: ModuleColors.notifications,
} onRefresh: () async =>
context.read<NotificationsBloc>().add(const LoadNotifications()),
return ListView.builder( child: notifications.isEmpty
? SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: _buildEmptyState(),
)
: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
itemCount: notifications.length, itemCount: notifications.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final notification = notifications[index]; final notification = notifications[index];
return _buildNotificationCard(notification); return _buildNotificationCard(notification);
}, },
),
); );
} }
@@ -310,10 +248,10 @@ class _NotificationsPageState extends State<NotificationsPage>
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon( Icon(
Icons.notifications_none_outlined, Icons.notifications_none_outlined,
size: 40, size: 40,
color: AppColors.textSecondaryLight, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
@@ -338,6 +276,10 @@ class _NotificationsPageState extends State<NotificationsPage>
final isRead = notification['isRead'] as bool; final isRead = notification['isRead'] as bool;
final type = notification['type'] as String; final type = notification['type'] as String;
final color = _getNotificationColor(type); 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( return CoreCard(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
@@ -349,8 +291,8 @@ class _NotificationsPageState extends State<NotificationsPage>
MiniAvatar( MiniAvatar(
fallbackText: _getNotificationIconSource(type), fallbackText: _getNotificationIconSource(type),
size: 32, size: 32,
backgroundColor: isRead ? AppColors.lightSurface : color.withOpacity(0.1), backgroundColor: isRead ? bgRead : color.withOpacity(isDark ? 0.2 : 0.1),
iconColor: isRead ? AppColors.textSecondaryLight : color, iconColor: isRead ? textSecondary : color,
isIcon: true, isIcon: true,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -366,7 +308,7 @@ class _NotificationsPageState extends State<NotificationsPage>
notification['title'].toString().toUpperCase(), notification['title'].toString().toUpperCase(),
style: AppTypography.actionText.copyWith( style: AppTypography.actionText.copyWith(
fontSize: 11, fontSize: 11,
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight, color: isRead ? textSecondary : textPrimary,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -382,7 +324,7 @@ class _NotificationsPageState extends State<NotificationsPage>
Text( Text(
notification['message'], notification['message'],
style: AppTypography.bodyTextSmall.copyWith( style: AppTypography.bodyTextSmall.copyWith(
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight, color: isRead ? textSecondary : textPrimary,
fontWeight: isRead ? FontWeight.normal : FontWeight.w500, fontWeight: isRead ? FontWeight.normal : FontWeight.w500,
), ),
maxLines: 2, maxLines: 2,
@@ -392,8 +334,8 @@ class _NotificationsPageState extends State<NotificationsPage>
const SizedBox(height: 4), const SizedBox(height: 4),
InfoBadge( InfoBadge(
text: 'NOUVEAU', text: 'NOUVEAU',
backgroundColor: AppColors.primaryGreen.withOpacity(0.1), backgroundColor: AppColors.primary.withOpacity(0.1),
textColor: AppColors.primaryGreen, textColor: AppColors.primary,
), ),
], ],
], ],
@@ -413,7 +355,7 @@ class _NotificationsPageState extends State<NotificationsPage>
child: Text('Supprimer', style: AppTypography.bodyTextSmall.copyWith(color: AppColors.error)), 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<NotificationsPage>
children: [ children: [
Icon( Icon(
icon, icon,
color: AppColors.primaryGreen, color: AppColors.primary,
size: 18, size: 18,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -604,7 +546,7 @@ class _NotificationsPageState extends State<NotificationsPage>
child: Switch( child: Switch(
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
activeColor: AppColors.primaryGreen, activeColor: AppColors.primary,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
), ),
@@ -659,15 +601,15 @@ class _NotificationsPageState extends State<NotificationsPage>
Color _getNotificationColor(String type) { Color _getNotificationColor(String type) {
switch (type) { switch (type) {
case 'Membres': case 'Membres':
return AppColors.primaryGreen; return AppColors.primary;
case 'Événements': case 'Événements':
return AppColors.success; return AppColors.success;
case 'Organisations': case 'Organisations':
return AppColors.primaryGreen; return AppColors.primary;
case 'Système': case 'Système':
return AppColors.warning; return AppColors.warning;
default: default:
return AppColors.textSecondaryLight; return AppColors.textSecondary;
} }
} }
@@ -756,7 +698,7 @@ class _NotificationsPageState extends State<NotificationsPage>
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), 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( TextButton(
onPressed: () { onPressed: () {
@@ -769,7 +711,7 @@ class _NotificationsPageState extends State<NotificationsPage>
}); });
_showSuccessSnackBar('Flux marqué comme lu'); _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<NotificationsPage>
_tabController.animateTo(1); // Aller à l'onglet Préférences _tabController.animateTo(1); // Aller à l'onglet Préférences
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen, backgroundColor: AppColors.primary,
foregroundColor: Colors.white, foregroundColor: AppColors.onPrimary,
), ),
child: const Text('Voir les préférences'), child: const Text('Voir les préférences'),
), ),
@@ -831,8 +773,8 @@ class _NotificationsPageState extends State<NotificationsPage>
_showSuccessSnackBar('Notification supprimée'); _showSuccessSnackBar('Notification supprimée');
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: AppColors.error,
foregroundColor: Colors.white, foregroundColor: AppColors.onError,
), ),
child: const Text('Supprimer'), child: const Text('Supprimer'),
), ),
@@ -884,7 +826,7 @@ class _NotificationsPageState extends State<NotificationsPage>
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.warning, backgroundColor: AppColors.warning,
foregroundColor: Colors.white, foregroundColor: AppColors.onPrimary,
), ),
child: Text(notification['actionText']), child: Text(notification['actionText']),
), ),

View File

@@ -5,6 +5,9 @@
library org_selector_page; library org_selector_page;
import 'package:flutter/material.dart'; 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 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/org_switcher_bloc.dart'; import '../../bloc/org_switcher_bloc.dart';
import '../../data/models/org_switcher_entry.dart'; import '../../data/models/org_switcher_entry.dart';
@@ -30,12 +33,14 @@ class _OrgSelectorPageState extends State<OrgSelectorPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: UFAppBar(
title: const Text('Choisir une organisation'), title: 'Choisir une organisation',
moduleGradient: ModuleColors.organisationsGradient,
automaticallyImplyLeading: !widget.required, automaticallyImplyLeading: !widget.required,
elevation: 0,
), ),
body: BlocConsumer<OrgSwitcherBloc, OrgSwitcherState>( body: SafeArea(
top: false,
child: BlocConsumer<OrgSwitcherBloc, OrgSwitcherState>(
listener: (context, state) { listener: (context, state) {
if (state is OrgSwitcherLoaded && widget.required && state.active != null) { if (state is OrgSwitcherLoaded && widget.required && state.active != null) {
// Une org a été auto-sélectionnée, on peut continuer // Une org a été auto-sélectionnée, on peut continuer
@@ -71,6 +76,7 @@ class _OrgSelectorPageState extends State<OrgSelectorPage> {
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
), ),
),
); );
} }
} }
@@ -103,7 +109,12 @@ class _OrgList extends StatelessWidget {
), ),
), ),
Expanded( Expanded(
child: RefreshIndicator(
color: ModuleColors.organisations,
onRefresh: () async =>
context.read<OrgSwitcherBloc>().add(const OrgSwitcherLoadRequested()),
child: ListView.separated( child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: organisations.length, itemCount: organisations.length,
separatorBuilder: (_, __) => const SizedBox(height: 8), separatorBuilder: (_, __) => const SizedBox(height: 8),
@@ -116,7 +127,8 @@ class _OrgList extends StatelessWidget {
onTap: () => onSelect(org), onTap: () => onSelect(org),
); );
}, },
), ), // ListView.separated
), // RefreshIndicator
), ),
], ],
); );
@@ -199,8 +211,8 @@ class _OrgCard extends StatelessWidget {
_Chip( _Chip(
org.statutMembre!, org.statutMembre!,
color: org.statutMembre == 'ACTIF' color: org.statutMembre == 'ACTIF'
? Colors.green.shade700 ? AppColors.success
: Colors.orange.shade700, : AppColors.warning,
), ),
], ],
], ],
@@ -268,7 +280,7 @@ class _ErrorView extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.cloud_off, size: 56, color: Colors.grey), Icon(Icons.cloud_off, size: 56, color: AppColors.textTertiary),
const SizedBox(height: 16), const SizedBox(height: 16),
Text(message, textAlign: TextAlign.center), Text(message, textAlign: TextAlign.center),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -295,7 +307,7 @@ class _EmptyView extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.business_outlined, size: 56, color: Colors.grey), Icon(Icons.business_outlined, size: 56, color: AppColors.textTertiary),
SizedBox(height: 16), SizedBox(height: 16),
Text( Text(
'Vous n\'êtes membre d\'aucune organisation active.', 'Vous n\'êtes membre d\'aucune organisation active.',

View File

@@ -30,9 +30,10 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.lightBackground, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: const UFAppBar( appBar: UFAppBar(
title: 'TYPES D\'ORGANISATIONS', title: "Types d'Organisations",
moduleGradient: ModuleColors.organisationsGradient,
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
), ),
body: BlocConsumer<OrgTypesBloc, OrgTypesState>( body: BlocConsumer<OrgTypesBloc, OrgTypesState>(
@@ -41,7 +42,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(state.message), content: Text(state.message),
backgroundColor: Colors.green, backgroundColor: AppColors.success,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
); );
@@ -49,7 +50,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(state.message), content: Text(state.message),
backgroundColor: Colors.red, backgroundColor: AppColors.error,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
); );
@@ -86,7 +87,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
if (!isSuperAdmin) return const SizedBox.shrink(); if (!isSuperAdmin) return const SizedBox.shrink();
return FloatingActionButton.small( return FloatingActionButton.small(
onPressed: () => _showTypeForm(context, null), onPressed: () => _showTypeForm(context, null),
backgroundColor: AppColors.primaryGreen, backgroundColor: AppColors.primary,
child: const Icon(Icons.add, color: Colors.white), 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) { Widget _buildTypeCard(BuildContext context, TypeReferenceEntity type, OrgTypesState state) {
final isOperating = state is OrgTypeOperating; final isOperating = state is OrgTypeOperating;
final color = _parseColor(type.couleur) ?? AppColors.primaryGreen; final color = _parseColor(type.couleur) ?? AppColors.primary;
final authState = context.read<AuthBloc>().state; final authState = context.read<AuthBloc>().state;
final isSuperAdmin = authState is AuthAuthenticated && final isSuperAdmin = authState is AuthAuthenticated &&
authState.effectiveRole == UserRole.superAdmin; 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( return Opacity(
opacity: isOperating ? 0.6 : 1.0, opacity: isOperating ? 0.6 : 1.0,
@@ -145,28 +149,28 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
), ),
if (type.estDefaut) ...[ if (type.estDefaut) ...[
const SizedBox(width: 6), 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) ...[ if (type.estSysteme) ...[
const SizedBox(width: 6), 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), const SizedBox(height: 4),
Text( Text(
type.libelle, type.libelle,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.textPrimaryLight, color: textPrimary,
), ),
), ),
if (type.description != null && type.description!.isNotEmpty) ...[ if (type.description != null && type.description!.isNotEmpty) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
type.description!, type.description!,
style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight), style: TextStyle(fontSize: 11, color: textSecondary),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -177,14 +181,14 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
if (isSuperAdmin && !type.estSysteme && !isOperating) ...[ if (isSuperAdmin && !type.estSysteme && !isOperating) ...[
IconButton( IconButton(
icon: const Icon(Icons.edit_outlined, size: 16), icon: const Icon(Icons.edit_outlined, size: 16),
color: AppColors.textSecondaryLight, color: AppColors.textSecondary,
onPressed: () => _showTypeForm(context, type), onPressed: () => _showTypeForm(context, type),
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete_outline, size: 16), icon: const Icon(Icons.delete_outline, size: 16),
color: Colors.red[400], color: AppColors.error,
onPressed: () => _confirmDelete(context, type), onPressed: () => _confirmDelete(context, type),
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
@@ -208,18 +212,24 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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 SizedBox(height: 12),
const Text( Text(
'Aucun type défini', '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), const SizedBox(height: 6),
Text( Text(
isSuperAdmin isSuperAdmin
? 'Créez votre premier type d\'organisation' ? 'Créez votre premier type d\'organisation'
: 'Aucun type d\'organisation disponible', : '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, textAlign: TextAlign.center,
), ),
if (isSuperAdmin) ...[ if (isSuperAdmin) ...[
@@ -229,8 +239,8 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
icon: const Icon(Icons.add, size: 16), icon: const Icon(Icons.add, size: 16),
label: const Text('Créer un type'), label: const Text('Créer un type'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen, backgroundColor: AppColors.primary,
foregroundColor: Colors.white, foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
@@ -249,17 +259,26 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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 SizedBox(height: 12),
const Text('Erreur de chargement', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700)), const Text('Erreur de chargement', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700)),
const SizedBox(height: 6), 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), const SizedBox(height: 16),
ElevatedButton( ElevatedButton(
onPressed: () => context.read<OrgTypesBloc>().add(const LoadOrgTypes()), onPressed: () => context.read<OrgTypesBloc>().add(const LoadOrgTypes()),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen, backgroundColor: AppColors.primary,
foregroundColor: Colors.white, foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
@@ -303,8 +322,8 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
bloc.add(DeleteOrgTypeEvent(type.id)); bloc.add(DeleteOrgTypeEvent(type.id));
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: AppColors.error,
foregroundColor: Colors.white, foregroundColor: AppColors.onError,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
), ),
@@ -370,12 +389,16 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
@override @override
Widget build(BuildContext context) { 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( return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container( child: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
), ),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Form( child: Form(
@@ -389,13 +412,13 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
child: Container( child: Container(
width: 36, width: 36,
height: 4, 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), const SizedBox(height: 14),
Text( Text(
isEdit ? 'Modifier le type' : 'Nouveau type d\'organisation', 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), const SizedBox(height: 14),
@@ -446,7 +469,7 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
const SizedBox(height: 12), const SizedBox(height: 12),
// Color picker // 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), const SizedBox(height: 6),
Wrap( Wrap(
spacing: 8, spacing: 8,
@@ -462,7 +485,7 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: color,
shape: BoxShape.circle, 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, child: selected ? const Icon(Icons.check, size: 14, color: Colors.white) : null,
), ),
@@ -477,8 +500,8 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _submit, onPressed: _submit,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen, backgroundColor: AppColors.primary,
foregroundColor: Colors.white, foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),

View File

@@ -11,10 +11,11 @@ import '../../bloc/organizations_event.dart';
import '../../bloc/organizations_state.dart'; import '../../bloc/organizations_state.dart';
import '../../domain/repositories/organization_repository.dart'; import '../../domain/repositories/organization_repository.dart';
import 'edit_organization_page.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/presentation/bloc/auth_bloc.dart';
import '../../../../features/authentication/data/models/user_role.dart'; import '../../../../features/authentication/data/models/user_role.dart';
class OrganizationDetailPage extends StatefulWidget { class OrganizationDetailPage extends StatefulWidget {
final String organizationId; final String organizationId;
@@ -44,12 +45,10 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.lightBackground, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: UFAppBar(
backgroundColor: AppColors.brandGreen, title: 'Détail Organisation',
foregroundColor: Colors.white, moduleGradient: ModuleColors.organisationsGradient,
title: const Text('Détail Organisation'),
elevation: 0,
actions: [ actions: [
Builder(builder: (ctx) { Builder(builder: (ctx) {
final authState = ctx.read<AuthBloc>().state; final authState = ctx.read<AuthBloc>().state;
@@ -67,15 +66,17 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
return PopupMenuButton<String>( return PopupMenuButton<String>(
onSelected: _handleMenuAction, onSelected: _handleMenuAction,
itemBuilder: (context) => [ itemBuilder: (context) => [
const PopupMenuItem(value: 'activate', child: Row(children: [Icon(Icons.check_circle, color: AppColors.success), SizedBox(width: 8), Text('Activer')])), 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.textSecondaryLight), SizedBox(width: 8), Text('Désactiver')])), 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: Colors.red), SizedBox(width: 8), Text('Supprimer')])), const PopupMenuItem(value: 'delete', child: Row(children: [Icon(Icons.delete, color: AppColors.error), SizedBox(width: SpacingTokens.md), Text('Supprimer')])),
], ],
); );
}), }),
], ],
), ),
body: BlocBuilder<OrganizationsBloc, OrganizationsState>( body: SafeArea(
top: false,
child: BlocBuilder<OrganizationsBloc, OrganizationsState>(
builder: (context, state) { builder: (context, state) {
if (state is OrganizationLoading) return _buildLoading(); if (state is OrganizationLoading) return _buildLoading();
if (state is OrganizationLoaded) return _buildContent(state.organization); if (state is OrganizationLoaded) return _buildContent(state.organization);
@@ -83,74 +84,82 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
return _buildEmpty(); return _buildEmpty();
}, },
), ),
),
); );
} }
Widget _buildContent(OrganizationModel org) { Widget _buildContent(OrganizationModel org) {
return SingleChildScrollView( return RefreshIndicator(
padding: const EdgeInsets.all(12), color: ModuleColors.organisations,
onRefresh: () async => context
.read<OrganizationsBloc>()
.add(LoadOrganizationById(widget.organizationId)),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildHeaderCard(org), _buildHeaderCard(org),
const SizedBox(height: 8), const SizedBox(height: SpacingTokens.md),
_buildInfoCard(org), _buildInfoCard(org),
const SizedBox(height: 8), const SizedBox(height: SpacingTokens.md),
_buildContactCard(org), _buildContactCard(org),
if (_hasAddress(org)) ...[const SizedBox(height: 8), _buildAddressCard(org)], if (_hasAddress(org)) ...[const SizedBox(height: SpacingTokens.md), _buildAddressCard(org)],
const SizedBox(height: 8), const SizedBox(height: SpacingTokens.md),
_buildStatsCard(org), _buildStatsCard(org),
if (_hasFinances(org)) ...[const SizedBox(height: 8), _buildFinancesCard(org)], if (_hasFinances(org)) ...[const SizedBox(height: SpacingTokens.md), _buildFinancesCard(org)],
if (_hasMission(org)) ...[const SizedBox(height: 8), _buildMissionCard(org)], if (_hasMission(org)) ...[const SizedBox(height: SpacingTokens.md), _buildMissionCard(org)],
if (_hasSupplementary(org)) ...[const SizedBox(height: 8), _buildSupplementaryCard(org)], if (_hasSupplementary(org)) ...[const SizedBox(height: SpacingTokens.md), _buildSupplementaryCard(org)],
if (org.notes?.isNotEmpty == true) ...[const SizedBox(height: 8), _buildNotesCard(org)], if (org.notes?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildNotesCard(org)],
const SizedBox(height: 8), const SizedBox(height: SpacingTokens.md),
_buildActionsCard(org), _buildActionsCard(org),
], ],
), ), // Column
); ), // SingleChildScrollView
); // RefreshIndicator
} }
// ── Header ───────────────────────────────────────────────────────────────── // ── Header ─────────────────────────────────────────────────────────────────
Widget _buildHeaderCard(OrganizationModel org) { Widget _buildHeaderCard(OrganizationModel org) {
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.brandGreen, AppColors.primaryGreen]), gradient: const LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ModuleColors.organisationsDark, ModuleColors.organisations]),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 2))], boxShadow: [BoxShadow(color: AppColors.shadowMedium, blurRadius: 8, offset: const Offset(0, 2))],
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd)),
child: const Icon(Icons.business_outlined, size: 24, color: Colors.white), 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: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(org.nom, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), Text(org.nom, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
if (org.nomCourt?.isNotEmpty == true) ...[ 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))), Text(org.nomCourt!, style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(0.9))),
], ],
const SizedBox(height: 6), const SizedBox(height: SpacingTokens.md),
Row(children: [ Row(children: [
_buildWhiteBadge(org.typeOrganisationLibelle ?? org.typeOrganisation), _buildWhiteBadge(org.typeOrganisationLibelle ?? org.typeOrganisation),
const SizedBox(width: 6), const SizedBox(width: SpacingTokens.md),
_buildWhiteBadge(org.statutLibelle ?? org.statut.displayName), _buildWhiteBadge(org.statutLibelle ?? org.statut.displayName),
]), ]),
])), ])),
]), ]),
if (org.description?.isNotEmpty == true) ...[ 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)), 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: [ Row(children: [
_buildBoolBadge(Icons.public, 'Public', org.organisationPublique), _buildBoolBadge(Icons.public, 'Public', org.organisationPublique),
const SizedBox(width: 8), const SizedBox(width: SpacingTokens.md),
_buildBoolBadge(Icons.person_add, 'Ouvert', org.accepteNouveauxMembres), _buildBoolBadge(Icons.person_add, 'Ouvert', org.accepteNouveauxMembres),
]), ]),
]), ]),
@@ -159,17 +168,19 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
Widget _buildWhiteBadge(String text) { Widget _buildWhiteBadge(String text) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: 3),
decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(10)), 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)), child: Text(text, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white)),
); );
} }
Widget _buildBoolBadge(IconData icon, String label, bool value) { 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: [ return Row(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 12, color: value ? Colors.greenAccent : Colors.white60), Icon(icon, size: 12, color: color),
const SizedBox(width: 4), const SizedBox(width: SpacingTokens.sm),
Text('$label: ${value ? 'Oui' : 'Non'}', style: TextStyle(fontSize: 11, color: value ? Colors.greenAccent : Colors.white60)), Text('$label: ${value ? 'Oui' : 'Non'}', style: TextStyle(fontSize: 11, color: color)),
]); ]);
} }
@@ -178,14 +189,14 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
Widget _buildInfoCard(OrganizationModel org) { Widget _buildInfoCard(OrganizationModel org) {
return _buildCard('Informations générales', Icons.info_outline, [ 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) _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) _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) _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)), _buildInfoRow(Icons.calendar_today, 'Créé dans le système', _formatDate(org.dateCreation)),
if (org.dateModification != null) ...[ if (org.dateModification != null) ...[
const SizedBox(height: 10), const SizedBox(height: SpacingTokens.md),
_buildInfoRow(Icons.edit_calendar, 'Dernière modification', _formatDate(org.dateModification)), _buildInfoRow(Icons.edit_calendar, 'Dernière modification', _formatDate(org.dateModification)),
], ],
]); ]);
@@ -204,11 +215,11 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
return _buildCard('Contact', Icons.contact_phone, [ return _buildCard('Contact', Icons.contact_phone, [
if (org.email?.isNotEmpty == true) _buildContactRow(Icons.email, 'Email', org.email!, onTap: () => _launchEmail(org.email!)), 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.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: 10), _buildContactRow(Icons.phone, 'Téléphone', org.telephone!, onTap: () => _launchPhone(org.telephone!))], 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: 10), _buildContactRow(Icons.phone_forwarded, 'Téléphone secondaire', org.telephoneSecondaire!, onTap: () => _launchPhone(org.telephoneSecondaire!))], 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: 10), _buildContactRow(Icons.web, 'Site web', org.siteWeb!, onTap: () => _launchWebsite(org.siteWeb!))], 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: 10), _buildInfoRow(Icons.share, 'Réseaux sociaux', org.reseauxSociaux!)], 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<OrganizationDetailPage> {
Widget _buildAddressCard(OrganizationModel org) { Widget _buildAddressCard(OrganizationModel org) {
return _buildCard('Localisation', Icons.location_on, [ return _buildCard('Localisation', Icons.location_on, [
if (org.adresse?.isNotEmpty == true) _buildInfoRow(Icons.location_on, 'Adresse', org.adresse!), 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.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.region?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.map, 'Région', org.region!)],
if (org.pays?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildInfoRow(Icons.flag, 'Pays', org.pays!)], if (org.pays?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.flag, 'Pays', org.pays!)],
]); ]);
} }
@@ -233,10 +244,10 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
Widget _buildStatsCard(OrganizationModel org) { Widget _buildStatsCard(OrganizationModel org) {
return _buildCard('Statistiques', Icons.bar_chart, [ return _buildCard('Statistiques', Icons.bar_chart, [
Row(children: [ Row(children: [
Expanded(child: _buildStatItem(Icons.people, 'Membres', (_memberCount ?? org.nombreMembres).toString(), AppColors.primaryGreen)), Expanded(child: _buildStatItem(Icons.people, 'Membres', (_memberCount ?? org.nombreMembres).toString(), ModuleColors.organisations)),
const SizedBox(width: 10), const SizedBox(width: SpacingTokens.md),
Expanded(child: _buildStatItem(Icons.admin_panel_settings, 'Admins', org.nombreAdministrateurs.toString(), AppColors.brandGreen)), Expanded(child: _buildStatItem(Icons.admin_panel_settings, 'Admins', org.nombreAdministrateurs.toString(), ModuleColors.organisationsDark)),
const SizedBox(width: 10), const SizedBox(width: SpacingTokens.md),
Expanded(child: _buildStatItem(Icons.event, 'Événements', (org.nombreEvenements ?? 0).toString(), AppColors.success)), Expanded(child: _buildStatItem(Icons.event, 'Événements', (org.nombreEvenements ?? 0).toString(), AppColors.success)),
]), ]),
]); ]);
@@ -244,11 +255,11 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
Widget _buildStatItem(IconData icon, String label, String value, Color color) { Widget _buildStatItem(IconData icon, String label, String value, Color color) {
return Container( return Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withOpacity(0.15))), decoration: BoxDecoration(color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), border: Border.all(color: color.withOpacity(0.15))),
child: Column(children: [ child: Column(children: [
Icon(icon, size: 20, color: color), 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(value, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: color)),
Text(label, style: TextStyle(fontSize: 11, color: color)), Text(label, style: TextStyle(fontSize: 11, color: color)),
]), ]),
@@ -263,10 +274,10 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
Widget _buildFinancesCard(OrganizationModel org) { Widget _buildFinancesCard(OrganizationModel org) {
return _buildCard('Finances', Icons.account_balance_wallet, [ return _buildCard('Finances', Icons.account_balance_wallet, [
_buildInfoRow(Icons.currency_exchange, 'Devise', org.devise), _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}')], if (org.budgetAnnuel != null) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.account_balance, 'Budget annuel', '${_formatMontant(org.budgetAnnuel)} ${org.devise}')],
const SizedBox(height: 10), const SizedBox(height: SpacingTokens.md),
_buildInfoRow(Icons.payments, 'Cotisation', org.cotisationObligatoire ? 'Obligatoire' : 'Facultative'), _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<OrganizationDetailPage> {
Widget _buildMissionCard(OrganizationModel org) { Widget _buildMissionCard(OrganizationModel org) {
return _buildCard('Mission & Activités', Icons.flag, [ return _buildCard('Mission & Activités', Icons.flag, [
if (org.objectifs?.isNotEmpty == true) _buildTextBlock(Icons.track_changes, 'Objectifs', org.objectifs!), 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!), if (org.activitesPrincipales?.isNotEmpty == true) _buildTextBlock(Icons.work, 'Activités principales', org.activitesPrincipales!),
]); ]);
} }
@@ -291,7 +302,7 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
Widget _buildSupplementaryCard(OrganizationModel org) { Widget _buildSupplementaryCard(OrganizationModel org) {
return _buildCard('Informations complémentaires', Icons.info, [ 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) _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!), if (org.partenaires?.isNotEmpty == true) _buildTextBlock(Icons.handshake, 'Partenaires', org.partenaires!),
]); ]);
} }
@@ -299,20 +310,34 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
// ── Notes internes ────────────────────────────────────────────────────────── // ── Notes internes ──────────────────────────────────────────────────────────
Widget _buildNotesCard(OrganizationModel org) { Widget _buildNotesCard(OrganizationModel org) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFBEB), // Fond warning adaptatif (jaune clair light / surface sombre dark)
borderRadius: BorderRadius.circular(8), color: isDark
border: Border.all(color: const Color(0xFFFCD34D), width: 1), ? AppColors.surfaceVariantDark
: AppColors.warningContainer,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(color: AppColors.warningUI, width: 1),
), ),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Icon(Icons.sticky_note_2, size: 18, color: Color(0xFFF59E0B)), const Icon(Icons.sticky_note_2, size: 18, color: AppColors.warningUI),
const SizedBox(width: 10), const SizedBox(width: SpacingTokens.md),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Notes internes', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFFF59E0B))), Text('Notes internes',
const SizedBox(height: 4), style: TextStyle(
Text(org.notes!, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, height: 1.4)), 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<OrganizationDetailPage> {
onPressed: _showEditPage, onPressed: _showEditPage,
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
label: const Text('Modifier'), label: const Text('Modifier'),
style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white), style: ElevatedButton.styleFrom(backgroundColor: ModuleColors.organisations, foregroundColor: AppColors.onPrimary),
)), )),
if (isSuperAdmin) ...[ if (isSuperAdmin) ...[
const SizedBox(width: 12), const SizedBox(width: SpacingTokens.lg),
Expanded(child: OutlinedButton.icon( Expanded(child: OutlinedButton.icon(
onPressed: () => _showDeleteConfirmation(org), onPressed: () => _showDeleteConfirmation(org),
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
label: const Text('Supprimer'), 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<OrganizationDetailPage> {
Widget _buildCard(String title, IconData icon, List<Widget> children) { Widget _buildCard(String title, IconData icon, List<Widget> children) {
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), 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: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Icon(icon, size: 15, color: AppColors.primaryGreen), Icon(icon, size: 15, color: ModuleColors.organisations),
const SizedBox(width: 6), const SizedBox(width: SpacingTokens.md),
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primaryGreen)), Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: ModuleColors.organisations)),
]), ]),
const SizedBox(height: 10), const SizedBox(height: SpacingTokens.md),
...children, ...children,
]), ]),
); );
@@ -372,12 +401,12 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
Widget _buildInfoRow(IconData icon, String label, String value) { Widget _buildInfoRow(IconData icon, String label, String value) {
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Icon(icon, size: 18, color: AppColors.primaryGreen), Icon(icon, size: 18, color: ModuleColors.organisations),
const SizedBox(width: 10), const SizedBox(width: SpacingTokens.md),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight, fontWeight: FontWeight.w500)), Text(label, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500)),
const SizedBox(height: 2), const SizedBox(height: SpacingTokens.xs),
Text(value, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, fontWeight: FontWeight.w600)), Text(value, style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600)),
])), ])),
]); ]);
} }
@@ -385,29 +414,29 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
Widget _buildTextBlock(IconData icon, String label, String value) { Widget _buildTextBlock(IconData icon, String label, String value) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Icon(icon, size: 15, color: AppColors.primaryGreen), Icon(icon, size: 15, color: ModuleColors.organisations),
const SizedBox(width: 6), const SizedBox(width: SpacingTokens.md),
Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondaryLight, fontWeight: FontWeight.w600)), Text(label, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600)),
]), ]),
const SizedBox(height: 4), const SizedBox(height: SpacingTokens.sm),
Text(value, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, height: 1.5)), 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}) { Widget _buildContactRow(IconData icon, String label, String value, {VoidCallback? onTap}) {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2), padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(children: [ child: Row(children: [
Icon(icon, size: 18, color: AppColors.primaryGreen), Icon(icon, size: 18, color: ModuleColors.organisations),
const SizedBox(width: 10), const SizedBox(width: SpacingTokens.md),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight)), Text(label, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant)),
Text(value, style: TextStyle(fontSize: 13, color: onTap != null ? AppColors.primaryGreen : AppColors.textPrimaryLight, fontWeight: FontWeight.w600, decoration: onTap != null ? TextDecoration.underline : null)), 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<OrganizationDetailPage> {
// ── États ──────────────────────────────────────────────────────────────────── // ── États ────────────────────────────────────────────────────────────────────
Widget _buildLoading() => const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Widget _buildLoading() => const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen)), CircularProgressIndicator(color: ModuleColors.organisations),
SizedBox(height: 16), SizedBox(height: SpacingTokens.xl),
Text('Chargement...', style: TextStyle(color: AppColors.textSecondaryLight)), Text('Chargement...'),
])); ]));
Widget _buildError(OrganizationsError state) => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Widget _buildError(OrganizationsError state) => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.error_outline, size: 64, color: Colors.red.shade400), Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.error),
const SizedBox(height: 16), const SizedBox(height: SpacingTokens.xl),
Text(state.message, textAlign: TextAlign.center, style: const TextStyle(color: AppColors.textSecondaryLight)), Text(state.message, textAlign: TextAlign.center, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton.icon( FilledButton.icon(
onPressed: () => context.read<OrganizationsBloc>().add(LoadOrganizationById(widget.organizationId)), onPressed: () => context.read<OrganizationsBloc>().add(LoadOrganizationById(widget.organizationId)),
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh, size: 18),
label: const Text('Réessayer'), 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: [ Widget _buildEmpty() => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.business_outlined, size: 64, color: AppColors.textSecondaryLight), Icon(Icons.business_outlined, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
SizedBox(height: 16), const SizedBox(height: SpacingTokens.xl),
Text('Organisation non trouvée', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimaryLight)), Text('Organisation non trouvée', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)),
])); ]));
// ── Actions ────────────────────────────────────────────────────────────────── // ── Actions ──────────────────────────────────────────────────────────────────
@@ -475,8 +504,8 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Annuler')), TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Annuler')),
ElevatedButton( ElevatedButton(
onPressed: () { Navigator.of(ctx).pop(); bloc.add(DeleteOrganization(widget.organizationId)); nav.pop(); }, onPressed: () { Navigator.of(ctx).pop(); bloc.add(DeleteOrganization(widget.organizationId)); nav.pop(); },
style: ElevatedButton.styleFrom(backgroundColor: Colors.red), style: ElevatedButton.styleFrom(backgroundColor: AppColors.error),
child: const Text('Supprimer', style: TextStyle(color: Colors.white)), child: const Text('Supprimer', style: TextStyle(color: AppColors.onPrimary)),
), ),
], ],
), ),

View File

@@ -10,9 +10,11 @@ import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../../../shared/design_system/unionflow_design_system.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/core_card.dart';
import '../../../../shared/widgets/info_badge.dart'; import '../../../../shared/widgets/info_badge.dart';
import '../../../../shared/widgets/mini_avatar.dart'; import '../../../../shared/widgets/mini_avatar.dart';
import '../../../../core/config/environment.dart';
import '../../../../core/l10n/locale_provider.dart'; import '../../../../core/l10n/locale_provider.dart';
import '../../../../core/theme/theme_provider.dart'; import '../../../../core/theme/theme_provider.dart';
import '../../../authentication/presentation/bloc/auth_bloc.dart'; import '../../../authentication/presentation/bloc/auth_bloc.dart';
@@ -66,15 +68,22 @@ class _ProfilePageState extends State<ProfilePage>
// Infos app (chargées via package_info_plus) // Infos app (chargées via package_info_plus)
String _appVersion = '...'; String _appVersion = '...';
String _platformInfo = '...'; String _platformInfo = '...';
String _osVersion = '...';
String _envInfo = '...';
// Tailles stockage (calculées au chargement) // Tailles stockage (calculées au chargement)
String _cacheSize = '...'; String _cacheSize = '...';
String _imagesSize = '...'; String _imagesSize = '...';
String _offlineSize = '...'; String _offlineSize = '...';
// Options développeur (persistées via SharedPreferences)
bool _devMode = false;
bool _detailedLogs = false;
final List<String> _themes = ['Système', 'Clair', 'Sombre']; final List<String> _themes = ['Système', 'Clair', 'Sombre'];
static const _keyNotifPush = 'notif_push'; static const _keyNotifPush = 'notif_push';
static const _keyNotifEmail = 'notif_email'; static const _keyNotifEmail = 'notif_email';
static const _keyNotifSon = 'notif_son'; static const _keyNotifSon = 'notif_son';
static const _keyDevMode = 'dev_mode';
static const _keyDetailedLogs = 'detailed_logs';
@override @override
void initState() { void initState() {
@@ -136,26 +145,39 @@ class _ProfilePageState extends State<ProfilePage>
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar( appBar: UFAppBar(
title: 'MON PROFIL', title: 'Mon Profil',
backgroundColor: Theme.of(context).cardColor, moduleGradient: ModuleColors.profilGradient,
foregroundColor: Theme.of(context).brightness == Brightness.dark
? AppColors.textPrimaryDark
: AppColors.textPrimaryLight,
actions: [ actions: [
IconButton( IconButton(
icon: Icon(_isEditing ? Icons.save_outlined : Icons.edit_outlined, size: 20), icon: Icon(_isEditing ? Icons.save_outlined : Icons.edit_outlined, size: 20),
onPressed: () => _isEditing ? _saveProfile() : _startEditing(), 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), body: SafeArea(
top: false,
child: Column(
children: [ children: [
_buildHeader(), Padding(
const SizedBox(height: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
_buildTabBar(), child: _buildHeader(),
SizedBox( ),
height: 600, // Ajuster selon contenu ou utiliser NestedScrollView Expanded(
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
@@ -169,6 +191,7 @@ class _ProfilePageState extends State<ProfilePage>
], ],
), ),
), ),
),
); );
} }
@@ -176,6 +199,7 @@ class _ProfilePageState extends State<ProfilePage>
Widget _buildHeader() { Widget _buildHeader() {
return BlocBuilder<ProfileBloc, ProfileState>( return BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, state) { builder: (context, state) {
final scheme = Theme.of(context).colorScheme;
final membre = (state is ProfileLoaded) final membre = (state is ProfileLoaded)
? state.membre ? state.membre
: (state is ProfileUpdated ? state.membre : (state is ProfileUpdated ? state.membre
@@ -189,39 +213,39 @@ class _ProfilePageState extends State<ProfilePage>
Color badgeColor; Color badgeColor;
if (isLoading) { if (isLoading) {
badgeText = 'CHARGEMENT...'; badgeText = 'CHARGEMENT...';
badgeColor = AppColors.textSecondaryLight; badgeColor = ColorTokens.textSecondary;
} else if (hasError) { } else if (hasError) {
badgeText = 'ERREUR CHARGEMENT'; badgeText = 'ERREUR CHARGEMENT';
badgeColor = AppColors.error; badgeColor = ColorTokens.error;
} else if (membre != null) { } else if (membre != null) {
// Priorité au rôle s'il est disponible // Priorité au rôle s'il est disponible
if (membre.role != null && membre.role!.isNotEmpty) { if (membre.role != null && membre.role!.isNotEmpty) {
badgeText = membre.role!.replaceAll('_', ' '); badgeText = membre.role!.replaceAll('_', ' ');
badgeColor = AppColors.primaryGreen; badgeColor = ModuleColors.profil;
} else { } else {
// Fallback sur le statut // Fallback sur le statut
switch (membre.statut) { switch (membre.statut) {
case StatutMembre.actif: case StatutMembre.actif:
badgeText = membre.cotisationAJour ? 'MEMBRE ACTIF' : 'COTISATION EN RETARD'; badgeText = membre.cotisationAJour ? 'MEMBRE ACTIF' : 'COTISATION EN RETARD';
badgeColor = membre.cotisationAJour ? AppColors.success : AppColors.warning; badgeColor = membre.cotisationAJour ? ColorTokens.success : ColorTokens.warning;
break; break;
case StatutMembre.inactif: case StatutMembre.inactif:
badgeText = 'INACTIF'; badgeText = 'INACTIF';
badgeColor = AppColors.textSecondaryLight; badgeColor = ColorTokens.textSecondary;
break; break;
case StatutMembre.suspendu: case StatutMembre.suspendu:
badgeText = 'SUSPENDU'; badgeText = 'SUSPENDU';
badgeColor = AppColors.error; badgeColor = ColorTokens.error;
break; break;
case StatutMembre.enAttente: case StatutMembre.enAttente:
badgeText = 'EN ATTENTE'; badgeText = 'EN ATTENTE';
badgeColor = AppColors.warning; badgeColor = ColorTokens.warning;
break; break;
} }
} }
} else { } else {
badgeText = 'CHARGEMENT...'; badgeText = 'CHARGEMENT...';
badgeColor = AppColors.textSecondaryLight; badgeColor = ColorTokens.textSecondary;
} }
// Ancienneté réelle // Ancienneté réelle
@@ -274,7 +298,7 @@ class _ProfilePageState extends State<ProfilePage>
), ),
Text( Text(
_emailController.text.toLowerCase(), _emailController.text.toLowerCase(),
style: AppTypography.subtitleSmall.copyWith(fontSize: 11), style: AppTypography.subtitleSmall.copyWith(fontSize: 11, color: scheme.onSurfaceVariant),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (isLoading) if (isLoading)
@@ -291,7 +315,7 @@ class _ProfilePageState extends State<ProfilePage>
child: Text( child: Text(
'Réessayer', 'Réessayer',
style: AppTypography.subtitleSmall.copyWith( style: AppTypography.subtitleSmall.copyWith(
color: AppColors.primaryGreen, color: ModuleColors.profil,
fontSize: 10, fontSize: 10,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
), ),
@@ -344,10 +368,11 @@ class _ProfilePageState extends State<ProfilePage>
} }
Widget _buildStatItem(String label, String value) { Widget _buildStatItem(String label, String value) {
final scheme = Theme.of(context).colorScheme;
return Column( return Column(
children: [ children: [
Text(value, style: AppTypography.headerSmall.copyWith(fontSize: 14)), 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<ProfilePage>
); );
} }
/// Barre d'onglets // _buildTabBar() supprimé : migré dans UFAppBar.bottom (pattern Adhésions)
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É'),
],
),
);
}
/// Onglet informations personnelles /// Onglet informations personnelles
Widget _buildPersonalInfoTab() { Widget _buildPersonalInfoTab() {
return SingleChildScrollView( return RefreshIndicator(
color: ModuleColors.profil,
onRefresh: () async =>
context.read<ProfileBloc>().add(const LoadMe()),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
@@ -533,8 +539,9 @@ class _ProfilePageState extends State<ProfilePage>
const SizedBox(height: 80), const SizedBox(height: 80),
], ],
), ), // Column
); ), // SingleChildScrollView
); // RefreshIndicator
} }
/// Section d'informations /// Section d'informations
@@ -551,7 +558,7 @@ class _ProfilePageState extends State<ProfilePage>
children: [ children: [
Row( Row(
children: [ children: [
Icon(icon, color: AppColors.primaryGreen, size: 16), Icon(icon, color: ModuleColors.profil, size: 16),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
title.toUpperCase(), title.toUpperCase(),
@@ -579,7 +586,7 @@ class _ProfilePageState extends State<ProfilePage>
int maxLines = 1, int maxLines = 1,
String? hintText, String? hintText,
}) { }) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
return TextFormField( return TextFormField(
controller: controller, controller: controller,
enabled: enabled, enabled: enabled,
@@ -587,35 +594,34 @@ class _ProfilePageState extends State<ProfilePage>
maxLines: maxLines, maxLines: maxLines,
style: AppTypography.bodyTextSmall.copyWith( style: AppTypography.bodyTextSmall.copyWith(
fontSize: 12, fontSize: 12,
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
), ),
decoration: InputDecoration( decoration: InputDecoration(
labelText: label.toUpperCase(), labelText: label.toUpperCase(),
labelStyle: AppTypography.subtitleSmall.copyWith( labelStyle: AppTypography.subtitleSmall.copyWith(
fontSize: 9, fontSize: 9,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, color: scheme.onSurfaceVariant,
), ),
hintText: hintText, 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, filled: true,
fillColor: isDark ? AppColors.darkSurface : AppColors.lightSurface, fillColor: scheme.surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder), borderSide: BorderSide(color: scheme.outlineVariant),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder), borderSide: BorderSide(color: scheme.outlineVariant),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: AppColors.primaryGreen, width: 1), borderSide: BorderSide(color: ModuleColors.profil, width: 1),
), ),
disabledBorder: OutlineInputBorder( disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4), 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) { validator: (value) {
@@ -631,15 +637,15 @@ class _ProfilePageState extends State<ProfilePage>
/// Boutons d'action /// Boutons d'action
Widget _buildActionButtons() { Widget _buildActionButtons() {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface : Colors.white, color: scheme.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: AppColors.shadow,
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -652,8 +658,8 @@ class _ProfilePageState extends State<ProfilePage>
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _isLoading ? null : _cancelEditing, onPressed: _isLoading ? null : _cancelEditing,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[100], backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
foregroundColor: Colors.grey[700], foregroundColor: AppColors.textSecondary,
elevation: 0, elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
), ),
@@ -666,7 +672,7 @@ class _ProfilePageState extends State<ProfilePage>
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _isLoading ? null : _saveProfile, onPressed: _isLoading ? null : _saveProfile,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen, backgroundColor: ModuleColors.profil,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@@ -689,7 +695,7 @@ class _ProfilePageState extends State<ProfilePage>
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _startEditing, onPressed: _startEditing,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen, backgroundColor: ModuleColors.profil,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@@ -720,9 +726,7 @@ class _ProfilePageState extends State<ProfilePage>
[ [
Builder( Builder(
builder: (ctx) { builder: (ctx) {
final isDark = Theme.of(ctx).brightness == Brightness.dark; final scheme = Theme.of(ctx).colorScheme;
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
return InkWell( return InkWell(
onTap: () async { onTap: () async {
await Navigator.of(context).push( await Navigator.of(context).push(
@@ -738,18 +742,18 @@ class _ProfilePageState extends State<ProfilePage>
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: Row( child: Row(
children: [ children: [
Icon(Icons.language, color: textSecondary, size: 22), Icon(Icons.language, color: scheme.onSurfaceVariant, size: 22),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)),
Text('Actuellement : $_selectedLanguage', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)), 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<ProfilePage>
[ [
Builder( Builder(
builder: (ctx) { builder: (ctx) {
final isDark = Theme.of(ctx).brightness == Brightness.dark; final scheme = Theme.of(ctx).colorScheme;
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
return InkWell( return InkWell(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@@ -837,18 +839,18 @@ class _ProfilePageState extends State<ProfilePage>
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: Row( child: Row(
children: [ children: [
Icon(Icons.privacy_tip, color: textSecondary, size: 22), Icon(Icons.privacy_tip, color: scheme.onSurfaceVariant, size: 22),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Gérer la confidentialité', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), 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: textSecondary)), 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<ProfilePage>
'Télécharger mes données', 'Télécharger mes données',
'Exporter toutes vos données personnelles', 'Exporter toutes vos données personnelles',
Icons.download, Icons.download,
AppColors.primaryGreen, ModuleColors.profil,
() => _exportUserData(), () => _exportUserData(),
), ),
_buildActionItem( _buildActionItem(
'Déconnecter tous les appareils', 'Déconnecter tous les appareils',
'Fermer toutes les sessions actives', 'Fermer toutes les sessions actives',
Icons.logout, Icons.logout,
AppColors.warning, ColorTokens.warning,
() => _logoutAllDevices(), () => _logoutAllDevices(),
), ),
_buildActionItem( _buildActionItem(
'Supprimer mon compte', 'Supprimer mon compte',
'Action irréversible - toutes les données seront perdues', 'Action irréversible - toutes les données seront perdues',
Icons.delete_forever, Icons.delete_forever,
Colors.red, ColorTokens.error,
() => _showDeleteAccountDialog(), () => _showDeleteAccountDialog(),
), ),
], ],
@@ -1019,14 +1021,22 @@ class _ProfilePageState extends State<ProfilePage>
_buildSwitchPreference( _buildSwitchPreference(
'Mode développeur', 'Mode développeur',
'Afficher les options de débogage', 'Afficher les options de débogage',
false, _devMode,
(value) => _showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}'), (value) async {
setState(() => _devMode = value);
await _savePreference(_keyDevMode, value);
_showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}');
},
), ),
_buildSwitchPreference( _buildSwitchPreference(
'Logs détaillés', 'Logs détaillés',
'Enregistrer plus d\'informations de débogage', 'Logs réseau et erreurs — actif au prochain démarrage',
false, _detailedLogs,
(value) => _showSuccessSnackBar('Logs détaillés ${value ? 'activés' : 'désactivés'}'), (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<ProfilePage>
[ [
_buildInfoItem('Version de l\'app', _appVersion), _buildInfoItem('Version de l\'app', _appVersion),
_buildInfoItem('Plateforme', _platformInfo), _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<ProfilePage>
[ [
Builder( Builder(
builder: (ctx) { builder: (ctx) {
final isDark = Theme.of(ctx).brightness == Brightness.dark; final scheme = Theme.of(ctx).colorScheme;
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
return InkWell( return InkWell(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@@ -1068,18 +1079,18 @@ class _ProfilePageState extends State<ProfilePage>
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: Row( child: Row(
children: [ children: [
Icon(Icons.edit_note, color: textSecondary, size: 22), Icon(Icons.edit_note, color: scheme.onSurfaceVariant, size: 22),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Envoyer des commentaires', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), 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: textSecondary)), 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<ProfilePage>
children: [ children: [
Row( Row(
children: [ children: [
Icon(icon, color: AppColors.primaryGreen, size: 16), Icon(icon, color: ModuleColors.profil, size: 16),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
title.toUpperCase(), title.toUpperCase(),
@@ -1159,7 +1170,7 @@ class _ProfilePageState extends State<ProfilePage>
List<String> options, List<String> options,
Function(String?) onChanged, Function(String?) onChanged,
) { ) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -1171,21 +1182,31 @@ class _ProfilePageState extends State<ProfilePage>
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface : AppColors.lightSurface, color: scheme.surface,
borderRadius: BorderRadius.circular(4), 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: DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
value: value, value: value,
isExpanded: true, isExpanded: true,
onChanged: onChanged, onChanged: onChanged,
icon: const Icon(Icons.arrow_drop_down, color: AppColors.primaryGreen, size: 18), dropdownColor: scheme.surface,
style: AppTypography.bodyTextSmall.copyWith(fontSize: 12), icon: Icon(Icons.arrow_drop_down, color: ModuleColors.profil, size: 18),
style: AppTypography.bodyTextSmall.copyWith(
fontSize: 12,
color: scheme.onSurface,
),
items: options.map((option) { items: options.map((option) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: option, value: option,
child: Text(option), child: Text(
option,
style: AppTypography.bodyTextSmall.copyWith(
fontSize: 12,
color: scheme.onSurface,
),
),
); );
}).toList(), }).toList(),
), ),
@@ -1202,7 +1223,7 @@ class _ProfilePageState extends State<ProfilePage>
bool value, bool value,
Function(bool) onChanged, Function(bool) onChanged,
) { ) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
return Row( return Row(
children: [ children: [
Expanded( Expanded(
@@ -1217,7 +1238,7 @@ class _ProfilePageState extends State<ProfilePage>
subtitle, subtitle,
style: AppTypography.bodyTextSmall.copyWith( style: AppTypography.bodyTextSmall.copyWith(
fontSize: 10, fontSize: 10,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, color: scheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -1228,7 +1249,7 @@ class _ProfilePageState extends State<ProfilePage>
child: Switch( child: Switch(
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
activeColor: AppColors.primaryGreen, activeColor: ModuleColors.profil,
), ),
), ),
], ],
@@ -1242,21 +1263,20 @@ class _ProfilePageState extends State<ProfilePage>
IconData icon, IconData icon,
VoidCallback onTap, VoidCallback onTap,
) { ) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface : AppColors.lightSurface, color: scheme.surface,
borderRadius: BorderRadius.circular(4), 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( child: Row(
children: [ children: [
Icon(icon, color: AppColors.primaryGreen, size: 16), Icon(icon, color: ModuleColors.profil, size: 16),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
@@ -1268,12 +1288,12 @@ class _ProfilePageState extends State<ProfilePage>
), ),
Text( Text(
subtitle, 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<ProfilePage>
IconData icon, IconData icon,
bool isCurrentDevice, bool isCurrentDevice,
) { ) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isCurrentDevice color: isCurrentDevice
? AppColors.primaryGreen.withOpacity(0.05) ? ModuleColors.profil.withOpacity(0.05)
: (isDark ? AppColors.darkSurface : AppColors.lightSurface), : scheme.surface,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all( border: Border.all(
color: isCurrentDevice color: isCurrentDevice
? AppColors.primaryGreen.withOpacity(0.3) ? ModuleColors.profil.withOpacity(0.3)
: (isDark ? AppColors.darkBorder : AppColors.lightBorder), : scheme.outlineVariant,
width: 0.5, width: 0.5,
), ),
), ),
@@ -1307,7 +1326,7 @@ class _ProfilePageState extends State<ProfilePage>
children: [ children: [
Icon( Icon(
icon, icon,
color: isCurrentDevice ? AppColors.primaryGreen : textSecondary, color: isCurrentDevice ? ModuleColors.profil : scheme.onSurfaceVariant,
size: 16, size: 16,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -1323,13 +1342,13 @@ class _ProfilePageState extends State<ProfilePage>
), ),
if (isCurrentDevice) ...[ if (isCurrentDevice) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
const InfoBadge(text: 'ACTUEL', backgroundColor: AppColors.primaryGreen), InfoBadge(text: 'ACTUEL', backgroundColor: ModuleColors.profil),
], ],
], ],
), ),
Text( Text(
subtitle, 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<ProfilePage>
Color color, Color color,
VoidCallback onTap, VoidCallback onTap,
) { ) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@@ -1374,7 +1393,7 @@ class _ProfilePageState extends State<ProfilePage>
subtitle, subtitle,
style: AppTypography.bodyTextSmall.copyWith( style: AppTypography.bodyTextSmall.copyWith(
fontSize: 10, fontSize: 10,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, color: scheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -1389,20 +1408,20 @@ class _ProfilePageState extends State<ProfilePage>
/// Élément de stockage /// Élément de stockage
Widget _buildStorageItem(String title, String size, VoidCallback onTap) { Widget _buildStorageItem(String title, String size, VoidCallback onTap) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface : AppColors.lightSurface, color: scheme.surface,
borderRadius: BorderRadius.circular(4), 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( child: Row(
children: [ children: [
const Icon(Icons.folder_outlined, color: AppColors.primaryGreen, size: 16), Icon(Icons.folder_outlined, color: ModuleColors.profil, size: 16),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
@@ -1412,10 +1431,10 @@ class _ProfilePageState extends State<ProfilePage>
), ),
Text( Text(
size, size,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10), style: AppTypography.subtitleSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant),
), ),
const SizedBox(width: 8), 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<ProfilePage>
/// Élément d'information /// Élément d'information
Widget _buildInfoItem(String title, String value) { Widget _buildInfoItem(String title, String value) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface : AppColors.lightSurface, color: scheme.surface,
borderRadius: BorderRadius.circular(4), 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( child: Row(
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
title.toUpperCase(), title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold), style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold, color: scheme.onSurfaceVariant),
), ),
), ),
Text( Text(
@@ -1565,7 +1584,7 @@ class _ProfilePageState extends State<ProfilePage>
); );
} }
/// Charger les préférences notifications depuis SharedPreferences /// Charger les préférences depuis SharedPreferences
Future<void> _loadPreferences() async { Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (mounted) { if (mounted) {
@@ -1573,6 +1592,8 @@ class _ProfilePageState extends State<ProfilePage>
_notifPush = prefs.getBool(_keyNotifPush) ?? true; _notifPush = prefs.getBool(_keyNotifPush) ?? true;
_notifEmail = prefs.getBool(_keyNotifEmail) ?? false; _notifEmail = prefs.getBool(_keyNotifEmail) ?? false;
_notifSon = prefs.getBool(_keyNotifSon) ?? true; _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<ProfilePage>
final info = await PackageInfo.fromPlatform(); final info = await PackageInfo.fromPlatform();
if (mounted) { if (mounted) {
setState(() { setState(() {
_appVersion = '${info.version} (Build ${info.buildNumber})'; _appVersion = '${info.version} (build ${info.buildNumber})';
final os = kIsWeb final os = kIsWeb
? 'Web' ? 'Web'
: Platform.isAndroid : Platform.isAndroid
@@ -1597,6 +1618,10 @@ class _ProfilePageState extends State<ProfilePage>
? 'iOS' ? 'iOS'
: Platform.operatingSystem; : Platform.operatingSystem;
_platformInfo = '$os · ${info.appName}'; _platformInfo = '$os · ${info.appName}';
_osVersion = kIsWeb
? 'Navigateur web'
: Platform.operatingSystemVersion;
_envInfo = AppConfig.environment.name.toUpperCase();
}); });
} }
} }
@@ -1684,7 +1709,7 @@ class _ProfilePageState extends State<ProfilePage>
confirmPassCtrl.dispose(); confirmPassCtrl.dispose();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen, backgroundColor: ModuleColors.profil,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
child: const Text('Modifier'), child: const Text('Modifier'),
@@ -1782,7 +1807,7 @@ class _ProfilePageState extends State<ProfilePage>
_showFinalDeleteConfirmation(); _showFinalDeleteConfirmation();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: ColorTokens.error,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
child: const Text('Continuer'), child: const Text('Continuer'),
@@ -1836,7 +1861,7 @@ class _ProfilePageState extends State<ProfilePage>
confirmCtrl.dispose(); confirmCtrl.dispose();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: ColorTokens.error,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
child: const Text('SUPPRIMER DÉFINITIVEMENT'), child: const Text('SUPPRIMER DÉFINITIVEMENT'),
@@ -1953,7 +1978,7 @@ class _ProfilePageState extends State<ProfilePage>
message.toUpperCase(), message.toUpperCase(),
style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
), ),
backgroundColor: AppColors.success, backgroundColor: ColorTokens.success,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
), ),
@@ -1967,7 +1992,7 @@ class _ProfilePageState extends State<ProfilePage>
message.toUpperCase(), message.toUpperCase(),
style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
), ),
backgroundColor: AppColors.error, backgroundColor: ColorTokens.error,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
), ),

View File

@@ -58,7 +58,7 @@ class _ReportsPageState extends State<ReportsPage>
} }
if (state is ReportsError) { if (state is ReportsError) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.orange), SnackBar(content: Text(state.message), backgroundColor: AppColors.warning),
); );
} }
if (state is ReportScheduled) { if (state is ReportScheduled) {
@@ -74,18 +74,49 @@ class _ReportsPageState extends State<ReportsPage>
}, },
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.lightBackground, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column( 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: [ children: [
_buildHeader(),
_buildTabBar(),
if (state is ReportsLoading) if (state is ReportsLoading)
const LinearProgressIndicator( const LinearProgressIndicator(
minHeight: 2, minHeight: 2,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen), valueColor: AlwaysStoppedAnimation<Color>(AppColors.primary),
), ),
Expanded( Expanded(
child: RefreshIndicator(
color: ModuleColors.rapports,
onRefresh: () async => context
.read<ReportsBloc>()
.add(const LoadDashboardReports()),
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
@@ -96,162 +127,22 @@ class _ReportsPageState extends State<ReportsPage>
], ],
), ),
), ),
),
], ],
), ),
),
); );
}, },
); );
} }
Widget _buildHeader() { // _buildHeader(), _buildHeaderStat() et _buildTabBar() supprimés :
return Container( // titre + action Export migrés dans UFAppBar, TabBar dans UFAppBar.bottom.
width: double.infinity, // Les KPIs sont toujours disponibles dans _buildKPICards() (onglet Global).
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'),
],
),
);
}
Widget _buildOverviewTab() { Widget _buildOverviewTab() {
return ListView( return ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
children: [ children: [
_buildKPICards(), _buildKPICards(),
@@ -282,7 +173,7 @@ class _ReportsPageState extends State<ReportsPage>
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ 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), const SizedBox(width: 8),
Expanded(child: _buildKPICard('Événements', totalEvenements, Icons.event_available_outlined, AppColors.warning)), Expanded(child: _buildKPICard('Événements', totalEvenements, Icons.event_available_outlined, AppColors.warning)),
], ],
@@ -332,7 +223,7 @@ class _ReportsPageState extends State<ReportsPage>
children: [ children: [
Row( Row(
children: [ 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), const SizedBox(width: 8),
Text( Text(
'Évolution de l\'Activité'.toUpperCase(), 'Évolution de l\'Activité'.toUpperCase(),
@@ -341,26 +232,29 @@ class _ReportsPageState extends State<ReportsPage>
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Container( Builder(builder: (context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
height: 180, height: 180,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.1), color: (isDark ? AppColors.borderDark : AppColors.border).withOpacity(0.15),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: const Center( child: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.auto_graph_outlined, color: AppColors.textSecondaryLight, size: 40), Icon(Icons.auto_graph_outlined,
SizedBox(height: 12), color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
Text( size: 40),
'Visualisation graphique en préparation', const SizedBox(height: 12),
style: AppTypography.subtitleSmall, Text('Visualisation graphique en préparation',
), style: AppTypography.subtitleSmall),
], ],
), ),
), ),
), );
}),
], ],
), ),
); );
@@ -400,19 +294,19 @@ class _ReportsPageState extends State<ReportsPage>
child: Container( child: Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.05), color: AppColors.border.withOpacity(0.05),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)), border: Border.all(color: AppColors.border.withOpacity(0.1)),
), ),
child: Row( child: Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.1), color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Icon(icon, color: AppColors.primaryGreen, size: 20), child: Icon(icon, color: AppColors.primary, size: 20),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
@@ -425,7 +319,11 @@ class _ReportsPageState extends State<ReportsPage>
], ],
), ),
), ),
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<ReportsPage>
/// Onglet membres /// Onglet membres
Widget _buildMembersTab() { Widget _buildMembersTab() {
return SingleChildScrollView( return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
@@ -473,9 +372,9 @@ class _ReportsPageState extends State<ReportsPage>
Row( Row(
children: [ children: [
Expanded(child: _buildStatItem('Total', total)), 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)), 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)), Expanded(child: _buildStatItem('Actifs %', actifs)),
], ],
), ),
@@ -512,6 +411,7 @@ class _ReportsPageState extends State<ReportsPage>
/// Onglet organisations /// Onglet organisations
Widget _buildOrganizationsTab() { Widget _buildOrganizationsTab() {
return SingleChildScrollView( return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
@@ -537,7 +437,7 @@ class _ReportsPageState extends State<ReportsPage>
children: [ children: [
Row( Row(
children: [ 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), const SizedBox(width: 12),
Text( Text(
'Indicateurs Organisations'.toUpperCase(), 'Indicateurs Organisations'.toUpperCase(),
@@ -549,9 +449,9 @@ class _ReportsPageState extends State<ReportsPage>
Row( Row(
children: [ children: [
Expanded(child: _buildStatItem('Total', total)), 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)), 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)), Expanded(child: _buildStatItem('Membres moy.', moy)),
], ],
), ),
@@ -568,7 +468,7 @@ class _ReportsPageState extends State<ReportsPage>
children: [ children: [
Row( Row(
children: [ 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), const SizedBox(width: 12),
Text( Text(
'Rapports Structures'.toUpperCase(), 'Rapports Structures'.toUpperCase(),
@@ -588,6 +488,7 @@ class _ReportsPageState extends State<ReportsPage>
/// Onglet événements /// Onglet événements
Widget _buildEventsTab() { Widget _buildEventsTab() {
return SingleChildScrollView( return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
@@ -625,9 +526,9 @@ class _ReportsPageState extends State<ReportsPage>
Row( Row(
children: [ children: [
Expanded(child: _buildStatItem('Total', total)), 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)), 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)), Expanded(child: _buildStatItem('Part. moyenne', participation)),
], ],
), ),
@@ -667,7 +568,7 @@ class _ReportsPageState extends State<ReportsPage>
children: [ children: [
Text( Text(
value, 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), const SizedBox(height: 4),
Text( Text(
@@ -688,19 +589,19 @@ class _ReportsPageState extends State<ReportsPage>
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.05), color: AppColors.border.withOpacity(0.05),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)), border: Border.all(color: AppColors.border.withOpacity(0.1)),
), ),
child: Row( child: Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.1), color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Icon(icon, color: AppColors.primaryGreen, size: 20), child: Icon(icon, color: AppColors.primary, size: 20),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
@@ -713,7 +614,11 @@ class _ReportsPageState extends State<ReportsPage>
], ],
), ),
), ),
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<ReportsPage>
Navigator.of(context).pop(); Navigator.of(context).pop();
context.read<ReportsBloc>().add(GenerateReportRequested('export', format: _selectedFormat)); context.read<ReportsBloc>().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'), child: const Text('Exporter'),
), ),
], ],

View File

@@ -28,18 +28,19 @@ class _DemandeAideDetailPageState extends State<DemandeAideDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: const UFAppBar( appBar: UFAppBar(
title: 'DÉTAIL DEMANDE', title: 'Détail Demande',
backgroundColor: AppColors.surface, moduleGradient: ModuleColors.solidariteGradient,
foregroundColor: AppColors.textPrimaryLight,
), ),
body: BlocConsumer<SolidarityBloc, SolidarityState>( body: SafeArea(
top: false,
child: BlocConsumer<SolidarityBloc, SolidarityState>(
listenWhen: (prev, curr) => prev.status != curr.status, listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) { listener: (context, state) {
if (state.status == SolidarityStatus.error && state.message != null) { if (state.status == SolidarityStatus.error && state.message != null) {
ScaffoldMessenger.of(context).showSnackBar( 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<DemandeAideDetailPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 16),
Text( Text(
'Demande introuvable', 'Demande introuvable',
@@ -65,7 +66,12 @@ class _DemandeAideDetailPageState extends State<DemandeAideDetailPage> {
), ),
); );
} }
return SingleChildScrollView( return RefreshIndicator(
color: ModuleColors.solidarite,
onRefresh: () async =>
context.read<SolidarityBloc>().add(LoadDemandeAideById(widget.demandeId)),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -97,10 +103,12 @@ class _DemandeAideDetailPageState extends State<DemandeAideDetailPage> {
_ActionsSection(demande: d, isGestionnaire: _isGestionnaire()), _ActionsSection(demande: d, isGestionnaire: _isGestionnaire()),
], ],
), ),
); ), // SingleChildScrollView
); // RefreshIndicator
}, },
), ), // BlocConsumer
); ), // SafeArea
); // Scaffold
} }
bool _isGestionnaire() { bool _isGestionnaire() {
@@ -136,7 +144,7 @@ class _InfoCard extends StatelessWidget {
style: AppTypography.subtitleSmall.copyWith( style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 9, fontSize: 9,
color: AppColors.textSecondaryLight, color: AppColors.textSecondary,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),

View File

@@ -76,17 +76,16 @@ class _DemandesAidePageState extends State<DemandesAidePage>
} }
}, },
child: Scaffold( child: Scaffold(
backgroundColor: AppColors.background, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar( appBar: UFAppBar(
title: 'SOLIDARITÉ', title: 'Solidarité',
backgroundColor: AppColors.surface, moduleGradient: ModuleColors.solidariteGradient,
foregroundColor: AppColors.textPrimaryLight,
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
onTap: _loadTab, onTap: _loadTab,
labelColor: AppColors.primaryGreen, labelColor: Colors.white,
unselectedLabelColor: AppColors.textSecondaryLight, unselectedLabelColor: Colors.white70,
indicatorColor: AppColors.primaryGreen, indicatorColor: Colors.white,
indicatorSize: TabBarIndicatorSize.label, indicatorSize: TabBarIndicatorSize.label,
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
tabs: const [ tabs: const [
@@ -125,7 +124,13 @@ class _DemandesAidePageState extends State<DemandesAidePage>
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 12),
Text('Aucune demande', style: AppTypography.subtitleSmall), 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), 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('MONTANT DEMANDÉ', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
Text( Text(
currencyFormat.format(demande.montantDemande ?? 0), 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; Color color;
switch (statut) { switch (statut) {
case 'APPROUVEE': case 'APPROUVEE':
@@ -246,10 +251,12 @@ class _DemandeCard extends StatelessWidget {
break; break;
case 'EN_ATTENTE': case 'EN_ATTENTE':
case 'SOUMISE': case 'SOUMISE':
color = AppColors.brandGreenLight; color = AppColors.primaryLight;
break; break;
default: default:
color = AppColors.textSecondaryLight; color = Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary;
} }
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color); return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
} }