Files
fin-tracker/app/lib/features/portfolio/instrument_edit_dialog.dart
Dmitry 62d36aa3e8 feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
2026-09-19 22:14:21 +03:00

148 lines
5.3 KiB
Dart

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('Сохранить'),
),
],
);
}
}