4be5658d32
Restructure the flat 7-item navbar into logical dropdown groups (Transactions, Receipts, Accounts), add a prominent Upload button, settings gear icon, breadcrumb navigation on 11 deep pages, and dashboard quick-action cards with hover effects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
10 KiB
Plaintext
230 lines
10 KiB
Plaintext
@page "{id:long}"
|
|
@model MoneyMap.Pages.ViewReceiptModel
|
|
@{
|
|
ViewData["Title"] = "View Receipt";
|
|
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
|
|
{
|
|
("Transactions", Url.Page("/Transactions")),
|
|
($"Transaction #{Model.Receipt.TransactionId}", Url.Page("/EditTransaction", new { id = Model.Receipt.TransactionId })),
|
|
("Receipt", null)
|
|
};
|
|
}
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h2>Receipt Details</h2>
|
|
<a asp-page="/EditTransaction" asp-route-id="@Model.Receipt.TransactionId" class="btn btn-outline-secondary">
|
|
Back to Transaction
|
|
</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">
|
|
<!-- Left column - Receipt Image/PDF -->
|
|
<div class="col-lg-8">
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header">
|
|
<strong>@Model.Receipt.FileName</strong>
|
|
</div>
|
|
<div class="card-body text-center">
|
|
@if (Model.Receipt.ContentType == "application/pdf")
|
|
{
|
|
<iframe src="@Model.ReceiptUrl"
|
|
style="width: 100%; height: 800px; border: 1px solid #ddd;"
|
|
title="Receipt PDF">
|
|
</iframe>
|
|
}
|
|
else
|
|
{
|
|
<img src="@Model.ReceiptUrl"
|
|
alt="Receipt"
|
|
class="img-fluid"
|
|
style="max-height: 800px; border: 1px solid #ddd;" />
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Line Items - Moved here for more space -->
|
|
@if (Model.LineItems.Any())
|
|
{
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header">
|
|
<strong>Line Items (@Model.LineItems.Count)</strong>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 50px;">#</th>
|
|
<th>Description</th>
|
|
<th class="text-center" style="width: 80px;">Qty</th>
|
|
<th class="text-end" style="width: 100px;">Unit Price</th>
|
|
<th class="text-end" style="width: 100px;">Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var item in Model.LineItems)
|
|
{
|
|
<tr>
|
|
<td class="text-muted">@item.LineNumber</td>
|
|
<td>@item.Description</td>
|
|
<td class="text-center">
|
|
@(item.Quantity?.ToString("0.##") ?? "-")
|
|
</td>
|
|
<td class="text-end">
|
|
@(item.UnitPrice?.ToString("C") ?? "-")
|
|
</td>
|
|
<td class="text-end">
|
|
<strong>@(item.LineTotal?.ToString("C") ?? "-")</strong>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Right column - Metadata -->
|
|
<div class="col-lg-4">
|
|
<!-- Receipt Info -->
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header">
|
|
<strong>Receipt Information</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<dl class="row mb-0">
|
|
@if (!string.IsNullOrWhiteSpace(Model.Receipt.Merchant))
|
|
{
|
|
<dt class="col-sm-4">Merchant</dt>
|
|
<dd class="col-sm-8">@Model.Receipt.Merchant</dd>
|
|
}
|
|
|
|
@if (Model.Receipt.ReceiptDate.HasValue)
|
|
{
|
|
<dt class="col-sm-4">Date</dt>
|
|
<dd class="col-sm-8">@Model.Receipt.ReceiptDate.Value.ToString("MMM d, yyyy")</dd>
|
|
}
|
|
|
|
@if (Model.Receipt.Subtotal.HasValue)
|
|
{
|
|
<dt class="col-sm-4">Subtotal</dt>
|
|
<dd class="col-sm-8">@Model.Receipt.Subtotal.Value.ToString("C")</dd>
|
|
}
|
|
|
|
@if (Model.Receipt.Tax.HasValue)
|
|
{
|
|
<dt class="col-sm-4">Tax</dt>
|
|
<dd class="col-sm-8">@Model.Receipt.Tax.Value.ToString("C")</dd>
|
|
}
|
|
|
|
@if (Model.Receipt.Total.HasValue)
|
|
{
|
|
<dt class="col-sm-4">Total</dt>
|
|
<dd class="col-sm-8"><strong>@Model.Receipt.Total.Value.ToString("C")</strong></dd>
|
|
}
|
|
|
|
<dt class="col-sm-4">File Size</dt>
|
|
<dd class="col-sm-8">@((Model.Receipt.FileSizeBytes / 1024.0).ToString("F1")) KB</dd>
|
|
|
|
<dt class="col-sm-4">Uploaded</dt>
|
|
<dd class="col-sm-8">@Model.Receipt.UploadedAtUtc.ToLocalTime().ToString("MMM d, yyyy h:mm tt")</dd>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(Model.Receipt.ParsingNotes))
|
|
{
|
|
<dt class="col-sm-4">AI Notes</dt>
|
|
<dd class="col-sm-8">@Model.Receipt.ParsingNotes</dd>
|
|
}
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Parse Receipt -->
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header">
|
|
<strong>Parse Receipt</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="post" asp-page-handler="Parse" asp-route-id="@Model.Receipt.Id">
|
|
<p class="text-muted small mb-2">
|
|
Using: <strong>@Model.SelectedModel</strong>
|
|
<a href="/Settings" class="ms-2 small">Change</a>
|
|
</p>
|
|
<div class="mb-2">
|
|
<label for="ParsingNotes" class="form-label small text-muted mb-1">Notes for AI</label>
|
|
<textarea asp-for="ParsingNotes" class="form-control form-control-sm" rows="3"
|
|
placeholder="Optional hints for parsing (e.g., 'This is a restaurant receipt', 'Ignore the voided items')"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-sm w-100">
|
|
Parse Receipt
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Parse Logs -->
|
|
@if (Model.ParseLogs.Any())
|
|
{
|
|
<div class="card shadow-sm">
|
|
<div class="card-header">
|
|
<strong>Parse History</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
@foreach (var log in Model.ParseLogs)
|
|
{
|
|
<div class="border-bottom pb-2 mb-2">
|
|
<div class="d-flex justify-content-between">
|
|
<span class="fw-bold">@log.Provider (@log.Model)</span>
|
|
@if (log.Success)
|
|
{
|
|
<span class="badge bg-success">Success</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-danger">Failed</span>
|
|
}
|
|
</div>
|
|
<small class="text-muted">
|
|
@log.StartedAtUtc.ToLocalTime().ToString("MMM d, yyyy h:mm tt")
|
|
</small>
|
|
@if (log.Confidence.HasValue)
|
|
{
|
|
<div class="small">Confidence: @((log.Confidence.Value * 100).ToString("F1"))%</div>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(log.Error))
|
|
{
|
|
<div class="small text-danger mt-1">@log.Error</div>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(log.RawProviderPayloadJson) && log.RawProviderPayloadJson != "{}")
|
|
{
|
|
<a class="small" data-bs-toggle="collapse" href="#rawPayload@(log.Id)" role="button" aria-expanded="false">
|
|
Show LLM Response
|
|
</a>
|
|
<div class="collapse mt-1" id="rawPayload@(log.Id)">
|
|
<pre class="bg-body-secondary p-2 rounded small" style="max-height: 400px; overflow: auto; white-space: pre-wrap; word-break: break-word;">@log.RawProviderPayloadJson</pre>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div> |