redisign multiple payouts for better ux and small fixes #485

Merged
tech merged 1 commits from SEND051 into main 2026-02-12 17:03:54 +00:00
19 changed files with 226 additions and 262 deletions

View File

@@ -348,7 +348,7 @@
"description": "Table column header for row count" "description": "Table column header for row count"
}, },
"amountColumn": "Amount", "amountColumn": "Amount",
"@amountColumn": { "@amountColumn": {
"description": "Table column header for the original amount" "description": "Table column header for the original amount"
}, },
@@ -515,6 +515,7 @@
"comment": "Comment", "comment": "Comment",
"uploadCSV": "Upload your CSV", "uploadCSV": "Upload your CSV",
"upload": "Upload", "upload": "Upload",
"changeFile": "Change file",
"hintUpload": "Supported format: .CSV · Max size 1 MB", "hintUpload": "Supported format: .CSV · Max size 1 MB",
"uploadHistory": "Upload History", "uploadHistory": "Upload History",
"viewWholeHistory": "View Whole History", "viewWholeHistory": "View Whole History",

View File

@@ -348,7 +348,7 @@
"description": "Заголовок столбца таблицы для количества строк" "description": "Заголовок столбца таблицы для количества строк"
}, },
"amountColumn": "Сумма", "amountColumn": "Сумма",
"@amountColumn": { "@amountColumn": {
"description": "Заголовок столбца таблицы для исходной суммы" "description": "Заголовок столбца таблицы для исходной суммы"
}, },
@@ -515,6 +515,7 @@
"comment": "Комментарий", "comment": "Комментарий",
"uploadCSV": "Загрузите ваш CSV", "uploadCSV": "Загрузите ваш CSV",
"upload": "Загрузить", "upload": "Загрузить",
"changeFile": "Заменить файл",
"hintUpload": "Поддерживаемый формат: .CSV · Макс. размер 1 МБ", "hintUpload": "Поддерживаемый формат: .CSV · Макс. размер 1 МБ",
"uploadHistory": "История загрузок", "uploadHistory": "История загрузок",
"viewWholeHistory": "Смотреть всю историю", "viewWholeHistory": "Смотреть всю историю",

View File

