feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import 'package:fintracker_api/fintracker_api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'labels.dart';
|
||||
|
||||
/// Edit form for the fields of an instrument a person may correct. Returns a patch holding
|
||||
/// only what changed, or null when cancelled or nothing changed.
|
||||
class InstrumentEditDialog extends StatefulWidget {
|
||||
const InstrumentEditDialog({required this.instrument, super.key});
|
||||
|
||||
final InstrumentOut instrument;
|
||||
|
||||
@override
|
||||
State<InstrumentEditDialog> createState() => _InstrumentEditDialogState();
|
||||
}
|
||||
|
||||
class _InstrumentEditDialogState extends State<InstrumentEditDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final _name = TextEditingController(text: widget.instrument.name);
|
||||
late final _board = TextEditingController(
|
||||
text: widget.instrument.board ?? '',
|
||||
);
|
||||
late final _lot = TextEditingController(text: '${widget.instrument.lot}');
|
||||
late final _sector = TextEditingController(
|
||||
text: widget.instrument.sector ?? '',
|
||||
);
|
||||
late String _assetClass = widget.instrument.assetClass;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_name.dispose();
|
||||
_board.dispose();
|
||||
_lot.dispose();
|
||||
_sector.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
InstrumentPatch? _patch() {
|
||||
final i = widget.instrument;
|
||||
final name = _name.text.trim();
|
||||
final board = _board.text.trim();
|
||||
final sector = _sector.text.trim();
|
||||
final lot = int.parse(_lot.text.trim());
|
||||
final patch = InstrumentPatch(
|
||||
name: name != i.name ? name : null,
|
||||
assetClass: _assetClass != i.assetClass ? _assetClass : null,
|
||||
// an empty field means "leave as is": the wire format cannot express "clear"
|
||||
board: board.isNotEmpty && board != (i.board ?? '') ? board : null,
|
||||
lot: lot != i.lot ? lot : null,
|
||||
sector: sector.isNotEmpty && sector != (i.sector ?? '') ? sector : null,
|
||||
);
|
||||
final changed =
|
||||
patch.name != null ||
|
||||
patch.assetClass != null ||
|
||||
patch.board != null ||
|
||||
patch.lot != null ||
|
||||
patch.sector != null;
|
||||
return changed ? patch : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Инструмент ${widget.instrument.ticker ?? ''}'.trim()),
|
||||
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<String>(
|
||||
initialValue: assetClassLabels.containsKey(_assetClass)
|
||||
? _assetClass
|
||||
: null,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(labelText: 'Класс актива'),
|
||||
items: [
|
||||
for (final e in assetClassLabels.entries)
|
||||
DropdownMenuItem(
|
||||
value: e.key,
|
||||
child: Text('${e.value} (${e.key})'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) =>
|
||||
setState(() => _assetClass = v ?? _assetClass),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _board,
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Доска (TQBR, TQTF…)',
|
||||
helperText: 'По ней синк Мосбиржи находит котировки',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _lot,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Лот',
|
||||
helperText:
|
||||
'Ребалансировка округляет сделки до целого числа лотов',
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
validator: (v) {
|
||||
final n = int.tryParse((v ?? '').trim());
|
||||
return n == null || n < 1
|
||||
? 'Целое число не меньше 1'
|
||||
: null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _sector,
|
||||
decoration: const InputDecoration(labelText: 'Сектор'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
Navigator.of(context).pop(_patch());
|
||||
},
|
||||
child: const Text('Сохранить'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user