diff --git a/lib/features/organizations/presentation/pages/organizations_page.dart b/lib/features/organizations/presentation/pages/organizations_page.dart index f33ef5f..70908ee 100644 --- a/lib/features/organizations/presentation/pages/organizations_page.dart +++ b/lib/features/organizations/presentation/pages/organizations_page.dart @@ -33,6 +33,18 @@ class _OrganizationsPageState extends State with TickerProvid final ScrollController _scrollController = ScrollController(); List _availableTypes = []; + // ── Nouvelles fonctionnalités (parité Annuaire Membres) ── + bool _isGridView = false; + String _filterStatut = 'Tous'; + static const _statutFilters = ['Tous', 'Active', 'En création', 'Inactive', 'Suspendue', 'Dissoute']; + static const _statutFilterMapping = { + 'Active': 'ACTIVE', + 'En création': 'EN_CREATION', + 'Inactive': 'INACTIVE', + 'Suspendue': 'SUSPENDUE', + 'Dissoute': 'DISSOUTE', + }; + /// Cache de la dernière liste connue. /// Évite de perdre l'affichage quand le bloc passe dans un état non-liste /// (OrganizationLoaded, OrganizationCreated, etc.) après navigation vers @@ -152,6 +164,11 @@ class _OrganizationsPageState extends State with TickerProvid moduleGradient: ModuleColors.organisationsGradient, elevation: 0, actions: [ + IconButton( + icon: Icon(_isGridView ? Icons.view_list_rounded : Icons.grid_view_rounded), + onPressed: () => setState(() => _isGridView = !_isGridView), + tooltip: _isGridView ? 'Vue liste' : 'Vue grille', + ), IconButton( icon: const Icon(Icons.category_outlined), onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const OrgTypesPage())), @@ -266,11 +283,15 @@ class _OrganizationsPageState extends State with TickerProvid curve: Curves.easeOut, child: _buildSearchBar(context, state), ), + const SizedBox(height: SpacingTokens.sm), + _buildStatutChips(context), const SizedBox(height: SpacingTokens.md), AnimatedSlideIn( duration: const Duration(milliseconds: 800), curve: Curves.easeOut, - child: _buildOrganizationsList(context, state), + child: _isGridView + ? _buildOrganizationsGrid(context, state) + : _buildOrganizationsList(context, state), ), const SizedBox(height: 80), ], @@ -390,10 +411,57 @@ class _OrganizationsPageState extends State with TickerProvid ); } - // ─── Liste des organisations ────────────────────────────────────────────── + // ─── Chips de filtre par statut ───────────────────────────────────────── + + Widget _buildStatutChips(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _statutFilters.map((label) { + final isSelected = _filterStatut == label; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () => setState(() => _filterStatut = label), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? ModuleColors.organisations : Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? ModuleColors.organisations : Theme.of(context).colorScheme.outline, + width: 1, + ), + ), + child: Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: isSelected ? AppColors.onPrimary : Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ); + }).toList(), + ), + ); + } + + /// Filtre les organisations par statut (chips) en plus des filtres BLoC (type + recherche) + List _applyStatutFilter(List orgs) { + if (_filterStatut == 'Tous') return orgs; + final mappedStatut = _statutFilterMapping[_filterStatut]; + if (mappedStatut == null) return orgs; + return orgs.where((o) => o.statut.name.toUpperCase() == mappedStatut).toList(); + } + + // ─── Liste des organisations (avec swipe) ───────────────────────────────── Widget _buildOrganizationsList(BuildContext context, OrganizationsLoaded state) { - final organizations = state.filteredOrganizations; + final organizations = _applyStatutFilter(state.filteredOrganizations); if (organizations.isEmpty) return _buildEmptyState(context); final authState = context.read().state; @@ -409,18 +477,189 @@ class _OrganizationsPageState extends State with TickerProvid final org = organizations[i]; return AnimatedFadeIn( duration: Duration(milliseconds: 300 + (i * 50)), - child: OrganizationCard( - organization: org, - onTap: () => _showOrganizationDetails(org), - onEdit: canManageOrgs ? () => _showEditOrganizationDialog(org) : null, - onDelete: canManageOrgs ? () => _confirmDeleteOrganization(org) : null, - showActions: canManageOrgs, - ), + child: _buildSwipeableCard(context, org, canManageOrgs), ); }, ); } + // ─── Vue grille ─────────────────────────────────────────────────────────── + + Widget _buildOrganizationsGrid(BuildContext context, OrganizationsLoaded state) { + final organizations = _applyStatutFilter(state.filteredOrganizations); + if (organizations.isEmpty) return _buildEmptyState(context); + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 0.85, + ), + itemCount: organizations.length, + itemBuilder: (context, i) { + final org = organizations[i]; + return _buildGridCard(context, org); + }, + ); + } + + Widget _buildGridCard(BuildContext context, OrganizationModel org) { + final scheme = Theme.of(context).colorScheme; + final statutColor = Color(int.parse(org.statut.color.substring(1), radix: 16) + 0xFF000000); + + return GestureDetector( + onTap: () => _showOrganizationDetails(org), + child: Container( + decoration: BoxDecoration( + color: scheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: scheme.outline.withOpacity(0.3)), + boxShadow: [BoxShadow(color: AppColors.shadow, blurRadius: 4, offset: const Offset(0, 2))], + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icône + statut + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: ModuleColors.organisations.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.business_outlined, size: 20, color: ModuleColors.organisations), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: statutColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + org.statut.displayName, + style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: statutColor), + ), + ), + ], + ), + const SizedBox(height: 10), + // Nom + Text( + org.nom, + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: scheme.onSurface), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (org.nomCourt?.isNotEmpty == true) + Text(org.nomCourt!, style: TextStyle(fontSize: 10, color: scheme.onSurfaceVariant)), + const Spacer(), + // Type + Text( + org.typeOrganisation, + style: TextStyle(fontSize: 10, color: ModuleColors.organisations, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + // Localisation + membres + Row( + children: [ + if (org.ville?.isNotEmpty == true) ...[ + Icon(Icons.location_on_outlined, size: 11, color: scheme.onSurfaceVariant), + const SizedBox(width: 2), + Expanded( + child: Text( + org.ville!, + style: TextStyle(fontSize: 10, color: scheme.onSurfaceVariant), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + Icon(Icons.people_outline, size: 11, color: scheme.onSurfaceVariant), + const SizedBox(width: 2), + Text( + '${org.nombreMembres}', + style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: scheme.onSurface), + ), + ], + ), + ], + ), + ), + ); + } + + // ─── Swipe actions ──────────────────────────────────────────────────────── + + Widget _buildSwipeableCard(BuildContext context, OrganizationModel org, bool canManage) { + if (!canManage) { + return OrganizationCard( + organization: org, + onTap: () => _showOrganizationDetails(org), + showActions: false, + ); + } + + return Dismissible( + key: Key('org_swipe_${org.id}'), + direction: DismissDirection.horizontal, + confirmDismiss: (dir) async { + if (dir == DismissDirection.startToEnd) { + _showEditOrganizationDialog(org); + } else if (dir == DismissDirection.endToStart) { + _confirmDeleteOrganization(org); + } + return false; + }, + background: Container( + margin: const EdgeInsets.symmetric(vertical: 1), + decoration: BoxDecoration( + color: ModuleColors.organisations.withOpacity(0.13), + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.edit_outlined, color: ModuleColors.organisations, size: 22), + SizedBox(height: 4), + Text('Modifier', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: ModuleColors.organisations)), + ], + ), + ), + secondaryBackground: Container( + margin: const EdgeInsets.symmetric(vertical: 1), + decoration: BoxDecoration( + color: AppColors.error.withOpacity(0.13), + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.delete_outline, color: AppColors.error, size: 22), + const SizedBox(height: 4), + Text('Supprimer', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.error)), + ], + ), + ), + child: OrganizationCard( + organization: org, + onTap: () => _showOrganizationDetails(org), + onEdit: () => _showEditOrganizationDialog(org), + onDelete: () => _confirmDeleteOrganization(org), + showActions: canManage, + ), + ); + } + Widget _buildLoadingMorePlaceholder(BuildContext context, List orgs) { return ListView.separated( shrinkWrap: true,