Extracted all inline JavaScript from Razor pages into dedicated external files for better code organization and browser caching: - upload.js: transaction preview, pagination, form handling - transactions.js: date range filters, Bootstrap tooltips - edit-transaction.js: category/merchant dropdowns, copy functionality - merchants.js: modal handling for merchant management - category-mappings.js: modal handling for category rules Updated all affected .cshtml files to reference external scripts instead of inline script blocks. This improves maintainability, enables browser caching, and provides better separation of concerns.
310 lines
14 KiB
Plaintext
310 lines
14 KiB
Plaintext
@page
|
|
@model MoneyMap.Pages.TransactionsModel
|
|
@{
|
|
ViewData["Title"] = "Transactions";
|
|
}
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h2>Transactions</h2>
|
|
<div class="d-flex gap-2">
|
|
<a asp-page="/Upload" class="btn btn-primary">Upload CSV</a>
|
|
<a asp-page="/Index" class="btn btn-outline-secondary">Back to Dashboard</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3">
|
|
<div class="col-md-12 mb-2">
|
|
<label for="Search" class="form-label">Search</label>
|
|
<input asp-for="Search" type="text" class="form-control" placeholder="Search by name, memo, category, notes, or merchant..." />
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="Category" class="form-label">Category</label>
|
|
<select asp-for="Category" class="form-select">
|
|
<option value="">All Categories</option>
|
|
@foreach (var cat in Model.AvailableCategories)
|
|
{
|
|
<option value="@(string.IsNullOrWhiteSpace(cat) ? "(blank)" : cat)">@(string.IsNullOrWhiteSpace(cat) ? "(blank)" : cat)</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="Merchant" class="form-label">Merchant</label>
|
|
<select asp-for="Merchant" class="form-select">
|
|
<option value="">All Merchants</option>
|
|
@foreach (var merchant in Model.AvailableMerchants)
|
|
{
|
|
<option value="@merchant">@merchant</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="CardId" class="form-label">Card</label>
|
|
<select asp-for="CardId" class="form-select">
|
|
<option value="">All Cards</option>
|
|
@foreach (var card in Model.AvailableCards)
|
|
{
|
|
<option value="@card.Id">@card.Owner - @card.Last4</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="StartDate" class="form-label">Start Date</label>
|
|
<input asp-for="StartDate" type="date" class="form-control" id="startDateInput" />
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="EndDate" class="form-label">End Date</label>
|
|
<input asp-for="EndDate" type="date" class="form-control" id="endDateInput" />
|
|
</div>
|
|
<div class="col-md-12 mb-2">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="Quick date ranges">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(30)">Last 30 Days</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(60)">Last 60 Days</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(90)">Last 90 Days</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(365)">Last Year</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="setDateRangeThisMonth()">This Month</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="setDateRangeLastMonth()">Last Month</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<button type="submit" class="btn btn-primary w-100">Filter</button>
|
|
</div>
|
|
</form>
|
|
@if (!string.IsNullOrWhiteSpace(Model.Search) || !string.IsNullOrWhiteSpace(Model.Category) || !string.IsNullOrWhiteSpace(Model.Merchant) || !string.IsNullOrWhiteSpace(Model.CardId) || Model.StartDate.HasValue || Model.EndDate.HasValue)
|
|
{
|
|
<div class="mt-2">
|
|
<a asp-page="/Transactions" class="btn btn-sm btn-outline-secondary">Clear Filters</a>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="text-muted small">Transactions</div>
|
|
<div class="fs-4 fw-bold">@Model.Stats.Count</div>
|
|
<div class="small text-muted">
|
|
Showing @Model.Transactions.Count on page @Model.PageNumber of @Model.TotalPages
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="text-muted small">Total Spent</div>
|
|
<div class="fs-4 fw-bold text-danger">@Model.Stats.TotalDebits.ToString("C")</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="text-muted small">Total Income</div>
|
|
<div class="fs-4 fw-bold text-success">@Model.Stats.TotalCredits.ToString("C")</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="text-muted small">Net</div>
|
|
<div class="fs-4 fw-bold @(Model.Stats.NetAmount >= 0 ? "text-success" : "text-danger")">
|
|
@Model.Stats.NetAmount.ToString("C")
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transactions Table -->
|
|
@if (Model.Transactions.Any())
|
|
{
|
|
<div class="card shadow-sm">
|
|
<div class="card-header">
|
|
@if (!string.IsNullOrWhiteSpace(Model.Category))
|
|
{
|
|
<strong>@Model.Category</strong>
|
|
<span class="text-muted">- @Model.Stats.Count transactions</span>
|
|
}
|
|
else
|
|
{
|
|
<strong>All Transactions</strong>
|
|
<span class="text-muted">- @Model.Stats.Count total</span>
|
|
}
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 70px;">ID</th>
|
|
<th style="width: 110px;">Date</th>
|
|
<th>Name</th>
|
|
<th>Memo</th>
|
|
<th style="width: 110px;" class="text-end">Amount</th>
|
|
<th style="width: 160px;">Category</th>
|
|
<th style="width: 140px;">Payment/Account</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var t in Model.Transactions)
|
|
{
|
|
<tr style="cursor: pointer;" title="Open details" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
|
<td class="small text-muted">#@t.Id</td>
|
|
<td>@t.Date.ToString("yyyy-MM-dd")</td>
|
|
<td>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<a asp-page="/EditTransaction" asp-route-id="@t.Id" class="text-decoration-none text-dark">@t.Name</a>
|
|
@if (t.ReceiptCount > 0)
|
|
{
|
|
<span class="badge bg-success" title="@t.ReceiptCount receipt(s) attached">
|
|
@t.ReceiptCount
|
|
</span>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(t.Notes))
|
|
{
|
|
<span class="badge bg-info"
|
|
title="@t.Notes"
|
|
data-bs-toggle="tooltip"
|
|
data-bs-placement="top">Note</span>
|
|
}
|
|
</div>
|
|
</td>
|
|
<td class="text-truncate" style="max-width:320px">@t.Memo</td>
|
|
<td class="text-end @(t.Amount >= 0 ? "text-success" : "")">
|
|
@t.Amount.ToString("C")
|
|
</td>
|
|
<td>
|
|
@if (string.IsNullOrWhiteSpace(t.Category))
|
|
{
|
|
<span class="text-muted">(uncategorized)</span>
|
|
}
|
|
else
|
|
{
|
|
@t.Category
|
|
}
|
|
</td>
|
|
<td class="small">
|
|
@t.CardLabel
|
|
</td>
|
|
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="alert alert-info">
|
|
No transactions found matching the selected filters.
|
|
</div>
|
|
}
|
|
|
|
<!-- Pagination -->
|
|
@if (Model.TotalPages > 1)
|
|
{
|
|
<nav aria-label="Transaction pagination" class="mt-3">
|
|
<ul class="pagination justify-content-center">
|
|
<!-- Previous -->
|
|
<li class="page-item @(Model.PageNumber == 1 ? "disabled" : "")">
|
|
<a class="page-link"
|
|
asp-page="/Transactions"
|
|
asp-route-pageNumber="@(Model.PageNumber - 1)"
|
|
asp-route-search="@Model.Search"
|
|
asp-route-category="@Model.Category"
|
|
asp-route-merchant="@Model.Merchant"
|
|
asp-route-cardId="@Model.CardId"
|
|
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
|
|
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
|
|
Previous
|
|
</a>
|
|
</li>
|
|
|
|
<!-- First page -->
|
|
@if (Model.PageNumber > 3)
|
|
{
|
|
<li class="page-item">
|
|
<a class="page-link"
|
|
asp-page="/Transactions"
|
|
asp-route-pageNumber="1"
|
|
asp-route-search="@Model.Search"
|
|
asp-route-category="@Model.Category"
|
|
asp-route-merchant="@Model.Merchant"
|
|
asp-route-cardId="@Model.CardId"
|
|
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
|
|
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
|
|
1
|
|
</a>
|
|
</li>
|
|
<li class="page-item disabled"><span class="page-link">...</span></li>
|
|
}
|
|
|
|
<!-- Page numbers -->
|
|
@for (int i = Math.Max(1, Model.PageNumber - 2); i <= Math.Min(Model.TotalPages, Model.PageNumber + 2); i++)
|
|
{
|
|
<li class="page-item @(i == Model.PageNumber ? "active" : "")">
|
|
<a class="page-link"
|
|
asp-page="/Transactions"
|
|
asp-route-pageNumber="@i"
|
|
asp-route-search="@Model.Search"
|
|
asp-route-category="@Model.Category"
|
|
asp-route-merchant="@Model.Merchant"
|
|
asp-route-cardId="@Model.CardId"
|
|
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
|
|
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
|
|
@i
|
|
</a>
|
|
</li>
|
|
}
|
|
|
|
<!-- Last page -->
|
|
@if (Model.PageNumber < Model.TotalPages - 2)
|
|
{
|
|
<li class="page-item disabled"><span class="page-link">...</span></li>
|
|
<li class="page-item">
|
|
<a class="page-link"
|
|
asp-page="/Transactions"
|
|
asp-route-pageNumber="@Model.TotalPages"
|
|
asp-route-search="@Model.Search"
|
|
asp-route-category="@Model.Category"
|
|
asp-route-merchant="@Model.Merchant"
|
|
asp-route-cardId="@Model.CardId"
|
|
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
|
|
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
|
|
@Model.TotalPages
|
|
</a>
|
|
</li>
|
|
}
|
|
|
|
<!-- Next -->
|
|
<li class="page-item @(Model.PageNumber >= Model.TotalPages ? "disabled" : "")">
|
|
<a class="page-link"
|
|
asp-page="/Transactions"
|
|
asp-route-pageNumber="@(Model.PageNumber + 1)"
|
|
asp-route-search="@Model.Search"
|
|
asp-route-category="@Model.Category"
|
|
asp-route-merchant="@Model.Merchant"
|
|
asp-route-cardId="@Model.CardId"
|
|
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
|
|
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
|
|
Next
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
}
|
|
|
|
@section Scripts {
|
|
<script src="~/js/transactions.js"></script>
|
|
}
|
|
|