style(app): остальные экраны под новый визуальный язык и форматирование

Доходы, ребалансировка, налоги, импорт, цели, здоровье данных, правила, транзакции, категории, cashflow, pending: новые виджеты и токены темы, dart format. Тесты подстроены под новые модели.
This commit is contained in:
Dmitry
2026-09-19 22:14:27 +03:00
parent 62d36aa3e8
commit 322c60a359
55 changed files with 2158 additions and 1080 deletions
@@ -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),
),
],
),
+14 -6
View File
@@ -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>(