Files
MoneyMap/MoneyMap/Pages/Index.cshtml
AJ Isaacs 59b8adc2d8 Improve: Restyle UI with modern fintech light theme
Replace the generic Bootstrap dark theme with a polished light theme
featuring an indigo primary color, refined cards with subtle shadows,
uppercase table headers, and updated chart palettes. Pure CSS restyle
with no layout or functionality changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:09:05 -05:00

382 lines
18 KiB
Plaintext
Raw Blame History

@page
@model MoneyMap.Pages.IndexModel
@{
ViewData["Title"] = "MoneyMap";
}
<div class="row g-3">
<div class="col-sm-6 col-lg-3">
<div class="card shadow-sm">
<div class="card-body">
<div class="text-muted">Transactions</div>
<div class="fs-3 fw-bold">@Model.Stats.TotalTransactions</div>
<div class="small text-muted">Credits: @Model.Stats.Credits <20> Debits: @Model.Stats.Debits</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card shadow-sm">
<div class="card-body">
<div class="text-muted">Uncategorized</div>
<div class="fs-3 fw-bold">@Model.Stats.Uncategorized</div>
<div class="small text-muted">
@if (Model.Stats.Uncategorized > 0)
{
<a asp-page="/Transactions" asp-route-category="(blank)" class="text-decoration-none">View uncategorized ?</a>
}
else
{
<span>All categorized!</span>
}
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card shadow-sm">
<div class="card-body">
<div class="text-muted">Receipts</div>
<div class="fs-3 fw-bold">@Model.Stats.Receipts</div>
<div class="small text-muted">Linked files</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card shadow-sm">
<div class="card-body">
<div class="text-muted">Cards</div>
<div class="fs-3 fw-bold">@Model.Stats.Cards</div>
<div class="small text-muted">Tracked accounts</div>
</div>
</div>
</div>
</div>
<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">
<div class="card shadow-sm h-100">
<div class="card-header">Spending by category (last 90 days)</div>
<div class="card-body">
<canvas id="categoryChart" height="220"></canvas>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header">Net cash flow (last 30 days)</div>
<div class="card-body">
<canvas id="trendChart" height="220"></canvas>
</div>
</div>
</div>
</div>
@if (Model.BudgetStatuses.Any())
{
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Budget Status</span>
<a asp-page="/Budgets" class="btn btn-sm btn-outline-secondary">Manage Budgets</a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm mb-0 table-hover">
<thead>
<tr>
<th>Budget</th>
<th>Period</th>
<th class="text-end">Limit</th>
<th class="text-end">Spent</th>
<th class="text-end">Remaining</th>
<th style="width: 180px;">Progress</th>
</tr>
</thead>
<tbody>
@foreach (var status in Model.BudgetStatuses)
{
<tr style="cursor: pointer;" onclick="window.location.href='@Url.Page("/Budgets")'">
<td>
<strong>@status.Budget.DisplayName</strong>
@if (status.Budget.IsTotalBudget)
{
<span class="badge bg-info ms-1">Total</span>
}
@if (status.IsOverBudget)
{
<span class="badge bg-danger ms-1">Over</span>
}
</td>
<td>
@status.Budget.Period
<br /><small class="text-muted">@status.PeriodDisplay</small>
</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: 18px;">
@{
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>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
@if (Model.TopCategories.Any())
{
<div class="card shadow-sm mb-3">
<div class="card-header">
Top expense categories (last 90 days)
<small class="text-muted"><3E> excludes transfers</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0 table-hover">
<thead>
<tr>
<th>Category</th>
<th class="text-end">Total Spend</th>
<th class="text-end">Avg Per Txn</th>
<th class="text-end">% of Total</th>
<th class="text-end">Txns</th>
</tr>
</thead>
<tbody>
@foreach (var c in Model.TopCategories)
{
<tr style="cursor: pointer;" onclick="window.location.href='@Url.Page("/Transactions", new { category = string.IsNullOrWhiteSpace(c.Category) ? "(blank)" : c.Category })'">
<td>
<a asp-page="/Transactions" asp-route-category="@(string.IsNullOrWhiteSpace(c.Category) ? "(blank)" : c.Category)" class="text-decoration-none text-body">
@(string.IsNullOrWhiteSpace(c.Category) ? "(uncategorized)" : c.Category)
</a>
</td>
<td class="text-end">@c.TotalSpend.ToString("C")</td>
<td class="text-end text-muted">@c.AveragePerTransaction.ToString("C")</td>
<td class="text-end">
<span class="badge bg-secondary">@c.PercentageOfTotal.ToString("F1")%</span>
</td>
<td class="text-end">@c.Count</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<div class="card shadow-sm">
<div class="card-header">Recent transactions</div>
<div class="card-body p-0">
<table class="table table-hover table-sm mb-0">
<thead>
<tr>
<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: 110px;">Card</th>
<th style="width: 90px;">Action</th>
</tr>
</thead>
<tbody>
@foreach (var t in Model.Recent)
{
<tr style="cursor: pointer;" title="View details" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
<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-body">@t.Name</a>
@if (t.ReceiptCount > 0)
{
<span class="badge bg-success" title="@t.ReceiptCount receipt(s) attached">
@t.ReceiptCount
</span>
}
</div>
</td>
<td class="text-truncate" style="max-width:320px">@t.Memo</td>
<td class="text-end">@t.Amount.ToString("C")</td>
<td>
@if (string.IsNullOrWhiteSpace(t.Category))
{
<a asp-page="/Transactions" asp-route-category="(blank)" class="text-decoration-none text-muted">(uncategorized)</a>
}
else
{
<a asp-page="/Transactions" asp-route-category="@t.Category" class="text-decoration-none text-body">@t.Category</a>
}
</td>
<td>@t.CardLabel</td>
<td>
<a asp-page="/EditTransaction" asp-route-id="@t.Id" class="btn btn-sm btn-outline-secondary">Open</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
(function(){
const topLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TopCategories.Select(c => string.IsNullOrWhiteSpace(c.Category) ? "(uncategorized)" : c.Category)));
const topValues = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TopCategories.Select(c => c.TotalSpend)));
const trendLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendLabels));
const trendBalance = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendRunningBalance));
const catCtx = document.getElementById('categoryChart');
if (catCtx && topLabels.length) {
new Chart(catCtx, {
type: 'doughnut',
data: {
labels: topLabels,
datasets: [{
data: topValues,
backgroundColor: ['#6366f1','#f59e0b','#ef4444','#10b981','#06b6d4','#8b5cf6','#f97316','#ec4899']
}]
},
options: {
plugins: { legend: { position: 'bottom', labels: { color: '#64748b', font: { size: 12 } } } },
maintainAspectRatio: false
}
});
}
const trendCtx = document.getElementById('trendChart');
if (trendCtx) {
new Chart(trendCtx, {
type: 'line',
data: {
labels: trendLabels,
datasets: [{
label: 'Net Cash Flow',
data: trendBalance,
borderColor: '#6366f1',
backgroundColor: 'rgba(99,102,241,0.10)',
fill: true,
tension: 0.3,
pointRadius: 0,
pointHoverRadius: 5
}]
},
options: {
scales: {
y: {
ticks: {
color: '#64748b',
callback: function(value) { return '$' + value.toLocaleString(); }
},
grid: { color: 'rgba(0,0,0,0.06)' }
},
x: {
ticks: { color: '#64748b', maxTicksLimit: 10 },
grid: { color: 'rgba(0,0,0,0.03)' }
}
},
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return '$' + context.parsed.y.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
}
}
},
maintainAspectRatio: false,
interaction: { intersect: false, mode: 'index' }
}
});
}
})();
</script>