feat(mobile): consolidation modules epargne, messaging, organisations

- Epargne: badge LCB-FT (bouclier ambre) sur comptes avec fonds bloques + note recap
- EpargneDetail: historique pagine (page/size), affichage soldeAvant/soldeApres/motif dans chaque transaction, bouton "Charger plus"
- TransactionEpargneRepository: getByCompte accepte page et size, gere reponse paginee Spring (content[])
- MessagingDatasource: markMessageAsRead silencieuse (pas d'endpoint unitaire), getUnreadCount somme unreadCount des conversations
- OrganizationDetail: _memberCount charge le vrai nombre depuis GET /membres/count, affiche la valeur reelle au lieu de nombreMembres (toujours 0)
This commit is contained in:
dahoud
2026-04-05 13:37:21 +00:00
parent 65b5c79c43
commit 289efc4956
7 changed files with 160 additions and 38 deletions

View File

@@ -32,6 +32,10 @@ class EpargneDetailPage extends StatefulWidget {
class _EpargneDetailPageState extends State<EpargneDetailPage> {
List<TransactionEpargneModel> _transactions = [];
bool _loadingTx = true;
bool _loadingMore = false;
bool _hasMore = true;
int _page = 0;
static const int _pageSize = 20;
String? _errorTx;
CompteEpargneModel? _compte; // rafraîchi après actions
@@ -39,7 +43,7 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
void initState() {
super.initState();
_compte = widget.compte;
_loadTransactions();
_loadTransactions(reset: true);
}
Future<void> _refreshCompte() async {
@@ -59,33 +63,41 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
}
}
Future<void> _loadTransactions() async {
Future<void> _loadTransactions({bool reset = false}) async {
if (_compte?.id == null) {
setState(() {
_loadingTx = false;
_transactions = [];
});
setState(() { _loadingTx = false; _transactions = []; });
return;
}
setState(() {
_loadingTx = true;
_errorTx = null;
});
if (reset) {
setState(() { _loadingTx = true; _errorTx = null; _page = 0; _hasMore = true; });
} else {
if (_loadingMore || !_hasMore) return;
setState(() => _loadingMore = true);
}
try {
final repo = GetIt.I<TransactionEpargneRepository>();
final list = await repo.getByCompte(_compte!.id!);
final list = await repo.getByCompte(_compte!.id!, page: _page, size: _pageSize);
if (!mounted) return;
final parsed = list.map((e) => TransactionEpargneModel.fromJson(e)).toList();
setState(() {
_transactions = list.map((e) => TransactionEpargneModel.fromJson(e)).toList();
if (reset) {
_transactions = parsed;
} else {
_transactions = [..._transactions, ...parsed];
}
_hasMore = parsed.length == _pageSize;
_page = _page + 1;
_loadingTx = false;
_loadingMore = false;
_errorTx = null;
});
} catch (e, st) {
AppLogger.error('EpargneDetailPage: _loadTransactions échoué', error: e, stackTrace: st);
if (!mounted) return;
setState(() {
_transactions = [];
if (reset) _transactions = [];
_loadingTx = false;
_loadingMore = false;
_errorTx = e.toString().replaceFirst('Exception: ', '');
});
}
@@ -99,7 +111,7 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
compteId: _compte!.id!,
onSuccess: () {
_refreshCompte();
_loadTransactions();
_loadTransactions(reset: true);
widget.onDataChanged?.call();
},
),
@@ -117,7 +129,7 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
soldeDisponible: soldeDispo,
onSuccess: () {
_refreshCompte();
_loadTransactions();
_loadTransactions(reset: true);
widget.onDataChanged?.call();
},
),
@@ -133,7 +145,7 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
tousLesComptes: widget.tousLesComptes,
onSuccess: () {
_refreshCompte();
_loadTransactions();
_loadTransactions(reset: true);
widget.onDataChanged?.call();
},
),
@@ -191,7 +203,7 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
child: RefreshIndicator(
onRefresh: () async {
await _refreshCompte();
await _loadTransactions();
await _loadTransactions(reset: true);
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
@@ -332,10 +344,13 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
textAlign: TextAlign.center,
),
)
else
else ...[
Card(
child: Column(
children: _transactions.take(10).map((t) {
children: _transactions.map((t) {
final dateStr = t.dateTransaction != null
? '${t.dateTransaction!.day.toString().padLeft(2, '0')}/${t.dateTransaction!.month.toString().padLeft(2, '0')}/${t.dateTransaction!.year} ${t.dateTransaction!.hour.toString().padLeft(2, '0')}:${t.dateTransaction!.minute.toString().padLeft(2, '0')}'
: null;
return ListTile(
leading: CircleAvatar(
backgroundColor: t.isCredit ? ColorTokens.success.withOpacity(0.2) : ColorTokens.error.withOpacity(0.2),
@@ -345,16 +360,28 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
size: 20,
),
),
title: Text(
_libelleType(t.type),
style: TypographyTokens.bodyMedium,
title: Text(_libelleType(t.type), style: TypographyTokens.bodyMedium),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (dateStr != null)
Text(dateStr, style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant)),
Text(
'Avant: ${t.soldeAvant.toStringAsFixed(0)} → Après: ${t.soldeApres.toStringAsFixed(0)} XOF',
style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant),
),
if (t.motif != null && t.motif!.isNotEmpty)
Text(
t.motif!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
subtitle: t.dateTransaction != null
? Text(
'${t.dateTransaction!.day.toString().padLeft(2, '0')}/${t.dateTransaction!.month.toString().padLeft(2, '0')}/${t.dateTransaction!.year} ${t.dateTransaction!.hour.toString().padLeft(2, '0')}:${t.dateTransaction!.minute.toString().padLeft(2, '0')}',
style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant),
)
: null,
trailing: Text(
'${t.isCredit ? '+' : '-'}${t.montant.toStringAsFixed(0)} XOF',
style: TypographyTokens.titleSmall.copyWith(
@@ -362,10 +389,22 @@ class _EpargneDetailPageState extends State<EpargneDetailPage> {
fontWeight: FontWeight.w600,
),
),
isThreeLine: t.motif != null && t.motif!.isNotEmpty,
);
}).toList(),
),
),
if (_hasMore)
Padding(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
child: _loadingMore
? const Center(child: CircularProgressIndicator())
: TextButton(
onPressed: () => _loadTransactions(),
child: const Text('Charger plus'),
),
),
],
],
),
),