diff --git a/lib/models/wallet.dart b/lib/models/wallet.dart index 69b3dfc..40d514a 100644 --- a/lib/models/wallet.dart +++ b/lib/models/wallet.dart @@ -35,3 +35,24 @@ abstract class SnWalletPocket with _$SnWalletPocket { factory SnWalletPocket.fromJson(Map json) => _$SnWalletPocketFromJson(json); } + +@freezed +abstract class SnTransaction with _$SnTransaction { + const factory SnTransaction({ + required String id, + required String currency, + required double amount, + required String? remarks, + required int type, + required String? payerWalletId, + required SnWallet? payerWallet, + required String? payeeWalletId, + required SnWallet? payeeWallet, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + }) = _SnTransaction; + + factory SnTransaction.fromJson(Map json) => + _$SnTransactionFromJson(json); +} diff --git a/lib/models/wallet.freezed.dart b/lib/models/wallet.freezed.dart index 2d3f339..466aaea 100644 --- a/lib/models/wallet.freezed.dart +++ b/lib/models/wallet.freezed.dart @@ -344,4 +344,218 @@ as DateTime?, } + +/// @nodoc +mixin _$SnTransaction { + + String get id; String get currency; double get amount; String? get remarks; int get type; String? get payerWalletId; SnWallet? get payerWallet; String? get payeeWalletId; SnWallet? get payeeWallet; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; +/// Create a copy of SnTransaction +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnTransactionCopyWith get copyWith => _$SnTransactionCopyWithImpl(this as SnTransaction, _$identity); + + /// Serializes this SnTransaction to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnTransaction&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.remarks, remarks) || other.remarks == remarks)&&(identical(other.type, type) || other.type == type)&&(identical(other.payerWalletId, payerWalletId) || other.payerWalletId == payerWalletId)&&(identical(other.payerWallet, payerWallet) || other.payerWallet == payerWallet)&&(identical(other.payeeWalletId, payeeWalletId) || other.payeeWalletId == payeeWalletId)&&(identical(other.payeeWallet, payeeWallet) || other.payeeWallet == payeeWallet)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,currency,amount,remarks,type,payerWalletId,payerWallet,payeeWalletId,payeeWallet,createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnTransaction(id: $id, currency: $currency, amount: $amount, remarks: $remarks, type: $type, payerWalletId: $payerWalletId, payerWallet: $payerWallet, payeeWalletId: $payeeWalletId, payeeWallet: $payeeWallet, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $SnTransactionCopyWith<$Res> { + factory $SnTransactionCopyWith(SnTransaction value, $Res Function(SnTransaction) _then) = _$SnTransactionCopyWithImpl; +@useResult +$Res call({ + String id, String currency, double amount, String? remarks, int type, String? payerWalletId, SnWallet? payerWallet, String? payeeWalletId, SnWallet? payeeWallet, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + +$SnWalletCopyWith<$Res>? get payerWallet;$SnWalletCopyWith<$Res>? get payeeWallet; + +} +/// @nodoc +class _$SnTransactionCopyWithImpl<$Res> + implements $SnTransactionCopyWith<$Res> { + _$SnTransactionCopyWithImpl(this._self, this._then); + + final SnTransaction _self; + final $Res Function(SnTransaction) _then; + +/// Create a copy of SnTransaction +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? currency = null,Object? amount = null,Object? remarks = freezed,Object? type = null,Object? payerWalletId = freezed,Object? payerWallet = freezed,Object? payeeWalletId = freezed,Object? payeeWallet = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable +as String,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable +as double,remarks: freezed == remarks ? _self.remarks : remarks // ignore: cast_nullable_to_non_nullable +as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as int,payerWalletId: freezed == payerWalletId ? _self.payerWalletId : payerWalletId // ignore: cast_nullable_to_non_nullable +as String?,payerWallet: freezed == payerWallet ? _self.payerWallet : payerWallet // ignore: cast_nullable_to_non_nullable +as SnWallet?,payeeWalletId: freezed == payeeWalletId ? _self.payeeWalletId : payeeWalletId // ignore: cast_nullable_to_non_nullable +as String?,payeeWallet: freezed == payeeWallet ? _self.payeeWallet : payeeWallet // ignore: cast_nullable_to_non_nullable +as SnWallet?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} +/// Create a copy of SnTransaction +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnWalletCopyWith<$Res>? get payerWallet { + if (_self.payerWallet == null) { + return null; + } + + return $SnWalletCopyWith<$Res>(_self.payerWallet!, (value) { + return _then(_self.copyWith(payerWallet: value)); + }); +}/// Create a copy of SnTransaction +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnWalletCopyWith<$Res>? get payeeWallet { + if (_self.payeeWallet == null) { + return null; + } + + return $SnWalletCopyWith<$Res>(_self.payeeWallet!, (value) { + return _then(_self.copyWith(payeeWallet: value)); + }); +} +} + + +/// @nodoc +@JsonSerializable() + +class _SnTransaction implements SnTransaction { + const _SnTransaction({required this.id, required this.currency, required this.amount, required this.remarks, required this.type, required this.payerWalletId, required this.payerWallet, required this.payeeWalletId, required this.payeeWallet, required this.createdAt, required this.updatedAt, required this.deletedAt}); + factory _SnTransaction.fromJson(Map json) => _$SnTransactionFromJson(json); + +@override final String id; +@override final String currency; +@override final double amount; +@override final String? remarks; +@override final int type; +@override final String? payerWalletId; +@override final SnWallet? payerWallet; +@override final String? payeeWalletId; +@override final SnWallet? payeeWallet; +@override final DateTime createdAt; +@override final DateTime updatedAt; +@override final DateTime? deletedAt; + +/// Create a copy of SnTransaction +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnTransactionCopyWith<_SnTransaction> get copyWith => __$SnTransactionCopyWithImpl<_SnTransaction>(this, _$identity); + +@override +Map toJson() { + return _$SnTransactionToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnTransaction&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.remarks, remarks) || other.remarks == remarks)&&(identical(other.type, type) || other.type == type)&&(identical(other.payerWalletId, payerWalletId) || other.payerWalletId == payerWalletId)&&(identical(other.payerWallet, payerWallet) || other.payerWallet == payerWallet)&&(identical(other.payeeWalletId, payeeWalletId) || other.payeeWalletId == payeeWalletId)&&(identical(other.payeeWallet, payeeWallet) || other.payeeWallet == payeeWallet)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,currency,amount,remarks,type,payerWalletId,payerWallet,payeeWalletId,payeeWallet,createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnTransaction(id: $id, currency: $currency, amount: $amount, remarks: $remarks, type: $type, payerWalletId: $payerWalletId, payerWallet: $payerWallet, payeeWalletId: $payeeWalletId, payeeWallet: $payeeWallet, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnTransactionCopyWith<$Res> implements $SnTransactionCopyWith<$Res> { + factory _$SnTransactionCopyWith(_SnTransaction value, $Res Function(_SnTransaction) _then) = __$SnTransactionCopyWithImpl; +@override @useResult +$Res call({ + String id, String currency, double amount, String? remarks, int type, String? payerWalletId, SnWallet? payerWallet, String? payeeWalletId, SnWallet? payeeWallet, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + +@override $SnWalletCopyWith<$Res>? get payerWallet;@override $SnWalletCopyWith<$Res>? get payeeWallet; + +} +/// @nodoc +class __$SnTransactionCopyWithImpl<$Res> + implements _$SnTransactionCopyWith<$Res> { + __$SnTransactionCopyWithImpl(this._self, this._then); + + final _SnTransaction _self; + final $Res Function(_SnTransaction) _then; + +/// Create a copy of SnTransaction +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? currency = null,Object? amount = null,Object? remarks = freezed,Object? type = null,Object? payerWalletId = freezed,Object? payerWallet = freezed,Object? payeeWalletId = freezed,Object? payeeWallet = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_SnTransaction( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable +as String,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable +as double,remarks: freezed == remarks ? _self.remarks : remarks // ignore: cast_nullable_to_non_nullable +as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as int,payerWalletId: freezed == payerWalletId ? _self.payerWalletId : payerWalletId // ignore: cast_nullable_to_non_nullable +as String?,payerWallet: freezed == payerWallet ? _self.payerWallet : payerWallet // ignore: cast_nullable_to_non_nullable +as SnWallet?,payeeWalletId: freezed == payeeWalletId ? _self.payeeWalletId : payeeWalletId // ignore: cast_nullable_to_non_nullable +as String?,payeeWallet: freezed == payeeWallet ? _self.payeeWallet : payeeWallet // ignore: cast_nullable_to_non_nullable +as SnWallet?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + +/// Create a copy of SnTransaction +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnWalletCopyWith<$Res>? get payerWallet { + if (_self.payerWallet == null) { + return null; + } + + return $SnWalletCopyWith<$Res>(_self.payerWallet!, (value) { + return _then(_self.copyWith(payerWallet: value)); + }); +}/// Create a copy of SnTransaction +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnWalletCopyWith<$Res>? get payeeWallet { + if (_self.payeeWallet == null) { + return null; + } + + return $SnWalletCopyWith<$Res>(_self.payeeWallet!, (value) { + return _then(_self.copyWith(payeeWallet: value)); + }); +} +} + // dart format on diff --git a/lib/models/wallet.g.dart b/lib/models/wallet.g.dart index 882e14f..1523309 100644 --- a/lib/models/wallet.g.dart +++ b/lib/models/wallet.g.dart @@ -59,3 +59,44 @@ Map _$SnWalletPocketToJson(_SnWalletPocket instance) => 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), }; + +_SnTransaction _$SnTransactionFromJson(Map json) => + _SnTransaction( + id: json['id'] as String, + currency: json['currency'] as String, + amount: (json['amount'] as num).toDouble(), + remarks: json['remarks'] as String?, + type: (json['type'] as num).toInt(), + payerWalletId: json['payer_wallet_id'] as String?, + payerWallet: + json['payer_wallet'] == null + ? null + : SnWallet.fromJson(json['payer_wallet'] as Map), + payeeWalletId: json['payee_wallet_id'] as String?, + payeeWallet: + json['payee_wallet'] == null + ? null + : SnWallet.fromJson(json['payee_wallet'] as Map), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: + json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + ); + +Map _$SnTransactionToJson(_SnTransaction instance) => + { + 'id': instance.id, + 'currency': instance.currency, + 'amount': instance.amount, + 'remarks': instance.remarks, + 'type': instance.type, + 'payer_wallet_id': instance.payerWalletId, + 'payer_wallet': instance.payerWallet?.toJson(), + 'payee_wallet_id': instance.payeeWalletId, + 'payee_wallet': instance.payeeWallet?.toJson(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + }; diff --git a/lib/screens/wallet.dart b/lib/screens/wallet.dart index 66cd55e..e1cf132 100644 --- a/lib/screens/wallet.dart +++ b/lib/screens/wallet.dart @@ -7,6 +7,7 @@ import 'package:island/pods/network.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:styled_widget/styled_widget.dart'; part 'wallet.g.dart'; @@ -23,6 +24,44 @@ const Map kCurrencyIconData = { 'golds': Symbols.attach_money, }; +@riverpod +class TransactionListNotifier extends _$TransactionListNotifier + with CursorPagingNotifierMixin { + static const int _pageSize = 20; + + @override + Future> build() => fetch(cursor: null); + + @override + Future> fetch({ + required String? cursor, + }) async { + final client = ref.read(apiClientProvider); + final offset = cursor == null ? 0 : int.parse(cursor); + + final queryParams = {'offset': offset, 'take': _pageSize}; + + final response = await client.get( + '/wallets/transactions', + queryParameters: queryParams, + ); + final total = int.parse(response.headers.value('X-Total') ?? '0'); + final List data = response.data; + final transactions = + data.map((json) => SnTransaction.fromJson(json)).toList(); + + final hasMore = offset + transactions.length < total; + final nextCursor = + hasMore ? (offset + transactions.length).toString() : null; + + return CursorPagingData( + items: transactions, + hasMore: hasMore, + nextCursor: nextCursor, + ); + } +} + @RoutePage() class WalletScreen extends HookConsumerWidget { const WalletScreen({super.key}); @@ -40,26 +79,75 @@ class WalletScreen extends HookConsumerWidget { body: wallet.when( data: (data) { return Column( - spacing: 8, children: [ - ...data.pockets.map( - (pocket) => Card( - margin: EdgeInsets.zero, - child: ListTile( - leading: Icon( - kCurrencyIconData[pocket.currency] ?? - Symbols.universal_currency_alt, - ), - title: - Text(getCurrencyTranslationKey(pocket.currency)).tr(), - subtitle: Text( - '${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}', + Column( + spacing: 8, + children: [ + ...data.pockets.map( + (pocket) => Card( + margin: EdgeInsets.zero, + child: ListTile( + leading: Icon( + kCurrencyIconData[pocket.currency] ?? + Symbols.universal_currency_alt, + ), + title: + Text( + getCurrencyTranslationKey(pocket.currency), + ).tr(), + subtitle: Text( + '${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}', + ), + ), ), ), + ], + ).padding(horizontal: 16, vertical: 16), + const Divider(height: 1), + Expanded( + child: PagingHelperView( + provider: transactionListNotifierProvider, + futureRefreshable: transactionListNotifierProvider.future, + notifierRefreshable: transactionListNotifierProvider.notifier, + contentBuilder: + (data, widgetCount, endItemView) => ListView.builder( + padding: EdgeInsets.zero, + itemCount: widgetCount, + itemBuilder: (context, index) { + if (index == widgetCount - 1) { + return endItemView; + } + + final transaction = data.items[index]; + final isIncome = + transaction.payeeWalletId == wallet.value?.id; + + return ListTile( + key: ValueKey(transaction.id), + leading: Icon( + isIncome + ? Symbols.arrow_upward + : Symbols.arrow_downward, + ), + title: Text(transaction.remarks ?? ''), + subtitle: Text( + DateFormat.yMd().add_Hm().format( + transaction.createdAt, + ), + ), + trailing: Text( + '${isIncome ? '+' : '-'}${transaction.amount.toStringAsFixed(2)} ${transaction.currency}', + style: TextStyle( + color: isIncome ? Colors.green : Colors.red, + ), + ), + ); + }, + ), ), ), ], - ).padding(horizontal: 16, vertical: 16); + ); }, error: (error, stackTrace) => Center(child: Text('Error: $error')), loading: () => const Center(child: CircularProgressIndicator()), diff --git a/lib/screens/wallet.g.dart b/lib/screens/wallet.g.dart index d6f0f4f..5c2db40 100644 --- a/lib/screens/wallet.g.dart +++ b/lib/screens/wallet.g.dart @@ -24,5 +24,26 @@ final walletCurrentProvider = AutoDisposeFutureProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef WalletCurrentRef = AutoDisposeFutureProviderRef; +String _$transactionListNotifierHash() => + r'148ffb0ee9e3be3b92de432f314d8ee2f09e9a24'; + +/// See also [TransactionListNotifier]. +@ProviderFor(TransactionListNotifier) +final transactionListNotifierProvider = AutoDisposeAsyncNotifierProvider< + TransactionListNotifier, + CursorPagingData +>.internal( + TransactionListNotifier.new, + name: r'transactionListNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$transactionListNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$TransactionListNotifier = + AutoDisposeAsyncNotifier>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package