Files
unionflow-mobile-apps/lib/core/navigation/more_page.dart
dahoud 70cbd1c873 fix(mobile): URL changement mdp corrigée + v3.0 — multi-org, AppAuth, sécurité prod
Auth:
- profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password

Multi-org (Phase 3):
- OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry
- org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role

Navigation:
- MorePage: navigation conditionnelle par typeOrganisation
- Suppression adaptive_navigation (remplacé par main_navigation_layout)

Auth AppAuth:
- keycloak_webview_auth_service: fixes AppAuth Android
- AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet

Onboarding:
- Nouveaux états: payment_method_page, onboarding_shared_widgets
- SouscriptionStatusModel mis à jour StatutValidationSouscription

Android:
- build.gradle: ProGuard/R8, network_security_config
- Gradle wrapper mis à jour
2026-04-07 20:56:03 +00:00

480 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../features/authentication/data/models/user_role.dart';
import '../../shared/design_system/unionflow_design_system.dart';
import '../../shared/widgets/core_card.dart';
import '../../shared/widgets/mini_avatar.dart';
import '../di/injection_container.dart';
import '../network/org_context_service.dart';
import '../../features/admin/presentation/pages/user_management_page.dart';
import '../../features/settings/presentation/pages/system_settings_page.dart';
import '../../features/backup/presentation/pages/backup_page.dart';
import '../../features/logs/presentation/pages/logs_page.dart';
import '../../features/reports/presentation/pages/reports_page_wrapper.dart';
import '../../features/epargne/presentation/pages/epargne_page.dart';
import '../../features/contributions/presentation/pages/contributions_page_wrapper.dart' show CotisationsPageWrapper;
import '../../features/adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../features/solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
import '../../features/organizations/presentation/pages/organizations_page_wrapper.dart';
import '../../features/organizations/presentation/pages/org_selector_page.dart';
import '../../features/organizations/bloc/org_switcher_bloc.dart';
import '../../features/profile/presentation/pages/profile_page_wrapper.dart';
/// Page "Plus" avec les fonctions avancées selon le rôle et les modules actifs.
class MorePage extends StatelessWidget {
const MorePage({super.key});
@override
Widget build(BuildContext context) {
final orgCtx = sl<OrgContextService>();
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is! AuthAuthenticated) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
backgroundColor: AppColors.lightBackground,
appBar: const UFAppBar(
title: 'PLUS',
automaticallyImplyLeading: false,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserProfile(context, state, orgCtx),
const SizedBox(height: SpacingTokens.md),
..._buildRoleBasedOptions(context, state, orgCtx),
..._buildModuleOptions(context, state, orgCtx),
const SizedBox(height: SpacingTokens.md),
..._buildCommonOptions(context, orgCtx),
],
),
),
);
},
);
}
Widget _buildUserProfile(
BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textColor = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
final roleColor = isDark ? AppColors.brandGreenLight : AppColors.primaryGreen;
return CoreCard(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ProfilePageWrapper()),
),
child: Row(
children: [
MiniAvatar(
fallbackText: state.user.firstName.isNotEmpty
? state.user.firstName[0].toUpperCase()
: 'U',
size: 40,
imageUrl: state.user.avatar,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${state.user.firstName} ${state.user.lastName}',
style: AppTypography.actionText.copyWith(color: textColor),
),
Text(
state.effectiveRole.displayName.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: roleColor,
fontWeight: FontWeight.bold,
),
),
if (orgCtx.hasContext) ...[
const SizedBox(height: 4),
_OrgBadge(orgCtx: orgCtx, onTap: () => _openOrgSelector(context)),
],
],
),
),
Icon(Icons.chevron_right,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
size: 16),
],
),
);
}
void _openOrgSelector(BuildContext context) {
// Vérifier que OrgSwitcherBloc est disponible (fourni par un ancêtre)
try {
showOrgSelector(context);
} catch (_) {
// OrgSwitcherBloc pas fourni dans ce contexte, navigation vers ProfilePage
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ProfilePageWrapper()),
);
}
}
List<Widget> _buildRoleBasedOptions(
BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
final options = <Widget>[];
if (state.effectiveRole == UserRole.superAdmin) {
options.addAll([
_buildSectionTitle(context, 'Administration Système'),
_buildOptionTile(context,
icon: Icons.people,
title: 'Gestion des utilisateurs',
subtitle: 'Utilisateurs Keycloak et rôles',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UserManagementPage())),
),
_buildOptionTile(context,
icon: Icons.settings,
title: 'Paramètres Système',
subtitle: 'Configuration globale',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SystemSettingsPage())),
),
_buildOptionTile(context,
icon: Icons.backup,
title: 'Sauvegarde & Restauration',
subtitle: 'Gestion des sauvegardes',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const BackupPage())),
),
_buildOptionTile(context,
icon: Icons.article,
title: 'Logs & Monitoring',
subtitle: 'Surveillance et journaux',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const LogsPage())),
),
]);
}
if (state.effectiveRole == UserRole.orgAdmin ||
state.effectiveRole == UserRole.superAdmin) {
options.addAll([
_buildSectionTitle(context, 'Administration'),
_buildOptionTile(context,
icon: Icons.business,
title: 'Gestion des Organisations',
subtitle: 'Créer et gérer les organisations',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const OrganizationsPageWrapper())),
),
_buildSectionTitle(context, 'Workflow Financier'),
_buildOptionTile(context,
icon: Icons.pending_actions,
title: 'Approbations en attente',
subtitle: 'Valider les transactions financières',
onTap: () => Navigator.pushNamed(context, '/approvals'),
),
_buildOptionTile(context,
icon: Icons.account_balance_wallet,
title: 'Gestion des Budgets',
subtitle: 'Créer et suivre les budgets',
onTap: () => Navigator.pushNamed(context, '/budgets'),
),
_buildSectionTitle(context, 'Communication'),
_buildOptionTile(context,
icon: Icons.message,
title: 'Messages & Broadcast',
subtitle: 'Communiquer avec les membres',
onTap: () => Navigator.pushNamed(context, '/messages'),
),
_buildSectionTitle(context, 'Rapports & Analytics'),
_buildOptionTile(context,
icon: Icons.assessment,
title: 'Rapports & Analytics',
subtitle: 'Statistiques détaillées',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ReportsPageWrapper())),
),
]);
}
if (state.effectiveRole == UserRole.moderator) {
options.addAll([
_buildSectionTitle(context, 'Communication'),
_buildOptionTile(context,
icon: Icons.message,
title: 'Messages aux membres',
subtitle: 'Communiquer avec les membres',
onTap: () => Navigator.pushNamed(context, '/messages'),
),
]);
}
return options;
}
/// Sections de modules métier — visibles uniquement si le module est actif.
List<Widget> _buildModuleOptions(
BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
final options = <Widget>[];
final isAdmin = state.effectiveRole == UserRole.orgAdmin ||
state.effectiveRole == UserRole.superAdmin;
// Module TONTINE
if (orgCtx.isModuleActif('TONTINE')) {
options.add(_buildSectionTitle(context, 'Tontine'));
if (isAdmin) {
options.add(_buildOptionTile(context,
icon: Icons.autorenew,
title: 'Gestion Tontine',
subtitle: 'Cycles, cotisations et remises',
onTap: () => Navigator.pushNamed(context, '/tontine'),
));
} else {
options.add(_buildOptionTile(context,
icon: Icons.autorenew,
title: 'Ma Tontine',
subtitle: 'Mes cycles et cotisations',
onTap: () => Navigator.pushNamed(context, '/tontine'),
));
}
}
// Module EPARGNE
if (orgCtx.isModuleActif('EPARGNE')) {
options.add(_buildSectionTitle(context, 'Épargne'));
options.add(_buildOptionTile(context,
icon: Icons.savings,
title: isAdmin ? 'Gestion Épargne' : 'Mon Épargne',
subtitle: isAdmin ? 'Comptes épargne et transactions' : 'Mon compte épargne',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EpargnePage())),
));
}
// Module CREDIT
if (orgCtx.isModuleActif('CREDIT')) {
options.add(_buildSectionTitle(context, 'Crédit'));
options.add(_buildOptionTile(context,
icon: Icons.account_balance,
title: isAdmin ? 'Gestion Crédit' : 'Mon Crédit',
subtitle: isAdmin ? 'Demandes et suivi des crédits' : 'Mes demandes de crédit',
onTap: () => Navigator.pushNamed(context, '/credit'),
));
}
// Module AGRICULTURE
if (orgCtx.isModuleActif('AGRICULTURE')) {
options.add(_buildSectionTitle(context, 'Agriculture'));
options.add(_buildOptionTile(context,
icon: Icons.eco,
title: 'Campagnes Agricoles',
subtitle: 'Parcelles, récoltes et stocks',
onTap: () => Navigator.pushNamed(context, '/agricole'),
));
}
// Module COLLECTE_FONDS
if (orgCtx.isModuleActif('COLLECTE_FONDS')) {
options.add(_buildSectionTitle(context, 'Collecte de Fonds'));
options.add(_buildOptionTile(context,
icon: Icons.volunteer_activism,
title: 'Campagnes de Collecte',
subtitle: 'Dons et levées de fonds',
onTap: () => Navigator.pushNamed(context, '/collecte'),
));
}
// Module PROJETS_ONG
if (orgCtx.isModuleActif('PROJETS_ONG')) {
options.add(_buildSectionTitle(context, 'Projets ONG'));
options.add(_buildOptionTile(context,
icon: Icons.public,
title: 'Projets',
subtitle: 'Gérer et suivre les projets',
onTap: () => Navigator.pushNamed(context, '/projets-ong'),
));
}
// Module CULTE_DONS
if (orgCtx.isModuleActif('CULTE_DONS')) {
options.add(_buildSectionTitle(context, 'Culte & Dons'));
options.add(_buildOptionTile(context,
icon: Icons.church,
title: 'Dons et Offrandes',
subtitle: 'Gestion des dons religieux',
onTap: () => Navigator.pushNamed(context, '/culte'),
));
}
// Module VOTES
if (orgCtx.isModuleActif('VOTES')) {
options.add(_buildSectionTitle(context, 'Votes'));
options.add(_buildOptionTile(context,
icon: Icons.how_to_vote,
title: 'Votes & Élections',
subtitle: 'Campagnes et résultats',
onTap: () => Navigator.pushNamed(context, '/votes'),
));
}
return options;
}
List<Widget> _buildCommonOptions(BuildContext context, OrgContextService orgCtx) {
return [
_buildSectionTitle(context, 'Général'),
_buildOptionTile(context,
icon: Icons.payment,
title: 'Cotisations',
subtitle: 'Gérer les cotisations',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const CotisationsPageWrapper())),
),
_buildOptionTile(context,
icon: Icons.how_to_reg,
title: 'Demandes d\'adhésion',
subtitle: 'Demandes d\'adhésion à une organisation',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const AdhesionsPageWrapper())),
),
_buildOptionTile(context,
icon: Icons.volunteer_activism,
title: 'Demandes d\'aide',
subtitle: 'Solidarité demandes d\'aide',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const DemandesAidePageWrapper())),
),
// Épargne — affiché en commun uniquement si le module n'est PAS actif (évite doublon avec section module)
if (!orgCtx.isModuleActif('EPARGNE'))
_buildOptionTile(context,
icon: Icons.savings_outlined,
title: 'Comptes épargne',
subtitle: 'Mutuelle épargne dépôts (LCB-FT)',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EpargnePage())),
),
];
}
Widget _buildSectionTitle(BuildContext context, String title) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 6, left: 4),
child: Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
),
),
);
}
Widget _buildOptionTile(
BuildContext context, {
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
Color? accentColor,
}) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final accent = accentColor ?? AppColors.primaryGreen;
final titleColor = accentColor != null
? accentColor
: (isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight);
final subtitleColor =
isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
final chevronColor =
isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
return CoreCard(
margin: const EdgeInsets.only(bottom: 8),
onTap: onTap,
child: Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: accent.withOpacity(isDark ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: accent, size: 18),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.actionText.copyWith(color: titleColor),
),
Text(
subtitle,
style: AppTypography.subtitleSmall.copyWith(color: subtitleColor),
),
],
),
),
Icon(Icons.chevron_right, color: chevronColor, size: 16),
],
),
);
}
}
/// Badge compact affichant l'organisation active avec bouton de changement.
class _OrgBadge extends StatelessWidget {
final OrgContextService orgCtx;
final VoidCallback onTap;
const _OrgBadge({required this.orgCtx, required this.onTap});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.business, size: 12, color: colorScheme.primary),
const SizedBox(width: 4),
Flexible(
child: Text(
orgCtx.activeOrganisationNom ?? '',
style: TextStyle(
fontSize: 11,
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 2),
Icon(Icons.swap_horiz, size: 12, color: colorScheme.primary),
],
),
),
);
}
}