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 0000000..edf009c Binary files /dev/null and b/ci/.DS_Store differ diff --git a/ci/prod/.DS_Store b/ci/prod/.DS_Store new file mode 100644 index 0000000..eb701f3 Binary files /dev/null and b/ci/prod/.DS_Store differ diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json new file mode 100644 index 0000000..dc71d8c --- /dev/null +++ b/frontend/.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": "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/pshared/lib/models/payment/operation.dart b/frontend/pshared/lib/models/payment/operation.dart index 40ed8b6..3d67957 100644 --- a/frontend/pshared/lib/models/payment/operation.dart +++ b/frontend/pshared/lib/models/payment/operation.dart @@ -1,3 +1,4 @@ +import 'package:pshared/models/payment/methods/type.dart'; import 'package:pshared/models/payment/status.dart'; @@ -10,6 +11,7 @@ class OperationItem { final String toCurrency; final String payId; final String? cardNumber; + final PaymentMethod? paymentMethod; final String name; final DateTime date; final String comment; @@ -23,6 +25,7 @@ class OperationItem { required this.toCurrency, required this.payId, this.cardNumber, + this.paymentMethod, required this.name, required this.date, required this.comment, diff --git a/frontend/pweb/devtools_options.yaml b/frontend/pweb/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/frontend/pweb/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/frontend/pweb/lib/app/app.dart b/frontend/pweb/lib/app/app.dart index f6edcc6..b34c3a2 100644 --- a/frontend/pweb/lib/app/app.dart +++ b/frontend/pweb/lib/app/app.dart @@ -18,7 +18,7 @@ class PayApp extends StatelessWidget { @override Widget build(BuildContext context) => 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 7172675..52b4648 100644 Binary files a/frontend/pweb/resources/logo.png and b/frontend/pweb/resources/logo.png differ diff --git a/frontend/pweb/web/index.html b/frontend/pweb/web/index.html index 475a0a7..b0cabc4 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 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": [