feat(app): offline-кэш на остальных экранах — фаза 5

accounts, cashflow, categories, goals, income, portfolio (+instrument),
rebalance, tax, rules переведены на Cached<T> по контракту
docs/ai/offline-cache.md. Общие/второстепенные селекторы (scopesProvider,
categoriesListProvider и т.п.) оставлены как есть — не основной контент
экрана. sync_page.dart и settings_page.dart не тронуты (первый — заменён
health-page отдельно, второй — чистые действия без списка для баннера).
This commit is contained in:
Dmitry
2026-09-19 14:00:13 +03:00
parent ffc5ed959a
commit 7b419f4188
39 changed files with 388 additions and 186 deletions
+27 -1
View File
@@ -2,8 +2,11 @@ import 'package:fintracker_api/fintracker_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/cache/cached.dart';
import '../../core/widgets/async_value_view.dart';
import '../../core/widgets/stale_banner.dart';
import 'allocation_tab.dart';
import 'benchmarks_card.dart';
import 'holdings_tab.dart';
import 'providers.dart';
@@ -12,11 +15,23 @@ import 'providers.dart';
/// 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.
///
/// One offline banner covers both tabs (`docs/ai/offline-cache.md`) — `TabBarView` builds
/// both eagerly anyway, so watching every provider here to compute it costs nothing extra.
class PortfolioPage extends ConsumerWidget {
const PortfolioPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final stale = oldestFetch([
ref.watch(portfolioSummaryProvider).valueOrNull?.fetchedAt,
ref.watch(holdingsProvider).valueOrNull?.fetchedAt,
ref.watch(valueSeriesProvider).valueOrNull?.fetchedAt,
ref.watch(portfolioReturnsProvider).valueOrNull?.fetchedAt,
ref.watch(benchmarkRowsProvider).valueOrNull?.fetchedAt,
ref.watch(allocationProvider).valueOrNull?.fetchedAt,
]);
return DefaultTabController(
length: 2,
child: Scaffold(
@@ -27,7 +42,18 @@ class PortfolioPage extends ConsumerWidget {
tabs: [Tab(text: 'Позиции'), Tab(text: 'Аллокация')],
),
),
body: const TabBarView(children: [HoldingsTab(), AllocationTab()]),
body: Column(
children: [
if (stale != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: StaleBanner(fetchedAt: stale),
),
const Expanded(
child: TabBarView(children: [HoldingsTab(), AllocationTab()]),
),
],
),
),
);
}