Pagination on Transactions
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user