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:
@@ -31,13 +31,14 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: const UFAppBar(
|
||||
title: 'DÉTAIL ADHÉSION',
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'Détail Adhésion',
|
||||
moduleGradient: ModuleColors.adhesionsGradient,
|
||||
),
|
||||
body: BlocConsumer<AdhesionsBloc, AdhesionsState>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: BlocConsumer<AdhesionsBloc, AdhesionsState>(
|
||||
listenWhen: (prev, curr) => prev.status != curr.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == AdhesionsStatus.error && state.message != null) {
|
||||
@@ -58,7 +59,7 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 64, color: AppColors.textSecondaryLight),
|
||||
Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Adhésion introuvable',
|
||||
@@ -68,7 +69,12 @@ class _AdhesionDetailPageState extends State<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),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -118,10 +124,12 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
|
||||
_ActionsSection(adhesion: a, currencyFormat: _currencyFormat, isGestionnaire: _isGestionnaire()),
|
||||
],
|
||||
),
|
||||
);
|
||||
), // SingleChildScrollView
|
||||
); // RefreshIndicator
|
||||
},
|
||||
),
|
||||
);
|
||||
), // BlocConsumer
|
||||
), // SafeArea
|
||||
); // Scaffold
|
||||
}
|
||||
|
||||
bool _isGestionnaire() {
|
||||
@@ -157,7 +165,7 @@ class _InfoCard extends StatelessWidget {
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 9,
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
@@ -187,13 +195,13 @@ Widget _buildStatutBadge(String? statut) {
|
||||
color = AppColors.error;
|
||||
break;
|
||||
case 'EN_ATTENTE':
|
||||
color = AppColors.brandGreenLight;
|
||||
color = AppColors.primaryLight;
|
||||
break;
|
||||
case 'EN_PAIEMENT':
|
||||
color = AppColors.warning;
|
||||
break;
|
||||
default:
|
||||
color = AppColors.textSecondaryLight;
|
||||
color = AppColors.textSecondary;
|
||||
}
|
||||
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
|
||||
}
|
||||
@@ -307,7 +315,7 @@ class _ActionsSection extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
|
||||
@@ -89,11 +89,10 @@ class _AdhesionsPageState extends State<AdhesionsPage>
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'ADHÉSIONS',
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
title: 'Adhésions',
|
||||
moduleGradient: ModuleColors.adhesionsGradient,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, size: 20),
|
||||
@@ -105,9 +104,9 @@ class _AdhesionsPageState extends State<AdhesionsPage>
|
||||
controller: _tabController,
|
||||
onTap: _loadTab,
|
||||
isScrollable: true,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
@@ -148,11 +147,17 @@ class _AdhesionsPageState extends State<AdhesionsPage>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.assignment_outlined, size: 40, color: AppColors.textSecondaryLight),
|
||||
Icon(
|
||||
Icons.assignment_outlined,
|
||||
size: 40,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucune demande d\'adhésion',
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 16, color: AppColors.textSecondaryLight),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton.icon(
|
||||
@@ -249,7 +254,7 @@ class _AdhesionCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStatutBadge(adhesion.statut),
|
||||
_buildStatutBadge(context, adhesion.statut),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -262,7 +267,7 @@ class _AdhesionCard extends StatelessWidget {
|
||||
Text('FRAIS D\'ADHÉSION', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
adhesion.fraisAdhesion != null ? currencyFormat.format(adhesion.fraisAdhesion) : '—',
|
||||
style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primaryGreen),
|
||||
style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -283,7 +288,7 @@ class _AdhesionCard extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'MEMBRE : ${adhesion.nomMembreComplet.toUpperCase()}',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 8, color: AppColors.textSecondaryLight),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 8, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -291,7 +296,7 @@ class _AdhesionCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatutBadge(String? statut) {
|
||||
Widget _buildStatutBadge(BuildContext context, String? statut) {
|
||||
Color color;
|
||||
switch (statut) {
|
||||
case 'APPROUVEE':
|
||||
@@ -303,13 +308,15 @@ class _AdhesionCard extends StatelessWidget {
|
||||
color = AppColors.error;
|
||||
break;
|
||||
case 'EN_ATTENTE':
|
||||
color = AppColors.brandGreenLight;
|
||||
color = AppColors.primaryLight;
|
||||
break;
|
||||
case 'EN_PAIEMENT':
|
||||
color = AppColors.warning;
|
||||
break;
|
||||
default:
|
||||
color = AppColors.textSecondaryLight;
|
||||
color = Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textSecondary;
|
||||
}
|
||||
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
|
||||
}
|
||||
|
||||
@@ -21,11 +21,14 @@ class UserManagementDetailPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: const UFAppBar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'Détail utilisateur',
|
||||
moduleGradient: ModuleColors.systemeGradient,
|
||||
),
|
||||
body: BlocBuilder<AdminUsersBloc, AdminUsersState>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: BlocBuilder<AdminUsersBloc, AdminUsersState>(
|
||||
builder: (context, state) {
|
||||
if (state is AdminUsersLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -35,11 +38,15 @@ class UserManagementDetailPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.read<AdminUsersBloc>().add(AdminUserDetailRequested(userId)),
|
||||
child: const Text('Réessayer'),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => context.read<AdminUsersBloc>()
|
||||
.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,
|
||||
);
|
||||
}
|
||||
// AdminUserRolesUpdated : rechargement en cours — garder un indicateur léger
|
||||
if (state is AdminUserRolesUpdated) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -96,7 +108,13 @@ class _UserDetailContentState extends State<_UserDetailContent> {
|
||||
);
|
||||
}
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
color: ModuleColors.systeme,
|
||||
onRefresh: () async => context
|
||||
.read<AdminUsersBloc>()
|
||||
.add(AdminUserDetailWithRolesRequested(widget.userId)),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -122,41 +140,95 @@ class _UserDetailContentState extends State<_UserDetailContent> {
|
||||
),
|
||||
),
|
||||
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(
|
||||
'RÔLES (SÉLECTION)',
|
||||
'RÔLES',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${_selectedRoleNames.length} sélectionné(s)',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontSize: 10,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.allRoles.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_outlined,
|
||||
size: 16, color: AppColors.error),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Impossible de charger les rôles disponibles.',
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: AppColors.error),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => context.read<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) {
|
||||
final selected = _selectedRoleNames.contains(role.name);
|
||||
return CheckboxListTile(
|
||||
title: Text(role.name, style: AppTypography.bodyTextSmall),
|
||||
activeColor: AppColors.primaryGreen,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
subtitle: role.description != null && role.description!.isNotEmpty
|
||||
? Text(role.description!,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10))
|
||||
: null,
|
||||
activeColor: AppColors.primary,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
dense: true,
|
||||
value: selected,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
onChanged: (v) => setState(() {
|
||||
if (v == true) {
|
||||
_selectedRoleNames.add(role.name);
|
||||
} else {
|
||||
_selectedRoleNames.remove(role.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.allRoles.isNotEmpty)
|
||||
UFPrimaryButton(
|
||||
label: 'Enregistrer les rôles',
|
||||
onPressed: () {
|
||||
context.read<AdminUsersBloc>().add(
|
||||
AdminUserRolesUpdateRequested(widget.userId, _selectedRoleNames.toList()),
|
||||
);
|
||||
},
|
||||
onPressed: () => context.read<AdminUsersBloc>().add(
|
||||
AdminUserRolesUpdateRequested(
|
||||
widget.userId, _selectedRoleNames.toList())),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(height: 1),
|
||||
@@ -171,7 +243,7 @@ class _UserDetailContentState extends State<_UserDetailContent> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Permet à cet utilisateur (ex. admin d\'organisation) de voir « Mes organisations » et d\'accéder au dashboard de l\'organisation.',
|
||||
style: AppTypography.bodyTextSmall.copyWith(color: AppColors.textSecondaryLight),
|
||||
style: AppTypography.bodyTextSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
@@ -181,13 +253,14 @@ class _UserDetailContentState extends State<_UserDetailContent> {
|
||||
icon: const Icon(Icons.business, size: 18),
|
||||
label: const Text('Associer à une organisation'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
side: const BorderSide(color: AppColors.primaryGreen),
|
||||
foregroundColor: AppColors.primary,
|
||||
side: const BorderSide(color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
), // SingleChildScrollView
|
||||
), // RefreshIndicator
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../../../shared/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../shared/design_system/tokens/module_colors.dart';
|
||||
import '../../../../shared/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
@@ -82,8 +83,10 @@ class _BackupPageState extends State<BackupPage>
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
body: Column(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildTabBar(),
|
||||
@@ -99,27 +102,28 @@ class _BackupPageState extends State<BackupPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Header harmonisé
|
||||
/// Header harmonisé avec bouton retour
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: SpacingTokens.sm, vertical: SpacingTokens.xs),
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: ColorTokens.primaryGradient,
|
||||
gradient: LinearGradient(
|
||||
colors: ModuleColors.backupGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusXl),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorTokens.primary.withOpacity(0.3),
|
||||
color: ModuleColors.backup.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
@@ -129,6 +133,19 @@ class _BackupPageState extends State<BackupPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// Bouton retour
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.of(context).maybePop(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.arrow_back_rounded, color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
@@ -137,7 +154,7 @@ class _BackupPageState extends State<BackupPage>
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.backup,
|
||||
color: Colors.white,
|
||||
color: AppColors.onGradient,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
@@ -151,14 +168,14 @@ class _BackupPageState extends State<BackupPage>
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: AppColors.onGradient,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Gestion des sauvegardes système',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
color: AppColors.onGradient.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -173,7 +190,7 @@ class _BackupPageState extends State<BackupPage>
|
||||
onPressed: () => _createBackupNow(),
|
||||
icon: const Icon(
|
||||
Icons.save,
|
||||
color: Colors.white,
|
||||
color: AppColors.onGradient,
|
||||
),
|
||||
tooltip: 'Sauvegarde immédiate',
|
||||
),
|
||||
@@ -275,21 +292,21 @@ class _BackupPageState extends State<BackupPage>
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 20),
|
||||
Icon(icon, color: AppColors.onGradient, size: 20),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: AppColors.onGradient,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
color: AppColors.onGradient.withOpacity(0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -303,11 +320,11 @@ class _BackupPageState extends State<BackupPage>
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -315,9 +332,9 @@ class _BackupPageState extends State<BackupPage>
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: Colors.grey[600],
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
labelColor: AppColors.primary,
|
||||
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
indicatorColor: AppColors.primary,
|
||||
indicatorWeight: 3,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12),
|
||||
tabs: const [
|
||||
@@ -331,7 +348,15 @@ class _BackupPageState extends State<BackupPage>
|
||||
|
||||
/// Onglet sauvegardes
|
||||
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),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -342,6 +367,7 @@ class _BackupPageState extends State<BackupPage>
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -358,11 +384,11 @@ class _BackupPageState extends State<BackupPage>
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -373,14 +399,14 @@ class _BackupPageState extends State<BackupPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.folder, color: AppColors.primaryGreen, size: 20),
|
||||
const Icon(Icons.folder, color: AppColors.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Sauvegardes disponibles',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -398,14 +424,14 @@ class _BackupPageState extends State<BackupPage>
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
backup['type'] == 'Auto' ? Icons.schedule : Icons.touch_app,
|
||||
color: backup['type'] == 'Auto' ? AppColors.primaryGreen : AppColors.success,
|
||||
color: backup['type'] == 'Auto' ? AppColors.primary : AppColors.success,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -415,17 +441,19 @@ class _BackupPageState extends State<BackupPage>
|
||||
children: [
|
||||
Text(
|
||||
backup['name']!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimaryLight,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textPrimaryDark
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${backup['date']} • ${backup['size']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -438,7 +466,12 @@ class _BackupPageState extends State<BackupPage>
|
||||
const PopupMenuItem(value: 'download', child: Text('Télécharger')),
|
||||
const PopupMenuItem(value: 'delete', child: Text('Supprimer')),
|
||||
],
|
||||
child: const Icon(Icons.more_vert, color: Colors.grey),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -447,7 +480,12 @@ class _BackupPageState extends State<BackupPage>
|
||||
|
||||
/// Onglet planification
|
||||
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),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -456,6 +494,7 @@ class _BackupPageState extends State<BackupPage>
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -464,11 +503,11 @@ class _BackupPageState extends State<BackupPage>
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -479,14 +518,14 @@ class _BackupPageState extends State<BackupPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.schedule, color: AppColors.primaryGreen, size: 20),
|
||||
const Icon(Icons.schedule, color: AppColors.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Configuration automatique',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -519,7 +558,12 @@ class _BackupPageState extends State<BackupPage>
|
||||
|
||||
/// Onglet restauration
|
||||
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),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -528,6 +572,7 @@ class _BackupPageState extends State<BackupPage>
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -536,11 +581,11 @@ class _BackupPageState extends State<BackupPage>
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -551,14 +596,14 @@ class _BackupPageState extends State<BackupPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.restore, color: AppColors.primaryGreen, size: 20),
|
||||
const Icon(Icons.restore, color: AppColors.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Options de restauration',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -568,7 +613,7 @@ class _BackupPageState extends State<BackupPage>
|
||||
'Restaurer depuis un fichier',
|
||||
'Importer une sauvegarde externe',
|
||||
Icons.file_upload,
|
||||
AppColors.primaryGreen,
|
||||
AppColors.primary,
|
||||
() => _restoreFromFile(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -601,11 +646,11 @@ class _BackupPageState extends State<BackupPage>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(value: value, onChanged: onChanged, activeColor: AppColors.primaryGreen),
|
||||
Switch(value: value, onChanged: onChanged, activeColor: AppColors.primary),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -619,9 +664,9 @@ class _BackupPageState extends State<BackupPage>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
@@ -656,11 +701,11 @@ class _BackupPageState extends State<BackupPage>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: color)),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16),
|
||||
Icon(Icons.arrow_forward_ios, color: Theme.of(context).colorScheme.onSurfaceVariant, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
library event_detail_page;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/design_system/tokens/module_colors.dart';
|
||||
import '../../../../shared/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../../../shared/design_system/components/uf_app_bar.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../bloc/evenements_bloc.dart';
|
||||
@@ -51,10 +54,9 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Détails de l\'événement'),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
appBar: UFAppBar(
|
||||
title: "Détails de l'événement",
|
||||
moduleGradient: ModuleColors.evenementsGradient,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
@@ -62,9 +64,15 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<EvenementsBloc, EvenementsState>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: BlocBuilder<EvenementsBloc, EvenementsState>(
|
||||
builder: (context, state) {
|
||||
return SingleChildScrollView(
|
||||
return RefreshIndicator(
|
||||
color: ModuleColors.evenements,
|
||||
onRefresh: _loadInscriptionStatus,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -76,9 +84,11 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
const SizedBox(height: 80), // Espace pour le bouton flottant
|
||||
],
|
||||
),
|
||||
);
|
||||
), // SingleChildScrollView
|
||||
); // RefreshIndicator
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: _buildInscriptionButton(context),
|
||||
);
|
||||
}
|
||||
@@ -87,12 +97,9 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.primaryGreen,
|
||||
AppColors.primaryGreen.withOpacity(0.8),
|
||||
],
|
||||
colors: ModuleColors.evenementsGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
@@ -191,7 +198,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 20),
|
||||
Icon(icon, color: ModuleColors.evenements, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -201,7 +208,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
@@ -261,7 +268,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, color: AppColors.primaryGreen),
|
||||
const Icon(Icons.location_on, color: ModuleColors.evenements),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -296,7 +303,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
'${widget.evenement.participantsActuels} inscrits',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -305,7 +312,7 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Row(
|
||||
@@ -335,14 +342,14 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
if (!isComplet) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () => _showInscriptionDialog(context, isInscrit),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: ModuleColors.evenements,
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('S\'inscrire'),
|
||||
);
|
||||
} else {
|
||||
return const FloatingActionButton.extended(
|
||||
onPressed: null,
|
||||
backgroundColor: Colors.grey,
|
||||
backgroundColor: AppColors.textTertiary,
|
||||
icon: Icon(Icons.block),
|
||||
label: Text('Complet'),
|
||||
);
|
||||
@@ -425,17 +432,17 @@ class _EventDetailPageState extends State<EventDetailPage> {
|
||||
Color _getStatutColor(StatutEvenement statut) {
|
||||
switch (statut) {
|
||||
case StatutEvenement.planifie:
|
||||
return AppColors.info;
|
||||
return ColorTokens.info;
|
||||
case StatutEvenement.confirme:
|
||||
return AppColors.success;
|
||||
return ColorTokens.success;
|
||||
case StatutEvenement.enCours:
|
||||
return AppColors.warning;
|
||||
return ColorTokens.warningLight;
|
||||
case StatutEvenement.termine:
|
||||
return AppColors.textSecondaryLight;
|
||||
return ColorTokens.textSecondary;
|
||||
case StatutEvenement.annule:
|
||||
return AppColors.error;
|
||||
return ColorTokens.error;
|
||||
case StatutEvenement.reporte:
|
||||
return AppColors.brandGreen;
|
||||
return ColorTokens.warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -71,18 +71,39 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
}
|
||||
if (state is NotificationsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
|
||||
SnackBar(content: Text(state.message), backgroundColor: AppColors.error),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'Notifications',
|
||||
moduleGradient: ModuleColors.notificationsGradient,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _markAllAsRead(),
|
||||
icon: const Icon(Icons.done_all, size: 20),
|
||||
tooltip: 'Tout marquer comme lu',
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(child: Text('FLUX')),
|
||||
Tab(child: Text('RÉGLAGES')),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
@@ -91,106 +112,13 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Header harmonisé avec le design system
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.brandGreen, AppColors.primaryGreen],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x1A000000),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.notifications_none,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'NOTIFICATIONS',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Restez connecté à votre réseau',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _markAllAsRead(),
|
||||
icon: const Icon(Icons.done_all, color: Colors.white, size: 20),
|
||||
tooltip: 'Tout marquer comme lu',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Barre d'onglets
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicator: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: AppColors.primaryGreen.withOpacity(0.1),
|
||||
),
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 12),
|
||||
unselectedLabelStyle: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
|
||||
tabs: const [
|
||||
Tab(text: 'FLUX'),
|
||||
Tab(text: 'RÉGLAGES'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// _buildHeader() et _buildTabBar() supprimés : gradient + TabBar migrés
|
||||
// dans UFAppBar + UFAppBar.bottom (pattern Adhésions).
|
||||
|
||||
/// Onglet des notifications
|
||||
Widget _buildNotificationsTab() {
|
||||
@@ -236,7 +164,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
child: Switch(
|
||||
value: _showOnlyUnread,
|
||||
onChanged: (value) => setState(() => _showOnlyUnread = value),
|
||||
activeColor: AppColors.primaryGreen,
|
||||
activeColor: AppColors.primary,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
@@ -262,22 +190,25 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
|
||||
/// Chip de filtre
|
||||
Widget _buildFilterChip(String label, bool isSelected) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final borderInactive= isDark ? AppColors.borderDark : AppColors.border;
|
||||
final textInactive = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _selectedFilter = label),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primaryGreen.withOpacity(0.1) : Colors.transparent,
|
||||
color: isSelected ? AppColors.primary.withOpacity(isDark ? 0.2 : 0.1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder,
|
||||
color: isSelected ? AppColors.primary : borderInactive,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
color: isSelected ? AppColors.primary : textInactive,
|
||||
fontSize: 9,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
@@ -290,17 +221,24 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
Widget _buildNotificationsList() {
|
||||
final notifications = _getFilteredNotifications();
|
||||
|
||||
if (notifications.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
return RefreshIndicator(
|
||||
color: ModuleColors.notifications,
|
||||
onRefresh: () async =>
|
||||
context.read<NotificationsBloc>().add(const LoadNotifications()),
|
||||
child: notifications.isEmpty
|
||||
? SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: _buildEmptyState(),
|
||||
)
|
||||
: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: notifications.length,
|
||||
itemBuilder: (context, index) {
|
||||
final notification = notifications[index];
|
||||
return _buildNotificationCard(notification);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,10 +248,10 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.notifications_none_outlined,
|
||||
size: 40,
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
@@ -338,6 +276,10 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
final isRead = notification['isRead'] as bool;
|
||||
final type = notification['type'] as String;
|
||||
final color = _getNotificationColor(type);
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
final bgRead = isDark ? AppColors.surfaceVariantDark : AppColors.surface;
|
||||
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
@@ -349,8 +291,8 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
MiniAvatar(
|
||||
fallbackText: _getNotificationIconSource(type),
|
||||
size: 32,
|
||||
backgroundColor: isRead ? AppColors.lightSurface : color.withOpacity(0.1),
|
||||
iconColor: isRead ? AppColors.textSecondaryLight : color,
|
||||
backgroundColor: isRead ? bgRead : color.withOpacity(isDark ? 0.2 : 0.1),
|
||||
iconColor: isRead ? textSecondary : color,
|
||||
isIcon: true,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -366,7 +308,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
notification['title'].toString().toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontSize: 11,
|
||||
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight,
|
||||
color: isRead ? textSecondary : textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -382,7 +324,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
Text(
|
||||
notification['message'],
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight,
|
||||
color: isRead ? textSecondary : textPrimary,
|
||||
fontWeight: isRead ? FontWeight.normal : FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
@@ -392,8 +334,8 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
const SizedBox(height: 4),
|
||||
InfoBadge(
|
||||
text: 'NOUVEAU',
|
||||
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
|
||||
textColor: AppColors.primaryGreen,
|
||||
backgroundColor: AppColors.primary.withOpacity(0.1),
|
||||
textColor: AppColors.primary,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -413,7 +355,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
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: [
|
||||
Icon(
|
||||
icon,
|
||||
color: AppColors.primaryGreen,
|
||||
color: AppColors.primary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -604,7 +546,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: AppColors.primaryGreen,
|
||||
activeColor: AppColors.primary,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
@@ -659,15 +601,15 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
Color _getNotificationColor(String type) {
|
||||
switch (type) {
|
||||
case 'Membres':
|
||||
return AppColors.primaryGreen;
|
||||
return AppColors.primary;
|
||||
case 'Événements':
|
||||
return AppColors.success;
|
||||
case 'Organisations':
|
||||
return AppColors.primaryGreen;
|
||||
return AppColors.primary;
|
||||
case 'Système':
|
||||
return AppColors.warning;
|
||||
default:
|
||||
return AppColors.textSecondaryLight;
|
||||
return AppColors.textSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,7 +698,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: AppColors.textSecondaryLight)),
|
||||
child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -769,7 +711,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
});
|
||||
_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
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
),
|
||||
child: const Text('Voir les préférences'),
|
||||
),
|
||||
@@ -831,8 +773,8 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
_showSuccessSnackBar('Notification supprimée');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.error,
|
||||
foregroundColor: AppColors.onError,
|
||||
),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
@@ -884,7 +826,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.warning,
|
||||
foregroundColor: Colors.white,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
),
|
||||
child: Text(notification['actionText']),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
library org_selector_page;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/design_system/tokens/module_colors.dart';
|
||||
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../../../shared/design_system/components/uf_app_bar.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../bloc/org_switcher_bloc.dart';
|
||||
import '../../data/models/org_switcher_entry.dart';
|
||||
@@ -30,12 +33,14 @@ class _OrgSelectorPageState extends State<OrgSelectorPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Choisir une organisation'),
|
||||
appBar: UFAppBar(
|
||||
title: 'Choisir une organisation',
|
||||
moduleGradient: ModuleColors.organisationsGradient,
|
||||
automaticallyImplyLeading: !widget.required,
|
||||
elevation: 0,
|
||||
),
|
||||
body: BlocConsumer<OrgSwitcherBloc, OrgSwitcherState>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: BlocConsumer<OrgSwitcherBloc, OrgSwitcherState>(
|
||||
listener: (context, state) {
|
||||
if (state is OrgSwitcherLoaded && widget.required && state.active != null) {
|
||||
// Une org a été auto-sélectionnée, on peut continuer
|
||||
@@ -71,6 +76,7 @@ class _OrgSelectorPageState extends State<OrgSelectorPage> {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -103,7 +109,12 @@ class _OrgList extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
color: ModuleColors.organisations,
|
||||
onRefresh: () async =>
|
||||
context.read<OrgSwitcherBloc>().add(const OrgSwitcherLoadRequested()),
|
||||
child: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
itemCount: organisations.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||
@@ -116,7 +127,8 @@ class _OrgList extends StatelessWidget {
|
||||
onTap: () => onSelect(org),
|
||||
);
|
||||
},
|
||||
),
|
||||
), // ListView.separated
|
||||
), // RefreshIndicator
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -199,8 +211,8 @@ class _OrgCard extends StatelessWidget {
|
||||
_Chip(
|
||||
org.statutMembre!,
|
||||
color: org.statutMembre == 'ACTIF'
|
||||
? Colors.green.shade700
|
||||
: Colors.orange.shade700,
|
||||
? AppColors.success
|
||||
: AppColors.warning,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -268,7 +280,7 @@ class _ErrorView extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_off, size: 56, color: Colors.grey),
|
||||
Icon(Icons.cloud_off, size: 56, color: AppColors.textTertiary),
|
||||
const SizedBox(height: 16),
|
||||
Text(message, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
@@ -295,7 +307,7 @@ class _EmptyView extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.business_outlined, size: 56, color: Colors.grey),
|
||||
Icon(Icons.business_outlined, size: 56, color: AppColors.textTertiary),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Vous n\'êtes membre d\'aucune organisation active.',
|
||||
|
||||
@@ -30,9 +30,10 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
appBar: const UFAppBar(
|
||||
title: 'TYPES D\'ORGANISATIONS',
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: "Types d'Organisations",
|
||||
moduleGradient: ModuleColors.organisationsGradient,
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: BlocConsumer<OrgTypesBloc, OrgTypesState>(
|
||||
@@ -41,7 +42,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: Colors.green,
|
||||
backgroundColor: AppColors.success,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -49,7 +50,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: AppColors.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
@@ -86,7 +87,7 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
if (!isSuperAdmin) return const SizedBox.shrink();
|
||||
return FloatingActionButton.small(
|
||||
onPressed: () => _showTypeForm(context, null),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: AppColors.primary,
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
);
|
||||
}),
|
||||
@@ -103,10 +104,13 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
|
||||
Widget _buildTypeCard(BuildContext context, TypeReferenceEntity type, OrgTypesState state) {
|
||||
final isOperating = state is OrgTypeOperating;
|
||||
final color = _parseColor(type.couleur) ?? AppColors.primaryGreen;
|
||||
final color = _parseColor(type.couleur) ?? AppColors.primary;
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
final isSuperAdmin = authState is AuthAuthenticated &&
|
||||
authState.effectiveRole == UserRole.superAdmin;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
|
||||
final textSecondary= isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
|
||||
return Opacity(
|
||||
opacity: isOperating ? 0.6 : 1.0,
|
||||
@@ -145,28 +149,28 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
),
|
||||
if (type.estDefaut) ...[
|
||||
const SizedBox(width: 6),
|
||||
const Icon(Icons.star_rounded, size: 13, color: Color(0xFFF59E0B)),
|
||||
const Icon(Icons.star_rounded, size: 13, color: AppColors.warning),
|
||||
],
|
||||
if (type.estSysteme) ...[
|
||||
const SizedBox(width: 6),
|
||||
Icon(Icons.lock_outline, size: 12, color: Colors.grey[500]),
|
||||
Icon(Icons.lock_outline, size: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
type.libelle,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimaryLight,
|
||||
color: textPrimary,
|
||||
),
|
||||
),
|
||||
if (type.description != null && type.description!.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
type.description!,
|
||||
style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight),
|
||||
style: TextStyle(fontSize: 11, color: textSecondary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -177,14 +181,14 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
if (isSuperAdmin && !type.estSysteme && !isOperating) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: AppColors.textSecondary,
|
||||
onPressed: () => _showTypeForm(context, type),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
color: Colors.red[400],
|
||||
color: AppColors.error,
|
||||
onPressed: () => _confirmDelete(context, type),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
@@ -208,18 +212,24 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.category_outlined, size: 48, color: Colors.grey[400]),
|
||||
Icon(Icons.category_outlined, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
Text(
|
||||
'Aucun type défini',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.textPrimaryLight),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textPrimaryDark
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
isSuperAdmin
|
||||
? 'Créez votre premier type d\'organisation'
|
||||
: 'Aucun type d\'organisation disponible',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (isSuperAdmin) ...[
|
||||
@@ -229,8 +239,8 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Créer un type'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
@@ -249,17 +259,26 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 40, color: Colors.red[400]),
|
||||
Icon(Icons.error_outline, size: 40, color: AppColors.error),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Erreur de chargement', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: 6),
|
||||
Text(message, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight), textAlign: TextAlign.center),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.read<OrgTypesBloc>().add(const LoadOrgTypes()),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
@@ -303,8 +322,8 @@ class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
bloc.add(DeleteOrgTypeEvent(type.id));
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.error,
|
||||
foregroundColor: AppColors.onError,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
),
|
||||
@@ -370,12 +389,16 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEdit = widget.existing != null;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
final onSurface = Theme.of(context).colorScheme.onSurface;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Form(
|
||||
@@ -389,13 +412,13 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
|
||||
decoration: BoxDecoration(color: Theme.of(context).colorScheme.outline, borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
isEdit ? 'Modifier le type' : 'Nouveau type d\'organisation',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: AppColors.textPrimaryLight),
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: textPrimary),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
@@ -446,7 +469,7 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Color picker
|
||||
const Text('Couleur', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textSecondaryLight)),
|
||||
Text('Couleur', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: textSecondary)),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@@ -462,7 +485,7 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: selected ? Border.all(color: Colors.black, width: 2) : null,
|
||||
border: selected ? Border.all(color: onSurface, width: 2) : null,
|
||||
),
|
||||
child: selected ? const Icon(Icons.check, size: 14, color: Colors.white) : null,
|
||||
),
|
||||
@@ -477,8 +500,8 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
|
||||
@@ -11,10 +11,11 @@ import '../../bloc/organizations_event.dart';
|
||||
import '../../bloc/organizations_state.dart';
|
||||
import '../../domain/repositories/organization_repository.dart';
|
||||
import 'edit_organization_page.dart';
|
||||
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../../features/authentication/data/models/user_role.dart';
|
||||
|
||||
|
||||
class OrganizationDetailPage extends StatefulWidget {
|
||||
final String organizationId;
|
||||
|
||||
@@ -44,12 +45,10 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.brandGreen,
|
||||
foregroundColor: Colors.white,
|
||||
title: const Text('Détail Organisation'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'Détail Organisation',
|
||||
moduleGradient: ModuleColors.organisationsGradient,
|
||||
actions: [
|
||||
Builder(builder: (ctx) {
|
||||
final authState = ctx.read<AuthBloc>().state;
|
||||
@@ -67,15 +66,17 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
return PopupMenuButton<String>(
|
||||
onSelected: _handleMenuAction,
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: 'activate', child: Row(children: [Icon(Icons.check_circle, color: AppColors.success), SizedBox(width: 8), Text('Activer')])),
|
||||
const PopupMenuItem(value: 'deactivate', child: Row(children: [Icon(Icons.pause_circle, color: AppColors.textSecondaryLight), SizedBox(width: 8), Text('Désactiver')])),
|
||||
const PopupMenuItem(value: 'delete', child: Row(children: [Icon(Icons.delete, color: Colors.red), SizedBox(width: 8), Text('Supprimer')])),
|
||||
const PopupMenuItem(value: 'activate', child: Row(children: [Icon(Icons.check_circle, color: AppColors.success), SizedBox(width: SpacingTokens.md), Text('Activer')])),
|
||||
const PopupMenuItem(value: 'deactivate', child: Row(children: [Icon(Icons.pause_circle, color: AppColors.textSecondary), SizedBox(width: SpacingTokens.md), Text('Désactiver')])),
|
||||
const PopupMenuItem(value: 'delete', child: Row(children: [Icon(Icons.delete, color: AppColors.error), SizedBox(width: SpacingTokens.md), Text('Supprimer')])),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<OrganizationsBloc, OrganizationsState>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: BlocBuilder<OrganizationsBloc, OrganizationsState>(
|
||||
builder: (context, state) {
|
||||
if (state is OrganizationLoading) return _buildLoading();
|
||||
if (state is OrganizationLoaded) return _buildContent(state.organization);
|
||||
@@ -83,74 +84,82 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
return _buildEmpty();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(OrganizationModel org) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
return RefreshIndicator(
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderCard(org),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildInfoCard(org),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildContactCard(org),
|
||||
if (_hasAddress(org)) ...[const SizedBox(height: 8), _buildAddressCard(org)],
|
||||
const SizedBox(height: 8),
|
||||
if (_hasAddress(org)) ...[const SizedBox(height: SpacingTokens.md), _buildAddressCard(org)],
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildStatsCard(org),
|
||||
if (_hasFinances(org)) ...[const SizedBox(height: 8), _buildFinancesCard(org)],
|
||||
if (_hasMission(org)) ...[const SizedBox(height: 8), _buildMissionCard(org)],
|
||||
if (_hasSupplementary(org)) ...[const SizedBox(height: 8), _buildSupplementaryCard(org)],
|
||||
if (org.notes?.isNotEmpty == true) ...[const SizedBox(height: 8), _buildNotesCard(org)],
|
||||
const SizedBox(height: 8),
|
||||
if (_hasFinances(org)) ...[const SizedBox(height: SpacingTokens.md), _buildFinancesCard(org)],
|
||||
if (_hasMission(org)) ...[const SizedBox(height: SpacingTokens.md), _buildMissionCard(org)],
|
||||
if (_hasSupplementary(org)) ...[const SizedBox(height: SpacingTokens.md), _buildSupplementaryCard(org)],
|
||||
if (org.notes?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildNotesCard(org)],
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildActionsCard(org),
|
||||
],
|
||||
),
|
||||
);
|
||||
), // Column
|
||||
), // SingleChildScrollView
|
||||
); // RefreshIndicator
|
||||
}
|
||||
|
||||
// ── Header ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildHeaderCard(OrganizationModel org) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.brandGreen, AppColors.primaryGreen]),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 2))],
|
||||
gradient: const LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ModuleColors.organisationsDark, ModuleColors.organisations]),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
boxShadow: [BoxShadow(color: AppColors.shadowMedium, blurRadius: 8, offset: const Offset(0, 2))],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd)),
|
||||
child: const Icon(Icons.business_outlined, size: 24, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: SpacingTokens.xl),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(org.nom, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
|
||||
if (org.nomCourt?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 2),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(org.nomCourt!, style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(0.9))),
|
||||
],
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Row(children: [
|
||||
_buildWhiteBadge(org.typeOrganisationLibelle ?? org.typeOrganisation),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
_buildWhiteBadge(org.statutLibelle ?? org.statut.displayName),
|
||||
]),
|
||||
])),
|
||||
]),
|
||||
if (org.description?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
Text(org.description!, style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(0.9), height: 1.4)),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Row(children: [
|
||||
_buildBoolBadge(Icons.public, 'Public', org.organisationPublique),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
_buildBoolBadge(Icons.person_add, 'Ouvert', org.accepteNouveauxMembres),
|
||||
]),
|
||||
]),
|
||||
@@ -159,17 +168,19 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
|
||||
Widget _buildWhiteBadge(String text) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: 3),
|
||||
decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg)),
|
||||
child: Text(text, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBoolBadge(IconData icon, String label, bool value) {
|
||||
// Sur gradient : vert clair lisible pour TRUE, blanc translucide pour FALSE
|
||||
final color = value ? AppColors.successUI : Colors.white.withOpacity(0.6);
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(icon, size: 12, color: value ? Colors.greenAccent : Colors.white60),
|
||||
const SizedBox(width: 4),
|
||||
Text('$label: ${value ? 'Oui' : 'Non'}', style: TextStyle(fontSize: 11, color: value ? Colors.greenAccent : Colors.white60)),
|
||||
Icon(icon, size: 12, color: color),
|
||||
const SizedBox(width: SpacingTokens.sm),
|
||||
Text('$label: ${value ? 'Oui' : 'Non'}', style: TextStyle(fontSize: 11, color: color)),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -178,14 +189,14 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
Widget _buildInfoCard(OrganizationModel org) {
|
||||
return _buildCard('Informations générales', Icons.info_outline, [
|
||||
if (org.dateFondation != null) _buildInfoRow(Icons.cake, 'Date de fondation', _formatDate(org.dateFondation)),
|
||||
if (org.dateFondation != null) const SizedBox(height: 10),
|
||||
if (org.dateFondation != null) const SizedBox(height: SpacingTokens.md),
|
||||
if (org.ancienneteAnnees > 0) _buildInfoRow(Icons.access_time, 'Ancienneté', '${org.ancienneteAnnees} an(s)'),
|
||||
if (org.ancienneteAnnees > 0) const SizedBox(height: 10),
|
||||
if (org.ancienneteAnnees > 0) const SizedBox(height: SpacingTokens.md),
|
||||
if (org.numeroEnregistrement?.isNotEmpty == true) _buildInfoRow(Icons.assignment, 'N° d\'enregistrement', org.numeroEnregistrement!),
|
||||
if (org.numeroEnregistrement?.isNotEmpty == true) const SizedBox(height: 10),
|
||||
if (org.numeroEnregistrement?.isNotEmpty == true) const SizedBox(height: SpacingTokens.md),
|
||||
_buildInfoRow(Icons.calendar_today, 'Créé dans le système', _formatDate(org.dateCreation)),
|
||||
if (org.dateModification != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildInfoRow(Icons.edit_calendar, 'Dernière modification', _formatDate(org.dateModification)),
|
||||
],
|
||||
]);
|
||||
@@ -204,11 +215,11 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
|
||||
return _buildCard('Contact', Icons.contact_phone, [
|
||||
if (org.email?.isNotEmpty == true) _buildContactRow(Icons.email, 'Email', org.email!, onTap: () => _launchEmail(org.email!)),
|
||||
if (org.emailSecondaire?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildContactRow(Icons.alternate_email, 'Email secondaire', org.emailSecondaire!, onTap: () => _launchEmail(org.emailSecondaire!))],
|
||||
if (org.telephone?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildContactRow(Icons.phone, 'Téléphone', org.telephone!, onTap: () => _launchPhone(org.telephone!))],
|
||||
if (org.telephoneSecondaire?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildContactRow(Icons.phone_forwarded, 'Téléphone secondaire', org.telephoneSecondaire!, onTap: () => _launchPhone(org.telephoneSecondaire!))],
|
||||
if (org.siteWeb?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildContactRow(Icons.web, 'Site web', org.siteWeb!, onTap: () => _launchWebsite(org.siteWeb!))],
|
||||
if (org.reseauxSociaux?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildInfoRow(Icons.share, 'Réseaux sociaux', org.reseauxSociaux!)],
|
||||
if (org.emailSecondaire?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildContactRow(Icons.alternate_email, 'Email secondaire', org.emailSecondaire!, onTap: () => _launchEmail(org.emailSecondaire!))],
|
||||
if (org.telephone?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildContactRow(Icons.phone, 'Téléphone', org.telephone!, onTap: () => _launchPhone(org.telephone!))],
|
||||
if (org.telephoneSecondaire?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildContactRow(Icons.phone_forwarded, 'Téléphone secondaire', org.telephoneSecondaire!, onTap: () => _launchPhone(org.telephoneSecondaire!))],
|
||||
if (org.siteWeb?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildContactRow(Icons.web, 'Site web', org.siteWeb!, onTap: () => _launchWebsite(org.siteWeb!))],
|
||||
if (org.reseauxSociaux?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.share, 'Réseaux sociaux', org.reseauxSociaux!)],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -221,10 +232,10 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
Widget _buildAddressCard(OrganizationModel org) {
|
||||
return _buildCard('Localisation', Icons.location_on, [
|
||||
if (org.adresse?.isNotEmpty == true) _buildInfoRow(Icons.location_on, 'Adresse', org.adresse!),
|
||||
if (org.adresse?.isNotEmpty == true && (org.ville?.isNotEmpty == true)) const SizedBox(height: 10),
|
||||
if (org.adresse?.isNotEmpty == true && (org.ville?.isNotEmpty == true)) const SizedBox(height: SpacingTokens.md),
|
||||
if (org.ville?.isNotEmpty == true) _buildInfoRow(Icons.location_city, 'Ville', '${org.ville!}${org.codePostal?.isNotEmpty == true ? ' — ${org.codePostal}' : ''}'),
|
||||
if (org.region?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildInfoRow(Icons.map, 'Région', org.region!)],
|
||||
if (org.pays?.isNotEmpty == true) ...[const SizedBox(height: 10), _buildInfoRow(Icons.flag, 'Pays', org.pays!)],
|
||||
if (org.region?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.map, 'Région', org.region!)],
|
||||
if (org.pays?.isNotEmpty == true) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.flag, 'Pays', org.pays!)],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -233,10 +244,10 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
Widget _buildStatsCard(OrganizationModel org) {
|
||||
return _buildCard('Statistiques', Icons.bar_chart, [
|
||||
Row(children: [
|
||||
Expanded(child: _buildStatItem(Icons.people, 'Membres', (_memberCount ?? org.nombreMembres).toString(), AppColors.primaryGreen)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: _buildStatItem(Icons.admin_panel_settings, 'Admins', org.nombreAdministrateurs.toString(), AppColors.brandGreen)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: _buildStatItem(Icons.people, 'Membres', (_memberCount ?? org.nombreMembres).toString(), ModuleColors.organisations)),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Expanded(child: _buildStatItem(Icons.admin_panel_settings, 'Admins', org.nombreAdministrateurs.toString(), ModuleColors.organisationsDark)),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Expanded(child: _buildStatItem(Icons.event, 'Événements', (org.nombreEvenements ?? 0).toString(), AppColors.success)),
|
||||
]),
|
||||
]);
|
||||
@@ -244,11 +255,11 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
|
||||
Widget _buildStatItem(IconData icon, String label, String value, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withOpacity(0.15))),
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), border: Border.all(color: color.withOpacity(0.15))),
|
||||
child: Column(children: [
|
||||
Icon(icon, size: 20, color: color),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(value, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: color)),
|
||||
Text(label, style: TextStyle(fontSize: 11, color: color)),
|
||||
]),
|
||||
@@ -263,10 +274,10 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
Widget _buildFinancesCard(OrganizationModel org) {
|
||||
return _buildCard('Finances', Icons.account_balance_wallet, [
|
||||
_buildInfoRow(Icons.currency_exchange, 'Devise', org.devise),
|
||||
if (org.budgetAnnuel != null) ...[const SizedBox(height: 10), _buildInfoRow(Icons.account_balance, 'Budget annuel', '${_formatMontant(org.budgetAnnuel)} ${org.devise}')],
|
||||
const SizedBox(height: 10),
|
||||
if (org.budgetAnnuel != null) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.account_balance, 'Budget annuel', '${_formatMontant(org.budgetAnnuel)} ${org.devise}')],
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildInfoRow(Icons.payments, 'Cotisation', org.cotisationObligatoire ? 'Obligatoire' : 'Facultative'),
|
||||
if (org.cotisationObligatoire && org.montantCotisationAnnuelle != null) ...[const SizedBox(height: 10), _buildInfoRow(Icons.money, 'Montant annuel', '${_formatMontant(org.montantCotisationAnnuelle)} ${org.devise}')],
|
||||
if (org.cotisationObligatoire && org.montantCotisationAnnuelle != null) ...[const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.money, 'Montant annuel', '${_formatMontant(org.montantCotisationAnnuelle)} ${org.devise}')],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -278,7 +289,7 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
Widget _buildMissionCard(OrganizationModel org) {
|
||||
return _buildCard('Mission & Activités', Icons.flag, [
|
||||
if (org.objectifs?.isNotEmpty == true) _buildTextBlock(Icons.track_changes, 'Objectifs', org.objectifs!),
|
||||
if (org.objectifs?.isNotEmpty == true && org.activitesPrincipales?.isNotEmpty == true) const SizedBox(height: 12),
|
||||
if (org.objectifs?.isNotEmpty == true && org.activitesPrincipales?.isNotEmpty == true) const SizedBox(height: SpacingTokens.lg),
|
||||
if (org.activitesPrincipales?.isNotEmpty == true) _buildTextBlock(Icons.work, 'Activités principales', org.activitesPrincipales!),
|
||||
]);
|
||||
}
|
||||
@@ -291,7 +302,7 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
Widget _buildSupplementaryCard(OrganizationModel org) {
|
||||
return _buildCard('Informations complémentaires', Icons.info, [
|
||||
if (org.certifications?.isNotEmpty == true) _buildTextBlock(Icons.verified, 'Certifications / Agréments', org.certifications!),
|
||||
if (org.certifications?.isNotEmpty == true && org.partenaires?.isNotEmpty == true) const SizedBox(height: 12),
|
||||
if (org.certifications?.isNotEmpty == true && org.partenaires?.isNotEmpty == true) const SizedBox(height: SpacingTokens.lg),
|
||||
if (org.partenaires?.isNotEmpty == true) _buildTextBlock(Icons.handshake, 'Partenaires', org.partenaires!),
|
||||
]);
|
||||
}
|
||||
@@ -299,20 +310,34 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
// ── Notes internes ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildNotesCard(OrganizationModel org) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFFBEB),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFFCD34D), width: 1),
|
||||
// Fond warning adaptatif (jaune clair light / surface sombre dark)
|
||||
color: isDark
|
||||
? AppColors.surfaceVariantDark
|
||||
: AppColors.warningContainer,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
border: Border.all(color: AppColors.warningUI, width: 1),
|
||||
),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Icon(Icons.sticky_note_2, size: 18, color: Color(0xFFF59E0B)),
|
||||
const SizedBox(width: 10),
|
||||
const Icon(Icons.sticky_note_2, size: 18, color: AppColors.warningUI),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Text('Notes internes', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFFF59E0B))),
|
||||
const SizedBox(height: 4),
|
||||
Text(org.notes!, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, height: 1.4)),
|
||||
Text('Notes internes',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? AppColors.warningUI : AppColors.warning,
|
||||
)),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(org.notes!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1.4,
|
||||
)),
|
||||
])),
|
||||
]),
|
||||
);
|
||||
@@ -337,15 +362,15 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
onPressed: _showEditPage,
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Modifier'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: ModuleColors.organisations, foregroundColor: AppColors.onPrimary),
|
||||
)),
|
||||
if (isSuperAdmin) ...[
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: SpacingTokens.lg),
|
||||
Expanded(child: OutlinedButton.icon(
|
||||
onPressed: () => _showDeleteConfirmation(org),
|
||||
icon: const Icon(Icons.delete),
|
||||
label: const Text('Supprimer'),
|
||||
style: OutlinedButton.styleFrom(foregroundColor: Colors.red, side: const BorderSide(color: Colors.red)),
|
||||
style: OutlinedButton.styleFrom(foregroundColor: AppColors.error, side: const BorderSide(color: AppColors.error)),
|
||||
)),
|
||||
],
|
||||
]),
|
||||
@@ -356,15 +381,19 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
|
||||
Widget _buildCard(String title, IconData icon, List<Widget> children) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant, width: 1),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Icon(icon, size: 15, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 6),
|
||||
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primaryGreen)),
|
||||
Icon(icon, size: 15, color: ModuleColors.organisations),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: ModuleColors.organisations)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
...children,
|
||||
]),
|
||||
);
|
||||
@@ -372,12 +401,12 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
|
||||
Widget _buildInfoRow(IconData icon, String label, String value) {
|
||||
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Icon(icon, size: 18, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 10),
|
||||
Icon(icon, size: 18, color: ModuleColors.organisations),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, fontWeight: FontWeight.w600)),
|
||||
Text(label, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(value, style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600)),
|
||||
])),
|
||||
]);
|
||||
}
|
||||
@@ -385,29 +414,29 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
Widget _buildTextBlock(IconData icon, String label, String value) {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Icon(icon, size: 15, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondaryLight, fontWeight: FontWeight.w600)),
|
||||
Icon(icon, size: 15, color: ModuleColors.organisations),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Text(label, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600)),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, height: 1.5)),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(value, style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurface, height: 1.5)),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildContactRow(IconData icon, String label, String value, {VoidCallback? onTap}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(children: [
|
||||
Icon(icon, size: 18, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 10),
|
||||
Icon(icon, size: 18, color: ModuleColors.organisations),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight)),
|
||||
Text(value, style: TextStyle(fontSize: 13, color: onTap != null ? AppColors.primaryGreen : AppColors.textPrimaryLight, fontWeight: FontWeight.w600, decoration: onTap != null ? TextDecoration.underline : null)),
|
||||
Text(label, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
Text(value, style: TextStyle(fontSize: 13, color: onTap != null ? ModuleColors.organisations : Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600, decoration: onTap != null ? TextDecoration.underline : null)),
|
||||
])),
|
||||
if (onTap != null) const Icon(Icons.open_in_new, size: 14, color: AppColors.primaryGreen),
|
||||
if (onTap != null) const Icon(Icons.open_in_new, size: 14, color: ModuleColors.organisations),
|
||||
]),
|
||||
),
|
||||
);
|
||||
@@ -416,28 +445,28 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
// ── États ────────────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildLoading() => const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen)),
|
||||
SizedBox(height: 16),
|
||||
Text('Chargement...', style: TextStyle(color: AppColors.textSecondaryLight)),
|
||||
CircularProgressIndicator(color: ModuleColors.organisations),
|
||||
SizedBox(height: SpacingTokens.xl),
|
||||
Text('Chargement...'),
|
||||
]));
|
||||
|
||||
Widget _buildError(OrganizationsError state) => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.red.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.message, textAlign: TextAlign.center, style: const TextStyle(color: AppColors.textSecondaryLight)),
|
||||
Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.error),
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
Text(state.message, textAlign: TextAlign.center, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
FilledButton.icon(
|
||||
onPressed: () => context.read<OrganizationsBloc>().add(LoadOrganizationById(widget.organizationId)),
|
||||
icon: const Icon(Icons.refresh),
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white),
|
||||
style: FilledButton.styleFrom(backgroundColor: ModuleColors.organisations),
|
||||
),
|
||||
]));
|
||||
|
||||
Widget _buildEmpty() => const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.business_outlined, size: 64, color: AppColors.textSecondaryLight),
|
||||
SizedBox(height: 16),
|
||||
Text('Organisation non trouvée', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimaryLight)),
|
||||
Widget _buildEmpty() => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.business_outlined, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
Text('Organisation non trouvée', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)),
|
||||
]));
|
||||
|
||||
// ── Actions ──────────────────────────────────────────────────────────────────
|
||||
@@ -475,8 +504,8 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Annuler')),
|
||||
ElevatedButton(
|
||||
onPressed: () { Navigator.of(ctx).pop(); bloc.add(DeleteOrganization(widget.organizationId)); nav.pop(); },
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: const Text('Supprimer', style: TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.error),
|
||||
child: const Text('Supprimer', style: TextStyle(color: AppColors.onPrimary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -10,9 +10,11 @@ import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../shared/widgets/info_badge.dart';
|
||||
import '../../../../shared/widgets/mini_avatar.dart';
|
||||
import '../../../../core/config/environment.dart';
|
||||
import '../../../../core/l10n/locale_provider.dart';
|
||||
import '../../../../core/theme/theme_provider.dart';
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
@@ -66,15 +68,22 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
// Infos app (chargées via package_info_plus)
|
||||
String _appVersion = '...';
|
||||
String _platformInfo = '...';
|
||||
String _osVersion = '...';
|
||||
String _envInfo = '...';
|
||||
// Tailles stockage (calculées au chargement)
|
||||
String _cacheSize = '...';
|
||||
String _imagesSize = '...';
|
||||
String _offlineSize = '...';
|
||||
// Options développeur (persistées via SharedPreferences)
|
||||
bool _devMode = false;
|
||||
bool _detailedLogs = false;
|
||||
final List<String> _themes = ['Système', 'Clair', 'Sombre'];
|
||||
|
||||
static const _keyNotifPush = 'notif_push';
|
||||
static const _keyNotifEmail = 'notif_email';
|
||||
static const _keyNotifSon = 'notif_son';
|
||||
static const _keyDevMode = 'dev_mode';
|
||||
static const _keyDetailedLogs = 'detailed_logs';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -136,26 +145,39 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'MON PROFIL',
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
foregroundColor: Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textPrimaryDark
|
||||
: AppColors.textPrimaryLight,
|
||||
title: 'Mon Profil',
|
||||
moduleGradient: ModuleColors.profilGradient,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_isEditing ? Icons.save_outlined : Icons.edit_outlined, size: 20),
|
||||
onPressed: () => _isEditing ? _saveProfile() : _startEditing(),
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(child: Text('PERSO')),
|
||||
Tab(child: Text('PRÉFÉRENCES')),
|
||||
Tab(child: Text('SÉCURITÉ')),
|
||||
Tab(child: Text('AVANCÉ')),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 8),
|
||||
_buildTabBar(),
|
||||
SizedBox(
|
||||
height: 600, // Ajuster selon contenu ou utiliser NestedScrollView
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: _buildHeader(),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
@@ -169,6 +191,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,6 +199,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Widget _buildHeader() {
|
||||
return BlocBuilder<ProfileBloc, ProfileState>(
|
||||
builder: (context, state) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final membre = (state is ProfileLoaded)
|
||||
? state.membre
|
||||
: (state is ProfileUpdated ? state.membre
|
||||
@@ -189,39 +213,39 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Color badgeColor;
|
||||
if (isLoading) {
|
||||
badgeText = 'CHARGEMENT...';
|
||||
badgeColor = AppColors.textSecondaryLight;
|
||||
badgeColor = ColorTokens.textSecondary;
|
||||
} else if (hasError) {
|
||||
badgeText = 'ERREUR CHARGEMENT';
|
||||
badgeColor = AppColors.error;
|
||||
badgeColor = ColorTokens.error;
|
||||
} else if (membre != null) {
|
||||
// Priorité au rôle s'il est disponible
|
||||
if (membre.role != null && membre.role!.isNotEmpty) {
|
||||
badgeText = membre.role!.replaceAll('_', ' ');
|
||||
badgeColor = AppColors.primaryGreen;
|
||||
badgeColor = ModuleColors.profil;
|
||||
} else {
|
||||
// Fallback sur le statut
|
||||
switch (membre.statut) {
|
||||
case StatutMembre.actif:
|
||||
badgeText = membre.cotisationAJour ? 'MEMBRE ACTIF' : 'COTISATION EN RETARD';
|
||||
badgeColor = membre.cotisationAJour ? AppColors.success : AppColors.warning;
|
||||
badgeColor = membre.cotisationAJour ? ColorTokens.success : ColorTokens.warning;
|
||||
break;
|
||||
case StatutMembre.inactif:
|
||||
badgeText = 'INACTIF';
|
||||
badgeColor = AppColors.textSecondaryLight;
|
||||
badgeColor = ColorTokens.textSecondary;
|
||||
break;
|
||||
case StatutMembre.suspendu:
|
||||
badgeText = 'SUSPENDU';
|
||||
badgeColor = AppColors.error;
|
||||
badgeColor = ColorTokens.error;
|
||||
break;
|
||||
case StatutMembre.enAttente:
|
||||
badgeText = 'EN ATTENTE';
|
||||
badgeColor = AppColors.warning;
|
||||
badgeColor = ColorTokens.warning;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
badgeText = 'CHARGEMENT...';
|
||||
badgeColor = AppColors.textSecondaryLight;
|
||||
badgeColor = ColorTokens.textSecondary;
|
||||
}
|
||||
|
||||
// Ancienneté réelle
|
||||
@@ -274,7 +298,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
),
|
||||
Text(
|
||||
_emailController.text.toLowerCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 11),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 11, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (isLoading)
|
||||
@@ -291,7 +315,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: Text(
|
||||
'Réessayer',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
color: ModuleColors.profil,
|
||||
fontSize: 10,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -344,10 +368,11 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String label, String value) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
children: [
|
||||
Text(value, style: AppTypography.headerSmall.copyWith(fontSize: 14)),
|
||||
Text(label, style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
|
||||
Text(label, style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold, color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -388,35 +413,16 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
);
|
||||
}
|
||||
|
||||
/// Barre d'onglets
|
||||
Widget _buildTabBar() {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(text: 'PERSO'),
|
||||
Tab(text: 'PRÉF'),
|
||||
Tab(text: 'SÉCU'),
|
||||
Tab(text: 'AVANCÉ'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// _buildTabBar() supprimé : migré dans UFAppBar.bottom (pattern Adhésions)
|
||||
|
||||
/// Onglet informations personnelles
|
||||
Widget _buildPersonalInfoTab() {
|
||||
return SingleChildScrollView(
|
||||
return RefreshIndicator(
|
||||
color: ModuleColors.profil,
|
||||
onRefresh: () async =>
|
||||
context.read<ProfileBloc>().add(const LoadMe()),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -533,8 +539,9 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
);
|
||||
), // Column
|
||||
), // SingleChildScrollView
|
||||
); // RefreshIndicator
|
||||
}
|
||||
|
||||
/// Section d'informations
|
||||
@@ -551,7 +558,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
Icon(icon, color: ModuleColors.profil, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
@@ -579,7 +586,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
int maxLines = 1,
|
||||
String? hintText,
|
||||
}) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
@@ -587,35 +594,34 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
maxLines: maxLines,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 12,
|
||||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: label.toUpperCase(),
|
||||
labelStyle: AppTypography.subtitleSmall.copyWith(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
hintText: hintText,
|
||||
prefixIcon: Icon(icon, color: enabled ? AppColors.primaryGreen : AppColors.textSecondaryLight, size: 16),
|
||||
prefixIcon: Icon(icon, color: enabled ? ModuleColors.profil : ColorTokens.textSecondary, size: 16),
|
||||
filled: true,
|
||||
fillColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
fillColor: scheme.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(color: AppColors.primaryGreen, width: 1),
|
||||
borderSide: BorderSide(color: ModuleColors.profil, width: 1),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
@@ -631,15 +637,15 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
|
||||
/// Boutons d'action
|
||||
Widget _buildActionButtons() {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : Colors.white,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -652,8 +658,8 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isLoading ? null : _cancelEditing,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[100],
|
||||
foregroundColor: Colors.grey[700],
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
foregroundColor: AppColors.textSecondary,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
@@ -666,7 +672,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isLoading ? null : _saveProfile,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: ModuleColors.profil,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@@ -689,7 +695,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _startEditing,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: ModuleColors.profil,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@@ -720,9 +726,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
[
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
final isDark = Theme.of(ctx).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(ctx).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
await Navigator.of(context).push(
|
||||
@@ -738,18 +742,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.language, color: textSecondary, size: 22),
|
||||
Icon(Icons.language, color: scheme.onSurfaceVariant, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)),
|
||||
Text('Actuellement : $_selectedLanguage', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)),
|
||||
Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)),
|
||||
Text('Actuellement : $_selectedLanguage', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: textSecondary, size: 20),
|
||||
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -823,9 +827,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
[
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
final isDark = Theme.of(ctx).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(ctx).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
@@ -837,18 +839,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.privacy_tip, color: textSecondary, size: 22),
|
||||
Icon(Icons.privacy_tip, color: scheme.onSurfaceVariant, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Gérer la confidentialité', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)),
|
||||
Text('Visibilité, partage de données, suppression de compte', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)),
|
||||
Text('Gérer la confidentialité', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)),
|
||||
Text('Visibilité, partage de données, suppression de compte', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: textSecondary, size: 20),
|
||||
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -950,21 +952,21 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
'Télécharger mes données',
|
||||
'Exporter toutes vos données personnelles',
|
||||
Icons.download,
|
||||
AppColors.primaryGreen,
|
||||
ModuleColors.profil,
|
||||
() => _exportUserData(),
|
||||
),
|
||||
_buildActionItem(
|
||||
'Déconnecter tous les appareils',
|
||||
'Fermer toutes les sessions actives',
|
||||
Icons.logout,
|
||||
AppColors.warning,
|
||||
ColorTokens.warning,
|
||||
() => _logoutAllDevices(),
|
||||
),
|
||||
_buildActionItem(
|
||||
'Supprimer mon compte',
|
||||
'Action irréversible - toutes les données seront perdues',
|
||||
Icons.delete_forever,
|
||||
Colors.red,
|
||||
ColorTokens.error,
|
||||
() => _showDeleteAccountDialog(),
|
||||
),
|
||||
],
|
||||
@@ -1019,14 +1021,22 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_buildSwitchPreference(
|
||||
'Mode développeur',
|
||||
'Afficher les options de débogage',
|
||||
false,
|
||||
(value) => _showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}'),
|
||||
_devMode,
|
||||
(value) async {
|
||||
setState(() => _devMode = value);
|
||||
await _savePreference(_keyDevMode, value);
|
||||
_showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}');
|
||||
},
|
||||
),
|
||||
_buildSwitchPreference(
|
||||
'Logs détaillés',
|
||||
'Enregistrer plus d\'informations de débogage',
|
||||
false,
|
||||
(value) => _showSuccessSnackBar('Logs détaillés ${value ? 'activés' : 'désactivés'}'),
|
||||
'Logs réseau et erreurs — actif au prochain démarrage',
|
||||
_detailedLogs,
|
||||
(value) async {
|
||||
setState(() => _detailedLogs = value);
|
||||
await _savePreference(_keyDetailedLogs, value);
|
||||
_showSuccessSnackBar('Logs détaillés ${value ? 'activés' : 'désactivés'} (prochain démarrage)');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1041,6 +1051,9 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
[
|
||||
_buildInfoItem('Version de l\'app', _appVersion),
|
||||
_buildInfoItem('Plateforme', _platformInfo),
|
||||
_buildInfoItem('Système', _osVersion),
|
||||
_buildInfoItem('Environnement', _envInfo),
|
||||
if (_devMode) _buildInfoItem('API URL', AppConfig.apiBaseUrl),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1054,9 +1067,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
[
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
final isDark = Theme.of(ctx).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(ctx).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
@@ -1068,18 +1079,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit_note, color: textSecondary, size: 22),
|
||||
Icon(Icons.edit_note, color: scheme.onSurfaceVariant, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Envoyer des commentaires', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)),
|
||||
Text('Suggestions, bugs ou idées d\'amélioration', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)),
|
||||
Text('Envoyer des commentaires', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)),
|
||||
Text('Suggestions, bugs ou idées d\'amélioration', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: textSecondary, size: 20),
|
||||
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1113,7 +1124,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
Icon(icon, color: ModuleColors.profil, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
@@ -1159,7 +1170,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
List<String> options,
|
||||
Function(String?) onChanged,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1171,21 +1182,31 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: value,
|
||||
isExpanded: true,
|
||||
onChanged: onChanged,
|
||||
icon: const Icon(Icons.arrow_drop_down, color: AppColors.primaryGreen, size: 18),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
|
||||
dropdownColor: scheme.surface,
|
||||
icon: Icon(Icons.arrow_drop_down, color: ModuleColors.profil, size: 18),
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 12,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
items: options.map((option) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: option,
|
||||
child: Text(option),
|
||||
child: Text(
|
||||
option,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 12,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
@@ -1202,7 +1223,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
bool value,
|
||||
Function(bool) onChanged,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -1217,7 +1238,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
subtitle,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 10,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1228,7 +1249,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: AppColors.primaryGreen,
|
||||
activeColor: ModuleColors.profil,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1242,21 +1263,20 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
IconData icon,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
Icon(icon, color: ModuleColors.profil, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -1268,12 +1288,12 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: textSecondary),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, color: textSecondary, size: 12),
|
||||
Icon(Icons.arrow_forward_ios, color: scheme.onSurfaceVariant, size: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1287,19 +1307,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
IconData icon,
|
||||
bool isCurrentDevice,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrentDevice
|
||||
? AppColors.primaryGreen.withOpacity(0.05)
|
||||
: (isDark ? AppColors.darkSurface : AppColors.lightSurface),
|
||||
? ModuleColors.profil.withOpacity(0.05)
|
||||
: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isCurrentDevice
|
||||
? AppColors.primaryGreen.withOpacity(0.3)
|
||||
: (isDark ? AppColors.darkBorder : AppColors.lightBorder),
|
||||
? ModuleColors.profil.withOpacity(0.3)
|
||||
: scheme.outlineVariant,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
@@ -1307,7 +1326,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isCurrentDevice ? AppColors.primaryGreen : textSecondary,
|
||||
color: isCurrentDevice ? ModuleColors.profil : scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -1323,13 +1342,13 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
),
|
||||
if (isCurrentDevice) ...[
|
||||
const SizedBox(width: 8),
|
||||
const InfoBadge(text: 'ACTUEL', backgroundColor: AppColors.primaryGreen),
|
||||
InfoBadge(text: 'ACTUEL', backgroundColor: ModuleColors.profil),
|
||||
],
|
||||
],
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: textSecondary),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1347,7 +1366,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Color color,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
@@ -1374,7 +1393,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
subtitle,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 10,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1389,20 +1408,20 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
|
||||
/// Élément de stockage
|
||||
Widget _buildStorageItem(String title, String size, VoidCallback onTap) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.folder_outlined, color: AppColors.primaryGreen, size: 16),
|
||||
Icon(Icons.folder_outlined, color: ModuleColors.profil, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -1412,10 +1431,10 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
),
|
||||
Text(
|
||||
size,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.clear, color: AppColors.error, size: 14),
|
||||
Icon(Icons.clear, color: ColorTokens.error, size: 14),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1424,20 +1443,20 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
|
||||
/// Élément d'information
|
||||
Widget _buildInfoItem(String title, String value) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -1565,7 +1584,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
);
|
||||
}
|
||||
|
||||
/// Charger les préférences notifications depuis SharedPreferences
|
||||
/// Charger les préférences depuis SharedPreferences
|
||||
Future<void> _loadPreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
@@ -1573,6 +1592,8 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_notifPush = prefs.getBool(_keyNotifPush) ?? true;
|
||||
_notifEmail = prefs.getBool(_keyNotifEmail) ?? false;
|
||||
_notifSon = prefs.getBool(_keyNotifSon) ?? true;
|
||||
_devMode = prefs.getBool(_keyDevMode) ?? AppConfig.enableDebugMode;
|
||||
_detailedLogs = prefs.getBool(_keyDetailedLogs) ?? AppConfig.enableLogging;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1588,7 +1609,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_appVersion = '${info.version} (Build ${info.buildNumber})';
|
||||
_appVersion = '${info.version} (build ${info.buildNumber})';
|
||||
final os = kIsWeb
|
||||
? 'Web'
|
||||
: Platform.isAndroid
|
||||
@@ -1597,6 +1618,10 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
? 'iOS'
|
||||
: Platform.operatingSystem;
|
||||
_platformInfo = '$os · ${info.appName}';
|
||||
_osVersion = kIsWeb
|
||||
? 'Navigateur web'
|
||||
: Platform.operatingSystemVersion;
|
||||
_envInfo = AppConfig.environment.name.toUpperCase();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1684,7 +1709,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
confirmPassCtrl.dispose();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: ModuleColors.profil,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Modifier'),
|
||||
@@ -1782,7 +1807,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_showFinalDeleteConfirmation();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: ColorTokens.error,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Continuer'),
|
||||
@@ -1836,7 +1861,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
confirmCtrl.dispose();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: ColorTokens.error,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('SUPPRIMER DÉFINITIVEMENT'),
|
||||
@@ -1953,7 +1978,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
message.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
backgroundColor: ColorTokens.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
@@ -1967,7 +1992,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
message.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: AppColors.error,
|
||||
backgroundColor: ColorTokens.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
|
||||
@@ -58,7 +58,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
}
|
||||
if (state is ReportsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.message), backgroundColor: Colors.orange),
|
||||
SnackBar(content: Text(state.message), backgroundColor: AppColors.warning),
|
||||
);
|
||||
}
|
||||
if (state is ReportScheduled) {
|
||||
@@ -74,18 +74,49 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
body: Column(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'Rapports & Analytics',
|
||||
moduleGradient: ModuleColors.rapportsGradient,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _showExportDialog(),
|
||||
icon: const Icon(Icons.file_download_outlined, size: 20),
|
||||
tooltip: 'Exporter',
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(child: Text('GLOBAL')),
|
||||
Tab(child: Text('MEMBRES')),
|
||||
Tab(child: Text('ORGS')),
|
||||
Tab(child: Text('EVENTS')),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildTabBar(),
|
||||
if (state is ReportsLoading)
|
||||
const LinearProgressIndicator(
|
||||
minHeight: 2,
|
||||
backgroundColor: Colors.transparent,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primary),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
color: ModuleColors.rapports,
|
||||
onRefresh: () async => context
|
||||
.read<ReportsBloc>()
|
||||
.add(const LoadDashboardReports()),
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
@@ -96,162 +127,22 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 12,
|
||||
bottom: 16,
|
||||
left: 12,
|
||||
right: 12,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColors.primaryGreen,
|
||||
AppColors.brandGreen,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(32),
|
||||
bottomRight: Radius.circular(32),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'UnionFlow Analytics'.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Rapports & Insights',
|
||||
style: AppTypography.headerSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => _showExportDialog(),
|
||||
icon: const Icon(Icons.file_download_outlined, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildHeaderStat(
|
||||
'Membres',
|
||||
_statsMembres['total']?.toString() ?? '...',
|
||||
Icons.people_outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildHeaderStat(
|
||||
'Organisations',
|
||||
_statsMembres['totalOrganisations']?.toString() ?? '...',
|
||||
Icons.business_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildHeaderStat(
|
||||
'Événements',
|
||||
_statsEvenements['total']?.toString() ?? '...',
|
||||
Icons.event_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderStat(String label, String value, IconData icon) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 18),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: AppTypography.headerSmall.copyWith(fontSize: 18),
|
||||
),
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.lightBackground,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
dividerColor: Colors.transparent,
|
||||
labelStyle: AppTypography.badgeText.copyWith(fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(text: 'GLOBAL'),
|
||||
Tab(text: 'MEMBRES'),
|
||||
Tab(text: 'ORGS'),
|
||||
Tab(text: 'EVENTS'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// _buildHeader(), _buildHeaderStat() et _buildTabBar() supprimés :
|
||||
// titre + action Export migrés dans UFAppBar, TabBar dans UFAppBar.bottom.
|
||||
// Les KPIs sont toujours disponibles dans _buildKPICards() (onglet Global).
|
||||
|
||||
Widget _buildOverviewTab() {
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: [
|
||||
_buildKPICards(),
|
||||
@@ -282,7 +173,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildKPICard('Cotisations', totalCotisations, Icons.payments_outlined, AppColors.brandGreen)),
|
||||
Expanded(child: _buildKPICard('Cotisations', totalCotisations, Icons.payments_outlined, AppColors.primaryDark)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _buildKPICard('Événements', totalEvenements, Icons.event_available_outlined, AppColors.warning)),
|
||||
],
|
||||
@@ -332,7 +223,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.analytics_outlined, color: AppColors.primaryGreen, size: 20),
|
||||
const Icon(Icons.analytics_outlined, color: AppColors.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Évolution de l\'Activité'.toUpperCase(),
|
||||
@@ -341,26 +232,29 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
Builder(builder: (context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Container(
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.lightBorder.withOpacity(0.1),
|
||||
color: (isDark ? AppColors.borderDark : AppColors.border).withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Center(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.auto_graph_outlined, color: AppColors.textSecondaryLight, size: 40),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Visualisation graphique en préparation',
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
Icon(Icons.auto_graph_outlined,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
|
||||
size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text('Visualisation graphique en préparation',
|
||||
style: AppTypography.subtitleSmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -400,19 +294,19 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.lightBorder.withOpacity(0.05),
|
||||
color: AppColors.border.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)),
|
||||
border: Border.all(color: AppColors.border.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withOpacity(0.1),
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: AppColors.primaryGreen, size: 20),
|
||||
child: Icon(icon, color: AppColors.primary, size: 20),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -425,7 +319,11 @@ class _ReportsPageState extends State<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
|
||||
Widget _buildMembersTab() {
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -473,9 +372,9 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildStatItem('Total', total)),
|
||||
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
|
||||
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
|
||||
Expanded(child: _buildStatItem('Nouveaux', nouveaux)),
|
||||
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
|
||||
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
|
||||
Expanded(child: _buildStatItem('Actifs %', actifs)),
|
||||
],
|
||||
),
|
||||
@@ -512,6 +411,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
/// Onglet organisations
|
||||
Widget _buildOrganizationsTab() {
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -537,7 +437,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.business_center_outlined, color: AppColors.primaryGreen, size: 20),
|
||||
const Icon(Icons.business_center_outlined, color: AppColors.primary, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Indicateurs Organisations'.toUpperCase(),
|
||||
@@ -549,9 +449,9 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildStatItem('Total', total)),
|
||||
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
|
||||
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
|
||||
Expanded(child: _buildStatItem('Actives', actives)),
|
||||
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
|
||||
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
|
||||
Expanded(child: _buildStatItem('Membres moy.', moy)),
|
||||
],
|
||||
),
|
||||
@@ -568,7 +468,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.folder_shared_outlined, color: AppColors.primaryGreen, size: 20),
|
||||
const Icon(Icons.folder_shared_outlined, color: AppColors.primary, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Rapports Structures'.toUpperCase(),
|
||||
@@ -588,6 +488,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
/// Onglet événements
|
||||
Widget _buildEventsTab() {
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -625,9 +526,9 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildStatItem('Total', total)),
|
||||
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
|
||||
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
|
||||
Expanded(child: _buildStatItem('À Venir', venir)),
|
||||
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
|
||||
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
|
||||
Expanded(child: _buildStatItem('Part. moyenne', participation)),
|
||||
],
|
||||
),
|
||||
@@ -667,7 +568,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: AppTypography.headerSmall.copyWith(color: AppColors.primaryGreen, fontWeight: FontWeight.bold),
|
||||
style: AppTypography.headerSmall.copyWith(color: AppColors.primary, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
@@ -688,19 +589,19 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.lightBorder.withOpacity(0.05),
|
||||
color: AppColors.border.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)),
|
||||
border: Border.all(color: AppColors.border.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withOpacity(0.1),
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: AppColors.primaryGreen, size: 20),
|
||||
child: Icon(icon, color: AppColors.primary, size: 20),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -713,7 +614,11 @@ class _ReportsPageState extends State<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();
|
||||
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'),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -28,18 +28,19 @@ class _DemandeAideDetailPageState extends State<DemandeAideDetailPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: const UFAppBar(
|
||||
title: 'DÉTAIL DEMANDE',
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'Détail Demande',
|
||||
moduleGradient: ModuleColors.solidariteGradient,
|
||||
),
|
||||
body: BlocConsumer<SolidarityBloc, SolidarityState>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: BlocConsumer<SolidarityBloc, SolidarityState>(
|
||||
listenWhen: (prev, curr) => prev.status != curr.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == SolidarityStatus.error && state.message != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.message!), backgroundColor: Colors.red),
|
||||
SnackBar(content: Text(state.message!), backgroundColor: AppColors.error),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -55,7 +56,7 @@ class _DemandeAideDetailPageState extends State<DemandeAideDetailPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 64, color: Colors.grey),
|
||||
const Icon(Icons.error_outline, size: 64, color: AppColors.textTertiary),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Demande introuvable',
|
||||
@@ -65,7 +66,12 @@ class _DemandeAideDetailPageState extends State<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),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -97,10 +103,12 @@ class _DemandeAideDetailPageState extends State<DemandeAideDetailPage> {
|
||||
_ActionsSection(demande: d, isGestionnaire: _isGestionnaire()),
|
||||
],
|
||||
),
|
||||
);
|
||||
), // SingleChildScrollView
|
||||
); // RefreshIndicator
|
||||
},
|
||||
),
|
||||
);
|
||||
), // BlocConsumer
|
||||
), // SafeArea
|
||||
); // Scaffold
|
||||
}
|
||||
|
||||
bool _isGestionnaire() {
|
||||
@@ -136,7 +144,7 @@ class _InfoCard extends StatelessWidget {
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 9,
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
|
||||
@@ -76,17 +76,16 @@ class _DemandesAidePageState extends State<DemandesAidePage>
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'SOLIDARITÉ',
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
title: 'Solidarité',
|
||||
moduleGradient: ModuleColors.solidariteGradient,
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
onTap: _loadTab,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
@@ -125,7 +124,13 @@ class _DemandesAidePageState extends State<DemandesAidePage>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.volunteer_activism_outlined, size: 32, color: AppColors.lightBorder),
|
||||
Icon(
|
||||
Icons.volunteer_activism_outlined,
|
||||
size: 32,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.borderDark
|
||||
: AppColors.border,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('Aucune demande', style: AppTypography.subtitleSmall),
|
||||
],
|
||||
@@ -204,7 +209,7 @@ class _DemandeCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStatutBadge(demande.statut),
|
||||
_buildStatutBadge(context, demande.statut),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -217,7 +222,7 @@ class _DemandeCard extends StatelessWidget {
|
||||
Text('MONTANT DEMANDÉ', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
currencyFormat.format(demande.montantDemande ?? 0),
|
||||
style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primaryGreen),
|
||||
style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -235,7 +240,7 @@ class _DemandeCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatutBadge(String? statut) {
|
||||
Widget _buildStatutBadge(BuildContext context, String? statut) {
|
||||
Color color;
|
||||
switch (statut) {
|
||||
case 'APPROUVEE':
|
||||
@@ -246,10 +251,12 @@ class _DemandeCard extends StatelessWidget {
|
||||
break;
|
||||
case 'EN_ATTENTE':
|
||||
case 'SOUMISE':
|
||||
color = AppColors.brandGreenLight;
|
||||
color = AppColors.primaryLight;
|
||||
break;
|
||||
default:
|
||||
color = AppColors.textSecondaryLight;
|
||||
color = Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textSecondary;
|
||||
}
|
||||
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user