Frontend first draft
This commit is contained in:
90
frontend/pweb/lib/pages/address_book/form/method_tile.dart
Normal file
90
frontend/pweb/lib/pages/address_book/form/method_tile.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/pages/payment_methods/form.dart';
|
||||
import 'package:pweb/pages/payment_methods/icon.dart';
|
||||
|
||||
|
||||
class AdressBookPaymentMethodTile extends StatefulWidget {
|
||||
final PaymentType type;
|
||||
final String title;
|
||||
final Map<PaymentType, Object?> methods;
|
||||
final ValueChanged<Object?> onChanged;
|
||||
|
||||
final double spacingM;
|
||||
final double spacingS;
|
||||
final double sizeM;
|
||||
final TextStyle? titleTextStyle;
|
||||
|
||||
const AdressBookPaymentMethodTile({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.methods,
|
||||
required this.onChanged,
|
||||
this.spacingM = 12,
|
||||
this.spacingS = 8,
|
||||
this.sizeM = 20,
|
||||
this.titleTextStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AdressBookPaymentMethodTile> createState() => _AdressBookPaymentMethodTileState();
|
||||
}
|
||||
|
||||
class _AdressBookPaymentMethodTileState extends State<AdressBookPaymentMethodTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isAdded = widget.methods.containsKey(widget.type);
|
||||
|
||||
return ExpansionTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
iconForPaymentType(widget.type),
|
||||
size: widget.sizeM,
|
||||
color: isAdded
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
SizedBox(width: widget.spacingS),
|
||||
Text(
|
||||
widget.title,
|
||||
style: widget.titleTextStyle ??
|
||||
theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isAdded ? theme.colorScheme.primary : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isAdded)
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: theme.colorScheme.error),
|
||||
onPressed: () {
|
||||
widget.onChanged(null);
|
||||
},
|
||||
),
|
||||
Icon(
|
||||
isAdded ? Icons.check_circle : Icons.add_circle_outline,
|
||||
color: isAdded ? theme.colorScheme.primary : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
PaymentMethodForm(
|
||||
key: ValueKey(widget.type),
|
||||
selectedType: widget.type,
|
||||
initialData: widget.methods[widget.type],
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
SizedBox(height: widget.spacingM),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
109
frontend/pweb/lib/pages/address_book/form/page.dart
Normal file
109
frontend/pweb/lib/pages/address_book/form/page.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/card.dart';
|
||||
import 'package:pshared/models/payment/methods/iban.dart';
|
||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
import 'package:pshared/models/recipient/type.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/form/view.dart';
|
||||
import 'package:pweb/services/amplitude.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AdressBookRecipientForm extends StatefulWidget {
|
||||
final Recipient? recipient;
|
||||
final ValueChanged<Recipient?>? onSaved;
|
||||
|
||||
const AdressBookRecipientForm({super.key, this.recipient, this.onSaved});
|
||||
|
||||
@override
|
||||
State<AdressBookRecipientForm> createState() => _AdressBookRecipientFormState();
|
||||
}
|
||||
|
||||
class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _nameCtrl;
|
||||
late TextEditingController _emailCtrl;
|
||||
RecipientType _type = RecipientType.internal;
|
||||
RecipientStatus _status = RecipientStatus.ready;
|
||||
final Map<PaymentType, Object?> _methods = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final r = widget.recipient;
|
||||
_nameCtrl = TextEditingController(text: r?.name ?? "");
|
||||
_emailCtrl = TextEditingController(text: r?.email ?? "");
|
||||
_type = r?.type ?? RecipientType.internal;
|
||||
_status = r?.status ?? RecipientStatus.ready;
|
||||
|
||||
if (r?.card != null) _methods[PaymentType.card] = r!.card;
|
||||
if (r?.iban != null) _methods[PaymentType.iban] = r!.iban;
|
||||
if (r?.wallet != null) _methods[PaymentType.wallet] = r!.wallet;
|
||||
if (r?.bank != null) _methods[PaymentType.bankAccount] = r!.bank;
|
||||
}
|
||||
|
||||
//TODO Change when registration is ready
|
||||
void _save() {
|
||||
if (!_formKey.currentState!.validate() || _methods.isEmpty) {
|
||||
AmplitudeService.recipientAddCompleted(
|
||||
_type,
|
||||
_status,
|
||||
_methods.keys.toSet(),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.recipientFormRule),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final recipient = Recipient(
|
||||
name: _nameCtrl.text,
|
||||
email: _emailCtrl.text,
|
||||
type: _type,
|
||||
status: _status,
|
||||
avatarUrl: null,
|
||||
card: _methods[PaymentType.card] as CardPaymentMethod?,
|
||||
iban: _methods[PaymentType.iban] as IbanPaymentMethod?,
|
||||
wallet: _methods[PaymentType.wallet] as WalletPaymentMethod?,
|
||||
bank: _methods[PaymentType.bankAccount] as RussianBankAccountPaymentMethod?,
|
||||
);
|
||||
|
||||
widget.onSaved?.call(recipient);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormView(
|
||||
formKey: _formKey,
|
||||
nameCtrl: _nameCtrl,
|
||||
emailCtrl: _emailCtrl,
|
||||
type: _type,
|
||||
status: _status,
|
||||
methods: _methods,
|
||||
onTypeChanged: (t) => setState(() => _type = t),
|
||||
onStatusChanged: (s) => setState(() => _status = s),
|
||||
onMethodsChanged: (type, data) {
|
||||
setState(() {
|
||||
if (data != null) {
|
||||
_methods[type] = data;
|
||||
} else {
|
||||
_methods.remove(type);
|
||||
}
|
||||
});
|
||||
},
|
||||
onSave: _save,
|
||||
isEditing: widget.recipient != null,
|
||||
onBack: () {
|
||||
widget.onSaved?.call(null);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
124
frontend/pweb/lib/pages/address_book/form/view.dart
Normal file
124
frontend/pweb/lib/pages/address_book/form/view.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
import 'package:pshared/models/recipient/type.dart';
|
||||
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
import 'package:pweb/pages/address_book/form/method_tile.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/button.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/email_field.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/header.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/name_field.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class FormView extends StatelessWidget {
|
||||
final GlobalKey<FormState> formKey;
|
||||
final TextEditingController nameCtrl;
|
||||
final TextEditingController emailCtrl;
|
||||
final RecipientType type;
|
||||
final RecipientStatus status;
|
||||
final Map<PaymentType, Object?> methods;
|
||||
final ValueChanged<RecipientType> onTypeChanged;
|
||||
final ValueChanged<RecipientStatus> onStatusChanged;
|
||||
final void Function(PaymentType, Object?) onMethodsChanged;
|
||||
final VoidCallback onSave;
|
||||
final bool isEditing;
|
||||
final VoidCallback onBack;
|
||||
|
||||
final double maxWidth;
|
||||
final double elevation;
|
||||
final double borderRadius;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final double spacingHeader;
|
||||
final double spacingFields;
|
||||
final double spacingDivider;
|
||||
final double spacingSave;
|
||||
final double spacingBottom;
|
||||
final TextStyle? titleTextStyle;
|
||||
|
||||
const FormView({
|
||||
super.key,
|
||||
required this.formKey,
|
||||
required this.nameCtrl,
|
||||
required this.emailCtrl,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.methods,
|
||||
required this.onTypeChanged,
|
||||
required this.onStatusChanged,
|
||||
required this.onMethodsChanged,
|
||||
required this.onSave,
|
||||
required this.isEditing,
|
||||
required this.onBack,
|
||||
this.maxWidth = 500,
|
||||
this.elevation = 4,
|
||||
this.borderRadius = 16,
|
||||
this.padding = const EdgeInsets.all(20),
|
||||
this.spacingHeader = 20,
|
||||
this.spacingFields = 12,
|
||||
this.spacingDivider = 40,
|
||||
this.spacingSave = 30,
|
||||
this.spacingBottom = 16,
|
||||
this.titleTextStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Material(
|
||||
elevation: elevation,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
color: theme.colorScheme.onSecondary,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HeaderWidget(
|
||||
isEditing: isEditing,
|
||||
onBack: onBack,
|
||||
),
|
||||
SizedBox(height: spacingHeader),
|
||||
NameField(controller: nameCtrl),
|
||||
SizedBox(height: spacingFields),
|
||||
EmailField(controller: emailCtrl),
|
||||
Divider(height: spacingDivider),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.choosePaymentMethod,
|
||||
style: titleTextStyle ??
|
||||
theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: spacingFields),
|
||||
...PaymentType.values.map(
|
||||
(p) => AdressBookPaymentMethodTile(
|
||||
type: p,
|
||||
title: getPaymentTypeLabel(context, p),
|
||||
methods: methods,
|
||||
onChanged: (data) => onMethodsChanged(p, data),
|
||||
),
|
||||
),
|
||||
SizedBox(height: spacingSave),
|
||||
SaveButton(onSave: onSave),
|
||||
SizedBox(height: spacingBottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class SaveButton extends StatelessWidget {
|
||||
final VoidCallback onSave;
|
||||
|
||||
final double width;
|
||||
final double height;
|
||||
final double borderRadius;
|
||||
final String? text;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
const SaveButton({
|
||||
super.key,
|
||||
required this.onSave,
|
||||
this.width = 200,
|
||||
this.height = 45,
|
||||
this.borderRadius = 12,
|
||||
this.text,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
onTap: onSave,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text ?? AppLocalizations.of(context)!.saveRecipient,
|
||||
style: textStyle ??
|
||||
theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class ChoiceChips<T> extends StatelessWidget {
|
||||
final String label;
|
||||
final List<T> values;
|
||||
final T selected;
|
||||
final ValueChanged<T> onChanged;
|
||||
|
||||
final double spacing;
|
||||
final double runSpacing;
|
||||
final double labelSpacing;
|
||||
|
||||
const ChoiceChips({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.values,
|
||||
required this.selected,
|
||||
required this.onChanged,
|
||||
this.spacing = 8,
|
||||
this.runSpacing = 8,
|
||||
this.labelSpacing = 8,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: labelSpacing),
|
||||
Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: runSpacing,
|
||||
children: values.map((v) {
|
||||
final isSelected = v == selected;
|
||||
return ChoiceChip(
|
||||
selectedColor: theme.colorScheme.primary,
|
||||
backgroundColor: theme.colorScheme.onSecondary,
|
||||
showCheckmark: false,
|
||||
label: Text(
|
||||
v.toString().split('.').last,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? theme.colorScheme.onSecondary
|
||||
: theme.colorScheme.inverseSurface,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onChanged(v),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class EmailField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
|
||||
final double borderRadius;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
const EmailField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.borderRadius = 12,
|
||||
this.contentPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: loc.username,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
contentPadding: contentPadding,
|
||||
),
|
||||
validator: (v) =>
|
||||
v == null || v.isEmpty ? loc.usernameErrorInvalid : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class HeaderWidget extends StatelessWidget {
|
||||
final bool isEditing;
|
||||
final VoidCallback? onBack;
|
||||
|
||||
final double spacing;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
const HeaderWidget({
|
||||
super.key,
|
||||
required this.isEditing,
|
||||
this.onBack,
|
||||
this.spacing = 8,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: theme.colorScheme.primary,
|
||||
onPressed: onBack,
|
||||
),
|
||||
SizedBox(width: spacing),
|
||||
Text(
|
||||
isEditing ? l10n.editRecipient : l10n.addRecipient,
|
||||
style: textStyle ??
|
||||
theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class NameField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
|
||||
final double borderRadius;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
const NameField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.borderRadius = 12,
|
||||
this.contentPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: loc.recipientName,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
contentPadding: contentPadding,
|
||||
),
|
||||
validator: (v) => v == null || v.isEmpty ? loc.enterRecipientName : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
61
frontend/pweb/lib/pages/address_book/page/filter_button.dart
Normal file
61
frontend/pweb/lib/pages/address_book/page/filter_button.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/filter.dart';
|
||||
|
||||
|
||||
class RecipientFilterButton extends StatelessWidget {
|
||||
final String text;
|
||||
final RecipientFilter filter;
|
||||
final RecipientFilter selected;
|
||||
final ValueChanged<RecipientFilter> onTap;
|
||||
|
||||
const RecipientFilterButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.filter,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSelected = selected == filter;
|
||||
final theme = Theme.of(context).colorScheme;
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: () => onTap(filter),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Colors.transparent),
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
elevation: WidgetStateProperty.all(0),
|
||||
),
|
||||
child: IntrinsicWidth(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: isSelected
|
||||
? theme.onPrimaryContainer
|
||||
: theme.onPrimaryContainer.withAlpha(60),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 2,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? theme.primary
|
||||
: theme.onPrimaryContainer.withAlpha(60),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
frontend/pweb/lib/pages/address_book/page/header.dart
Normal file
44
frontend/pweb/lib/pages/address_book/page/header.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class RecipientAddressBookHeader extends StatelessWidget {
|
||||
final VoidCallback onAddRecipient;
|
||||
|
||||
const RecipientAddressBookHeader({super.key, required this.onAddRecipient});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).colorScheme;
|
||||
final l10 = AppLocalizations.of(context)!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
l10.recipients,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: onAddRecipient,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(theme.primary),
|
||||
shadowColor: WidgetStateProperty.all(theme.onPrimaryContainer),
|
||||
elevation: WidgetStateProperty.all(2),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
icon: Icon(Icons.add, color: theme.onSecondary),
|
||||
label: Text(l10.addRecipient, style: TextStyle(color: theme.onSecondary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
40
frontend/pweb/lib/pages/address_book/page/list.dart
Normal file
40
frontend/pweb/lib/pages/address_book/page/list.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/page/recipient/item.dart';
|
||||
|
||||
|
||||
class RecipientAddressBookList extends StatelessWidget {
|
||||
final List<Recipient> filteredRecipients;
|
||||
final ValueChanged<Recipient>? onSelected;
|
||||
final ValueChanged<Recipient>? onEdit;
|
||||
final ValueChanged<Recipient>? onDelete;
|
||||
|
||||
const RecipientAddressBookList({
|
||||
super.key,
|
||||
required this.filteredRecipients,
|
||||
this.onSelected,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: filteredRecipients.length,
|
||||
itemBuilder: (context, index) {
|
||||
final recipient = filteredRecipients[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: RecipientAddressBookItem(
|
||||
recipient: recipient,
|
||||
onTap: () => onSelected?.call(recipient),
|
||||
onEdit: () => onEdit?.call(recipient),
|
||||
onDelete: () => onDelete?.call(recipient),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
102
frontend/pweb/lib/pages/address_book/page/page.dart
Normal file
102
frontend/pweb/lib/pages/address_book/page/page.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/filter.dart';
|
||||
import 'package:pweb/pages/address_book/page/filter_button.dart';
|
||||
import 'package:pweb/pages/address_book/page/header.dart';
|
||||
import 'package:pweb/pages/address_book/page/list.dart';
|
||||
import 'package:pweb/pages/address_book/page/search.dart';
|
||||
import 'package:pweb/providers/recipient.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class RecipientAddressBookPage extends StatelessWidget {
|
||||
final ValueChanged<Recipient> onRecipientSelected;
|
||||
final VoidCallback onAddRecipient;
|
||||
final ValueChanged<Recipient>? onEditRecipient;
|
||||
|
||||
const RecipientAddressBookPage({
|
||||
super.key,
|
||||
required this.onRecipientSelected,
|
||||
required this.onAddRecipient,
|
||||
this.onEditRecipient,
|
||||
});
|
||||
|
||||
static const double _expandedHeight = 550;
|
||||
static const double _paddingAll = 16;
|
||||
static const double _bigBox = 30;
|
||||
static const double _smallBox = 20;
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final provider = context.watch<RecipientProvider>();
|
||||
|
||||
if (provider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator()); //TODO This should be in the provider
|
||||
}
|
||||
|
||||
if (provider.error != null) {
|
||||
return Center(child: Text('Error: ${provider.error}'));
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RecipientAddressBookHeader(onAddRecipient: onAddRecipient),
|
||||
const SizedBox(height: _smallBox),
|
||||
RecipientSearchField(
|
||||
controller: TextEditingController(text: provider.query),
|
||||
focusNode: FocusNode(),
|
||||
onChanged: provider.setQuery,
|
||||
),
|
||||
const SizedBox(height: _bigBox),
|
||||
Row(
|
||||
children: [
|
||||
RecipientFilterButton(
|
||||
text: loc.allStatus,
|
||||
filter: RecipientFilter.all,
|
||||
selected: provider.selectedFilter,
|
||||
onTap: provider.setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.readyStatus,
|
||||
filter: RecipientFilter.ready,
|
||||
selected: provider.selectedFilter,
|
||||
onTap: provider.setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.registeredStatus,
|
||||
filter: RecipientFilter.registered,
|
||||
selected: provider.selectedFilter,
|
||||
onTap: provider.setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.notRegisteredStatus,
|
||||
filter: RecipientFilter.notRegistered,
|
||||
selected: provider.selectedFilter,
|
||||
onTap: provider.setFilter,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: _expandedHeight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(_paddingAll),
|
||||
child: RecipientAddressBookList(
|
||||
filteredRecipients: provider.filteredRecipients,
|
||||
onEdit: (recipient) => onEditRecipient?.call(recipient),
|
||||
onSelected: onRecipientSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class RecipientActions extends StatelessWidget {
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const RecipientActions({super.key, required this.onEdit, required this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(icon: Icon(Icons.edit, color: Theme.of(context).colorScheme.primary), onPressed: onEdit),
|
||||
IconButton(icon: Icon(Icons.delete, color: Theme.of(context).colorScheme.error), onPressed: onDelete),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class RecipientInfoColumn extends StatelessWidget {
|
||||
final String name;
|
||||
final String email;
|
||||
|
||||
const RecipientInfoColumn({super.key, required this.name, required this.email});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(name, style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 19)),
|
||||
Text(email, style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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 RecipientAddressBookInfoRow extends StatelessWidget {
|
||||
final PaymentType type;
|
||||
final String value;
|
||||
|
||||
final double spacingWidth;
|
||||
final double spacingHeight;
|
||||
final double iconSize;
|
||||
final double titleFontSize;
|
||||
final double valueFontSize;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
const RecipientAddressBookInfoRow({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.value,
|
||||
this.spacingWidth = 8.0,
|
||||
this.spacingHeight = 2.0,
|
||||
this.iconSize = 20.0,
|
||||
this.titleFontSize = 16.0,
|
||||
this.valueFontSize = 12.0,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = textStyle ?? Theme.of(context).textTheme.bodySmall!;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(iconForPaymentType(type), size: iconSize),
|
||||
SizedBox(width: spacingWidth),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
getPaymentTypeLabel(context, type),
|
||||
style: style.copyWith(fontSize: titleFontSize),
|
||||
),
|
||||
SizedBox(height: spacingHeight),
|
||||
Text(
|
||||
value,
|
||||
style: style.copyWith(fontSize: valueFontSize),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/page/recipient/actions.dart';
|
||||
import 'package:pweb/pages/address_book/page/recipient/info_column.dart';
|
||||
import 'package:pweb/pages/address_book/page/recipient/payment_row.dart';
|
||||
import 'package:pweb/pages/address_book/page/recipient/status.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
|
||||
|
||||
|
||||
class RecipientAddressBookItem extends StatefulWidget {
|
||||
final Recipient recipient;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
final double borderRadius;
|
||||
final double elevation;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final double spacingDotAvatar;
|
||||
final double spacingAvatarInfo;
|
||||
final double spacingBottom;
|
||||
final double avatarRadius;
|
||||
|
||||
const RecipientAddressBookItem({
|
||||
super.key,
|
||||
required this.recipient,
|
||||
required this.onTap,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
this.borderRadius = 12,
|
||||
this.elevation = 4,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
this.spacingDotAvatar = 8,
|
||||
this.spacingAvatarInfo = 16,
|
||||
this.spacingBottom = 10,
|
||||
this.avatarRadius = 24,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RecipientAddressBookItem> createState() => _RecipientAddressBookItemState();
|
||||
}
|
||||
|
||||
class _RecipientAddressBookItemState extends State<RecipientAddressBookItem> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final recipient = widget.recipient;
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius)),
|
||||
elevation: widget.elevation,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
child: Padding(
|
||||
padding: widget.padding,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
RecipientStatusDot(status: recipient.status),
|
||||
SizedBox(width: widget.spacingDotAvatar),
|
||||
RecipientAvatar(
|
||||
name: recipient.name,
|
||||
avatarUrl: recipient.avatarUrl,
|
||||
isVisible: false,
|
||||
avatarRadius: widget.avatarRadius,
|
||||
),
|
||||
SizedBox(width: widget.spacingAvatarInfo),
|
||||
Expanded(
|
||||
child: RecipientInfoColumn(
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
),
|
||||
),
|
||||
if (_isHovered)
|
||||
RecipientActions(
|
||||
onEdit: widget.onEdit, onDelete: widget.onDelete),
|
||||
],
|
||||
),
|
||||
SizedBox(height: widget.spacingBottom),
|
||||
RecipientPaymentRow(recipient: recipient),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/page/recipient/info_row.dart';
|
||||
|
||||
|
||||
class RecipientPaymentRow extends StatelessWidget {
|
||||
final Recipient recipient;
|
||||
final double spacing;
|
||||
|
||||
const RecipientPaymentRow({
|
||||
super.key,
|
||||
required this.recipient,
|
||||
this.spacing = 18
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
spacing: spacing,
|
||||
children: [
|
||||
if (recipient.bank?.accountNumber.isNotEmpty ?? false)
|
||||
RecipientAddressBookInfoRow(
|
||||
type: PaymentType.bankAccount,
|
||||
value: recipient.bank!.accountNumber
|
||||
),
|
||||
if (recipient.card?.pan.isNotEmpty ?? false)
|
||||
RecipientAddressBookInfoRow(
|
||||
type: PaymentType.card,
|
||||
value: recipient.card!.pan
|
||||
),
|
||||
if (recipient.iban?.iban.isNotEmpty ?? false)
|
||||
RecipientAddressBookInfoRow(
|
||||
type: PaymentType.iban,
|
||||
value: recipient.iban!.iban
|
||||
),
|
||||
if (recipient.wallet?.walletId.isNotEmpty ?? false)
|
||||
RecipientAddressBookInfoRow(
|
||||
type: PaymentType.wallet,
|
||||
value: recipient.wallet!.walletId
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
|
||||
|
||||
class RecipientStatusDot extends StatelessWidget {
|
||||
final RecipientStatus status;
|
||||
|
||||
const RecipientStatusDot({super.key, required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color color;
|
||||
switch (status) {
|
||||
case RecipientStatus.ready:
|
||||
color = Colors.green;
|
||||
break;
|
||||
case RecipientStatus.notRegistered:
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
case RecipientStatus.registered:
|
||||
color = Colors.yellow;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
frontend/pweb/lib/pages/address_book/page/search.dart
Normal file
44
frontend/pweb/lib/pages/address_book/page/search.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class RecipientSearchField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String> onChanged;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
const RecipientSearchField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onChanged,
|
||||
this.focusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
hintText: l10n.searchHint,
|
||||
border: const OutlineInputBorder(),
|
||||
fillColor: Theme.of(context).colorScheme.onSecondary,
|
||||
filled: true,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
onChanged('');
|
||||
focusNode?.unfocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: onChanged,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user