Files
dahoud b2f29922d3 feat(navigation): titre 'Mon/Mes Organisations' dynamique + fallback modules
Plus page (more_page.dart) :
- Titre entrée Organisations dynamique selon rôle/nombre d'orgs :
  * SuperAdmin → 'Gestion des Organisations'
  * OrgAdmin 1 org → 'Mon Organisation'
  * OrgAdmin 2+ orgs → 'Mes Organisations'
- Sous-titres adaptés
- Helpers _orgTitle/_orgSubtitle utilisent OrgSwitcherBloc pour le count
- 7 modules non encore implémentés (TONTINE, CREDIT, AGRICULTURE, COLLECTE_FONDS,
  PROJETS_ONG, CULTE_DONS, VOTES) : Navigator.pushNamed remplacé par _comingSoon()
  SnackBar 'Disponible prochainement' (les routes n'étaient enregistrées nulle part)
- Entrée 'Comptes épargne' → 'Épargne & Crédit' (terme métier pour mutuelles)

Organizations page :
- AppBar title dynamique via _appBarTitle() (même logique que more_page)
- SnackBars OrganizationCreated/Updated/Deleted : Color(0xFF16A34A) → AppColors.success
2026-04-15 20:14:42 +00:00

479 lines
18 KiB
Dart
Raw Permalink 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/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 '../../shared/design_system/tokens/app_colors.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(
appBar: const UFAppBar(
title: 'PLUS',
automaticallyImplyLeading: false,
moduleGradient: ModuleColors.systemeGradient,
),
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 scheme = Theme.of(context).colorScheme;
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: scheme.onSurface),
),
Text(
state.effectiveRole.displayName.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: ModuleColors.profil,
fontWeight: FontWeight.bold,
),
),
if (orgCtx.hasContext) ...[
const SizedBox(height: 4),
_OrgBadge(orgCtx: orgCtx, onTap: () => _openOrgSelector(context)),
],
],
),
),
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 16),
],
),
);
}
void _openOrgSelector(BuildContext context) {
try {
showOrgSelector(context);
} catch (_) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ProfilePageWrapper()),
);
}
}
List<Widget> _buildRoleBasedOptions(
BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
final options = <Widget>[];
// Note: les items SYSTÈME (Gestion utilisateurs, Paramètres Système,
// Sauvegarde & Restauration, Logs & Monitoring) ont été déplacés dans
// le burger drawer (section "Système" — super admin only) afin de
// respecter la démarcation Métier (Plus) / Système (Drawer).
if (state.effectiveRole == UserRole.orgAdmin ||
state.effectiveRole == UserRole.superAdmin) {
final isSuperAdmin = state.effectiveRole == UserRole.superAdmin;
final orgTitle = _orgTitle(context, isSuperAdmin);
final orgSubtitle = _orgSubtitle(context, isSuperAdmin);
options.addAll([
_buildSectionTitle(context, 'Administration'),
_buildOptionTile(context,
icon: Icons.business,
title: orgTitle,
subtitle: orgSubtitle,
accentColor: ModuleColors.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',
accentColor: ModuleColors.financeWorkflow,
onTap: () => Navigator.pushNamed(context, '/approvals'),
),
_buildOptionTile(context,
icon: Icons.account_balance_wallet,
title: 'Gestion des Budgets',
subtitle: 'Créer et suivre les budgets',
accentColor: ModuleColors.financeWorkflow,
onTap: () => Navigator.pushNamed(context, '/budgets'),
),
_buildSectionTitle(context, 'Communication'),
_buildOptionTile(context,
icon: Icons.message,
title: 'Messages & Broadcast',
subtitle: 'Communiquer avec les membres',
accentColor: ModuleColors.communication,
onTap: () => Navigator.pushNamed(context, '/messages'),
),
_buildSectionTitle(context, 'Rapports & Analytics'),
_buildOptionTile(context,
icon: Icons.assessment,
title: 'Rapports & Analytics',
subtitle: 'Statistiques détaillées',
accentColor: ModuleColors.rapports,
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',
accentColor: ModuleColors.communication,
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;
if (orgCtx.isModuleActif('TONTINE')) {
options.add(_buildSectionTitle(context, 'Tontine'));
options.add(_buildOptionTile(context,
icon: Icons.autorenew,
title: isAdmin ? 'Gestion Tontine' : 'Ma Tontine',
subtitle: isAdmin ? 'Cycles, cotisations et remises' : 'Mes cycles et cotisations',
accentColor: ModuleColors.cotisations,
onTap: () => _comingSoon(context, 'Tontine'),
));
}
if (orgCtx.isModuleActif('EPARGNE')) {
options.add(_buildSectionTitle(context, 'Épargne'));
options.add(_buildOptionTile(context,
icon: Icons.savings,
title: isAdmin ? 'Gestion Épargne' : 'Mon Épargne',
subtitle: isAdmin ? 'Épargne, dépôts, crédits et transactions' : 'Mon épargne et mes crédits',
accentColor: ModuleColors.epargne,
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EpargnePage())),
));
}
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',
accentColor: ModuleColors.financeWorkflow,
onTap: () => _comingSoon(context, 'Crédit'),
));
}
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',
accentColor: ModuleColors.epargne,
onTap: () => _comingSoon(context, 'Agriculture'),
));
}
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',
accentColor: ModuleColors.solidarite,
onTap: () => _comingSoon(context, 'Collecte de Fonds'),
));
}
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',
accentColor: ModuleColors.rapports,
onTap: () => _comingSoon(context, 'Projets ONG'),
));
}
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',
accentColor: ModuleColors.solidarite,
onTap: () => _comingSoon(context, 'Culte & Dons'),
));
}
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',
accentColor: ModuleColors.financeWorkflow,
onTap: () => _comingSoon(context, 'Votes & Élections'),
));
}
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',
accentColor: ModuleColors.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',
accentColor: ModuleColors.adhesions,
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const AdhesionsPageWrapper())),
),
_buildOptionTile(context,
icon: Icons.volunteer_activism,
title: 'Demandes d\'aide',
subtitle: 'Solidarité demandes d\'aide',
accentColor: ModuleColors.solidarite,
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const DemandesAidePageWrapper())),
),
if (!orgCtx.isModuleActif('EPARGNE'))
_buildOptionTile(context,
icon: Icons.savings_outlined,
title: 'Épargne & Crédit',
subtitle: 'Comptes épargne, dépôts et crédits (LCB-FT)',
accentColor: ModuleColors.epargne,
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EpargnePage())),
),
];
}
// ─── Helpers titre organisation ─────────────────────────────────────────
/// Titre de l'entrée "Organisation(s)" selon le rôle et le nombre d'orgs gérées.
String _orgTitle(BuildContext context, bool isSuperAdmin) {
if (isSuperAdmin) return 'Gestion des Organisations';
final switcherState = context.read<OrgSwitcherBloc>().state;
final count = switcherState is OrgSwitcherLoaded
? switcherState.organisations.length
: 1;
return count > 1 ? 'Mes Organisations' : 'Mon Organisation';
}
String _orgSubtitle(BuildContext context, bool isSuperAdmin) {
if (isSuperAdmin) return 'Créer et gérer les organisations';
final switcherState = context.read<OrgSwitcherBloc>().state;
final count = switcherState is OrgSwitcherLoaded
? switcherState.organisations.length
: 1;
return count > 1 ? 'Gérer mes organisations' : 'Gérer mon organisation';
}
// ─── Modules non encore implémentés ─────────────────────────────────────
void _comingSoon(BuildContext context, String feature) {
final isDark = Theme.of(context).brightness == Brightness.dark;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(children: [
const Icon(Icons.construction_outlined, color: Colors.white, size: 16),
const SizedBox(width: 8),
Expanded(child: Text('$feature — Disponible prochainement')),
]),
backgroundColor: isDark ? AppColors.surfaceVariantDark : AppColors.textSecondary,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 3),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
Widget _buildSectionTitle(BuildContext context, String title) {
final scheme = Theme.of(context).colorScheme;
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: scheme.onSurfaceVariant,
),
),
);
}
Widget _buildOptionTile(
BuildContext context, {
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
Color? accentColor,
}) {
final scheme = Theme.of(context).colorScheme;
final accent = accentColor ?? scheme.primary;
return CoreCard(
margin: const EdgeInsets.only(bottom: 8),
onTap: onTap,
child: Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: accent.withOpacity(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: scheme.onSurface,
),
),
Text(
subtitle,
style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant),
),
],
),
),
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, 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),
],
),
),
);
}
}