feat(organizations): parité UX avec Annuaire Membres — filtres statut + grille + swipe
3 fonctionnalités ajoutées pour aligner avec l'UX de l'Annuaire Membres : 1. Filtres par statut (chips) : Tous, Active, En création, Inactive, Suspendue, Dissoute Chips horizontaux scrollables sous la barre de recherche, filtre local (combiné avec le filtre par type de la TabBar + recherche texte) 2. Vue grille toggle : bouton grid/list dans l'AppBar Grid 2 colonnes avec cartes compactes (icône, nom, statut, type, localisation, membres) 3. Swipe actions : swipe DROIT → modifier, swipe GAUCHE → supprimer Uniquement pour SuperAdmin/OrgAdmin (canManage=true) Background coloré avec icônes + labels confirmDismiss retourne false → la carte ne disparaît pas
This commit is contained in:
@@ -33,6 +33,18 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
|||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
List<String?> _availableTypes = [];
|
List<String?> _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.
|
/// Cache de la dernière liste connue.
|
||||||
/// Évite de perdre l'affichage quand le bloc passe dans un état non-liste
|
/// Évite de perdre l'affichage quand le bloc passe dans un état non-liste
|
||||||
/// (OrganizationLoaded, OrganizationCreated, etc.) après navigation vers
|
/// (OrganizationLoaded, OrganizationCreated, etc.) après navigation vers
|
||||||
@@ -152,6 +164,11 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
|||||||
moduleGradient: ModuleColors.organisationsGradient,
|
moduleGradient: ModuleColors.organisationsGradient,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(_isGridView ? Icons.view_list_rounded : Icons.grid_view_rounded),
|
||||||
|
onPressed: () => setState(() => _isGridView = !_isGridView),
|
||||||
|
tooltip: _isGridView ? 'Vue liste' : 'Vue grille',
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.category_outlined),
|
icon: const Icon(Icons.category_outlined),
|
||||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const OrgTypesPage())),
|
onPressed: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const OrgTypesPage())),
|
||||||
@@ -266,11 +283,15 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
|||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
child: _buildSearchBar(context, state),
|
child: _buildSearchBar(context, state),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: SpacingTokens.sm),
|
||||||
|
_buildStatutChips(context),
|
||||||
const SizedBox(height: SpacingTokens.md),
|
const SizedBox(height: SpacingTokens.md),
|
||||||
AnimatedSlideIn(
|
AnimatedSlideIn(
|
||||||
duration: const Duration(milliseconds: 800),
|
duration: const Duration(milliseconds: 800),
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
child: _buildOrganizationsList(context, state),
|
child: _isGridView
|
||||||
|
? _buildOrganizationsGrid(context, state)
|
||||||
|
: _buildOrganizationsList(context, state),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 80),
|
const SizedBox(height: 80),
|
||||||
],
|
],
|
||||||
@@ -390,10 +411,57 @@ class _OrganizationsPageState extends State<OrganizationsPage> 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<OrganizationModel> _applyStatutFilter(List<OrganizationModel> 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) {
|
Widget _buildOrganizationsList(BuildContext context, OrganizationsLoaded state) {
|
||||||
final organizations = state.filteredOrganizations;
|
final organizations = _applyStatutFilter(state.filteredOrganizations);
|
||||||
if (organizations.isEmpty) return _buildEmptyState(context);
|
if (organizations.isEmpty) return _buildEmptyState(context);
|
||||||
|
|
||||||
final authState = context.read<AuthBloc>().state;
|
final authState = context.read<AuthBloc>().state;
|
||||||
@@ -409,18 +477,189 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
|||||||
final org = organizations[i];
|
final org = organizations[i];
|
||||||
return AnimatedFadeIn(
|
return AnimatedFadeIn(
|
||||||
duration: Duration(milliseconds: 300 + (i * 50)),
|
duration: Duration(milliseconds: 300 + (i * 50)),
|
||||||
child: OrganizationCard(
|
child: _buildSwipeableCard(context, org, canManageOrgs),
|
||||||
organization: org,
|
|
||||||
onTap: () => _showOrganizationDetails(org),
|
|
||||||
onEdit: canManageOrgs ? () => _showEditOrganizationDialog(org) : null,
|
|
||||||
onDelete: canManageOrgs ? () => _confirmDeleteOrganization(org) : null,
|
|
||||||
showActions: 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<OrganizationModel> orgs) {
|
Widget _buildLoadingMorePlaceholder(BuildContext context, List<OrganizationModel> orgs) {
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user