import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/utils/ru_date.dart'; import '../../core/widgets/async_value_view.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/money_text.dart'; import '../../core/widgets/section_card.dart'; import '../portfolio/labels.dart' show formatQty, signColor; import 'data/tax_api.dart'; import 'providers.dart'; /// Лоты и ЛДВ: the practical screen of the phase — what a sale costs **today** versus what /// it costs after the three-year mark. /// /// Lots close to the ЛДВ date are called out, because selling a lot 20 days early is the /// one mistake this screen exists to prevent. class TaxLotsTab extends ConsumerStatefulWidget { const TaxLotsTab({super.key}); @override ConsumerState createState() => _TaxLotsTabState(); } class _TaxLotsTabState extends ConsumerState { bool _onlyNearLdv = false; @override Widget build(BuildContext context) { final lots = ref.watch(taxLotsProvider); return RefreshIndicator( onRefresh: () async => ref.invalidate(taxLotsProvider), child: AsyncValueView( value: lots, onRetry: () => ref.invalidate(taxLotsProvider), data: (cached) { final all = cached.data; final near = all.where((l) => l.nearLdv).toList(); // a copy: the provider's list must not be reordered under other watchers final rows = [...(_onlyNearLdv ? near : all)]; // soonest ЛДВ first among the lots that do not have it yet, eligible ones last rows.sort((a, b) { if (a.ldvEligible != b.ldvEligible) return a.ldvEligible ? 1 : -1; return (a.daysToLdv ?? 1 << 30).compareTo(b.daysToLdv ?? 1 << 30); }); if (all.isEmpty) { return ListView( padding: const EdgeInsets.all(16), children: const [ SizedBox(height: 48), EmptyState( icon: Icons.inventory_2_outlined, message: 'Открытых лотов нет.', ), ], ); } return ListView( padding: const EdgeInsets.all(16), children: [ if (near.isNotEmpty) Card( color: Theme.of(context).colorScheme.tertiaryContainer, child: ListTile( leading: const Icon(Icons.schedule), title: Text('До ЛДВ меньше полугода: ${near.length} лот(ов)'), subtitle: const Text( 'Продажа до этой даты облагается налогом на весь прирост.'), trailing: FilterChip( label: const Text('только они'), selected: _onlyNearLdv, onSelected: (v) => setState(() => _onlyNearLdv = v), ), ), ), const SizedBox(height: 12), SectionCard( title: 'Открытые лоты', subtitle: 'налог при продаже сегодня — оценка по ставке из сводки', child: _LotsTable(rows: rows), ), ], ); }, ), ); } } class _LotsTable extends StatelessWidget { const _LotsTable({required this.rows}); final List rows; @override Widget build(BuildContext context) { final theme = Theme.of(context); final headerStyle = theme.textTheme.labelMedium; return SingleChildScrollView( scrollDirection: Axis.horizontal, child: DataTable( columns: [ DataColumn(label: Text('Бумага', style: headerStyle)), DataColumn(label: Text('Куплен', style: headerStyle)), DataColumn(label: Text('Кол-во', style: headerStyle), numeric: true), DataColumn(label: Text('Стоимость', style: headerStyle), numeric: true), DataColumn(label: Text('Рынок', style: headerStyle), numeric: true), DataColumn(label: Text('Нереализ.', style: headerStyle), numeric: true), DataColumn(label: Text('Дата ЛДВ', style: headerStyle)), DataColumn(label: Text('Дней до ЛДВ', style: headerStyle), numeric: true), DataColumn(label: Text('Налог при продаже', style: headerStyle), numeric: true), ], rows: [ for (final l in rows) DataRow( color: l.nearLdv ? WidgetStatePropertyAll( theme.colorScheme.tertiaryContainer.withValues(alpha: 0.5)) : null, onSelectChanged: l.instrumentId == null ? null : (_) => context.push('/portfolio/instrument/${l.instrumentId}'), cells: [ DataCell(Row( mainAxisSize: MainAxisSize.min, children: [ Text(l.title), if (l.nearLdv) ...[ const SizedBox(width: 6), Tooltip( message: 'До ЛДВ осталось ${l.daysToLdv} дн. — ' 'продажа сейчас облагается налогом полностью', child: Icon(Icons.schedule, size: 16, color: theme.colorScheme.error), ), ], ], )), DataCell(Text(l.openDate == null ? '—' : ruDate(l.openDate!))), DataCell(Text(l.qtyRemaining == null ? '—' : formatQty(l.qtyRemaining!))), DataCell(l.costRub == null ? const Text('—') : MoneyText(l.costRub!, currency: 'RUB')), DataCell(l.marketValueRub == null ? const Text('—') : MoneyText(l.marketValueRub!, currency: 'RUB')), DataCell(l.unrealizedGainRub == null ? const Text('—') : MoneyText( l.unrealizedGainRub!, currency: 'RUB', style: TextStyle(color: signColor(context, l.unrealizedGainRub)), )), DataCell(l.ldvEligible ? const Text('уже действует') : Text(l.ldvDate == null ? '—' : ruDate(l.ldvDate!))), DataCell(l.ldvEligible ? const Text('—') : Text( l.daysToLdv == null ? '—' : '${l.daysToLdv}', style: l.nearLdv ? TextStyle( color: theme.colorScheme.error, fontWeight: FontWeight.w600) : null, )), DataCell(l.taxIfSoldNowRub == null ? const Text('—') : MoneyText(l.taxIfSoldNowRub!, currency: 'RUB')), ], ), ], ), ); } }