Позиции и аллокация сделаны вкладками одного экрана, а не двумя пунктами навигации: они отвечают на две половины одного вопроса, и десятый пункт в bottom bar оставил бы по сорок пикселей на подпись. Scope переключается один раз и сразу для всех трёх экранов — три экрана с разными scope были бы ловушкой. Позиция без цены показывается прочерком, никогда нулём: её стоимость не входит в итоги выше, и 0 ₽ читался бы как «ничего не стоит» вместо «неизвестно». То же для общей прибыли, когда в портфеле есть хоть одна неоценённая бумага. Подписи ключей берутся по .value, а не по .name: генератор придумывает Dart-имя (assetClass для asset_class), и словарь, ключёванный по .name, молча не совпадает никогда. Тесты пампят экран на высокой поверхности: вкладки это длинные списки, а ListView строит только видимое, и на дефолтных 800x600 таблица позиций просто не существует.
71 lines
2.3 KiB
Dart
71 lines
2.3 KiB
Dart
import 'package:fintracker_api/fintracker_api.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../core/widgets/async_value_view.dart';
|
|
import 'allocation_tab.dart';
|
|
import 'holdings_tab.dart';
|
|
import 'providers.dart';
|
|
|
|
/// Портфель: позиции и аллокация as two tabs of one screen, sharing one scope.
|
|
///
|
|
/// They are tabs rather than two navigation destinations because they answer two halves of
|
|
/// the same question, and because a tenth item in the bottom bar would leave 40 px per
|
|
/// label on a phone.
|
|
class PortfolioPage extends ConsumerWidget {
|
|
const PortfolioPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return DefaultTabController(
|
|
length: 2,
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Портфель'),
|
|
actions: const [_ScopeSelector(), SizedBox(width: 8)],
|
|
bottom: const TabBar(
|
|
tabs: [Tab(text: 'Позиции'), Tab(text: 'Аллокация')],
|
|
),
|
|
),
|
|
body: const TabBarView(children: [HoldingsTab(), AllocationTab()]),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Switches every portfolio screen at once. Hidden while there is nothing to choose
|
|
/// between — a dropdown with one option is furniture, not a control.
|
|
class _ScopeSelector extends ConsumerWidget {
|
|
const _ScopeSelector();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final scopes = ref.watch(scopesProvider);
|
|
final current = ref.watch(scopeProvider);
|
|
|
|
return AsyncValueView<List<ScopeOut>>(
|
|
value: scopes,
|
|
data: (rows) {
|
|
if (rows.length < 2) return const SizedBox.shrink();
|
|
final known = rows.any((s) => s.scope == current) ? current : rows.first.scope;
|
|
return DropdownButtonHideUnderline(
|
|
child: DropdownButton<String>(
|
|
value: known,
|
|
borderRadius: BorderRadius.circular(8),
|
|
items: [
|
|
for (final s in rows)
|
|
DropdownMenuItem(
|
|
value: s.scope,
|
|
child: Text(s.name, overflow: TextOverflow.ellipsis),
|
|
),
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) ref.read(scopeProvider.notifier).state = value;
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|