Feature: Add category pie chart and case-insensitive search
- Add doughnut chart showing spending by category - Fix search to use EF.Functions.Like for explicit case-insensitivity - Include Chart.js CDN for chart rendering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pie Chart -->
|
||||
@if (Model.CategoryBreakdowns.Any())
|
||||
{
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header">Spending by category</div>
|
||||
<div class="card-body">
|
||||
<canvas id="categoryChart" height="220"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Transactions Table -->
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
@@ -304,6 +315,31 @@ else
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="~/js/transactions.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
const categoryLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.CategoryBreakdowns.Select(c => string.IsNullOrWhiteSpace(c.Category) ? "(uncategorized)" : c.Category)));
|
||||
const categoryValues = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.CategoryBreakdowns.Select(c => c.TotalSpend)));
|
||||
|
||||
const catCtx = document.getElementById('categoryChart');
|
||||
if (catCtx && categoryLabels.length) {
|
||||
new Chart(catCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: categoryLabels,
|
||||
datasets: [{
|
||||
data: categoryValues,
|
||||
backgroundColor: ['#4e79a7','#f28e2c','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: { legend: { position: 'bottom' } },
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace MoneyMap.Pages
|
||||
public List<string> AvailableMerchants { get; set; } = new();
|
||||
public List<Card> AvailableCards { get; set; } = new();
|
||||
public TransactionStats Stats { get; set; } = new();
|
||||
public List<CategoryBreakdown> CategoryBreakdowns { get; set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
@@ -61,15 +62,16 @@ namespace MoneyMap.Pages
|
||||
.Include(t => t.Merchant)
|
||||
.AsQueryable();
|
||||
|
||||
// Apply filters
|
||||
// Apply filters (case-insensitive search using EF.Functions.Like)
|
||||
if (!string.IsNullOrWhiteSpace(Search))
|
||||
{
|
||||
var searchPattern = $"%{Search}%";
|
||||
query = query.Where(t =>
|
||||
t.Name.Contains(Search) ||
|
||||
(t.Memo != null && t.Memo.Contains(Search)) ||
|
||||
(t.Category != null && t.Category.Contains(Search)) ||
|
||||
(t.Notes != null && t.Notes.Contains(Search)) ||
|
||||
(t.Merchant != null && t.Merchant.Name.Contains(Search)));
|
||||
EF.Functions.Like(t.Name, searchPattern) ||
|
||||
(t.Memo != null && EF.Functions.Like(t.Memo, searchPattern)) ||
|
||||
(t.Category != null && EF.Functions.Like(t.Category, searchPattern)) ||
|
||||
(t.Notes != null && EF.Functions.Like(t.Notes, searchPattern)) ||
|
||||
(t.Merchant != null && EF.Functions.Like(t.Merchant.Name, searchPattern)));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Category))
|
||||
@@ -154,6 +156,21 @@ namespace MoneyMap.Pages
|
||||
// Calculate stats for filtered results (all pages, not just current)
|
||||
Stats = await _statsService.CalculateStatsAsync(query);
|
||||
|
||||
// Calculate category breakdown for pie chart (only expenses)
|
||||
var expenseQuery = query.Where(t => t.Amount < 0).ExcludeTransfers();
|
||||
var categoryGroups = await expenseQuery
|
||||
.GroupBy(t => t.Category ?? "")
|
||||
.Select(g => new CategoryBreakdown
|
||||
{
|
||||
Category = g.Key,
|
||||
TotalSpend = g.Sum(x => -x.Amount),
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderByDescending(x => x.TotalSpend)
|
||||
.ToListAsync();
|
||||
|
||||
CategoryBreakdowns = categoryGroups;
|
||||
|
||||
// Get available categories for filter dropdown
|
||||
AvailableCategories = await _referenceDataService.GetAvailableCategoriesAsync();
|
||||
|
||||
@@ -178,5 +195,12 @@ namespace MoneyMap.Pages
|
||||
public string AccountLabel { get; set; } = "";
|
||||
public int ReceiptCount { get; set; }
|
||||
}
|
||||
|
||||
public class CategoryBreakdown
|
||||
{
|
||||
public string Category { get; set; } = "";
|
||||
public decimal TotalSpend { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user