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:
@@ -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.',
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user