feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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('Создать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user