feat(app): календарь выплат сеткой по месяцам
Месячная сетка с дивидендами, купонами, амортизациями и погашениями по дням, выплаченными и ожидаемыми, с сводкой за месяц и списком выплат выбранного дня. Раскладка без прокрутки страницы: высота ячеек подстраивается под окно, на широком экране список справа. Прежний список остался вторым режимом.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/utils/ru_date.dart';
|
||||
import '../../core/widgets/money_text.dart';
|
||||
import '../portfolio/labels.dart' show formatQty;
|
||||
import 'data/income_api.dart';
|
||||
import 'labels.dart';
|
||||
|
||||
/// One payment as a list row: what, when, on which basis, and how much. The basis chip is on
|
||||
/// every row on purpose — it is what tells a declared payment from an extrapolated one.
|
||||
class IncomeEntryRow extends StatelessWidget {
|
||||
const IncomeEntryRow({required this.entry, super.key});
|
||||
|
||||
final IncomeEntry entry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final details = [
|
||||
incomeKindLabel(entry.kind),
|
||||
if (entry.qty != null && entry.perUnit != null)
|
||||
'${formatQty(entry.qty!)} × ${MoneyText.format(entry.perUnit!, entry.currency)}',
|
||||
if (entry.recordDate != null) 'отсечка ${ruDate(entry.recordDate!)}',
|
||||
if (entry.taxWithheld != null)
|
||||
'налог ${MoneyText.format(entry.taxWithheld!, entry.currency)}',
|
||||
].join(' · ');
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
onTap: entry.instrumentId == null
|
||||
? null
|
||||
: () => context.push('/portfolio/instrument/${entry.instrumentId}'),
|
||||
leading: Icon(
|
||||
incomeKindIcon(entry.kind),
|
||||
color: basisColor(entry.basis),
|
||||
size: 20,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Flexible(child: Text(entry.title, overflow: TextOverflow.ellipsis)),
|
||||
const SizedBox(width: 8),
|
||||
BasisChip(basis: entry.basis),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'${entry.expectedDate == null ? 'дата неизвестна' : ruDate(entry.expectedDate!)} · $details',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (entry.amount != null)
|
||||
MoneyText(
|
||||
entry.amount!,
|
||||
currency: entry.currency,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
// no FX rate for the date ⇒ no rouble figure. An em dash, never 0 ₽.
|
||||
if (entry.currency != 'RUB')
|
||||
Text(
|
||||
entry.amountRub == null
|
||||
? '— ₽ (нет курса)'
|
||||
: MoneyText.format(entry.amountRub!, 'RUB'),
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user