style(app): остальные экраны под новый визуальный язык и форматирование
Доходы, ребалансировка, налоги, импорт, цели, здоровье данных, правила, транзакции, категории, cashflow, pending: новые виджеты и токены темы, dart format. Тесты подстроены под новые модели.
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user