WIP
This commit is contained in:
1 parent
38067df8c3
commit
492d86db9e
30 files changed
+2473
-40
No files matched your search
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user