Frontend first draft

This commit is contained in:
Arseni
2025-11-13 15:06:15 +03:00
parent e47f343afb
commit ddb54ddfdc
504 changed files with 25498 additions and 1 deletions

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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