diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 0b9a2966..60811736 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -348,7 +348,7 @@ "description": "Table column header for row count" }, - "amountColumn": "Amount", + "amountColumn": "Amount ₽", "@amountColumn": { "description": "Table column header for the original amount" }, @@ -515,6 +515,7 @@ "comment": "Comment", "uploadCSV": "Upload your CSV", "upload": "Upload", + "changeFile": "Change file", "hintUpload": "Supported format: .CSV · Max size 1 MB", "uploadHistory": "Upload History", "viewWholeHistory": "View Whole History", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index ae3d480d..492a9693 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -348,7 +348,7 @@ "description": "Заголовок столбца таблицы для количества строк" }, - "amountColumn": "Сумма", + "amountColumn": "Сумма ₽", "@amountColumn": { "description": "Заголовок столбца таблицы для исходной суммы" }, @@ -515,6 +515,7 @@ "comment": "Комментарий", "uploadCSV": "Загрузите ваш CSV", "upload": "Загрузить", + "changeFile": "Заменить файл", "hintUpload": "Поддерживаемый формат: .CSV · Макс. размер 1 МБ", "uploadHistory": "История загрузок", "viewWholeHistory": "Смотреть всю историю", diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/helpers.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart similarity index 92% rename from frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/helpers.dart rename to frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart index bb618b5b..3f8f4a67 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/helpers.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart @@ -4,7 +4,7 @@ import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/widgets/dialogs/payment_status_dialog.dart'; -Future handleUploadSend( +Future handleMultiplePayoutSend( BuildContext context, MultiplePayoutsController controller, ) async { diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/header.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/header.dart index d6ebe0ba..67e3294b 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/header.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/header.dart @@ -6,15 +6,12 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class SourceQuotePanelHeader extends StatelessWidget { const SourceQuotePanelHeader({ super.key, - required this.theme, - required this.l10n, }); - final ThemeData theme; - final AppLocalizations l10n; - @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; return Text( l10n.sourceOfFunds, style: theme.textTheme.titleSmall?.copyWith( diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart index f3141ec9..8bfb6b32 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart @@ -3,32 +3,29 @@ import 'package:flutter/material.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pweb/controllers/multiple_payouts.dart'; +import 'package:pweb/pages/dashboard/payouts/multiple/actions.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/header.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart'; -import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/selector.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - - class SourceQuotePanel extends StatelessWidget { const SourceQuotePanel({ super.key, required this.controller, required this.walletsController, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; final WalletsController walletsController; - final ThemeData theme; - final AppLocalizations l10n; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; return Container( width: double.infinity, padding: const EdgeInsets.all(12), @@ -40,13 +37,11 @@ class SourceQuotePanel extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SourceQuotePanelHeader(theme: theme, l10n: l10n), + SourceQuotePanelHeader(), const SizedBox(height: 8), SourceWalletSelector( - controller: controller, walletsController: walletsController, - theme: theme, - l10n: l10n, + isBusy: controller.isBusy, ), const SizedBox(height: 12), const Divider(height: 1), @@ -54,6 +49,26 @@ class SourceQuotePanel extends StatelessWidget { SourceQuoteSummary(controller: controller, spacing: 12), const SizedBox(height: 12), MultipleQuoteStatusCard(controller: controller), + const SizedBox(height: 12), + Center( + child: ElevatedButton( + onPressed: controller.canSend + ? () => handleMultiplePayoutSend(context, controller) + : null, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + textStyle: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + child: Text(l10n.send), + ), + ), ], ), ); diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart index 9911d6a2..95742f7a 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart @@ -9,49 +9,26 @@ class UploadPanelActions extends StatelessWidget { const UploadPanelActions({ super.key, required this.controller, - required this.l10n, - required this.onSend, }); - + final MultiplePayoutsController controller; - final AppLocalizations l10n; - final VoidCallback onSend; - - static const double _buttonVerticalPadding = 12; - static const double _buttonHorizontalPadding = 24; @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final hasFile = controller.selectedFileName != null; - return Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.center, - children: [ - OutlinedButton( - onPressed: !hasFile || controller.isBusy - ? null - : () => controller.removeUploadedFile(), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: _buttonHorizontalPadding, - vertical: _buttonVerticalPadding, - ), - ), - child: Text(l10n.cancel), + if (!hasFile) return const SizedBox.shrink(); + + return TextButton( + onPressed: controller.isBusy ? null : () => controller.pickAndQuote(), + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, ), - ElevatedButton( - onPressed: controller.canSend ? onSend : null, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: _buttonHorizontalPadding, - vertical: _buttonVerticalPadding, - ), - ), - child: Text(l10n.send), - ), - ], + ), + child: Text(l10n.changeFile), ); } } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart index 5bb0629f..24b4f7bf 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:dotted_border/dotted_border.dart'; + import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -9,100 +11,88 @@ class UploadDropZone extends StatelessWidget { const UploadDropZone({ super.key, required this.controller, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; - final ThemeData theme; - final AppLocalizations l10n; static const double _panelRadius = 12; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; final hasFile = controller.selectedFileName != null; + final borderRadius = BorderRadius.circular(_panelRadius); + final borderColor = theme.colorScheme.outlineVariant.withValues( + alpha: hasFile ? 0.55 : 1, + ); + final surfaceColor = theme.colorScheme.surfaceContainerHighest.withValues( + alpha: hasFile ? 0.2 : 0.35, + ); + final iconColor = hasFile + ? theme.colorScheme.onSurfaceVariant + : theme.colorScheme.primary; - return InkWell( - onTap: controller.isBusy - ? null - : () => controller.pickAndQuote(), - borderRadius: BorderRadius.circular(_panelRadius), - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest.withValues( - alpha: 0.5, - ), - border: Border.all(color: theme.colorScheme.outlineVariant), - borderRadius: BorderRadius.circular(_panelRadius), + return ClipRRect( + borderRadius: borderRadius, + child: DottedBorder( + options: RoundedRectDottedBorderOptions( + radius: Radius.circular(_panelRadius), + dashPattern: const [8, 5], + strokeWidth: 1.4, + color: borderColor, ), - child: Column( - children: [ - Icon( - Icons.upload_file, - size: 34, - color: theme.colorScheme.primary, - ), - const SizedBox(height: 8), - Text( - hasFile ? controller.selectedFileName! : l10n.uploadCSV, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - if (!hasFile) ...[ - const SizedBox(height: 8), - Container( + child: Material( + color: surfaceColor, + child: InkWell( + onTap: controller.isBusy + ? null + : () => controller.pickAndQuote(), + borderRadius: borderRadius, + child: SizedBox( + width: double.infinity, + child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, + horizontal: 18, + vertical: 16, ), - decoration: BoxDecoration( - color: theme.colorScheme.primary.withValues(alpha: 0.12), - border: Border.all( - color: theme.colorScheme.primary.withValues(alpha: 0.5), - ), - borderRadius: BorderRadius.circular(999), - ), - child: Row( - mainAxisSize: MainAxisSize.min, + child: Column( children: [ Icon( - Icons.touch_app, - size: 16, - color: theme.colorScheme.primary, + Icons.upload_file, + size: 34, + color: iconColor, ), - const SizedBox(width: 6), + const SizedBox(height: 8), Text( - l10n.upload, - style: theme.textTheme.labelLarge?.copyWith( - color: theme.colorScheme.primary, + hasFile + ? controller.selectedFileName! + : l10n.uploadCSV, + style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), + textAlign: TextAlign.center, ), + const SizedBox(height: 6), + Text( + l10n.hintUpload, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + if (hasFile) ...[ + const SizedBox(height: 6), + Text( + '${l10n.payout}: ${controller.rows.length}', + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], ], ), ), - ], - const SizedBox(height: 6), - Text( - l10n.hintUpload, - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, ), - if (hasFile) ...[ - const SizedBox(height: 6), - Text( - '${l10n.payout}: ${controller.rows.length}', - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.primary, - ), - ), - ], - ], + ), ), ), ); diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart index 10c9deac..a89dc913 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart @@ -5,14 +5,13 @@ class UploadQuoteProgress extends StatelessWidget { const UploadQuoteProgress({ super.key, required this.isQuoting, - required this.theme, }); final bool isQuoting; - final ThemeData theme; @override Widget build(BuildContext context) { + final theme = Theme.of(context); if (!isQuoting) return const SizedBox.shrink(); return Column( diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart index 864df391..59cdd05a 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart @@ -9,16 +9,15 @@ class UploadPanelStatus extends StatelessWidget { const UploadPanelStatus({ super.key, required this.controller, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; - final ThemeData theme; - final AppLocalizations l10n; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + if (controller.sentCount <= 0 && controller.error == null) { return const SizedBox.shrink(); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart index 380c2e9a..613a611e 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart @@ -4,40 +4,32 @@ import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart'; -import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/helpers.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; - class UploadPanel extends StatelessWidget { const UploadPanel({ super.key, required this.controller, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; - final ThemeData theme; - final AppLocalizations l10n; @override Widget build(BuildContext context) { + final hasFile = controller.selectedFileName != null; return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - UploadDropZone(controller: controller, theme: theme, l10n: l10n), - UploadQuoteProgress(isQuoting: controller.isQuoting, theme: theme), - const SizedBox(height: 12), - UploadPanelActions( - controller: controller, - l10n: l10n, - onSend: () => handleUploadSend(context, controller), - ), - UploadPanelStatus(controller: controller, theme: theme, l10n: l10n), + UploadDropZone(controller: controller), + UploadQuoteProgress(isQuoting: controller.isQuoting), + if (hasFile) ...[ + const SizedBox(height: 12), + UploadPanelActions(controller: controller), + ], + UploadPanelStatus(controller: controller), ], ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart index 7844d6b8..41dda609 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart @@ -24,7 +24,7 @@ class FileFormatSampleTable extends StatelessWidget { DataColumn(label: Text(l10n.firstName)), DataColumn(label: Text(l10n.lastName)), DataColumn(label: Text(l10n.expiryDate)), - DataColumn(label: Text(l10n.amount)), + DataColumn(label: Text(l10n.amountColumn)), ], rows: rows.map((row) { return DataRow( diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart index 55d9d870..db28f61b 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart @@ -2,20 +2,19 @@ import 'package:flutter/material.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class UploadCsvHeader extends StatelessWidget { const UploadCsvHeader({ super.key, required this.theme, - required this.l10n, }); final ThemeData theme; - final AppLocalizations l10n; - static const double _iconTextSpacing = 5; @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Row( children: [ const Icon(Icons.upload), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart index b9e8e761..61598de7 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart @@ -3,11 +3,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pweb/controllers/multiple_payouts.dart'; - import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart'; @@ -16,17 +13,14 @@ class UploadCsvLayout extends StatelessWidget { super.key, required this.controller, required this.walletsController, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; final WalletsController walletsController; - final ThemeData theme; - final AppLocalizations l10n; @override Widget build(BuildContext context) { + final hasFile = controller.selectedFileName != null; return LayoutBuilder( builder: (context, constraints) { final useHorizontal = constraints.maxWidth >= 760; @@ -34,24 +28,29 @@ class UploadCsvLayout extends StatelessWidget { return Column( children: [ PanelCard( - theme: theme, child: UploadPanel( controller: controller, - theme: theme, - l10n: l10n, ), ), - const SizedBox(height: 12), - SourceQuotePanel( - controller: controller, - walletsController: walletsController, - theme: theme, - l10n: l10n, - ), + if (hasFile) ...[ + const SizedBox(height: 12), + SourceQuotePanel( + controller: controller, + walletsController: walletsController, + ), + ], ], ); } + if (!hasFile) { + return PanelCard( + child: UploadPanel( + controller: controller, + ), + ); + } + return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -59,11 +58,8 @@ class UploadCsvLayout extends StatelessWidget { Expanded( flex: 3, child: PanelCard( - theme: theme, child: UploadPanel( controller: controller, - theme: theme, - l10n: l10n, ), ), ), @@ -73,8 +69,6 @@ class UploadCsvLayout extends StatelessWidget { child: SourceQuotePanel( controller: controller, walletsController: walletsController, - theme: theme, - l10n: l10n, ), ), ], diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart index e922bf69..ab19e583 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart @@ -4,11 +4,9 @@ import 'package:flutter/material.dart'; class PanelCard extends StatelessWidget { const PanelCard({ super.key, - required this.theme, required this.child, }); - final ThemeData theme; final Widget child; @override @@ -16,11 +14,6 @@ class PanelCard extends StatelessWidget { return Container( width: double.infinity, padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: theme.colorScheme.surface, - border: Border.all(color: theme.colorScheme.outlineVariant), - borderRadius: BorderRadius.circular(8), - ), child: child, ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/widget.dart index a30f33cf..fd1f51cf 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/widget.dart @@ -6,8 +6,6 @@ import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; - class UploadCSVSection extends StatelessWidget { const UploadCSVSection({super.key}); @@ -18,18 +16,15 @@ class UploadCSVSection extends StatelessWidget { Widget build(BuildContext context) { final controller = context.watch(); final theme = Theme.of(context); - final l10n = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - UploadCsvHeader(theme: theme, l10n: l10n), + UploadCsvHeader(theme: theme), const SizedBox(height: _verticalSpacing), UploadCsvLayout( controller: controller, walletsController: context.watch(), - theme: theme, - l10n: l10n, ), ], ); diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart index 3d6fdc08..829182a4 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/models/payment/wallet.dart'; -import 'package:pweb/utils/payment/dropdown.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector.dart'; class PaymentMethodSelector extends StatelessWidget { @@ -18,9 +18,8 @@ class PaymentMethodSelector extends StatelessWidget { @override Widget build(BuildContext context) => Consumer( - builder: (context, provider, _) => PaymentMethodDropdown( - methods: provider.wallets, - selectedMethod: provider.selectedWallet, + builder: (context, provider, _) => SourceWalletSelector( + walletsController: provider, onChanged: onMethodChanged, ), ); diff --git a/frontend/pweb/lib/services/payments/csv_input.dart b/frontend/pweb/lib/services/payments/csv_input.dart index eb70e733..0d6c2116 100644 --- a/frontend/pweb/lib/services/payments/csv_input.dart +++ b/frontend/pweb/lib/services/payments/csv_input.dart @@ -19,17 +19,11 @@ class WebCsvInputService implements CsvInputService { Future pickCsv() async { final input = html.FileUploadInputElement() ..accept = '.csv,text/csv' - ..multiple = false; + ..multiple = false + ..style.display = 'none'; - final completer = Completer(); - input.onChange.listen((_) { - completer.complete( - input.files?.isNotEmpty == true ? input.files!.first : null, - ); - }); - input.click(); - - final file = await completer.future; + html.document.body?.append(input); + final file = await _pickFile(input); if (file == null) return null; final reader = html.FileReader(); @@ -50,4 +44,52 @@ class WebCsvInputService implements CsvInputService { final content = await readCompleter.future; return PickedCsvFile(name: file.name, content: content); } + + Future _pickFile(html.FileUploadInputElement input) async { + final completer = Completer(); + + void completeWith(html.File? file) { + if (!completer.isCompleted) completer.complete(file); + } + + StreamSubscription? changeSub; + StreamSubscription? inputSub; + StreamSubscription? focusSub; + Timer? timeout; + + void cleanup() { + changeSub?.cancel(); + inputSub?.cancel(); + focusSub?.cancel(); + timeout?.cancel(); + input.remove(); + } + + void tryComplete() { + final file = input.files?.isNotEmpty == true ? input.files!.first : null; + if (file != null) { + completeWith(file); + } + } + + changeSub = input.onChange.listen((_) => tryComplete()); + inputSub = input.onInput.listen((_) => tryComplete()); + focusSub = html.window.onFocus.listen((_) async { + if (completer.isCompleted) return; + for (var i = 0; i < 6; i++) { + tryComplete(); + if (completer.isCompleted) return; + await Future.delayed(const Duration(milliseconds: 120)); + } + if (!completer.isCompleted && (input.value ?? '').isEmpty) { + completeWith(null); + } + }); + + timeout = Timer(const Duration(minutes: 2), () => completeWith(null)); + + completer.future.whenComplete(cleanup); + input.click(); + return completer.future; + } } diff --git a/frontend/pweb/lib/utils/payment/dropdown.dart b/frontend/pweb/lib/utils/payment/dropdown.dart deleted file mode 100644 index 291b30c8..00000000 --- a/frontend/pweb/lib/utils/payment/dropdown.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/type.dart'; -import 'package:pshared/models/payment/wallet.dart'; - -import 'package:pweb/pages/payment_methods/icon.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class PaymentMethodDropdown extends StatelessWidget { - final List methods; - final ValueChanged onChanged; - final Wallet? selectedMethod; - - const PaymentMethodDropdown({ - super.key, - required this.methods, - required this.onChanged, - this.selectedMethod, - }); - - @override - Widget build(BuildContext context) => DropdownButtonFormField( - dropdownColor: Theme.of(context).colorScheme.onSecondary, - initialValue: _getSelectedMethod(), - decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.whereGetMoney, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - ), - items: methods.map((method) => DropdownMenuItem( - value: method, - child: Row( - children: [ - Icon(iconForPaymentType(PaymentType.managedWallet), size: 20), - const SizedBox(width: 8), - Text(method.name), - ], - ), - )).toList(), - onChanged: (value) { - if (value != null) { - onChanged(value); - } - }, - ); - - Wallet? _getSelectedMethod() { - if (selectedMethod != null) return selectedMethod; - if (methods.isEmpty) return null; - return methods.first; - } -} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/selector.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector.dart similarity index 55% rename from frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/selector.dart rename to frontend/pweb/lib/widgets/payment/source_wallet_selector.dart index 0f89dd13..a004bc77 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/selector.dart +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector.dart @@ -1,31 +1,30 @@ import 'package:flutter/material.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/utils/currency.dart'; -import 'package:pweb/controllers/multiple_payouts.dart'; - import 'package:pweb/generated/i18n/app_localizations.dart'; class SourceWalletSelector extends StatelessWidget { const SourceWalletSelector({ super.key, - required this.controller, required this.walletsController, - required this.theme, - required this.l10n, + this.isBusy = false, + this.onChanged, }); - final MultiplePayoutsController controller; final WalletsController walletsController; - final ThemeData theme; - final AppLocalizations l10n; + final bool isBusy; + final ValueChanged? onChanged; @override Widget build(BuildContext context) { final wallets = walletsController.wallets; final selectedWalletRef = walletsController.selectedWalletRef; + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; if (wallets.isEmpty) { return Text(l10n.noWalletsAvailable, style: theme.textTheme.bodySmall); @@ -47,18 +46,43 @@ class SourceWalletSelector extends StatelessWidget { (wallet) => DropdownMenuItem( value: wallet.id, child: Text( - '${wallet.name} - ${amountToString(wallet.balance)} ${currencyCodeToString(wallet.currency)}', + '${_walletLabel(wallet)} - ${currencyCodeToSymbol(wallet.currency)} ${amountToString(wallet.balance)}', overflow: TextOverflow.ellipsis, ), ), ) .toList(growable: false), - onChanged: controller.isBusy + onChanged: isBusy ? null : (value) { if (value == null) return; walletsController.selectWalletByRef(value); + final selected = walletsController.selectedWallet; + if (selected != null) { + onChanged?.call(selected); + } }, ); } + + String _walletLabel(Wallet wallet) { + final description = wallet.description?.trim(); + if (description != null && description.isNotEmpty) { + return description; + } + final name = wallet.name.trim(); + if (name.isNotEmpty && !_looksLikeId(name)) { + return name; + } + final token = wallet.tokenSymbol?.trim(); + if (token != null && token.isNotEmpty) { + return '$token wallet'; + } + return '${currencyCodeToString(wallet.currency)} wallet'; + } + + bool _looksLikeId(String value) { + return RegExp(r'^[a-f0-9]{12,}$', caseSensitive: false) + .hasMatch(value); + } }