This commit is contained in:
rossmill76 committed 2026-03-05 18:57:12 +00:00
1 parent 38067df8c3
commit 492d86db9e
30 files changed
+2473 -40

No files matched your search

+9 -1
View File
@@ -49,6 +49,7 @@ public class CategoryService(ApplicationDbContext db)
{
Name = d.Name,
Icon = d.Icon,
IsDefault = true,
UserId = userId
});
@@ -57,7 +58,7 @@ public class CategoryService(ApplicationDbContext db)
return;
}
// Backfill icons on existing default categories that have empty icons
// Backfill icons and IsDefault on existing default categories
var defaultLookup = DefaultCategories.ToDictionary(d => d.Name, d => d.Icon);
bool updated = false;
@@ -68,6 +69,11 @@ public class CategoryService(ApplicationDbContext db)
category.Icon = icon;
updated = true;
}
if (!category.IsDefault && defaultLookup.ContainsKey(category.Name))
{
category.IsDefault = true;
updated = true;
}
}
if (updated)
@@ -88,6 +94,7 @@ public class CategoryService(ApplicationDbContext db)
.FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId);
if (category is not null)
{
if (category.IsDefault) return;
category.Name = name;
category.Icon = icon;
await _db.SaveChangesAsync();
@@ -100,6 +107,7 @@ public class CategoryService(ApplicationDbContext db)
.FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId);
if (category is not null)
{
if (category.IsDefault) return;
_db.Categories.Remove(category);
await _db.SaveChangesAsync();
}
+8 -5
View File
@@ -24,12 +24,13 @@ public class DashboardService(TransactionService transactionService, AccountServ
var allTransactions = await _transactionService.GetTransactionsInRangeAsync(userId, fetchStart, fetchEnd);
var recent = allTransactions
.Where(t => t.Date.Date >= windowStart.Date && t.Date.Date <= DateTime.Today)
.Where(t => (t.Date.Date >= windowStart.Date && t.Date.Date <= DateTime.Today)
|| (t.IsPaid && t.Date.Date > DateTime.Today && t.Date.Date <= windowEnd.Date))
.OrderByDescending(t => t.Date)
.ToList();
var upcoming = allTransactions
.Where(t => t.Date.Date > DateTime.Today && t.Date.Date <= windowEnd.Date)
.Where(t => t.Date.Date > DateTime.Today && t.Date.Date <= windowEnd.Date && !t.IsPaid)
.OrderBy(t => t.Date)
.ToList();
@@ -57,8 +58,9 @@ public class DashboardService(TransactionService transactionService, AccountServ
{
labels.Add(d.ToString("MMM dd"));
// Paid future transactions are excluded — their effect is already in CurrentBalance
var dayTxns = transactions
.Where(t => t.Date.Date == d.Date)
.Where(t => t.Date.Date == d.Date && !(t.IsPaid && t.Date.Date > today))
.Select(t => $"{(t.Type == TransactionType.Income ? "+" : "-")}{t.Account.FormatAmount(t.Amount)} {t.Description} ({t.Account.Name})")
.ToList();
dayTransactionLabels.Add(dayTxns);
@@ -85,8 +87,9 @@ public class DashboardService(TransactionService transactionService, AccountServ
}
else
{
// Future-only window: project forward from CurrentBalance using only unpaid transactions
foreach (var t in accountTransactions
.Where(t => t.Date.Date > today && t.Date.Date < startDate.Date)
.Where(t => t.Date.Date > today && t.Date.Date < startDate.Date && !t.IsPaid)
.OrderBy(t => t.Date))
{
runningBalance += t.Type == TransactionType.Income ? t.Amount : -t.Amount;
@@ -95,7 +98,7 @@ public class DashboardService(TransactionService transactionService, AccountServ
for (var d = startDate; d <= endDate; d = d.AddDays(1))
{
var dayTxns = accountTransactions.Where(t => t.Date.Date == d.Date);
var dayTxns = accountTransactions.Where(t => t.Date.Date == d.Date && !(t.IsPaid && t.Date.Date > today));
foreach (var t in dayTxns)
{
runningBalance += t.Type == TransactionType.Income ? t.Amount : -t.Amount;
+117
View File
@@ -0,0 +1,117 @@
using Microsoft.EntityFrameworkCore;
using BudgetApp.Data;
namespace BudgetApp.Services;
public class SalaryService(ApplicationDbContext db)
{
private readonly ApplicationDbContext _db = db;
public async Task<List<Salary>> GetSalariesAsync(string userId)
{
return await _db.Salaries
.Include(s => s.Account)
.Include(s => s.Category)
.Where(s => s.UserId == userId)
.OrderBy(s => s.Name)
.ToListAsync();
}
public async Task CreateSalaryAsync(Salary salary, DateTime windowEnd)
{
var salaryCategoryId = await _db.Categories
.Where(c => c.UserId == salary.UserId && c.Name == "Salary")
.Select(c => (int?)c.Id)
.FirstOrDefaultAsync();
salary.CategoryId = salaryCategoryId;
_db.Salaries.Add(salary);
await _db.SaveChangesAsync();
await MaterializeSalaryTransactionsAsync(salary.UserId, windowEnd);
}
public async Task DeleteSalaryAsync(int id, string userId)
{
var exists = await _db.Salaries.AnyAsync(s => s.Id == id && s.UserId == userId);
if (!exists) return;
await using var transaction = await _db.Database.BeginTransactionAsync();
// Delete future unpaid transactions generated by this salary
await _db.Transactions
.Where(t => t.SalaryId == id && t.Date.Date > DateTime.Today && !t.IsPaid)
.ExecuteDeleteAsync();
// Unlink past / paid transactions so they stay in history without the salary reference
await _db.Transactions
.Where(t => t.SalaryId == id)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.SalaryId, (int?)null));
await _db.Salaries
.Where(s => s.Id == id && s.UserId == userId)
.ExecuteDeleteAsync();
await transaction.CommitAsync();
}
public async Task MaterializeSalaryTransactionsAsync(string userId, DateTime windowEnd)
{
var salaries = await _db.Salaries
.Where(s => s.UserId == userId)
.ToListAsync();
var newTransactions = new List<Transaction>();
foreach (var salary in salaries)
{
var latestDate = await _db.Transactions
.Where(t => t.SalaryId == salary.Id)
.OrderByDescending(t => t.Date)
.Select(t => (DateTime?)t.Date)
.FirstOrDefaultAsync();
var nextDate = latestDate.HasValue
? GetNextDate(latestDate.Value, salary)
: salary.StartDate.Date;
while (nextDate <= windowEnd)
{
newTransactions.Add(new Transaction
{
Description = salary.Name,
Amount = salary.Amount,
Date = nextDate,
Type = TransactionType.Income,
AccountId = salary.AccountId,
CategoryId = salary.CategoryId,
RecurrenceType = RecurrenceType.None,
SalaryId = salary.Id
});
nextDate = GetNextDate(nextDate, salary);
}
}
if (newTransactions.Count > 0)
{
_db.Transactions.AddRange(newTransactions);
await _db.SaveChangesAsync();
}
}
public async Task<DateTime?> GetNextPaydateAsync(int salaryId)
{
return await _db.Transactions
.Where(t => t.SalaryId == salaryId && t.Date.Date >= DateTime.Today)
.OrderBy(t => t.Date)
.Select(t => (DateTime?)t.Date)
.FirstOrDefaultAsync();
}
private static DateTime GetNextDate(DateTime from, Salary salary) => salary.RecurrenceType switch
{
RecurrenceType.Weekly => from.AddDays(7),
RecurrenceType.Monthly => from.AddMonths(1),
RecurrenceType.Yearly => from.AddYears(1),
_ => from.AddMonths(1)
};
}
+12
View File
@@ -220,6 +220,18 @@ public class TransactionService(ApplicationDbContext db)
await _db.SaveChangesAsync();
}
public async Task MarkAsPaidAsync(int id, string userId, bool isPaid = true)
{
var transaction = await _db.Transactions
.Include(t => t.Account)
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
if (transaction is null) return;
transaction.IsPaid = isPaid;
await _db.SaveChangesAsync();
}
public async Task MaterializeRecurringTransactionsAsync(string userId, DateTime? windowEnd = null)
{
var recurringParents = await _db.Transactions