import 'package:dio/dio.dart'; import 'package:fintracker_api/fintracker_api.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/api/api_client.dart'; import '../../core/cache/cached.dart'; /// Every provider below returns `Cached`, 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>((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>>((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?, ); }); /// The current (partial) month's cashflow, or null before any data exists. final cashflowThisMonthProvider = FutureProvider.autoDispose>((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>>((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>(( ref, ) async { final r = await ref.watch(apiProvider).getCashflowApi().cashflowRunway(); return r.cached; }); /// 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>((ref) async { final r = await ref.watch(apiProvider).getMetricsApi().metricsStatus(); return r.cached; }); /// 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 waitForMetricsRefresh( Future 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.delayed(interval); } } final dataQualityProvider = FutureProvider.autoDispose>>((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. void invalidateHomeProviders(WidgetRef ref) { ref.invalidate(netWorthBreakdownProvider); ref.invalidate(netWorthSeriesProvider); ref.invalidate(cashflowThisMonthProvider); ref.invalidate(cashflowLast12Provider); ref.invalidate(runwayProvider); 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>((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>>((ref) async { final r = await ref .watch(apiProvider) .getAnalyticsApi() .analyticsOverview(); return Cached( r.data ?? const [], fetchedAt: r.extra['fetchedAt'] as DateTime?, ); });