Added Localizations and ran small fixes

This commit is contained in:
Arseni
2025-11-25 08:20:09 +03:00
parent 72d8da1fe8
commit fcb5ab4f2c
41 changed files with 444 additions and 233 deletions

BIN
frontend/.DS_Store vendored Normal file

Binary file not shown.

BIN
frontend/pshared/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -21,8 +21,8 @@ class ResourceContainer<T extends GenericProvider> extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer<T>(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);
});
}

View File

@@ -5,6 +5,6 @@ class Constants {
class AppConfig {
static const String appName = String.fromEnvironment(
'APP_NAME',
defaultValue: 'SendiCo',
defaultValue: 'sendico',
);
}

View File

@@ -433,5 +433,36 @@
"companyDescriptionHint": "Describe any of the fields of the Company's business",
"optional": "optional",
"ownerRole": "Organization Owner",
"ownerRoleDescription": "This role is granted to the organizations creator, providing full administrative privileges"
"ownerRoleDescription": "This role is granted to the organizations creator, providing full administrative privileges",
"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"
}

View File

@@ -425,5 +425,36 @@
"noRecipientSelected": "Получатель не выбран",
"ownerRole": "Владелец организации",
"ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права"
"ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права",
"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": "Немецкий"
}

View File

@@ -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;
}
}

View File

@@ -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<Recipient> onRecipientSelected;
final VoidCallback onAddRecipient;
final ValueChanged<Recipient>? onEditRecipient;
@@ -31,32 +31,65 @@ class RecipientAddressBookPage extends StatelessWidget {
static const double _bigBox = 30;
static const double _smallBox = 20;
@override
State<RecipientAddressBookPage> createState() => _RecipientAddressBookPageState();
}
class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
late final TextEditingController _searchController;
late final FocusNode _searchFocusNode;
@override
void initState() {
super.initState();
final provider = context.read<RecipientProvider>();
_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<RecipientProvider>();
_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,
),
),
),
],
);
}
}
}

View File

@@ -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,

View File

@@ -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<WalletsProvider>();
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,
);
}
}
}

View File

@@ -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 ?? [];

View File

@@ -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<Recipient> onSelected;
@@ -54,6 +56,7 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final provider = context.watch<RecipientProvider>();
if (provider.isLoading) {
@@ -61,7 +64,7 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
}
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<AdressBookPayout> {
),
);
}
}
}

View File

@@ -21,7 +21,7 @@ class PaymentMethodTypeSelector extends StatelessWidget {
final l10n = AppLocalizations.of(context)!;
return DropdownButtonFormField<PaymentType>(
value: value,
initialValue: value,
decoration: InputDecoration(labelText: l10n.paymentType),
items: PaymentType.values.map((type) {
final label = getPaymentTypeLabel(context, type);

View File

@@ -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 {
],
);
}
}
}

View File

@@ -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),
),
],
),
],
),
);
}
}

View File

@@ -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,

View File

@@ -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<RecipientProvider>();
final flowProvider = context.watch<PaymentFlowProvider>();
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,

View File

@@ -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<Recipient> 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),
);
},
),
);
}
}
}

View File

@@ -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<Recipient> 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),
);
},
),
);
}
}

View File

@@ -21,11 +21,12 @@ class PaymentConfigController {
}
Future<void> addMethod() async {
final methodsProvider = context.read<PaymentMethodsProvider>();
await showDialog<PaymentMethodData>(
context: context,
builder: (_) => const AddPaymentMethodDialog(),
);
loadMethods();
methodsProvider.loadMethods();
}
Future<void> editMethod(PaymentMethod method) async {
@@ -33,19 +34,20 @@ class PaymentConfigController {
}
Future<void> deleteMethod(PaymentMethod method) async {
final methodsProvider = context.read<PaymentMethodsProvider>();
final l10n = AppLocalizations.of(context)!;
final confirmed = await showDialog<bool>(
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<PaymentMethodsProvider>().deleteMethod(method);
methodsProvider.deleteMethod(method);
}
}

View File

@@ -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<PaymentMethodsProvider>();
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 {
],
);
}
}
}

View File

@@ -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 {
),
);
}
}
}

View File

@@ -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),
);
}
}

View File

@@ -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),
);
}
}
}

View File

