Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
172 lines
5.6 KiB
Dart
172 lines
5.6 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:fintracker_api/fintracker_api.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../core/api/api_client.dart';
|
|
import '../../core/auth/auth_controller.dart';
|
|
import 'providers.dart';
|
|
|
|
/// The brokers whose accounts are created by hand: T-Invest accounts come from the sync.
|
|
const _brokerChoices = [
|
|
(Broker.sber, 'Сбер'),
|
|
(Broker.vtb, 'ВТБ'),
|
|
(Broker.other, 'Другой (CSV)'),
|
|
];
|
|
|
|
/// Maps the import contract's broker key (`sber | vtb | csv`) onto a creatable broker.
|
|
Broker? brokerFromImportKey(String? key) => switch (key) {
|
|
'sber' => Broker.sber,
|
|
'vtb' => Broker.vtb,
|
|
'csv' => Broker.other,
|
|
_ => null,
|
|
};
|
|
|
|
/// Asks for the details of a broker account and creates it. Returns the new account, or null
|
|
/// when cancelled or when the server refused (the reason is shown in the dialog).
|
|
Future<AccountOut?> showAccountCreateDialog(
|
|
BuildContext context, {
|
|
Broker? broker,
|
|
String? sourceId,
|
|
String? name,
|
|
}) => showDialog<AccountOut>(
|
|
context: context,
|
|
builder: (_) =>
|
|
_AccountCreateDialog(broker: broker, sourceId: sourceId, name: name),
|
|
);
|
|
|
|
class _AccountCreateDialog extends ConsumerStatefulWidget {
|
|
const _AccountCreateDialog({this.broker, this.sourceId, this.name});
|
|
|
|
final Broker? broker;
|
|
final String? sourceId;
|
|
final String? name;
|
|
|
|
@override
|
|
ConsumerState<_AccountCreateDialog> createState() =>
|
|
_AccountCreateDialogState();
|
|
}
|
|
|
|
class _AccountCreateDialogState extends ConsumerState<_AccountCreateDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late final _name = TextEditingController(text: widget.name ?? '');
|
|
late final _sourceId = TextEditingController(text: widget.sourceId ?? '');
|
|
final _currency = TextEditingController(text: 'RUB');
|
|
late Broker _broker = widget.broker ?? Broker.sber;
|
|
bool _saving = false;
|
|
String? _error;
|
|
|
|
@override
|
|
void dispose() {
|
|
_name.dispose();
|
|
_sourceId.dispose();
|
|
_currency.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _save() async {
|
|
if (!(_formKey.currentState?.validate() ?? false)) return;
|
|
setState(() {
|
|
_saving = true;
|
|
_error = null;
|
|
});
|
|
try {
|
|
final r = await ref
|
|
.read(apiProvider)
|
|
.getAccountsApi()
|
|
.accountsCreate(
|
|
accountCreate: AccountCreate(
|
|
name: _name.text.trim(),
|
|
broker: _broker,
|
|
sourceId: _sourceId.text.trim(),
|
|
currency: _currency.text.trim().toUpperCase(),
|
|
),
|
|
);
|
|
ref.invalidate(accountsProvider);
|
|
if (mounted) Navigator.of(context).pop(r.data);
|
|
} on DioException catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_saving = false;
|
|
_error = problemMessage(e);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Новый брокерский счёт'),
|
|
content: SizedBox(
|
|
width: 420,
|
|
child: Form(
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextFormField(
|
|
controller: _name,
|
|
decoration: const InputDecoration(labelText: 'Название'),
|
|
validator: (v) =>
|
|
(v ?? '').trim().isEmpty ? 'Обязательное поле' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
DropdownButtonFormField<Broker>(
|
|
initialValue: _broker,
|
|
decoration: const InputDecoration(labelText: 'Брокер'),
|
|
items: [
|
|
for (final (b, label) in _brokerChoices)
|
|
DropdownMenuItem(value: b, child: Text(label)),
|
|
],
|
|
onChanged: (b) => setState(() => _broker = b ?? _broker),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
controller: _sourceId,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Номер договора',
|
|
helperText: 'Как напечатан в отчёте брокера — по нему импорт находит счёт',
|
|
helperMaxLines: 2,
|
|
),
|
|
validator: (v) =>
|
|
(v ?? '').trim().isEmpty ? 'Обязательное поле' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
controller: _currency,
|
|
textCapitalization: TextCapitalization.characters,
|
|
decoration: const InputDecoration(labelText: 'Валюта'),
|
|
validator: (v) => (v ?? '').trim().length == 3
|
|
? null
|
|
: 'Три буквы, например RUB',
|
|
),
|
|
if (_error != null) ...[
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_error!,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.error,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: _saving ? null : () => Navigator.of(context).pop(),
|
|
child: const Text('Отмена'),
|
|
),
|
|
FilledButton(
|
|
onPressed: _saving ? null : _save,
|
|
child: const Text('Создать'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|