Files
unionflow-mobile-apps/lib/features/epargne/presentation/widgets/historique_epargne_sheet.dart
dahoud d094d6db9c Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
2026-03-15 16:30:08 +00:00

209 lines
8.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import '../../data/models/compte_epargne_model.dart';
import '../../data/models/transaction_epargne_model.dart';
import '../../data/repositories/transaction_epargne_repository.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
/// Bottom sheet affichant l'historique complet des transactions d'un compte (charge et rafraîchit les données).
class HistoriqueEpargneSheet extends StatefulWidget {
final CompteEpargneModel compte;
const HistoriqueEpargneSheet({
super.key,
required this.compte,
});
@override
State<HistoriqueEpargneSheet> createState() => _HistoriqueEpargneSheetState();
}
class _HistoriqueEpargneSheetState extends State<HistoriqueEpargneSheet> {
List<TransactionEpargneModel> _transactions = [];
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
if (widget.compte.id == null) {
setState(() {
_loading = false;
_transactions = [];
});
return;
}
setState(() {
_loading = true;
_error = null;
});
try {
final repo = GetIt.I<TransactionEpargneRepository>();
final list = await repo.getByCompte(widget.compte.id!);
if (!mounted) return;
setState(() {
_transactions = list.map((e) => TransactionEpargneModel.fromJson(e)).toList();
_loading = false;
_error = null;
});
} catch (e) {
if (!mounted) return;
setState(() {
_transactions = [];
_loading = false;
_error = e.toString().replaceFirst('Exception: ', '');
});
}
}
String _libelleType(String? type) {
if (type == null) return '';
const map = {
'DEPOT': 'Dépôt',
'RETRAIT': 'Retrait',
'TRANSFERT_ENTRANT': 'Virement reçu',
'TRANSFERT_SORTANT': 'Virement envoyé',
};
return map[type] ?? type;
}
@override
Widget build(BuildContext context) {
final compte = widget.compte;
return DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.3,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return Column(
children: [
Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: ColorTokens.onSurfaceVariant.withOpacity(0.4),
borderRadius: BorderRadius.circular(2),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Historique — ${compte.numeroCompte ?? compte.id}',
style: TypographyTokens.titleMedium,
),
IconButton(
icon: _loading ? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
) : const Icon(Icons.refresh),
onPressed: _loading ? null : _load,
tooltip: 'Actualiser',
),
],
),
),
const SizedBox(height: 8),
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(_error!, style: TextStyle(color: ColorTokens.error), textAlign: TextAlign.center),
const SizedBox(height: SpacingTokens.md),
FilledButton.tonal(onPressed: _load, child: const Text('Réessayer')),
],
),
),
)
: _transactions.isEmpty
? Center(
child: Text(
'Aucune transaction',
style: TypographyTokens.bodySmall?.copyWith(color: ColorTokens.onSurfaceVariant),
),
)
: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg, vertical: SpacingTokens.sm),
itemCount: _transactions.length,
itemBuilder: (context, index) {
final t = _transactions[index];
return Card(
margin: const EdgeInsets.only(bottom: SpacingTokens.sm),
child: ListTile(
leading: CircleAvatar(
backgroundColor: t.isCredit
? ColorTokens.success.withOpacity(0.2)
: ColorTokens.error.withOpacity(0.2),
child: Icon(
t.isCredit ? Icons.arrow_downward : Icons.arrow_upward,
color: t.isCredit ? ColorTokens.success : ColorTokens.error,
size: 20,
),
),
title: Text(
_libelleType(t.type),
style: TypographyTokens.bodyMedium,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (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),
),
if (t.motif != null && t.motif!.isNotEmpty)
Text(
t.motif!,
style: TypographyTokens.bodySmall?.copyWith(color: ColorTokens.onSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${t.isCredit ? '+' : '-'}${t.montant.toStringAsFixed(0)} XOF',
style: TypographyTokens.titleSmall?.copyWith(
color: t.isCredit ? ColorTokens.success : ColorTokens.error,
fontWeight: FontWeight.w600,
),
),
Text(
'Solde: ${t.soldeApres.toStringAsFixed(0)}',
style: TypographyTokens.labelSmall?.copyWith(color: ColorTokens.onSurfaceVariant),
),
],
),
),
);
},
),
),
],
);
},
);
}
}