6520ebf221
Self-hosted, plugin-extensible personal finance manager built with ASP.NET Core 9.0, Vue 3 + Vuetify 3, and PostgreSQL 17. Backend (8 .NET projects): - Core: 19 domain models, 6 enums, 14 DTOs, 12 service interfaces - Data: EF Core DbContext, 16 entity configurations, category seeder - API: 14 controllers, 15 services, JWT auth, SignalR, middleware - Plugins: Abstractions + OFX/CSV/QIF file parsers - Tests: 28 xUnit tests (fingerprinting, duplicate detection, parsers) Frontend (Vue 3 + Vuetify 3 + TypeScript): - 13 views, Pinia stores, Axios API services with JWT interceptors - Dashboard, accounts, transactions, categories, imports, and more Deployment: - Docker Compose (PostgreSQL 17 + .NET API + nginx frontend) - Auto-migration on startup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
81 lines
3.9 KiB
C#
81 lines
3.9 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Purrse.Core.DTOs;
|
|
using Purrse.Core.Enums;
|
|
using Purrse.Core.Interfaces.Services;
|
|
using Purrse.Data;
|
|
|
|
namespace Purrse.Api.Services;
|
|
|
|
public class DashboardService : IDashboardService
|
|
{
|
|
private readonly PurrseDbContext _db;
|
|
|
|
public DashboardService(PurrseDbContext db) => _db = db;
|
|
|
|
public async Task<DashboardResponse> GetDashboardAsync(Guid userId)
|
|
{
|
|
var accounts = await _db.Accounts
|
|
.Where(a => a.UserId == userId && a.IsActive)
|
|
.OrderBy(a => a.SortOrder)
|
|
.ToListAsync();
|
|
|
|
var assetTypes = new[] { AccountType.Checking, AccountType.Savings, AccountType.Cash, AccountType.Brokerage, AccountType.Retirement401k, AccountType.IRA };
|
|
var totalAssets = accounts.Where(a => assetTypes.Contains(a.Type)).Sum(a => a.Balance);
|
|
var totalLiabilities = accounts.Where(a => !assetTypes.Contains(a.Type)).Sum(a => Math.Abs(a.Balance));
|
|
var netWorth = totalAssets - totalLiabilities;
|
|
|
|
var accountSummaries = accounts.Select(a => new AccountSummaryResponse(a.Id, a.Name, a.Type, a.Institution, a.Balance, a.CreditLimit)).ToList();
|
|
|
|
var now = DateTime.UtcNow;
|
|
var startOfMonth = new DateTime(now.Year, now.Month, 1);
|
|
var startOfLastMonth = startOfMonth.AddMonths(-1);
|
|
|
|
var recentTxns = await _db.Transactions
|
|
.Include(t => t.Account).Include(t => t.Category)
|
|
.Where(t => t.Account.UserId == userId && !t.IsVoid)
|
|
.OrderByDescending(t => t.Date).ThenByDescending(t => t.CreatedAt)
|
|
.Take(10)
|
|
.Select(t => new TransactionResponse(t.Id, t.AccountId, t.Account.Name, t.Date, t.Amount,
|
|
t.PayeeId, t.PayeeName, t.CategoryId, t.Category != null ? t.Category.Name : null,
|
|
t.Memo, t.ReferenceNumber, t.CheckNumber, t.Type, t.Status, t.IsVoid,
|
|
t.TransferTransactionId, t.ImportBatchId, null, t.CreatedAt))
|
|
.ToListAsync();
|
|
|
|
var thisMonthSpending = await _db.Transactions
|
|
.Where(t => t.Account.UserId == userId && t.Date >= startOfMonth && t.Amount < 0 && !t.IsVoid)
|
|
.SumAsync(t => Math.Abs(t.Amount));
|
|
|
|
var lastMonthSpending = await _db.Transactions
|
|
.Where(t => t.Account.UserId == userId && t.Date >= startOfLastMonth && t.Date < startOfMonth && t.Amount < 0 && !t.IsVoid)
|
|
.SumAsync(t => Math.Abs(t.Amount));
|
|
|
|
var changePercent = lastMonthSpending == 0 ? 0 : ((thisMonthSpending - lastMonthSpending) / lastMonthSpending) * 100;
|
|
|
|
var topCategories = await _db.Transactions
|
|
.Include(t => t.Category)
|
|
.Where(t => t.Account.UserId == userId && t.Date >= startOfMonth && t.Amount < 0 && !t.IsVoid && t.CategoryId.HasValue)
|
|
.GroupBy(t => new { t.CategoryId, CategoryName = t.Category!.Name })
|
|
.Select(g => new CategorySpending(g.Key.CategoryName, Math.Abs(g.Sum(t => t.Amount)), 0, null))
|
|
.OrderByDescending(c => c.Amount)
|
|
.Take(5)
|
|
.ToListAsync();
|
|
|
|
var totalSpending = topCategories.Sum(c => c.Amount);
|
|
topCategories = topCategories.Select(c => new CategorySpending(c.CategoryName, c.Amount,
|
|
totalSpending > 0 ? (c.Amount / totalSpending) * 100 : 0, null)).ToList();
|
|
|
|
var spendingSummary = new SpendingSummary(thisMonthSpending, lastMonthSpending, changePercent, topCategories);
|
|
|
|
var upcomingBills = await _db.ScheduledTransactions
|
|
.Include(s => s.Account)
|
|
.Where(s => s.UserId == userId && s.IsActive && s.NextDueDate <= now.AddDays(14))
|
|
.OrderBy(s => s.NextDueDate)
|
|
.Take(10)
|
|
.Select(s => new UpcomingBill(s.Id, s.PayeeName ?? "", s.Amount, s.NextDueDate, s.Account.Name, s.AutoPost))
|
|
.ToListAsync();
|
|
|
|
return new DashboardResponse(netWorth, totalAssets, totalLiabilities, accountSummaries,
|
|
recentTxns, spendingSummary, upcomingBills);
|
|
}
|
|
}
|