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

RefreshIndicator ajouté (dispatche les events BLoC appropriés) :
- adhesion_detail, adhesions_page, demande_aide_detail, demandes_aide_page
- event_detail, organization_detail, org_selector, org_types
- user_management_detail, reports (TabBarView), logs (Dashboard tab)
- profile (onglet Perso), backup (3 onglets), notifications

Fixes associés :
- AlwaysScrollableScrollPhysics sur tous les scroll widgets
  (permet pull-to-refresh même si contenu < écran)
- Empty states des listes : wrappés dans SingleChildScrollView pour refresh
- Dark mode adaptatif sur textes/surfaces/borders hardcodés
- backup_page : bouton retour ajouté dans le header gradient
- org_types : chevron/star/border adaptatifs
- reports : couleurs placeholders graphique + chevrons
This commit is contained in:
dahoud
2026-04-15 20:13:50 +00:00
parent f78892e5f6
commit 55f84da49a
14 changed files with 1565 additions and 1538 deletions

View File

@@ -5,6 +5,9 @@
library org_selector_page;
import 'package:flutter/material.dart';
import '../../../../shared/design_system/tokens/module_colors.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../shared/design_system/components/uf_app_bar.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/org_switcher_bloc.dart';
import '../../data/models/org_switcher_entry.dart';
@@ -30,46 +33,49 @@ class _OrgSelectorPageState extends State<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>(
listener: (context, state) {
if (state is OrgSwitcherLoaded && widget.required && state.active != null) {
// Une org a été auto-sélectionnée, on peut continuer
}
},
builder: (context, state) {
if (state is OrgSwitcherLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is OrgSwitcherError) {
return _ErrorView(
message: state.message,
onRetry: () => context
.read<OrgSwitcherBloc>()
.add(const OrgSwitcherLoadRequested()),
);
}
if (state is OrgSwitcherLoaded) {
if (state.organisations.isEmpty) {
return const _EmptyView();
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
}
return _OrgList(
organisations: state.organisations,
active: state.active,
onSelect: (org) {
context
},
builder: (context, state) {
if (state is OrgSwitcherLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is OrgSwitcherError) {
return _ErrorView(
message: state.message,
onRetry: () => context
.read<OrgSwitcherBloc>()
.add(OrgSwitcherSelectRequested(org));
Navigator.of(context).pop(org);
},
);
}
return const SizedBox.shrink();
},
.add(const OrgSwitcherLoadRequested()),
);
}
if (state is OrgSwitcherLoaded) {
if (state.organisations.isEmpty) {
return const _EmptyView();
}
return _OrgList(
organisations: state.organisations,
active: state.active,
onSelect: (org) {
context
.read<OrgSwitcherBloc>()
.add(OrgSwitcherSelectRequested(org));
Navigator.of(context).pop(org);
},
);
}
return const SizedBox.shrink();
},
),
),
);
}
@@ -103,7 +109,12 @@ class _OrgList extends StatelessWidget {
),
),
Expanded(
child: ListView.separated(
child: RefreshIndicator(
color: ModuleColors.organisations,
onRefresh: () async =>
context.read<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.',

View File

@@ -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)),
),
@@ -369,13 +388,17 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
@override
Widget build(BuildContext context) {
final isEdit = widget.existing != null;
final isEdit = widget.existing != null;
final isDark = Theme.of(context).brightness == Brightness.dark;
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
final onSurface = Theme.of(context).colorScheme.onSurface;
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Form(
@@ -389,13 +412,13 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
child: Container(
width: 36,
height: 4,
decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.outline, borderRadius: BorderRadius.circular(2)),
),
),
const SizedBox(height: 14),
Text(
isEdit ? 'Modifier le type' : 'Nouveau type d\'organisation',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: AppColors.textPrimaryLight),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: textPrimary),
),
const SizedBox(height: 14),
@@ -446,7 +469,7 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
const SizedBox(height: 12),
// Color picker
const Text('Couleur', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textSecondaryLight)),
Text('Couleur', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: textSecondary)),
const SizedBox(height: 6),
Wrap(
spacing: 8,
@@ -462,7 +485,7 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: selected ? Border.all(color: Colors.black, width: 2) : null,
border: selected ? Border.all(color: onSurface, width: 2) : null,
),
child: selected ? const Icon(Icons.check, size: 14, color: Colors.white) : null,
),
@@ -477,8 +500,8 @@ class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
child: ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),

View File

@@ -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,90 +66,100 @@ 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>(
builder: (context, state) {
if (state is OrganizationLoading) return _buildLoading();
if (state is OrganizationLoaded) return _buildContent(state.organization);
if (state is OrganizationsError) return _buildError(state);
return _buildEmpty();
},
body: SafeArea(
top: false,
child: BlocBuilder<OrganizationsBloc, OrganizationsState>(
builder: (context, state) {
if (state is OrganizationLoading) return _buildLoading();
if (state is OrganizationLoaded) return _buildContent(state.organization);
if (state is OrganizationsError) return _buildError(state);
return _buildEmpty();
},
),
),
);
}
Widget _buildContent(OrganizationModel org) {
return SingleChildScrollView(
padding: const EdgeInsets.all(12),
return RefreshIndicator(
color: ModuleColors.organisations,
onRefresh: () async => context
.read<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)),
),
],
),