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
+89 -39
View File
@@ -13,24 +13,24 @@ import '../../core/widgets/stale_banner.dart';
import 'providers.dart';
String ruleKindLabel(RuleKind k) => switch (k) {
RuleKind.savings => 'Сбережения',
RuleKind.oneOff => 'Разовая трата',
RuleKind.category => 'Категория',
RuleKind.payee => 'Плательщик',
RuleKind.brokerTarget => 'Целевой брокер',
RuleKind.ignore => 'Игнорировать',
RuleKind.unknownDefaultOpenApi => 'Неизвестно',
};
RuleKind.savings => 'Сбережения',
RuleKind.oneOff => 'Разовая трата',
RuleKind.category => 'Категория',
RuleKind.payee => 'Плательщик',
RuleKind.brokerTarget => 'Целевой брокер',
RuleKind.ignore => 'Игнорировать',
RuleKind.unknownDefaultOpenApi => 'Неизвестно',
};
String ruleMatchTypeLabel(RuleMatchType t) => switch (t) {
RuleMatchType.id => 'ID транзакции',
RuleMatchType.payee => 'Плательщик',
RuleMatchType.comment => 'Комментарий',
RuleMatchType.category => 'Категория',
RuleMatchType.mcc => 'MCC',
RuleMatchType.account => 'Счёт',
RuleMatchType.unknownDefaultOpenApi => 'Неизвестно',
};
RuleMatchType.id => 'ID транзакции',
RuleMatchType.payee => 'Плательщик',
RuleMatchType.comment => 'Комментарий',
RuleMatchType.category => 'Категория',
RuleMatchType.mcc => 'MCC',
RuleMatchType.account => 'Счёт',
RuleMatchType.unknownDefaultOpenApi => 'Неизвестно',
};
const _kinds = [
RuleKind.savings,
@@ -65,9 +65,13 @@ class RulesPage extends ConsumerWidget {
try {
final r = await ref.read(apiProvider).getRulesApi().rulesApply();
final ok = r.data?.error == null;
messenger.showSnackBar(SnackBar(
content: Text(ok ? 'Правила применены' : 'Ошибка применения: ${r.data?.error}'),
));
messenger.showSnackBar(
SnackBar(
content: Text(
ok ? 'Правила применены' : 'Ошибка применения: ${r.data?.error}',
),
),
);
} on DioException catch (e) {
messenger.showSnackBar(SnackBar(content: Text(problemMessage(e))));
} finally {
@@ -113,11 +117,15 @@ class RulesPage extends ConsumerWidget {
return ListView(
children: [
if (stale != null) StaleBanner(fetchedAt: stale),
const EmptyState(icon: Icons.rule_folder_outlined, message: 'Правил ещё нет.'),
const EmptyState(
icon: Icons.rule_folder_outlined,
message: 'Правил ещё нет.',
),
],
);
}
final sorted = [...rows]..sort((a, b) => a.priority.compareTo(b.priority));
final sorted = [...rows]
..sort((a, b) => a.priority.compareTo(b.priority));
return ListView(
padding: const EdgeInsets.all(16),
children: [
@@ -138,7 +146,11 @@ class RulesPage extends ConsumerWidget {
}
class _RuleTile extends ConsumerStatefulWidget {
const _RuleTile({required this.rule, required this.stale, required this.onChanged});
const _RuleTile({
required this.rule,
required this.stale,
required this.onChanged,
});
final RuleOut rule;
final bool stale;
@@ -154,14 +166,18 @@ class _RuleTileState extends ConsumerState<_RuleTile> {
Future<void> _toggle(bool enabled) async {
setState(() => _busy = true);
try {
await ref.read(apiProvider).getRulesApi().rulesPatch(
await ref
.read(apiProvider)
.getRulesApi()
.rulesPatch(
ruleId: widget.rule.id,
rulePatch: RulePatch(enabled: enabled),
);
widget.onChanged();
} on DioException catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(problemMessage(e))));
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(problemMessage(e))));
} finally {
if (mounted) setState(() => _busy = false);
}
@@ -174,18 +190,28 @@ class _RuleTileState extends ConsumerState<_RuleTile> {
title: const Text('Удалить правило?'),
content: Text('«${widget.rule.pattern}» — действие необратимо.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Отмена')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Удалить')),
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Удалить'),
),
],
),
);
if (confirmed != true) return;
try {
await ref.read(apiProvider).getRulesApi().rulesDelete(ruleId: widget.rule.id);
await ref
.read(apiProvider)
.getRulesApi()
.rulesDelete(ruleId: widget.rule.id);
widget.onChanged();
} on DioException catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(problemMessage(e))));
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(problemMessage(e))));
}
}
@@ -196,7 +222,8 @@ class _RuleTileState extends ConsumerState<_RuleTile> {
child: ListTile(
onTap: () => showDialog(
context: context,
builder: (_) => _RuleDialog(existing: rule, onSaved: widget.onChanged),
builder: (_) =>
_RuleDialog(existing: rule, onSaved: widget.onChanged),
),
title: Row(
children: [
@@ -222,7 +249,8 @@ class _RuleTileState extends ConsumerState<_RuleTile> {
[
'${rule.pattern}${rule.value ?? ''}',
'совпадений: ${rule.matchCount}',
if (rule.lastMatchedAt != null) 'последнее: ${ruDate(rule.lastMatchedAt!.toLocal())}',
if (rule.lastMatchedAt != null)
'последнее: ${ruDate(rule.lastMatchedAt!.toLocal())}',
].join(' · '),
),
),
@@ -230,7 +258,10 @@ class _RuleTileState extends ConsumerState<_RuleTile> {
mainAxisSize: MainAxisSize.min,
children: [
Switch(value: rule.enabled, onChanged: _busy ? null : _toggle),
IconButton(icon: const Icon(Icons.delete_outline), onPressed: _delete),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: _delete,
),
],
),
),
@@ -317,7 +348,8 @@ class _RuleDialogState extends ConsumerState<_RuleDialog> {
if (mounted) Navigator.pop(context);
} on DioException catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(problemMessage(e))));
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(problemMessage(e))));
} finally {
if (mounted) setState(() => _saving = false);
}
@@ -337,7 +369,10 @@ class _RuleDialogState extends ConsumerState<_RuleDialog> {
DropdownButtonFormField<RuleKind>(
initialValue: _kind,
decoration: const InputDecoration(labelText: 'Тип правила'),
items: [for (final k in _kinds) DropdownMenuItem(value: k, child: Text(ruleKindLabel(k)))],
items: [
for (final k in _kinds)
DropdownMenuItem(value: k, child: Text(ruleKindLabel(k))),
],
onChanged: (v) => setState(() => _kind = v!),
),
DropdownButtonFormField<RuleMatchType>(
@@ -345,18 +380,27 @@ class _RuleDialogState extends ConsumerState<_RuleDialog> {
decoration: const InputDecoration(labelText: 'Совпадение по'),
items: [
for (final t in _matchTypes)
DropdownMenuItem(value: t, child: Text(ruleMatchTypeLabel(t))),
DropdownMenuItem(
value: t,
child: Text(ruleMatchTypeLabel(t)),
),
],
onChanged: (v) => setState(() => _matchType = v!),
),
TextFormField(
controller: _pattern,
decoration: const InputDecoration(labelText: 'Шаблон (pattern)'),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Обязательное поле' : null,
decoration: const InputDecoration(
labelText: 'Шаблон (pattern)',
),
validator: (v) => (v == null || v.trim().isEmpty)
? 'Обязательное поле'
: null,
),
TextFormField(
controller: _value,
decoration: const InputDecoration(labelText: 'Значение (value)'),
decoration: const InputDecoration(
labelText: 'Значение (value)',
),
),
TextFormField(
controller: _note,
@@ -378,8 +422,14 @@ class _RuleDialogState extends ConsumerState<_RuleDialog> {
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Отмена')),
FilledButton(onPressed: _saving ? null : _save, child: const Text('Сохранить')),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Отмена'),
),
FilledButton(
onPressed: _saving ? null : _save,
child: const Text('Сохранить'),
),
],
);
}