Files
Budgeteer/BudgetApp/Components/Pages/Reports.razor
T
2026-03-06 11:05:01 +00:00

168 lines
7.4 KiB
Plaintext

@page "/reports"
@attribute [Authorize]
@inject ITransactionRepository TransactionService
@inject IAccountRepository AccountService
@inject ICategoryRepository CategoryService
@inject ISalaryRepository SalaryService
@inject AuthenticationStateProvider AuthStateProvider
@inject IJSRuntime Js
@implements IAsyncDisposable
@rendermode InteractiveServer
<PageTitle>Reports</PageTitle>
<h3 class="mb-2">Reports</h3>
<p class="text-muted mb-4">Visual breakdown of your spending and income by category for a specified time period.</p>
@if (_loading)
{
<p><em>Loading...</em></p>
}
else if (_accounts.Count == 0)
{
<div class="alert alert-info">
<h5>No data yet</h5>
<p>Add some <a href="/accounts">accounts</a> and <a href="/transactions">transactions</a> to see your spending reports.</p>
</div>
}
else
{
@* ── Date Range & Account Filters ── *@
<div class="card mb-4">
<div class="card-header d-flex flex-wrap align-items-center justify-content-between gap-2">
<h5 class="mb-0">Filters</h5>
<div class="d-flex flex-wrap align-items-center gap-2">
<div class="btn-group btn-group-sm">
<button class="btn @(_preset == "7d" ? "btn-primary" : "btn-outline-secondary")" @onclick="() => SetPreset(7)">7d</button>
<button class="btn @(_preset == "30d" ? "btn-primary" : "btn-outline-secondary")" @onclick="() => SetPreset(30)">30d</button>
<button class="btn @(_preset == "90d" ? "btn-primary" : "btn-outline-secondary")" @onclick="() => SetPreset(90)">90d</button>
<button class="btn @(_preset == "1y" ? "btn-primary" : "btn-outline-secondary")" @onclick="() => SetPreset(365)">1y</button>
</div>
<input type="date" class="form-control form-control-sm" style="width:auto;"
title="Start date" @bind="_dateFrom" @bind:after="OnDateChanged" />
<span class="text-muted">to</span>
<input type="date" class="form-control form-control-sm" style="width:auto;"
title="End date" @bind="_dateTo" @bind:after="OnDateChanged" />
</div>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
@foreach (var account in _accounts)
{
<button class="btn btn-sm @(_selectedAccountIds.Contains(account.Id) ? "btn-primary" : "btn-outline-secondary")"
title="Toggle @account.Name" @onclick="() => ToggleAccount(account.Id)">
<span class="d-inline-block rounded-circle me-1" style="width:10px;height:10px;background-color:@account.Color;"></span>
@account.Name
</button>
}
</div>
</div>
</div>
@* ── Expense by Category (Doughnut) ── *@
<div class="row mb-4">
<div class="col-lg-6 mb-4 mb-lg-0">
<div class="card h-100">
<div class="card-header"><h5 class="mb-0">Expenses by Category</h5></div>
<div class="card-body">
@if (_categoryBreakdown.Count == 0)
{
<p class="text-muted">No expenses in this period.</p>
}
else
{
<div style="position:relative;height:clamp(250px,35vw,400px);">
<canvas id="expensePieChart"></canvas>
</div>
}
</div>
</div>
</div>
@* ── Income by Category (Doughnut) ── *@
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header"><h5 class="mb-0">Income by Category</h5></div>
<div class="card-body">
@if (_incomeBreakdown.Count == 0)
{
<p class="text-muted">No income in this period.</p>
}
else
{
<div style="position:relative;height:clamp(250px,35vw,400px);">
<canvas id="incomePieChart"></canvas>
</div>
}
</div>
</div>
</div>
</div>
@* ── Monthly Income vs Expenses (Bar) ── *@
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0">Monthly Income vs Expenses</h5></div>
<div class="card-body">
@if (_monthlyLabels.Count == 0)
{
<p class="text-muted">No transactions in this period.</p>
}
else
{
<div style="position:relative;height:clamp(250px,30vw,350px);">
<canvas id="monthlyBarChart"></canvas>
</div>
}
</div>
</div>
@* ── Category Breakdown Table ── *@
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0">Category Summary</h5></div>
<div class="card-body">
@if (_categoryBreakdown.Count == 0 && _incomeBreakdown.Count == 0)
{
<p class="text-muted">No transactions in this period.</p>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Category</th>
<th class="text-end">Income</th>
<th class="text-end">Expenses</th>
<th class="text-end">Net</th>
</tr>
</thead>
<tbody>
@foreach (var row in _categorySummary)
{
<tr>
<td>@row.Icon @row.Name</td>
<td class="text-end text-success">@_currencySymbol@row.Income.ToString("N2")</td>
<td class="text-end text-danger">@_currencySymbol@row.Expense.ToString("N2")</td>
<td class="text-end @(row.Net >= 0 ? "text-success" : "text-danger")">
@_currencySymbol@row.Net.ToString("N2")
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="fw-bold">
<td>Total</td>
<td class="text-end text-success">@_currencySymbol@_categorySummary.Sum(r => r.Income).ToString("N2")</td>
<td class="text-end text-danger">@_currencySymbol@_categorySummary.Sum(r => r.Expense).ToString("N2")</td>
@{ var totalNet = _categorySummary.Sum(r => r.Net); }
<td class="text-end @(totalNet >= 0 ? "text-success" : "text-danger")">
@_currencySymbol@totalNet.ToString("N2")
</td>
</tr>
</tfoot>
</table>
</div>
}
</div>
</div>
}