redisign multiple payouts for better ux and small fixes

This commit is contained in:
Arseni
2026-02-12 18:48:57 +03:00
parent ea68d161d6
commit 45d3c3145c
19 changed files with 226 additions and 262 deletions

View File

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

View File

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

View File

@@ -1,64 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/controllers/balance_mask/wallets.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,
});
final MultiplePayoutsController controller;
final WalletsController walletsController;
final ThemeData theme;
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
final wallets = walletsController.wallets;
final selectedWalletRef = walletsController.selectedWalletRef;
if (wallets.isEmpty) {
return Text(l10n.noWalletsAvailable, style: theme.textTheme.bodySmall);
}
return DropdownButtonFormField<String>(
initialValue: selectedWalletRef,
isExpanded: true,
decoration: InputDecoration(
labelText: l10n.whereGetMoney,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
items: wallets
.map(
(wallet) => DropdownMenuItem<String>(
value: wallet.id,
child: Text(
'${wallet.name} - ${amountToString(wallet.balance)} ${currencyCodeToString(wallet.currency)}',
overflow: TextOverflow.ellipsis,
),
),
)
.toList(growable: false),
onChanged: controller.isBusy
? null
: (value) {
if (value == null) return;
walletsController.selectWalletByRef(value);
},
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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/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<MultiplePayoutsController>();
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,
),
],
);

View File

@@ -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<WalletsController>(
builder: (context, provider, _) => PaymentMethodDropdown(
methods: provider.wallets,
selectedMethod: provider.selectedWallet,
builder: (context, provider, _) => SourceWalletSelector(
walletsController: provider,
onChanged: onMethodChanged,
),
);