style(app): остальные экраны под новый визуальный язык и форматирование
Доходы, ребалансировка, налоги, импорт, цели, здоровье данных, правила, транзакции, категории, cashflow, pending: новые виджеты и токены темы, dart format. Тесты подстроены под новые модели.
This commit is contained in:
@@ -18,20 +18,29 @@ class _Group {
|
||||
final String rootName;
|
||||
final List<SpendingRow> rows = [];
|
||||
|
||||
Decimal get total => rows.fold(Decimal.zero, (a, r) => a + Decimal.parse(r.amountRub));
|
||||
Decimal get total =>
|
||||
rows.fold(Decimal.zero, (a, r) => a + Decimal.parse(r.amountRub));
|
||||
}
|
||||
|
||||
List<_Group> _group(List<SpendingRow> rows) {
|
||||
final byRoot = <int?, _Group>{};
|
||||
for (final r in rows) {
|
||||
final key = r.categoryId == null ? null : (r.rootCategoryId ?? r.categoryId);
|
||||
final key = r.categoryId == null
|
||||
? null
|
||||
: (r.rootCategoryId ?? r.categoryId);
|
||||
final group = byRoot.putIfAbsent(
|
||||
key,
|
||||
() => _Group(key, key == null ? 'Без категории' : (r.rootCategoryName ?? r.categoryName ?? '—')),
|
||||
() => _Group(
|
||||
key,
|
||||
key == null
|
||||
? 'Без категории'
|
||||
: (r.rootCategoryName ?? r.categoryName ?? '—'),
|
||||
),
|
||||
);
|
||||
group.rows.add(r);
|
||||
}
|
||||
final groups = byRoot.values.toList()..sort((a, b) => b.total.compareTo(a.total));
|
||||
final groups = byRoot.values.toList()
|
||||
..sort((a, b) => b.total.compareTo(a.total));
|
||||
return groups;
|
||||
}
|
||||
|
||||
@@ -51,7 +60,9 @@ class _CategoriesPageState extends ConsumerState<CategoriesPage> {
|
||||
super.initState();
|
||||
final initial = widget.initialMonth;
|
||||
if (initial != null) {
|
||||
Future.microtask(() => ref.read(selectedSpendingMonthProvider.notifier).state = initial);
|
||||
Future.microtask(
|
||||
() => ref.read(selectedSpendingMonthProvider.notifier).state = initial,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +77,9 @@ class _CategoriesPageState extends ConsumerState<CategoriesPage> {
|
||||
initialDatePickerMode: DatePickerMode.year,
|
||||
);
|
||||
if (picked != null) {
|
||||
ref.read(selectedSpendingMonthProvider.notifier).state =
|
||||
monthKey(DateTime(picked.year, picked.month));
|
||||
ref.read(selectedSpendingMonthProvider.notifier).state = monthKey(
|
||||
DateTime(picked.year, picked.month),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +146,10 @@ class _CategoriesPageState extends ConsumerState<CategoriesPage> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Всего расходов', style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(
|
||||
'Всего расходов',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
MoneyText(
|
||||
total.toString(),
|
||||
currency: 'RUB',
|
||||
@@ -144,7 +159,8 @@ class _CategoriesPageState extends ConsumerState<CategoriesPage> {
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
for (final g in groups) _GroupTile(group: g, maxTotal: maxTotal),
|
||||
for (final g in groups)
|
||||
_GroupTile(group: g, maxTotal: maxTotal),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -163,7 +179,8 @@ class _GroupTile extends StatelessWidget {
|
||||
final Decimal maxTotal;
|
||||
|
||||
bool get _flat =>
|
||||
group.rows.length == 1 && (group.rootId == null || group.rows.first.categoryId == group.rootId);
|
||||
group.rows.length == 1 &&
|
||||
(group.rootId == null || group.rows.first.categoryId == group.rootId);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -176,16 +193,26 @@ class _GroupTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
final children = [...group.rows]
|
||||
..sort((a, b) => Decimal.parse(b.amountRub).compareTo(Decimal.parse(a.amountRub)));
|
||||
..sort(
|
||||
(a, b) =>
|
||||
Decimal.parse(b.amountRub).compareTo(Decimal.parse(a.amountRub)),
|
||||
);
|
||||
return ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
title: _CategoryBar(name: group.rootName, amount: group.total, maxAmount: maxTotal, bold: true),
|
||||
title: _CategoryBar(
|
||||
name: group.rootName,
|
||||
amount: group.total,
|
||||
maxAmount: maxTotal,
|
||||
bold: true,
|
||||
),
|
||||
children: [
|
||||
for (final r in children)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: _CategoryBar(
|
||||
name: r.categoryId == group.rootId ? 'Без подкатегории' : (r.categoryName ?? '—'),
|
||||
name: r.categoryId == group.rootId
|
||||
? 'Без подкатегории'
|
||||
: (r.categoryName ?? '—'),
|
||||
amount: Decimal.parse(r.amountRub),
|
||||
maxAmount: group.total,
|
||||
bold: false,
|
||||
@@ -240,7 +267,9 @@ class _CategoryBar extends StatelessWidget {
|
||||
Container(
|
||||
height: 6,
|
||||
width: constraints.maxWidth * ratio,
|
||||
color: bold ? ChartColors.expense : ChartColors.expense.withValues(alpha: 0.6),
|
||||
color: bold
|
||||
? ChartColors.expense
|
||||
: ChartColors.expense.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,7 +7,9 @@ import '../../core/utils/ru_date.dart';
|
||||
|
||||
/// Flat ZenMoney tag tree — the client nests it by `parent_id` where needed. Shared with
|
||||
/// Транзакции; not wrapped in `Cached` since it is a lookup, not a screen's own primary read.
|
||||
final categoriesListProvider = FutureProvider.autoDispose<List<CategoryOut>>((ref) async {
|
||||
final categoriesListProvider = FutureProvider.autoDispose<List<CategoryOut>>((
|
||||
ref,
|
||||
) async {
|
||||
final r = await ref.watch(apiProvider).getCategoriesApi().categoriesList();
|
||||
return r.data ?? const [];
|
||||
});
|
||||
@@ -20,11 +22,17 @@ final categoryNamesProvider = Provider.autoDispose<Map<int, String>>((ref) {
|
||||
|
||||
/// Spending by category for one month (`YYYY-MM`); null means "the latest month". See
|
||||
/// `docs/ai/offline-cache.md`.
|
||||
final spendingProvider =
|
||||
FutureProvider.autoDispose.family<Cached<List<SpendingRow>>, String?>((ref, month) async {
|
||||
final r = await ref.watch(apiProvider).getCashflowApi().cashflowSpending(month: month);
|
||||
return Cached(r.data ?? const [], fetchedAt: r.extra['fetchedAt'] as DateTime?);
|
||||
});
|
||||
final spendingProvider = FutureProvider.autoDispose
|
||||
.family<Cached<List<SpendingRow>>, String?>((ref, month) async {
|
||||
final r = await ref
|
||||
.watch(apiProvider)
|
||||
.getCashflowApi()
|
||||
.cashflowSpending(month: month);
|
||||
return Cached(
|
||||
r.data ?? const [],
|
||||
fetchedAt: r.extra['fetchedAt'] as DateTime?,
|
||||
);
|
||||
});
|
||||
|
||||
/// The month currently selected on the Категории screen, `YYYY-MM`.
|
||||
final selectedSpendingMonthProvider = StateProvider.autoDispose<String>(
|
||||
|
||||
Reference in New Issue
Block a user