initial commit
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/api/api_client.dart';
|
||||
import '../../../core/api/api_endpoints.dart';
|
||||
import '../domain/wallet.dart';
|
||||
|
||||
/// Dépôt de données pour le portefeuille électronique.
|
||||
class WalletRepository {
|
||||
WalletRepository(this._apiClient);
|
||||
|
||||
final ApiClient _apiClient;
|
||||
|
||||
Future<Wallet> fetchWallet() async {
|
||||
final response = await _apiClient.get(ApiEndpoints.wallet);
|
||||
final json = _extractObject(response.data);
|
||||
return Wallet.fromJson(json);
|
||||
}
|
||||
|
||||
Future<List<WalletTransaction>> fetchTransactions() async {
|
||||
final response = await _apiClient.get(ApiEndpoints.walletTransactions);
|
||||
final list = _extractList(response.data);
|
||||
|
||||
return list
|
||||
.map((json) => WalletTransaction.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<dynamic> _extractList(dynamic data) {
|
||||
if (data is List<dynamic>) return data;
|
||||
if (data is Map<String, dynamic> && data['data'] is List<dynamic>) {
|
||||
return data['data'] as List<dynamic>;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractObject(dynamic data) {
|
||||
if (data is Map<String, dynamic>) {
|
||||
if (data['data'] is Map<String, dynamic>) {
|
||||
return data['data'] as Map<String, dynamic>;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
throw StateError('Réponse API inattendue');
|
||||
}
|
||||
}
|
||||
|
||||
final walletRepositoryProvider = Provider<WalletRepository>((ref) {
|
||||
return WalletRepository(ref.watch(apiClientProvider));
|
||||
});
|
||||
|
||||
final walletProvider = FutureProvider<Wallet>((ref) async {
|
||||
return ref.watch(walletRepositoryProvider).fetchWallet();
|
||||
});
|
||||
|
||||
final walletTransactionsProvider =
|
||||
FutureProvider<List<WalletTransaction>>((ref) async {
|
||||
return ref.watch(walletRepositoryProvider).fetchTransactions();
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/// Modèle portefeuille électronique.
|
||||
class Wallet {
|
||||
const Wallet({
|
||||
required this.currency,
|
||||
required this.currentBalance,
|
||||
this.status = 'active',
|
||||
});
|
||||
|
||||
final String currency;
|
||||
final double currentBalance;
|
||||
final String status;
|
||||
|
||||
factory Wallet.fromJson(Map<String, dynamic> json) {
|
||||
return Wallet(
|
||||
currency: json['currency'] as String? ?? 'EUR',
|
||||
currentBalance: (json['current_balance'] as num?)?.toDouble() ?? 0,
|
||||
status: json['status'] as String? ?? 'active',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mouvement sur le portefeuille.
|
||||
class WalletTransaction {
|
||||
const WalletTransaction({
|
||||
required this.uuid,
|
||||
required this.type,
|
||||
required this.amount,
|
||||
required this.balanceAfter,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String uuid;
|
||||
final String type;
|
||||
final double amount;
|
||||
final double balanceAfter;
|
||||
final DateTime? createdAt;
|
||||
|
||||
factory WalletTransaction.fromJson(Map<String, dynamic> json) {
|
||||
return WalletTransaction(
|
||||
uuid: json['uuid'] as String,
|
||||
type: json['type'] as String,
|
||||
amount: (json['amount'] as num?)?.toDouble() ?? 0,
|
||||
balanceAfter: (json['balance_after'] as num?)?.toDouble() ?? 0,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.tryParse(json['created_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../data/wallet_repository.dart';
|
||||
|
||||
/// Écran du portefeuille électronique — solde et historique.
|
||||
class WalletScreen extends ConsumerWidget {
|
||||
const WalletScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final walletAsync = ref.watch(walletProvider);
|
||||
final transactionsAsync = ref.watch(walletTransactionsProvider);
|
||||
final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '€');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Mon portefeuille')),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(walletProvider);
|
||||
ref.invalidate(walletTransactionsProvider);
|
||||
},
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
walletAsync.when(
|
||||
loading: () => const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
error: (error, _) => Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text('Erreur solde : $error'),
|
||||
),
|
||||
),
|
||||
data: (wallet) => Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Solde disponible',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
currencyFormat.format(wallet.currentBalance),
|
||||
style: Theme.of(context).textTheme.displaySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Historique',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
transactionsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Text('Erreur historique : $error'),
|
||||
data: (transactions) {
|
||||
if (transactions.isEmpty) {
|
||||
return const Text('Aucune transaction pour le moment');
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: transactions.map((tx) {
|
||||
final isCredit = tx.type == 'credit' || tx.type == 'refund';
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
isCredit ? Icons.add_circle_outline : Icons.remove_circle_outline,
|
||||
color: isCredit ? Colors.green : Colors.red,
|
||||
),
|
||||
title: Text(tx.type),
|
||||
subtitle: tx.createdAt != null
|
||||
? Text(DateFormat('dd/MM/yyyy HH:mm').format(tx.createdAt!))
|
||||
: null,
|
||||
trailing: Text(
|
||||
'${isCredit ? '+' : '-'}${currencyFormat.format(tx.amount)}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isCredit ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Rechargement — à connecter à l\'API')),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Recharger'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user