// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras // // SPDX-License-Identifier: AGPL-3.0-only import 'dart:math'; import 'package:currency_picker/currency_picker.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:prasule/api/category.dart'; import 'package:prasule/api/entry_data.dart'; import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/main.dart'; part 'wallet.g.dart'; Currency _currencyFromJson(Map data) => Currency.from(json: data); /// Represents a single wallet /// /// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s @JsonSerializable() class Wallet { /// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s Wallet({ required this.name, required this.currency, this.categories = const [], this.entries = const [], this.recurringEntries = const [], this.starterBalance = 0, }); /// Generates a class instance from a Map factory Wallet.fromJson(Map json) => _$WalletFromJson(json); /// A list of all [RecurringWalletEntry]s final List recurringEntries; /// Name of the wallet final String name; /// A list of available categories final List categories; /// List of saved entries final List entries; /// The starting balance of the wallet /// /// Used to calculate current balance double starterBalance; /// Selected currency @JsonKey(fromJson: _currencyFromJson) final Currency currency; /// Converts the data in this instance into a Map Map toJson() => _$WalletToJson(this); /// Getter for the next unused unique number ID in the wallet's **entry** list int get nextId { var id = 1; while (entries.where((element) => element.id == id).isNotEmpty) { id++; // create unique ID } return id; } /// Getter for the next unused unique number ID in the wallet's **category** /// list int get nextCategoryId { var id = 0; while (categories.where((element) => element.id == id).isNotEmpty) { id++; // create unique ID } return id; } /// Handles adding recurring entries to the entry list void recurEntries() { final n = DateTime.now(); for (final ent in recurringEntries) { var m = DateTime( (ent.recurType == RecurType.year) ? ent.lastRunDate.year + ent.repeatAfter : ent.lastRunDate.year, (ent.recurType == RecurType.month) ? ent.lastRunDate.month + ent.repeatAfter : ent.lastRunDate.month, (ent.recurType == RecurType.day) ? ent.lastRunDate.day + ent.repeatAfter : ent.lastRunDate.day, ); // create the date after which we should recur while (n.isAfter( m, )) { logger ..i("Adding recurring entry ${ent.data.name}") ..d("Current entry count: ${entries.length}"); recurringEntries[recurringEntries.indexOf(ent)].lastRunDate = m; // update date on recurring entry final addedEntry = WalletSingleEntry( data: recurringEntries[recurringEntries.indexOf(ent)].data, type: recurringEntries[recurringEntries.indexOf(ent)].type, date: m, category: recurringEntries[recurringEntries.indexOf(ent)].category, id: nextId, ); entries.add(addedEntry); m = DateTime( (ent.recurType == RecurType.year) ? recurringEntries[recurringEntries.indexOf(ent)] .lastRunDate .year + ent.repeatAfter : recurringEntries[recurringEntries.indexOf(ent)] .lastRunDate .year, (ent.recurType == RecurType.month) ? recurringEntries[recurringEntries.indexOf(ent)] .lastRunDate .month + ent.repeatAfter : recurringEntries[recurringEntries.indexOf(ent)] .lastRunDate .month, (ent.recurType == RecurType.day) ? recurringEntries[recurringEntries.indexOf(ent)] .lastRunDate .day + ent.repeatAfter : recurringEntries[recurringEntries.indexOf(ent)].lastRunDate.day, ); // add the variable again to check if we aren't missing any entries logger.i( "Last recurred date is now on ${DateFormat.yMMMMd().format(m)} (${n.isAfter(m)})", ); } WalletManager.saveWallet(this); // save wallet } } /// Removes the specified category. /// /// All [WalletSingleEntry]s will have their category reassigned /// to the default *No category* void removeCategory(WalletCategory category) { // First remove the category from existing entries for (final entryToChange in entries.where((element) => element.category.id == category.id)) { entryToChange.category = categories.where((element) => element.id == 0).first; } // Remove the category categories.removeWhere((element) => element.id == category.id); // Save WalletManager.saveWallet(this); } /// Returns the current balance /// /// Basically just takes *starterBalance* and adds all the entries to it double get currentBalance { var toAdd = 0.0; for (final e in entries) { toAdd += (e.type == EntryType.income) ? e.data.amount : -e.data.amount; } return starterBalance + toAdd; } /// Returns the amount that was made/lost during a month double calculateMonthStatus(int month, int year) { var f = 0.0; for (final e in entries.where( (element) => element.date.year == year && element.date.month == month, )) { f += (e.type == EntryType.income) ? e.data.amount : -e.data.amount; } return f; } /// Empty wallet used for placeholders static final Wallet empty = Wallet( name: "Empty", entries: [], recurringEntries: [], categories: [ WalletCategory( name: "Default", id: 0, icon: IconData( Icons.payments.codePoint, fontFamily: 'MaterialIcons', ), color: Colors.white, ), ], currency: Currency.from( json: { "code": "USD", "name": "United States Dollar", "symbol": r"$", "flag": "USD", "decimal_digits": 2, "number": 840, "name_plural": "US dollars", "thousands_separator": ",", "decimal_separator": ".", "space_between_amount_and_symbol": false, "symbol_on_left": true, }, ), ); /// Creates test data used for debugging purposes void createTestEntries() { entries.clear(); recurringEntries.clear(); final random = Random(); for (var i = 0; i < 30; i++) { entries.add( WalletSingleEntry( data: EntryData( name: "Test Entry #${i + 1}", amount: random.nextInt(20000).toDouble(), ), type: (random.nextInt(3) > 0) ? EntryType.expense : EntryType.income, date: DateTime( 2023, random.nextInt(12) + 1, random.nextInt(28) + 1, ), category: categories[random.nextInt(categories.length)], id: nextId, ), ); } logger.d( "Created ${entries.length} regular entries", ); for (var i = 0; i < 3; i++) { final type = random.nextInt(3); recurringEntries.add( RecurringWalletEntry( data: EntryData( name: "Recurring Entry #${i + 1}", amount: random.nextInt(20000).toDouble(), ), type: (random.nextInt(3) > 0) ? EntryType.expense : EntryType.income, date: DateTime( 2023, random.nextInt(12) + 1, random.nextInt(28) + 1, ), category: categories[random.nextInt(categories.length)], id: nextId, lastRunDate: DateTime.now().subtract( Duration( days: (type > 0) ? 3 : 3 * 31, ), ), recurType: (type > 0) ? RecurType.day : RecurType.month, ), ); } logger.d( "Created ${recurringEntries.length} recurring entries", ); // save and reload WalletManager.saveWallet(this); } }