172 lines
7.1 KiB
C#
172 lines
7.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Clario.Data;
|
|
using Clario.Models;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using LiveChartsCore;
|
|
using LiveChartsCore.SkiaSharpView.Avalonia;
|
|
using LiveChartsCore.SkiaSharpView.Painting;
|
|
using SkiaSharp;
|
|
|
|
namespace Clario.ViewModels;
|
|
|
|
public partial class BudgetViewModel : ViewModelBase
|
|
{
|
|
public required ViewModelBase parentViewModel;
|
|
public GeneralDataRepo AppData => DataRepo.General;
|
|
|
|
[ObservableProperty] private ObservableCollection<Budget> _visibleBudgets = new();
|
|
|
|
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(NextPeriodCommand), nameof(PreviousPeriodCommand))]
|
|
private DateTime _currentPeriod = DateTime.Now.Date;
|
|
|
|
public bool CanGoToNextPeriod => CurrentPeriod.Month < DateTime.Now.Month;
|
|
public bool CanGoToPreviousPeriod => AppData.Transactions.Any() && CurrentPeriod.Month > AppData.Transactions.Min(x => x.Date.Month);
|
|
public string CurrentPeriodFormatted => CurrentPeriod.ToString("MMMM yyyy");
|
|
|
|
[ObservableProperty] private ISeries[] _spendingBreakdownChartSeries = [];
|
|
[ObservableProperty] private List<Budget> _spendingBreakdownLegends = [];
|
|
|
|
[ObservableProperty] private decimal _totalSpent;
|
|
[ObservableProperty] private decimal _totalBudgeted;
|
|
public string SpentPercentageFormatted => (TotalSpent / TotalBudgeted).ToString("P0") + " of total budget.";
|
|
|
|
public decimal TotalLeft => Math.Clamp(Math.Round(TotalBudgeted - TotalSpent), 0, decimal.MaxValue);
|
|
public string TotalLeftFormatted => TotalLeft.ToString("C0") + " left";
|
|
|
|
public string SavingsHint => TotalLeft >= (AppData.Profile != null ? AppData.Profile.SavingsGoal : 0)
|
|
? "You're on track!"
|
|
: $"Reduce your spending by ${Math.Round((AppData.Profile != null ? AppData.Profile.SavingsGoal ?? 0 : 0) - TotalLeft)} to hit your goal.";
|
|
|
|
private int _onTrackCount;
|
|
private int _approachingCount;
|
|
private int _overBudgetCount;
|
|
|
|
public string OnTrackCountFormatted => _onTrackCount == 1 ? _onTrackCount + " Budget" : _onTrackCount + " Budgets";
|
|
public string ApproachingCountFormatted => _approachingCount == 1 ? _approachingCount + " Budget" : _approachingCount + " Budgets";
|
|
public string OverBudgetCountFormatted => _overBudgetCount == 1 ? _overBudgetCount + " Budget" : _overBudgetCount + " Budgets";
|
|
|
|
public int PeriodLength => DateTime.DaysInMonth(CurrentPeriod.Year, CurrentPeriod.Month);
|
|
public int PeriodDaysPassed => DateTime.Now.Day;
|
|
private int PeriodDaysLeft => PeriodLength - PeriodDaysPassed;
|
|
public string PeriodDaysLeftFormatted => PeriodDaysLeft == 1 ? PeriodDaysLeft + " day left" : PeriodDaysLeft + " days left";
|
|
|
|
public string DailyBudgetLeftFormatted =>
|
|
((TotalBudgeted - TotalSpent) / ((PeriodDaysLeft == 0) ? 1 : PeriodDaysLeft)).ToString("C", new CultureInfo("en-US"));
|
|
|
|
public BudgetViewModel()
|
|
{
|
|
AppData.Budgets.CollectionChanged += async (_, _) => { await Initialize(); };
|
|
AppData.Transactions.CollectionChanged += async (_, _) => { await Initialize(); };
|
|
_ = Initialize();
|
|
}
|
|
|
|
private async Task Initialize()
|
|
{
|
|
try
|
|
{
|
|
await ProcessBudgets();
|
|
ProcessChartData();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void CreateBudget()
|
|
{
|
|
((MainViewModel)parentViewModel).OpenAddBudgetCommand.Execute(null);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void EditBudget(Budget budget)
|
|
{
|
|
((MainViewModel)parentViewModel).OpenEditBudgetCommand.Execute(budget);
|
|
}
|
|
|
|
private void ProcessChartData()
|
|
{
|
|
var tempCategorySpendingBreakdown = new List<(Category category, double[] spent)>();
|
|
var tempSpendingBreakdownLegends = new List<Budget>();
|
|
foreach (var category in AppData.Categories)
|
|
{
|
|
var spent = AppData.Transactions
|
|
.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase) &&
|
|
x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year)
|
|
.Sum(x => x.Amount);
|
|
if (spent == 0) continue;
|
|
double[] values = [(double)spent];
|
|
tempCategorySpendingBreakdown.Add((category, values));
|
|
tempSpendingBreakdownLegends.Add(new Budget() { Category = category, Spent = spent });
|
|
}
|
|
|
|
|
|
SpendingBreakdownChartSeries = tempCategorySpendingBreakdown.OrderByDescending(x => x.spent.Sum()).Select(x => (ISeries)new XamlPieSeries()
|
|
{
|
|
Name = x.category.Name,
|
|
Values = x.spent,
|
|
Fill = new SolidColorPaint(SKColor.Parse(x.category.Color)),
|
|
InnerRadius = 60,
|
|
ToolTipLabelFormatter = point => $"${point.Coordinate.PrimaryValue:N0}"
|
|
}).ToArray();
|
|
|
|
SpendingBreakdownLegends = tempSpendingBreakdownLegends.OrderByDescending(x => x.Spent).ToList();
|
|
}
|
|
|
|
private async Task ProcessBudgets()
|
|
{
|
|
VisibleBudgets.Clear();
|
|
VisibleBudgets = new ObservableCollection<Budget>(await DataRepo.General.FetchProcessedBudgets(CurrentPeriod));
|
|
_onTrackCount = VisibleBudgets.Count(x => x is { IsOnTrack: true, GroupHeader: false });
|
|
_approachingCount = VisibleBudgets.Count(x => x is { IsWarning: true, GroupHeader: false });
|
|
_overBudgetCount = VisibleBudgets.Count(x => x is { IsOverBudget: true, GroupHeader: false });
|
|
TotalBudgeted = VisibleBudgets.Sum(x => x.LimitAmount);
|
|
TotalSpent = VisibleBudgets.Sum(x => x.Spent);
|
|
|
|
NotifyComputedPropertiesOnChanged();
|
|
}
|
|
|
|
private void NotifyComputedPropertiesOnChanged()
|
|
{
|
|
OnPropertyChanged(nameof(CanGoToNextPeriod));
|
|
OnPropertyChanged(nameof(CanGoToPreviousPeriod));
|
|
OnPropertyChanged(nameof(CurrentPeriodFormatted));
|
|
OnPropertyChanged(nameof(SpentPercentageFormatted));
|
|
OnPropertyChanged(nameof(TotalLeft));
|
|
OnPropertyChanged(nameof(TotalLeftFormatted));
|
|
OnPropertyChanged(nameof(SavingsHint));
|
|
OnPropertyChanged(nameof(OnTrackCountFormatted));
|
|
OnPropertyChanged(nameof(ApproachingCountFormatted));
|
|
OnPropertyChanged(nameof(OverBudgetCountFormatted));
|
|
OnPropertyChanged(nameof(PeriodLength));
|
|
OnPropertyChanged(nameof(PeriodDaysPassed));
|
|
OnPropertyChanged(nameof(PeriodDaysLeftFormatted));
|
|
OnPropertyChanged(nameof(DailyBudgetLeftFormatted));
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanGoToNextPeriod))]
|
|
private async Task NextPeriod()
|
|
{
|
|
CurrentPeriod = CurrentPeriod.AddMonths(1);
|
|
OnPropertyChanged(nameof(CurrentPeriodFormatted));
|
|
ProcessChartData();
|
|
await ProcessBudgets();
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanGoToPreviousPeriod))]
|
|
private async Task PreviousPeriod()
|
|
{
|
|
CurrentPeriod = CurrentPeriod.AddMonths(-1);
|
|
OnPropertyChanged(nameof(CurrentPeriodFormatted));
|
|
ProcessChartData();
|
|
await ProcessBudgets();
|
|
}
|
|
} |