@@ -4,7 +4,7 @@ import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart'; import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
Future<void> handleUploadSend( Future<void> handleMultiplePayoutSend(
BuildContext context, BuildContext context,
MultiplePayoutsController controller, MultiplePayoutsController controller,
) async { ) async {

View File

@@ -6,15 +6,12 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class SourceQuotePanelHeader extends StatelessWidget { class SourceQuotePanelHeader extends StatelessWidget {
const SourceQuotePanelHeader({ const SourceQuotePanelHeader({
super.key, super.key,
required this.theme,
required this.l10n,
}); });
final ThemeData theme;
final AppLocalizations l10n;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
return Text( return Text(
l10n.sourceOfFunds, l10n.sourceOfFunds,
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(

View File

@@ -3,32 +3,29 @@ import 'package:flutter/material.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pweb/controllers/multiple_payouts.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/header.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.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/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'; import 'package:pweb/generated/i18n/app_localizations.dart';
class SourceQuotePanel extends StatelessWidget { class SourceQuotePanel extends StatelessWidget {
const SourceQuotePanel({ const SourceQuotePanel({
super.key, super.key,
required this.controller, required this.controller,
required this.walletsController, required this.walletsController,
required this.theme,
required this.l10n,
}); });
final MultiplePayoutsController controller; final MultiplePayoutsController controller;
final WalletsController walletsController; final WalletsController walletsController;
final ThemeData theme;
final AppLocalizations l10n;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@@ -40,13 +37,11 @@ class SourceQuotePanel extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SourceQuotePanelHeader(theme: theme, l10n: l10n), SourceQuotePanelHeader(),
const SizedBox(height: 8), const SizedBox(height: 8),
SourceWalletSelector( SourceWalletSelector(
controller: controller,
walletsController: walletsController, walletsController: walletsController,
theme: theme, isBusy: controller.isBusy,
l10n: l10n,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
const Divider(height: 1), const Divider(height: 1),
@@ -54,6 +49,26 @@ class SourceQuotePanel extends StatelessWidget {
SourceQuoteSummary(controller: controller, spacing: 12), SourceQuoteSummary(controller: controller, spacing: 12),
const SizedBox(height: 12), const SizedBox(height: 12),
MultipleQuoteStatusCard(controller: controller), 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),
),
),
], ],
), ),
); );

View File

@@ -9,49 +9,26 @@ class UploadPanelActions extends StatelessWidget {
const UploadPanelActions({ const UploadPanelActions({
super.key, super.key,
required this.controller, required this.controller,
required this.l10n,
required this.onSend,
}); });
final MultiplePayoutsController controller; final MultiplePayoutsController controller;
final AppLocalizations l10n;
final VoidCallback onSend;
static const double _buttonVerticalPadding = 12;
static const double _buttonHorizontalPadding = 24;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final hasFile = controller.selectedFileName != null; final hasFile = controller.selectedFileName != null;
return Wrap( if (!hasFile) return const SizedBox.shrink();
spacing: 8,
runSpacing: 8, return TextButton(
alignment: WrapAlignment.center, onPressed: controller.isBusy ? null : () => controller.pickAndQuote(),
children: [ style: TextButton.styleFrom(
OutlinedButton( padding: EdgeInsets.symmetric(
onPressed: !hasFile || controller.isBusy horizontal: 24,
? null vertical: 12,
: () => controller.removeUploadedFile(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: _buttonHorizontalPadding,
vertical: _buttonVerticalPadding,
),
),
child: Text(l10n.cancel),
), ),
ElevatedButton( ),
onPressed: controller.canSend ? onSend : null, child: Text(l10n.changeFile),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: _buttonHorizontalPadding,
vertical: _buttonVerticalPadding,
),
),
child: Text(l10n.send),
),
],
); );
} }
} }

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -9,100 +11,88 @@ class UploadDropZone extends StatelessWidget {
const UploadDropZone({ const UploadDropZone({
super.key, super.key,
required this.controller, required this.controller,
required this.theme,
required this.l10n,
}); });
final MultiplePayoutsController controller; final MultiplePayoutsController controller;
final ThemeData theme;
final AppLocalizations l10n;
static const double _panelRadius = 12; static const double _panelRadius = 12;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
final hasFile = controller.selectedFileName != null; 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( return ClipRRect(
onTap: controller.isBusy borderRadius: borderRadius,
? null child: DottedBorder(
: () => controller.pickAndQuote(), options: RoundedRectDottedBorderOptions(
borderRadius: BorderRadius.circular(_panelRadius), radius: Radius.circular(_panelRadius),
child: Container( dashPattern: const [8, 5],
width: double.infinity, strokeWidth: 1.4,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), color: borderColor,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withValues(
alpha: 0.5,
),
border: Border.all(color: theme.colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(_panelRadius),
), ),
child: Column( child: Material(
children: [ color: surfaceColor,
Icon( child: InkWell(
Icons.upload_file, onTap: controller.isBusy
size: 34, ? null
color: theme.colorScheme.primary, : () => controller.pickAndQuote(),
), borderRadius: borderRadius,
const SizedBox(height: 8), child: SizedBox(
Text( width: double.infinity,
hasFile ? controller.selectedFileName! : l10n.uploadCSV, child: Padding(
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
if (!hasFile) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 14, horizontal: 18,
vertical: 8, vertical: 16,
), ),
decoration: BoxDecoration( child: Column(
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,
children: [ children: [
Icon( Icon(
Icons.touch_app, Icons.upload_file,
size: 16, size: 34,
color: theme.colorScheme.primary, color: iconColor,
), ),
const SizedBox(width: 6), const SizedBox(height: 8),
Text( Text(
l10n.upload, hasFile
style: theme.textTheme.labelLarge?.copyWith( ? controller.selectedFileName!
color: theme.colorScheme.primary, : l10n.uploadCSV,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, 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,
),
),
],
],
), ),
), ),
); );

View File

@@ -5,14 +5,13 @@ class UploadQuoteProgress extends StatelessWidget {
const UploadQuoteProgress({ const UploadQuoteProgress({
super.key, super.key,
required this.isQuoting, required this.isQuoting,
required this.theme,
}); });
final bool isQuoting; final bool isQuoting;
final ThemeData theme;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
if (!isQuoting) return const SizedBox.shrink(); if (!isQuoting) return const SizedBox.shrink();
return Column( return Column(

View File

@@ -9,16 +9,15 @@ class UploadPanelStatus extends StatelessWidget {
const UploadPanelStatus({ const UploadPanelStatus({
super.key, super.key,
required this.controller, required this.controller,
required this.theme,
required this.l10n,
}); });
final MultiplePayoutsController controller; final MultiplePayoutsController controller;
final ThemeData theme;
final AppLocalizations l10n;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
if (controller.sentCount <= 0 && controller.error == null) { if (controller.sentCount <= 0 && controller.error == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }

View File

@@ -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/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/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/status.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/progress.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 { class UploadPanel extends StatelessWidget {
const UploadPanel({ const UploadPanel({
super.key, super.key,
required this.controller, required this.controller,
required this.theme,
required this.l10n,
}); });
final MultiplePayoutsController controller; final MultiplePayoutsController controller;
final ThemeData theme;
final AppLocalizations l10n;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasFile = controller.selectedFileName != null;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
UploadDropZone(controller: controller, theme: theme, l10n: l10n), UploadDropZone(controller: controller),
UploadQuoteProgress(isQuoting: controller.isQuoting, theme: theme), UploadQuoteProgress(isQuoting: controller.isQuoting),
const SizedBox(height: 12), if (hasFile) ...[
UploadPanelActions( const SizedBox(height: 12),
controller: controller, UploadPanelActions(controller: controller),
l10n: l10n, ],
onSend: () => handleUploadSend(context, controller), UploadPanelStatus(controller: controller),
),
UploadPanelStatus(controller: controller, theme: theme, l10n: l10n),
], ],
); );
} }

View File

@@ -24,7 +24,7 @@ class FileFormatSampleTable extends StatelessWidget {
DataColumn(label: Text(l10n.firstName)), DataColumn(label: Text(l10n.firstName)),
DataColumn(label: Text(l10n.lastName)), DataColumn(label: Text(l10n.lastName)),
DataColumn(label: Text(l10n.expiryDate)), DataColumn(label: Text(l10n.expiryDate)),
DataColumn(label: Text(l10n.amount)), DataColumn(label: Text(l10n.amountColumn)),
], ],
rows: rows.map((row) { rows: rows.map((row) {
return DataRow( return DataRow(

View File

@@ -2,20 +2,19 @@ import 'package:flutter/material.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class UploadCsvHeader extends StatelessWidget { class UploadCsvHeader extends StatelessWidget {
const UploadCsvHeader({ const UploadCsvHeader({
super.key, super.key,
required this.theme, required this.theme,
required this.l10n,
}); });
final ThemeData theme; final ThemeData theme;
final AppLocalizations l10n;
static const double _iconTextSpacing = 5; static const double _iconTextSpacing = 5;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Row( return Row(
children: [ children: [
const Icon(Icons.upload), const Icon(Icons.upload),

View File

@@ -3,11 +3,8 @@ import 'package:flutter/material.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pweb/controllers/multiple_payouts.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/source_quote/widget.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/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'; import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart';
@@ -16,17 +13,14 @@ class UploadCsvLayout extends StatelessWidget {
super.key, super.key,
required this.controller, required this.controller,
required this.walletsController, required this.walletsController,
required this.theme,
required this.l10n,
}); });
final MultiplePayoutsController controller; final MultiplePayoutsController controller;
final WalletsController walletsController; final WalletsController walletsController;
final ThemeData theme;
final AppLocalizations l10n;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasFile = controller.selectedFileName != null;
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final useHorizontal = constraints.maxWidth >= 760; final useHorizontal = constraints.maxWidth >= 760;
@@ -34,24 +28,29 @@ class UploadCsvLayout extends StatelessWidget {
return Column( return Column(
children: [ children: [
PanelCard( PanelCard(
theme: theme,
child: UploadPanel( child: UploadPanel(
controller: controller, controller: controller,
theme: theme,
l10n: l10n,
), ),
), ),
const SizedBox(height: 12), if (hasFile) ...[
SourceQuotePanel( const SizedBox(height: 12),
controller: controller, SourceQuotePanel(
walletsController: walletsController, controller: controller,
theme: theme, walletsController: walletsController,
l10n: l10n, ),
), ],
], ],
); );
} }
if (!hasFile) {
return PanelCard(
child: UploadPanel(
controller: controller,
),
);
}
return IntrinsicHeight( return IntrinsicHeight(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -59,11 +58,8 @@ class UploadCsvLayout extends StatelessWidget {
Expanded( Expanded(
flex: 3, flex: 3,
child: PanelCard( child: PanelCard(
theme: theme,
child: UploadPanel( child: UploadPanel(
controller: controller, controller: controller,
theme: theme,
l10n: l10n,
), ),
), ),
), ),
@@ -73,8 +69,6 @@ class UploadCsvLayout extends StatelessWidget {
child: SourceQuotePanel( child: SourceQuotePanel(
controller: controller, controller: controller,
walletsController: walletsController, walletsController: walletsController,
theme: theme,
l10n: l10n,
), ),
), ),
], ],

View File

@@ -4,11 +4,9 @@ import 'package:flutter/material.dart';
class PanelCard extends StatelessWidget { class PanelCard extends StatelessWidget {
const PanelCard({ const PanelCard({
super.key, super.key,
required this.theme,
required this.child, required this.child,
}); });
final ThemeData theme;
final Widget child; final Widget child;
@override @override
@@ -16,11 +14,6 @@ class PanelCard extends StatelessWidget {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
border: Border.all(color: theme.colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(8),
),
child: child, child: child,
); );
} }

View File

@@ -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/header.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/layout.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 { class UploadCSVSection extends StatelessWidget {
const UploadCSVSection({super.key}); const UploadCSVSection({super.key});
@@ -18,18 +16,15 @@ class UploadCSVSection extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = context.watch<MultiplePayoutsController>(); final controller = context.watch<MultiplePayoutsController>();
final theme = Theme.of(context); final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
UploadCsvHeader(theme: theme, l10n: l10n), UploadCsvHeader(theme: theme),
const SizedBox(height: _verticalSpacing), const SizedBox(height: _verticalSpacing),
UploadCsvLayout( UploadCsvLayout(
controller: controller, controller: controller,
walletsController: context.watch(), walletsController: context.watch(),
theme: theme,
l10n: l10n,
), ),
], ],
); );

View File

@@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/wallet.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 { class PaymentMethodSelector extends StatelessWidget {
@@ -18,9 +18,8 @@ class PaymentMethodSelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Consumer<WalletsController>( Widget build(BuildContext context) => Consumer<WalletsController>(
builder: (context, provider, _) => PaymentMethodDropdown( builder: (context, provider, _) => SourceWalletSelector(
methods: provider.wallets, walletsController: provider,
selectedMethod: provider.selectedWallet,
onChanged: onMethodChanged, onChanged: onMethodChanged,
), ),
); );

View File

@@ -19,17 +19,11 @@ class WebCsvInputService implements CsvInputService {
Future<PickedCsvFile?> pickCsv() async { Future<PickedCsvFile?> pickCsv() async {
final input = html.FileUploadInputElement() final input = html.FileUploadInputElement()
..accept = '.csv,text/csv' ..accept = '.csv,text/csv'
..multiple = false; ..multiple = false
..style.display = 'none';
final completer = Completer<html.File?>(); html.document.body?.append(input);
input.onChange.listen((_) { final file = await _pickFile(input);
completer.complete(
input.files?.isNotEmpty == true ? input.files!.first : null,
);
});
input.click();
final file = await completer.future;
if (file == null) return null; if (file == null) return null;
final reader = html.FileReader(); final reader = html.FileReader();
@@ -50,4 +44,52 @@ class WebCsvInputService implements CsvInputService {
final content = await readCompleter.future; final content = await readCompleter.future;
return PickedCsvFile(name: file.name, content: content); return PickedCsvFile(name: file.name, content: content);
} }
Future<html.File?> _pickFile(html.FileUploadInputElement input) async {
final completer = Completer<html.File?>();
void completeWith(html.File? file) {
if (!completer.isCompleted) completer.complete(file);
}
StreamSubscription<html.Event>? changeSub;
StreamSubscription<html.Event>? inputSub;
StreamSubscription<html.Event>? 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<void>.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;
}
} }

View File

@@ -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<Wallet> methods;
final ValueChanged<Wallet> onChanged;
final Wallet? selectedMethod;
const PaymentMethodDropdown({
super.key,
required this.methods,
required this.onChanged,
this.selectedMethod,
});
@override
Widget build(BuildContext context) => DropdownButtonFormField<Wallet>(
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<Wallet>(
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;
}
}

View File

@@ -1,31 +1,30 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/currency.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class SourceWalletSelector extends StatelessWidget { class SourceWalletSelector extends StatelessWidget {
const SourceWalletSelector({ const SourceWalletSelector({
super.key, super.key,
required this.controller,
required this.walletsController, required this.walletsController,
required this.theme, this.isBusy = false,
required this.l10n, this.onChanged,
}); });
final MultiplePayoutsController controller;
final WalletsController walletsController; final WalletsController walletsController;
final ThemeData theme; final bool isBusy;
final AppLocalizations l10n; final ValueChanged<Wallet>? onChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final wallets = walletsController.wallets; final wallets = walletsController.wallets;
final selectedWalletRef = walletsController.selectedWalletRef; final selectedWalletRef = walletsController.selectedWalletRef;
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
if (wallets.isEmpty) { if (wallets.isEmpty) {
return Text(l10n.noWalletsAvailable, style: theme.textTheme.bodySmall); return Text(l10n.noWalletsAvailable, style: theme.textTheme.bodySmall);
@@ -47,18 +46,43 @@ class SourceWalletSelector extends StatelessWidget {
(wallet) => DropdownMenuItem<String>( (wallet) => DropdownMenuItem<String>(
value: wallet.id, value: wallet.id,
child: Text( child: Text(
'${wallet.name} - ${amountToString(wallet.balance)} ${currencyCodeToString(wallet.currency)}', '${_walletLabel(wallet)} - ${currencyCodeToSymbol(wallet.currency)} ${amountToString(wallet.balance)}',
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
) )
.toList(growable: false), .toList(growable: false),
onChanged: controller.isBusy onChanged: isBusy
? null ? null
: (value) { : (value) {
if (value == null) return; if (value == null) return;
walletsController.selectWalletByRef(value); 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);
}
} }