Improves readability of percentage text when progress bar is in warning state (80-99% of budget used). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
317 lines
16 KiB
Plaintext
317 lines
16 KiB
Plaintext
@page
|
|
@model MoneyMap.Pages.BudgetsModel
|
|
@using MoneyMap.Models
|
|
@{
|
|
ViewData["Title"] = "Budgets";
|
|
}
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h2>Budgets</h2>
|
|
<a asp-page="/Index" class="btn btn-outline-secondary">Back to Dashboard</a>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
|
|
{
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
@Model.SuccessMessage
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
|
|
{
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
@Model.ErrorMessage
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Add New Budget Button -->
|
|
<div class="mb-3">
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
|
|
+ Add New Budget
|
|
</button>
|
|
</div>
|
|
|
|
@if (Model.BudgetStatuses.Any())
|
|
{
|
|
<!-- Active Budgets -->
|
|
var activeBudgets = Model.BudgetStatuses.Where(s => s.Budget.IsActive).ToList();
|
|
if (activeBudgets.Any())
|
|
{
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header">
|
|
<strong>Active Budgets</strong>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Category</th>
|
|
<th>Period</th>
|
|
<th class="text-end">Budget</th>
|
|
<th class="text-end">Spent</th>
|
|
<th class="text-end">Remaining</th>
|
|
<th style="width: 200px;">Progress</th>
|
|
<th style="width: 180px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var status in activeBudgets)
|
|
{
|
|
var transactionsUrl = status.Budget.IsTotalBudget
|
|
? Url.Page("/Transactions", new { startDate = status.PeriodStart.ToString("yyyy-MM-dd"), endDate = status.PeriodEnd.ToString("yyyy-MM-dd") })
|
|
: Url.Page("/Transactions", new { category = status.Budget.Category, startDate = status.PeriodStart.ToString("yyyy-MM-dd"), endDate = status.PeriodEnd.ToString("yyyy-MM-dd") });
|
|
|
|
<tr style="cursor: pointer;" onclick="window.location.href='@transactionsUrl'">
|
|
<td>
|
|
<a href="@transactionsUrl" class="text-decoration-none">
|
|
<strong>@status.Budget.DisplayName</strong>
|
|
</a>
|
|
@if (status.Budget.IsTotalBudget)
|
|
{
|
|
<span class="badge bg-info ms-1">Total</span>
|
|
}
|
|
@if (status.IsOverBudget)
|
|
{
|
|
<span class="badge bg-danger ms-1">Over Budget</span>
|
|
}
|
|
<br />
|
|
<small class="text-muted">@status.PeriodDisplay</small>
|
|
</td>
|
|
<td>@status.Budget.Period</td>
|
|
<td class="text-end">@status.Budget.Amount.ToString("C")</td>
|
|
<td class="text-end">@status.Spent.ToString("C")</td>
|
|
<td class="text-end @(status.IsOverBudget ? "text-danger fw-bold" : "")">
|
|
@status.Remaining.ToString("C")
|
|
</td>
|
|
<td>
|
|
<div class="progress" style="height: 20px;">
|
|
@{
|
|
var percent = Math.Min(status.PercentUsed, 100);
|
|
var progressClass = status.StatusClass;
|
|
var textClass = progressClass == "warning" ? "text-dark" : "";
|
|
}
|
|
<div class="progress-bar bg-@progressClass @textClass" role="progressbar"
|
|
style="width: @percent%"
|
|
aria-valuenow="@status.PercentUsed" aria-valuemin="0" aria-valuemax="100">
|
|
@status.PercentUsed.ToString("F0")%
|
|
</div>
|
|
</div>
|
|
@if (status.IsOverBudget)
|
|
{
|
|
<small class="text-danger">Over by @(Math.Abs(status.Remaining).ToString("C"))</small>
|
|
}
|
|
</td>
|
|
<td onclick="event.stopPropagation()">
|
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
|
onclick="openEditModal(@status.Budget.Id, '@(status.Budget.Category?.Replace("'", "\\'") ?? "")', @status.Budget.Amount.ToString(System.Globalization.CultureInfo.InvariantCulture), @((int)status.Budget.Period), '@status.Budget.StartDate.ToString("yyyy-MM-dd")', @status.Budget.IsActive.ToString().ToLower(), '@(status.Budget.Notes?.Replace("'", "\\'").Replace("\n", "\\n") ?? "")')">
|
|
Edit
|
|
</button>
|
|
<form method="post" asp-page-handler="ToggleActive" asp-route-id="@status.Budget.Id" class="d-inline">
|
|
<button type="submit" class="btn btn-sm btn-outline-secondary" title="Deactivate">
|
|
Pause
|
|
</button>
|
|
</form>
|
|
<form method="post" asp-page-handler="DeleteBudget" asp-route-id="@status.Budget.Id"
|
|
onsubmit="return confirm('Delete this budget?')" class="d-inline">
|
|
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Inactive Budgets -->
|
|
var inactiveBudgets = Model.BudgetStatuses.Where(s => !s.Budget.IsActive).ToList();
|
|
if (inactiveBudgets.Any())
|
|
{
|
|
<div class="card shadow-sm">
|
|
<div class="card-header">
|
|
<strong>Inactive Budgets</strong>
|
|
<small class="text-muted ms-2">Paused budgets are not tracked</small>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Category</th>
|
|
<th>Period</th>
|
|
<th class="text-end">Budget Amount</th>
|
|
<th>Notes</th>
|
|
<th style="width: 180px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var status in inactiveBudgets)
|
|
{
|
|
<tr class="text-muted">
|
|
<td>
|
|
@status.Budget.DisplayName
|
|
@if (status.Budget.IsTotalBudget)
|
|
{
|
|
<span class="badge bg-secondary ms-1">Total</span>
|
|
}
|
|
</td>
|
|
<td>@status.Budget.Period</td>
|
|
<td class="text-end">@status.Budget.Amount.ToString("C")</td>
|
|
<td>@(status.Budget.Notes ?? "-")</td>
|
|
<td>
|
|
<form method="post" asp-page-handler="ToggleActive" asp-route-id="@status.Budget.Id" class="d-inline">
|
|
<button type="submit" class="btn btn-sm btn-outline-success" title="Activate">
|
|
Resume
|
|
</button>
|
|
</form>
|
|
<form method="post" asp-page-handler="DeleteBudget" asp-route-id="@status.Budget.Id"
|
|
onsubmit="return confirm('Delete this budget?')" class="d-inline">
|
|
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<div class="alert alert-info">
|
|
<h5>No budgets found</h5>
|
|
<p>Click "Add New Budget" to create your first budget. You can create budgets for specific categories or a total spending budget.</p>
|
|
</div>
|
|
}
|
|
|
|
<!-- Add Modal -->
|
|
<div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form method="post" asp-page-handler="AddBudget">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="addModalLabel">Add New Budget</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="addCategory" class="form-label">Category</label>
|
|
<select name="model.Category" id="addCategory" class="form-select" asp-items="Model.CategoryOptions">
|
|
</select>
|
|
<div class="form-text">Select a category or leave as "Total Spending" to track all expenses</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="addAmount" class="form-label">Budget Amount</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">$</span>
|
|
<input name="model.Amount" id="addAmount" type="number" step="0.01" min="0.01" class="form-control" required />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="addPeriod" class="form-label">Period</label>
|
|
<select name="model.Period" id="addPeriod" class="form-select" asp-items="Model.PeriodOptions">
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="addStartDate" class="form-label">Start Date</label>
|
|
<input name="model.StartDate" id="addStartDate" type="date" class="form-control" value="@DateTime.Today.ToString("yyyy-MM-dd")" />
|
|
<div class="form-text">Used to calculate period boundaries (e.g., if monthly budget starts on the 15th, periods run 15th to 14th)</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="addNotes" class="form-label">Notes (optional)</label>
|
|
<textarea name="model.Notes" id="addNotes" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Add Budget</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form method="post" asp-page-handler="UpdateBudget">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="editModalLabel">Edit Budget</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" name="model.Id" id="editId" />
|
|
|
|
<div class="mb-3">
|
|
<label for="editCategory" class="form-label">Category</label>
|
|
<select name="model.Category" id="editCategory" class="form-select" asp-items="Model.CategoryOptions">
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAmount" class="form-label">Budget Amount</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">$</span>
|
|
<input name="model.Amount" id="editAmount" type="number" step="0.01" min="0.01" class="form-control" required />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editPeriod" class="form-label">Period</label>
|
|
<select name="model.Period" id="editPeriod" class="form-select" asp-items="Model.PeriodOptions">
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editStartDate" class="form-label">Start Date</label>
|
|
<input name="model.StartDate" id="editStartDate" type="date" class="form-control" required />
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editNotes" class="form-label">Notes (optional)</label>
|
|
<textarea name="model.Notes" id="editNotes" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
|
|
<div class="form-check mb-3">
|
|
<input type="checkbox" name="model.IsActive" id="editIsActive" class="form-check-input" value="true" />
|
|
<label for="editIsActive" class="form-check-label">Active</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
function openEditModal(id, category, amount, period, startDate, isActive, notes) {
|
|
document.getElementById('editId').value = id;
|
|
document.getElementById('editCategory').value = category;
|
|
document.getElementById('editAmount').value = amount;
|
|
document.getElementById('editPeriod').value = period;
|
|
document.getElementById('editStartDate').value = startDate;
|
|
document.getElementById('editIsActive').checked = isActive;
|
|
document.getElementById('editNotes').value = notes;
|
|
|
|
var modal = new bootstrap.Modal(document.getElementById('editModal'));
|
|
modal.show();
|
|
}
|
|
</script>
|
|
}
|