From 1e060c10f5247f8abd7a628a13121b9b295d1b2e Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 12 Oct 2025 04:03:23 -0400 Subject: [PATCH] Add Merchants management page with modal edit/delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created a dedicated page for viewing and managing merchants: **Backend (Merchants.cshtml.cs):** - List all merchants with transaction and mapping counts - Add new merchant with duplicate name validation - Edit merchant name (updates all linked transactions/mappings) - Delete merchant (unlinks from transactions/mappings via SET NULL) - Full CRUD operations with proper validation **Frontend (Merchants.cshtml):** - Clean table view showing merchant name, transaction count, mapping count - Add modal for creating new merchants - Edit modal for renaming merchants (click row or Edit button) - Delete with confirmation showing impact (# of transactions/mappings) - Success/error message display - Responsive Bootstrap layout **Navigation:** - Added "Merchants" link to main navigation bar - Positioned between "Categories" and "Recategorize" **Features:** - Shows transaction count per merchant (useful for seeing merchant usage) - Shows mapping count per merchant (useful for seeing pattern coverage) - Inline edit with modal dialog (consistent with CategoryMappings UI) - Safe delete with SET NULL (transactions/mappings remain, just unlinked) - Duplicate name prevention on add/edit **Benefits:** - Easy merchant management without SQL queries - Visual feedback on merchant usage - Rename propagates to all transactions/mappings automatically - Consistent UI with rest of application 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- MoneyMap/Pages/Merchants.cshtml | 186 +++++++++++++++++++++++++++ MoneyMap/Pages/Merchants.cshtml.cs | 169 ++++++++++++++++++++++++ MoneyMap/Pages/Shared/_Layout.cshtml | 3 + 3 files changed, 358 insertions(+) create mode 100644 MoneyMap/Pages/Merchants.cshtml create mode 100644 MoneyMap/Pages/Merchants.cshtml.cs diff --git a/MoneyMap/Pages/Merchants.cshtml b/MoneyMap/Pages/Merchants.cshtml new file mode 100644 index 0000000..61b4eb5 --- /dev/null +++ b/MoneyMap/Pages/Merchants.cshtml @@ -0,0 +1,186 @@ +@page +@model MoneyMap.Pages.MerchantsModel +@{ + ViewData["Title"] = "Merchants"; +} + +
+

Merchants

+ Back to Dashboard +
+ +@if (!string.IsNullOrEmpty(Model.SuccessMessage)) +{ + +} + +@if (!string.IsNullOrEmpty(Model.ErrorMessage)) +{ + +} + +
+
+
+
+
Total Merchants
+
@Model.TotalMerchants
+
+
+
+
+ + +
+ +
+ +@if (Model.Merchants.Any()) +{ +
+
+ All Merchants + Click to edit, or delete merchants +
+
+
+ + + + + + + + + + + @foreach (var merchant in Model.Merchants) + { + + + + + + + } + +
Merchant NameTransactionsMappingsActions
+ @merchant.Name + + @merchant.TransactionCount + + @merchant.MappingCount + + +
+ +
+
+
+
+
+} +else +{ +
+
No merchants found
+

Click "Add New Merchant" to create your first merchant.

+
+} + + + + + + + +@section Scripts { + +} diff --git a/MoneyMap/Pages/Merchants.cshtml.cs b/MoneyMap/Pages/Merchants.cshtml.cs new file mode 100644 index 0000000..f2d144a --- /dev/null +++ b/MoneyMap/Pages/Merchants.cshtml.cs @@ -0,0 +1,169 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using MoneyMap.Data; +using MoneyMap.Models; + +namespace MoneyMap.Pages +{ + public class MerchantsModel : PageModel + { + private readonly MoneyMapContext _db; + + public MerchantsModel(MoneyMapContext db) + { + _db = db; + } + + public List Merchants { get; set; } = new(); + public int TotalMerchants { get; set; } + + [TempData] + public string? SuccessMessage { get; set; } + + [TempData] + public string? ErrorMessage { get; set; } + + public async Task OnGetAsync() + { + await LoadDataAsync(); + } + + public async Task OnPostAddMerchantAsync(AddMerchantModel model) + { + if (!ModelState.IsValid) + { + ErrorMessage = "Please fill in all required fields."; + await LoadDataAsync(); + return Page(); + } + + // Check if merchant already exists + var existing = await _db.Merchants + .FirstOrDefaultAsync(m => m.Name == model.Name.Trim()); + + if (existing != null) + { + ErrorMessage = $"Merchant '{model.Name}' already exists."; + await LoadDataAsync(); + return Page(); + } + + var merchant = new Merchant + { + Name = model.Name.Trim() + }; + + _db.Merchants.Add(merchant); + await _db.SaveChangesAsync(); + + SuccessMessage = $"Added merchant '{merchant.Name}'."; + return RedirectToPage(); + } + + public async Task OnPostUpdateMerchantAsync(UpdateMerchantModel model) + { + if (!ModelState.IsValid) + { + ErrorMessage = "Please fill in all required fields."; + await LoadDataAsync(); + return Page(); + } + + var merchant = await _db.Merchants.FindAsync(model.Id); + if (merchant == null) + { + ErrorMessage = "Merchant not found."; + return RedirectToPage(); + } + + // Check if another merchant with the same name exists + var existing = await _db.Merchants + .FirstOrDefaultAsync(m => m.Name == model.Name.Trim() && m.Id != model.Id); + + if (existing != null) + { + ErrorMessage = $"Merchant '{model.Name}' already exists."; + await LoadDataAsync(); + return Page(); + } + + merchant.Name = model.Name.Trim(); + await _db.SaveChangesAsync(); + + SuccessMessage = "Merchant updated successfully."; + return RedirectToPage(); + } + + public async Task OnPostDeleteMerchantAsync(int id) + { + var merchant = await _db.Merchants + .Include(m => m.Transactions) + .Include(m => m.CategoryMappings) + .FirstOrDefaultAsync(m => m.Id == id); + + if (merchant == null) + { + ErrorMessage = "Merchant not found."; + return RedirectToPage(); + } + + var transactionCount = merchant.Transactions.Count; + var mappingCount = merchant.CategoryMappings.Count; + + _db.Merchants.Remove(merchant); + await _db.SaveChangesAsync(); + + SuccessMessage = $"Deleted merchant '{merchant.Name}'. {transactionCount} transactions and {mappingCount} category mappings are now unlinked."; + return RedirectToPage(); + } + + private async Task LoadDataAsync() + { + var merchants = await _db.Merchants + .Include(m => m.Transactions) + .Include(m => m.CategoryMappings) + .OrderBy(m => m.Name) + .ToListAsync(); + + Merchants = merchants.Select(m => new MerchantRow + { + Id = m.Id, + Name = m.Name, + TransactionCount = m.Transactions.Count, + MappingCount = m.CategoryMappings.Count + }).ToList(); + + TotalMerchants = Merchants.Count; + } + + public class MerchantRow + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public int TransactionCount { get; set; } + public int MappingCount { get; set; } + } + + public class AddMerchantModel + { + [Required(ErrorMessage = "Merchant name is required")] + [StringLength(100)] + public string Name { get; set; } = ""; + } + + public class UpdateMerchantModel + { + [Required] + public int Id { get; set; } + + [Required(ErrorMessage = "Merchant name is required")] + [StringLength(100)] + public string Name { get; set; } = ""; + } + } +} diff --git a/MoneyMap/Pages/Shared/_Layout.cshtml b/MoneyMap/Pages/Shared/_Layout.cshtml index e5a6773..c4039fd 100644 --- a/MoneyMap/Pages/Shared/_Layout.cshtml +++ b/MoneyMap/Pages/Shared/_Layout.cshtml @@ -34,6 +34,9 @@ +