Files
MoneyMap/MoneyMap/Pages/Index.cshtml

226 lines
9.3 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 · 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="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>
</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">Cash flow (last 30 days)</div>
<div class="card-body">
<canvas id="trendChart" height="220"></canvas>
</div>
</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">· 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-dark">
@(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-dark">@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-dark">@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 trendDebits = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendDebitsAbs));
const trendCredits = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendCredits));
const trendNet = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendNet));
const catCtx = document.getElementById('categoryChart');
if (catCtx && topLabels.length) {
new Chart(catCtx, {
type: 'doughnut',
data: {
labels: topLabels,
datasets: [{
data: topValues,
backgroundColor: ['#4e79a7','#f28e2c','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7']
}]
},
options: {
plugins: { legend: { position: 'bottom' } },
maintainAspectRatio: false
}
});
}
const trendCtx = document.getElementById('trendChart');
if (trendCtx) {
new Chart(trendCtx, {
data: {
labels: trendLabels,
datasets: [
{ type: 'bar', label: 'Debits', data: trendDebits, backgroundColor: 'rgba(225,87,89,0.6)' },
{ type: 'bar', label: 'Credits', data: trendCredits, backgroundColor: 'rgba(76,175,80,0.6)' },
{ type: 'line', label: 'Net', data: trendNet, borderColor: '#4e79a7', backgroundColor: 'transparent', tension: 0.2, yAxisID: 'y' }
]
},
options: {
scales: {
y: { beginAtZero: true }
},
maintainAspectRatio: false
}
});
}
})();
</script>