feat(app): экраны импорта отчётов, целей, доходов, ребалансировки, налогов и бенчмарков

Импорт (/imports, /instruments/pending) и фаза 4 (/goals, /income,
/rebalance, /tax, аналитика-хаб с benchmarks_card) — по
docs/ai/import-contract.md и docs/ai/phase4-contract.md. file_picker для
загрузки отчёта.
This commit is contained in:
Dmitry
2026-09-19 10:44:38 +03:00
parent 15f5812ea4
commit b69bb4a0c9
52 changed files with 7404 additions and 13 deletions
+35
View File
@@ -0,0 +1,35 @@
import 'package:decimal/decimal.dart';
/// Weights travel as shares (`"0.60"` = 60 %) and are edited as percents. The conversion is
/// exact on both sides: `Decimal`, never `double`, so 33,33 % round-trips unchanged and a
/// set that sums to exactly 1 does not fail validation because of binary floating point.
final _hundred = Decimal.fromInt(100);
/// `"0.605"` → `"60,5"`; empty for anything unparseable.
String shareToPercentText(String? share) {
final d = share == null ? null : Decimal.tryParse(share);
if (d == null) return '';
final p = d * _hundred;
final text = p == p.truncate() ? p.truncate().toString() : p.toString();
return text.replaceAll('.', ',');
}
/// `"60,5"` → `"0.605"`; null when the text is not a number.
String? percentTextToShare(String text) {
final normalized = text.trim().replaceAll(',', '.').replaceAll(' ', '');
if (normalized.isEmpty) return null;
final d = Decimal.tryParse(normalized);
if (d == null) return null;
return (d / _hundred).toDecimal(scaleOnInfinitePrecision: 10).toString();
}
/// `"0.032"` → `"3,20 %"`, signed. Kept local to the rebalance screen because drift is a
/// share, not a return, and the portfolio helper would sign it the same way by accident.
String formatShareAsPercent(String? share, {bool signed = false, int digits = 2}) {
final d = share == null ? null : Decimal.tryParse(share);
if (d == null) return '';
final p = (d * _hundred).toDouble();
final sign = signed && p > 0 ? '+' : '';
return '$sign${p.toStringAsFixed(digits).replaceAll('.', ',')} %';
}