AccountingNext/Schaad.Accounting.Services/ChartService.cs
Claudio Schaad 5d6d49ed8b Group booking-text charts by first word instead of full text
Bookings like "Narbonne Car" and "Narbonne Food" should roll up into
one series/slice instead of staying separate, so grouping now uses
the text up to the first space via TransactionDataset.GetTextGroup().
2026-08-07 18:23:22 +02:00

152 lines
6.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Datasets.Charts;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.Services
{
public class ChartService : IChartService
{
private readonly ISettingsService settingsService;
private readonly IViewService viewService;
public ChartService(ISettingsService settingsService, IViewService viewService)
{
this.settingsService = settingsService;
this.viewService = viewService;
}
public async Task<IReadOnlyList<DataSerie>> GetExpensesPerMonthAsync()
{
var expenseTransactions = (await viewService.GetTransactionViewListAsync())
.Where(t => t.TargetAccount.Class == ClassIds.Expenses)
.ToList();
var distinctSubClasses = expenseTransactions
.Select(t => t.TargetAccount.SubClass)
.Distinct()
.Count();
return distinctSubClasses > 1
? await GetSubClassExpensesPerMonthAsync(expenseTransactions)
: await GetAccountExpensesPerMonthAsync();
}
private async Task<List<DataSerie>> GetSubClassExpensesPerMonthAsync(List<TransactionDataset> expenseTransactions)
{
var year = settingsService.GetYear();
var maxMonth = expenseTransactions
.OrderByDescending(t => t.ValueDate)
.FirstOrDefault()?.ValueDate.Month ?? 12;
var subClassNameByNumber = (await viewService.GetAccountViewListAsync())
.GroupBy(a => a.SubClass)
.ToDictionary(g => g.Key, g => g.First().SubClassName);
var list = new List<DataSerie>();
foreach (var grp in expenseTransactions.GroupBy(t => t.TargetAccount.SubClass))
{
var groupedByMonth = grp
.GroupBy(t => t.ValueDate.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(t.TargetAccount.IsFxAccount)));
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
list.Add(new DataSerie(
Id: grp.Key.ToString(),
Name: subClassNameByNumber[grp.Key],
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
));
}
return list;
}
private async Task<List<DataSerie>> GetAccountExpensesPerMonthAsync()
{
var year = settingsService.GetYear();
var allTransactions = await viewService.GetTransactionViewListAsync();
var expenseAccounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Expenses);
var list = new List<DataSerie>();
foreach (var account in expenseAccounts)
{
var transactions = allTransactions.Where(t => t.TargetAccountId == account.Id).ToList();
if (transactions.Count == 0)
{
continue;
}
var maxMonth = transactions.Max(t => t.ValueDate.Month);
var groupedByMonth = transactions
.GroupBy(t => t.ValueDate.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(account.IsFxAccount)));
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
list.Add(new DataSerie(
Id: account.Id,
Name: account.Name,
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
));
}
return list;
}
public async Task<IReadOnlyList<DataSerie>> GetBookingTextExpensesPerMonthAsync(string accountId)
{
var account = await viewService.GetAccountViewAsync(accountId);
var transactions = await viewService.GetTransactionViewListAsync(accountId);
if (transactions.Count == 0)
{
return new List<DataSerie>();
}
var year = settingsService.GetYear();
var maxMonth = transactions.Max(t => t.ValueDate.Month);
var list = new List<DataSerie>();
foreach (var grp in transactions.GroupBy(t => t.GetTextGroup()))
{
var groupedByMonth = grp
.GroupBy(t => t.ValueDate.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(account.IsFxAccount, accountId)));
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
list.Add(new DataSerie(
Id: grp.Key,
Name: grp.Key,
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
));
}
return list;
}
public async Task<IReadOnlyList<CategoryTotal>> GetBookingTextTotalsAsync(string accountId)
{
var account = await viewService.GetAccountViewAsync(accountId);
var transactions = await viewService.GetTransactionViewListAsync(accountId);
return transactions
.Where(t => t.TargetAccountId == accountId)
.GroupBy(t => t.GetTextGroup())
.Select(g => new CategoryTotal(g.Key, g.Sum(t => t.GetValue(account.IsFxAccount, accountId))))
.Where(c => c.Value != 0)
.ToList();
}
private static void EnsureEntryForEveryMonth(Dictionary<int, decimal> values, int maxMonth = 12)
{
for (int i = 1; i <= maxMonth; i++)
{
if (values.ContainsKey(i) == false)
{
values.Add(i, 0);
}
}
}
}
}