/// Named Overview chart filter snapshots (local favourites). library; import '../models/transaction_meta.dart'; import '../widgets/label_filter_dialog.dart'; /// Persisted Overview lens: period, metric, and Labels-chart options. class OverviewFavourite { const OverviewFavourite({ required this.id, required this.name, this.display = true, this.metric = OverviewMetricMode.flow, this.period = OverviewPeriodMode.monthly, this.labelIds = const [], this.flow = TxFlowFilter.expense, this.barStyle = LabelChartBarStyle.parallel, this.invertExpensePerspective = false, this.updatedAt = 0, this.deleted = false, }); final String id; final String name; /// When true, this favourite appears on the Favourites overview view. final bool display; final OverviewMetricMode metric; final OverviewPeriodMode period; /// Empty = all labels (Labels metric only). final List labelIds; final TxFlowFilter flow; final LabelChartBarStyle barStyle; final bool invertExpensePerspective; /// Epoch ms; used for last-write-wins sync across devices. final int updatedAt; /// Soft-delete so removals propagate via sync instead of being recreated. final bool deleted; OverviewFavourite copyWith({ String? id, String? name, bool? display, OverviewMetricMode? metric, OverviewPeriodMode? period, List? labelIds, TxFlowFilter? flow, LabelChartBarStyle? barStyle, bool? invertExpensePerspective, int? updatedAt, bool? deleted, bool touchUpdatedAt = false, }) { return OverviewFavourite( id: id ?? this.id, name: name ?? this.name, display: display ?? this.display, metric: metric ?? this.metric, period: period ?? this.period, labelIds: labelIds ?? this.labelIds, flow: flow ?? this.flow, barStyle: barStyle ?? this.barStyle, invertExpensePerspective: invertExpensePerspective ?? this.invertExpensePerspective, updatedAt: touchUpdatedAt ? DateTime.now().millisecondsSinceEpoch : (updatedAt ?? this.updatedAt), deleted: deleted ?? this.deleted, ); } Map toJson() => { 'id': id, 'name': name, 'display': display, 'metric': metric.name, 'period': period.name, 'labelIds': labelIds, 'flow': flow.name, 'barStyle': barStyle.name, 'invertExpensePerspective': invertExpensePerspective, 'updatedAt': updatedAt, 'deleted': deleted, }; factory OverviewFavourite.fromJson(Map json) { final rawUpdated = json['updatedAt'] ?? json['updated_at']; final updatedAt = switch (rawUpdated) { int n => n, num n => n.round(), String s => int.tryParse(s) ?? 0, _ => 0, }; return OverviewFavourite( id: json['id'] as String? ?? 'fav_${DateTime.now().millisecondsSinceEpoch}', name: (json['name'] as String?)?.trim().isNotEmpty == true ? (json['name'] as String).trim() : 'Favourite', display: json['display'] as bool? ?? true, metric: OverviewMetricMode.values.firstWhere( (e) => e.name == json['metric'], orElse: () => OverviewMetricMode.flow, ), period: OverviewPeriodMode.values.firstWhere( (e) => e.name == json['period'], orElse: () => OverviewPeriodMode.monthly, ), labelIds: [ for (final id in (json['labelIds'] as List? ?? const [])) if (id is String && id.isNotEmpty) id, ], flow: TxFlowFilter.values.firstWhere( (e) => e.name == json['flow'], orElse: () => TxFlowFilter.expense, ), barStyle: LabelChartBarStyle.values.firstWhere( (e) => e.name == json['barStyle'], orElse: () => LabelChartBarStyle.parallel, ), invertExpensePerspective: json['invertExpensePerspective'] as bool? ?? false, updatedAt: updatedAt, deleted: json['deleted'] as bool? ?? false, ); } } enum OverviewMetricMode { flow, labels } enum OverviewPeriodMode { monthly, yearly } /// Union favourites by id with last-write-wins on [OverviewFavourite.updatedAt]. List mergeOverviewFavourites( List local, List remote, ) { final byId = {}; void put(OverviewFavourite fav) { final existing = byId[fav.id]; if (existing == null || fav.updatedAt >= existing.updatedAt) { byId[fav.id] = fav; } } for (final fav in remote) { put(fav); } for (final fav in local) { put(fav); } return byId.values.toList(growable: false); }