Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user