From 87636a7ec3ecad78d40d8164b4ec032a3c6e0839 Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 21 Nov 2025 19:22:23 +0300 Subject: [PATCH 1/5] Multiple Wallet support, history of each wallet and updated payment page --- .vscode/launch.json | 48 +++ ci/.DS_Store | Bin 0 -> 6148 bytes ci/prod/.DS_Store | Bin 0 -> 8196 bytes frontend/.vscode/launch.json | 48 +++ .../pshared/lib/models/payment/operation.dart | 3 + frontend/pweb/devtools_options.yaml | 3 + frontend/pweb/lib/app/app.dart | 2 +- frontend/pweb/lib/main.dart | 31 +- frontend/pweb/lib/models/wallet.dart | 2 +- .../pweb/lib/models/wallet_transaction.dart | 74 +++++ .../lib/pages/payment_methods/add/card.dart | 13 +- .../lib/pages/payment_methods/add/iban.dart | 14 +- .../payment_methods/add/russian_bank.dart | 17 +- .../lib/pages/payment_methods/add/wallet.dart | 10 +- .../payment_methods/delete_confirmation.dart | 2 + .../lib/pages/payment_methods/header.dart | 34 ++ .../payment_methods/method_selector.dart | 27 ++ .../pweb/lib/pages/payment_methods/page.dart | 290 ++++++------------ .../payment_methods/payment_details.dart | 33 ++ .../pages/payment_methods/send_button.dart | 44 +++ .../widgets/payment_info_section.dart | 68 ++++ .../widgets/payment_page_body.dart | 144 +++++++++ .../widgets/recipient_section.dart | 187 +++++++++++ .../widgets/section_title.dart | 18 ++ .../wallet/edit/buttons/send.dart | 29 -- .../payment_page/wallet/edit/fields.dart | 49 --- .../payment_page/wallet/edit/header.dart | 122 -------- .../pages/payment_page/wallet/edit/page.dart | 55 ---- .../methods/advanced.dart | 0 .../methods/controller.dart | 0 .../methods/header.dart | 0 .../methods/list.dart | 2 +- .../methods/widget.dart | 8 +- .../{payment_page => payout_page}/page.dart | 4 +- .../wallet/card.dart | 0 .../wallet/edit/buttons/buttons.dart | 24 +- .../wallet/edit/buttons/save.dart | 0 .../payout_page/wallet/edit/buttons/send.dart | 29 ++ .../wallet/edit/buttons/top_up.dart | 0 .../pages/payout_page/wallet/edit/fields.dart | 53 ++++ .../pages/payout_page/wallet/edit/header.dart | 113 +++++++ .../pages/payout_page/wallet/edit/page.dart | 73 +++++ .../payout_page/wallet/history/chip.dart | 48 +++ .../payout_page/wallet/history/filters.dart | 94 ++++++ .../payout_page/wallet/history/history.dart | 116 +++++++ .../payout_page/wallet/history/table.dart | 88 ++++++ .../wallet/wigets.dart | 2 +- frontend/pweb/lib/pages/report/page.dart | 187 ++++------- .../pweb/lib/pages/report/table/widget.dart | 3 +- frontend/pweb/lib/providers/balance.dart | 32 -- frontend/pweb/lib/providers/operatioins.dart | 82 +++++ .../pweb/lib/providers/page_selector.dart | 85 ++++- .../lib/providers/payment_flow_provider.dart | 78 +++++ .../pweb/lib/providers/payment_methods.dart | 4 +- .../lib/providers/wallet_transactions.dart | 104 +++++++ frontend/pweb/lib/providers/wallets.dart | 7 +- frontend/pweb/lib/services/balance.dart | 22 -- frontend/pweb/lib/services/operations.dart | 85 +++++ .../services/payments/payment_methods.dart | 6 + .../lib/services/wallet_transactions.dart | 109 +++++++ frontend/pweb/lib/widgets/sidebar/page.dart | 8 +- frontend/pweb/macos/Podfile | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 6 +- frontend/pweb/pubspec.lock | 8 +- frontend/pweb/pubspec.yaml | 2 +- frontend/pweb/resources/logo.png | Bin 34400 -> 27268 bytes frontend/pweb/web/index.html | 2 +- frontend/pweb/web/manifest.json | 2 +- 68 files changed, 2154 insertions(+), 701 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 ci/.DS_Store create mode 100644 ci/prod/.DS_Store create mode 100644 frontend/.vscode/launch.json create mode 100644 frontend/pweb/devtools_options.yaml create mode 100644 frontend/pweb/lib/models/wallet_transaction.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/header.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/method_selector.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/payment_details.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/send_button.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/widgets/section_title.dart delete mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/send.dart delete mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/fields.dart delete mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/header.dart delete mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/page.dart rename frontend/pweb/lib/pages/{payment_page => payout_page}/methods/advanced.dart (100%) rename frontend/pweb/lib/pages/{payment_page => payout_page}/methods/controller.dart (100%) rename frontend/pweb/lib/pages/{payment_page => payout_page}/methods/header.dart (100%) rename frontend/pweb/lib/pages/{payment_page => payout_page}/methods/list.dart (94%) rename frontend/pweb/lib/pages/{payment_page => payout_page}/methods/widget.dart (82%) rename frontend/pweb/lib/pages/{payment_page => payout_page}/page.dart (87%) rename frontend/pweb/lib/pages/{payment_page => payout_page}/wallet/card.dart (100%) rename frontend/pweb/lib/pages/{payment_page => payout_page}/wallet/edit/buttons/buttons.dart (55%) rename frontend/pweb/lib/pages/{payment_page => payout_page}/wallet/edit/buttons/save.dart (100%) create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart rename frontend/pweb/lib/pages/{payment_page => payout_page}/wallet/edit/buttons/top_up.dart (100%) create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/history/chip.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/history/history.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/history/table.dart rename frontend/pweb/lib/pages/{payment_page => payout_page}/wallet/wigets.dart (95%) delete mode 100644 frontend/pweb/lib/providers/balance.dart create mode 100644 frontend/pweb/lib/providers/operatioins.dart create mode 100644 frontend/pweb/lib/providers/payment_flow_provider.dart create mode 100644 frontend/pweb/lib/providers/wallet_transactions.dart delete mode 100644 frontend/pweb/lib/services/balance.dart create mode 100644 frontend/pweb/lib/services/operations.dart create mode 100644 frontend/pweb/lib/services/wallet_transactions.dart diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9fe2095 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "pshared", + "cwd": "frontend/pshared", + "request": "launch", + "type": "dart" + }, + { + "name": "pshared (profile mode)", + "cwd": "frontend/pshared", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "pshared (release mode)", + "cwd": "frontend/pshared", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "pweb", + "cwd": "frontend/pweb", + "request": "launch", + "type": "dart" + }, + { + "name": "pweb (profile mode)", + "cwd": "frontend/pweb", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "pweb (release mode)", + "cwd": "frontend/pweb", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/ci/.DS_Store b/ci/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..edf009cf7306f12dad9e2d583d4ff5dbb9d45dc5 GIT binary patch literal 6148 zcmeHKPfrs;6rTqbwji>Uf+*2!QsW5&0wE9&t_5RIV~MR;1gyL5P&aIMn%!*+B1zAB z^aJSKFW|+*qhCNzCf+^y0X+ETPXR%%#+Z4@%vEaowyNYop7B$6&Ul~j_$at9&MG(^dbvl#|vmE0=x=E zmyL&BP+_esYXp@@#OCFIku;Jeb8K&KdSPzDnwg*7pRo28rl%&X*_pZh{iHFHnY?zt zwC&dd_CgW}c-{yZP}qT&s2r@vD>v171YbJq`DzYzR+Q}PKRs|}&>Tt)r&A-T(b3WL z*|Bry$Hy;}%wg9lZ&yS9u8YiPVb*WZ^=jx<@^z=|1=V6qcjCPFjF!yQG1vRF^7znH z*sY^a`p_WK-3loUM84eCRz&0C5q+&>4o2mW`Th!vyx3#G+9PMDpyu0%ko4u4)|P`U zhLbmu@LZjHMXNN75Kn{`*PJkPf-24KZu*W({alb`am;GmlV>ZlU?rqHQ_(}6FDrBX zyQDN@-)RdiV_%XMlVqfVWg*?s6hh`h?Tye2fm5T&WX8Uyh759rERq~4k{WqVUXj=2 zEqPBql27Ct`2jtUhRZMs^Kb)h!(Dg)t5AhHG@uDBnW3xm%`d8q3ZRY3!8dQ_!@y@j z6>S}V<)`5o1BxG`|Cn7rY6dg|ngM=3SlBq)3fmIJ(Se<~0w5;PECh9am*6p$LR(>5 zB95Sd3`LZopspAo!_jUjKU-m2q6{aXE+3#~26aOLF+1kBggXISqV6>Vnt@*#;Q#(8 zCa?cDaS#9D{x6(#ry0-;{GSX!Vky6r#kHy0dSaEl)>_z}VB^C5+7d+wcB&i;g_q(n cY(j9%;tHazuq_cSDEdc0(4afbz+YwHC(y~yC;$Ke literal 0 HcmV?d00001 diff --git a/ci/prod/.DS_Store b/ci/prod/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..eb701f3ea64ee0c7097a3744fdc8bc9fa2be0fe3 GIT binary patch literal 8196 zcmeHMPfrs;6n_Iqw<5AYL6m4VsqusXfe?rX*Mc#q5ke~#0qeFM>W1x3v%94rlJu-c zKY-r-0$xl!`UUi4;@y)Uz=Lo8RG@`}7h_;1nfX25pIP3oueP{uw$#8?V#g>{J{6!(Uf5h67PoCaOar;_4V@33nj2M7?STGy|;+@ZLQJ1?=3*P&wGYvpiZp z?@?-Vvm0soI@na6^ZSEZ|^z%8aLPQmp{*@$jOY4-)qnSD*J zOSJfOUzORZpig37A-204Q0j~Q7B)6Q8f6d2D><_#ECkH+7Fp;0?^a;`VnF#$ zSr=uVR`S+Q$#2rWUl&r+zA7~)B&C3_k%@++kR(5q-w8S6J7pS=C+&yIWWaTpg$2lB z)xCr_@D|>~NB9h1;0OF7og_i7kx?>DZj*cD0eM80Nr_ZQm28ok+@Y=U$zM@BDj; MaterialApp.router( - title: 'Profee Pay', + title: 'sendico', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Constants.themeColor), useMaterial3: true, diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 0ee7f60..2963427 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -15,21 +15,23 @@ import 'package:pshared/provider/pfe/provider.dart'; import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; -import 'package:pweb/providers/balance.dart'; import 'package:pweb/providers/carousel.dart'; import 'package:pweb/providers/mock_payment.dart'; +import 'package:pweb/providers/operatioins.dart'; import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/providers/recipient.dart'; import 'package:pweb/providers/two_factor.dart'; import 'package:pweb/providers/upload_history.dart'; import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/providers/wallet_transactions.dart'; import 'package:pweb/services/amplitude.dart'; import 'package:pweb/services/auth.dart'; -import 'package:pweb/services/balance.dart'; +import 'package:pweb/services/operations.dart'; import 'package:pweb/services/payments/payment_methods.dart'; import 'package:pweb/services/payments/upload_history.dart'; import 'package:pweb/services/recipient/recipient.dart'; +import 'package:pweb/services/wallet_transactions.dart'; import 'package:pweb/services/wallets.dart'; @@ -78,27 +80,32 @@ void main() async { ChangeNotifierProvider( create: (_) => WalletsProvider(MockWalletsService())..loadData(), ), + ChangeNotifierProvider( + create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(), + ), ChangeNotifierProvider( create: (_) => MockPaymentProvider(), ), ChangeNotifierProvider( create: (_) => RecipientProvider(RecipientService())..loadRecipients(), ), - ChangeNotifierProvider( - create: (context) { - final recipient = context.read(); - final wallets = context.read(); - return PageSelectorProvider( - recipientProvider: recipient, - walletsProvider: wallets, - ); - }, + + ChangeNotifierProxyProvider3( + create: (context) => PageSelectorProvider(), + update: (context, recipientProv, walletsProv, methodsProv, previous) => + previous ?? PageSelectorProvider( + recipientProvider: recipientProv, + walletsProvider: walletsProv, + methodsProvider: methodsProv, + )..update(recipientProv, walletsProv, methodsProv), ), + ChangeNotifierProvider( - create: (_) => BalanceProvider(MockBalanceService())..loadData(), + create: (_) => OperationProvider(OperationService())..loadOperations(), ), ], child: const PayApp(), ), + ); } diff --git a/frontend/pweb/lib/models/wallet.dart b/frontend/pweb/lib/models/wallet.dart index ed376bb..aef5133 100644 --- a/frontend/pweb/lib/models/wallet.dart +++ b/frontend/pweb/lib/models/wallet.dart @@ -35,4 +35,4 @@ class Wallet { isHidden: isHidden ?? this.isHidden, ); } -} +} \ No newline at end of file diff --git a/frontend/pweb/lib/models/wallet_transaction.dart b/frontend/pweb/lib/models/wallet_transaction.dart new file mode 100644 index 0000000..adcd036 --- /dev/null +++ b/frontend/pweb/lib/models/wallet_transaction.dart @@ -0,0 +1,74 @@ +import 'package:flutter/widgets.dart'; +import 'package:pshared/models/payment/status.dart'; + +import 'package:pweb/models/currency.dart'; + + +enum WalletTransactionType { topUp, payout } + +extension WalletTransactionTypeX on WalletTransactionType { + String label(BuildContext context) { + switch (this) { + case WalletTransactionType.topUp: + return 'Top up'; + case WalletTransactionType.payout: + return 'Payout'; + } + } + + String get sign => this == WalletTransactionType.topUp ? '+' : '-'; +} + +class WalletTransaction { + final String id; + final String walletId; + final WalletTransactionType type; + final OperationStatus status; + final double amount; + final Currency currency; + final DateTime date; + final String description; + final String? counterparty; + final double? balanceAfter; + + const WalletTransaction({ + required this.id, + required this.walletId, + required this.type, + required this.status, + required this.amount, + required this.currency, + required this.date, + required this.description, + this.counterparty, + this.balanceAfter, + }); + + bool get isTopUp => type == WalletTransactionType.topUp; + + WalletTransaction copyWith({ + String? id, + String? walletId, + WalletTransactionType? type, + OperationStatus? status, + double? amount, + Currency? currency, + DateTime? date, + String? description, + String? counterparty, + double? balanceAfter, + }) { + return WalletTransaction( + id: id ?? this.id, + walletId: walletId ?? this.walletId, + type: type ?? this.type, + status: status ?? this.status, + amount: amount ?? this.amount, + currency: currency ?? this.currency, + date: date ?? this.date, + description: description ?? this.description, + counterparty: counterparty ?? this.counterparty, + balanceAfter: balanceAfter ?? this.balanceAfter, + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/add/card.dart b/frontend/pweb/lib/pages/payment_methods/add/card.dart index a59ee5c..9449338 100644 --- a/frontend/pweb/lib/pages/payment_methods/add/card.dart +++ b/frontend/pweb/lib/pages/payment_methods/add/card.dart @@ -56,10 +56,21 @@ class _CardFormMinimalState extends State { @override void didUpdateWidget(covariant CardFormMinimal oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.initialData == null && oldWidget.initialData != null) { + final newData = widget.initialData; + final oldData = oldWidget.initialData; + + if (newData == null && oldData != null) { _panController.clear(); _firstNameController.clear(); _lastNameController.clear(); + return; + } + + if (newData != null && newData != oldData) { + _panController.text = newData.pan; + _firstNameController.text = newData.firstName; + _lastNameController.text = newData.lastName; + WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid()); } } diff --git a/frontend/pweb/lib/pages/payment_methods/add/iban.dart b/frontend/pweb/lib/pages/payment_methods/add/iban.dart index cf76227..6bd7fd4 100644 --- a/frontend/pweb/lib/pages/payment_methods/add/iban.dart +++ b/frontend/pweb/lib/pages/payment_methods/add/iban.dart @@ -57,11 +57,23 @@ class _IbanFormState extends State { @override void didUpdateWidget(covariant IbanForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.initialData == null && oldWidget.initialData != null) { + final newData = widget.initialData; + final oldData = oldWidget.initialData; + + if (newData == null && oldData != null) { _ibanController.clear(); _accountHolderController.clear(); _bicController.clear(); _bankNameController.clear(); + return; + } + + if (newData != null && newData != oldData) { + _ibanController.text = newData.iban; + _accountHolderController.text = newData.accountHolder; + _bicController.text = newData.bic ?? ''; + _bankNameController.text = newData.bankName ?? ''; + WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid()); } } diff --git a/frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart b/frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart index a6c944d..6a3aa61 100644 --- a/frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart +++ b/frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart @@ -66,7 +66,10 @@ class _RussianBankFormState extends State { @override void didUpdateWidget(covariant RussianBankForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.initialData == null && oldWidget.initialData != null) { + final newData = widget.initialData; + final oldData = oldWidget.initialData; + + if (newData == null && oldData != null) { _recipientNameController.clear(); _innController.clear(); _kppController.clear(); @@ -74,6 +77,18 @@ class _RussianBankFormState extends State { _bikController.clear(); _accountNumberController.clear(); _correspondentAccountController.clear(); + return; + } + + if (newData != null && newData != oldData) { + _recipientNameController.text = newData.recipientName; + _innController.text = newData.inn; + _kppController.text = newData.kpp; + _bankNameController.text = newData.bankName; + _bikController.text = newData.bik; + _accountNumberController.text = newData.accountNumber; + _correspondentAccountController.text = newData.correspondentAccount; + WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid()); } } diff --git a/frontend/pweb/lib/pages/payment_methods/add/wallet.dart b/frontend/pweb/lib/pages/payment_methods/add/wallet.dart index 28d136e..ad25650 100644 --- a/frontend/pweb/lib/pages/payment_methods/add/wallet.dart +++ b/frontend/pweb/lib/pages/payment_methods/add/wallet.dart @@ -41,8 +41,16 @@ class _WalletFormState extends State { @override void didUpdateWidget(covariant WalletForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.initialData == null && oldWidget.initialData != null) { + final newData = widget.initialData; + final oldData = oldWidget.initialData; + + if (newData == null && oldData != null) { _walletIdController.clear(); + return; + } + + if (newData != null && newData != oldData) { + _walletIdController.text = newData.walletId; } } diff --git a/frontend/pweb/lib/pages/payment_methods/delete_confirmation.dart b/frontend/pweb/lib/pages/payment_methods/delete_confirmation.dart index 290ab18..edf0404 100644 --- a/frontend/pweb/lib/pages/payment_methods/delete_confirmation.dart +++ b/frontend/pweb/lib/pages/payment_methods/delete_confirmation.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; + import 'package:pweb/generated/i18n/app_localizations.dart'; + Future showDeleteConfirmationDialog(BuildContext context) async { final l10n = AppLocalizations.of(context)!; return await showDialog( diff --git a/frontend/pweb/lib/pages/payment_methods/header.dart b/frontend/pweb/lib/pages/payment_methods/header.dart new file mode 100644 index 0000000..7b259cb --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/header.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/utils/dimensions.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentHeader extends StatelessWidget { + + const PaymentHeader({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final dimensions = AppDimensions(); + + return Row( + children: [ + Icon( + Icons.send_rounded, + color: theme.colorScheme.primary, + size: dimensions.iconSizeLarge + ), + SizedBox(width: dimensions.spacingSmall), + Text( + AppLocalizations.of(context)!.sendTo, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/method_selector.dart b/frontend/pweb/lib/pages/payment_methods/method_selector.dart new file mode 100644 index 0000000..a87c91c --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/method_selector.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/type.dart'; + +import 'package:pweb/providers/payment_methods.dart'; +import 'package:pweb/utils/payment/dropdown.dart'; + + +class PaymentMethodSelector extends StatelessWidget { + final PaymentMethodsProvider methodsProvider; + final ValueChanged onMethodChanged; + + const PaymentMethodSelector({ + super.key, + required this.methodsProvider, + required this.onMethodChanged, + }); + + @override + Widget build(BuildContext context) { + return PaymentMethodDropdown( + methods: methodsProvider.methods, + initialValue: methodsProvider.selectedMethod, + onChanged: onMethodChanged, + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/page.dart b/frontend/pweb/lib/pages/payment_methods/page.dart index 5f0f861..bd25550 100644 --- a/frontend/pweb/lib/pages/payment_methods/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -1,232 +1,120 @@ -import 'package:amplitude_flutter/amplitude.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; -import 'package:pweb/pages/dashboard/payouts/payment_form.dart'; -import 'package:pweb/pages/dashboard/payouts/single/form/details.dart'; -import 'package:pweb/pages/dashboard/payouts/single/form/header.dart'; +import 'package:pweb/providers/payment_flow_provider.dart'; +import 'package:pweb/pages/payment_methods/widgets/payment_page_body.dart'; +import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/providers/recipient.dart'; -import 'package:pweb/services/amplitude.dart'; -import 'package:pweb/utils/dimensions.dart'; -import 'package:pweb/utils/payment/dropdown.dart'; -import 'package:pweb/utils/payment/selector_type.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; -import 'package:pweb/widgets/sidebar/destinations.dart'; - - -//TODO: decide whether to make AppDimensions universal for the whole app or leave it as it is - unique for this page alone class PaymentPage extends StatefulWidget { - final PaymentType? type; final ValueChanged? onBack; - const PaymentPage({super.key, this.type, this.onBack}); + const PaymentPage({super.key, this.onBack}); @override State createState() => _PaymentPageState(); } class _PaymentPageState extends State { - late Map _availableTypes; - late PaymentType _selectedType; - bool _isFormVisible = false; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final recipientProvider = context.watch(); - final methodsProvider = context.watch(); - final recipient = recipientProvider.selectedRecipient; - - // Initialize available types based on whether we have a recipient - if (recipient != null) { - // We have a recipient - use their payment methods - _availableTypes = { - if (recipient.card != null) PaymentType.card: recipient.card!, - if (recipient.iban != null) PaymentType.iban: recipient.iban!, - if (recipient.wallet != null) PaymentType.wallet: recipient.wallet!, - if (recipient.bank != null) PaymentType.bankAccount: recipient.bank!, - }; - - // Set selected type if it's available, otherwise use first available type - if (_availableTypes.containsKey(_selectedType)) { - // Keep current selection if valid - } else if (_availableTypes.isNotEmpty) { - _selectedType = _availableTypes.keys.first; - } else { - // Fallback if recipient has no payment methods - _selectedType = PaymentType.bankAccount; - } - } else { - // No recipient - we're creating a new payment from scratch - _availableTypes = {}; - _selectedType = widget.type ?? PaymentType.bankAccount; - _isFormVisible = true; // Always show form when creating new payment - } - - // Load payment methods if not already loaded - if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) { - WidgetsBinding.instance.addPostFrameCallback((_) { - methodsProvider.loadMethods(); - }); - } - } + late final TextEditingController _searchController; + late final FocusNode _searchFocusNode; + late final PaymentFlowProvider _flowProvider; @override void initState() { super.initState(); - // Initial values - _availableTypes = {}; - _selectedType = widget.type ?? PaymentType.bankAccount; + _searchController = TextEditingController(); + _searchFocusNode = FocusNode(); + final pageSelector = context.read(); + _flowProvider = PaymentFlowProvider( + initialType: pageSelector.getDefaultPaymentType(), + ); + + WidgetsBinding.instance.addPostFrameCallback((_) => _initializePaymentPage()); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + _flowProvider.dispose(); + super.dispose(); + } + + void _initializePaymentPage() { + final pageSelector = context.read(); + final methodsProvider = context.read(); + final recipientProvider = context.read(); + + pageSelector.handleWalletAutoSelection(); + + if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) { + methodsProvider.loadMethods(); + } + + if (recipientProvider.recipients.isEmpty && !recipientProvider.isLoading) { + recipientProvider.loadRecipients(); + } + + _flowProvider.syncWithSelector(pageSelector); + } + + void _handleSearchChanged(String query) { + context.read().setQuery(query); + } + + void _handleRecipientSelected(Recipient recipient) { + final pageSelector = context.read(); + final recipientProvider = context.read(); + + recipientProvider.selectRecipient(recipient); + pageSelector.selectRecipient(recipient); + _flowProvider.reset(pageSelector); + _clearSearchField(); + } + + void _handleRecipientCleared() { + final pageSelector = context.read(); + final recipientProvider = context.read(); + + recipientProvider.selectRecipient(null); + pageSelector.selectRecipient(null); + _flowProvider.reset(pageSelector); + _clearSearchField(); + } + + void _clearSearchField() { + _searchController.clear(); + _searchFocusNode.unfocus(); + context.read().setQuery(''); + } + + void _handleSendPayment() { + // TODO: Handle Payment logic + // AmplitudeService.paymentInitiated(); } @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final dimensions = AppDimensions(); - final recipientProvider = context.watch(); - final methodsProvider = context.watch(); - final recipient = recipientProvider.selectedRecipient; + final pageSelector = context.watch(); + _flowProvider.syncWithSelector(pageSelector); - // Show loading state for payment methods - if (methodsProvider.isLoading) { - return const Center(child: CircularProgressIndicator()); - } - - // Show error state for payment methods - if (methodsProvider.error != null) { - return Center( - child: Text('Error: ${methodsProvider.error}'), - ); - } - - return Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth), - child: Material( - elevation: dimensions.elevationSmall, - borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium), - color: theme.colorScheme.onSecondary, - child: Padding( - padding: EdgeInsets.all(dimensions.paddingLarge), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Back button - Align( - alignment: Alignment.topLeft, - child: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - widget.onBack?.call(recipient); - }, - ), - ), - SizedBox(height: dimensions.paddingSmall), - - // Header - Row( - children: [ - Icon( - Icons.send_rounded, - color: theme.colorScheme.primary, - size: dimensions.iconSizeLarge - ), - SizedBox(width: dimensions.spacingSmall), - Text( - AppLocalizations.of(context)!.sendTo, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold - ), - ), - ], - ), - SizedBox(height: dimensions.paddingXXLarge), - - // Payment method dropdown (user's payment methods) - PaymentMethodDropdown( - methods: methodsProvider.methods, - initialValue: methodsProvider.selectedMethod, - onChanged: (method) { - methodsProvider.selectMethod(method); - }, - ), - SizedBox(height: dimensions.paddingXLarge), - - // Recipient section (only show if we have a recipient) - if (recipient != null) ...[ - RecipientHeader(recipient: recipient), - SizedBox(height: dimensions.paddingMedium), - - // Payment type selector (recipient's payment methods) - if (_availableTypes.isNotEmpty) - PaymentTypeSelector( - availableTypes: _availableTypes, - selectedType: _selectedType, - onSelected: (type) => setState(() => _selectedType = type), - ), - SizedBox(height: dimensions.paddingMedium), - ], - - // Payment details section - PaymentDetailsSection( - isFormVisible: recipient == null || _isFormVisible, - onToggle: recipient != null - ? () => setState(() => _isFormVisible = !_isFormVisible) - : null, // No toggle when creating new payment - selectedType: _selectedType, - data: _availableTypes[_selectedType], - isEditable: recipient == null, - ), - - const PaymentFormWidget(), - - SizedBox(height: dimensions.paddingXXXLarge), - - Center( - child: SizedBox( - width: dimensions.buttonWidth, - height: dimensions.buttonHeight, - child: InkWell( - borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), - onTap: () => - // TODO: Handle Payment logic - AmplitudeService.pageOpened(PayoutDestination.payment), //TODO: replace with payment event - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), - ), - child: Center( - child: Text( - AppLocalizations.of(context)!.send, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSecondary, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - ), - ), - SizedBox(height: dimensions.paddingLarge), - ], - ), - ), - ), - ), + return ChangeNotifierProvider.value( + value: _flowProvider, + child: PaymentPageBody( + onBack: widget.onBack, + searchController: _searchController, + searchFocusNode: _searchFocusNode, + onSearchChanged: _handleSearchChanged, + onRecipientSelected: _handleRecipientSelected, + onRecipientCleared: _handleRecipientCleared, + onSend: _handleSendPayment, ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payment_methods/payment_details.dart b/frontend/pweb/lib/pages/payment_methods/payment_details.dart new file mode 100644 index 0000000..ab6f355 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/payment_details.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + + +class PaymentDetailsSection extends StatelessWidget { + final bool isFormVisible; + final VoidCallback? onToggle; + final PaymentType selectedType; + final Object? data; + final bool isEditable; + + const PaymentDetailsSection({ + super.key, + required this.isFormVisible, + this.onToggle, + required this.selectedType, + required this.data, + required this.isEditable, + }); + + @override + Widget build(BuildContext context) { + + return PaymentDetailsSection( + isFormVisible: isFormVisible, + onToggle: onToggle, + selectedType: selectedType, + data: data, + isEditable: isEditable, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/send_button.dart b/frontend/pweb/lib/pages/payment_methods/send_button.dart new file mode 100644 index 0000000..019cb29 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/send_button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/utils/dimensions.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SendButton extends StatelessWidget { + final VoidCallback onPressed; + + const SendButton({super.key, required this.onPressed}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final dimensions = AppDimensions(); + + return Center( + child: SizedBox( + width: dimensions.buttonWidth, + height: dimensions.buttonHeight, + child: InkWell( + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + onTap: onPressed, + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + ), + child: Center( + child: Text( + AppLocalizations.of(context)!.send, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSecondary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart b/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart new file mode 100644 index 0000000..534a727 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/payment_methods/form.dart'; +import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; +import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/providers/payment_flow_provider.dart'; +import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/utils/payment/selector_type.dart'; + + +class PaymentInfoSection extends StatelessWidget { + final AppDimensions dimensions; + final PageSelectorProvider pageSelector; + final PaymentFlowProvider flowProvider; + final Recipient? recipient; + + const PaymentInfoSection({ + super.key, + required this.dimensions, + required this.pageSelector, + required this.flowProvider, + required this.recipient, + }); + + @override + Widget build(BuildContext context) { + final hasRecipient = recipient != null; + final availableTypes = hasRecipient + ? pageSelector.getAvailablePaymentTypes() + : {for (final type in PaymentType.values) type: type}; + + if (hasRecipient && availableTypes.isEmpty) { + return const Text('This recipient has no available payment details.'); + } + + final selectedType = flowProvider.selectedType; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionTitle('Payment info'), + SizedBox(height: dimensions.paddingSmall), + PaymentTypeSelector( + availableTypes: availableTypes, + selectedType: selectedType, + onSelected: (type) => flowProvider.selectType( + type, + resetManualData: !hasRecipient, + ), + ), + SizedBox(height: dimensions.paddingMedium), + PaymentMethodForm( + selectedType: selectedType, + onChanged: (data) { + if (!hasRecipient) { + flowProvider.setManualPaymentData(data); + } + }, + initialData: hasRecipient ? availableTypes[selectedType] : flowProvider.manualPaymentData, + isEditable: !hasRecipient, + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart new file mode 100644 index 0000000..68fe249 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/payment_methods/header.dart'; +import 'package:pweb/pages/payment_methods/method_selector.dart'; +import 'package:pweb/pages/payment_methods/send_button.dart'; +import 'package:pweb/pages/dashboard/payouts/payment_form.dart'; +import 'package:pweb/pages/payment_methods/widgets/payment_info_section.dart'; +import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart'; +import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; +import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/providers/payment_flow_provider.dart'; +import 'package:pweb/providers/payment_methods.dart'; +import 'package:pweb/providers/recipient.dart'; +import 'package:pweb/utils/dimensions.dart'; + + +class PaymentPageBody extends StatelessWidget { + final ValueChanged? onBack; + final TextEditingController searchController; + final FocusNode searchFocusNode; + final ValueChanged onSearchChanged; + final ValueChanged onRecipientSelected; + final VoidCallback onRecipientCleared; + final VoidCallback onSend; + + const PaymentPageBody({ + super.key, + required this.onBack, + required this.searchController, + required this.searchFocusNode, + required this.onSearchChanged, + required this.onRecipientSelected, + required this.onRecipientCleared, + required this.onSend, + }); + + @override + Widget build(BuildContext context) { + final dimensions = AppDimensions(); + final pageSelector = context.watch(); + final methodsProvider = context.watch(); + final recipientProvider = context.watch(); + final flowProvider = context.watch(); + final recipient = pageSelector.selectedRecipient; + + if (methodsProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (methodsProvider.error != null) { + return Center(child: Text('Error: ${methodsProvider.error}')); + } + + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth), + child: Material( + elevation: dimensions.elevationSmall, + borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium), + color: Theme.of(context).colorScheme.onSecondary, + child: Padding( + padding: EdgeInsets.all(dimensions.paddingLarge), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PaymentBackButton(onBack: onBack, pageSelector: pageSelector), + SizedBox(height: dimensions.paddingSmall), + PaymentHeader(), + SizedBox(height: dimensions.paddingXXLarge), + + const SectionTitle('Source of funds'), + SizedBox(height: dimensions.paddingSmall), + PaymentMethodSelector( + methodsProvider: methodsProvider, + onMethodChanged: methodsProvider.selectMethod, + ), + SizedBox(height: dimensions.paddingXLarge), + + RecipientSection( + recipient: recipient, + dimensions: dimensions, + recipientProvider: recipientProvider, + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearchChanged: onSearchChanged, + onRecipientSelected: onRecipientSelected, + onRecipientCleared: onRecipientCleared, + ), + + SizedBox(height: dimensions.paddingXLarge), + + PaymentInfoSection( + dimensions: dimensions, + pageSelector: pageSelector, + flowProvider: flowProvider, + recipient: recipient, + ), + + SizedBox(height: dimensions.paddingLarge), + const PaymentFormWidget(), + + SizedBox(height: dimensions.paddingXXXLarge), + SendButton(onPressed: onSend), + SizedBox(height: dimensions.paddingLarge), + ], + ), + ), + ), + ), + ), + ); + } +} + +class PaymentBackButton extends StatelessWidget { + final ValueChanged? onBack; + final PageSelectorProvider pageSelector; + + const PaymentBackButton({ + super.key, + required this.onBack, + required this.pageSelector, + }); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topLeft, + child: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + onBack?.call(pageSelector.selectedRecipient); + pageSelector.goBackFromPayment(); + }, + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart new file mode 100644 index 0000000..69c1c84 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/address_book/page/search.dart'; +import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; +import 'package:pweb/providers/recipient.dart'; +import 'package:pweb/utils/dimensions.dart'; + + +class RecipientSection extends StatelessWidget { + final Recipient? recipient; + final AppDimensions dimensions; + final RecipientProvider recipientProvider; + final TextEditingController searchController; + final FocusNode searchFocusNode; + final ValueChanged onSearchChanged; + final ValueChanged onRecipientSelected; + final VoidCallback onRecipientCleared; + + const RecipientSection({ + super.key, + required this.recipient, + required this.dimensions, + required this.recipientProvider, + required this.searchController, + required this.searchFocusNode, + required this.onSearchChanged, + required this.onRecipientSelected, + required this.onRecipientCleared, + }); + + @override + Widget build(BuildContext context) { + if (recipient != null) { + return SelectedRecipientCard( + dimensions: dimensions, + recipient: recipient!, + onClear: onRecipientCleared, + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionTitle('Recipient'), + SizedBox(height: dimensions.paddingSmall), + RecipientSearchField( + controller: searchController, + onChanged: onSearchChanged, + focusNode: searchFocusNode, + ), + if (recipientProvider.query.isNotEmpty) ...[ + SizedBox(height: dimensions.paddingMedium), + RecipientSearchResults( + dimensions: dimensions, + recipientProvider: recipientProvider, + onRecipientSelected: onRecipientSelected, + ), + ], + ], + ); + } +} + +class SelectedRecipientCard extends StatelessWidget { + final AppDimensions dimensions; + final Recipient recipient; + final VoidCallback onClear; + + const SelectedRecipientCard({ + super.key, + required this.dimensions, + required this.recipient, + required this.onClear, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: EdgeInsets.all(dimensions.paddingMedium), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.4), + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionTitle('Recipient'), + SizedBox(height: dimensions.paddingSmall), + Row( + children: [ + CircleAvatar( + child: Text(recipient.name.substring(0, 1).toUpperCase()), + ), + SizedBox(width: dimensions.paddingMedium), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(recipient.name, style: theme.textTheme.titleMedium), + if (recipient.email.isNotEmpty) + Text( + recipient.email, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ), + ), + TextButton( + onPressed: onClear, + child: const Text('Choose another recipient'), + ), + ], + ), + ], + ), + ); + } +} + +class RecipientSearchResults extends StatelessWidget { + final AppDimensions dimensions; + final RecipientProvider recipientProvider; + final ValueChanged onRecipientSelected; + + const RecipientSearchResults({ + super.key, + required this.dimensions, + required this.recipientProvider, + required this.onRecipientSelected, + }); + + @override + Widget build(BuildContext context) { + if (recipientProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (recipientProvider.error != null) { + return Text( + recipientProvider.error!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ); + } + + if (recipientProvider.recipients.isEmpty) { + return const Text('No recipients yet.'); + } + + final results = recipientProvider.filteredRecipients; + + if (results.isEmpty) { + return const Text('No recipients found for this query.'); + } + + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 240), + child: ListView.separated( + shrinkWrap: true, + itemCount: results.length, + separatorBuilder: (_, __) => SizedBox(height: dimensions.paddingSmall), + itemBuilder: (context, index) { + final recipient = results[index]; + return ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + ), + tileColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.2), + leading: CircleAvatar( + child: Text(recipient.name.substring(0, 1).toUpperCase()), + ), + title: Text(recipient.name), + subtitle: Text(recipient.email), + trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16), + onTap: () => onRecipientSelected(recipient), + ); + }, + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/section_title.dart b/frontend/pweb/lib/pages/payment_methods/widgets/section_title.dart new file mode 100644 index 0000000..565a6d4 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/widgets/section_title.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + + +class SectionTitle extends StatelessWidget { + final String title; + + const SectionTitle(this.title, {super.key}); + + @override + Widget build(BuildContext context) { + return Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/send.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/send.dart deleted file mode 100644 index daae32f..0000000 --- a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/send.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:provider/provider.dart'; - -import 'package:pweb/models/wallet.dart'; -import 'package:pweb/providers/wallets.dart'; - - -class SendPayoutButton extends StatelessWidget { - - const SendPayoutButton({ - super.key, - }); - - - @override - Widget build(BuildContext context) { - return ElevatedButton( - style: ElevatedButton.styleFrom( - shadowColor: null, - elevation: 0, - ), - onPressed: () => ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Add functionality')), - ), - child: Text('Send Payout'), - ); - } -} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/fields.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/fields.dart deleted file mode 100644 index ebbf0cc..0000000 --- a/frontend/pweb/lib/pages/payment_page/wallet/edit/fields.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:provider/provider.dart'; - -import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; -import 'package:pweb/providers/wallets.dart'; - - -class WalletEditFields extends StatelessWidget { - - const WalletEditFields({super.key}); - - @override - Widget build(BuildContext context) { - final wallet = context.watch().wallets?.first; - - if (wallet == null) { - return const SizedBox.shrink(); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - BalanceAmount( - wallet: wallet, - onToggleVisibility: () { - context.read().toggleVisibility(wallet.id); - }, - ), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(wallet.walletUserID, style: Theme.of(context).textTheme.bodyLarge), - IconButton( - icon: Icon(Icons.copy), - iconSize: 18, - onPressed: () => Clipboard.setData(ClipboardData(text: wallet.walletUserID)), - ), - ], - ), - ], - ); - } -} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/header.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/header.dart deleted file mode 100644 index 113aa18..0000000 --- a/frontend/pweb/lib/pages/payment_page/wallet/edit/header.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:provider/provider.dart'; - -import 'package:pweb/utils/currency.dart'; -import 'package:pweb/utils/dimensions.dart'; -import 'package:pweb/providers/wallets.dart'; - - -// class WalletEditHeader extends StatefulWidget { -// const WalletEditHeader({super.key}); - -// @override -// State createState() => _WalletEditHeaderState(); -// } - -// class _WalletEditHeaderState extends State { -// bool _isEditing = false; -// late TextEditingController _controller; - -// @override -// void initState() { -// super.initState(); -// _controller = TextEditingController(); -// } - -// @override -// void dispose() { -// _controller.dispose(); -// super.dispose(); -// } - -// @override -// Widget build(BuildContext context) { -// final provider = context.watch(); -// final currentWallet = provider.getWalletById(provider.wallets!.id); - - -// if (wallet == null) { -// return const SizedBox.shrink(); -// } - -// final theme = Theme.of(context); -// final dimensions = AppDimensions(); - -// if (!_isEditing) { -// _controller.text = wallet.name; -// } - -// return Row( -// spacing: 8, -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: [ -// Icon( -// iconForCurrencyType(wallet.currency), -// color: theme.colorScheme.primary, -// size: dimensions.iconSizeLarge, -// ), - -// Expanded( -// child: !_isEditing -// ? Row( -// children: [ -// Expanded( -// child: Text( -// wallet.name, -// style: theme.textTheme.headlineMedium!.copyWith( -// fontWeight: FontWeight.bold,), -// ), -// ), -// IconButton( -// icon: const Icon(Icons.edit), -// onPressed: () { -// setState(() { -// _isEditing = true; -// }); -// }, -// ), -// ], -// ) -// : Row( -// children: [ -// Expanded( -// child: TextFormField( -// controller: _controller, -// decoration: const InputDecoration( -// border: OutlineInputBorder(), -// isDense: true, -// hintText: 'Wallet name', -// ), -// ), -// ), -// IconButton( -// icon: const Icon(Icons.check), -// color: theme.colorScheme.primary, -// onPressed: () async { -// provider.updateName(wallet.id, _controller.text); -// await provider.updateWallet(wallet.copyWith(name: _controller.text)); -// ScaffoldMessenger.of(context).showSnackBar( -// const SnackBar(content: Text('Wallet name saved')), -// ); -// setState(() { -// _isEditing = false; -// }); -// }, -// ), -// IconButton( -// icon: const Icon(Icons.close), -// onPressed: () { -// _controller.text = wallet.name; -// setState(() { -// _isEditing = false; -// }); -// }, -// ), -// ], -// ), -// ), -// ], -// ); -// } -// } \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/page.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/page.dart deleted file mode 100644 index e50bc3d..0000000 --- a/frontend/pweb/lib/pages/payment_page/wallet/edit/page.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pweb/models/wallet.dart'; -import 'package:pweb/pages/payment_page/wallet/edit/buttons/buttons.dart'; -import 'package:pweb/pages/payment_page/wallet/edit/fields.dart'; -import 'package:pweb/utils/dimensions.dart'; - - -class WalletEditPage extends StatelessWidget { - final Wallet wallet; - final VoidCallback onBack; - - const WalletEditPage({super.key, required this.wallet, required this.onBack}); - - @override - Widget build(BuildContext context) { - final dimensions = AppDimensions(); - - return Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth), - child: Material( - elevation: dimensions.elevationSmall, - color: Theme.of(context).colorScheme.onSecondary, - borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium), - child: Padding( - padding: EdgeInsets.all(dimensions.paddingLarge), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: onBack, - ), - - // WalletEditHeader(), - - WalletEditFields(), - - const SizedBox(height: 24), - - ButtonsWalletWidget(), - - const SizedBox(height: 24), - ], - ), - ), - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/methods/advanced.dart b/frontend/pweb/lib/pages/payout_page/methods/advanced.dart similarity index 100% rename from frontend/pweb/lib/pages/payment_page/methods/advanced.dart rename to frontend/pweb/lib/pages/payout_page/methods/advanced.dart diff --git a/frontend/pweb/lib/pages/payment_page/methods/controller.dart b/frontend/pweb/lib/pages/payout_page/methods/controller.dart similarity index 100% rename from frontend/pweb/lib/pages/payment_page/methods/controller.dart rename to frontend/pweb/lib/pages/payout_page/methods/controller.dart diff --git a/frontend/pweb/lib/pages/payment_page/methods/header.dart b/frontend/pweb/lib/pages/payout_page/methods/header.dart similarity index 100% rename from frontend/pweb/lib/pages/payment_page/methods/header.dart rename to frontend/pweb/lib/pages/payout_page/methods/header.dart diff --git a/frontend/pweb/lib/pages/payment_page/methods/list.dart b/frontend/pweb/lib/pages/payout_page/methods/list.dart similarity index 94% rename from frontend/pweb/lib/pages/payment_page/methods/list.dart rename to frontend/pweb/lib/pages/payout_page/methods/list.dart index 3c98471..911e255 100644 --- a/frontend/pweb/lib/pages/payment_page/methods/list.dart +++ b/frontend/pweb/lib/pages/payout_page/methods/list.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pweb/pages/payment_methods/title.dart'; -import 'package:pweb/pages/payment_page/methods/controller.dart'; +import 'package:pweb/pages/payout_page/methods/controller.dart'; import 'package:pweb/providers/payment_methods.dart'; diff --git a/frontend/pweb/lib/pages/payment_page/methods/widget.dart b/frontend/pweb/lib/pages/payout_page/methods/widget.dart similarity index 82% rename from frontend/pweb/lib/pages/payment_page/methods/widget.dart rename to frontend/pweb/lib/pages/payout_page/methods/widget.dart index 5ae0623..f54b81f 100644 --- a/frontend/pweb/lib/pages/payment_page/methods/widget.dart +++ b/frontend/pweb/lib/pages/payout_page/methods/widget.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:pweb/pages/payment_page/methods/advanced.dart'; -import 'package:pweb/pages/payment_page/methods/controller.dart'; -import 'package:pweb/pages/payment_page/methods/header.dart'; -import 'package:pweb/pages/payment_page/methods/list.dart'; +import 'package:pweb/pages/payout_page/methods/advanced.dart'; +import 'package:pweb/pages/payout_page/methods/controller.dart'; +import 'package:pweb/pages/payout_page/methods/header.dart'; +import 'package:pweb/pages/payout_page/methods/list.dart'; class MethodsWidget extends StatefulWidget { diff --git a/frontend/pweb/lib/pages/payment_page/page.dart b/frontend/pweb/lib/pages/payout_page/page.dart similarity index 87% rename from frontend/pweb/lib/pages/payment_page/page.dart rename to frontend/pweb/lib/pages/payout_page/page.dart index 2898ff7..818f1c6 100644 --- a/frontend/pweb/lib/pages/payment_page/page.dart +++ b/frontend/pweb/lib/pages/payout_page/page.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pweb/models/wallet.dart'; -import 'package:pweb/pages/payment_page/methods/widget.dart'; -import 'package:pweb/pages/payment_page/wallet/wigets.dart'; +import 'package:pweb/pages/payout_page/methods/widget.dart'; +import 'package:pweb/pages/payout_page/wallet/wigets.dart'; import 'package:pweb/providers/payment_methods.dart'; diff --git a/frontend/pweb/lib/pages/payment_page/wallet/card.dart b/frontend/pweb/lib/pages/payout_page/wallet/card.dart similarity index 100% rename from frontend/pweb/lib/pages/payment_page/wallet/card.dart rename to frontend/pweb/lib/pages/payout_page/wallet/card.dart diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/buttons.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart similarity index 55% rename from frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/buttons.dart rename to frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart index a9aad41..ab79cc2 100644 --- a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/buttons.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; + import 'package:provider/provider.dart'; -import 'package:pweb/pages/payment_page/wallet/edit/buttons/send.dart'; -import 'package:pweb/pages/payment_page/wallet/edit/buttons/top_up.dart'; + +import 'package:pweb/pages/payout_page/wallet/edit/buttons/send.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/buttons/top_up.dart'; import 'package:pweb/providers/wallets.dart'; -import 'package:pweb/utils/dimensions.dart'; class ButtonsWalletWidget extends StatelessWidget { @@ -16,21 +17,7 @@ class ButtonsWalletWidget extends StatelessWidget { if (wallet == null) return const SizedBox.shrink(); - final dimensions = AppDimensions(); - - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceBright, - borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.primary.withAlpha(50), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( + return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( @@ -45,7 +32,6 @@ class ButtonsWalletWidget extends StatelessWidget { child: TopUpButton(), ), ], - ), ); } } \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/save.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart similarity index 100% rename from frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/save.dart rename to frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart new file mode 100644 index 0000000..4cfaccb --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/providers/wallets.dart'; + + +class SendPayoutButton extends StatelessWidget { + const SendPayoutButton({super.key}); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + shadowColor: null, + elevation: 0, + ), + onPressed: () { + final pageSelectorProvider = context.read(); + final walletsProvider = context.read(); + final wallet = walletsProvider.selectedWallet; + + if (wallet != null) { + pageSelectorProvider.startPaymentFromWallet(wallet); + } + }, + child: Text('Send Payout'), + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/top_up.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart similarity index 100% rename from frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/top_up.dart rename to frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart new file mode 100644 index 0000000..ca28e90 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; +import 'package:pweb/providers/wallets.dart'; + + +class WalletEditFields extends StatelessWidget { + const WalletEditFields({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + final wallet = provider.selectedWallet; + + if (wallet == null) { + return SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + BalanceAmount( + wallet: wallet, + onToggleVisibility: () { + context.read().toggleVisibility(wallet.id); + }, + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(wallet.walletUserID, style: Theme.of(context).textTheme.bodyLarge), + IconButton( + icon: Icon(Icons.copy), + iconSize: 18, + onPressed: () => Clipboard.setData(ClipboardData(text: wallet.walletUserID)), + ), + ], + ), + ], + ); + }, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart new file mode 100644 index 0000000..d91e449 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/providers/wallets.dart'; + + +class WalletEditHeader extends StatefulWidget { + const WalletEditHeader({super.key}); + + @override + State createState() => _WalletEditHeaderState(); +} + +class _WalletEditHeaderState extends State { + bool _isEditing = false; + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + final wallet = provider.selectedWallet; + + if (wallet == null) { + return SizedBox.shrink(); + } + + final theme = Theme.of(context); + + if (!_isEditing) { + _controller.text = wallet.name; + } + + return Row( + spacing: 8, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: !_isEditing + ? Row( + children: [ + Expanded( + child: Text( + wallet.name, + style: theme.textTheme.headlineMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + setState(() { + _isEditing = true; + }); + }, + ), + ], + ) + : Row( + children: [ + Expanded( + child: TextFormField( + controller: _controller, + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: true, + hintText: 'Wallet name', + ), + ), + ), + IconButton( + icon: const Icon(Icons.check), + color: theme.colorScheme.primary, + onPressed: () async { + provider.updateName(wallet.id, _controller.text); + await provider.updateWallet(wallet.copyWith(name: _controller.text)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Wallet name saved')), + ); + setState(() { + _isEditing = false; + }); + }, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _controller.text = wallet.name; + setState(() { + _isEditing = false; + }); + }, + ), + ], + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart new file mode 100644 index 0000000..635b259 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/pages/payout_page/wallet/edit/buttons/buttons.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/header.dart'; +import 'package:pweb/pages/payout_page/wallet/history/history.dart'; +import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/utils/dimensions.dart'; + + +class WalletEditPage extends StatelessWidget { + final VoidCallback onBack; + + const WalletEditPage({super.key, required this.onBack}); + + @override + Widget build(BuildContext context) { + final dimensions = AppDimensions(); + + return Consumer( + builder: (context, provider, child) { + final wallet = provider.selectedWallet; + + if (wallet == null) { + return Center(child: Text('Кошелёк не выбран')); + } + + return Align( + alignment: Alignment.topCenter, + child: Column( + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth), + child: Material( + elevation: dimensions.elevationSmall, + color: Theme.of(context).colorScheme.onSecondary, + borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium), + child: Padding( + padding: EdgeInsets.all(dimensions.paddingLarge), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: onBack, + ), + WalletEditHeader(), + WalletEditFields(), + const SizedBox(height: 24), + ButtonsWalletWidget(), + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 24), + Expanded( + child: SingleChildScrollView( + child: WalletHistory(wallet: wallet), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/chip.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/chip.dart new file mode 100644 index 0000000..98be3df --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/chip.dart @@ -0,0 +1,48 @@ + +import 'package:flutter/material.dart'; + +import 'package:pweb/models/wallet_transaction.dart'; + + +class TypeChip extends StatelessWidget { + final WalletTransactionType type; + + const TypeChip({super.key, required this.type}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isTopUp = type == WalletTransactionType.topUp; + final bg = isTopUp + ? theme.colorScheme.secondaryContainer + : theme.colorScheme.errorContainer; + + final fg = bg.computeLuminance() > 0.5 ? Colors.black : Colors.white; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isTopUp ? Icons.arrow_downward_rounded : Icons.arrow_upward_rounded, + size: 16, + color: fg, + ), + const SizedBox(width: 6), + Text( + type.label(context), + style: TextStyle( + color: fg, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart new file mode 100644 index 0000000..65639e4 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart @@ -0,0 +1,94 @@ + +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/status.dart'; +import 'package:pshared/utils/localization.dart'; + +import 'package:pweb/models/wallet_transaction.dart'; +import 'package:pweb/providers/wallet_transactions.dart'; + + +class WalletHistoryFilters extends StatelessWidget { + final WalletTransactionsProvider provider; + final VoidCallback onPickRange; + + const WalletHistoryFilters({ + super.key, + required this.provider, + required this.onPickRange, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + elevation: 2, + color: theme.colorScheme.onSecondary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Wallet activity', + style: theme.textTheme.titleMedium, + ), + if (provider.hasFilters) + TextButton( + onPressed: provider.resetFilters, + child: const Text('Reset'), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: WalletTransactionType.values.map((type) { + final isSelected = provider.selectedTypes.contains(type); + return FilterChip( + label: Text(type.label(context)), + selected: isSelected, + onSelected: (_) => provider.toggleType(type), + pressElevation: 0, + ); + }).toList(), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: OperationStatus.values.map((status) { + final isSelected = provider.selectedStatuses.contains(status); + return FilterChip( + label: Text(status.localized(context)), + selected: isSelected, + onSelected: (_) => provider.toggleStatus(status), + pressElevation: 0, + ); + }).toList(), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: OutlinedButton.icon( + onPressed: onPickRange, + icon: const Icon(Icons.date_range_outlined), + label: Text( + provider.dateRange == null + ? 'Select period' + : '${dateToLocalFormat(context, provider.dateRange!.start)} – ${dateToLocalFormat(context, provider.dateRange!.end)}', + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart new file mode 100644 index 0000000..1e64438 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/pages/payout_page/wallet/history/filters.dart'; +import 'package:pweb/pages/payout_page/wallet/history/table.dart'; +import 'package:pweb/providers/wallet_transactions.dart'; + + +class WalletHistory extends StatefulWidget { + final Wallet wallet; + + const WalletHistory({super.key, required this.wallet}); + + @override + State createState() => _WalletHistoryState(); +} + +class _WalletHistoryState extends State { + @override + void initState() { + super.initState(); + _load(); + } + + @override + void didUpdateWidget(covariant WalletHistory oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.wallet.id != widget.wallet.id) { + _load(); + } + } + + void _load() { + WidgetsBinding.instance.addPostFrameCallback((_) { + context + .read() + .load(walletId: widget.wallet.id); + }); + } + + Future _pickRange() async { + final provider = context.read(); + final now = DateTime.now(); + final initial = provider.dateRange ?? + DateTimeRange( + start: now.subtract(const Duration(days: 30)), + end: now, + ); + + final picked = await showDateRangePicker( + context: context, + firstDate: now.subtract(const Duration(days: 365)), + lastDate: now.add(const Duration(days: 1)), + initialDateRange: initial, + ); + + if (picked != null) { + provider.setDateRange(picked); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Consumer( + builder: (context, provider, child) { + if (provider.isLoading) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: CircularProgressIndicator()), + ); + } + + if (provider.error != null) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Failed to load history', + style: theme.textTheme.titleMedium! + .copyWith(color: theme.colorScheme.error), + ), + const SizedBox(height: 8), + Text(provider.error!), + const SizedBox(height: 8), + OutlinedButton( + onPressed: _load, + child: const Text('Retry'), + ), + ], + ), + ); + } + + final transactions = provider.filteredTransactions; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + WalletHistoryFilters( + provider: provider, + onPickRange: _pickRange, + ), + const SizedBox(height: 12), + WalletTransactionsTable(transactions: transactions), + ], + ); + }, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart new file mode 100644 index 0000000..49cd9c0 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart @@ -0,0 +1,88 @@ + +import 'package:flutter/material.dart'; + +import 'package:pweb/models/wallet_transaction.dart'; +import 'package:pweb/pages/payout_page/wallet/history/chip.dart'; +import 'package:pweb/pages/report/table/badge.dart'; +import 'package:pweb/utils/currency.dart'; + + +class WalletTransactionsTable extends StatelessWidget { + final List transactions; + + const WalletTransactionsTable({super.key, required this.transactions}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (transactions.isEmpty) { + return Card( + color: theme.colorScheme.onSecondary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: const Padding( + padding: EdgeInsets.all(16), + child: Text('No history yet'), + ), + ); + } + + return Card( + color: theme.colorScheme.onSecondary, + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columnSpacing: 18, + headingTextStyle: const TextStyle(fontWeight: FontWeight.w600), + columns: const [ + DataColumn(label: Text('Status')), + DataColumn(label: Text('Type')), + DataColumn(label: Text('Amount')), + DataColumn(label: Text('Balance')), + DataColumn(label: Text('Counterparty')), + DataColumn(label: Text('Date')), + DataColumn(label: Text('Comment')), + ], + rows: List.generate( + transactions.length, + (index) { + final tx = transactions[index]; + final color = WidgetStateProperty.resolveWith( + (states) => index.isEven + ? theme.colorScheme.surfaceContainerHighest + : null, + ); + + return DataRow.byIndex( + index: index, + color: color, + cells: [ + DataCell(OperationStatusBadge(status: tx.status)), + DataCell(TypeChip(type: tx.type)), + DataCell(Text( + '${tx.type.sign}${tx.amount.toStringAsFixed(2)} ${currencyCodeToSymbol(tx.currency)}')), + DataCell(Text( + tx.balanceAfter == null + ? '-' + : '${tx.balanceAfter!.toStringAsFixed(2)} ${currencyCodeToSymbol(tx.currency)}', + )), + DataCell(Text(tx.counterparty ?? '-')), + DataCell(Text( + '${TimeOfDay.fromDateTime(tx.date).format(context)}\n' + '${tx.date.toLocal().toIso8601String().split("T").first}', + )), + DataCell(Text(tx.description)), + ], + ); + }, + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/wigets.dart b/frontend/pweb/lib/pages/payout_page/wallet/wigets.dart similarity index 95% rename from frontend/pweb/lib/pages/payment_page/wallet/wigets.dart rename to frontend/pweb/lib/pages/payout_page/wallet/wigets.dart index 66901e8..d440ac7 100644 --- a/frontend/pweb/lib/pages/payment_page/wallet/wigets.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/wigets.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pweb/models/wallet.dart'; -import 'package:pweb/pages/payment_page/wallet/card.dart'; +import 'package:pweb/pages/payout_page/wallet/card.dart'; import 'package:pweb/providers/wallets.dart'; diff --git a/frontend/pweb/lib/pages/report/page.dart b/frontend/pweb/lib/pages/report/page.dart index 4904e3b..31b1477 100644 --- a/frontend/pweb/lib/pages/report/page.dart +++ b/frontend/pweb/lib/pages/report/page.dart @@ -1,13 +1,12 @@ -// operation_history_page.dart import 'package:flutter/material.dart'; -import 'package:pshared/models/payment/operation.dart'; -import 'package:pshared/models/payment/status.dart'; +import 'package:provider/provider.dart'; import 'package:pweb/pages/report/charts/distribution.dart'; import 'package:pweb/pages/report/charts/status.dart'; import 'package:pweb/pages/report/table/filters.dart'; import 'package:pweb/pages/report/table/widget.dart'; +import 'package:pweb/providers/operatioins.dart'; class OperationHistoryPage extends StatefulWidget { @@ -18,153 +17,95 @@ class OperationHistoryPage extends StatefulWidget { } class _OperationHistoryPageState extends State { - // Mock data - final List _allOps = [ - OperationItem( - status: OperationStatus.error, - fileName: 'cards_payout_sample_june.csv', - amount: 10, - currency: 'EUR', - toAmount: 10, - toCurrency: 'EUR', - payId: '860163800', - cardNumber: null, - name: 'John Snow', - date: DateTime(2025, 7, 14, 19, 59, 2), - comment: 'EUR visa', - ), - OperationItem( - status: OperationStatus.processing, - fileName: 'cards_payout_sample_june.csv', - amount: 10, - currency: 'EUR', - toAmount: 10, - toCurrency: 'EUR', - payId: '860163700', - cardNumber: null, - name: 'Baltasar Gelt', - date: DateTime(2025, 7, 14, 19, 59, 2), - comment: 'EUR master', - ), - OperationItem( - status: OperationStatus.error, - fileName: 'cards_payout_sample_june.csv', - amount: 10, - currency: 'EUR', - toAmount: 10, - toCurrency: 'EUR', - payId: '40000000****0077', - cardNumber: '40000000****0077', - name: 'John Snow', - date: DateTime(2025, 7, 14, 19, 23, 22), - comment: 'EUR visa', - ), - OperationItem( - status: OperationStatus.success, - fileName: null, - amount: 10, - currency: 'EUR', - toAmount: 10, - toCurrency: 'EUR', - payId: '54133300****0019', - cardNumber: '54133300****0019', - name: 'Baltasar Gelt', - date: DateTime(2025, 7, 14, 19, 23, 21), - comment: 'EUR master', - ), - OperationItem( - status: OperationStatus.success, - fileName: null, - amount: 130, - currency: 'EUR', - toAmount: 130, - toCurrency: 'EUR', - payId: '54134300****0019', - cardNumber: '54153300****0019', - name: 'Ivan Brokov', - date: DateTime(2025, 7, 15, 19, 23, 21), - comment: 'EUR master 2', - ), - ]; - DateTimeRange? _range; - final Set _statuses = {}; - late List _filtered; - @override void initState() { super.initState(); - _filtered = List.from(_allOps); - } - - void _applyFilter() { - setState(() { - _filtered = _allOps.where((op) { - final okStatus = _statuses.isEmpty || _statuses.contains(op.status.localized(context)); - final okRange = _range == null || - (op.date.isAfter(_range!.start.subtract(const Duration(seconds: 1))) && - op.date.isBefore(_range!.end.add(const Duration(seconds: 1)))); - return okStatus && okRange; - }).toList(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadOperations(); }); } Future _pickRange() async { + final provider = context.read(); final now = DateTime.now(); - final initial = _range ?? + final initial = provider.dateRange ?? DateTimeRange( start: now.subtract(const Duration(days: 30)), end: now, ); + final picked = await showDateRangePicker( context: context, firstDate: DateTime(2000), lastDate: now.add(const Duration(days: 1)), initialDateRange: initial, ); + if (picked != null) { - setState(() => _range = picked); + provider.setDateRange(picked); } } - void _toggleStatus(String status) { - setState(() { - if (_statuses.contains(status)) _statuses.remove(status); - else _statuses.add(status); - }); - } - @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 16, - children: [ - SizedBox( - height: 200, // same height for both - child: Row( - spacing: 16, + return Consumer( + builder: (context, provider, child) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (provider.error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded(child: StatusChart(operations: _allOps)), - Expanded(child: PayoutDistributionChart(operations: _allOps)), + Text('Error: ${provider.error}'), + ElevatedButton( + onPressed: () => provider.loadOperations(), + child: const Text('Retry'), + ), ], ), + ); + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + SizedBox( + height: 200, + child: Row( + spacing: 16, + children: [ + Expanded( + child: StatusChart(operations: provider.allOperations), + ), + Expanded( + child: PayoutDistributionChart( + operations: provider.allOperations, + ), + ), + ], + ), + ), + OperationFilters( + selectedRange: provider.dateRange, + selectedStatuses: provider.selectedStatuses, + onPickRange: _pickRange, + onToggleStatus: provider.toggleStatus, + onApply: () => provider.applyFilters(context), + ), + OperationsTable( + operations: provider.filteredOperations, + showFileNameColumn: provider.hasFileName, + ), + ], ), - OperationFilters( - selectedRange: _range, - selectedStatuses: _statuses, - onPickRange: _pickRange, - onToggleStatus: _toggleStatus, - onApply: _applyFilter, - ), - OperationsTable( - operations: _filtered, - showFileNameColumn: - _allOps.any((op) => op.fileName != null), - ), - ], - ), + ); + }, ); } -} +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/report/table/widget.dart b/frontend/pweb/lib/pages/report/table/widget.dart index fb64d90..83179bc 100644 --- a/frontend/pweb/lib/pages/report/table/widget.dart +++ b/frontend/pweb/lib/pages/report/table/widget.dart @@ -4,6 +4,7 @@ import 'package:pshared/models/payment/operation.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/pages/report/table/row.dart'; + class OperationsTable extends StatelessWidget { final List operations; final bool showFileNameColumn; @@ -21,7 +22,7 @@ class OperationsTable extends StatelessWidget { return Expanded( child: SingleChildScrollView( child: DataTable( - columnSpacing: 24, + columnSpacing: 20, headingTextStyle: const TextStyle( fontWeight: FontWeight.bold, ), diff --git a/frontend/pweb/lib/providers/balance.dart b/frontend/pweb/lib/providers/balance.dart deleted file mode 100644 index 95c9c76..0000000 --- a/frontend/pweb/lib/providers/balance.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pweb/services/balance.dart'; - - -class BalanceProvider with ChangeNotifier { - final BalanceService _service; - - BalanceProvider(this._service); - - double? _balance; - String? _walletName; - String? _walletId; - bool _isHidden = true; - - double? get balance => _balance; - String? get walletName => _walletName; - String? get walletId => _walletId; - bool get isHidden => _isHidden; - - Future loadData() async { - _balance = await _service.getBalance(); - _walletName = await _service.getWalletName(); - _walletId = await _service.getWalletId(); - notifyListeners(); - } - - void toggleVisibility() { - _isHidden = !_isHidden; - notifyListeners(); - } -} diff --git a/frontend/pweb/lib/providers/operatioins.dart b/frontend/pweb/lib/providers/operatioins.dart new file mode 100644 index 0000000..08b73f4 --- /dev/null +++ b/frontend/pweb/lib/providers/operatioins.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/operation.dart'; +import 'package:pshared/models/payment/status.dart'; + +import 'package:pweb/services/operations.dart'; + + +class OperationProvider extends ChangeNotifier { + final OperationService _service; + + OperationProvider(this._service); + + List _allOperations = []; + List _filteredOperations = []; + DateTimeRange? _dateRange; + final Set _selectedStatuses = {}; + bool _isLoading = false; + String? _error; + + // Getters + List get allOperations => _allOperations; + List get filteredOperations => _filteredOperations; + DateTimeRange? get dateRange => _dateRange; + Set get selectedStatuses => _selectedStatuses; + bool get isLoading => _isLoading; + String? get error => _error; + bool get hasFileName => _allOperations.any((op) => op.fileName != null); + + Future loadOperations() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _allOperations = await _service.fetchOperations(); + _filteredOperations = List.from(_allOperations); + _isLoading = false; + notifyListeners(); + } catch (e) { + _error = e.toString(); + _isLoading = false; + notifyListeners(); + } + } + + void setDateRange(DateTimeRange? range) { + _dateRange = range; + notifyListeners(); + } + + void toggleStatus(String status) { + if (_selectedStatuses.contains(status)) { + _selectedStatuses.remove(status); + } else { + _selectedStatuses.add(status); + } + notifyListeners(); + } + + void applyFilters(BuildContext context) { + _filteredOperations = _allOperations.where((op) { + final statusMatch = _selectedStatuses.isEmpty || + _selectedStatuses.contains(op.status.localized(context)); + + final dateMatch = _dateRange == null || + (op.date.isAfter(_dateRange!.start.subtract(const Duration(seconds: 1))) && + op.date.isBefore(_dateRange!.end.add(const Duration(seconds: 1)))); + + return statusMatch && dateMatch; + }).toList(); + + notifyListeners(); + } + + void resetFilters() { + _dateRange = null; + _selectedStatuses.clear(); + _filteredOperations = List.from(_allOperations); + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/providers/page_selector.dart b/frontend/pweb/lib/providers/page_selector.dart index b1b1874..aff7623 100644 --- a/frontend/pweb/lib/providers/page_selector.dart +++ b/frontend/pweb/lib/providers/page_selector.dart @@ -1,9 +1,13 @@ +import 'package:collection/collection.dart'; + import 'package:flutter/material.dart'; +import 'package:pshared/models/payment/methods/type.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pweb/models/wallet.dart'; +import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/providers/wallets.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/services/amplitude.dart'; @@ -15,13 +19,29 @@ class PageSelectorProvider extends ChangeNotifier { PaymentType? _type; bool _cameFromRecipientList = false; - final RecipientProvider? recipientProvider; - final WalletsProvider? walletsProvider; + RecipientProvider? recipientProvider; + WalletsProvider? walletsProvider; + PaymentMethodsProvider? methodsProvider; PayoutDestination get selected => _selected; PaymentType? get type => _type; + bool get cameFromRecipientList => _cameFromRecipientList; - PageSelectorProvider({this.recipientProvider, this.walletsProvider}); + PageSelectorProvider({ + this.recipientProvider, + this.walletsProvider, + this.methodsProvider, + }); + + void update( + RecipientProvider recipientProv, + WalletsProvider walletsProv, + PaymentMethodsProvider methodsProv, + ) { + recipientProvider = recipientProv; + walletsProvider = walletsProv; + methodsProvider = methodsProv; + } void selectPage(PayoutDestination dest) { _selected = dest; @@ -90,7 +110,64 @@ class PageSelectorProvider extends ChangeNotifier { _selected = PayoutDestination.editwallet; notifyListeners(); } else { - debugPrint("RecipientProvider is null — cannot select wallet"); + debugPrint("WalletsProvider is null — cannot select wallet"); + } + } + + void startPaymentFromWallet(Wallet wallet) { + _type = PaymentType.wallet; + _cameFromRecipientList = true; + _selected = PayoutDestination.payment; + notifyListeners(); + } + + PaymentMethod? getPaymentMethodForWallet(Wallet wallet) { + if (methodsProvider == null || methodsProvider!.methods.isEmpty) { + return null; + } + + return methodsProvider!.methods.firstWhereOrNull( + (method) => method.type == PaymentType.wallet && + method.details.contains(wallet.walletUserID) + ); + } + + Map getAvailablePaymentTypes() { + final recipient = selectedRecipient; + if (recipient == null) return {}; + + return { + if (recipient.card != null) PaymentType.card: recipient.card!, + if (recipient.iban != null) PaymentType.iban: recipient.iban!, + if (recipient.wallet != null) PaymentType.wallet: recipient.wallet!, + if (recipient.bank != null) PaymentType.bankAccount: recipient.bank!, + }; + } + + PaymentType getDefaultPaymentType() { + final availableTypes = getAvailablePaymentTypes(); + final currentType = _type ?? PaymentType.bankAccount; + + if (availableTypes.containsKey(currentType)) { + return currentType; + } else if (availableTypes.isNotEmpty) { + return availableTypes.keys.first; + } else { + return PaymentType.bankAccount; + } + } + + bool shouldShowPaymentForm() { + return selectedRecipient == null; + } + + void handleWalletAutoSelection() { + if (selectedWallet != null && methodsProvider != null) { + final wallet = selectedWallet!; + final matchingMethod = getPaymentMethodForWallet(wallet); + if (matchingMethod != null) { + methodsProvider!.selectMethod(matchingMethod); + } } } diff --git a/frontend/pweb/lib/providers/payment_flow_provider.dart b/frontend/pweb/lib/providers/payment_flow_provider.dart new file mode 100644 index 0000000..d7001af --- /dev/null +++ b/frontend/pweb/lib/providers/payment_flow_provider.dart @@ -0,0 +1,78 @@ +import 'package:flutter/foundation.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/providers/page_selector.dart'; + + +class PaymentFlowProvider extends ChangeNotifier { + PaymentType _selectedType; + Object? _manualPaymentData; + + PaymentFlowProvider({ + required PaymentType initialType, + }) : _selectedType = initialType; + + PaymentType get selectedType => _selectedType; + Object? get manualPaymentData => _manualPaymentData; + + void syncWithSelector(PageSelectorProvider selector) { + final recipient = selector.selectedRecipient; + final resolvedType = _resolveSelectedType(selector, recipient); + + var hasChanges = false; + if (resolvedType != _selectedType) { + _selectedType = resolvedType; + hasChanges = true; + } + + if (recipient != null && _manualPaymentData != null) { + _manualPaymentData = null; + hasChanges = true; + } + + if (hasChanges) notifyListeners(); + } + + void reset(PageSelectorProvider selector) { + _selectedType = selector.getDefaultPaymentType(); + _manualPaymentData = null; + notifyListeners(); + } + + void selectType(PaymentType type, {bool resetManualData = false}) { + if (_selectedType == type && (!resetManualData || _manualPaymentData == null)) { + return; + } + + _selectedType = type; + if (resetManualData) { + _manualPaymentData = null; + } + notifyListeners(); + } + + void setManualPaymentData(Object? data) { + _manualPaymentData = data; + notifyListeners(); + } + + PaymentType _resolveSelectedType( + PageSelectorProvider selector, + Recipient? recipient, + ) { + final available = selector.getAvailablePaymentTypes(); + final current = _selectedType; + + if (recipient == null) { + return current; + } + + if (available.keys.contains(current)) { + return current; + } + + return selector.getDefaultPaymentType(); + } +} diff --git a/frontend/pweb/lib/providers/payment_methods.dart b/frontend/pweb/lib/providers/payment_methods.dart index 22dc3d7..4996469 100644 --- a/frontend/pweb/lib/providers/payment_methods.dart +++ b/frontend/pweb/lib/providers/payment_methods.dart @@ -59,7 +59,9 @@ class PaymentMethodsProvider extends ChangeNotifier { } void makeMain(PaymentMethod method) { - for (final m in _methods) m.isMain = false; + for (final m in _methods) { + m.isMain = false; + } method.isMain = true; selectMethod(method); } diff --git a/frontend/pweb/lib/providers/wallet_transactions.dart b/frontend/pweb/lib/providers/wallet_transactions.dart new file mode 100644 index 0000000..0999532 --- /dev/null +++ b/frontend/pweb/lib/providers/wallet_transactions.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:pshared/models/payment/status.dart'; + +import 'package:pweb/models/wallet_transaction.dart'; +import 'package:pweb/services/wallet_transactions.dart'; + + +class WalletTransactionsProvider extends ChangeNotifier { + final WalletTransactionsService _service; + + WalletTransactionsProvider(this._service); + + List _transactions = []; + List _filteredTransactions = []; + DateTimeRange? _dateRange; + final Set _selectedStatuses = {}; + final Set _selectedTypes = {}; + String? _walletId; + bool _isLoading = false; + String? _error; + + List get transactions => _transactions; + List get filteredTransactions => _filteredTransactions; + DateTimeRange? get dateRange => _dateRange; + Set get selectedStatuses => _selectedStatuses; + Set get selectedTypes => _selectedTypes; + String? get walletId => _walletId; + bool get isLoading => _isLoading; + String? get error => _error; + bool get hasFilters => + _dateRange != null || + _selectedStatuses.isNotEmpty || + _selectedTypes.isNotEmpty; + + Future load({String? walletId}) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _walletId = walletId ?? _walletId; + _transactions = await _service.fetchHistory(walletId: _walletId); + _applyFilters(notify: false); + _isLoading = false; + notifyListeners(); + } catch (e) { + _error = e.toString(); + _isLoading = false; + notifyListeners(); + } + } + + void setWallet(String walletId) { + _walletId = walletId; + _applyFilters(); + } + + void setDateRange(DateTimeRange? range) { + _dateRange = range; + _applyFilters(); + } + + void toggleStatus(OperationStatus status) { + if (_selectedStatuses.contains(status)) { + _selectedStatuses.remove(status); + } else { + _selectedStatuses.add(status); + } + _applyFilters(); + } + + void toggleType(WalletTransactionType type) { + if (_selectedTypes.contains(type)) { + _selectedTypes.remove(type); + } else { + _selectedTypes.add(type); + } + _applyFilters(); + } + + void resetFilters() { + _dateRange = null; + _selectedStatuses.clear(); + _selectedTypes.clear(); + _applyFilters(); + } + + void _applyFilters({bool notify = true}) { + _filteredTransactions = _transactions.where((tx) { + final walletMatch = _walletId == null || tx.walletId == _walletId; + final statusMatch = + _selectedStatuses.isEmpty || _selectedStatuses.contains(tx.status); + final typeMatch = + _selectedTypes.isEmpty || _selectedTypes.contains(tx.type); + final dateMatch = _dateRange == null || + (tx.date.isAfter(_dateRange!.start.subtract(const Duration(seconds: 1))) && + tx.date.isBefore(_dateRange!.end.add(const Duration(seconds: 1)))); + + return walletMatch && statusMatch && typeMatch && dateMatch; + }).toList(); + + if (notify) notifyListeners(); + } +} diff --git a/frontend/pweb/lib/providers/wallets.dart b/frontend/pweb/lib/providers/wallets.dart index f7c16f4..93b0661 100644 --- a/frontend/pweb/lib/providers/wallets.dart +++ b/frontend/pweb/lib/providers/wallets.dart @@ -13,7 +13,7 @@ class WalletsProvider with ChangeNotifier { bool _isLoading = false; String? _error; Wallet? _selectedWallet; - bool _isHidden = true; + final bool _isHidden = true; List? get wallets => _wallets; bool get isLoading => _isLoading; @@ -120,6 +120,11 @@ class WalletsProvider with ChangeNotifier { if (index != null && index >= 0) { final wallet = _wallets![index]; _wallets![index] = wallet.copyWith(isHidden: !wallet.isHidden); + + if (_selectedWallet?.id == walletId) { + _selectedWallet = _wallets![index]; + } + notifyListeners(); } } diff --git a/frontend/pweb/lib/services/balance.dart b/frontend/pweb/lib/services/balance.dart deleted file mode 100644 index 8587afa..0000000 --- a/frontend/pweb/lib/services/balance.dart +++ /dev/null @@ -1,22 +0,0 @@ -abstract class BalanceService { - Future getBalance(); - Future getWalletName(); - Future getWalletId(); -} - -class MockBalanceService implements BalanceService { - @override - Future getBalance() async { - return 3000000.56; - } - - @override - Future getWalletName() async { - return "Wallet"; - } - - @override - Future getWalletId() async { - return "WA-12345667"; - } -} diff --git a/frontend/pweb/lib/services/operations.dart b/frontend/pweb/lib/services/operations.dart new file mode 100644 index 0000000..4115c80 --- /dev/null +++ b/frontend/pweb/lib/services/operations.dart @@ -0,0 +1,85 @@ +import 'package:pshared/models/payment/operation.dart'; +import 'package:pshared/models/payment/status.dart'; + + +class OperationService { + Future> fetchOperations() async { + await Future.delayed(const Duration(milliseconds: 500)); + + return [ + OperationItem( + status: OperationStatus.error, + fileName: 'cards_payout_sample_june.csv', + amount: 10, + currency: 'EUR', + toAmount: 10, + toCurrency: 'EUR', + payId: '860163800', + cardNumber: null, + name: 'John Snow', + date: DateTime(2025, 7, 14, 19, 59, 2), + comment: 'EUR visa', + ), + OperationItem( + status: OperationStatus.processing, + fileName: 'cards_payout_sample_june.csv', + amount: 10, + currency: 'EUR', + toAmount: 10, + toCurrency: 'EUR', + payId: '860163700', + cardNumber: null, + name: 'Baltasar Gelt', + date: DateTime(2025, 7, 14, 19, 59, 2), + comment: 'EUR master', + ), + OperationItem( + status: OperationStatus.error, + fileName: 'cards_payout_sample_june.csv', + amount: 10, + currency: 'EUR', + toAmount: 10, + toCurrency: 'EUR', + payId: '40000000****0077', + cardNumber: '40000000****0077', + name: 'John Snow', + date: DateTime(2025, 7, 14, 19, 23, 22), + comment: 'EUR visa', + ), + OperationItem( + status: OperationStatus.success, + fileName: null, + amount: 10, + currency: 'EUR', + toAmount: 10, + toCurrency: 'EUR', + payId: '54133300****0019', + cardNumber: '54133300****0019', + name: 'Baltasar Gelt', + date: DateTime(2025, 7, 14, 19, 23, 21), + comment: 'EUR master', + ), + OperationItem( + status: OperationStatus.success, + fileName: null, + amount: 130, + currency: 'EUR', + toAmount: 130, + toCurrency: 'EUR', + payId: '54134300****0019', + cardNumber: '54153300****0019', + name: 'Ivan Brokov', + date: DateTime(2025, 7, 15, 19, 23, 21), + comment: 'EUR master 2', + ), + ]; + } + + // Add real API: + // Future> fetchOperations() async { + // final response = await _httpClient.get('/api/operations'); + // return (response.data as List) + // .map((json) => OperationItem.fromJson(json)) + // .toList(); + // } +} diff --git a/frontend/pweb/lib/services/payments/payment_methods.dart b/frontend/pweb/lib/services/payments/payment_methods.dart index 5946b93..7886ac5 100644 --- a/frontend/pweb/lib/services/payments/payment_methods.dart +++ b/frontend/pweb/lib/services/payments/payment_methods.dart @@ -33,6 +33,12 @@ class MockPaymentMethodsService implements PaymentMethodsService { ), PaymentMethod( id: '4', + label: 'Wallet', + details: 'WA-76654321', + type: PaymentType.wallet, + ), + PaymentMethod( + id: '5', label: 'Credit Card', details: '21•• •••• •••• 8901', type: PaymentType.card, diff --git a/frontend/pweb/lib/services/wallet_transactions.dart b/frontend/pweb/lib/services/wallet_transactions.dart new file mode 100644 index 0000000..aa34059 --- /dev/null +++ b/frontend/pweb/lib/services/wallet_transactions.dart @@ -0,0 +1,109 @@ +import 'package:pshared/models/payment/status.dart'; + +import 'package:pweb/models/currency.dart'; +import 'package:pweb/models/wallet_transaction.dart'; + + +abstract class WalletTransactionsService { + Future> fetchHistory({String? walletId}); +} + +class MockWalletTransactionsService implements WalletTransactionsService { + final List _history = [ + WalletTransaction( + id: 'wt-001', + walletId: '1124', + type: WalletTransactionType.topUp, + status: OperationStatus.success, + amount: 150000, + currency: Currency.rub, + date: DateTime(2024, 9, 14, 10, 12), + counterparty: 'VISA ••0019', + description: 'Top up via corporate card', + balanceAfter: 10150000, + ), + WalletTransaction( + id: 'wt-002', + walletId: '1124', + type: WalletTransactionType.payout, + status: OperationStatus.processing, + amount: 2500, + currency: Currency.rub, + date: DateTime(2024, 9, 15, 12, 10), + counterparty: 'Bank transfer RU239', + description: 'Invoice #239 shipping', + balanceAfter: 10147500, + ), + WalletTransaction( + id: 'wt-003', + walletId: '1124', + type: WalletTransactionType.payout, + status: OperationStatus.error, + amount: 1200, + currency: Currency.rub, + date: DateTime(2024, 9, 13, 16, 40), + counterparty: '4000 **** 0077', + description: 'Payout to card declined', + balanceAfter: 10000000, + ), + WalletTransaction( + id: 'wt-004', + walletId: '2124', + type: WalletTransactionType.topUp, + status: OperationStatus.success, + amount: 1800, + currency: Currency.usd, + date: DateTime(2024, 9, 12, 9, 0), + counterparty: 'Wire payment 8831', + description: 'Top up via USD wire', + balanceAfter: 4300.5, + ), + WalletTransaction( + id: 'wt-005', + walletId: '2124', + type: WalletTransactionType.payout, + status: OperationStatus.success, + amount: 400, + currency: Currency.usd, + date: DateTime(2024, 9, 16, 14, 30), + counterparty: 'IBAN DE09••1122', + description: 'Payout to John Snow', + balanceAfter: 3900.5, + ), + WalletTransaction( + id: 'wt-006', + walletId: '1124', + type: WalletTransactionType.payout, + status: OperationStatus.success, + amount: 70000, + currency: Currency.rub, + date: DateTime(2024, 9, 17, 8, 45), + counterparty: 'Payroll batch', + description: 'Monthly reimbursements', + balanceAfter: 10080000, + ), + WalletTransaction( + id: 'wt-007', + walletId: '1124', + type: WalletTransactionType.topUp, + status: OperationStatus.processing, + amount: 200000, + currency: Currency.rub, + date: DateTime(2024, 9, 18, 9, 30), + counterparty: 'Bank wire RU511', + description: 'Top up pending confirmation', + balanceAfter: 10280000, + ), + ]; + + @override + Future> fetchHistory({String? walletId}) async { + await Future.delayed(const Duration(milliseconds: 350)); + + final source = walletId == null + ? _history + : _history.where((tx) => tx.walletId == walletId).toList(); + + return List.from(source); + } +} diff --git a/frontend/pweb/lib/widgets/sidebar/page.dart b/frontend/pweb/lib/widgets/sidebar/page.dart index 68c8d99..5f3ba0a 100644 --- a/frontend/pweb/lib/widgets/sidebar/page.dart +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -5,8 +5,8 @@ import 'package:provider/provider.dart'; import 'package:pweb/pages/address_book/form/page.dart'; import 'package:pweb/pages/address_book/page/page.dart'; import 'package:pweb/pages/payment_methods/page.dart'; -import 'package:pweb/pages/payment_page/page.dart'; -import 'package:pweb/pages/payment_page/wallet/edit/page.dart'; +import 'package:pweb/pages/payout_page/page.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/page.dart'; import 'package:pweb/pages/report/page.dart'; import 'package:pweb/pages/settings/profile/page.dart'; import 'package:pweb/providers/page_selector.dart'; @@ -36,7 +36,7 @@ class PageSelector extends StatelessWidget { case PayoutDestination.recipients: content = RecipientAddressBookPage( onRecipientSelected: (recipient) => - provider.selectRecipient(recipient, fromList: true), + provider.selectRecipient(recipient, fromList: true), onAddRecipient: provider.goToAddRecipient, onEditRecipient: provider.editRecipient, ); @@ -52,7 +52,6 @@ class PageSelector extends StatelessWidget { case PayoutDestination.payment: content = PaymentPage( - type: provider.type, onBack: (_) => provider.goBackFromPayment(), ); break; @@ -75,7 +74,6 @@ class PageSelector extends StatelessWidget { final wallet = provider.walletsProvider?.selectedWallet; content = wallet != null ? WalletEditPage( - wallet: wallet, onBack: () => provider.goBackFromPayment(), ) : const Center(child: Text('No wallet selected')); //TODO Localize diff --git a/frontend/pweb/macos/Podfile b/frontend/pweb/macos/Podfile index 29c8eb3..ff5ddb3 100644 --- a/frontend/pweb/macos/Podfile +++ b/frontend/pweb/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/frontend/pweb/macos/Runner.xcodeproj/project.pbxproj b/frontend/pweb/macos/Runner.xcodeproj/project.pbxproj index a1153bb..b05da32 100644 --- a/frontend/pweb/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/pweb/macos/Runner.xcodeproj/project.pbxproj @@ -461,7 +461,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -543,7 +543,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -593,7 +593,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/frontend/pweb/pubspec.lock b/frontend/pweb/pubspec.lock index 1fea89d..3cd1a40 100644 --- a/frontend/pweb/pubspec.lock +++ b/frontend/pweb/pubspec.lock @@ -713,10 +713,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1125,10 +1125,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" timeago: dependency: "direct main" description: diff --git a/frontend/pweb/pubspec.yaml b/frontend/pweb/pubspec.yaml index 3949303..00784a0 100644 --- a/frontend/pweb/pubspec.yaml +++ b/frontend/pweb/pubspec.yaml @@ -1,5 +1,5 @@ name: pweb -description: "Profee Pay B2B Web Client" +description: "sendico B2B Web Client" # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev diff --git a/frontend/pweb/resources/logo.png b/frontend/pweb/resources/logo.png index 7172675e545585c4d8a18566f210127aa68e94a7..52b4648f7f3e832d657a44953a1e05e3358c2e7e 100644 GIT binary patch literal 27268 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 - + diff --git a/frontend/pweb/web/manifest.json b/frontend/pweb/web/manifest.json index e5b4f87..5181b90 100644 --- a/frontend/pweb/web/manifest.json +++ b/frontend/pweb/web/manifest.json @@ -5,7 +5,7 @@ "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "A new Flutter project.", + "description": "sendico", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ -- 2.49.1 From acbab71a579e7c0db3c128c9ad8fc6ba5cea5d41 Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 21 Nov 2025 19:25:37 +0300 Subject: [PATCH 2/5] Multiple Wallet support, history of each wallet and updated payment page --- ci/.DS_Store | Bin 6148 -> 0 bytes ci/prod/.DS_Store | Bin 8196 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ci/.DS_Store delete mode 100644 ci/prod/.DS_Store diff --git a/ci/.DS_Store b/ci/.DS_Store deleted file mode 100644 index edf009cf7306f12dad9e2d583d4ff5dbb9d45dc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKPfrs;6rTqbwji>Uf+*2!QsW5&0wE9&t_5RIV~MR;1gyL5P&aIMn%!*+B1zAB z^aJSKFW|+*qhCNzCf+^y0X+ETPXR%%#+Z4@%vEaowyNYop7B$6&Ul~j_$at9&MG(^dbvl#|vmE0=x=E zmyL&BP+_esYXp@@#OCFIku;Jeb8K&KdSPzDnwg*7pRo28rl%&X*_pZh{iHFHnY?zt zwC&dd_CgW}c-{yZP}qT&s2r@vD>v171YbJq`DzYzR+Q}PKRs|}&>Tt)r&A-T(b3WL z*|Bry$Hy;}%wg9lZ&yS9u8YiPVb*WZ^=jx<@^z=|1=V6qcjCPFjF!yQG1vRF^7znH z*sY^a`p_WK-3loUM84eCRz&0C5q+&>4o2mW`Th!vyx3#G+9PMDpyu0%ko4u4)|P`U zhLbmu@LZjHMXNN75Kn{`*PJkPf-24KZu*W({alb`am;GmlV>ZlU?rqHQ_(}6FDrBX zyQDN@-)RdiV_%XMlVqfVWg*?s6hh`h?Tye2fm5T&WX8Uyh759rERq~4k{WqVUXj=2 zEqPBql27Ct`2jtUhRZMs^Kb)h!(Dg)t5AhHG@uDBnW3xm%`d8q3ZRY3!8dQ_!@y@j z6>S}V<)`5o1BxG`|Cn7rY6dg|ngM=3SlBq)3fmIJ(Se<~0w5;PECh9am*6p$LR(>5 zB95Sd3`LZopspAo!_jUjKU-m2q6{aXE+3#~26aOLF+1kBggXISqV6>Vnt@*#;Q#(8 zCa?cDaS#9D{x6(#ry0-;{GSX!Vky6r#kHy0dSaEl)>_z}VB^C5+7d+wcB&i;g_q(n cY(j9%;tHazuq_cSDEdc0(4afbz+YwHC(y~yC;$Ke diff --git a/ci/prod/.DS_Store b/ci/prod/.DS_Store deleted file mode 100644 index eb701f3ea64ee0c7097a3744fdc8bc9fa2be0fe3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMPfrs;6n_Iqw<5AYL6m4VsqusXfe?rX*Mc#q5ke~#0qeFM>W1x3v%94rlJu-c zKY-r-0$xl!`UUi4;@y)Uz=Lo8RG@`}7h_;1nfX25pIP3oueP{uw$#8?V#g>{J{6!(Uf5h67PoCaOar;_4V@33nj2M7?STGy|;+@ZLQJ1?=3*P&wGYvpiZp z?@?-Vvm0soI@na6^ZSEZ|^z%8aLPQmp{*@$jOY4-)qnSD*J zOSJfOUzORZpig37A-204Q0j~Q7B)6Q8f6d2D><_#ECkH+7Fp;0?^a;`VnF#$ zSr=uVR`S+Q$#2rWUl&r+zA7~)B&C3_k%@++kR(5q-w8S6J7pS=C+&yIWWaTpg$2lB z)xCr_@D|>~NB9h1;0OF7og_i7kx?>DZj*cD0eM80Nr_ZQm28ok+@Y=U$zM@BDj; Date: Fri, 21 Nov 2025 21:09:32 +0300 Subject: [PATCH 3/5] Fixed navigation --- .../widgets/payment_page_body.dart | 7 ++++-- .../pweb/lib/providers/page_selector.dart | 23 +++++++++++++++---- frontend/pweb/lib/widgets/sidebar/page.dart | 4 ++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart index 68fe249..8c43c52 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart @@ -135,8 +135,11 @@ class PaymentBackButton extends StatelessWidget { child: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { - onBack?.call(pageSelector.selectedRecipient); - pageSelector.goBackFromPayment(); + if (onBack != null) { + onBack!(pageSelector.selectedRecipient); + } else { + pageSelector.goBackFromPayment(); + } }, ), ); diff --git a/frontend/pweb/lib/providers/page_selector.dart b/frontend/pweb/lib/providers/page_selector.dart index aff7623..c113eff 100644 --- a/frontend/pweb/lib/providers/page_selector.dart +++ b/frontend/pweb/lib/providers/page_selector.dart @@ -18,6 +18,7 @@ class PageSelectorProvider extends ChangeNotifier { PayoutDestination _selected = PayoutDestination.dashboard; PaymentType? _type; bool _cameFromRecipientList = false; + PayoutDestination? _previousDestination; RecipientProvider? recipientProvider; WalletsProvider? walletsProvider; @@ -52,6 +53,7 @@ class PageSelectorProvider extends ChangeNotifier { if (recipientProvider != null) { recipientProvider!.selectRecipient(recipient); _cameFromRecipientList = fromList; + _setPreviousDestination(); _selected = PayoutDestination.payment; notifyListeners(); } else { @@ -88,15 +90,19 @@ class PageSelectorProvider extends ChangeNotifier { } _type = type; _cameFromRecipientList = false; + _setPreviousDestination(); _selected = PayoutDestination.payment; notifyListeners(); } void goBackFromPayment() { - _selected = _cameFromRecipientList - ? PayoutDestination.recipients - : PayoutDestination.dashboard; + _selected = _previousDestination ?? + (_cameFromRecipientList + ? PayoutDestination.recipients + : PayoutDestination.dashboard); _type = null; + _previousDestination = null; + _cameFromRecipientList = false; notifyListeners(); } @@ -116,7 +122,8 @@ class PageSelectorProvider extends ChangeNotifier { void startPaymentFromWallet(Wallet wallet) { _type = PaymentType.wallet; - _cameFromRecipientList = true; + _cameFromRecipientList = false; + _setPreviousDestination(); _selected = PayoutDestination.payment; notifyListeners(); } @@ -171,6 +178,12 @@ class PageSelectorProvider extends ChangeNotifier { } } + void _setPreviousDestination() { + if (_selected != PayoutDestination.payment) { + _previousDestination = _selected; + } + } + Recipient? get selectedRecipient => recipientProvider?.selectedRecipient; Wallet? get selectedWallet => walletsProvider?.selectedWallet; -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/widgets/sidebar/page.dart b/frontend/pweb/lib/widgets/sidebar/page.dart index 5f3ba0a..84d9528 100644 --- a/frontend/pweb/lib/widgets/sidebar/page.dart +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -74,7 +74,7 @@ class PageSelector extends StatelessWidget { final wallet = provider.walletsProvider?.selectedWallet; content = wallet != null ? WalletEditPage( - onBack: () => provider.goBackFromPayment(), + onBack: provider.goBackFromWalletEdit, ) : const Center(child: Text('No wallet selected')); //TODO Localize break; @@ -106,4 +106,4 @@ class PageSelector extends StatelessWidget { ), ); } -} \ No newline at end of file +} -- 2.49.1 From 1fcd77cd954809cdc58d4a604481cd66fa6fcbbd Mon Sep 17 00:00:00 2001 From: Stephan D Date: Sun, 23 Nov 2025 15:49:24 +0100 Subject: [PATCH 4/5] fixes --- .vscode/launch.json | 48 --------------------------------- .vscode/settings.json | 9 ------- frontend/.vscode/launch.json | 48 --------------------------------- frontend/pweb/pubspec.yaml | 2 +- frontend/pweb/web/index.html | 2 +- frontend/pweb/web/manifest.json | 2 +- 6 files changed, 3 insertions(+), 108 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json delete mode 100644 frontend/.vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 9fe2095..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "pshared", - "cwd": "frontend/pshared", - "request": "launch", - "type": "dart" - }, - { - "name": "pshared (profile mode)", - "cwd": "frontend/pshared", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "pshared (release mode)", - "cwd": "frontend/pshared", - "request": "launch", - "type": "dart", - "flutterMode": "release" - }, - { - "name": "pweb", - "cwd": "frontend/pweb", - "request": "launch", - "type": "dart" - }, - { - "name": "pweb (profile mode)", - "cwd": "frontend/pweb", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "pweb (release mode)", - "cwd": "frontend/pweb", - "request": "launch", - "type": "dart", - "flutterMode": "release" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 56d4e72..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "yaml.schemas": { - "https://raw.githubusercontent.com/woodpecker-ci/woodpecker/main/pipeline/frontend/yaml/linter/schema/schema.json": [ - ".woodpecker/*.yml", - ".woodpecker.yml", - "woodpecker.yml" - ] - } -} \ No newline at end of file diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json deleted file mode 100644 index dc71d8c..0000000 --- a/frontend/.vscode/launch.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "pshared", - "cwd": "pshared", - "request": "launch", - "type": "dart" - }, - { - "name": "pshared (profile mode)", - "cwd": "pshared", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "pshared (release mode)", - "cwd": "pshared", - "request": "launch", - "type": "dart", - "flutterMode": "release" - }, - { - "name": "pweb", - "cwd": "pweb", - "request": "launch", - "type": "dart" - }, - { - "name": "pweb (profile mode)", - "cwd": "pweb", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "pweb (release mode)", - "cwd": "pweb", - "request": "launch", - "type": "dart", - "flutterMode": "release" - } - ] -} \ No newline at end of file diff --git a/frontend/pweb/pubspec.yaml b/frontend/pweb/pubspec.yaml index 00784a0..7d5ecc6 100644 --- a/frontend/pweb/pubspec.yaml +++ b/frontend/pweb/pubspec.yaml @@ -1,5 +1,5 @@ name: pweb -description: "sendico B2B Web Client" +description: "Sendico B2B Web Client" # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev diff --git a/frontend/pweb/web/index.html b/frontend/pweb/web/index.html index b0cabc4..02926d6 100644 --- a/frontend/pweb/web/index.html +++ b/frontend/pweb/web/index.html @@ -18,7 +18,7 @@ - + diff --git a/frontend/pweb/web/manifest.json b/frontend/pweb/web/manifest.json index 5181b90..ebf2709 100644 --- a/frontend/pweb/web/manifest.json +++ b/frontend/pweb/web/manifest.json @@ -5,7 +5,7 @@ "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "sendico", + "description": "Sendico", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ -- 2.49.1 From 72d8da1fe857a0c48bddcf9c2b5df2bfb90fc9ae Mon Sep 17 00:00:00 2001 From: Stephan D Date: Sun, 23 Nov 2025 15:50:46 +0100 Subject: [PATCH 5/5] removed dev file --- frontend/pweb/devtools_options.yaml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 frontend/pweb/devtools_options.yaml diff --git a/frontend/pweb/devtools_options.yaml b/frontend/pweb/devtools_options.yaml deleted file mode 100644 index fa0b357..0000000 --- a/frontend/pweb/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: -- 2.49.1