/// Hand-written client for `/api/v1/goals`. /// /// **Temporary.** The goal routes are not in `openapi/openapi.json` yet, so /// `app/packages/api_client` has no generated models or methods for them. Everything here /// follows `docs/ai/phase4-contract.md` §4 literally and is meant to be **replaced by the /// generated client** once the routes land in the spec and `just gen-client` runs. /// /// Raw Dio comes from `ref.read(apiProvider).dio`: base URL, bearer header and the one-shot /// refresh on 401 are already wired there. library; import 'package:dio/dio.dart'; import '../../../core/utils/json.dart'; class Goal { const Goal({ required this.id, required this.name, required this.scope, required this.targetAmount, required this.currency, this.targetDate, this.monthlyContribution, this.note, this.archived = false, }); final int id; final String name; final String scope; final String targetAmount; final String currency; final DateTime? targetDate; final String? monthlyContribution; final String? note; final bool archived; /// The create/patch body. `id` is never sent; `null` fields are omitted so a PATCH that /// touches one field does not blank the rest. Map toJson() => { 'name': name, 'scope': scope, 'target_amount': targetAmount, 'currency': currency, 'target_date': ?_isoDate(targetDate), 'monthly_contribution': ?monthlyContribution, 'note': ?note, 'archived': archived, }; static Goal fromJson(Map json) => Goal( id: asInt(json['id']) ?? 0, name: asString(json['name']) ?? 'Без названия', scope: asString(json['scope']) ?? 'all', targetAmount: asString(json['target_amount']) ?? '0', currency: asString(json['currency']) ?? 'RUB', targetDate: asDate(json['target_date']), monthlyContribution: asString(json['monthly_contribution']), note: asString(json['note']), archived: asBool(json['archived']), ); static String? _isoDate(DateTime? d) => d == null ? null : '${d.year.toString().padLeft(4, '0')}-' '${d.month.toString().padLeft(2, '0')}-' '${d.day.toString().padLeft(2, '0')}'; } class GoalProgress { const GoalProgress({ required this.goalId, required this.basis, required this.onTrack, this.asOf, this.currentValueRub, this.targetAmountRub, this.progress, this.projectedDate, this.assumedRate, this.monthlyNeededRub, }); final int goalId; final DateTime? asOf; final String? currentValueRub; final String? targetAmountRub; /// A share: `"0.4128"` is 41,28 %. final String? progress; /// **Null means the goal is not reached at the current trend** — an honest answer, and the /// contract forbids substituting a far-off date for it. The UI must say so in words. final DateTime? projectedDate; /// `xirr | contribution | none` — what the projection is built on. final String basis; final String? assumedRate; final String? monthlyNeededRub; final bool onTrack; /// The projection exists only when a date came back. `basis == 'none'` means there was /// nothing to project from in the first place. bool get isUnreachable => projectedDate == null && basis != 'none'; static GoalProgress fromJson(Map json) => GoalProgress( goalId: asInt(json['goal_id']) ?? 0, asOf: asDate(json['as_of']), currentValueRub: asString(json['current_value_rub']), targetAmountRub: asString(json['target_amount_rub']), progress: asString(json['progress']), projectedDate: asDate(json['projected_date']), basis: asString(json['basis']) ?? 'none', assumedRate: asString(json['assumed_rate']), monthlyNeededRub: asString(json['monthly_needed_rub']), onTrack: asBool(json['on_track']), ); } class GoalsApi { const GoalsApi(this._dio); final Dio _dio; static const _base = '/api/v1/goals'; Future> list() async { final r = await _dio.get>(_base); return (r.data ?? const []) .map((e) => Goal.fromJson(Map.from(e as Map))) .toList(); } Future create(Goal goal) async { final r = await _dio.post>(_base, data: goal.toJson()); return Goal.fromJson(r.data ?? const {}); } Future patch(int id, Map changes) async { final r = await _dio.patch>('$_base/$id', data: changes); return Goal.fromJson(r.data ?? const {}); } Future delete(int id) => _dio.delete('$_base/$id'); Future progress(int id) async { final r = await _dio.get>('$_base/$id/progress'); return GoalProgress.fromJson(r.data ?? const {}); } }