diff --git a/frontend/.DS_Store b/frontend/.DS_Store new file mode 100644 index 0000000..fd8ce27 Binary files /dev/null and b/frontend/.DS_Store differ diff --git a/frontend/pshared/.DS_Store b/frontend/pshared/.DS_Store new file mode 100644 index 0000000..b7383e8 Binary files /dev/null and b/frontend/pshared/.DS_Store differ diff --git a/frontend/pshared/lib/models/resources.dart b/frontend/pshared/lib/models/resources.dart index 851f2d6..62c6f59 100644 --- a/frontend/pshared/lib/models/resources.dart +++ b/frontend/pshared/lib/models/resources.dart @@ -22,10 +22,6 @@ enum ResourceType { @JsonValue('clients') clients, - /// Represents comments on tasks or other resources - @JsonValue('comments') - comments, - /// Represents invitations sent to users @JsonValue('invitations') invitations, @@ -46,6 +42,9 @@ enum ResourceType { @JsonValue('organizations') organizations, + @JsonValue('chain_wallets') + chainWallets, + /// Represents permissions service @JsonValue('permissions') permissions, @@ -54,25 +53,6 @@ enum ResourceType { @JsonValue('policies') policies, - /// Represents task or project priorities - @JsonValue('priorities') - priorities, - - /// Represents priority groups - @JsonValue('priority_groups') - priorityGroups, - - /// Represents projects managed in the system - @JsonValue('projects') - projects, - - @JsonValue('properties') - properties, - - /// Represents reactions - @JsonValue('reactions') - reactions, - /// Represents refresh tokens for authentication @JsonValue('refresh_tokens') refreshTokens, @@ -88,20 +68,4 @@ enum ResourceType { /// Represents steps in workflows or processes @JsonValue('steps') steps, - - /// Represents tasks managed in the system - @JsonValue('tasks') - tasks, - - /// Represents teams managed in the system - @JsonValue('teams') - teams, - - /// Represents workflows for tasks or projects - @JsonValue('workflows') - workflows, - - /// Represents workspaces containing projects and teams - @JsonValue('workspaces') - workspaces; } diff --git a/frontend/pshared/lib/widgets/template.dart b/frontend/pshared/lib/widgets/template.dart index 5692f67..9041c88 100644 --- a/frontend/pshared/lib/widgets/template.dart +++ b/frontend/pshared/lib/widgets/template.dart @@ -21,8 +21,8 @@ class ResourceContainer extends StatelessWidget { @override Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { if (provider.isLoading) return loading ?? Center(child: CircularProgressIndicator()); - if (provider.error != null) return error ?? Text('Error while loading data. Try again'); //TODO: need to implement localizations and add more details to the error - if (provider.isEmpty) return empty ?? Text('Empty data'); //TODO: need to implement localizations too + if (provider.error != null) return error ?? Text('Error while loading data. Try again'); + if (provider.isEmpty) return empty ?? Text('Empty data'); return builder(context, provider); }); } 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/config/constants.dart b/frontend/pweb/lib/config/constants.dart index d6b52bc..3e371cd 100644 --- a/frontend/pweb/lib/config/constants.dart +++ b/frontend/pweb/lib/config/constants.dart @@ -5,6 +5,6 @@ class Constants { class AppConfig { static const String appName = String.fromEnvironment( 'APP_NAME', - defaultValue: 'SendiCo', + defaultValue: 'sendico', ); } diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 56dd816..08af507 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -36,9 +36,9 @@ "signupError": "Failed to signup: {error}", "signupSuccess": "Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.", "connectivityError": "Cannot reach the server at {serverAddress}. Check your network and try again.", - "errorAccountExists": "Account already exists", "errorAccountNotVerified": "Your account hasn't been verified yet. Please check your email to complete the verification", "errorLoginUnauthorized": "Login or password is incorrect. Please try again", + "errorAccountExists": "Account with this login already exists", "errorInternalError": "An internal error occurred. We're aware of the issue and working to resolve it. Please try again later", "errorVerificationTokenNotFound": "Account for verification not found. Sign up again", "created": "Created", @@ -441,5 +441,36 @@ "verificationStatusErrorUnknown": "Unexpected error occurred while verification. Try once again or contact support", "accountVerified": "Account Verified!", "accountVerifiedDescription": "Your account has been successfully verified. You can now log in to access your account.", - "retryVerification": "Retry Verification" -} \ No newline at end of file + "retryVerification": "Retry Verification", + "save": "Save", + "editWallet": "Edit Wallet", + "userNamePlaceholder": "User Name", + "noWalletSelected": "No wallet selected", + "noWalletsAvailable": "No wallets available", + "walletActivity": "Wallet activity", + "reset": "Reset", + "failedToLoadHistory": "Failed to load history", + "retry": "Retry", + "walletName": "Wallet name", + "walletNameSaved": "Wallet name saved", + "topUpBalance": "Top Up Balance", + "addFunctionality": "Add functionality", + "walletHistoryEmpty": "No history yet", + "colType": "Type", + "colAmount": "Amount", + "colBalance": "Balance", + "colCounterparty": "Counterparty", + "colDate": "Date", + "colComment": "Comment", + "recipientNoPaymentDetails": "This recipient has no available payment details.", + "paymentInfo": "Payment info", + "recipient": "Recipient", + "chooseAnotherRecipient": "Choose another recipient", + "noRecipientsYet": "No recipients yet.", + "noRecipientsFound": "No recipients found for this query.", + "sourceOfFunds": "Source of funds", + "walletTopUp": "Top up", + "englishLanguage": "English", + "russianLanguage": "Russian", + "germanLanguage": "German" +} diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 492c830..fb24bfa 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -38,6 +38,7 @@ "connectivityError": "Не удается связаться с сервером {serverAddress}. Проверьте ваше интернет-соединение и попробуйте снова.", "errorAccountNotVerified": "Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации", "errorLoginUnauthorized": "Неверный логин или пароль. Пожалуйста, попробуйте снова", + "errorAccountExists": "Аккаунт с таким логином уже существует", "errorInternalError": "Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже", "errorVerificationTokenNotFound": "Аккаунт для верификации не найден. Зарегистрируйтесь снова", "created": "Создано", @@ -433,5 +434,36 @@ "verificationStatusErrorUnknown": "Произошла непредвиденная ошибка при подтверждении. Попробуйте еще раз или обратитесь в службу поддержки", "accountVerified": "Аккаунт подтвержден!", "accountVerifiedDescription": "Ваш аккаунт успешно подтвержден. Теперь вы можете войти, чтобы получить доступ к своему аккаунту", - "retryVerification": "Повторить подтверждение" -} \ No newline at end of file + "retryVerification": "Повторить подтверждение", + "save": "Сохранить", + "editWallet": "Редактировать кошелек", + "userNamePlaceholder": "Имя пользователя", + "noWalletSelected": "Кошелек не выбран", + "noWalletsAvailable": "Кошельки отсутствуют", + "walletActivity": "Активность кошелька", + "reset": "Сбросить", + "failedToLoadHistory": "Не удалось загрузить историю", + "retry": "Повторить", + "walletName": "Название кошелька", + "walletNameSaved": "Название кошелька сохранено", + "topUpBalance": "Пополнить баланс", + "addFunctionality": "Добавить функциональность", + "walletHistoryEmpty": "История пуста", + "colType": "Тип", + "colAmount": "Сумма", + "colBalance": "Баланс", + "colCounterparty": "Контрагент", + "colDate": "Дата", + "colComment": "Комментарий", + "recipientNoPaymentDetails": "У этого получателя нет доступных платежных данных.", + "paymentInfo": "Платежная информация", + "recipient": "Получатель", + "chooseAnotherRecipient": "Выбрать другого получателя", + "noRecipientsYet": "Получателей пока нет.", + "noRecipientsFound": "Получатели по запросу не найдены.", + "sourceOfFunds": "Источник средств", + "walletTopUp": "Пополнение", + "englishLanguage": "Английский", + "russianLanguage": "Русский", + "germanLanguage": "Немецкий" +} diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 8ce1032..49e81a3 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -8,8 +8,9 @@ import 'package:provider/provider.dart'; import 'package:logging/logging.dart'; import 'package:pshared/config/constants.dart'; -import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; +import 'package:pshared/provider/permissions.dart'; +import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/organizations.dart'; import 'package:pweb/app/app.dart'; @@ -63,6 +64,11 @@ void main() async { create: (_) => TwoFactorProvider(), update: (context, accountProvider, provider) => provider!..update(accountProvider), ), + ChangeNotifierProxyProvider( + create: (_) => PermissionsProvider(), + update: (context, orgnization, provider) => provider!..update(orgnization), + ), + ChangeNotifierProvider(create: (_) => TwoFactorProvider()), ChangeNotifierProvider(create: (_) => OrganizationsProvider()), ChangeNotifierProvider(create: (_) => CarouselIndexProvider()), diff --git a/frontend/pweb/lib/models/wallet_transaction.dart b/frontend/pweb/lib/models/wallet_transaction.dart index adcd036..b8a9f79 100644 --- a/frontend/pweb/lib/models/wallet_transaction.dart +++ b/frontend/pweb/lib/models/wallet_transaction.dart @@ -1,18 +1,22 @@ import 'package:flutter/widgets.dart'; + import 'package:pshared/models/payment/status.dart'; import 'package:pweb/models/currency.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + enum WalletTransactionType { topUp, payout } extension WalletTransactionTypeX on WalletTransactionType { String label(BuildContext context) { + final loc = AppLocalizations.of(context)!; switch (this) { case WalletTransactionType.topUp: - return 'Top up'; + return loc.walletTopUp; case WalletTransactionType.payout: - return 'Payout'; + return loc.payout; } } diff --git a/frontend/pweb/lib/pages/address_book/page/page.dart b/frontend/pweb/lib/pages/address_book/page/page.dart index e459bba..2ed2a90 100644 --- a/frontend/pweb/lib/pages/address_book/page/page.dart +++ b/frontend/pweb/lib/pages/address_book/page/page.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/models/recipient/recipient.dart'; - import 'package:pshared/models/recipient/filter.dart'; + import 'package:pweb/pages/address_book/page/filter_button.dart'; import 'package:pweb/pages/address_book/page/header.dart'; import 'package:pweb/pages/address_book/page/list.dart'; @@ -14,7 +14,7 @@ import 'package:pweb/providers/recipient.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -class RecipientAddressBookPage extends StatelessWidget { +class RecipientAddressBookPage extends StatefulWidget { final ValueChanged onRecipientSelected; final VoidCallback onAddRecipient; final ValueChanged? onEditRecipient; @@ -31,32 +31,65 @@ class RecipientAddressBookPage extends StatelessWidget { static const double _bigBox = 30; static const double _smallBox = 20; + @override + State createState() => _RecipientAddressBookPageState(); +} + +class _RecipientAddressBookPageState extends State { + late final TextEditingController _searchController; + late final FocusNode _searchFocusNode; + + @override + void initState() { + super.initState(); + final provider = context.read(); + _searchController = TextEditingController(text: provider.query); + _searchFocusNode = FocusNode(); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + void _syncSearchField(RecipientProvider provider) { + final query = provider.query; + if (_searchController.text == query) return; + + _searchController.value = TextEditingValue( + text: query, + selection: TextSelection.collapsed(offset: query.length), + ); + } @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final provider = context.watch(); + _syncSearchField(provider); if (provider.isLoading) { - return const Center(child: CircularProgressIndicator()); //TODO This should be in the provider + return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { - return Center(child: Text('Error: ${provider.error}')); + return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation))); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RecipientAddressBookHeader(onAddRecipient: onAddRecipient), - const SizedBox(height: _smallBox), + RecipientAddressBookHeader(onAddRecipient: widget.onAddRecipient), + const SizedBox(height: RecipientAddressBookPage._smallBox), RecipientSearchField( - controller: TextEditingController(text: provider.query), - focusNode: FocusNode(), + controller: _searchController, + focusNode: _searchFocusNode, onChanged: provider.setQuery, ), - const SizedBox(height: _bigBox), + const SizedBox(height: RecipientAddressBookPage._bigBox), Row( children: [ RecipientFilterButton( @@ -86,17 +119,17 @@ class RecipientAddressBookPage extends StatelessWidget { ], ), SizedBox( - height: _expandedHeight, + height: RecipientAddressBookPage._expandedHeight, child: Padding( - padding: const EdgeInsets.all(_paddingAll), + padding: const EdgeInsets.all(RecipientAddressBookPage._paddingAll), child: RecipientAddressBookList( filteredRecipients: provider.filteredRecipients, - onEdit: (recipient) => onEditRecipient?.call(recipient), - onSelected: onRecipientSelected, + onEdit: (recipient) => widget.onEditRecipient?.call(recipient), + onSelected: widget.onRecipientSelected, ), ), ), ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart index 49f4658..502f9c9 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceAddFunds extends StatelessWidget { final VoidCallback onTopUp; @@ -19,6 +21,7 @@ class BalanceAddFunds extends StatelessWidget { Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; + final loc = AppLocalizations.of(context)!; return InkWell( onTap: onTopUp, @@ -37,7 +40,7 @@ class BalanceAddFunds extends StatelessWidget { ), const SizedBox(width: _spacingMedium), Text( - 'Add funds', + loc.addFunds, style: textTheme.bodyMedium?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.w500, diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart index 6f2fb87..cb4f39b 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart @@ -5,6 +5,8 @@ import 'package:provider/provider.dart'; import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart'; import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceWidget extends StatelessWidget { const BalanceWidget({super.key}); @@ -12,6 +14,7 @@ class BalanceWidget extends StatelessWidget { @override Widget build(BuildContext context) { final walletsProvider = context.watch(); + final loc = AppLocalizations.of(context)!; if (walletsProvider.isLoading) { return const Center(child: CircularProgressIndicator()); @@ -20,7 +23,7 @@ class BalanceWidget extends StatelessWidget { final wallets = walletsProvider.wallets; if (wallets == null || wallets.isEmpty) { - return const Center(child: Text('No wallets available')); + return Center(child: Text(loc.noWalletsAvailable)); } return @@ -29,4 +32,4 @@ class BalanceWidget extends StatelessWidget { onWalletChanged: walletsProvider.selectWallet, ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart index 0760430..056109d 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart @@ -23,7 +23,7 @@ class UploadHistorySection extends StatelessWidget { return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { - return Text("Error: ${provider.error}"); + return Text(l10.notificationError(provider.error ?? l10.noErrorInformation)); } final items = provider.data ?? []; diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart index 4974e05..bceda13 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart @@ -9,6 +9,8 @@ import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/long_l import 'package:pweb/pages/dashboard/payouts/single/adress_book/short_list.dart'; import 'package:pweb/providers/recipient.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class AdressBookPayout extends StatefulWidget { final ValueChanged onSelected; @@ -54,6 +56,7 @@ class _AdressBookPayoutState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; final provider = context.watch(); if (provider.isLoading) { @@ -61,7 +64,7 @@ class _AdressBookPayoutState extends State { } if (provider.error != null) { - return Center(child: Text('Error: ${provider.error}')); + return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation))); } return SizedBox( @@ -100,4 +103,4 @@ class _AdressBookPayoutState extends State { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/loaders/account.dart b/frontend/pweb/lib/pages/loaders/account.dart index 8c78257..d891e1c 100644 --- a/frontend/pweb/lib/pages/loaders/account.dart +++ b/frontend/pweb/lib/pages/loaders/account.dart @@ -26,13 +26,10 @@ class AccountLoader extends StatelessWidget { ); navigateAndReplace(context, Pages.login); } - if ((provider.error == null) && (provider.account == null)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - provider.restore(); - }); + if (provider.account == null) { + WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login)); return const Center(child: CircularProgressIndicator()); } return child; }); } - diff --git a/frontend/pweb/lib/pages/loaders/permissions.dart b/frontend/pweb/lib/pages/loaders/permissions.dart index 1488048..b77a7de 100644 --- a/frontend/pweb/lib/pages/loaders/permissions.dart +++ b/frontend/pweb/lib/pages/loaders/permissions.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/permissions.dart'; import 'package:pweb/app/router/pages.dart'; @@ -16,8 +17,10 @@ class PermissionsLoader extends StatelessWidget { const PermissionsLoader({super.key, required this.child}); @override - Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { - if (provider.isLoading) return const Center(child: CircularProgressIndicator()); + Widget build(BuildContext context) => Consumer2(builder: (context, provider, accountProvider, _) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } if (provider.error != null) { postNotifyUserOfErrorX( context: context, @@ -26,7 +29,7 @@ class PermissionsLoader extends StatelessWidget { ); navigateAndReplace(context, Pages.login); } - if ((provider.error == null) && (provider.permissions.isEmpty)) { + if (provider.error == null && !provider.isReady && accountProvider.account != null) { WidgetsBinding.instance.addPostFrameCallback((_) { provider.load(); }); diff --git a/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart b/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart index 9411619..3e19495 100644 --- a/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart +++ b/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart @@ -21,7 +21,7 @@ class PaymentMethodTypeSelector extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return DropdownButtonFormField( - value: value, + initialValue: value, decoration: InputDecoration(labelText: l10n.paymentType), items: PaymentType.values.map((type) { final label = getPaymentTypeLabel(context, type); diff --git a/frontend/pweb/lib/pages/payment_methods/title.dart b/frontend/pweb/lib/pages/payment_methods/title.dart index 54136fd..c14c93b 100644 --- a/frontend/pweb/lib/pages/payment_methods/title.dart +++ b/frontend/pweb/lib/pages/payment_methods/title.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:pweb/pages/payment_methods/icon.dart'; import 'package:pshared/models/payment/methods/type.dart'; +import 'package:pweb/pages/payment_methods/icon.dart'; + import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentMethodTile extends StatelessWidget { const PaymentMethodTile({ super.key, @@ -62,7 +64,7 @@ class PaymentMethodTile extends StatelessWidget { Widget _buildMakeMainButton(BuildContext context) { final theme = Theme.of(context); return IconButton( - tooltip: 'Make main', + tooltip: AppLocalizations.of(context)!.makeMain, icon: Icon( method.isMain ? Icons.star : Icons.star_outline, color: method.isMain ? theme.colorScheme.primary : null, @@ -97,4 +99,4 @@ class PaymentMethodTile extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/card.dart b/frontend/pweb/lib/pages/payment_methods/widgets/card.dart new file mode 100644 index 0000000..bc6edf2 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/widgets/card.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; +import 'package:pweb/utils/dimensions.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +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 loc = AppLocalizations.of(context)!; + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: EdgeInsets.all(dimensions.paddingMedium), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionTitle(loc.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, + ), + ), + ], + ), + ), + TextButton( + onPressed: onClear, + child: Text(loc.chooseAnotherRecipient), + ), + ], + ), + ], + ), + ); + } +} 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 index 534a727..5ba43bd 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart @@ -10,6 +10,8 @@ import 'package:pweb/providers/payment_flow_provider.dart'; import 'package:pweb/utils/dimensions.dart'; import 'package:pweb/utils/payment/selector_type.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentInfoSection extends StatelessWidget { final AppDimensions dimensions; @@ -27,13 +29,14 @@ class PaymentInfoSection extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(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.'); + return Text(loc.recipientNoPaymentDetails); } final selectedType = flowProvider.selectedType; @@ -41,7 +44,7 @@ class PaymentInfoSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SectionTitle('Payment info'), + SectionTitle(loc.paymentInfo), SizedBox(height: dimensions.paddingSmall), PaymentTypeSelector( availableTypes: availableTypes, diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart index 8c43c52..6479aac 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart @@ -15,6 +15,7 @@ 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'; +import 'package:pweb/generated/i18n/app_localizations.dart'; class PaymentPageBody extends StatelessWidget { @@ -45,13 +46,14 @@ class PaymentPageBody extends StatelessWidget { final recipientProvider = context.watch(); final flowProvider = context.watch(); final recipient = pageSelector.selectedRecipient; + final loc = AppLocalizations.of(context)!; if (methodsProvider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (methodsProvider.error != null) { - return Center(child: Text('Error: ${methodsProvider.error}')); + return Center(child: Text(loc.notificationError(methodsProvider.error ?? loc.noErrorInformation))); } return Align( @@ -74,7 +76,7 @@ class PaymentPageBody extends StatelessWidget { PaymentHeader(), SizedBox(height: dimensions.paddingXXLarge), - const SectionTitle('Source of funds'), + SectionTitle(loc.sourceOfFunds), SizedBox(height: dimensions.paddingSmall), PaymentMethodSelector( methodsProvider: methodsProvider, diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart index 69c1c84..1d284fa 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart @@ -3,10 +3,14 @@ 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/card.dart'; +import 'package:pweb/pages/payment_methods/widgets/search.dart'; import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; import 'package:pweb/providers/recipient.dart'; import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class RecipientSection extends StatelessWidget { final Recipient? recipient; @@ -32,6 +36,7 @@ class RecipientSection extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; if (recipient != null) { return SelectedRecipientCard( dimensions: dimensions, @@ -43,7 +48,7 @@ class RecipientSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SectionTitle('Recipient'), + SectionTitle(loc.recipient), SizedBox(height: dimensions.paddingSmall), RecipientSearchField( controller: searchController, @@ -61,127 +66,4 @@ class RecipientSection extends StatelessWidget { ], ); } -} - -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), - ); - }, - ), - ); - } -} +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/search.dart b/frontend/pweb/lib/pages/payment_methods/widgets/search.dart new file mode 100644 index 0000000..c6acf25 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/widgets/search.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/providers/recipient.dart'; +import 'package:pweb/utils/dimensions.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +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) { + final loc = AppLocalizations.of(context)!; + if (recipientProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (recipientProvider.error != null) { + return Text( + loc.notificationError(recipientProvider.error ?? loc.noErrorInformation), + style: TextStyle(color: Theme.of(context).colorScheme.error), + ); + } + + if (recipientProvider.recipients.isEmpty) { + return Text(loc.noRecipientsYet); + } + + final results = recipientProvider.filteredRecipients; + + if (results.isEmpty) { + return Text(loc.noRecipientsFound); + } + + 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), + ), + 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/payout_page/methods/controller.dart b/frontend/pweb/lib/pages/payout_page/methods/controller.dart index e0496f4..f964b0a 100644 --- a/frontend/pweb/lib/pages/payout_page/methods/controller.dart +++ b/frontend/pweb/lib/pages/payout_page/methods/controller.dart @@ -21,11 +21,12 @@ class PaymentConfigController { } Future addMethod() async { + final methodsProvider = context.read(); await showDialog( context: context, builder: (_) => const AddPaymentMethodDialog(), ); - loadMethods(); + methodsProvider.loadMethods(); } Future editMethod(PaymentMethod method) async { @@ -33,19 +34,20 @@ class PaymentConfigController { } Future deleteMethod(PaymentMethod method) async { + final methodsProvider = context.read(); final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( context: context, - builder: (_) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text(l10n.delete), content: Text(l10n.deletePaymentConfirmation), actions: [ TextButton( - onPressed: () => Navigator.pop(context, false), + onPressed: () => Navigator.pop(dialogContext, false), child: Text(l10n.cancel), ), ElevatedButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.pop(dialogContext, true), child: Text(l10n.delete), ), ], @@ -53,7 +55,7 @@ class PaymentConfigController { ); if (confirmed == true) { - context.read().deleteMethod(method); + methodsProvider.deleteMethod(method); } } diff --git a/frontend/pweb/lib/pages/payout_page/page.dart b/frontend/pweb/lib/pages/payout_page/page.dart index 818f1c6..5ef4fbb 100644 --- a/frontend/pweb/lib/pages/payout_page/page.dart +++ b/frontend/pweb/lib/pages/payout_page/page.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; + import 'package:pweb/models/wallet.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'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentConfigPage extends StatelessWidget { final Function(Wallet) onWalletTap; @@ -16,13 +19,14 @@ class PaymentConfigPage extends StatelessWidget { @override Widget build(BuildContext context) { final provider = context.watch(); + final loc = AppLocalizations.of(context)!; if (provider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { - return Center(child: Text('Error: ${provider.error}')); + return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation))); } return Column( @@ -34,4 +38,4 @@ class PaymentConfigPage extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart index 8d64fea..0529585 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart @@ -3,25 +3,28 @@ import 'package:flutter/material.dart'; import 'package:pweb/models/wallet.dart'; import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class SaveWalletButton extends StatelessWidget { final Wallet wallet; final TextEditingController nameController; final TextEditingController balanceController; - final VoidCallback onSave; // Changed to VoidCallback + final VoidCallback onSave; const SaveWalletButton({ super.key, required this.wallet, required this.nameController, required this.balanceController, - required this.onSave, // Now matches _saveWallet signature + required this.onSave, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final dimensions = AppDimensions(); + final loc = AppLocalizations.of(context)!; return Center( child: SizedBox( @@ -29,7 +32,7 @@ class SaveWalletButton extends StatelessWidget { height: dimensions.buttonHeight, child: InkWell( borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), - onTap: onSave, // Directly use onSave now + onTap: onSave, child: Container( decoration: BoxDecoration( color: theme.colorScheme.primary, @@ -37,7 +40,7 @@ class SaveWalletButton extends StatelessWidget { ), child: Center( child: Text( - 'Save', + loc.save, style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSecondary, fontWeight: FontWeight.w600, @@ -49,4 +52,4 @@ class SaveWalletButton extends StatelessWidget { ), ); } -} \ No newline at end of file +} 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 index 4cfaccb..4232719 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; + import 'package:provider/provider.dart'; + import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class SendPayoutButton extends StatelessWidget { const SendPayoutButton({super.key}); @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; return ElevatedButton( style: ElevatedButton.styleFrom( shadowColor: null, @@ -23,7 +28,7 @@ class SendPayoutButton extends StatelessWidget { pageSelectorProvider.startPaymentFromWallet(wallet); } }, - child: Text('Send Payout'), + child: Text(loc.payoutNavSendPayout), ); } } diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart index 898fdb9..7e9b69c 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class TopUpButton extends StatelessWidget{ const TopUpButton({super.key}); @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; return ElevatedButton( style: ElevatedButton.styleFrom( shadowColor: null, @@ -13,10 +16,10 @@ class TopUpButton extends StatelessWidget{ ), onPressed: () { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Add functionality')), + SnackBar(content: Text(loc.addFunctionality)), ); }, - child: Text('Top Up Balance'), + child: Text(loc.topUpBalance), ); } -} \ 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 index 1693c9e..c12db0f 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart @@ -5,6 +5,8 @@ import 'package:provider/provider.dart'; import 'package:pweb/providers/wallets.dart'; import 'package:pweb/widgets/error/snackbar.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletEditHeader extends StatefulWidget { const WalletEditHeader({super.key}); @@ -33,7 +35,9 @@ class _WalletEditHeaderState extends State { Widget build(BuildContext context) { final provider = context.watch(); final wallet = provider.selectedWallet; - + final loc = AppLocalizations.of(context)!; + final messanger = ScaffoldMessenger.of(context); + if (wallet == null) { return SizedBox.shrink(); } @@ -75,10 +79,10 @@ class _WalletEditHeaderState extends State { Expanded( child: TextFormField( controller: _controller, - decoration: const InputDecoration( + decoration: InputDecoration( border: OutlineInputBorder(), isDense: true, - hintText: 'Wallet name', + hintText: loc.walletName, ), ), ), @@ -112,4 +116,4 @@ class _WalletEditHeaderState extends State { ], ); } -} \ 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 index 635b259..e95431c 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart @@ -9,6 +9,8 @@ import 'package:pweb/pages/payout_page/wallet/history/history.dart'; import 'package:pweb/providers/wallets.dart'; import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletEditPage extends StatelessWidget { final VoidCallback onBack; @@ -18,13 +20,14 @@ class WalletEditPage extends StatelessWidget { @override Widget build(BuildContext context) { final dimensions = AppDimensions(); + final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, child) { final wallet = provider.selectedWallet; if (wallet == null) { - return Center(child: Text('Кошелёк не выбран')); + return Center(child: Text(loc.noWalletSelected)); } return Align( diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart index 65639e4..7b80d4f 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:pshared/models/payment/status.dart'; @@ -7,6 +6,8 @@ import 'package:pshared/utils/localization.dart'; import 'package:pweb/models/wallet_transaction.dart'; import 'package:pweb/providers/wallet_transactions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletHistoryFilters extends StatelessWidget { final WalletTransactionsProvider provider; @@ -21,6 +22,7 @@ class WalletHistoryFilters extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; return Card( elevation: 2, @@ -35,13 +37,13 @@ class WalletHistoryFilters extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Wallet activity', + loc.walletActivity, style: theme.textTheme.titleMedium, ), if (provider.hasFilters) TextButton( onPressed: provider.resetFilters, - child: const Text('Reset'), + child: Text(loc.reset), ), ], ), @@ -81,7 +83,7 @@ class WalletHistoryFilters extends StatelessWidget { icon: const Icon(Icons.date_range_outlined), label: Text( provider.dateRange == null - ? 'Select period' + ? loc.selectPeriod : '${dateToLocalFormat(context, provider.dateRange!.start)} – ${dateToLocalFormat(context, provider.dateRange!.end)}', ), ), @@ -91,4 +93,4 @@ class WalletHistoryFilters extends StatelessWidget { ), ); } -} \ 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 index 1e64438..b6117d3 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart @@ -7,6 +7,8 @@ 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'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletHistory extends StatefulWidget { final Wallet wallet; @@ -64,6 +66,7 @@ class _WalletHistoryState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, child) { @@ -81,16 +84,16 @@ class _WalletHistoryState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Failed to load history', + loc.failedToLoadHistory, style: theme.textTheme.titleMedium! .copyWith(color: theme.colorScheme.error), ), const SizedBox(height: 8), - Text(provider.error!), + Text(loc.notificationError(provider.error ?? loc.noErrorInformation)), const SizedBox(height: 8), OutlinedButton( onPressed: _load, - child: const Text('Retry'), + child: Text(loc.retry), ), ], ), @@ -113,4 +116,4 @@ class _WalletHistoryState extends State { }, ); } -} \ 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 index 49cd9c0..43312fb 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:pweb/models/wallet_transaction.dart'; @@ -6,6 +5,8 @@ import 'package:pweb/pages/payout_page/wallet/history/chip.dart'; import 'package:pweb/pages/report/table/badge.dart'; import 'package:pweb/utils/currency.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletTransactionsTable extends StatelessWidget { final List transactions; @@ -15,14 +16,15 @@ class WalletTransactionsTable extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.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'), + child: Padding( + padding: const EdgeInsets.all(16), + child: Text(loc.walletHistoryEmpty), ), ); } @@ -38,14 +40,14 @@ class WalletTransactionsTable extends StatelessWidget { 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')), + columns: [ + DataColumn(label: Text(loc.colStatus)), + DataColumn(label: Text(loc.colType)), + DataColumn(label: Text(loc.colAmount)), + DataColumn(label: Text(loc.colBalance)), + DataColumn(label: Text(loc.colCounterparty)), + DataColumn(label: Text(loc.colDate)), + DataColumn(label: Text(loc.colComment)), ], rows: List.generate( transactions.length, @@ -85,4 +87,4 @@ class WalletTransactionsTable extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/report/charts/distribution.dart b/frontend/pweb/lib/pages/report/charts/distribution.dart index b491985..db016b3 100644 --- a/frontend/pweb/lib/pages/report/charts/distribution.dart +++ b/frontend/pweb/lib/pages/report/charts/distribution.dart @@ -16,7 +16,7 @@ class PayoutDistributionChart extends StatelessWidget { // 1) Aggregate sums final sums = {}; for (var op in operations) { - final name = op.name ?? AppLocalizations.of(context)!.unknown; + final name = op.name; sums[name] = (sums[name] ?? 0) + op.amount; } if (sums.isEmpty) { @@ -32,7 +32,7 @@ class PayoutDistributionChart extends StatelessWidget { final palette = [ Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.secondary, - Theme.of(context).colorScheme.tertiary ?? Colors.grey, + Theme.of(context).colorScheme.tertiary, Theme.of(context).colorScheme.primaryContainer, Theme.of(context).colorScheme.secondaryContainer, ]; diff --git a/frontend/pweb/lib/pages/report/page.dart b/frontend/pweb/lib/pages/report/page.dart index 31b1477..3b14e66 100644 --- a/frontend/pweb/lib/pages/report/page.dart +++ b/frontend/pweb/lib/pages/report/page.dart @@ -8,6 +8,8 @@ import 'package:pweb/pages/report/table/filters.dart'; import 'package:pweb/pages/report/table/widget.dart'; import 'package:pweb/providers/operatioins.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class OperationHistoryPage extends StatefulWidget { const OperationHistoryPage({super.key}); @@ -48,6 +50,7 @@ class _OperationHistoryPageState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, child) { if (provider.isLoading) { @@ -59,10 +62,10 @@ class _OperationHistoryPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Error: ${provider.error}'), + Text(loc.notificationError(provider.error ?? loc.noErrorInformation)), ElevatedButton( onPressed: () => provider.loadOperations(), - child: const Text('Retry'), + child: Text(loc.retry), ), ], ), @@ -108,4 +111,4 @@ class _OperationHistoryPageState extends State { }, ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/settings/profile/account/locale.dart b/frontend/pweb/lib/pages/settings/profile/account/locale.dart index 31d5982..22ff7e1 100644 --- a/frontend/pweb/lib/pages/settings/profile/account/locale.dart +++ b/frontend/pweb/lib/pages/settings/profile/account/locale.dart @@ -4,9 +4,10 @@ import 'package:provider/provider.dart'; import 'package:pshared/provider/locale.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/services/amplitude.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class LocalePicker extends StatelessWidget { final String title; @@ -74,13 +75,13 @@ class LocalePicker extends StatelessWidget { String _localizedLocaleName(Locale locale, AppLocalizations loc) { switch (locale.languageCode) { case 'en': - return 'English'; + return loc.englishLanguage; case 'ru': - return 'Русский'; + return loc.russianLanguage; case 'de': - return 'Deutsch'; + return loc.germanLanguage; default: return locale.toString(); } } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/settings/profile/page.dart b/frontend/pweb/lib/pages/settings/profile/page.dart index ce850f4..861ab09 100644 --- a/frontend/pweb/lib/pages/settings/profile/page.dart +++ b/frontend/pweb/lib/pages/settings/profile/page.dart @@ -37,7 +37,7 @@ class ProfileSettingsPage extends StatelessWidget { errorText: loc.avatarUpdateError, ), AccountName( - name: 'User Name', + name: loc.userNamePlaceholder, title: loc.accountName, hintText: loc.accountNameHint, errorText: loc.accountNameUpdateError, @@ -50,4 +50,4 @@ class ProfileSettingsPage extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/settings/widgets/pick.dart b/frontend/pweb/lib/pages/settings/widgets/pick.dart index 7bf7933..e607007 100644 --- a/frontend/pweb/lib/pages/settings/widgets/pick.dart +++ b/frontend/pweb/lib/pages/settings/widgets/pick.dart @@ -78,14 +78,21 @@ class SelectValueTile extends BaseEditTile { ), ), Flexible( - child: ListView( - shrinkWrap: true, - children: displayedOptions.map((o) => RadioListTile( - value: o, - groupValue: initial, - title: Text(labelBuilder(o)), - onChanged: isSaving ? null : (v) { if (v != null) onSave(v); }, - )).toList(), + child: RadioGroup( + groupValue: initial, + onChanged: (value) { + if (value != null && !isSaving) { + onSave(value); + } + }, + child: ListView( + shrinkWrap: true, + children: displayedOptions.map((o) => RadioListTile( + value: o, + title: Text(labelBuilder(o)), + enabled: !isSaving, + )).toList(), + ), ), ), const Divider(), diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index 4f941fc..83bf1d1 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -8,8 +8,8 @@ import 'package:provider/provider.dart'; import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/models/describable.dart'; -import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; +import 'package:pshared/provider/account.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/signup/form/content.dart'; diff --git a/frontend/pweb/lib/providers/two_factor.dart b/frontend/pweb/lib/providers/two_factor.dart index 7236b9e..548244a 100644 --- a/frontend/pweb/lib/providers/two_factor.dart +++ b/frontend/pweb/lib/providers/two_factor.dart @@ -71,4 +71,4 @@ class TwoFactorProvider extends ChangeNotifier { notifyListeners(); } } -} +} \ No newline at end of file diff --git a/frontend/pweb/lib/services/accounts.dart b/frontend/pweb/lib/services/accounts.dart new file mode 100644 index 0000000..d840bbc --- /dev/null +++ b/frontend/pweb/lib/services/accounts.dart @@ -0,0 +1,105 @@ +import 'package:collection/collection.dart'; + +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/describable.dart'; +import 'package:pshared/models/storable.dart'; + +import 'mock_ids.dart'; + +class InvalidCredentialsException implements Exception { + @override + String toString() => 'InvalidCredentialsException'; +} + +class DuplicateAccountException implements Exception { + @override + String toString() => 'DuplicateAccountException'; +} + +class AccountLoginResult { + final Account account; + final String roleId; + + const AccountLoginResult({ + required this.account, + required this.roleId, + }); +} + +class _AccountRecord { + final Account account; + final String password; + final String roleId; + + const _AccountRecord({ + required this.account, + required this.password, + required this.roleId, + }); +} + +class AccountsService { + final List<_AccountRecord> _accounts = [ + _AccountRecord( + account: Account( + storable: newStorable(id: companyAccountRef), + describable: newDescribable(name: 'Sendico Company'), + avatarUrl: null, + lastName: 'Owner', + login: 'company@sendico.com', + locale: 'ru', + ), + password: 'password123A', + roleId: companyRoleId, + ), + _AccountRecord( + account: Account( + storable: newStorable(id: recipientAccountRef), + describable: newDescribable(name: 'John Recipient'), + avatarUrl: null, + lastName: 'Doe', + login: 'recipient@sendico.com', + locale: 'ru', + ), + password: 'password123A', + roleId: recipientRoleId, + ), + ]; + + Future login(String email, String password, {String? locale}) async { + await Future.delayed(const Duration(milliseconds: 300)); + + final normalized = email.trim().toLowerCase(); + final record = _accounts.where((acc) => acc.account.login.toLowerCase() == normalized).singleOrNull; + + if (record == null || record.password != password) { + throw InvalidCredentialsException(); + } + + return AccountLoginResult(account: record.account, roleId: record.roleId); + } + + Future signup(AccountData data, {String roleId = recipientRoleId}) async { + await Future.delayed(const Duration(milliseconds: 300)); + + final normalized = data.login.trim().toLowerCase(); + if (_accounts.any((acc) => acc.account.login.toLowerCase() == normalized)) { + throw DuplicateAccountException(); + } + + final account = Account( + storable: newStorable(id: 'account-${_accounts.length + 1}'), + describable: newDescribable(name: data.name), + avatarUrl: null, + lastName: data.lastName, + login: normalized, + locale: data.locale, + ); + + _accounts.add(_AccountRecord(account: account, password: data.password, roleId: roleId)); + return AccountLoginResult(account: account, roleId: roleId); + } + + Account? getByRef(String accountRef) => _accounts.where((acc) => acc.account.id == accountRef).singleOrNull?.account; +} diff --git a/frontend/pweb/lib/services/auth.dart b/frontend/pweb/lib/services/auth.dart new file mode 100644 index 0000000..7c0c099 --- /dev/null +++ b/frontend/pweb/lib/services/auth.dart @@ -0,0 +1,18 @@ +class AuthenticationService { + Future verifyTwoFactorCode(String code) async { + await Future.delayed(const Duration(seconds: 2)); + + if (code == '000000') { + return true; + } else { + throw const WrongCodeException(); + } + } +} + +class WrongCodeException implements Exception { + const WrongCodeException(); + + @override + String toString() => 'WrongCodeException'; +} diff --git a/frontend/pweb/lib/services/mock_ids.dart b/frontend/pweb/lib/services/mock_ids.dart new file mode 100644 index 0000000..ffde7ff --- /dev/null +++ b/frontend/pweb/lib/services/mock_ids.dart @@ -0,0 +1,12 @@ +// Centralized identifiers for mock auth/permission data to keep the +// mock services in sync and make it easy to swap in a real API later. +const String mockOrganizationRef = 'org-sendico'; + +const String companyRoleId = 'role-company'; +const String recipientRoleId = 'role-recipient'; + +const String companyAccountRef = 'account-company'; +const String recipientAccountRef = 'account-recipient'; + +const String accountsPolicyDescriptionId = 'policy-accounts'; +const String rolesPolicyDescriptionId = 'policy-roles'; diff --git a/frontend/pweb/lib/utils/error_handler.dart b/frontend/pweb/lib/utils/error_handler.dart index 2313b4f..90625da 100644 --- a/frontend/pweb/lib/utils/error_handler.dart +++ b/frontend/pweb/lib/utils/error_handler.dart @@ -5,6 +5,7 @@ import 'package:pshared/api/responses/error/server.dart'; import 'package:pshared/config/constants.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/services/accounts.dart'; class ErrorHandler { @@ -48,6 +49,8 @@ class ErrorHandler { final errorHandlers = { ErrorResponse: (ex) => _handleErrorResponseLocs(locs, ex as ErrorResponse), ConnectivityError: (ex) => _handleConnectivityErrorLocs(locs, ex as ConnectivityError), + InvalidCredentialsException: (_) => locs.errorLoginUnauthorized, + DuplicateAccountException: (_) => locs.errorAccountExists, }; return errorHandlers[e.runtimeType]?.call(e) ?? e.toString(); diff --git a/frontend/pweb/lib/widgets/drawer/avatar.dart b/frontend/pweb/lib/widgets/drawer/avatar.dart index 11dc659..632ea53 100644 --- a/frontend/pweb/lib/widgets/drawer/avatar.dart +++ b/frontend/pweb/lib/widgets/drawer/avatar.dart @@ -6,16 +6,19 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:pshared/provider/account.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class AccountAvatar extends StatelessWidget { const AccountAvatar({super.key}); @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, _) => UserAccountsDrawerHeader( - accountName: Text(provider.account?.name ?? 'John Doe'), - accountEmail: Text(provider.account?.login ?? 'john.doe@acme.com'), + accountName: Text(provider.account?.name ?? loc.userNamePlaceholder), + accountEmail: Text(provider.account?.login ?? loc.usernameHint), currentAccountPicture: CircleAvatar( backgroundImage: (provider.account?.avatarUrl?.isNotEmpty ?? false) ? CachedNetworkImageProvider(provider.account!.avatarUrl!) diff --git a/frontend/pweb/lib/widgets/protected/widget.dart b/frontend/pweb/lib/widgets/protected/widget.dart index 49ce816..43b6c4c 100644 --- a/frontend/pweb/lib/widgets/protected/widget.dart +++ b/frontend/pweb/lib/widgets/protected/widget.dart @@ -13,4 +13,4 @@ T? protectedWidgetctx(BuildContext context, ResourceType resou T? protectedWidget(PermissionsProvider provider, ResourceType resource, T child, {perm.Action? action}) { return provider.canAccessResource(resource, action: action) ? child : null; -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/widgets/sidebar/destinations.dart b/frontend/pweb/lib/widgets/sidebar/destinations.dart index 62598c9..899e805 100644 --- a/frontend/pweb/lib/widgets/sidebar/destinations.dart +++ b/frontend/pweb/lib/widgets/sidebar/destinations.dart @@ -40,7 +40,7 @@ enum PayoutDestination { case PayoutDestination.addrecipient: return loc.addRecipient; case PayoutDestination.editwallet: - return 'Edit Wallet'; + return loc.editWallet; } } } diff --git a/frontend/pweb/lib/widgets/sidebar/page.dart b/frontend/pweb/lib/widgets/sidebar/page.dart index 57d773b..315ff99 100644 --- a/frontend/pweb/lib/widgets/sidebar/page.dart +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/models/resources.dart'; import 'package:pshared/provider/account.dart'; +import 'package:pshared/provider/permissions.dart'; import 'package:pweb/pages/address_book/form/page.dart'; import 'package:pweb/pages/address_book/page/page.dart'; @@ -13,25 +15,50 @@ import 'package:pweb/pages/report/page.dart'; import 'package:pweb/pages/settings/profile/page.dart'; import 'package:pweb/pages/dashboard/dashboard.dart'; import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/app/router/pages.dart'; import 'package:pweb/widgets/appbar/app_bar.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/widgets/sidebar/sidebar.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class PageSelector extends StatelessWidget { const PageSelector({super.key}); - void _logout(BuildContext context) => context.read().logout(); + void _logout(BuildContext context) { + context.read().logout(); + navigateAndReplace(context, Pages.login); + } @override - Widget build(BuildContext context) => Consumer(builder:(context, provider, _) { + Widget build(BuildContext context) { + final provider = context.watch(); + final permissions = context.watch(); + final loc = AppLocalizations.of(context)!; + + final bool restrictedAccess = !permissions.canRead(ResourceType.chainWallets); + final allowedDestinations = restrictedAccess + ? { + PayoutDestination.settings, + PayoutDestination.methods, + PayoutDestination.editwallet, + } + : PayoutDestination.values.toSet(); + + final selected = allowedDestinations.contains(provider.selected) + ? provider.selected + : (restrictedAccess ? PayoutDestination.settings : PayoutDestination.dashboard); + + if (selected != provider.selected) { + WidgetsBinding.instance.addPostFrameCallback((_) => provider.selectPage(selected)); + } Widget content; - switch (provider.selected) { + switch (selected) { case PayoutDestination.dashboard: content = DashboardPage( - onRecipientSelected: (recipient) => - provider.selectRecipient(recipient), + onRecipientSelected: (recipient) => provider.selectRecipient(recipient), onGoToPaymentWithoutRecipient: provider.startPaymentWithoutRecipient, ); break; @@ -79,16 +106,16 @@ class PageSelector extends StatelessWidget { ? WalletEditPage( onBack: provider.goBackFromWalletEdit, ) - : const Center(child: Text('No wallet selected')); //TODO Localize + : Center(child: Text(loc.noWalletSelected)); break; default: - content = Text(provider.selected.name); + content = Text(selected.name); } return Scaffold( appBar: PayoutAppBar( - title: Text(provider.selected.localizedLabel(context)), + title: Text(selected.localizedLabel(context)), onAddFundsPressed: () {}, onLogout: () => _logout(context), ), @@ -99,7 +126,7 @@ class PageSelector extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ PayoutSidebar( - selected: provider.selected, + selected: selected, onSelected: provider.selectPage, onLogout: () => _logout(context), ), @@ -108,5 +135,5 @@ class PageSelector extends StatelessWidget { ), ), ); - }); + } } diff --git a/frontend/pweb/lib/widgets/sidebar/sidebar.dart b/frontend/pweb/lib/widgets/sidebar/sidebar.dart index d3dac81..b51e012 100644 --- a/frontend/pweb/lib/widgets/sidebar/sidebar.dart +++ b/frontend/pweb/lib/widgets/sidebar/sidebar.dart @@ -13,6 +13,7 @@ class PayoutSidebar extends StatelessWidget { this.onLogout, this.userName, this.avatarUrl, + this.items, }); final PayoutDestination selected; @@ -21,11 +22,13 @@ class PayoutSidebar extends StatelessWidget { final String? userName; final String? avatarUrl; + final List? items; @override Widget build(BuildContext context) { - final items = [ + final menuItems = items ?? + [ PayoutDestination.dashboard, PayoutDestination.recipients, PayoutDestination.methods, @@ -49,11 +52,11 @@ class PayoutSidebar extends StatelessWidget { theme: theme, avatarUrl: avatarUrl, userName: userName, - items: items, + items: menuItems, selected: selected, onSelected: onSelected, ), ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/widgets/sidebar/user.dart b/frontend/pweb/lib/widgets/sidebar/user.dart index a979614..0370bcf 100644 --- a/frontend/pweb/lib/widgets/sidebar/user.dart +++ b/frontend/pweb/lib/widgets/sidebar/user.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class UserProfileCard extends StatelessWidget { final ThemeData theme; @@ -21,6 +23,7 @@ class UserProfileCard extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; bool isSelected = selected == PayoutDestination.settings; final backgroundColor = isSelected ? theme.colorScheme.primaryContainer @@ -52,7 +55,7 @@ class UserProfileCard extends StatelessWidget { const SizedBox(width: 8), Flexible( child: Text( - userName ?? 'User Name', + userName ?? loc.userNamePlaceholder, style: theme.textTheme.bodyLarge?.copyWith( fontSize: 20, fontWeight: FontWeight.w500, @@ -67,4 +70,4 @@ class UserProfileCard extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/resources/logo.png b/frontend/pweb/resources/logo.png index 52b4648..bc5ab8c 100644 Binary files a/frontend/pweb/resources/logo.png and b/frontend/pweb/resources/logo.png differ diff --git a/frontend/pweb/untranslated.txt b/frontend/pweb/untranslated.txt index 5039bda..9deb798 100644 --- a/frontend/pweb/untranslated.txt +++ b/frontend/pweb/untranslated.txt @@ -1,6 +1,5 @@ { "ru": [ - "errorAccountExists", "companyName", "companynameRequired", "errorSignUp",