Pagination on Transactions

This commit is contained in:
user1
2025-10-04 03:32:25 -04:00
parent d04db5af12
commit f01b44e9ac
5 changed files with 151 additions and 14 deletions

View File

@@ -6,6 +6,13 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Utility\**" />
<Content Remove="Utility\**" />
<EmbeddedResource Remove="Utility\**" />
<None Remove="Utility\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.8.2" />
@@ -20,7 +27,6 @@
<ItemGroup>
<Folder Include="Migrations\" />
<Folder Include="Utility\" />
<Folder Include="wwwroot\receipts\" />
</ItemGroup>

View File

@@ -61,6 +61,9 @@
<div class="card-body">
<div class="text-muted small">Transactions</div>
<div class="fs-4 fw-bold">@Model.Stats.Count</div>
<div class="small text-muted">
Showing @Model.Transactions.Count on page @Model.PageNumber of @Model.TotalPages
</div>
</div>
</div>
</div>
@@ -182,6 +185,90 @@ else
</div>
}
<!-- Pagination -->
@if (Model.TotalPages > 1)
{
<nav aria-label="Transaction pagination" class="mt-3">
<ul class="pagination justify-content-center">
<!-- Previous -->
<li class="page-item @(Model.PageNumber == 1 ? "disabled" : "")">
<a class="page-link"
asp-page="/Transactions"
asp-route-pageNumber="@(Model.PageNumber - 1)"
asp-route-category="@Model.Category"
asp-route-cardId="@Model.CardId"
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
Previous
</a>
</li>
<!-- First page -->
@if (Model.PageNumber > 3)
{
<li class="page-item">
<a class="page-link"
asp-page="/Transactions"
asp-route-pageNumber="1"
asp-route-category="@Model.Category"
asp-route-cardId="@Model.CardId"
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
1
</a>
</li>
<li class="page-item disabled"><span class="page-link">...</span></li>
}
<!-- Page numbers -->
@for (int i = Math.Max(1, Model.PageNumber - 2); i <= Math.Min(Model.TotalPages, Model.PageNumber + 2); i++)
{
<li class="page-item @(i == Model.PageNumber ? "active" : "")">
<a class="page-link"
asp-page="/Transactions"
asp-route-pageNumber="@i"
asp-route-category="@Model.Category"
asp-route-cardId="@Model.CardId"
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
@i
</a>
</li>
}
<!-- Last page -->
@if (Model.PageNumber < Model.TotalPages - 2)
{
<li class="page-item disabled"><span class="page-link">...</span></li>
<li class="page-item">
<a class="page-link"
asp-page="/Transactions"
asp-route-pageNumber="@Model.TotalPages"
asp-route-category="@Model.Category"
asp-route-cardId="@Model.CardId"
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
@Model.TotalPages
</a>
</li>
}
<!-- Next -->
<li class="page-item @(Model.PageNumber >= Model.TotalPages ? "disabled" : "")">
<a class="page-link"
asp-page="/Transactions"
asp-route-pageNumber="@(Model.PageNumber + 1)"
asp-route-category="@Model.Category"
asp-route-cardId="@Model.CardId"
asp-route-startDate="@Model.StartDate?.ToString("yyyy-MM-dd")"
asp-route-endDate="@Model.EndDate?.ToString("yyyy-MM-dd")">
Next
</a>
</li>
</ul>
</nav>
}
@section Scripts {
<script>
// Initialize Bootstrap tooltips for notes badges

View File

@@ -31,6 +31,13 @@ namespace MoneyMap.Pages
[BindProperty(SupportsGet = true)]
public DateTime? EndDate { get; set; }
[BindProperty(SupportsGet = true)]
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 50;
public int TotalPages { get; set; }
public int TotalCount { get; set; }
public List<TransactionRow> Transactions { get; set; } = new();
public List<string> AvailableCategories { get; set; } = new();
public List<Card> AvailableCards { get; set; } = new();
@@ -68,12 +75,32 @@ namespace MoneyMap.Pages
query = query.Where(t => t.Date <= EndDate.Value);
}
// Get transactions
// Get total count for pagination
TotalCount = await query.CountAsync();
TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
// Ensure page number is valid
if (PageNumber < 1) PageNumber = 1;
if (PageNumber > TotalPages && TotalPages > 0) PageNumber = TotalPages;
// Get paginated transactions
var transactions = await query
.OrderByDescending(t => t.Date)
.ThenByDescending(t => t.Id)
.Skip((PageNumber - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
// Get receipt counts for the current page only
var transactionIds = transactions.Select(t => t.Id).ToList();
var receiptCounts = await _db.Receipts
.Where(r => transactionIds.Contains(r.TransactionId))
.GroupBy(r => r.TransactionId)
.Select(g => new { TransactionId = g.Key, Count = g.Count() })
.ToListAsync();
var receiptCountDict = receiptCounts.ToDictionary(x => x.TransactionId, x => x.Count);
Transactions = transactions.Select(t => new TransactionRow
{
Id = t.Id,
@@ -86,16 +113,17 @@ namespace MoneyMap.Pages
CardLabel = t.Card != null
? $"{t.Card.Issuer} {t.Card.Last4}"
: (string.IsNullOrEmpty(t.CardLast4) ? "" : $"•••• {t.CardLast4}"),
ReceiptCount = t.Receipts?.Count ?? 0
ReceiptCount = receiptCountDict.ContainsKey(t.Id) ? receiptCountDict[t.Id] : 0
}).ToList();
// Calculate stats for filtered results
// Calculate stats for filtered results (all pages, not just current)
var allFilteredTransactions = await query.ToListAsync();
Stats = new TransactionStats
{
Count = transactions.Count,
TotalDebits = transactions.Where(t => t.Amount < 0).Sum(t => t.Amount),
TotalCredits = transactions.Where(t => t.Amount > 0).Sum(t => t.Amount),
NetAmount = transactions.Sum(t => t.Amount)
Count = allFilteredTransactions.Count,
TotalDebits = allFilteredTransactions.Where(t => t.Amount < 0).Sum(t => t.Amount),
TotalCredits = allFilteredTransactions.Where(t => t.Amount > 0).Sum(t => t.Amount),
NetAmount = allFilteredTransactions.Sum(t => t.Amount)
};
// Get available categories for filter dropdown

View File

@@ -23,13 +23,22 @@ namespace MoneyMap.Services
{
private readonly MoneyMapContext _db;
private readonly IWebHostEnvironment _environment;
private readonly IConfiguration _configuration;
private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
private static readonly string[] AllowedExtensions = { ".jpg", ".jpeg", ".png", ".pdf", ".gif", ".heic" };
public ReceiptManager(MoneyMapContext db, IWebHostEnvironment environment)
public ReceiptManager(MoneyMapContext db, IWebHostEnvironment environment, IConfiguration configuration)
{
_db = db;
_environment = environment;
_configuration = configuration;
}
private string GetReceiptsBasePath()
{
// Get from config, default to "receipts" in wwwroot
var relativePath = _configuration["Receipts:StoragePath"] ?? "receipts";
return Path.Combine(_environment.WebRootPath, relativePath);
}
public async Task<ReceiptUploadResult> UploadReceiptAsync(long transactionId, IFormFile file)
@@ -51,9 +60,9 @@ namespace MoneyMap.Services
return ReceiptUploadResult.Failure("Transaction not found.");
// Create receipts directory if it doesn't exist
var receiptsPath = Path.Combine(_environment.WebRootPath, "receipts");
if (!Directory.Exists(receiptsPath))
Directory.CreateDirectory(receiptsPath);
var receiptsBasePath = GetReceiptsBasePath();
if (!Directory.Exists(receiptsBasePath))
Directory.CreateDirectory(receiptsBasePath);
// Calculate SHA256 hash
string fileHash;
@@ -73,7 +82,7 @@ namespace MoneyMap.Services
// Generate unique filename
var storedFileName = $"{transactionId}_{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(receiptsPath, storedFileName);
var filePath = Path.Combine(receiptsBasePath, storedFileName);
// Save file
using (var fileStream = new FileStream(filePath, FileMode.Create))
@@ -81,12 +90,16 @@ namespace MoneyMap.Services
await file.CopyToAsync(fileStream);
}
// Store relative path in database
var relativePath = _configuration["Receipts:StoragePath"] ?? "receipts";
var relativeStoragePath = $"{relativePath}/{storedFileName}";
// Create receipt record
var receipt = new Receipt
{
TransactionId = transactionId,
FileName = file.FileName,
StoragePath = $"receipts/{storedFileName}",
StoragePath = relativeStoragePath,
FileSizeBytes = file.Length,
ContentType = file.ContentType,
FileHashSha256 = fileHash,

View File

@@ -2,6 +2,9 @@
"ConnectionStrings": {
"MoneyMapDb": "Server=barge.lan;Database=MoneyMap;User Id=moneymap;Password=Cn87oXQPj7EEkx;TrustServerCertificate=True;"
},
"Receipts": {
"StoragePath": "\\\\TRUENAS\\aj\\Documents\\Receipts"
},
"Logging": {
"LogLevel": {
"Default": "Information",