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
+85 -31
View File
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/utils/json.dart';
import '../../core/widgets/async_value_view.dart';
import '../../core/widgets/section_card.dart';
import '../../core/widgets/help_tip.dart';
import '../portfolio/labels.dart' show assetClassLabels;
import 'data/rebalance_api.dart';
import 'labels.dart';
@@ -38,7 +39,8 @@ class _TargetsTabState extends ConsumerState<TargetsTab> {
_dirty = false;
}
Decimal get _sum => sumDecimals((_draft ?? const []).map((t) => t.targetWeight));
Decimal get _sum =>
sumDecimals((_draft ?? const []).map((t) => t.targetWeight));
bool get _sumIsValid => (_sum - Decimal.one).abs() <= Decimal.parse('0.0001');
@@ -52,13 +54,17 @@ class _TargetsTabState extends ConsumerState<TargetsTab> {
try {
await ref
.read(rebalanceApiProvider)
.putTargets(portfolioId, TargetSet(dimension: dimension, targets: draft));
.putTargets(
portfolioId,
TargetSet(dimension: dimension, targets: draft),
);
if (!mounted) return;
_dirty = false;
_seededFor = null; // refetch reseeds the draft from the saved set
invalidateRebalanceProviders(ref);
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Целевые веса сохранены')));
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Целевые веса сохранены')));
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context)
@@ -84,11 +90,16 @@ class _TargetsTabState extends ConsumerState<TargetsTab> {
return ListView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 88),
children: [
_SumBanner(sum: _sum, valid: _sumIsValid, serverSum: set.weightsSum),
_SumBanner(
sum: _sum,
valid: _sumIsValid,
serverSum: set.weightsSum,
),
const SizedBox(height: 12),
SectionCard(
title: 'Целевые веса',
subtitle: 'Вес — доля портфеля; допуск (band) — ширина коридора, '
subtitle:
'Вес — доля портфеля; допуск (band) — ширина коридора, '
'внутри которого сделки не предлагаются.',
child: Column(
children: [
@@ -118,7 +129,11 @@ class _TargetsTabState extends ConsumerState<TargetsTab> {
onPressed: () => setState(() {
_draft = [
...draft,
const TargetWeight(bucket: '', targetWeight: '0', band: '0.05'),
const TargetWeight(
bucket: '',
targetWeight: '0',
band: '0.05',
),
];
_dirty = true;
}),
@@ -133,12 +148,18 @@ class _TargetsTabState extends ConsumerState<TargetsTab> {
Row(
children: [
FilledButton.icon(
onPressed: _saving || !_sumIsValid || draft.any((t) => t.bucket.isEmpty)
onPressed:
_saving ||
!_sumIsValid ||
draft.any((t) => t.bucket.isEmpty)
? null
: _save,
icon: _saving
? const SizedBox(
width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.save_outlined),
label: const Text('Сохранить'),
),
@@ -192,7 +213,7 @@ class _SumBanner extends StatelessWidget {
valid
? 'Набор можно сохранить.'
: 'Нужно ровно 100 %. Разница: '
'${formatShareAsPercent((sum - Decimal.one).toString(), signed: true)}',
'${formatShareAsPercent((sum - Decimal.one).toString(), signed: true)}',
),
trailing: serverSum == null
? null
@@ -224,12 +245,15 @@ class _TargetRow extends StatefulWidget {
}
class _TargetRowState extends State<_TargetRow> {
late final TextEditingController _weight =
TextEditingController(text: shareToPercentText(widget.target.targetWeight));
late final TextEditingController _band =
TextEditingController(text: shareToPercentText(widget.target.band));
late final TextEditingController _bucket =
TextEditingController(text: widget.target.bucket);
late final TextEditingController _weight = TextEditingController(
text: shareToPercentText(widget.target.targetWeight),
);
late final TextEditingController _band = TextEditingController(
text: shareToPercentText(widget.target.band),
);
late final TextEditingController _bucket = TextEditingController(
text: widget.target.bucket,
);
@override
void dispose() {
@@ -257,20 +281,32 @@ class _TargetRowState extends State<_TargetRow> {
child: knownBuckets.isEmpty
? TextField(
controller: _bucket,
decoration: const InputDecoration(labelText: 'Группа', isDense: true),
onChanged: (v) => widget.onChanged(widget.target.copyWith(bucket: v)),
decoration: const InputDecoration(
labelText: 'Группа',
isDense: true,
),
onChanged: (v) =>
widget.onChanged(widget.target.copyWith(bucket: v)),
)
: DropdownButtonFormField<String>(
initialValue:
knownBuckets.contains(widget.target.bucket) ? widget.target.bucket : null,
initialValue: knownBuckets.contains(widget.target.bucket)
? widget.target.bucket
: null,
isExpanded: true,
decoration: const InputDecoration(labelText: 'Группа', isDense: true),
decoration: const InputDecoration(
labelText: 'Группа',
isDense: true,
),
items: [
for (final b in knownBuckets)
DropdownMenuItem(value: b, child: Text(bucketLabelForKey('asset_class', b))),
DropdownMenuItem(
value: b,
child: Text(bucketLabelForKey('asset_class', b)),
),
],
onChanged: (v) =>
widget.onChanged(widget.target.copyWith(bucket: v ?? '')),
onChanged: (v) => widget.onChanged(
widget.target.copyWith(bucket: v ?? ''),
),
),
),
const SizedBox(width: 12),
@@ -278,11 +314,18 @@ class _TargetRowState extends State<_TargetRow> {
flex: 2,
child: TextField(
controller: _weight,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(labelText: 'Вес, %', isDense: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Вес, %',
isDense: true,
),
onChanged: (v) {
final share = percentTextToShare(v);
widget.onChanged(widget.target.copyWith(targetWeight: share ?? '0'));
widget.onChanged(
widget.target.copyWith(targetWeight: share ?? '0'),
);
},
),
),
@@ -291,10 +334,21 @@ class _TargetRowState extends State<_TargetRow> {
flex: 2,
child: TextField(
controller: _band,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(labelText: 'Допуск, %', isDense: true),
onChanged: (v) =>
widget.onChanged(widget.target.copyWith(band: percentTextToShare(v))),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Допуск, %',
isDense: true,
suffixIcon: HelpTip(
'Допуск (band) — ширина коридора вокруг целевого веса: пока фактическая доля '
'внутри него, сделки не предлагаются. ±5 % значит «не трогать, пока доля в '
'пределах 5 процентных пунктов от цели».',
),
),
onChanged: (v) => widget.onChanged(
widget.target.copyWith(band: percentTextToShare(v)),
),
),
),
IconButton(