Quotation
This commit is contained in:
@@ -48,51 +48,54 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PageViewLoader(
|
||||
child: 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,
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return PageViewLoader(
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: TransactionRefButton(
|
||||
onTap: () => _setActive(true),
|
||||
isActive: _showContainerSingle,
|
||||
label: l10n.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(width: AppSpacing.small),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: TransactionRefButton(
|
||||
onTap: () => _setActive(false),
|
||||
isActive: _showContainerMultiple,
|
||||
label: l10n.sendMultiple,
|
||||
icon: Icons.group_add,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.medium),
|
||||
BalanceWidget(
|
||||
onTopUp: widget.onTopUp,
|
||||
),
|
||||
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(),
|
||||
],
|
||||
const SizedBox(height: AppSpacing.medium),
|
||||
BalanceWidget(
|
||||
onTopUp: widget.onTopUp,
|
||||
),
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/amount.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/fee_payer.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/quote_status/quote_status.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/summary/widget.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
@@ -36,8 +37,9 @@ class PaymentFormWidget extends StatelessWidget {
|
||||
const SizedBox(height: _largeSpacing),
|
||||
|
||||
const PaymentSummary(spacing: _extraSpacing),
|
||||
const SizedBox(height: _mediumSpacing),
|
||||
const QuoteStatus(spacing: _smallSpacing),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
import 'package:pweb/providers/quotation/quotation.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/body.dart';
|
||||
import 'package:pweb/utils/quote_duration_format.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class QuoteStatus extends StatelessWidget {
|
||||
final double spacing;
|
||||
|
||||
const QuoteStatus({super.key, required this.spacing});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final controller = context.watch<QuotationController>();
|
||||
final timeLeft = controller.timeLeft;
|
||||
final isLoading = controller.isLoading;
|
||||
final statusType = controller.quoteStatus;
|
||||
final autoRefreshMode = controller.autoRefreshMode;
|
||||
|
||||
String statusText;
|
||||
String? helperText;
|
||||
switch (statusType) {
|
||||
case QuoteStatusType.loading:
|
||||
statusText = loc.quoteUpdating;
|
||||
break;
|
||||
case QuoteStatusType.error:
|
||||
statusText = loc.quoteErrorGeneric;
|
||||
break;
|
||||
case QuoteStatusType.missing:
|
||||
statusText = loc.quoteUnavailable;
|
||||
break;
|
||||
case QuoteStatusType.expired:
|
||||
statusText = loc.quoteExpired;
|
||||
helperText = loc.quoteRefreshRequired;
|
||||
break;
|
||||
case QuoteStatusType.active:
|
||||
statusText = timeLeft == null
|
||||
? loc.quoteActive
|
||||
: loc.quoteExpiresIn(formatQuoteDuration(timeLeft));
|
||||
break;
|
||||
}
|
||||
|
||||
final canRefresh = controller.canRefresh && !isLoading;
|
||||
final showPrimaryRefresh = canRefresh &&
|
||||
(statusType == QuoteStatusType.expired ||
|
||||
statusType == QuoteStatusType.error ||
|
||||
statusType == QuoteStatusType.missing);
|
||||
|
||||
return QuoteStatusBody(
|
||||
spacing: spacing,
|
||||
statusType: statusType,
|
||||
statusText: statusText,
|
||||
helperText: helperText,
|
||||
isLoading: isLoading,
|
||||
canRefresh: canRefresh,
|
||||
showPrimaryRefresh: showPrimaryRefresh,
|
||||
autoRefreshMode: autoRefreshMode,
|
||||
onAutoRefreshModeChanged: controller.setAutoRefreshMode,
|
||||
onRefresh: controller.refreshQuotation,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
import 'package:pshared/models/auto_refresh_mode.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/refresh_section.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/card.dart';
|
||||
|
||||
|
||||
class QuoteStatusBody extends StatelessWidget {
|
||||
final double spacing;
|
||||
final QuoteStatusType statusType;
|
||||
final String statusText;
|
||||
final String? helperText;
|
||||
final bool isLoading;
|
||||
final bool canRefresh;
|
||||
final bool showPrimaryRefresh;
|
||||
final AutoRefreshMode autoRefreshMode;
|
||||
final ValueChanged<AutoRefreshMode> onAutoRefreshModeChanged;
|
||||
final VoidCallback onRefresh;
|
||||
|
||||
const QuoteStatusBody({
|
||||
super.key,
|
||||
required this.spacing,
|
||||
required this.statusType,
|
||||
required this.statusText,
|
||||
required this.helperText,
|
||||
required this.isLoading,
|
||||
required this.canRefresh,
|
||||
required this.showPrimaryRefresh,
|
||||
required this.autoRefreshMode,
|
||||
required this.onAutoRefreshModeChanged,
|
||||
required this.onRefresh,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
QuoteStatusCard(
|
||||
statusType: statusType,
|
||||
isLoading: isLoading,
|
||||
statusText: statusText,
|
||||
helperText: helperText,
|
||||
canRefresh: canRefresh,
|
||||
showPrimaryRefresh: showPrimaryRefresh,
|
||||
onRefresh: onRefresh,
|
||||
),
|
||||
SizedBox(height: spacing),
|
||||
QuoteAutoRefreshSection(
|
||||
autoRefreshMode: autoRefreshMode,
|
||||
canRefresh: canRefresh,
|
||||
onModeChanged: onAutoRefreshModeChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class QuoteStatusCard extends StatelessWidget {
|
||||
final QuoteStatusType statusType;
|
||||
final bool isLoading;
|
||||
final String statusText;
|
||||
final String? helperText;
|
||||
final bool canRefresh;
|
||||
final bool showPrimaryRefresh;
|
||||
final VoidCallback onRefresh;
|
||||
|
||||
const QuoteStatusCard({
|
||||
super.key,
|
||||
required this.statusType,
|
||||
required this.isLoading,
|
||||
required this.statusText,
|
||||
required this.helperText,
|
||||
required this.canRefresh,
|
||||
required this.showPrimaryRefresh,
|
||||
required this.onRefresh,
|
||||
});
|
||||
|
||||
static const double _cardRadius = 12;
|
||||
static const double _cardSpacing = 12;
|
||||
static const double _iconSize = 18;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final foregroundColor = _resolveForegroundColor(theme, statusType);
|
||||
final statusStyle = theme.textTheme.bodyMedium?.copyWith(color: foregroundColor);
|
||||
final helperStyle = theme.textTheme.bodySmall?.copyWith(
|
||||
color: foregroundColor.withValues(alpha: 0.8),
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(_cardSpacing),
|
||||
decoration: BoxDecoration(
|
||||
color: _resolveCardColor(theme, statusType),
|
||||
borderRadius: BorderRadius.circular(_cardRadius),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
width: _iconSize,
|
||||
height: _iconSize,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(foregroundColor),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
_resolveIcon(statusType),
|
||||
size: _iconSize,
|
||||
color: foregroundColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _cardSpacing),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(statusText, style: statusStyle),
|
||||
if (helperText != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(helperText!, style: helperStyle),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (canRefresh)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: _cardSpacing),
|
||||
child: showPrimaryRefresh
|
||||
? ElevatedButton(
|
||||
onPressed: canRefresh ? onRefresh : null,
|
||||
child: Text(AppLocalizations.of(context)!.quoteRefresh),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: canRefresh ? onRefresh : null,
|
||||
child: Text(AppLocalizations.of(context)!.quoteRefresh),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _resolveCardColor(ThemeData theme, QuoteStatusType status) {
|
||||
switch (status) {
|
||||
case QuoteStatusType.loading:
|
||||
return theme.colorScheme.secondaryContainer;
|
||||
case QuoteStatusType.error:
|
||||
case QuoteStatusType.expired:
|
||||
return theme.colorScheme.errorContainer;
|
||||
case QuoteStatusType.active:
|
||||
return theme.colorScheme.primaryContainer;
|
||||
case QuoteStatusType.missing:
|
||||
return theme.colorScheme.surfaceContainerHighest;
|
||||
}
|
||||
}
|
||||
|
||||
Color _resolveForegroundColor(ThemeData theme, QuoteStatusType status) {
|
||||
switch (status) {
|
||||
case QuoteStatusType.loading:
|
||||
return theme.colorScheme.onSecondaryContainer;
|
||||
case QuoteStatusType.error:
|
||||
case QuoteStatusType.expired:
|
||||
return theme.colorScheme.onErrorContainer;
|
||||
case QuoteStatusType.active:
|
||||
return theme.colorScheme.onPrimaryContainer;
|
||||
case QuoteStatusType.missing:
|
||||
return theme.colorScheme.onSurfaceVariant;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _resolveIcon(QuoteStatusType status) {
|
||||
switch (status) {
|
||||
case QuoteStatusType.loading:
|
||||
return Icons.sync_rounded;
|
||||
case QuoteStatusType.error:
|
||||
return Icons.warning_amber_rounded;
|
||||
case QuoteStatusType.expired:
|
||||
return Icons.error_outline_rounded;
|
||||
case QuoteStatusType.active:
|
||||
return Icons.timer_outlined;
|
||||
case QuoteStatusType.missing:
|
||||
return Icons.info_outline_rounded;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/auto_refresh_mode.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class QuoteAutoRefreshSection extends StatelessWidget {
|
||||
final AutoRefreshMode autoRefreshMode;
|
||||
final bool canRefresh;
|
||||
final ValueChanged<AutoRefreshMode> onModeChanged;
|
||||
|
||||
const QuoteAutoRefreshSection({
|
||||
super.key,
|
||||
required this.autoRefreshMode,
|
||||
required this.canRefresh,
|
||||
required this.onModeChanged,
|
||||
});
|
||||
|
||||
static const double _autoRefreshSpacing = 8;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
autoRefreshMode == AutoRefreshMode.on
|
||||
? loc.quoteAutoRefreshEnabled
|
||||
: loc.quoteAutoRefreshDisabled,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
loc.quoteAutoRefreshHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.textTheme.bodySmall?.color?.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _autoRefreshSpacing),
|
||||
ToggleButtons(
|
||||
isSelected: [
|
||||
autoRefreshMode == AutoRefreshMode.off,
|
||||
autoRefreshMode == AutoRefreshMode.on,
|
||||
],
|
||||
onPressed: canRefresh
|
||||
? (index) {
|
||||
final nextMode =
|
||||
index == 1 ? AutoRefreshMode.on : AutoRefreshMode.off;
|
||||
if (nextMode == autoRefreshMode) return;
|
||||
onModeChanged(nextMode);
|
||||
}
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
constraints: const BoxConstraints(minHeight: 32, minWidth: 56),
|
||||
selectedColor: theme.colorScheme.onPrimary,
|
||||
fillColor: theme.colorScheme.primary,
|
||||
color: theme.colorScheme.onSurface,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(loc.toggleOff),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(loc.toggleOn),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/quotation.dart';
|
||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/quotation.dart';
|
||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/quotation.dart';
|
||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user