import 'package:decimal/decimal.dart'; import 'package:fintracker_api/fintracker_api.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/chart_colors.dart'; import '../../core/widgets/help_tip.dart'; import '../../core/widgets/money_text.dart'; import '../portfolio/labels.dart' show formatPercent, signColor; import '../portfolio/providers.dart' show scopeProvider; /// The home grid: a card per portfolio and account — value, result, the last day, return and /// expected passive income at a glance. A tap opens Портфель scoped to that card. class ScopeCards extends ConsumerWidget { const ScopeCards({required this.cards, super.key}); final List cards; static const _gap = 16.0; @override Widget build(BuildContext context, WidgetRef ref) { return LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth; final columns = width >= 980 ? 3 : (width >= 620 ? 2 : 1); final itemWidth = (width - _gap * (columns - 1)) / columns; return Wrap( spacing: _gap, runSpacing: _gap, children: [ for (final c in cards) SizedBox( width: itemWidth, child: ScopeCard( card: c, onTap: () { ref.read(scopeProvider.notifier).state = c.scope; context.go('/portfolio'); }, ), ), ], ); }, ); } } IconData _icon(String kind) => switch (kind) { 'all' => Icons.layers_outlined, 'portfolio' => Icons.layers, _ => Icons.account_balance_wallet_outlined, }; class ScopeCard extends StatelessWidget { const ScopeCard({required this.card, required this.onTap, super.key}); final ScopeCardOut card; final VoidCallback onTap; static String _money(String? v) => v == null ? '—' : MoneyText.format(v, 'RUB'); /// `+6 643,47 ₽` — a signed amount; a plain minus and plus, never a bare number. static String _signedMoney(String? v) { if (v == null) return '—'; final d = Decimal.parse(v); final text = MoneyText.format(v, 'RUB'); return d > Decimal.zero ? '+$text' : text; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final label = theme.textTheme.bodyMedium?.copyWith( color: scheme.onSurfaceVariant, ); Widget row(String name, Widget value) => Padding( padding: const EdgeInsets.only(top: 6), child: Row( children: [ Expanded( child: Align( alignment: Alignment.centerLeft, child: TermLabel(name, style: label), ), ), value, ], ), ); /// «+6 643,47 ₽ (▲ 1,5 %)» in the colour of its sign. Widget change(String? amount, String? share) { if (amount == null) return const Text('—'); final color = signColor(context, amount) ?? scheme.onSurface; final positive = Decimal.parse(amount) > Decimal.zero; final negative = Decimal.parse(amount) < Decimal.zero; return Row( mainAxisSize: MainAxisSize.min, children: [ Text(_signedMoney(amount), style: TextStyle(color: color)), if (share != null) ...[ Text(' (', style: TextStyle(color: color)), if (positive || negative) Icon( positive ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18, color: color, ), Text( formatPercent(share, signed: false).replaceAll('-', ''), style: TextStyle(color: color), ), Text(')', style: TextStyle(color: color)), ], ], ); } final income = Decimal.parse(card.incomeYearRub); return Card( child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( _icon(card.kind), size: 18, color: scheme.onSurfaceVariant, ), const SizedBox(width: 8), Expanded( child: Text( card.name.toUpperCase(), overflow: TextOverflow.ellipsis, style: theme.textTheme.labelLarge?.copyWith( color: scheme.onSurface, letterSpacing: 0.4, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 12), Text( _money(card.totalRub), style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), row('Прибыль', change(card.pnlRub, card.pnlPct)), row('За день', change(card.dayChangeRub, card.dayChangePct)), row( 'Доходность', Text( card.xirr == null ? '—' : formatPercent(card.xirr, signed: false), style: TextStyle(color: signColor(context, card.xirr)), ), ), row( 'Пассивный доход', Text( income == Decimal.zero ? '—' : '${card.incomeYearPct == null ? '' : '${formatPercent(card.incomeYearPct, signed: false)} '}' '(${MoneyText.format(card.incomeYearRub, 'RUB')})', style: TextStyle( color: income == Decimal.zero ? scheme.onSurfaceVariant : ChartColors.gain, ), ), ), ], ), ), ), ); } }