158 lines
4.6 KiB
Dart
158 lines
4.6 KiB
Dart
/// 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<String> 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<String>? 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<String, dynamic> 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<String, dynamic> 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<OverviewFavourite> mergeOverviewFavourites(
|
|
List<OverviewFavourite> local,
|
|
List<OverviewFavourite> remote,
|
|
) {
|
|
final byId = <String, OverviewFavourite>{};
|
|
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);
|
|
}
|