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:
@@ -111,12 +111,23 @@ class TransactionEpargneRepository {
|
||||
throw Exception('Erreur transfert: ${response.statusCode}');
|
||||
}
|
||||
|
||||
/// Historique des transactions d'un compte.
|
||||
Future<List<Map<String, dynamic>>> getByCompte(String compteId) async {
|
||||
final response = await _apiClient.get('$_base/compte/$compteId');
|
||||
/// Historique des transactions d'un compte (paginé).
|
||||
Future<List<Map<String, dynamic>>> getByCompte(
|
||||
String compteId, {
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
}) async {
|
||||
final response = await _apiClient.get(
|
||||
'$_base/compte/$compteId',
|
||||
queryParameters: {'page': page, 'size': size},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
if (data is List) return List<Map<String, dynamic>>.from(data.map((e) => e as Map<String, dynamic>));
|
||||
if (data is Map && data.containsKey('content')) {
|
||||
final content = data['content'] as List? ?? [];
|
||||
return List<Map<String, dynamic>>.from(content.map((e) => e as Map<String, dynamic>));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
throw Exception('Erreur chargement historique: ${response.statusCode}');
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -186,6 +186,19 @@ class _EpargnePageState extends State<EpargnePage> {
|
||||
'Solde disponible total • ${_comptes.length} compte${_comptes.length > 1 ? 's' : ''}',
|
||||
style: AppTypography.bodyTextSmall.copyWith(color: AppColors.textSecondaryLight),
|
||||
),
|
||||
if (_comptes.any((c) => c.soldeBloque > 0)) ...[
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.shield_outlined, size: 12, color: Colors.amber.shade700),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Certains fonds sont sous surveillance LCB-FT',
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: Colors.amber.shade700),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -231,6 +244,29 @@ class _EpargnePageState extends State<EpargnePage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (c.soldeBloque > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: SpacingTokens.xs),
|
||||
child: Tooltip(
|
||||
message: 'Fonds bloqués — surveillance LCB-FT',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.amber.shade700, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.shield_outlined, size: 12, color: Colors.amber.shade700),
|
||||
const SizedBox(width: 3),
|
||||
Text('LCB-FT', style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: Colors.amber.shade700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (c.statut != null)
|
||||
InfoBadge(
|
||||
text: c.statut!,
|
||||
|
||||
Reference in New Issue
Block a user