226 lines
7.0 KiB
Dart
226 lines
7.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:sxchnge_wallet/sxchnge_wallet.dart';
|
|
|
|
import 'paste.dart';
|
|
|
|
import '../generated/i18n/app_localizations.dart';
|
|
|
|
class BankFields extends StatefulWidget {
|
|
const BankFields({
|
|
super.key,
|
|
required this.onChanged,
|
|
this.initial,
|
|
this.errorField,
|
|
});
|
|
final ValueChanged<DestinationDetails?> onChanged;
|
|
final RussianBankDestination? initial;
|
|
final String? errorField;
|
|
@override
|
|
State<BankFields> createState() => _BankFieldsState();
|
|
}
|
|
|
|
class _BankFieldsState extends State<BankFields> {
|
|
late final first = TextEditingController(text: widget.initial?.firstName);
|
|
late final last = TextEditingController(text: widget.initial?.lastName);
|
|
late final patronymic = TextEditingController(
|
|
text: widget.initial?.patronymic,
|
|
);
|
|
late final account = TextEditingController(text: widget.initial?.account);
|
|
final query = TextEditingController();
|
|
late final search = BankSearchProvider(
|
|
context.read<DestinationsProvider>().service,
|
|
);
|
|
late Bank? bank = widget.initial?.bank;
|
|
String? pastedHolder;
|
|
bool conflict = false;
|
|
Map<String, String> pasted = {};
|
|
bool get mismatch =>
|
|
bank != null &&
|
|
((pasted['bank'] != null && pasted['bank'] != bank!.name) ||
|
|
(pasted['correspondent'] != null &&
|
|
pasted['correspondent'] != bank!.correspondent));
|
|
void changed() => widget.onChanged(
|
|
bank == null
|
|
? null
|
|
: RussianBankDestination(
|
|
firstName: first.text,
|
|
lastName: last.text,
|
|
patronymic: patronymic.text,
|
|
bank: bank!,
|
|
account: account.text,
|
|
),
|
|
);
|
|
Future<void> paste() async {
|
|
final parsed = await showDialog<BankPaste>(
|
|
context: context,
|
|
builder: (_) => const BankPasteDialog(),
|
|
);
|
|
if (!mounted || parsed == null) return;
|
|
for (final entry in {
|
|
'firstName': first,
|
|
'lastName': last,
|
|
'patronymic': patronymic,
|
|
'account': account,
|
|
}.entries) {
|
|
if (parsed.conflicts.contains(entry.key)) {
|
|
entry.value.clear();
|
|
} else if (parsed.values[entry.key] case final String value) {
|
|
entry.value.text = value;
|
|
}
|
|
}
|
|
setState(() {
|
|
pasted = parsed.values;
|
|
conflict = parsed.conflicts.isNotEmpty;
|
|
pastedHolder = parsed.values['holder'];
|
|
});
|
|
if (parsed.conflicts.contains('bik')) {
|
|
setState(() => bank = null);
|
|
}
|
|
if (parsed.values['bik'] case final String bik) {
|
|
query.text = bik;
|
|
search.search(bik);
|
|
setState(() => bank = null);
|
|
}
|
|
changed();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
for (final c in [first, last, patronymic, account, query]) {
|
|
c.dispose();
|
|
}
|
|
search.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Widget field(
|
|
String label,
|
|
TextEditingController c, {
|
|
String? id,
|
|
bool numeric = false,
|
|
String? error,
|
|
}) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
child: TextField(
|
|
key: id == null ? null : ValueKey(id),
|
|
controller: c,
|
|
keyboardType: numeric ? TextInputType.number : TextInputType.name,
|
|
decoration: InputDecoration(labelText: label, errorText: error),
|
|
onChanged: (_) => changed(),
|
|
),
|
|
);
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l = AppLocalizations.of(context)!;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(l.dHolder, style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: 16),
|
|
LayoutBuilder(
|
|
builder: (context, b) {
|
|
final a = field(
|
|
l.dFirst,
|
|
first,
|
|
id: 'destination-first',
|
|
error: widget.errorField == 'holder' ? l.dInvalid : null,
|
|
);
|
|
final z = field(l.dLast, last, id: 'destination-last');
|
|
return b.maxWidth >= 480
|
|
? Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(child: a),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: z),
|
|
],
|
|
)
|
|
: Column(children: [a, z]);
|
|
},
|
|
),
|
|
field(l.dPatronymic, patronymic),
|
|
if (pastedHolder != null) SelectableText(pastedHolder!),
|
|
if (conflict) Text(l.dPasteConflict),
|
|
Text(l.dBankDetails, style: Theme.of(context).textTheme.titleMedium),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: TextButton.icon(
|
|
onPressed: paste,
|
|
icon: const Icon(Icons.content_paste),
|
|
label: Text(l.dPaste),
|
|
),
|
|
),
|
|
ListenableBuilder(
|
|
listenable: search,
|
|
builder: (context, _) => Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (bank == null) ...[
|
|
TextField(
|
|
controller: query,
|
|
decoration: InputDecoration(labelText: l.dBankSearch),
|
|
onChanged: search.search,
|
|
),
|
|
if (search.loading) const LinearProgressIndicator(),
|
|
for (final b in search.results)
|
|
ListTile(
|
|
title: Text(b.name),
|
|
subtitle: Text('${b.bik} · ${b.city}'),
|
|
onTap: () {
|
|
setState(() => bank = b);
|
|
changed();
|
|
},
|
|
),
|
|
if (search.error != null)
|
|
Text(
|
|
search.error == DestinationIssue.unavailable
|
|
? l.dBankFailure
|
|
: l.dBankUnknown,
|
|
),
|
|
if (search.error == DestinationIssue.unavailable)
|
|
TextButton(
|
|
onPressed: () => search.search(query.text),
|
|
child: Text(l.wRefresh),
|
|
),
|
|
] else ...[
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(bank!.name),
|
|
subtitle: Text('${l.dBik} ${bank!.bik}'),
|
|
trailing: TextButton(
|
|
onPressed: () {
|
|
setState(() => bank = null);
|
|
changed();
|
|
},
|
|
child: Text(l.dChangeBank),
|
|
),
|
|
),
|
|
ExpansionTile(
|
|
title: Text(l.dBankDetails),
|
|
children: [
|
|
SelectableText(
|
|
'${l.dCorrespondent}: ${bank!.correspondent}',
|
|
),
|
|
],
|
|
),
|
|
if (!bank!.supported) Text(l.dUnsupported),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
field(
|
|
l.dAccount,
|
|
account,
|
|
id: 'destination-account',
|
|
numeric: true,
|
|
error: widget.errorField == 'account' ? l.dInvalid : null,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|