- Add hidden fields for immutable transaction properties to preserve values during form submission - Make category field optional by removing validation for empty values - Simplify category input handling by removing duplicate hidden field - Clean up JavaScript by using proper element IDs instead of querySelector 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
338 lines
17 KiB
Plaintext
338 lines
17 KiB
Plaintext
@page "{id:long}"
|
||
@model MoneyMap.Pages.EditTransactionModel
|
||
@{
|
||
ViewData["Title"] = "Edit Transaction";
|
||
}
|
||
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<h2 class="mb-0">Edit Transaction <small class="text-muted">#@Model.Transaction.Id</small> <button type="button" class="btn btn-sm btn-outline-secondary ms-2" onclick="copyTransactionId()">Copy ID</button></h2>
|
||
<a asp-page="/Transactions" class="btn btn-outline-secondary">Back to Transactions</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>
|
||
}
|
||
|
||
<div class="row">
|
||
<div class="col-lg-8">
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header">
|
||
<strong>Transaction Details</strong>
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="post">
|
||
<input type="hidden" asp-for="Transaction.Id" />
|
||
<input type="hidden" asp-for="Transaction.Date" />
|
||
<input type="hidden" asp-for="Transaction.Name" />
|
||
<input type="hidden" asp-for="Transaction.Memo" />
|
||
<input type="hidden" asp-for="Transaction.Amount" />
|
||
<input type="hidden" asp-for="Transaction.CardLabel" />
|
||
<input type="hidden" asp-for="Transaction.AccountLabel" />
|
||
|
||
<!-- Read-only fields -->
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label fw-bold">Date</label>
|
||
<div class="form-control-plaintext">@Model.Transaction.Date.ToString("yyyy-MM-dd")</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label fw-bold">Amount</label>
|
||
<div class="form-control-plaintext">
|
||
<span class="fs-5 @(Model.Transaction.Amount >= 0 ? "text-success" : "text-danger")">
|
||
@Model.Transaction.Amount.ToString("C")
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label fw-bold">Transaction Name</label>
|
||
<div class="form-control-plaintext">@Model.Transaction.Name</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label fw-bold">Memo</label>
|
||
<div class="form-control-plaintext">@Model.Transaction.Memo</div>
|
||
</div>
|
||
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label fw-bold">Payment Method</label>
|
||
<div class="form-control-plaintext">@Model.Transaction.CardLabel</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label fw-bold">Account</label>
|
||
<div class="form-control-plaintext">@Model.Transaction.AccountLabel</div>
|
||
</div>
|
||
</div>
|
||
|
||
<hr class="my-4" />
|
||
|
||
<!-- Editable fields -->
|
||
<div class="mb-3">
|
||
<label class="form-label fw-bold">Category</label>
|
||
<select class="form-select mb-2" id="categorySelect" onchange="handleCategoryChange()">
|
||
<option value="">(uncategorized)</option>
|
||
@foreach (var cat in Model.AvailableCategories)
|
||
{
|
||
<option value="@cat" selected="@(Model.Transaction.Category == cat)">@cat</option>
|
||
}
|
||
<option value="__custom__" selected="@Model.UseCustomCategory">+ Enter custom category</option>
|
||
</select>
|
||
|
||
<div id="customCategoryInput" style="display: @(Model.UseCustomCategory ? "block" : "none")">
|
||
<input asp-for="Transaction.Category"
|
||
class="form-control"
|
||
id="categoryInput"
|
||
placeholder="Enter custom category" />
|
||
</div>
|
||
|
||
<div class="form-text">Select a category or create a new one</div>
|
||
<span asp-validation-for="Transaction.Category" class="text-danger"></span>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label fw-bold">Merchant</label>
|
||
<select class="form-select mb-2" id="merchantSelect" onchange="handleMerchantChange()">
|
||
<option value="">(no merchant)</option>
|
||
@foreach (var merchant in Model.AvailableMerchants)
|
||
{
|
||
<option value="@merchant.Id" selected="@(Model.Transaction.MerchantId == merchant.Id)">@merchant.Name</option>
|
||
}
|
||
<option value="__custom__" selected="@Model.UseCustomMerchant">+ Enter custom merchant</option>
|
||
</select>
|
||
|
||
<div id="customMerchantInput" style="display: @(Model.UseCustomMerchant ? "block" : "none")">
|
||
<input asp-for="Transaction.MerchantName"
|
||
class="form-control"
|
||
placeholder="Enter merchant name" />
|
||
</div>
|
||
|
||
<input type="hidden" asp-for="Transaction.MerchantId" id="merchantHidden" />
|
||
|
||
<div class="form-text">Select a merchant or create a new one</div>
|
||
<span asp-validation-for="Transaction.MerchantId" class="text-danger"></span>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label asp-for="Transaction.Notes" class="form-label fw-bold">Notes</label>
|
||
<textarea asp-for="Transaction.Notes"
|
||
class="form-control"
|
||
rows="3"
|
||
placeholder="Add any additional details about this transaction..."></textarea>
|
||
<div class="form-text">Optional: Add context or details to help you remember what this transaction was for</div>
|
||
<span asp-validation-for="Transaction.Notes" class="text-danger"></span>
|
||
</div>
|
||
|
||
<div class="d-flex gap-2">
|
||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||
<a asp-page="/Transactions" class="btn btn-secondary">Cancel</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header">
|
||
<strong>Receipts (@Model.Receipts.Count)</strong>
|
||
</div>
|
||
<div class="card-body">
|
||
@if (Model.Receipts.Any())
|
||
{
|
||
@foreach (var item in Model.Receipts)
|
||
{
|
||
<div class="card mb-3">
|
||
<div class="card-body p-2">
|
||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||
<div class="flex-grow-1">
|
||
<div class="fw-bold text-truncate" style="max-width: 200px;" title="@item.Receipt.FileName">
|
||
@item.Receipt.FileName
|
||
</div>
|
||
<small class="text-muted">
|
||
@((item.Receipt.FileSizeBytes / 1024.0).ToString("F1")) KB
|
||
<20> @item.Receipt.UploadedAtUtc.ToLocalTime().ToString("MMM d, yyyy")
|
||
</small>
|
||
@if (!string.IsNullOrWhiteSpace(item.Receipt.Merchant))
|
||
{
|
||
<div class="small text-success mt-1">
|
||
<strong>@item.Receipt.Merchant</strong>
|
||
@if (item.Receipt.Total.HasValue)
|
||
{
|
||
<span> - @item.Receipt.Total.Value.ToString("C")</span>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
@if (item.LineItems.Any())
|
||
{
|
||
<div class="border rounded p-2 mb-2 bg-light">
|
||
<div class="small fw-bold mb-1">Line Items (@item.LineItems.Count)</div>
|
||
<div style="max-height: 150px; overflow-y: auto;">
|
||
@foreach (var lineItem in item.LineItems)
|
||
{
|
||
<div class="d-flex justify-content-between small">
|
||
<span class="text-truncate" style="max-width: 120px;" title="@lineItem.Description">
|
||
@if (lineItem.Quantity.HasValue)
|
||
{
|
||
<text>@lineItem.Quantity.Value.ToString("0.##")x </text>
|
||
}
|
||
@lineItem.Description
|
||
</span>
|
||
<span class="text-end">@(lineItem.LineTotal?.ToString("C") ?? "-")</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<div class="d-flex gap-1 flex-wrap">
|
||
<a asp-page="/ViewReceipt" asp-route-id="@item.Receipt.Id"
|
||
target="_blank"
|
||
class="btn btn-sm btn-outline-primary">
|
||
View
|
||
</a>
|
||
@if (!item.LineItems.Any())
|
||
{
|
||
<form method="post" asp-page-handler="ParseReceipt" asp-route-receiptId="@item.Receipt.Id" class="d-inline">
|
||
<button type="submit" class="btn btn-sm btn-outline-info">Parse</button>
|
||
</form>
|
||
}
|
||
<form method="post" asp-page-handler="UnmapReceipt" asp-route-receiptId="@item.Receipt.Id" class="d-inline">
|
||
<button type="submit" class="btn btn-sm btn-outline-warning" title="Unmap from this transaction (receipt will go back to Receipts page)">Unmap</button>
|
||
</form>
|
||
<form method="post" asp-page-handler="DeleteReceipt" asp-route-receiptId="@item.Receipt.Id"
|
||
onsubmit="return confirm('Are you sure? This will PERMANENTLY DELETE the receipt file!')" class="d-inline">
|
||
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<p class="text-muted mb-3">No receipts attached</p>
|
||
}
|
||
|
||
<hr />
|
||
|
||
<form method="post" asp-page-handler="UploadReceipt" enctype="multipart/form-data">
|
||
<input type="hidden" asp-for="Transaction.Id" />
|
||
<div class="mb-2">
|
||
<label for="ReceiptFile" class="form-label small fw-bold">Upload Receipt</label>
|
||
<input asp-for="ReceiptFile" type="file" class="form-control form-control-sm" accept="image/*,.pdf" />
|
||
</div>
|
||
<button type="submit" class="btn btn-sm btn-primary w-100">Upload</button>
|
||
<div class="form-text small">
|
||
Accepts: JPG, PNG, PDF, GIF, HEIC (max 10MB)
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card shadow-sm mt-3">
|
||
<div class="card-header">
|
||
<strong>Quick Tips</strong>
|
||
</div>
|
||
<div class="card-body">
|
||
<ul class="small mb-0">
|
||
<li>Use the category dropdown to see existing categories</li>
|
||
<li>Leave category blank to mark as uncategorized</li>
|
||
<li>You can create new categories by typing them in</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@section Scripts {
|
||
<partial name="_ValidationScriptsPartial" />
|
||
<script>
|
||
function handleCategoryChange() {
|
||
const select = document.getElementById('categorySelect');
|
||
const customInputDiv = document.getElementById('customCategoryInput');
|
||
const categoryInput = document.getElementById('categoryInput');
|
||
|
||
if (select.value === '__custom__') {
|
||
customInputDiv.style.display = 'block';
|
||
categoryInput.value = '';
|
||
categoryInput.focus();
|
||
} else {
|
||
customInputDiv.style.display = 'none';
|
||
categoryInput.value = select.value;
|
||
}
|
||
}
|
||
|
||
function handleMerchantChange() {
|
||
const select = document.getElementById('merchantSelect');
|
||
const customInput = document.getElementById('customMerchantInput');
|
||
const hiddenInput = document.getElementById('merchantHidden');
|
||
const merchantNameInput = document.querySelector('input[name="Transaction.MerchantName"]');
|
||
|
||
if (select.value === '__custom__') {
|
||
customInput.style.display = 'block';
|
||
hiddenInput.value = '';
|
||
merchantNameInput.value = '';
|
||
merchantNameInput.focus();
|
||
} else {
|
||
customInput.style.display = 'none';
|
||
hiddenInput.value = select.value;
|
||
merchantNameInput.value = '';
|
||
}
|
||
}
|
||
|
||
// Update hidden field when custom input changes
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const categoryInput = document.querySelector('input[name="Transaction.Category"]');
|
||
const categorySelect = document.getElementById('categorySelect');
|
||
|
||
if (categoryInput) {
|
||
categoryInput.addEventListener('input', function() {
|
||
if (categorySelect.value === '__custom__') {
|
||
// Keep custom selected when typing
|
||
}
|
||
});
|
||
}
|
||
|
||
// Initialize on page load
|
||
handleCategoryChange();
|
||
handleMerchantChange();
|
||
});
|
||
</script>
|
||
}
|
||
|
||
|
||
<script>
|
||
function copyTransactionId() {
|
||
var id = '@Model.Transaction.Id';
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
navigator.clipboard.writeText(id.toString());
|
||
} else {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = id;
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
}
|
||
var btns = document.querySelectorAll('button[onclick="copyTransactionId()"]');
|
||
btns.forEach(function(btn){ var old=btn.textContent; btn.textContent='Copied!'; setTimeout(function(){ btn.textContent=old; }, 1500); });
|
||
}
|
||
</script>
|