feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события

Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
Dmitry
2026-09-19 22:14:21 +03:00
parent a559d6de3e
commit 62d36aa3e8
73 changed files with 6406 additions and 1192 deletions
+112
View File
@@ -0,0 +1,112 @@
import 'package:fintracker_api/fintracker_api.dart';
import 'package:fintracker_app/core/widgets/money_text.dart';
import 'package:fintracker_app/features/home/scope_cards.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
ScopeCardOut _card({
String scope = 'portfolio:1',
String name = 'ИИСы',
String kind = 'portfolio',
String total = '224608.65',
String? pnl = '6643.47',
String? pnlPct = '0.015',
String? day = '-265.68',
String? dayPct = '-0.001',
String? xirr = '0.0298',
String income = '17387.49',
String? incomePct = '0.043',
}) => ScopeCardOut(
scope: scope,
name: name,
kind: kind,
asOf: DateTime(2026, 9, 19),
totalRub: total,
investedRub: '200000',
pnlRub: pnl,
pnlPct: pnlPct,
dayChangeRub: day,
dayChangePct: dayPct,
xirr: xirr,
incomeYearRub: income,
incomeYearPct: incomePct,
);
Widget _app(List<ScopeCardOut> cards, {double width = 1200}) => ProviderScope(
child: MaterialApp(
home: MediaQuery(
data: MediaQueryData(size: Size(width, 900)),
child: Scaffold(
body: SingleChildScrollView(child: ScopeCards(cards: cards)),
),
),
),
);
void main() {
testWidgets('a card shows the name, the value and the four figures', (
tester,
) async {
await tester.pumpWidget(_app([_card()]));
expect(find.text('ИИСЫ'), findsOneWidget);
expect(find.text(MoneyText.format('224608.65', 'RUB')), findsOneWidget);
for (final label in [
'Прибыль',
'За день',
'Доходность',
'Пассивный доход',
]) {
expect(find.text(label), findsOneWidget, reason: label);
}
// the gain carries an explicit plus, the loss its own minus
expect(find.text('+${MoneyText.format('6643.47', 'RUB')}'), findsOneWidget);
expect(find.text(MoneyText.format('-265.68', 'RUB')), findsOneWidget);
expect(find.text('2,98 %'), findsOneWidget);
expect(
find.textContaining(MoneyText.format('17387.49', 'RUB')),
findsOneWidget,
);
});
testWidgets('missing figures read as a dash, not as zero', (tester) async {
await tester.pumpWidget(
_app([
_card(
pnl: null,
pnlPct: null,
day: null,
dayPct: null,
xirr: null,
income: '0',
incomePct: null,
),
]),
);
expect(find.text(''), findsNWidgets(4));
});
testWidgets('cards flow into columns by the available width', (tester) async {
final cards = [
for (var i = 0; i < 6; i++) _card(scope: 'account:$i', name: 'Счёт $i'),
];
addTearDown(tester.view.reset);
Future<int> columns(double width) async {
tester.view.physicalSize = Size(width, 900);
tester.view.devicePixelRatio = 1;
await tester.pumpWidget(_app(cards, width: width));
final xs = {
for (final c in cards)
tester.getTopLeft(find.text('СЧЁТ ${cards.indexOf(c)}')).dx.round(),
};
return xs.length;
}
expect(await columns(1200), 3);
expect(await columns(800), 2);
expect(await columns(400), 1);
});
}