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
+103 -85
View File
@@ -17,14 +17,20 @@ import '../../pending/data/pending_api.dart';
/// A candidate account for an import whose `account_id` the server could not resolve.
class AccountSuggestion {
const AccountSuggestion({required this.id, required this.name, this.broker, this.sourceId});
const AccountSuggestion({
required this.id,
required this.name,
this.broker,
this.sourceId,
});
final int id;
final String name;
final String? broker;
final String? sourceId;
static AccountSuggestion fromJson(Map<String, dynamic> json) => AccountSuggestion(
static AccountSuggestion fromJson(Map<String, dynamic> json) =>
AccountSuggestion(
id: asInt(json['id'])!,
name: asString(json['name']) ?? '#${json['id']}',
broker: asString(json['broker']),
@@ -97,15 +103,15 @@ class ReconPosition {
String get title => instrumentName ?? ticker ?? isin ?? '';
static ReconPosition fromJson(Map<String, dynamic> json) => ReconPosition(
matches: json['matches'] == true,
instrumentId: asInt(json['instrument_id']),
instrumentName: asString(json['instrument_name']),
ticker: asString(json['ticker']),
isin: asString(json['isin']),
qtyReport: asString(json['qty_report']),
qtyDerived: asString(json['qty_derived']),
qtyDelta: asString(json['qty_delta']),
);
matches: json['matches'] == true,
instrumentId: asInt(json['instrument_id']),
instrumentName: asString(json['instrument_name']),
ticker: asString(json['ticker']),
isin: asString(json['isin']),
qtyReport: asString(json['qty_report']),
qtyDerived: asString(json['qty_derived']),
qtyDelta: asString(json['qty_delta']),
);
}
class ReconCash {
@@ -124,12 +130,12 @@ class ReconCash {
final String? delta;
static ReconCash fromJson(Map<String, dynamic> json) => ReconCash(
currency: asString(json['currency']) ?? 'RUB',
matches: json['matches'] == true,
balanceReport: asString(json['balance_report']),
balanceDerived: asString(json['balance_derived']),
delta: asString(json['delta']),
);
currency: asString(json['currency']) ?? 'RUB',
matches: json['matches'] == true,
balanceReport: asString(json['balance_report']),
balanceDerived: asString(json['balance_derived']),
delta: asString(json['delta']),
);
}
class Reconciliation {
@@ -197,23 +203,23 @@ class SampleEvent {
final String? description;
static SampleEvent fromJson(Map<String, dynamic> json) => SampleEvent(
lineNo: asInt(json['line_no']) ?? 0,
kind: asString(json['kind']) ?? 'other',
isDuplicate: json['is_duplicate'] == true,
tradeDate: asDate(json['trade_date']),
settleDate: asDate(json['settle_date']),
instrumentKey: asString(json['instrument_key']),
instrumentName: asString(json['instrument_name']),
instrumentId: asInt(json['instrument_id']),
quantity: asString(json['quantity']),
price: asString(json['price']),
amount: asString(json['amount']),
currency: asString(json['currency']),
fee: asString(json['fee']),
tradeNo: asString(json['trade_no']),
dedupeKey: asString(json['dedupe_key']),
description: asString(json['description']),
);
lineNo: asInt(json['line_no']) ?? 0,
kind: asString(json['kind']) ?? 'other',
isDuplicate: json['is_duplicate'] == true,
tradeDate: asDate(json['trade_date']),
settleDate: asDate(json['settle_date']),
instrumentKey: asString(json['instrument_key']),
instrumentName: asString(json['instrument_name']),
instrumentId: asInt(json['instrument_id']),
quantity: asString(json['quantity']),
price: asString(json['price']),
amount: asString(json['amount']),
currency: asString(json['currency']),
fee: asString(json['fee']),
tradeNo: asString(json['trade_no']),
dedupeKey: asString(json['dedupe_key']),
description: asString(json['description']),
);
}
/// `ImportPreview` and `ImportSummary` in one class: the summary is the same object without
@@ -281,34 +287,38 @@ class ImportPreview {
bool get canDelete => !isCommitted;
static ImportPreview fromJson(Map<String, dynamic> json) => ImportPreview(
id: asInt(json['id'])!,
filename: asString(json['filename']) ?? 'без имени',
parseStatus: asString(json['parse_status']) ?? 'uploaded',
broker: asString(json['broker']),
sha256: asString(json['sha256']),
sizeBytes: asInt(json['size_bytes']),
parserName: asString(json['parser_name']),
parserVersion: asString(json['parser_version']),
error: asString(json['error']),
duplicateOfId: asInt(json['duplicate_of_id']),
accountExternalId: asString(json['account_external_id']),
accountId: asInt(json['account_id']),
accountName: asString(json['account_name']),
accountSuggestions:
asList(json['account_suggestions']).map(AccountSuggestion.fromJson).toList(),
periodFrom: asDate(json['period_from']),
periodTo: asDate(json['period_to']),
uploadedAt: asDate(json['uploaded_at']),
committedAt: asDate(json['committed_at']),
counts: ImportCounts.fromJson(asMap(json['counts'])),
pendingInstruments:
asList(json['pending_instruments']).map(PendingInstrument.fromJson).toList(),
reconciliation: Reconciliation.fromJson(asMap(json['reconciliation'])),
warnings: json['warnings'] is List
? (json['warnings'] as List).map((e) => e.toString()).toList()
: const [],
sampleEvents: asList(json['sample_events']).map(SampleEvent.fromJson).toList(),
);
id: asInt(json['id'])!,
filename: asString(json['filename']) ?? 'без имени',
parseStatus: asString(json['parse_status']) ?? 'uploaded',
broker: asString(json['broker']),
sha256: asString(json['sha256']),
sizeBytes: asInt(json['size_bytes']),
parserName: asString(json['parser_name']),
parserVersion: asString(json['parser_version']),
error: asString(json['error']),
duplicateOfId: asInt(json['duplicate_of_id']),
accountExternalId: asString(json['account_external_id']),
accountId: asInt(json['account_id']),
accountName: asString(json['account_name']),
accountSuggestions: asList(json['account_suggestions'])
.map(AccountSuggestion.fromJson)
.toList(),
periodFrom: asDate(json['period_from']),
periodTo: asDate(json['period_to']),
uploadedAt: asDate(json['uploaded_at']),
committedAt: asDate(json['committed_at']),
counts: ImportCounts.fromJson(asMap(json['counts'])),
pendingInstruments: asList(json['pending_instruments'])
.map(PendingInstrument.fromJson)
.toList(),
reconciliation: Reconciliation.fromJson(asMap(json['reconciliation'])),
warnings: json['warnings'] is List
? (json['warnings'] as List).map((e) => e.toString()).toList()
: const [],
sampleEvents: asList(json['sample_events'])
.map(SampleEvent.fromJson)
.toList(),
);
}
/// What `POST /imports/{id}/commit` reports back.
@@ -336,16 +346,16 @@ class ImportResult {
final bool metricsRefreshed;
static ImportResult fromJson(Map<String, dynamic> json) => ImportResult(
importId: asInt(json['import_id']) ?? 0,
committed: json['committed'] == true,
eventsCreated: asInt(json['events_created']) ?? 0,
eventsUpdated: asInt(json['events_updated']) ?? 0,
eventsSkipped: asInt(json['events_skipped']) ?? 0,
eventsShadow: asInt(json['events_shadow']) ?? 0,
pendingInstruments: asInt(json['pending_instruments']) ?? 0,
reconciliation: Reconciliation.fromJson(asMap(json['reconciliation'])),
metricsRefreshed: json['metrics_refreshed'] == true,
);
importId: asInt(json['import_id']) ?? 0,
committed: json['committed'] == true,
eventsCreated: asInt(json['events_created']) ?? 0,
eventsUpdated: asInt(json['events_updated']) ?? 0,
eventsSkipped: asInt(json['events_skipped']) ?? 0,
eventsShadow: asInt(json['events_shadow']) ?? 0,
pendingInstruments: asInt(json['pending_instruments']) ?? 0,
reconciliation: Reconciliation.fromJson(asMap(json['reconciliation'])),
metricsRefreshed: json['metrics_refreshed'] == true,
);
}
/// A report chosen by the user. On web `file_picker` can only hand over [bytes]; on desktop
@@ -366,14 +376,14 @@ class ImportsApi {
static const _base = '/api/v1/imports';
Future<List<ImportPreview>> list({int limit = 50, int offset = 0, String? status}) async {
Future<List<ImportPreview>> list({
int limit = 50,
int offset = 0,
String? status,
}) async {
final r = await _dio.get<List<dynamic>>(
_base,
queryParameters: {
'limit': limit,
'offset': offset,
'status': ?status,
},
queryParameters: {'limit': limit, 'offset': offset, 'status': ?status},
);
return (r.data ?? const [])
.map((e) => ImportPreview.fromJson(Map<String, dynamic>.from(e as Map)))
@@ -385,7 +395,11 @@ class ImportsApi {
return ImportPreview.fromJson(r.data!);
}
Future<ImportPreview> upload(PickedReport report, {int? accountId, String? parser}) async {
Future<ImportPreview> upload(
PickedReport report, {
int? accountId,
String? parser,
}) async {
final bytes = report.bytes;
final form = FormData.fromMap({
'file': bytes != null
@@ -404,11 +418,14 @@ class ImportsApi {
bool confirmDuplicates = false,
bool dryRun = false,
}) async {
final r = await _dio.post<Map<String, dynamic>>('$_base/$id/commit', data: {
'account_id': ?accountId,
'confirm_duplicates': confirmDuplicates,
'dry_run': dryRun,
});
final r = await _dio.post<Map<String, dynamic>>(
'$_base/$id/commit',
data: {
'account_id': ?accountId,
'confirm_duplicates': confirmDuplicates,
'dry_run': dryRun,
},
);
return ImportResult.fromJson(r.data ?? const {});
}
@@ -438,4 +455,5 @@ List<Map<String, dynamic>> asList(Object? v) => v is List
? v.whereType<Map>().map((e) => Map<String, dynamic>.from(e)).toList()
: const [];
Map<String, dynamic>? asMap(Object? v) => v is Map ? Map<String, dynamic>.from(v) : null;
Map<String, dynamic>? asMap(Object? v) =>
v is Map ? Map<String, dynamic>.from(v) : null;