827 lines
28 KiB
Dart
827 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../../bloc/organizations_bloc.dart';
|
|
import '../../bloc/organizations_event.dart';
|
|
import '../../bloc/organizations_state.dart';
|
|
import '../../data/models/organization_model.dart';
|
|
import '../widgets/organization_card.dart';
|
|
import '../widgets/create_organization_dialog.dart';
|
|
import '../widgets/edit_organization_dialog.dart';
|
|
import 'organization_detail_page.dart';
|
|
import '../../../../shared/design_system/unionflow_design_system.dart';
|
|
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
|
import '../../../../shared/design_system/unionflow_design_v2.dart';
|
|
import '../../../../shared/design_system/components/animated_fade_in.dart';
|
|
import '../../../../shared/design_system/components/animated_slide_in.dart';
|
|
import '../../../../shared/design_system/components/african_pattern_background.dart';
|
|
import '../../../../shared/design_system/components/uf_app_bar.dart';
|
|
|
|
/// Page de gestion des organisations - Interface sophistiquée et exhaustive
|
|
///
|
|
/// Cette page offre une interface complète pour la gestion des organisations
|
|
/// avec des fonctionnalités avancées de recherche, filtrage, statistiques
|
|
/// et actions de gestion basées sur les permissions utilisateur.
|
|
///
|
|
/// **Design System V2** - Utilise UnionFlowColors et composants standardisés
|
|
/// **Backend connecté** - Toutes les données proviennent d'OrganizationsBloc
|
|
class OrganizationsPage extends StatefulWidget {
|
|
const OrganizationsPage({super.key});
|
|
|
|
@override
|
|
State<OrganizationsPage> createState() => _OrganizationsPageState();
|
|
}
|
|
|
|
class _OrganizationsPageState extends State<OrganizationsPage> with TickerProviderStateMixin {
|
|
// Controllers et état
|
|
final TextEditingController _searchController = TextEditingController();
|
|
TabController? _tabController;
|
|
final ScrollController _scrollController = ScrollController();
|
|
List<TypeOrganization?> _availableTypes = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_scrollController.addListener(_onScroll);
|
|
// Les organisations sont déjà chargées par OrganizationsPageWrapper
|
|
// Le TabController sera initialisé dans didChangeDependencies
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tabController?.dispose();
|
|
_searchController.dispose();
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onScroll() {
|
|
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.9) {
|
|
// Charger plus d'organisations quand on approche du bas
|
|
context.read<OrganizationsBloc>().add(const LoadMoreOrganizations());
|
|
}
|
|
}
|
|
|
|
/// Calcule les types d'organisations disponibles dans les données
|
|
List<TypeOrganization?> _calculateAvailableTypes(List<OrganizationModel> organizations) {
|
|
if (organizations.isEmpty) {
|
|
return [null]; // Seulement "Toutes"
|
|
}
|
|
|
|
// Extraire tous les types uniques
|
|
final typesSet = organizations.map((org) => org.typeOrganisation).toSet();
|
|
final types = typesSet.toList()..sort((a, b) => a.displayName.compareTo(b.displayName));
|
|
|
|
// null en premier pour "Toutes", puis les types triés alphabétiquement
|
|
return [null, ...types];
|
|
}
|
|
|
|
/// Initialise ou met à jour le TabController si les types ont changé
|
|
void _updateTabController(List<TypeOrganization?> newTypes) {
|
|
if (_availableTypes.length != newTypes.length ||
|
|
!_availableTypes.every((type) => newTypes.contains(type))) {
|
|
_availableTypes = newTypes;
|
|
_tabController?.dispose();
|
|
_tabController = TabController(length: _availableTypes.length, vsync: this);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocConsumer<OrganizationsBloc, OrganizationsState>(
|
|
listener: (context, state) {
|
|
// Gestion des messages de succès et erreurs
|
|
if (state is OrganizationsError) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(state.message),
|
|
backgroundColor: UnionFlowColors.error,
|
|
duration: const Duration(seconds: 4),
|
|
action: SnackBarAction(
|
|
label: 'Réessayer',
|
|
textColor: Colors.white,
|
|
onPressed: () {
|
|
context.read<OrganizationsBloc>().add(const LoadOrganizations(refresh: true));
|
|
},
|
|
),
|
|
),
|
|
);
|
|
} else if (state is OrganizationCreated) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Organisation créée avec succès'),
|
|
backgroundColor: UnionFlowColors.success,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
} else if (state is OrganizationUpdated) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Organisation mise à jour avec succès'),
|
|
backgroundColor: UnionFlowColors.success,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
} else if (state is OrganizationDeleted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Organisation supprimée avec succès'),
|
|
backgroundColor: UnionFlowColors.success,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
builder: (context, state) {
|
|
return AfricanPatternBackground(
|
|
child: Scaffold(
|
|
backgroundColor: Colors.transparent,
|
|
appBar: UFAppBar(
|
|
title: 'Gestion des Organisations',
|
|
backgroundColor: UnionFlowColors.surface,
|
|
foregroundColor: UnionFlowColors.textPrimary,
|
|
elevation: 0,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: () {
|
|
context.read<OrganizationsBloc>().add(const RefreshOrganizations());
|
|
},
|
|
tooltip: 'Rafraîchir',
|
|
),
|
|
],
|
|
),
|
|
body: SafeArea(
|
|
child: _buildBody(state),
|
|
),
|
|
floatingActionButton: _buildActionButton(state),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildBody(OrganizationsState state) {
|
|
if (state is OrganizationsInitial || state is OrganizationsLoading) {
|
|
return _buildLoadingState();
|
|
}
|
|
|
|
if (state is OrganizationsLoaded) {
|
|
final loadedState = state;
|
|
|
|
// Calculer les types disponibles et mettre à jour le TabController
|
|
final availableTypes = _calculateAvailableTypes(loadedState.organizations);
|
|
_updateTabController(availableTypes);
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
context.read<OrganizationsBloc>().add(const RefreshOrganizations());
|
|
},
|
|
child: SingleChildScrollView(
|
|
controller: _scrollController,
|
|
padding: const EdgeInsets.all(SpacingTokens.md),
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: AnimatedFadeIn(
|
|
duration: const Duration(milliseconds: 400),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header avec design system
|
|
AnimatedSlideIn(
|
|
duration: const Duration(milliseconds: 500),
|
|
curve: Curves.easeOut,
|
|
child: _buildHeader(loadedState),
|
|
),
|
|
const SizedBox(height: SpacingTokens.md),
|
|
|
|
// Section statistiques
|
|
AnimatedSlideIn(
|
|
duration: const Duration(milliseconds: 600),
|
|
curve: Curves.easeOut,
|
|
child: _buildStatsSection(loadedState),
|
|
),
|
|
const SizedBox(height: SpacingTokens.md),
|
|
|
|
// Barre de recherche
|
|
AnimatedSlideIn(
|
|
duration: const Duration(milliseconds: 700),
|
|
curve: Curves.easeOut,
|
|
child: _buildSearchBar(loadedState),
|
|
),
|
|
const SizedBox(height: SpacingTokens.md),
|
|
|
|
// Onglets de catégories dynamiques
|
|
AnimatedSlideIn(
|
|
duration: const Duration(milliseconds: 800),
|
|
curve: Curves.easeOut,
|
|
child: _buildCategoryTabs(availableTypes),
|
|
),
|
|
const SizedBox(height: SpacingTokens.md),
|
|
|
|
// Liste des organisations
|
|
AnimatedSlideIn(
|
|
duration: const Duration(milliseconds: 900),
|
|
curve: Curves.easeOut,
|
|
child: _buildOrganizationsList(loadedState),
|
|
),
|
|
|
|
const SizedBox(height: 80), // Espace pour le FAB
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (state is OrganizationsLoadingMore) {
|
|
// Show current organizations with loading indicator at bottom
|
|
return SingleChildScrollView(
|
|
controller: _scrollController,
|
|
padding: const EdgeInsets.all(SpacingTokens.md),
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildLoadingMorePlaceholder(state.currentOrganizations),
|
|
const Padding(
|
|
padding: EdgeInsets.all(SpacingTokens.md),
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
color: UnionFlowColors.unionGreen,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 80),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (state is OrganizationsError) {
|
|
return _buildErrorState(state);
|
|
}
|
|
|
|
return _buildLoadingState();
|
|
}
|
|
|
|
/// Placeholder pour affichage pendant le chargement de plus d'organisations
|
|
Widget _buildLoadingMorePlaceholder(List<OrganizationModel> currentOrganizations) {
|
|
return ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: currentOrganizations.length,
|
|
separatorBuilder: (context, index) => const SizedBox(height: SpacingTokens.sm),
|
|
itemBuilder: (context, index) {
|
|
final org = currentOrganizations[index];
|
|
return OrganizationCard(
|
|
organization: org,
|
|
onTap: () => _showOrganizationDetails(org),
|
|
showActions: false,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Bouton d'action harmonisé avec Design System V2
|
|
Widget? _buildActionButton(OrganizationsState state) {
|
|
// Afficher le FAB seulement si les données sont chargées
|
|
if (state is! OrganizationsLoaded && state is! OrganizationsLoadingMore) {
|
|
return null;
|
|
}
|
|
|
|
return FloatingActionButton.extended(
|
|
onPressed: _showCreateOrganizationDialog,
|
|
backgroundColor: UnionFlowColors.unionGreen,
|
|
elevation: 8,
|
|
icon: const Icon(Icons.add, color: Colors.white),
|
|
label: const Text(
|
|
'Nouvelle organisation',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Header épuré avec Design System V2
|
|
Widget _buildHeader(OrganizationsLoaded state) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(SpacingTokens.md),
|
|
decoration: BoxDecoration(
|
|
gradient: UnionFlowColors.primaryGradient,
|
|
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
|
boxShadow: UnionFlowColors.greenGlowShadow,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(SpacingTokens.sm),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
|
),
|
|
child: const Icon(
|
|
Icons.business,
|
|
color: Colors.white,
|
|
size: 24,
|
|
),
|
|
),
|
|
const SizedBox(width: SpacingTokens.md),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Gestion des Organisations',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
'${state.filteredOrganizations.length} organisation(s)',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
context.read<OrganizationsBloc>().add(const RefreshOrganizations());
|
|
},
|
|
icon: const Icon(Icons.refresh, color: Colors.white),
|
|
tooltip: 'Rafraîchir',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Section statistiques avec données réelles et Design System V2
|
|
Widget _buildStatsSection(OrganizationsLoaded state) {
|
|
final totalOrgs = state.organizations.length;
|
|
final activeOrgs = state.organizations.where((o) => o.statut == StatutOrganization.active).length;
|
|
final totalMembers = state.organizations.fold<int>(0, (sum, o) => sum + o.nombreMembres);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(SpacingTokens.md),
|
|
decoration: BoxDecoration(
|
|
color: UnionFlowColors.surface,
|
|
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
|
boxShadow: UnionFlowColors.softShadow,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(
|
|
Icons.analytics_outlined,
|
|
color: UnionFlowColors.textSecondary,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: SpacingTokens.xs),
|
|
const Text(
|
|
'Statistiques',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: UnionFlowColors.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: SpacingTokens.md),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
'Total',
|
|
totalOrgs.toString(),
|
|
Icons.business_outlined,
|
|
UnionFlowColors.unionGreen,
|
|
),
|
|
),
|
|
const SizedBox(width: SpacingTokens.sm),
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
'Actives',
|
|
activeOrgs.toString(),
|
|
Icons.check_circle_outline,
|
|
UnionFlowColors.success,
|
|
),
|
|
),
|
|
const SizedBox(width: SpacingTokens.sm),
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
'Membres',
|
|
totalMembers.toString(),
|
|
Icons.people_outline,
|
|
UnionFlowColors.info,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Carte de statistique avec Design System V2
|
|
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(SpacingTokens.sm),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
|
border: Border.all(
|
|
color: color.withOpacity(0.2),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: color, size: 20),
|
|
const SizedBox(height: SpacingTokens.xs),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: UnionFlowColors.textPrimary,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: UnionFlowColors.textSecondary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Barre de recherche avec Design System V2
|
|
Widget _buildSearchBar(OrganizationsLoaded state) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(SpacingTokens.md),
|
|
decoration: BoxDecoration(
|
|
color: UnionFlowColors.surface,
|
|
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
|
boxShadow: UnionFlowColors.softShadow,
|
|
),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: UnionFlowColors.surfaceVariant,
|
|
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
|
border: Border.all(
|
|
color: UnionFlowColors.border,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: TextField(
|
|
controller: _searchController,
|
|
onChanged: (value) {
|
|
context.read<OrganizationsBloc>().add(SearchOrganizations(value));
|
|
},
|
|
decoration: InputDecoration(
|
|
hintText: 'Rechercher par nom, type, localisation...',
|
|
hintStyle: const TextStyle(
|
|
color: UnionFlowColors.textSecondary,
|
|
fontSize: 14,
|
|
),
|
|
prefixIcon: const Icon(Icons.search, color: UnionFlowColors.unionGreen),
|
|
suffixIcon: _searchController.text.isNotEmpty
|
|
? IconButton(
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
context.read<OrganizationsBloc>().add(const SearchOrganizations(''));
|
|
},
|
|
icon: const Icon(Icons.clear, color: UnionFlowColors.textSecondary),
|
|
)
|
|
: null,
|
|
border: InputBorder.none,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: SpacingTokens.md,
|
|
vertical: SpacingTokens.sm,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Onglets de catégories générés dynamiquement selon les types disponibles
|
|
Widget _buildCategoryTabs(List<TypeOrganization?> availableTypes) {
|
|
if (_tabController == null || availableTypes.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return BlocBuilder<OrganizationsBloc, OrganizationsState>(
|
|
builder: (context, state) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: UnionFlowColors.surface,
|
|
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
|
boxShadow: UnionFlowColors.softShadow,
|
|
),
|
|
child: TabBar(
|
|
controller: _tabController!,
|
|
isScrollable: availableTypes.length > 4, // Scrollable si plus de 4 types
|
|
labelColor: UnionFlowColors.unionGreen,
|
|
unselectedLabelColor: UnionFlowColors.textSecondary,
|
|
indicatorColor: UnionFlowColors.unionGreen,
|
|
indicatorWeight: 3,
|
|
indicatorSize: TabBarIndicatorSize.tab,
|
|
labelStyle: const TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
),
|
|
unselectedLabelStyle: const TextStyle(
|
|
fontWeight: FontWeight.normal,
|
|
fontSize: 14,
|
|
),
|
|
onTap: (index) {
|
|
// Filtrer par type selon l'onglet sélectionné
|
|
final selectedType = availableTypes[index];
|
|
|
|
if (selectedType != null) {
|
|
context.read<OrganizationsBloc>().add(FilterOrganizationsByType(selectedType));
|
|
} else {
|
|
// null = "Toutes" → effacer les filtres
|
|
context.read<OrganizationsBloc>().add(const ClearOrganizationsFilters());
|
|
}
|
|
},
|
|
tabs: availableTypes.map((type) {
|
|
// null = "Toutes", sinon utiliser le displayName du type
|
|
final label = type == null ? 'Toutes' : type.displayName;
|
|
final icon = type?.icon; // Emoji du type
|
|
|
|
return Tab(
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Text(icon, style: const TextStyle(fontSize: 16)),
|
|
const SizedBox(width: SpacingTokens.xs),
|
|
],
|
|
Text(label),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Liste des organisations avec données réelles et OrganizationCard
|
|
Widget _buildOrganizationsList(OrganizationsLoaded state) {
|
|
final organizations = state.filteredOrganizations;
|
|
|
|
if (organizations.isEmpty) {
|
|
return _buildEmptyState();
|
|
}
|
|
|
|
return ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: organizations.length,
|
|
separatorBuilder: (context, index) => const SizedBox(height: SpacingTokens.sm),
|
|
itemBuilder: (context, index) {
|
|
final org = organizations[index];
|
|
return AnimatedFadeIn(
|
|
duration: Duration(milliseconds: 300 + (index * 50)),
|
|
child: OrganizationCard(
|
|
organization: org,
|
|
onTap: () => _showOrganizationDetails(org),
|
|
onEdit: () => _showEditOrganizationDialog(org),
|
|
onDelete: () => _confirmDeleteOrganization(org),
|
|
showActions: true,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// État vide avec Design System V2
|
|
Widget _buildEmptyState() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(SpacingTokens.xl),
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(SpacingTokens.xl),
|
|
decoration: BoxDecoration(
|
|
color: UnionFlowColors.unionGreenPale,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.business_outlined,
|
|
size: 64,
|
|
color: UnionFlowColors.unionGreen,
|
|
),
|
|
),
|
|
const SizedBox(height: SpacingTokens.lg),
|
|
const Text(
|
|
'Aucune organisation trouvée',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: UnionFlowColors.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: SpacingTokens.xs),
|
|
const Text(
|
|
'Essayez de modifier vos critères de recherche\nou créez une nouvelle organisation',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: UnionFlowColors.textSecondary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: SpacingTokens.lg),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
context.read<OrganizationsBloc>().add(const ClearOrganizationsFilters());
|
|
_searchController.clear();
|
|
},
|
|
icon: const Icon(Icons.clear_all),
|
|
label: const Text('Réinitialiser les filtres'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: UnionFlowColors.unionGreen,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: SpacingTokens.lg,
|
|
vertical: SpacingTokens.sm,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// État de chargement avec Design System V2
|
|
Widget _buildLoadingState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(
|
|
color: UnionFlowColors.unionGreen,
|
|
strokeWidth: 3,
|
|
),
|
|
const SizedBox(height: SpacingTokens.md),
|
|
const Text(
|
|
'Chargement des organisations...',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: UnionFlowColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// État d'erreur avec Design System V2
|
|
Widget _buildErrorState(OrganizationsError state) {
|
|
return Center(
|
|
child: Container(
|
|
margin: const EdgeInsets.all(SpacingTokens.xl),
|
|
padding: const EdgeInsets.all(SpacingTokens.xl),
|
|
decoration: BoxDecoration(
|
|
color: UnionFlowColors.errorPale,
|
|
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
|
border: Border.all(
|
|
color: UnionFlowColors.errorLight,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
Icons.error_outline,
|
|
size: 64,
|
|
color: UnionFlowColors.error,
|
|
),
|
|
const SizedBox(height: SpacingTokens.md),
|
|
Text(
|
|
state.message,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: UnionFlowColors.textPrimary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if (state.details != null) ...[
|
|
const SizedBox(height: SpacingTokens.xs),
|
|
Text(
|
|
state.details!,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: UnionFlowColors.textSecondary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
const SizedBox(height: SpacingTokens.lg),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
context.read<OrganizationsBloc>().add(const LoadOrganizations(refresh: true));
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Réessayer'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: UnionFlowColors.unionGreen,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: SpacingTokens.lg,
|
|
vertical: SpacingTokens.sm,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Méthodes d'actions
|
|
void _showOrganizationDetails(OrganizationModel org) {
|
|
final orgId = org.id;
|
|
if (orgId == null || orgId.isEmpty) return;
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (context) => BlocProvider.value(
|
|
value: context.read<OrganizationsBloc>(),
|
|
child: OrganizationDetailPage(organizationId: orgId),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showCreateOrganizationDialog() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => const CreateOrganizationDialog(),
|
|
);
|
|
}
|
|
|
|
void _showEditOrganizationDialog(OrganizationModel org) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => EditOrganizationDialog(organization: org),
|
|
);
|
|
}
|
|
|
|
void _confirmDeleteOrganization(OrganizationModel org) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Supprimer l\'organisation'),
|
|
content: Text('Voulez-vous vraiment supprimer "${org.nom}" ?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
if (org.id != null) {
|
|
context.read<OrganizationsBloc>().add(DeleteOrganization(org.id!));
|
|
}
|
|
Navigator.of(context).pop();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: UnionFlowColors.error,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|