@@ -4,6 +4,8 @@ import 'package:provider/provider.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletEditHeader extends StatefulWidget {
const WalletEditHeader({super.key});
@@ -32,7 +34,9 @@ class _WalletEditHeaderState extends State<WalletEditHeader> {
Widget build(BuildContext context) {
final provider = context.watch<WalletsProvider>();
final wallet = provider.selectedWallet;
final loc = AppLocalizations.of(context)!;
final messanger = ScaffoldMessenger.of(context);
if (wallet == null) {
return SizedBox.shrink();
}
@@ -74,10 +78,10 @@ class _WalletEditHeaderState extends State<WalletEditHeader> {
Expanded(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(
decoration: InputDecoration(
border: OutlineInputBorder(),
isDense: true,
hintText: 'Wallet name',
hintText: loc.walletName,
),
),
),
@@ -87,8 +91,8 @@ class _WalletEditHeaderState extends State<WalletEditHeader> {
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')),
messanger.showSnackBar(
SnackBar(content: Text(loc.walletNameSaved)),
);
setState(() {
_isEditing = false;
@@ -110,4 +114,4 @@ class _WalletEditHeaderState extends State<WalletEditHeader> {
],
);
}
}
}

View File

@@ -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<WalletsProvider>(
builder: (context, provider, child) {
final wallet = provider.selectedWallet;
if (wallet == null) {
return Center(child: Text('Кошелёк не выбран'));
return Center(child: Text(loc.noWalletSelected));
}
return Align(

View File

@@ -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 {
),
);
}
}
}

View File

@@ -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<WalletHistory> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
return Consumer<WalletTransactionsProvider>(
builder: (context, provider, child) {
@@ -81,16 +84,16 @@ class _WalletHistoryState extends State<WalletHistory> {
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<WalletHistory> {
},
);
}
}
}

View File

@@ -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<WalletTransaction> 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 {
),
);
}
}
}

View File

@@ -16,7 +16,7 @@ class PayoutDistributionChart extends StatelessWidget {
// 1) Aggregate sums
final sums = <String, double>{};
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,
];

View File

@@ -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<OperationHistoryPage> {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
return Consumer<OperationProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
@@ -59,10 +62,10 @@ class _OperationHistoryPageState extends State<OperationHistoryPage> {
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<OperationHistoryPage> {
},
);
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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 {
),
);
}
}
}

View File

@@ -78,14 +78,21 @@ class SelectValueTile<T> extends BaseEditTile<T> {
),
),
Flexible(
child: ListView(
shrinkWrap: true,
children: displayedOptions.map((o) => RadioListTile<T>(
value: o,
groupValue: initial,
title: Text(labelBuilder(o)),
onChanged: isSaving ? null : (v) { if (v != null) onSave(v); },
)).toList(),
child: RadioGroup<T>(
groupValue: initial,
onChanged: (value) {
if (value != null && !isSaving) {
onSave(value);
}
},
child: ListView(
shrinkWrap: true,
children: displayedOptions.map((o) => RadioListTile<T>(
value: o,
title: Text(labelBuilder(o)),
enabled: !isSaving,
)).toList(),
),
),
),
const Divider(),

View File

@@ -0,0 +1,18 @@
class AuthenticationService {
Future<bool> 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';
}

View File

@@ -26,7 +26,7 @@ class MockWalletsService implements WalletsService {
Future<Wallet> getWallet(String walletId) async {
return _wallets.firstWhere(
(wallet) => wallet.id == walletId,
orElse: () => throw Exception('Wallet not found'),
orElse: () => throw const WalletNotFoundException(),
);
}
@@ -38,4 +38,11 @@ class MockWalletsService implements WalletsService {
@override
Future<List<Wallet>> deleteWallet() async => [];
}
}
class WalletNotFoundException implements Exception {
const WalletNotFoundException();
@override
String toString() => 'WalletNotFoundException';
}

View File

@@ -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<AccountProvider>(
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!)

View File

@@ -40,7 +40,7 @@ enum PayoutDestination {
case PayoutDestination.addrecipient:
return loc.addRecipient;
case PayoutDestination.editwallet:
return 'Edit Wallet';
return loc.editWallet;
}
}
}

View File

@@ -15,6 +15,8 @@ import 'package:pweb/pages/dashboard/dashboard.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});
@@ -22,6 +24,7 @@ class PageSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
final provider = context.watch<PageSelectorProvider>();
final loc = AppLocalizations.of(context)!;
Widget content;
switch (provider.selected) {
@@ -76,7 +79,7 @@ class PageSelector extends StatelessWidget {
? WalletEditPage(
onBack: provider.goBackFromWalletEdit,
)
: const Center(child: Text('No wallet selected')); //TODO Localize
: Center(child: Text(loc.noWalletSelected));
break;
default:

View File

@@ -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 {
),
);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB