feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
@@ -8,54 +8,102 @@ import '../../core/cache/cached.dart';
|
||||
/// Every provider below returns `Cached<T>`, not `T`: `HomePage` reads
|
||||
/// `.data` for the tiles and `.fetchedAt` (via `oldestFetch`) for the one
|
||||
/// offline banner at the top. See `docs/ai/offline-cache.md`.
|
||||
final netWorthBreakdownProvider = FutureProvider.autoDispose<Cached<NetWorthBreakdown>>((ref) async {
|
||||
final r = await ref.watch(apiProvider).getNetworthApi().networthBreakdown();
|
||||
return r.cached;
|
||||
});
|
||||
final netWorthBreakdownProvider =
|
||||
FutureProvider.autoDispose<Cached<NetWorthBreakdown>>((ref) async {
|
||||
final r = await ref
|
||||
.watch(apiProvider)
|
||||
.getNetworthApi()
|
||||
.networthBreakdown();
|
||||
return r.cached;
|
||||
});
|
||||
|
||||
/// Daily net worth for the last 365 days.
|
||||
final netWorthSeriesProvider = FutureProvider.autoDispose<Cached<List<NetWorthDay>>>((ref) async {
|
||||
final now = DateTime.now();
|
||||
final r = await ref.watch(apiProvider).getNetworthApi().networthSeries(
|
||||
from: now.subtract(const Duration(days: 365)),
|
||||
to: now,
|
||||
final netWorthSeriesProvider =
|
||||
FutureProvider.autoDispose<Cached<List<NetWorthDay>>>((ref) async {
|
||||
final now = DateTime.now();
|
||||
final r = await ref
|
||||
.watch(apiProvider)
|
||||
.getNetworthApi()
|
||||
.networthSeries(
|
||||
from: now.subtract(const Duration(days: 365)),
|
||||
to: now,
|
||||
);
|
||||
return Cached(
|
||||
r.data ?? const [],
|
||||
fetchedAt: r.extra['fetchedAt'] as DateTime?,
|
||||
);
|
||||
return Cached(r.data ?? const [], fetchedAt: r.extra['fetchedAt'] as DateTime?);
|
||||
});
|
||||
});
|
||||
|
||||
/// The current (partial) month's cashflow, or null before any data exists.
|
||||
final cashflowThisMonthProvider = FutureProvider.autoDispose<Cached<CashFlowMonth?>>((ref) async {
|
||||
final r = await ref.watch(apiProvider).getCashflowApi().cashflowMonthly(months: 1);
|
||||
final rows = r.data ?? const [];
|
||||
return Cached(rows.isEmpty ? null : rows.last, fetchedAt: r.extra['fetchedAt'] as DateTime?);
|
||||
});
|
||||
final cashflowThisMonthProvider =
|
||||
FutureProvider.autoDispose<Cached<CashFlowMonth?>>((ref) async {
|
||||
final r = await ref
|
||||
.watch(apiProvider)
|
||||
.getCashflowApi()
|
||||
.cashflowMonthly(months: 1);
|
||||
final rows = r.data ?? const [];
|
||||
return Cached(
|
||||
rows.isEmpty ? null : rows.last,
|
||||
fetchedAt: r.extra['fetchedAt'] as DateTime?,
|
||||
);
|
||||
});
|
||||
|
||||
/// The last 12 months, for the income-vs-expense bar chart.
|
||||
final cashflowLast12Provider = FutureProvider.autoDispose<Cached<List<CashFlowMonth>>>((ref) async {
|
||||
final r = await ref.watch(apiProvider).getCashflowApi().cashflowMonthly(months: 12);
|
||||
return Cached(r.data ?? const [], fetchedAt: r.extra['fetchedAt'] as DateTime?);
|
||||
});
|
||||
final cashflowLast12Provider =
|
||||
FutureProvider.autoDispose<Cached<List<CashFlowMonth>>>((ref) async {
|
||||
final r = await ref
|
||||
.watch(apiProvider)
|
||||
.getCashflowApi()
|
||||
.cashflowMonthly(months: 12);
|
||||
return Cached(
|
||||
r.data ?? const [],
|
||||
fetchedAt: r.extra['fetchedAt'] as DateTime?,
|
||||
);
|
||||
});
|
||||
|
||||
final runwayProvider = FutureProvider.autoDispose<Cached<RunwayOut>>((ref) async {
|
||||
final runwayProvider = FutureProvider.autoDispose<Cached<RunwayOut>>((
|
||||
ref,
|
||||
) async {
|
||||
final r = await ref.watch(apiProvider).getCashflowApi().cashflowRunway();
|
||||
return r.cached;
|
||||
});
|
||||
|
||||
/// When the metric tables were last rebuilt; null before the first refresh.
|
||||
final metricsStatusProvider = FutureProvider.autoDispose<Cached<RefreshLogOut?>>((ref) async {
|
||||
try {
|
||||
final r = await ref.watch(apiProvider).getMetricsApi().metricsStatus();
|
||||
return r.cached;
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) return const Cached(null);
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
/// When the metric tables were last rebuilt, whether they are one consistent snapshot, and
|
||||
/// whether another rebuild is on its way.
|
||||
final metricsStatusProvider =
|
||||
FutureProvider.autoDispose<Cached<MetricsStatusOut>>((ref) async {
|
||||
final r = await ref.watch(apiProvider).getMetricsApi().metricsStatus();
|
||||
return r.cached;
|
||||
});
|
||||
|
||||
final dataQualityProvider = FutureProvider.autoDispose<Cached<List<DataQualityRow>>>((ref) async {
|
||||
final r = await ref.watch(apiProvider).getMetricsApi().metricsDataQuality();
|
||||
return Cached(r.data ?? const [], fetchedAt: r.extra['fetchedAt'] as DateTime?);
|
||||
});
|
||||
/// Waits for a queued metrics rebuild to finish: `POST /metrics/refresh` only queues it, so the
|
||||
/// numbers are stale until `refreshing` turns false. Returns the last status seen, or null if
|
||||
/// [timeout] ran out first (a rebuild stuck behind a long sync, say).
|
||||
Future<MetricsStatusOut?> waitForMetricsRefresh(
|
||||
Future<MetricsStatusOut> Function() fetch, {
|
||||
Duration interval = const Duration(seconds: 2),
|
||||
Duration timeout = const Duration(minutes: 5),
|
||||
}) async {
|
||||
final clock = Stopwatch()..start();
|
||||
while (true) {
|
||||
final status = await fetch();
|
||||
if (!status.refreshing) return status;
|
||||
if (clock.elapsed >= timeout) return null;
|
||||
await Future<void>.delayed(interval);
|
||||
}
|
||||
}
|
||||
|
||||
final dataQualityProvider =
|
||||
FutureProvider.autoDispose<Cached<List<DataQualityRow>>>((ref) async {
|
||||
final r = await ref
|
||||
.watch(apiProvider)
|
||||
.getMetricsApi()
|
||||
.metricsDataQuality();
|
||||
return Cached(
|
||||
r.data ?? const [],
|
||||
fetchedAt: r.extra['fetchedAt'] as DateTime?,
|
||||
);
|
||||
});
|
||||
|
||||
/// Every dashboard provider, refreshed together after a manual
|
||||
/// `POST /metrics/refresh` or a pull-to-refresh.
|
||||
@@ -68,16 +116,35 @@ void invalidateHomeProviders(WidgetRef ref) {
|
||||
ref.invalidate(metricsStatusProvider);
|
||||
ref.invalidate(dataQualityProvider);
|
||||
ref.invalidate(portfolioSummaryHomeProvider);
|
||||
ref.invalidate(scopeCardsProvider);
|
||||
}
|
||||
|
||||
/// The investment side of the dashboard: one scope-wide summary, `all` by default.
|
||||
/// Null when the ledger is empty, which is the normal state before a broker sync.
|
||||
final portfolioSummaryHomeProvider = FutureProvider.autoDispose<Cached<SummaryOut?>>((ref) async {
|
||||
try {
|
||||
final r = await ref.watch(apiProvider).getAnalyticsApi().analyticsSummary();
|
||||
return r.cached;
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) return const Cached(null);
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
final portfolioSummaryHomeProvider =
|
||||
FutureProvider.autoDispose<Cached<SummaryOut?>>((ref) async {
|
||||
try {
|
||||
final r = await ref
|
||||
.watch(apiProvider)
|
||||
.getAnalyticsApi()
|
||||
.analyticsSummary();
|
||||
return r.cached;
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) return const Cached(null);
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
|
||||
/// One card per portfolio and account for the home grid (`GET /analytics/overview`): value,
|
||||
/// result, the last day's change, return and expected passive income, in one round trip.
|
||||
final scopeCardsProvider =
|
||||
FutureProvider.autoDispose<Cached<List<ScopeCardOut>>>((ref) async {
|
||||
final r = await ref
|
||||
.watch(apiProvider)
|
||||
.getAnalyticsApi()
|
||||
.analyticsOverview();
|
||||
return Cached(
|
||||
r.data ?? const [],
|
||||
fetchedAt: r.extra['fetchedAt'] as DateTime?,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user