redisign multiple payouts for better ux and small fixes #485
@@ -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",
|
||||||
|
|||||||
@@ -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": "Смотреть всю историю",
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
child: Text(l10n.changeFile),
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: controller.canSend ? onSend : null,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: _buttonHorizontalPadding,
|
|
||||||
vertical: _buttonVerticalPadding,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(l10n.send),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,84 +11,68 @@ 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(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
child: DottedBorder(
|
||||||
|
options: RoundedRectDottedBorderOptions(
|
||||||
|
radius: Radius.circular(_panelRadius),
|
||||||
|
dashPattern: const [8, 5],
|
||||||
|
strokeWidth: 1.4,
|
||||||
|
color: borderColor,
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: surfaceColor,
|
||||||
|
child: InkWell(
|
||||||
onTap: controller.isBusy
|
onTap: controller.isBusy
|
||||||
? null
|
? null
|
||||||
: () => controller.pickAndQuote(),
|
: () => controller.pickAndQuote(),
|
||||||
borderRadius: BorderRadius.circular(_panelRadius),
|
borderRadius: borderRadius,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
|
child: Padding(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(
|
||||||
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
horizontal: 18,
|
||||||
alpha: 0.5,
|
vertical: 16,
|
||||||
),
|
|
||||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
||||||
borderRadius: BorderRadius.circular(_panelRadius),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.upload_file,
|
Icons.upload_file,
|
||||||
size: 34,
|
size: 34,
|
||||||
color: theme.colorScheme.primary,
|
color: iconColor,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
hasFile ? controller.selectedFileName! : l10n.uploadCSV,
|
hasFile
|
||||||
|
? controller.selectedFileName!
|
||||||
|
: l10n.uploadCSV,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
if (!hasFile) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 14,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.touch_app,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
l10n.upload,
|
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
l10n.hintUpload,
|
l10n.hintUpload,
|
||||||
@@ -98,13 +84,17 @@ class UploadDropZone extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'${l10n.payout}: ${controller.rows.length}',
|
'${l10n.payout}: ${controller.rows.length}',
|
||||||
style: theme.textTheme.labelMedium?.copyWith(
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
if (hasFile) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
UploadPanelActions(
|
UploadPanelActions(controller: controller),
|
||||||
controller: controller,
|
],
|
||||||
l10n: l10n,
|
UploadPanelStatus(controller: controller),
|
||||||
onSend: () => handleUploadSend(context, controller),
|
|
||||||
),
|
|
||||||
UploadPanelStatus(controller: controller, theme: theme, l10n: l10n),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,21 +28,26 @@ 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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (hasFile) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SourceQuotePanel(
|
SourceQuotePanel(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
walletsController: walletsController,
|
walletsController: walletsController,
|
||||||
theme: theme,
|
|
||||||
l10n: l10n,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFile) {
|
||||||
|
return PanelCard(
|
||||||
|
child: UploadPanel(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user