using Microsoft.EntityFrameworkCore; using ROLAC.API.Data; using ROLAC.API.DTOs.Finance; using ROLAC.API.Entities; namespace ROLAC.API.Services; /// /// Read-only aggregation over Givings + Expenses for the Finance Dashboard. /// Expense scope is Paid+Approved everywhere (decided with the user); income is Givings only. /// public class FinanceDashboardService : IFinanceDashboardService { private readonly AppDbContext _db; public FinanceDashboardService(AppDbContext db) => _db = db; // Paid+Approved expenses, optionally bounded by ExpenseDate. Shared by the // income/expense pie and the breakdown so the filter is identical in both. private IQueryable PaidApproved(DateOnly? from, DateOnly? to) { var q = _db.Expenses.Where(e => e.Status == "Paid" || e.Status == "Approved"); if (from.HasValue) q = q.Where(e => e.ExpenseDate >= from.Value); if (to.HasValue) q = q.Where(e => e.ExpenseDate <= to.Value); return q; } public async Task GetSummaryAsync() { var income = await _db.Givings.SumAsync(g => (decimal?)g.Amount) ?? 0m; var expense = await PaidApproved(null, null).SumAsync(e => (decimal?)e.Amount) ?? 0m; return new FinanceSummaryDto { TotalIncome = income, TotalExpenses = expense, Balance = income - expense, }; } public async Task GetIncomeExpenseAsync(DateOnly? from, DateOnly? to) { var giving = _db.Givings.AsQueryable(); if (from.HasValue) giving = giving.Where(g => g.GivingDate >= from.Value); if (to.HasValue) giving = giving.Where(g => g.GivingDate <= to.Value); return new IncomeExpenseDto { Income = await giving.SumAsync(g => (decimal?)g.Amount) ?? 0m, Expense = await PaidApproved(from, to).SumAsync(e => (decimal?)e.Amount) ?? 0m, }; } public async Task> GetExpenseBreakdownAsync( DateOnly? from, DateOnly? to, int? ministryId, int? categoryGroupId) { var q = PaidApproved(from, to); if (ministryId.HasValue) q = q.Where(e => e.MinistryId == ministryId.Value); if (categoryGroupId.HasValue) q = q.Where(e => e.CategoryGroupId == categoryGroupId.Value); // Group by the deepest level whose parent id is supplied. List<(int Id, decimal Amount)> grouped; if (categoryGroupId.HasValue) grouped = (await q.GroupBy(e => e.SubCategoryId) .Select(g => new { Id = g.Key, Amount = g.Sum(x => x.Amount) }).ToListAsync()) .Select(x => (x.Id, x.Amount)).ToList(); else if (ministryId.HasValue) grouped = (await q.GroupBy(e => e.CategoryGroupId) .Select(g => new { Id = g.Key, Amount = g.Sum(x => x.Amount) }).ToListAsync()) .Select(x => (x.Id, x.Amount)).ToList(); else grouped = (await q.GroupBy(e => e.MinistryId) .Select(g => new { Id = g.Key, Amount = g.Sum(x => x.Amount) }).ToListAsync()) .Select(x => (x.Id, x.Amount)).ToList(); var ids = grouped.Select(x => x.Id).ToHashSet(); Dictionary names = categoryGroupId.HasValue ? await _db.ExpenseSubCategories.Where(s => ids.Contains(s.Id)) .ToDictionaryAsync(s => s.Id, s => (s.Name_en, s.Name_zh)) : ministryId.HasValue ? await _db.ExpenseCategoryGroups.Where(g => ids.Contains(g.Id)) .ToDictionaryAsync(g => g.Id, g => (g.Name_en, g.Name_zh)) : await _db.Ministries.Where(m => ids.Contains(m.Id)) .ToDictionaryAsync(m => m.Id, m => (m.Name_en, m.Name_zh)); return grouped.Select(x => { names.TryGetValue(x.Id, out var n); return new BreakdownSliceDto { Id = x.Id, Name_en = n.En ?? "", Name_zh = n.Zh, Amount = x.Amount }; }).OrderByDescending(s => s.Amount).ToList(); } }