Files
dahoud 55f84da49a 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
2026-04-15 20:13:50 +00:00

264 lines
8.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
import '../../../../shared/widgets/info_badge.dart';
import '../../../../shared/widgets/mini_avatar.dart';
import '../../bloc/solidarity_bloc.dart';
import '../../data/models/demande_aide_model.dart';
import 'demande_aide_detail_page.dart';
import '../../../authentication/presentation/bloc/auth_bloc.dart';
/// Page liste des demandes d'aide (solidarité) - Version Épurée
class DemandesAidePage extends StatefulWidget {
const DemandesAidePage({super.key});
@override
State<DemandesAidePage> createState() => _DemandesAidePageState();
}
class _DemandesAidePageState extends State<DemandesAidePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final _currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: 'FCFA', decimalDigits: 0);
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_loadTab(0);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _loadTab(int index) {
bool isGestionnaire = false;
final authState = context.read<AuthBloc>().state;
if (authState is AuthAuthenticated) {
isGestionnaire = authState.effectiveRole.level >= 50;
}
if (isGestionnaire) {
switch (index) {
case 0:
context.read<SolidarityBloc>().add(const SearchDemandesAide()); // Search sans statut = getAll
break;
case 1:
context.read<SolidarityBloc>().add(const SearchDemandesAide(statut: 'EN_ATTENTE'));
break;
case 2:
context.read<SolidarityBloc>().add(const SearchDemandesAide(statut: 'APPROUVEE'));
break;
}
} else {
// Normal member always fetches their own requests
context.read<SolidarityBloc>().add(const LoadDemandesAide());
}
}
@override
Widget build(BuildContext context) {
return BlocListener<SolidarityBloc, SolidarityState>(
listener: (context, state) {
if (state.status == SolidarityStatus.error && state.message != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message!,
style: AppTypography.bodyTextSmall.copyWith(color: Colors.white)),
backgroundColor: AppColors.error,
),
);
}
},
child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'Solidarité',
moduleGradient: ModuleColors.solidariteGradient,
bottom: TabBar(
controller: _tabController,
onTap: _loadTab,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
indicatorSize: TabBarIndicatorSize.label,
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
tabs: const [
Tab(child: Text('TOUTES')),
Tab(child: Text('ATTENTE')),
Tab(child: Text('APPROUVÉES')),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildList(null),
_buildList('EN_ATTENTE'),
_buildList('APPROUVEE'),
],
),
),
);
}
Widget _buildList(String? statutFilter) {
return BlocBuilder<SolidarityBloc, SolidarityState>(
buildWhen: (prev, curr) =>
prev.status != curr.status || prev.demandes != curr.demandes,
builder: (context, state) {
if (state.status == SolidarityStatus.loading && state.demandes.isEmpty) {
return const Center(child: CircularProgressIndicator(strokeWidth: 2));
}
var list = state.demandes;
if (statutFilter != null) {
list = list.where((d) => d.statut == statutFilter).toList();
}
if (list.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.volunteer_activism_outlined,
size: 32,
color: Theme.of(context).brightness == Brightness.dark
? AppColors.borderDark
: AppColors.border,
),
const SizedBox(height: 12),
Text('Aucune demande', style: AppTypography.subtitleSmall),
],
),
);
}
return RefreshIndicator(
onRefresh: () async => _loadTab(_tabController.index),
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
itemCount: list.length,
itemBuilder: (context, index) {
return _DemandeCard(
demande: list[index],
currencyFormat: _currencyFormat,
onTap: () => _openDetail(list[index]),
);
},
),
);
},
);
}
void _openDetail(DemandeAideModel d) {
if (d.id == null) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: context.read<SolidarityBloc>(),
child: DemandeAideDetailPage(demandeId: d.id!),
),
),
).then((_) => _loadTab(_tabController.index));
}
}
class _DemandeCard extends StatelessWidget {
final DemandeAideModel demande;
final NumberFormat currencyFormat;
final VoidCallback onTap;
const _DemandeCard({
required this.demande,
required this.currencyFormat,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return CoreCard(
margin: const EdgeInsets.only(bottom: 10),
onTap: onTap,
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const MiniAvatar(size: 24, fallbackText: '?'),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
demande.titre ?? 'Demande sans titre',
style: AppTypography.actionText.copyWith(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
demande.numeroReference ?? demande.id?.substring(0, 8) ?? '',
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
),
],
),
),
_buildStatutBadge(context, demande.statut),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('MONTANT DEMANDÉ', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
Text(
currencyFormat.format(demande.montantDemande ?? 0),
style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primary),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('TYPE', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
Text(demande.typeLibelle, style: AppTypography.bodyTextSmall.copyWith(fontSize: 10)),
],
),
],
),
],
),
);
}
Widget _buildStatutBadge(BuildContext context, String? statut) {
Color color;
switch (statut) {
case 'APPROUVEE':
color = AppColors.success;
break;
case 'REJETEE':
color = AppColors.error;
break;
case 'EN_ATTENTE':
case 'SOUMISE':
color = AppColors.primaryLight;
break;
default:
color = Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary;
}
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
}
}