136 lines
3.9 KiB
Dart
136 lines
3.9 KiB
Dart
/// Local transaction categories and per-transaction annotations.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// User-facing spend / income categories (stored locally, not on the bank API).
|
|
enum TxCategory {
|
|
uncategorized,
|
|
groceries,
|
|
energy,
|
|
salary,
|
|
mortgage,
|
|
transport,
|
|
dining,
|
|
shopping,
|
|
health,
|
|
transfer,
|
|
other,
|
|
}
|
|
|
|
extension TxCategoryX on TxCategory {
|
|
String get label => switch (this) {
|
|
TxCategory.uncategorized => 'Uncategorized',
|
|
TxCategory.groceries => 'Groceries',
|
|
TxCategory.energy => 'Energy',
|
|
TxCategory.salary => 'Salary',
|
|
TxCategory.mortgage => 'Mortgage',
|
|
TxCategory.transport => 'Transport',
|
|
TxCategory.dining => 'Dining',
|
|
TxCategory.shopping => 'Shopping',
|
|
TxCategory.health => 'Health',
|
|
TxCategory.transfer => 'Transfer',
|
|
TxCategory.other => 'Other',
|
|
};
|
|
|
|
IconData get icon => switch (this) {
|
|
TxCategory.uncategorized => Icons.label_outline,
|
|
TxCategory.groceries => Icons.shopping_cart_outlined,
|
|
TxCategory.energy => Icons.bolt_outlined,
|
|
TxCategory.salary => Icons.payments_outlined,
|
|
TxCategory.mortgage => Icons.home_work_outlined,
|
|
TxCategory.transport => Icons.directions_bus_outlined,
|
|
TxCategory.dining => Icons.restaurant_outlined,
|
|
TxCategory.shopping => Icons.shopping_bag_outlined,
|
|
TxCategory.health => Icons.local_hospital_outlined,
|
|
TxCategory.transfer => Icons.swap_horiz,
|
|
TxCategory.other => Icons.more_horiz,
|
|
};
|
|
|
|
Color get color => switch (this) {
|
|
TxCategory.uncategorized => const Color(0xFF64748B),
|
|
TxCategory.groceries => const Color(0xFF16A34A),
|
|
TxCategory.energy => const Color(0xFFEAB308),
|
|
TxCategory.salary => const Color(0xFF0F766E),
|
|
TxCategory.mortgage => const Color(0xFF7C3AED),
|
|
TxCategory.transport => const Color(0xFF2563EB),
|
|
TxCategory.dining => const Color(0xFFEA580C),
|
|
TxCategory.shopping => const Color(0xFFDB2777),
|
|
TxCategory.health => const Color(0xFFDC2626),
|
|
TxCategory.transfer => const Color(0xFF0891B2),
|
|
TxCategory.other => const Color(0xFF57534E),
|
|
};
|
|
|
|
static TxCategory fromName(String? name) {
|
|
if (name == null || name.isEmpty) return TxCategory.uncategorized;
|
|
return TxCategory.values.firstWhere(
|
|
(c) => c.name == name,
|
|
orElse: () => TxCategory.uncategorized,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Local overlay for a bank transaction.
|
|
class TxMeta {
|
|
const TxMeta({
|
|
this.category = TxCategory.uncategorized,
|
|
this.recurring = false,
|
|
this.notes = '',
|
|
});
|
|
|
|
final TxCategory category;
|
|
final bool recurring;
|
|
final String notes;
|
|
|
|
static const empty = TxMeta();
|
|
|
|
bool get hasCategory => category != TxCategory.uncategorized;
|
|
bool get hasNotes => notes.trim().isNotEmpty;
|
|
|
|
TxMeta copyWith({
|
|
TxCategory? category,
|
|
bool? recurring,
|
|
String? notes,
|
|
}) {
|
|
return TxMeta(
|
|
category: category ?? this.category,
|
|
recurring: recurring ?? this.recurring,
|
|
notes: notes ?? this.notes,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'category': category.name,
|
|
'recurring': recurring,
|
|
'notes': notes,
|
|
};
|
|
|
|
factory TxMeta.fromJson(Map<String, dynamic> json) {
|
|
return TxMeta(
|
|
category: TxCategoryX.fromName(json['category'] as String?),
|
|
recurring: json['recurring'] as bool? ?? false,
|
|
notes: json['notes'] as String? ?? '',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Notes / recurring template keyed by exact merchant+amount match.
|
|
class TxExactTemplate {
|
|
const TxExactTemplate({this.notes = '', this.recurring = false});
|
|
|
|
final String notes;
|
|
final bool recurring;
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'notes': notes,
|
|
'recurring': recurring,
|
|
};
|
|
|
|
factory TxExactTemplate.fromJson(Map<String, dynamic> json) {
|
|
return TxExactTemplate(
|
|
notes: json['notes'] as String? ?? '',
|
|
recurring: json['recurring'] as bool? ?? false,
|
|
);
|
|
}
|
|
}
|