Improve: Overhaul navigation with grouped dropdowns, breadcrumbs, and quick-actions

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>
This commit is contained in:
2026-02-15 19:41:56 -05:00
parent 324ab2c627
commit 4be5658d32
15 changed files with 249 additions and 23 deletions

View File

@@ -2,6 +2,12 @@
@model MoneyMap.Pages.AICategorizePreviewModel
@{
ViewData["Title"] = "AI Categorization Preview";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Transactions", Url.Page("/Transactions")),
("Recategorize", Url.Page("/Recategorize")),
("AI Preview", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">

View File

@@ -2,6 +2,11 @@
@model MoneyMap.Pages.AccountDetailsModel
@{
ViewData["Title"] = $"Account - {Model.Account.DisplayLabel}";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Accounts", Url.Page("/Accounts")),
(Model.Account.DisplayLabel, null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">

View File

@@ -3,6 +3,11 @@
@model MoneyMap.Pages.EditAccountModel
@{
ViewData["Title"] = Model.IsNew ? "Add Account" : "Edit Account";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Accounts", Url.Page("/Accounts")),
(Model.IsNew ? "Add Account" : "Edit Account", null)
};
}
<h2>@ViewData["Title"]</h2>

View File

@@ -2,6 +2,12 @@
@model MoneyMap.Pages.EditCardModel
@{
ViewData["Title"] = Model.IsNewCard ? "Add Card" : "Edit Card";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Accounts", Url.Page("/Accounts")),
("Cards", Url.Page("/Cards")),
(Model.IsNewCard ? "Add Card" : "Edit Card", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">

View File

@@ -2,6 +2,11 @@
@model MoneyMap.Pages.EditTransactionModel
@{
ViewData["Title"] = "Edit Transaction";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Transactions", Url.Page("/Transactions")),
($"#{Model.Transaction.Id}", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">

View File

@@ -52,11 +52,69 @@
</div>
</div>
<div class="my-3 d-flex gap-2">
<a class="btn btn-primary" asp-page="/Upload">Upload CSV</a>
<a class="btn btn-outline-secondary" asp-page="/Transactions">View All Transactions</a>
<a class="btn btn-outline-secondary" asp-page="/CategoryMappings">Categories</a>
<a class="btn btn-outline-secondary" asp-page="/Budgets">Budgets</a>
<div class="row g-3 my-3">
<div class="col-sm-6 col-lg-3">
<a asp-page="/Upload" class="card shadow-sm quick-action-card d-block h-100 text-decoration-none">
<div class="card-body d-flex align-items-center gap-3">
<div class="quick-action-icon bg-primary bg-opacity-25 text-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
</svg>
</div>
<div>
<div class="fw-semibold text-body">Upload Transactions</div>
<small class="text-muted">Import CSV files</small>
</div>
</div>
</a>
</div>
<div class="col-sm-6 col-lg-3">
<a asp-page="/Transactions" asp-route-category="(blank)" class="card shadow-sm quick-action-card d-block h-100 text-decoration-none">
<div class="card-body d-flex align-items-center gap-3">
<div class="quick-action-icon bg-warning bg-opacity-25 text-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M7.005 3.1a1 1 0 1 1 1.99 0l-.388 6.35a.61.61 0 0 1-1.214 0zM7 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0"/>
</svg>
</div>
<div>
<div class="fw-semibold text-body">Review Uncategorized</div>
<small class="text-muted">@Model.Stats.Uncategorized transactions</small>
</div>
</div>
</a>
</div>
<div class="col-sm-6 col-lg-3">
<a asp-page="/ReceiptQueue" class="card shadow-sm quick-action-card d-block h-100 text-decoration-none">
<div class="card-body d-flex align-items-center gap-3">
<div class="quick-action-icon bg-info bg-opacity-25 text-info">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
<path d="M4.5 12.5A.5.5 0 0 1 5 12h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5m0-2A.5.5 0 0 1 5 10h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5m1.639-3.708 1.33.886 1.854-1.855a.25.25 0 0 1 .289-.047l1.888.974V8.5a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V8z"/>
</svg>
</div>
<div>
<div class="fw-semibold text-body">Receipt Parse Queue</div>
<small class="text-muted">Process pending receipts</small>
</div>
</div>
</a>
</div>
<div class="col-sm-6 col-lg-3">
<a asp-page="/Budgets" class="card shadow-sm quick-action-card d-block h-100 text-decoration-none">
<div class="card-body d-flex align-items-center gap-3">
<div class="quick-action-icon bg-success bg-opacity-25 text-success">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 10.781c.148 1.667 1.513 2.85 3.591 3.003V15h1.043v-1.216c2.27-.179 3.678-1.438 3.678-3.3 0-1.59-.947-2.51-2.956-3.028l-.722-.187V3.467c1.122.11 1.879.714 2.07 1.616h1.47c-.166-1.6-1.54-2.748-3.54-2.875V1H7.591v1.233c-1.939.23-3.27 1.472-3.27 3.156 0 1.454.966 2.483 2.661 2.917l.61.162v4.031c-1.149-.17-1.94-.8-2.131-1.718zm3.391-3.836c-1.043-.263-1.6-.825-1.6-1.616 0-.944.704-1.641 1.8-1.828v3.495l-.2-.05zm1.591 1.872c1.287.323 1.852.859 1.852 1.769 0 1.097-.826 1.828-2.2 1.939V8.73z"/>
</svg>
</div>
<div>
<div class="fw-semibold text-body">Budgets</div>
<small class="text-muted">@Model.BudgetStatuses.Count active budgets</small>
</div>
</div>
</a>
</div>
</div>
<div class="row g-3 my-2">
<div class="col-lg-6">

View File

@@ -2,6 +2,11 @@
@model MoneyMap.Pages.ReceiptQueueModel
@{
ViewData["Title"] = "Receipt Queue";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Receipts", Url.Page("/Receipts")),
("Parse Queue", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">

View File

@@ -2,6 +2,11 @@
@model MoneyMap.Pages.ReviewAISuggestionsModel
@{
ViewData["Title"] = "AI Categorization Suggestions";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Transactions", Url.Page("/Transactions")),
("AI Suggestions", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">

View File

@@ -2,6 +2,12 @@
@model MoneyMap.Pages.ReviewAISuggestionsWithProposalsModel
@{
ViewData["Title"] = "Review AI Suggestions";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Transactions", Url.Page("/Transactions")),
("AI Suggestions", Url.Page("/ReviewAISuggestions")),
("Review Proposals", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">

View File

@@ -2,6 +2,11 @@
@model MoneyMap.Pages.ReviewReceiptsModel
@{
ViewData["Title"] = "Review Receipts";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Receipts", Url.Page("/Receipts")),
("Review Mappings", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">

View File

@@ -22,23 +22,62 @@
<li class="nav-item">
<a class="nav-link" asp-page="/Index">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/Transactions">Transactions</a>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Transactions
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" asp-page="/Transactions">All Transactions</a></li>
<li><a class="dropdown-item" asp-page="/Recategorize">Recategorize</a></li>
<li><a class="dropdown-item" asp-page="/AICategorizePreview">AI Review</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Receipts
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" asp-page="/Receipts">All Receipts</a></li>
<li><a class="dropdown-item" asp-page="/ReceiptQueue">Parse Queue</a></li>
<li><a class="dropdown-item" asp-page="/ReviewReceipts">Review Mappings</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Accounts
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" asp-page="/Accounts">Bank Accounts</a></li>
<li><a class="dropdown-item" asp-page="/Cards">Cards</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/Receipts">Receipts</a>
<a class="nav-link" asp-page="/Budgets">Budgets</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/Accounts">Accounts</a>
</ul>
<ul class="navbar-nav ms-auto align-items-center">
<li class="nav-item me-2">
<a class="btn btn-sm btn-primary" asp-page="/Upload">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
</svg>
Upload
</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/CategoryMappings">Categories</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/Merchants">Merchants</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/Recategorize">Recategorize</a>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Settings">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-gear" viewBox="0 0 16 16">
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"/>
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.892 3.434-.901 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.892-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.658.06a1.873 1.873 0 0 1 3.724 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16a1.873 1.873 0 0 1 2.693 2.693l-.16.291a1.873 1.873 0 0 0 1.116 2.693l.318.094a1.873 1.873 0 0 1 0 3.724l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291a1.873 1.873 0 0 1-2.693 2.693l-.292-.16a1.873 1.873 0 0 0-2.693 1.116l-.094.318a1.873 1.873 0 0 1-3.724 0l-.094-.319a1.873 1.873 0 0 0-2.693-1.115l-.291.16a1.873 1.873 0 0 1-2.693-2.693l.16-.291a1.873 1.873 0 0 0-1.116-2.693l-.318-.094a1.873 1.873 0 0 1 0-3.724l.319-.094a1.873 1.873 0 0 0 1.115-2.693l-.16-.291a1.873 1.873 0 0 1 2.693-2.693l.292.16a1.873 1.873 0 0 0 2.693-1.116z"/>
</svg>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" asp-page="/CategoryMappings">Category Rules</a></li>
<li><a class="dropdown-item" asp-page="/Merchants">Merchants</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" asp-page="/Settings">AI Settings</a></li>
</ul>
</li>
</ul>
</div>
@@ -47,6 +86,25 @@
</header>
<div class="@(ViewData["FullWidth"] is true ? "container-fluid px-3" : "container")">
<main role="main" class="pb-3">
@if (ViewData["Breadcrumbs"] is List<(string Label, string? Url)> breadcrumbs && breadcrumbs.Count > 0)
{
<nav aria-label="breadcrumb" class="breadcrumb-nav">
<ol class="breadcrumb">
@for (var i = 0; i < breadcrumbs.Count; i++)
{
var crumb = breadcrumbs[i];
if (i == breadcrumbs.Count - 1)
{
<li class="breadcrumb-item active" aria-current="page">@crumb.Label</li>
}
else
{
<li class="breadcrumb-item"><a href="@crumb.Url" class="text-decoration-none">@crumb.Label</a></li>
}
}
</ol>
</nav>
}
@RenderBody()
</main>
</div>
@@ -58,7 +116,7 @@
</footer>
<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)

View File

@@ -3,6 +3,11 @@
@{
ViewData["Title"] = "Upload Transactions";
ViewData["FullWidth"] = Model.PreviewTransactions.Any();
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Transactions", Url.Page("/Transactions")),
("Upload", null)
};
}
<h2>Upload Transactions</h2>

View File

@@ -2,6 +2,12 @@
@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">

View File

@@ -19,4 +19,40 @@ html {
body {
margin-bottom: 60px;
}
/* Active dropdown parent highlighting */
.navbar .nav-item.dropdown .nav-link.dropdown-toggle.active-parent {
color: rgba(255, 255, 255, 0.9);
}
/* Breadcrumb styling */
.breadcrumb-nav {
margin-bottom: 1rem;
}
.breadcrumb-nav .breadcrumb {
background: transparent;
padding: 0;
margin-bottom: 0;
font-size: 0.875rem;
}
/* Quick-action cards on dashboard */
.quick-action-card {
transition: transform 0.15s ease, box-shadow 0.15s ease;
text-decoration: none;
color: inherit;
}
.quick-action-card:hover {
transform: translateY(-2px);
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.3) !important;
}
.quick-action-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}

View File

@@ -1,4 +1,19 @@
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.
// Highlight active dropdown parent when a child page is active
(function () {
var path = window.location.pathname.toLowerCase().replace(/\/+$/, '') || '/';
document.querySelectorAll('.navbar .dropdown-menu .dropdown-item').forEach(function (item) {
var href = (item.getAttribute('href') || '').toLowerCase().replace(/\/+$/, '');
if (href && (path === href || path.startsWith(href + '/'))) {
item.classList.add('active');
var toggle = item.closest('.dropdown').querySelector('.dropdown-toggle');
if (toggle) toggle.classList.add('active-parent');
}
});
// Direct nav-links (non-dropdown)
document.querySelectorAll('.navbar .nav-link:not(.dropdown-toggle)').forEach(function (link) {
var href = (link.getAttribute('href') || '').toLowerCase().replace(/\/+$/, '');
if (href && path === href) {
link.classList.add('active');
}
});
})();