Frontend first draft
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class BalanceAddFunds extends StatelessWidget {
|
||||
final VoidCallback onTopUp;
|
||||
|
||||
const BalanceAddFunds({
|
||||
super.key,
|
||||
required this.onTopUp,
|
||||
});
|
||||
|
||||
static const double _borderRadius = 5.0;
|
||||
static const double _iconSize = 24.0;
|
||||
static const double _paddingVertical = 2.0;
|
||||
static const double _spacingSmall = 3.0;
|
||||
static const double _spacingMedium = 5.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return InkWell(
|
||||
onTap: onTopUp,
|
||||
borderRadius: BorderRadius.circular(_borderRadius),
|
||||
hoverColor: colorScheme.primaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: _paddingVertical),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: _spacingSmall),
|
||||
Icon(
|
||||
Icons.add_circle,
|
||||
color: colorScheme.primary,
|
||||
size: _iconSize,
|
||||
),
|
||||
const SizedBox(width: _spacingMedium),
|
||||
Text(
|
||||
'Add funds',
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _spacingSmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pweb/models/wallet.dart';
|
||||
import 'package:pweb/utils/currency.dart';
|
||||
|
||||
|
||||
class BalanceAmount extends StatelessWidget {
|
||||
final Wallet wallet;
|
||||
final VoidCallback onToggleVisibility;
|
||||
|
||||
const BalanceAmount({
|
||||
super.key,
|
||||
required this.wallet,
|
||||
required this.onToggleVisibility,
|
||||
});
|
||||
|
||||
static const double _iconSpacing = 12.0;
|
||||
static const double _iconSize = 24.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final currencyBalance = currencyCodeToSymbol(wallet.currency);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
wallet.isHidden ? '•••• $currencyBalance' : '${wallet.balance.toStringAsFixed(2)} $currencyBalance',
|
||||
style: textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _iconSpacing),
|
||||
GestureDetector(
|
||||
onTap: onToggleVisibility,
|
||||
child: Icon(
|
||||
wallet.isHidden ? Icons.visibility_off : Icons.visibility,
|
||||
size: _iconSize,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart';
|
||||
import 'package:pweb/providers/wallets.dart';
|
||||
|
||||
|
||||
class BalanceWidget extends StatelessWidget {
|
||||
const BalanceWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final walletsProvider = context.watch<WalletsProvider>();
|
||||
|
||||
if (walletsProvider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final wallets = walletsProvider.wallets;
|
||||
|
||||
if (wallets == null || wallets.isEmpty) {
|
||||
return const Center(child: Text('No wallets available'));
|
||||
}
|
||||
|
||||
return
|
||||
WalletCarousel(
|
||||
wallets: wallets,
|
||||
onWalletChanged: walletsProvider.selectWallet,
|
||||
);
|
||||
}
|
||||
}
|
||||
54
frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart
Normal file
54
frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/models/wallet.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||
import 'package:pweb/providers/wallets.dart';
|
||||
|
||||
|
||||
class WalletCard extends StatelessWidget {
|
||||
final Wallet wallet;
|
||||
|
||||
const WalletCard({
|
||||
super.key,
|
||||
required this.wallet,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
elevation: WalletCardConfig.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
),
|
||||
child: Padding(
|
||||
padding: WalletCardConfig.contentPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BalanceHeader(
|
||||
walletName: wallet.name,
|
||||
walletId: wallet.walletUserID,
|
||||
),
|
||||
BalanceAmount(
|
||||
wallet: wallet,
|
||||
onToggleVisibility: () {
|
||||
context.read<WalletsProvider>().toggleVisibility(wallet.id);
|
||||
},
|
||||
),
|
||||
BalanceAddFunds(
|
||||
onTopUp: () {
|
||||
// TODO: Implement top-up functionality
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
113
frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart
Normal file
113
frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/models/wallet.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/card.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart';
|
||||
import 'package:pweb/providers/carousel.dart';
|
||||
|
||||
|
||||
class WalletCarousel extends StatefulWidget {
|
||||
final List<Wallet> wallets;
|
||||
final ValueChanged<Wallet> onWalletChanged;
|
||||
|
||||
const WalletCarousel({
|
||||
super.key,
|
||||
required this.wallets,
|
||||
required this.onWalletChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WalletCarousel> createState() => _WalletCarouselState();
|
||||
}
|
||||
|
||||
class _WalletCarouselState extends State<WalletCarousel> {
|
||||
late final PageController _pageController;
|
||||
int _currentPage = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController(
|
||||
viewportFraction: WalletCardConfig.viewportFraction,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onPageChanged(int index) {
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
context.read<CarouselIndexProvider>().updateIndex(index);
|
||||
widget.onWalletChanged(widget.wallets[index]);
|
||||
}
|
||||
|
||||
void _goToPreviousPage() {
|
||||
if (_currentPage > 0) {
|
||||
_pageController.animateToPage(
|
||||
_currentPage - 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _goToNextPage() {
|
||||
if (_currentPage < widget.wallets.length - 1) {
|
||||
_pageController.animateToPage(
|
||||
_currentPage + 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: WalletCardConfig.cardHeight,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.wallets.length,
|
||||
onPageChanged: _onPageChanged,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: WalletCardConfig.cardPadding,
|
||||
child: WalletCard(wallet: widget.wallets[index]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _currentPage > 0 ? _goToPreviousPage : null,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
CarouselIndicator(itemCount: widget.wallets.length),
|
||||
const SizedBox(width: 16),
|
||||
IconButton(
|
||||
onPressed: _currentPage < widget.wallets.length - 1
|
||||
? _goToNextPage
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
abstract class WalletCardConfig {
|
||||
static const double cardHeight = 130.0;
|
||||
static const double elevation = 4.0;
|
||||
static const double borderRadius = 16.0;
|
||||
static const double viewportFraction = 0.9;
|
||||
|
||||
static const EdgeInsets cardPadding = EdgeInsets.symmetric(horizontal: 8);
|
||||
static const EdgeInsets contentPadding = EdgeInsets.all(16);
|
||||
|
||||
static const double dotSize = 8.0;
|
||||
static const EdgeInsets dotMargin = EdgeInsets.symmetric(horizontal: 4);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class BalanceHeader extends StatelessWidget {
|
||||
final String walletName;
|
||||
final String walletId;
|
||||
|
||||
const BalanceHeader({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const double _spacing = 8.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
walletName,
|
||||
style: textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _spacing),
|
||||
Text(
|
||||
walletId,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
|
||||
import 'package:pweb/providers/carousel.dart';
|
||||
|
||||
|
||||
class CarouselIndicator extends StatelessWidget {
|
||||
final int itemCount;
|
||||
|
||||
const CarouselIndicator({
|
||||
super.key,
|
||||
required this.itemCount,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentIndex = context.watch<CarouselIndexProvider>().currentIndex;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
itemCount,
|
||||
(index) => _Dot(isActive: currentIndex == index),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Dot extends StatelessWidget {
|
||||
final bool isActive;
|
||||
|
||||
const _Dot({required this.isActive});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: WalletCardConfig.dotSize,
|
||||
height: WalletCardConfig.dotSize,
|
||||
margin: WalletCardConfig.dotMargin,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isActive
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.primary.withAlpha(60),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
68
frontend/pweb/lib/pages/dashboard/buttons/buttons.dart
Normal file
68
frontend/pweb/lib/pages/dashboard/buttons/buttons.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class TransactionRefButton extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
final bool isActive;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
|
||||
const TransactionRefButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.isActive,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
static const double _horizontalPadding = 10.0;
|
||||
static const double _verticalPadding = 5.0;
|
||||
static const double _iconSize = 24.0;
|
||||
static const double _spacing = 10.0;
|
||||
static const double _borderRadius = 12.0;
|
||||
static const FontWeight _fontWeight = FontWeight.w400;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).colorScheme;
|
||||
|
||||
final backgroundColor = isActive ? theme.primary : theme.onSecondary;
|
||||
final foregroundColor = isActive ? theme.onPrimary : theme.onPrimaryContainer;
|
||||
final hoverColor = isActive ? theme.primary : theme.secondaryContainer;
|
||||
|
||||
return Material(
|
||||
color: backgroundColor,
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(_borderRadius),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(_borderRadius),
|
||||
hoverColor: hoverColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: _horizontalPadding,
|
||||
vertical: _verticalPadding,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: _fontWeight,
|
||||
color: foregroundColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _spacing),
|
||||
Icon(
|
||||
icon,
|
||||
color: foregroundColor,
|
||||
size: _iconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
92
frontend/pweb/lib/pages/dashboard/dashboard.dart
Normal file
92
frontend/pweb/lib/pages/dashboard/dashboard.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/balance.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/buttons.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/multiple/title.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/multiple/widget.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/widget.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AppSpacing {
|
||||
static const double small = 10;
|
||||
static const double medium = 16;
|
||||
static const double large = 20;
|
||||
}
|
||||
|
||||
class DashboardPage extends StatefulWidget {
|
||||
final ValueChanged<Recipient> onRecipientSelected;
|
||||
final void Function(PaymentType type) onGoToPaymentWithoutRecipient;
|
||||
|
||||
const DashboardPage({
|
||||
super.key,
|
||||
required this.onRecipientSelected,
|
||||
required this.onGoToPaymentWithoutRecipient,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardPage> createState() => _DashboardPageState();
|
||||
}
|
||||
|
||||
class _DashboardPageState extends State<DashboardPage> {
|
||||
bool _showContainerSingle = true;
|
||||
bool _showContainerMultiple = false;
|
||||
|
||||
void _setActive(bool single) {
|
||||
setState(() {
|
||||
_showContainerSingle = single;
|
||||
_showContainerMultiple = !single;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: TransactionRefButton(
|
||||
onTap: () => _setActive(true),
|
||||
isActive: _showContainerSingle,
|
||||
label: AppLocalizations.of(context)!.sendSingle,
|
||||
icon: Icons.person_add,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.small),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: TransactionRefButton(
|
||||
onTap: () => _setActive(false),
|
||||
isActive: _showContainerMultiple,
|
||||
label: AppLocalizations.of(context)!.sendMultiple,
|
||||
icon: Icons.group_add,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.medium),
|
||||
BalanceWidget(),
|
||||
const SizedBox(height: AppSpacing.small),
|
||||
if (_showContainerMultiple) TitleMultiplePayout(),
|
||||
const SizedBox(height: AppSpacing.medium),
|
||||
if (_showContainerSingle)
|
||||
SinglePayoutForm(
|
||||
onRecipientSelected: widget.onRecipientSelected,
|
||||
onGoToPayment: widget.onGoToPaymentWithoutRecipient,
|
||||
),
|
||||
if (_showContainerMultiple) MultiplePayoutForm(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
frontend/pweb/lib/pages/dashboard/organization/button.dart
Normal file
12
frontend/pweb/lib/pages/dashboard/organization/button.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class OrganizationButton extends StatelessWidget {
|
||||
const OrganizationButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => IconButton(
|
||||
icon: Icon(Icons.person),
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
70
frontend/pweb/lib/pages/dashboard/payouts/multiple/csv.dart
Normal file
70
frontend/pweb/lib/pages/dashboard/payouts/multiple/csv.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class UploadCSVSection extends StatelessWidget {
|
||||
const UploadCSVSection({super.key});
|
||||
|
||||
static const double _verticalSpacing = 10;
|
||||
static const double _iconTextSpacing = 5;
|
||||
static const double _buttonVerticalPadding = 12;
|
||||
static const double _buttonHorizontalPadding = 24;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.upload),
|
||||
const SizedBox(width: _iconTextSpacing),
|
||||
Text(
|
||||
l10n.uploadCSV,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: _verticalSpacing),
|
||||
Container(
|
||||
height: 140,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.upload_file, size: 36, color: theme.colorScheme.primary),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: _buttonHorizontalPadding,
|
||||
vertical: _buttonVerticalPadding,
|
||||
),
|
||||
),
|
||||
child: Text(l10n.upload),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.hintUpload,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
13
frontend/pweb/lib/pages/dashboard/payouts/multiple/form.dart
Normal file
13
frontend/pweb/lib/pages/dashboard/payouts/multiple/form.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
class MultiplePayoutRow {
|
||||
final String token;
|
||||
final String amount;
|
||||
final String currency;
|
||||
final String comment;
|
||||
|
||||
const MultiplePayoutRow({
|
||||
required this.token,
|
||||
required this.amount,
|
||||
required this.currency,
|
||||
required this.comment,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
import 'package:pweb/providers/upload_history.dart';
|
||||
|
||||
|
||||
class UploadHistorySection extends StatelessWidget {
|
||||
const UploadHistorySection({super.key});
|
||||
|
||||
static const double _smallBox = 5;
|
||||
static const double _radius = 6;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<UploadHistoryProvider>();
|
||||
final theme = Theme.of(context);
|
||||
final l10 = AppLocalizations.of(context)!;
|
||||
|
||||
|
||||
if (provider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (provider.error != null) {
|
||||
return Text("Error: ${provider.error}");
|
||||
}
|
||||
final items = provider.data ?? [];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.history),
|
||||
const SizedBox(width: _smallBox),
|
||||
Text(l10.uploadHistory, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
),
|
||||
DataTable(
|
||||
columns: [
|
||||
DataColumn(label: Text(l10.fileNameColumn)),
|
||||
DataColumn(label: Text(l10.colStatus)),
|
||||
DataColumn(label: Text(l10.dateColumn)),
|
||||
DataColumn(label: Text(l10.details)),
|
||||
],
|
||||
rows: items.map((file) {
|
||||
final isError = file.status == "Error";
|
||||
final statusColor = isError ? Colors.red : Colors.green;
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text(file.name)),
|
||||
DataCell(Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
),
|
||||
child: Text(file.status, style: TextStyle(color: statusColor)),
|
||||
)),
|
||||
DataCell(Text(file.time)),
|
||||
DataCell(TextButton(onPressed: () {}, child: Text(l10.showDetails))),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/multiple/form.dart';
|
||||
|
||||
|
||||
class FileFormatSampleSection extends StatelessWidget {
|
||||
const FileFormatSampleSection({super.key});
|
||||
|
||||
static final List<MultiplePayoutRow> sampleRows = [
|
||||
MultiplePayoutRow(token: "d921...161", amount: "500", currency: "RUB", comment: "cashback001"),
|
||||
MultiplePayoutRow(token: "d921...162", amount: "100", currency: "USD", comment: "cashback002"),
|
||||
MultiplePayoutRow(token: "d921...163", amount: "120", currency: "EUR", comment: "cashback003"),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
final titleStyle = theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
final linkStyle = theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.filter_list),
|
||||
const SizedBox(width: 5),
|
||||
Text(l10n.exampleTitle, style: titleStyle),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDataTable(l10n),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
||||
child: Text(l10n.downloadSampleCSV, style: linkStyle),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataTable(AppLocalizations l10n) {
|
||||
return DataTable(
|
||||
columnSpacing: 20,
|
||||
columns: [
|
||||
DataColumn(label: Text(l10n.tokenColumn)),
|
||||
DataColumn(label: Text(l10n.amount)),
|
||||
DataColumn(label: Text(l10n.currency)),
|
||||
DataColumn(label: Text(l10n.comment)),
|
||||
],
|
||||
rows: sampleRows.map((row) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(row.token)),
|
||||
DataCell(Text(row.amount)),
|
||||
DataCell(Text(row.currency)),
|
||||
DataCell(Text(row.comment)),
|
||||
]);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class TitleMultiplePayout extends StatelessWidget {
|
||||
const TitleMultiplePayout({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.multiplePayout,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.howItWorks,
|
||||
style: theme.textTheme.bodyLarge!.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/multiple/csv.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/multiple/history.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/multiple/sample.dart';
|
||||
|
||||
|
||||
class MultiplePayoutForm extends StatelessWidget {
|
||||
const MultiplePayoutForm({super.key});
|
||||
|
||||
static const double _spacing = 12;
|
||||
static const double _bottomSpacing = 40;
|
||||
|
||||
static final List<Widget> _cards = const [
|
||||
FileFormatSampleSection(),
|
||||
UploadCSVSection(),
|
||||
UploadHistorySection(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
for (int i = 0; i < _cards.length; i++) ...[
|
||||
_StyledCard(child: _cards[i]),
|
||||
if (i < _cards.length - 1) const SizedBox(height: _spacing),
|
||||
],
|
||||
const SizedBox(height: _bottomSpacing),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StyledCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
const _StyledCard({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 4,
|
||||
color: theme.colorScheme.onSecondary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
92
frontend/pweb/lib/pages/dashboard/payouts/payment_form.dart
Normal file
92
frontend/pweb/lib/pages/dashboard/payouts/payment_form.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/providers/mock_payment.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentFormWidget extends StatelessWidget {
|
||||
const PaymentFormWidget({super.key});
|
||||
|
||||
static const double _smallSpacing = 5;
|
||||
static const double _mediumSpacing = 10;
|
||||
static const double _largeSpacing = 16;
|
||||
static const double _extraSpacing = 15;
|
||||
|
||||
String _formatAmount(double amount) => amount.toStringAsFixed(2);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<MockPaymentProvider>(context);
|
||||
final theme = Theme.of(context);
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(loc.details, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: _smallSpacing),
|
||||
|
||||
TextField(
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: loc.amount,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (val) {
|
||||
final parsed = double.tryParse(val.replaceAll(',', '.')) ?? 0.0;
|
||||
provider.setAmount(parsed);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: _mediumSpacing),
|
||||
|
||||
Row(
|
||||
spacing: _mediumSpacing,
|
||||
children: [
|
||||
Text(loc.recipientPaysFee, style: theme.textTheme.titleMedium),
|
||||
Switch(
|
||||
value: !provider.payerCoversFee,
|
||||
onChanged: (val) => provider.setPayerCoversFee(!val),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: _largeSpacing),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_SummaryRow(label: loc.sentAmount(_formatAmount(provider.amount)), style: theme.textTheme.titleMedium),
|
||||
_SummaryRow(label: loc.fee(_formatAmount(provider.fee)), style: theme.textTheme.titleMedium),
|
||||
_SummaryRow(label: loc.recipientWillReceive(_formatAmount(provider.recipientGets)), style: theme.textTheme.titleMedium),
|
||||
|
||||
const SizedBox(height: _extraSpacing),
|
||||
|
||||
_SummaryRow(
|
||||
label: loc.total(_formatAmount(provider.total)),
|
||||
style: theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SummaryRow extends StatelessWidget {
|
||||
final String label;
|
||||
final TextStyle? style;
|
||||
|
||||
const _SummaryRow({required this.label, this.style});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(label, style: style);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/utils/initials.dart';
|
||||
|
||||
|
||||
class RecipientAvatar extends StatelessWidget {
|
||||
final String name;
|
||||
final String? avatarUrl;
|
||||
final double avatarRadius;
|
||||
final TextStyle? nameStyle;
|
||||
final bool isVisible;
|
||||
|
||||
static const double _verticalSpacing = 5;
|
||||
|
||||
const RecipientAvatar({
|
||||
super.key,
|
||||
required this.name,
|
||||
this.avatarUrl,
|
||||
required this.avatarRadius,
|
||||
this.nameStyle,
|
||||
required this.isVisible,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = Theme.of(context).colorScheme.onPrimary;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: avatarRadius,
|
||||
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: avatarUrl == null
|
||||
? Text(
|
||||
getInitials(name),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: avatarRadius * 0.8,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: _verticalSpacing),
|
||||
if (isVisible)
|
||||
Text(
|
||||
name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: nameStyle ?? Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PaymentInfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const PaymentInfoRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.bodySmall),
|
||||
const SizedBox(width: 8),
|
||||
Text(value, style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
|
||||
|
||||
class RecipientItem extends StatelessWidget {
|
||||
final Recipient recipient;
|
||||
final VoidCallback onTap;
|
||||
|
||||
static const double _horizontalPadding = 16.0;
|
||||
static const double _verticalPadding = 8.0;
|
||||
static const double _avatarRadius = 20;
|
||||
static const double _spacingWidth = 12;
|
||||
|
||||
const RecipientItem({
|
||||
super.key,
|
||||
required this.recipient,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: _horizontalPadding,
|
||||
vertical: _verticalPadding,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: RecipientAvatar(
|
||||
isVisible: false,
|
||||
name: recipient.name,
|
||||
avatarUrl: recipient.avatarUrl,
|
||||
avatarRadius: _avatarRadius,
|
||||
nameStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
title: Text(recipient.name),
|
||||
subtitle: Text(recipient.email),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _spacingWidth),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (recipient.bank?.accountNumber.isNotEmpty == true)
|
||||
PaymentInfoRow(
|
||||
label: getPaymentTypeLabel(context, PaymentType.bankAccount),
|
||||
value: recipient.bank!.accountNumber,
|
||||
),
|
||||
if (recipient.card?.pan.isNotEmpty == true)
|
||||
PaymentInfoRow(
|
||||
label: getPaymentTypeLabel(context, PaymentType.card),
|
||||
value: recipient.card!.pan,
|
||||
),
|
||||
if (recipient.iban?.iban.isNotEmpty == true)
|
||||
PaymentInfoRow(
|
||||
label: getPaymentTypeLabel(context, PaymentType.iban),
|
||||
value: recipient.iban!.iban,
|
||||
),
|
||||
if (recipient.wallet?.walletId.isNotEmpty == true)
|
||||
PaymentInfoRow(
|
||||
label: getPaymentTypeLabel(context, PaymentType.wallet),
|
||||
value: recipient.wallet!.walletId,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/item.dart';
|
||||
|
||||
|
||||
class LongListAdressBookPayout extends StatelessWidget {
|
||||
final List<Recipient> filteredRecipients;
|
||||
final ValueChanged<Recipient>? onSelected;
|
||||
|
||||
const LongListAdressBookPayout({
|
||||
super.key,
|
||||
required this.filteredRecipients,
|
||||
this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: filteredRecipients.length,
|
||||
itemBuilder: (context, index) {
|
||||
final recipient = filteredRecipients[index];
|
||||
return RecipientItem(
|
||||
recipient: recipient,
|
||||
onTap: () => onSelected!(recipient),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
|
||||
|
||||
|
||||
class ShortListAdressBookPayout extends StatelessWidget {
|
||||
final List<Recipient> recipients;
|
||||
final ValueChanged<Recipient> onSelected;
|
||||
|
||||
const ShortListAdressBookPayout({
|
||||
super.key,
|
||||
required this.recipients,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
static const double _avatarRadius = 20;
|
||||
static const double _avatarSize = 80;
|
||||
static const EdgeInsets _padding = EdgeInsets.symmetric(horizontal: 10, vertical: 8);
|
||||
static const TextStyle _nameStyle = TextStyle(fontSize: 12);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: recipients.map((recipient) {
|
||||
return Padding(
|
||||
padding: _padding,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
onTap: () => onSelected(recipient),
|
||||
child: SizedBox(
|
||||
height: _avatarSize,
|
||||
width: _avatarSize,
|
||||
child: RecipientAvatar(
|
||||
isVisible: true,
|
||||
name: recipient.name,
|
||||
avatarUrl: recipient.avatarUrl,
|
||||
avatarRadius: _avatarRadius,
|
||||
nameStyle: _nameStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/page/search.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/short_list.dart';
|
||||
import 'package:pweb/providers/recipient.dart';
|
||||
|
||||
|
||||
class AdressBookPayout extends StatefulWidget {
|
||||
final ValueChanged<Recipient> onSelected;
|
||||
|
||||
const AdressBookPayout({
|
||||
super.key,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AdressBookPayout> createState() => _AdressBookPayoutState();
|
||||
}
|
||||
|
||||
class _AdressBookPayoutState extends State<AdressBookPayout> {
|
||||
static const double _expandedHeight = 400;
|
||||
static const double _collapsedHeight = 200;
|
||||
static const double _cardMargin = 1;
|
||||
static const double _paddingAll = 16;
|
||||
static const double _spacingBetween = 16;
|
||||
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
late final TextEditingController _searchController;
|
||||
|
||||
bool get _isExpanded => _searchFocusNode.hasFocus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final provider = context.read<RecipientProvider>();
|
||||
_searchController = TextEditingController(text: provider.query);
|
||||
|
||||
_searchController.addListener(() {
|
||||
provider.setQuery(_searchController.text);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<RecipientProvider>();
|
||||
|
||||
if (provider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (provider.error != null) {
|
||||
return Center(child: Text('Error: ${provider.error}'));
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: _isExpanded ? _expandedHeight : _collapsedHeight,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(_cardMargin),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 4,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(_paddingAll),
|
||||
child: Column(
|
||||
children: [
|
||||
RecipientSearchField(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
onChanged: (_) {},
|
||||
),
|
||||
const SizedBox(height: _spacingBetween),
|
||||
Expanded(
|
||||
child: _isExpanded
|
||||
? LongListAdressBookPayout(
|
||||
filteredRecipients: provider.filteredRecipients,
|
||||
onSelected: widget.onSelected,
|
||||
)
|
||||
: ShortListAdressBookPayout(
|
||||
recipients: provider.recipients,
|
||||
onSelected: widget.onSelected,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/pages/payment_methods/form.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentDetailsSection extends StatelessWidget {
|
||||
final bool isFormVisible;
|
||||
final bool isEditable;
|
||||
final VoidCallback? onToggle;
|
||||
final PaymentType? selectedType;
|
||||
final Object? data;
|
||||
|
||||
const PaymentDetailsSection({
|
||||
super.key,
|
||||
required this.isFormVisible,
|
||||
this.onToggle,
|
||||
required this.selectedType,
|
||||
required this.data,
|
||||
required this.isEditable,
|
||||
});
|
||||
|
||||
static const double toggleSpacing = 8.0;
|
||||
static const double formVisibleSpacing = 30.0;
|
||||
static const double formHiddenSpacing = 20.0;
|
||||
static const Duration animationDuration = Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
final toggleIcon = isFormVisible ? Icons.expand_less : Icons.expand_more;
|
||||
final toggleText = isFormVisible ? loc.hideDetails : loc.showDetails;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (!isEditable && onToggle != null)
|
||||
TextButton.icon(
|
||||
onPressed: onToggle,
|
||||
icon: Icon(toggleIcon, color: theme.colorScheme.primary),
|
||||
label: Text(
|
||||
toggleText,
|
||||
style: TextStyle(color: theme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: toggleSpacing),
|
||||
AnimatedCrossFade(
|
||||
duration: animationDuration,
|
||||
crossFadeState: isFormVisible
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: PaymentMethodForm(
|
||||
key: const ValueKey('formVisible'),
|
||||
isEditable: isEditable,
|
||||
selectedType: selectedType,
|
||||
onChanged: (_) {},
|
||||
initialData: data,
|
||||
),
|
||||
secondChild: const SizedBox.shrink(key: ValueKey('formHidden')),
|
||||
),
|
||||
SizedBox(height: isFormVisible ? formVisibleSpacing : formHiddenSpacing),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
|
||||
|
||||
|
||||
class RecipientHeader extends StatelessWidget{
|
||||
final Recipient recipient;
|
||||
|
||||
const RecipientHeader({super.key, required this.recipient});
|
||||
|
||||
final double _avatarRadius = 20;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: RecipientAvatar(
|
||||
isVisible: false,
|
||||
name: recipient.name,
|
||||
avatarUrl: recipient.avatarUrl,
|
||||
avatarRadius: _avatarRadius,
|
||||
nameStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
title: Text(recipient.name, style: theme.textTheme.titleLarge),
|
||||
subtitle: Text(recipient.email, style: theme.textTheme.bodyLarge),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/single/new_recipient/type.dart';
|
||||
|
||||
|
||||
class SinglePayout extends StatelessWidget {
|
||||
final void Function(PaymentType type) onGoToPayment;
|
||||
|
||||
static const double _cardPadding = 30.0;
|
||||
static const double _dividerPaddingVertical = 12.0;
|
||||
static const double _cardBorderRadius = 12.0;
|
||||
static const double _dividerThickness = 1.0;
|
||||
|
||||
const SinglePayout({super.key, required this.onGoToPayment});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final paymentTypes = PaymentType.values;
|
||||
final dividerColor = Theme.of(context).dividerColor;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(_cardBorderRadius),
|
||||
),
|
||||
elevation: 4,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(_cardPadding),
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < paymentTypes.length; i++) ...[
|
||||
PaymentTypeTile(
|
||||
type: paymentTypes[i],
|
||||
onSelected: onGoToPayment,
|
||||
),
|
||||
if (i < paymentTypes.length - 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: _dividerPaddingVertical),
|
||||
child: Divider(thickness: _dividerThickness, color: dividerColor),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/pages/payment_methods/icon.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
|
||||
|
||||
class PaymentTypeTile extends StatelessWidget {
|
||||
final PaymentType type;
|
||||
final void Function(PaymentType type) onSelected;
|
||||
|
||||
const PaymentTypeTile({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label = getPaymentTypeLabel(context, type);
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => onSelected(type),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(iconForPaymentType(type), size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
frontend/pweb/lib/pages/dashboard/payouts/single/widget.dart
Normal file
35
frontend/pweb/lib/pages/dashboard/payouts/single/widget.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/widget.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/new_recipient/payout.dart';
|
||||
|
||||
|
||||
class SinglePayoutForm extends StatelessWidget {
|
||||
final ValueChanged<Recipient> onRecipientSelected;
|
||||
final void Function(PaymentType type) onGoToPayment;
|
||||
|
||||
const SinglePayoutForm({
|
||||
super.key,
|
||||
required this.onRecipientSelected,
|
||||
required this.onGoToPayment,
|
||||
});
|
||||
|
||||
static const double _spacingBetweenAddressAndForm = 20.0;
|
||||
static const double _bottomSpacing = 40.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
AdressBookPayout(onSelected: onRecipientSelected),
|
||||
const SizedBox(height: _spacingBetweenAddressAndForm),
|
||||
SinglePayout(onGoToPayment: onGoToPayment),
|
||||
const SizedBox(height: _bottomSpacing),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user