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>
382 lines
18 KiB
Plaintext
382 lines
18 KiB
Plaintext
@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>
|
||
|