/// Transaction labels (built-in + user-created) and local annotations. library; import 'package:flutter/material.dart'; /// A spend/income label shown on transactions. class TxLabel { const TxLabel({ required this.id, required this.name, required this.color, this.icon = Icons.label_outline, this.custom = false, }); final String id; final String name; final Color color; final IconData icon; final bool custom; Map toJson() => { 'id': id, 'name': name, // ignore: deprecated_member_use — wide compatibility across Flutter versions 'color': color.value, 'custom': custom, }; factory TxLabel.fromJson(Map json) { return TxLabel( id: json['id'] as String? ?? 'custom', name: json['name'] as String? ?? 'Label', color: Color(json['color'] as int? ?? 0xFF64748B), custom: json['custom'] as bool? ?? true, ); } } /// Built-in presets (no "uncategorized" / "other"). const List kBuiltinLabels = [ TxLabel( id: 'groceries', name: 'Groceries', color: Color(0xFF16A34A), icon: Icons.shopping_cart_outlined, ), TxLabel( id: 'energy', name: 'Energy', color: Color(0xFFEAB308), icon: Icons.bolt_outlined, ), TxLabel( id: 'salary', name: 'Salary', color: Color(0xFF0F766E), icon: Icons.payments_outlined, ), TxLabel( id: 'mortgage', name: 'Mortgage', color: Color(0xFF7C3AED), icon: Icons.home_work_outlined, ), TxLabel( id: 'transport', name: 'Transport', color: Color(0xFF2563EB), icon: Icons.directions_bus_outlined, ), TxLabel( id: 'dining', name: 'Dining', color: Color(0xFFEA580C), icon: Icons.restaurant_outlined, ), TxLabel( id: 'shopping', name: 'Shopping', color: Color(0xFFDB2777), icon: Icons.shopping_bag_outlined, ), TxLabel( id: 'health', name: 'Health', color: Color(0xFFDC2626), icon: Icons.local_hospital_outlined, ), TxLabel( id: 'transfer', name: 'Transfer', color: Color(0xFF0891B2), icon: Icons.swap_horiz, ), ]; /// Palette for newly created custom labels. const List kCustomLabelColors = [ Color(0xFF0F766E), Color(0xFF7C3AED), Color(0xFFEA580C), Color(0xFF2563EB), Color(0xFFDB2777), Color(0xFF16A34A), Color(0xFFDC2626), Color(0xFF57534E), ]; /// Local overlay for a bank transaction. class TxMeta { const TxMeta({ this.labelIds = const [], this.recurring = false, this.notes = '', }); /// Zero or more label ids on this transaction. final List labelIds; final bool recurring; final String notes; static const empty = TxMeta(); bool get hasLabel => labelIds.isNotEmpty; bool get hasNotes => notes.trim().isNotEmpty; /// First label id, if any (for compact UI). String? get primaryLabelId => labelIds.isEmpty ? null : labelIds.first; bool hasLabelId(String id) => labelIds.contains(id); TxMeta copyWith({ List? labelIds, bool clearLabels = false, bool? recurring, String? notes, }) { return TxMeta( labelIds: clearLabels ? const [] : (labelIds ?? this.labelIds), recurring: recurring ?? this.recurring, notes: notes ?? this.notes, ); } Map toJson() => { 'labelIds': labelIds, // Legacy single-label keys for older builds. 'labelId': primaryLabelId, 'category': primaryLabelId, 'recurring': recurring, 'notes': notes, }; factory TxMeta.fromJson(Map json) { return TxMeta( labelIds: _parseLabelIds(json), recurring: json['recurring'] as bool? ?? false, notes: json['notes'] as String? ?? '', ); } } List _parseLabelIds(Map json) { final raw = json['labelIds']; if (raw is List) { return [ for (final item in raw) if (item is String && item.isNotEmpty && item != 'uncategorized' && item != 'other') item, ]; } final single = (json['labelId'] as String?) ?? _migrateLegacyCategory(json['category'] as String?); if (single == null) return const []; return [single]; } String? _migrateLegacyCategory(String? name) { if (name == null || name.isEmpty) return null; if (name == 'uncategorized' || name == 'other') return null; return name; } /// 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 toJson() => { 'notes': notes, 'recurring': recurring, }; factory TxExactTemplate.fromJson(Map json) { return TxExactTemplate( notes: json['notes'] as String? ?? '', recurring: json['recurring'] as bool? ?? false, ); } } /// Strip bank noise so "AH Campus … Transactie:123" matches other AH Campus txs. String merchantKeyFromDescription(String description) { var s = description.trim().toLowerCase().replaceAll(RegExp(r'\s+'), ' '); if (s.startsWith('naam:')) { s = s.substring(5).trim(); final om = s.indexOf(' omschrijving:'); if (om > 0) s = s.substring(0, om); } const markers = [ ' pasvolgnr', ' kaartnr', ' datum:', ' tijd:', ' transactie:', ' term:', ' valutadatum', ' iban:', ' kenmerk:', ' machtiging', ' overige partij', ' retourpin', ]; for (final marker in markers) { final index = s.indexOf(marker); if (index > 6) { s = s.substring(0, index); } } s = s .replaceAll(RegExp(r'\s+nld\s*$'), '') .replaceAll(RegExp(r'^bck\*'), '') .trim(); return s; }