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

View File

@@ -61,6 +61,9 @@
<div class="card-body"> <div class="card-body">
<div class="text-muted small">Transactions</div> <div class="text-muted small">Transactions</div>
<div class="fs-4 fw-bold">@Model.Stats.Count</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> </div>
</div> </div>
@@ -182,6 +185,90 @@ else
</div> </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 { @section Scripts {
<script> <script>
// Initialize Bootstrap tooltips for notes badges // Initialize Bootstrap tooltips for notes badges

View File

@@ -31,6 +31,13 @@ namespace MoneyMap.Pages
[BindProperty(SupportsGet = true)] [BindProperty(SupportsGet = true)]
public DateTime? EndDate { get; set; } 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<TransactionRow> Transactions { get; set; } = new();
public List<string> AvailableCategories { get; set; } = new(); public List<string> AvailableCategories { get; set; } = new();
public List<Card> AvailableCards { 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); 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 var transactions = await query
.OrderByDescending(t => t.Date) .OrderByDescending(t => t.Date)
.ThenByDescending(t => t.Id) .ThenByDescending(t => t.Id)
.Skip((PageNumber - 1) * PageSize)
.Take(PageSize)
.ToListAsync(); .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 Transactions = transactions.Select(t => new TransactionRow
{ {
Id = t.Id, Id = t.Id,
@@ -86,16 +113,17 @@ namespace MoneyMap.Pages
CardLabel = t.Card != null CardLabel = t.Card != null
? $"{t.Card.Issuer} {t.Card.Last4}" ? $"{t.Card.Issuer} {t.Card.Last4}"
: (string.IsNullOrEmpty(t.CardLast4) ? "" : $"•••• {t.CardLast4}"), : (string.IsNullOrEmpty(t.CardLast4) ? "" : $"•••• {t.CardLast4}"),
ReceiptCount = t.Receipts?.Count ?? 0 ReceiptCount = receiptCountDict.ContainsKey(t.Id) ? receiptCountDict[t.Id] : 0
}).ToList(); }).ToList();
// Calculate stats for filtered results // Calculate stats for filtered results (all pages, not just current)
var allFilteredTransactions = await query.ToListAsync();
Stats = new TransactionStats Stats = new TransactionStats
{ {
Count = transactions.Count, Count = allFilteredTransactions.Count,
TotalDebits = transactions.Where(t => t.Amount < 0).Sum(t => t.Amount), TotalDebits = allFilteredTransactions.Where(t => t.Amount < 0).Sum(t => t.Amount),
TotalCredits = transactions.Where(t => t.Amount > 0).Sum(t => t.Amount), TotalCredits = allFilteredTransactions.Where(t => t.Amount > 0).Sum(t => t.Amount),
NetAmount = transactions.Sum(t => t.Amount) NetAmount = allFilteredTransactions.Sum(t => t.Amount)
}; };
// Get available categories for filter dropdown // Get available categories for filter dropdown

View File

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

View File

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