feat(app): экраны портфеля — позиции, аллокация и карточка инструмента

Позиции и аллокация сделаны вкладками одного экрана, а не двумя пунктами навигации:
они отвечают на две половины одного вопроса, и десятый пункт в bottom bar оставил бы
по сорок пикселей на подпись. Scope переключается один раз и сразу для всех трёх
экранов — три экрана с разными scope были бы ловушкой.

Позиция без цены показывается прочерком, никогда нулём: её стоимость не входит в
итоги выше, и 0 ₽ читался бы как «ничего не стоит» вместо «неизвестно». То же для
общей прибыли, когда в портфеле есть хоть одна неоценённая бумага.

Подписи ключей берутся по .value, а не по .name: генератор придумывает Dart-имя
(assetClass для asset_class), и словарь, ключёванный по .name, молча не совпадает
никогда.

Тесты пампят экран на высокой поверхности: вкладки это длинные списки, а ListView
строит только видимое, и на дефолтных 800x600 таблица позиций просто не существует.
This commit is contained in:
Dmitry
2026-09-18 14:21:06 +03:00
parent 28ff63bdfa
commit 1d7769ffbf
11 changed files with 1530 additions and 2 deletions
+9 -2
View File
@@ -12,6 +12,7 @@ class _Destination {
const _destinations = [
_Destination('/', Icons.dashboard_outlined, Icons.dashboard, 'Обзор'),
_Destination('/accounts', Icons.account_balance_outlined, Icons.account_balance, 'Счета'),
_Destination('/portfolio', Icons.pie_chart_outline, Icons.pie_chart, 'Портфель'),
_Destination('/cashflow', Icons.swap_horiz_outlined, Icons.swap_horiz, 'Потоки'),
_Destination('/categories', Icons.category_outlined, Icons.category, 'Категории'),
_Destination(
@@ -36,8 +37,14 @@ class AppShell extends StatelessWidget {
final Widget child;
int get _selectedIndex {
final i = _destinations.indexWhere((d) => d.path == location);
return i == -1 ? 0 : i;
// longest prefix wins, so /portfolio/instrument/311 keeps Портфель selected
var best = -1;
for (var i = 0; i < _destinations.length; i++) {
final path = _destinations[i].path;
final matches = path == '/' ? location == '/' : location.startsWith(path);
if (matches && (best == -1 || path.length > _destinations[best].path.length)) best = i;
}
return best == -1 ? 0 : best;
}
void _onSelect(BuildContext context, int index) {