Files
fin-tracker/app/lib/features/tax/tax_page.dart
T
Dmitry 7b419f4188 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 отдельно, второй — чистые действия без списка для баннера).
2026-09-19 14:00:13 +03:00

112 lines
3.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/cache/cached.dart';
import '../../core/widgets/stale_banner.dart';
import 'data/tax_api.dart';
import 'lots_tab.dart';
import 'providers.dart';
import 'summary_tab.dart';
/// Налоги: the year summary and the open-lot list with ЛДВ dates.
///
/// The word «оценка» is on the screen itself, not in a tooltip: the tax agent is the
/// broker, and these numbers exist so that the broker's statement can be checked against
/// something — not to replace it.
class TaxPage extends ConsumerWidget {
const TaxPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final stale = oldestFetch([
ref.watch(taxSummaryProvider).valueOrNull?.fetchedAt,
ref.watch(taxLotsProvider).valueOrNull?.fetchedAt,
]);
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Налоги (оценка)'),
actions: [
const _YearSelector(),
IconButton(
tooltip: 'Обновить',
icon: const Icon(Icons.refresh),
onPressed: () => invalidateTaxProviders(ref),
),
],
bottom: const TabBar(
tabs: [Tab(text: 'Сводка за год'), Tab(text: 'Лоты и ЛДВ')],
),
),
body: Column(
children: [
const EstimateBanner(),
if (stale != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: StaleBanner(fetchedAt: stale),
),
const Expanded(child: TabBarView(children: [TaxSummaryTab(), TaxLotsTab()])),
],
),
),
);
}
}
/// The disclaimer, always visible above both tabs.
class EstimateBanner extends ConsumerWidget {
const EstimateBanner({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final summary = ref.watch(taxSummaryProvider).valueOrNull?.data;
final scheme = Theme.of(context).colorScheme;
final text = summary?.disclaimer ?? TaxSummary.defaultDisclaimer;
return Material(
color: scheme.tertiaryContainer,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
const Icon(Icons.info_outline, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
),
);
}
}
class _YearSelector extends ConsumerWidget {
const _YearSelector();
@override
Widget build(BuildContext context, WidgetRef ref) {
final year = ref.watch(taxYearProvider);
final now = DateTime.now().year;
return DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: year,
borderRadius: BorderRadius.circular(8),
items: [
for (var y = now; y >= now - 6; y--)
DropdownMenuItem(value: y, child: Text('$y')),
],
onChanged: (v) {
if (v != null) ref.read(taxYearProvider.notifier).state = v;
},
),
);
}
}