Add card management pages and enhance category mappings UI.

Features added:
- Cards page with list/create/edit/delete functionality
- EditCard page for modifying card details
- Enhanced CategoryMappings UI with inline add/edit/delete modals
- Category dropdown autocomplete in mapping forms
- Priority badges and visual indicators for high-priority mappings
- Click-to-edit functionality on patterns

Improvements:
- TransactionCategorizer: Add TARGET.COM to online shopping patterns
- TransactionCategorizer: Set Credit Card Payment to priority 200 to catch before Banking
- TransactionCategorizer: Add database migration comments
- Layout: Add Cards link to navigation
- Layout: Fix Bootstrap JS path
- ValidationScriptsPartial: Use Url.Content for script paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AJ
2025-10-09 16:49:18 -04:00
parent 7ac0dd9dbc
commit ff14aed65f
12 changed files with 806 additions and 30 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(git add:*)",
"Bash(git commit:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -0,0 +1,93 @@
@page
@model MoneyMap.Pages.CardsModel
@{
ViewData["Title"] = "Manage Cards";
}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>Manage Cards</h2>
<div>
<a asp-page="/EditCard" class="btn btn-primary">Add New Card</a>
<a asp-page="/Index" class="btn btn-outline-secondary">Back to Dashboard</a>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@Model.SuccessMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@Model.ErrorMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
@if (Model.Cards.Any())
{
<div class="card shadow-sm">
<div class="card-header">
<strong>Your Cards (@Model.Cards.Count)</strong>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Owner</th>
<th>Issuer</th>
<th>Last 4</th>
<th class="text-end">Transactions</th>
<th style="width: 150px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Cards)
{
<tr>
<td>@item.Card.Owner</td>
<td>@item.Card.Issuer</td>
<td>•••• @item.Card.Last4</td>
<td class="text-end">@item.TransactionCount</td>
<td>
<div class="d-flex gap-1">
<a asp-page="/EditCard" asp-route-id="@item.Card.Id" class="btn btn-sm btn-outline-primary">
Edit
</a>
@if (item.TransactionCount == 0)
{
<form method="post" asp-page-handler="Delete" asp-route-id="@item.Card.Id"
onsubmit="return confirm('Delete this card?')" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
</form>
}
else
{
<button type="button" class="btn btn-sm btn-outline-secondary" disabled
title="Cannot delete - card has transactions">
Delete
</button>
}
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
else
{
<div class="alert alert-info">
<h5>No cards found</h5>
<p>Add your first card to start tracking transactions.</p>
<a asp-page="/EditCard" class="btn btn-primary">Add New Card</a>
</div>
}

View File

@@ -0,0 +1,81 @@
using System.Collections.Generic;
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 CardsModel : PageModel
{
private readonly MoneyMapContext _db;
public CardsModel(MoneyMapContext db)
{
_db = db;
}
public List<CardWithStats> Cards { get; set; } = new();
[TempData]
public string? SuccessMessage { get; set; }
[TempData]
public string? ErrorMessage { get; set; }
public async Task OnGetAsync()
{
var cards = await _db.Cards
.OrderBy(c => c.Owner)
.ThenBy(c => c.Last4)
.ToListAsync();
var cardStats = new List<CardWithStats>();
foreach (var card in cards)
{
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
cardStats.Add(new CardWithStats
{
Card = card,
TransactionCount = transactionCount
});
}
Cards = cardStats;
}
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var card = await _db.Cards.FindAsync(id);
if (card == null)
{
ErrorMessage = "Card not found.";
return RedirectToPage();
}
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
if (transactionCount > 0)
{
ErrorMessage = $"Cannot delete card. It has {transactionCount} transaction(s) associated with it.";
return RedirectToPage();
}
_db.Cards.Remove(card);
await _db.SaveChangesAsync();
SuccessMessage = "Card deleted successfully.";
return RedirectToPage();
}
public class CardWithStats
{
public Card Card { get; set; } = null!;
public int TransactionCount { get; set; }
}
}
}

View File

@@ -19,6 +19,14 @@
</div>
}
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@Model.ErrorMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
<div class="row mb-3">
<div class="col-md-4">
<div class="card shadow-sm">
@@ -57,6 +65,13 @@
</div>
</div>
<!-- Add New Mapping Button -->
<div class="mb-3">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
+ Add New Pattern
</button>
</div>
@if (Model.CategoryGroups.Any())
{
<div class="card shadow-sm">
@@ -75,13 +90,22 @@
<span class="badge bg-secondary">@group.Count</span>
</h6>
<div class="small">
@foreach (var pattern in group.Patterns.Take(5))
@foreach (var mapping in group.Mappings)
{
<div class="text-muted">• @pattern</div>
}
@if (group.Patterns.Count > 5)
{
<div class="text-muted fst-italic">+ @(group.Patterns.Count - 5) more...</div>
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-muted" style="cursor: pointer;"
onclick="openEditModal(@mapping.Id, '@Html.Raw(mapping.Category.Replace("'", "\\'"))', '@Html.Raw(mapping.Pattern.Replace("'", "\\'"))', @mapping.Priority)">
@if (mapping.Priority > 0)
{
<span class="badge bg-info me-1" title="Priority @mapping.Priority">P@(mapping.Priority)</span>
}
@mapping.Pattern
</span>
<form method="post" asp-page-handler="DeleteMapping" asp-route-id="@mapping.Id"
onsubmit="return confirm('Delete pattern \'@mapping.Pattern\'?')" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger py-0">×</button>
</form>
</div>
}
</div>
</div>
@@ -107,5 +131,233 @@ else
<li>Categories in the CSV file (if present) take precedence over auto-categorization</li>
<li>Transactions with no match will have an empty category and can be categorized manually</li>
<li><strong>Special rule:</strong> Gas stations with purchases under $20 are categorized as "Convenience Store"</li>
<li><strong>Priority:</strong> Higher priority patterns are checked first (useful for Banking, Income, etc.)</li>
</ul>
</div>
<!-- Add Modal -->
<div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" asp-page-handler="AddMapping">
<div class="modal-header">
<h5 class="modal-title" id="addModalLabel">Add New Pattern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label asp-for="NewMapping.Category" class="form-label">Category</label>
<div class="position-relative">
<input asp-for="NewMapping.Category" id="addCategory" class="form-control" placeholder="e.g., Online shopping" autocomplete="off" />
<div id="addCategoryDropdown" class="dropdown-menu" style="width: 100%; max-height: 200px; overflow-y: auto;">
@foreach (var group in Model.CategoryGroups)
{
<a class="dropdown-item" href="#" data-category="@group.Category">@group.Category</a>
}
</div>
</div>
<span asp-validation-for="NewMapping.Category" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="NewMapping.Pattern" class="form-label">Pattern</label>
<input asp-for="NewMapping.Pattern" id="addPattern" class="form-control" placeholder="e.g., TARGET.COM" />
<span asp-validation-for="NewMapping.Pattern" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="NewMapping.Priority" class="form-label">Priority</label>
<input asp-for="NewMapping.Priority" type="number" id="addPriority" class="form-control" value="0" />
<div class="form-text">Higher priority = checked first (0 = normal, 100 = high, 200 = critical)</div>
<span asp-validation-for="NewMapping.Priority" class="text-danger"></span>
</div>
<div class="form-text">
Pattern will match anywhere in the merchant name (case-insensitive).
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Pattern</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" asp-page-handler="UpdateMapping">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit Pattern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" asp-for="EditMapping.Id" id="editId" />
<div class="mb-3">
<label asp-for="EditMapping.Category" class="form-label">Category</label>
<div class="position-relative">
<input asp-for="EditMapping.Category" id="editCategory" class="form-control" autocomplete="off" />
<div id="editCategoryDropdown" class="dropdown-menu" style="width: 100%; max-height: 200px; overflow-y: auto;">
@foreach (var group in Model.CategoryGroups)
{
<a class="dropdown-item" href="#" data-category="@group.Category">@group.Category</a>
}
</div>
</div>
<span asp-validation-for="EditMapping.Category" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="EditMapping.Pattern" class="form-label">Pattern</label>
<input asp-for="EditMapping.Pattern" id="editPattern" class="form-control" />
</div>
<div class="mb-3">
<label asp-for="EditMapping.Priority" class="form-label">Priority</label>
<input asp-for="EditMapping.Priority" type="number" id="editPriority" class="form-control" />
<div class="form-text">Higher priority = checked first (0 = normal, 100 = high, 200 = critical)</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
function openEditModal(id, category, pattern, priority) {
console.log('Opening modal for:', id, category, pattern, priority);
document.getElementById('editId').value = id;
document.getElementById('editCategory').value = category;
document.getElementById('editPattern').value = pattern;
document.getElementById('editPriority').value = priority;
var modalElement = document.getElementById('editModal');
if (modalElement && typeof bootstrap !== 'undefined') {
var modal = new bootstrap.Modal(modalElement);
modal.show();
} else {
console.error('Bootstrap modal not available');
alert('Error: Modal system not loaded. Please refresh the page.');
}
}
// Category dropdown functionality
function setupCategoryDropdown(inputId, dropdownId) {
const input = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
if (!input || !dropdown) return;
const dropdownItems = dropdown.querySelectorAll('.dropdown-item');
// Show dropdown on focus/click
input.addEventListener('focus', function() {
filterDropdown('');
dropdown.classList.add('show');
});
input.addEventListener('click', function() {
filterDropdown('');
dropdown.classList.add('show');
});
// Filter dropdown as user types
input.addEventListener('input', function() {
filterDropdown(this.value);
dropdown.classList.add('show');
});
// Select category from dropdown
dropdownItems.forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const category = this.getAttribute('data-category');
input.value = category;
dropdown.classList.remove('show');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!input.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.classList.remove('show');
}
});
// Filter dropdown items based on search text
function filterDropdown(searchText) {
const search = searchText.toLowerCase();
dropdownItems.forEach(item => {
const category = item.getAttribute('data-category').toLowerCase();
if (category.includes(search)) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
setupCategoryDropdown('addCategory', 'addCategoryDropdown');
setupCategoryDropdown('editCategory', 'editCategoryDropdown');
// Debug: Log form values before submission
var addForm = document.querySelector('#addModal form');
if (addForm) {
addForm.addEventListener('submit', function(e) {
var categoryInput = document.getElementById('addCategory');
var patternInput = document.getElementById('addPattern');
var priorityInput = document.getElementById('addPriority');
console.log('=== ADD FORM SUBMISSION ===');
console.log('Category:', categoryInput.value, 'Name:', categoryInput.name);
console.log('Pattern:', patternInput.value, 'Name:', patternInput.name);
console.log('Priority:', priorityInput.value, 'Name:', priorityInput.name);
console.log('Form Data:');
var formData = new FormData(addForm);
for (var pair of formData.entries()) {
console.log(' ' + pair[0] + ': ' + pair[1]);
}
});
}
var editForm = document.querySelector('#editModal form');
if (editForm) {
editForm.addEventListener('submit', function(e) {
var id = document.getElementById('editId').value;
var category = document.getElementById('editCategory').value;
var pattern = document.getElementById('editPattern').value;
var priority = document.getElementById('editPriority').value;
console.log('Submitting Edit form:', { id, category, pattern, priority });
console.log('Category input name:', document.getElementById('editCategory').name);
});
}
// Reopen modals if there are validation errors
var addCategoryInput = document.getElementById('addCategory');
var editCategoryInput = document.getElementById('editCategory');
if (addCategoryInput && addCategoryInput.classList.contains('input-validation-error')) {
var addModal = new bootstrap.Modal(document.getElementById('addModal'));
addModal.show();
}
if (editCategoryInput && editCategoryInput.classList.contains('input-validation-error')) {
var editModal = new bootstrap.Modal(document.getElementById('editModal'));
editModal.show();
}
});
</script>
}

View File

@@ -1,18 +1,23 @@
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.Services;
namespace MoneyMap.Pages
{
public class CategoryMappingsModel : PageModel
{
private readonly MoneyMapContext _db;
private readonly ITransactionCategorizer _categorizer;
public CategoryMappingsModel(ITransactionCategorizer categorizer)
public CategoryMappingsModel(MoneyMapContext db, ITransactionCategorizer categorizer)
{
_db = db;
_categorizer = categorizer;
}
@@ -20,26 +25,21 @@ namespace MoneyMap.Pages
public int TotalMappings { get; set; }
public int TotalCategories { get; set; }
[BindProperty]
public MappingEditModel NewMapping { get; set; } = new();
[BindProperty]
public MappingEditModel EditMapping { get; set; } = new();
[TempData]
public string? SuccessMessage { get; set; }
[TempData]
public string? ErrorMessage { get; set; }
public async Task OnGetAsync()
{
var mappings = await _categorizer.GetAllMappingsAsync();
CategoryGroups = mappings
.GroupBy(m => m.Category)
.Select(g => new CategoryGroup
{
Category = g.Key,
Patterns = g.Select(m => m.Pattern).ToList(),
Count = g.Count()
})
.OrderBy(g => g.Category)
.ToList();
TotalMappings = mappings.Count;
TotalCategories = CategoryGroups.Count;
await LoadDataAsync();
}
public async Task<IActionResult> OnPostSeedDefaultsAsync()
@@ -49,11 +49,120 @@ namespace MoneyMap.Pages
return RedirectToPage();
}
public async Task<IActionResult> OnPostAddMappingAsync()
{
// Remove validation errors for EditMapping since we're not using it in this handler
ModelState.Remove("EditMapping.Category");
ModelState.Remove("EditMapping.Pattern");
ModelState.Remove("EditMapping.Priority");
if (!ModelState.IsValid)
{
ErrorMessage = "Please fill in all required fields.";
await LoadDataAsync();
return Page();
}
var mapping = new CategoryMapping
{
Category = NewMapping.Category.Trim(),
Pattern = NewMapping.Pattern.Trim(),
Priority = NewMapping.Priority
};
_db.CategoryMappings.Add(mapping);
await _db.SaveChangesAsync();
SuccessMessage = $"Added pattern '{mapping.Pattern}' to category '{mapping.Category}'.";
return RedirectToPage();
}
public async Task<IActionResult> OnPostUpdateMappingAsync()
{
// Remove validation errors for NewMapping since we're not using it in this handler
ModelState.Remove("NewMapping.Category");
ModelState.Remove("NewMapping.Pattern");
ModelState.Remove("NewMapping.Priority");
if (!ModelState.IsValid)
{
ErrorMessage = "Please fill in all required fields.";
await LoadDataAsync();
return Page();
}
var mapping = await _db.CategoryMappings.FindAsync(EditMapping.Id);
if (mapping == null)
{
ErrorMessage = "Mapping not found.";
return RedirectToPage();
}
mapping.Category = EditMapping.Category.Trim();
mapping.Pattern = EditMapping.Pattern.Trim();
mapping.Priority = EditMapping.Priority;
await _db.SaveChangesAsync();
SuccessMessage = "Mapping updated successfully.";
return RedirectToPage();
}
public async Task<IActionResult> OnPostDeleteMappingAsync(int id)
{
var mapping = await _db.CategoryMappings.FindAsync(id);
if (mapping == null)
{
ErrorMessage = "Mapping not found.";
return RedirectToPage();
}
_db.CategoryMappings.Remove(mapping);
await _db.SaveChangesAsync();
SuccessMessage = "Mapping deleted successfully.";
return RedirectToPage();
}
private async Task LoadDataAsync()
{
var mappings = await _categorizer.GetAllMappingsAsync();
CategoryGroups = mappings
.GroupBy(m => m.Category)
.Select(g => new CategoryGroup
{
Category = g.Key,
Mappings = g.OrderByDescending(m => m.Priority).ToList(),
Count = g.Count()
})
.OrderBy(g => g.Category)
.ToList();
TotalMappings = mappings.Count;
TotalCategories = CategoryGroups.Count;
}
public class CategoryGroup
{
public required string Category { get; set; }
public required List<string> Patterns { get; set; }
public required List<CategoryMapping> Mappings { get; set; }
public int Count { get; set; }
}
public class MappingEditModel
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Category { get; set; } = "";
[Required]
[StringLength(200)]
public string Pattern { get; set; } = "";
public int Priority { get; set; } = 0;
}
}
}

View File

@@ -0,0 +1,81 @@
@page
@model MoneyMap.Pages.EditCardModel
@{
ViewData["Title"] = Model.IsNewCard ? "Add Card" : "Edit Card";
}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>@(Model.IsNewCard ? "Add Card" : "Edit Card")</h2>
<a asp-page="/Cards" class="btn btn-outline-secondary">Back to Cards</a>
</div>
<div class="row">
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header">
<strong>Card Details</strong>
</div>
<div class="card-body">
<form method="post">
<input type="hidden" asp-for="Card.Id" />
<div class="mb-3">
<label asp-for="Card.Owner" class="form-label">Owner</label>
<input asp-for="Card.Owner" class="form-control" placeholder="e.g., John Smith" />
<span asp-validation-for="Card.Owner" class="text-danger"></span>
<div class="form-text">Who owns this card?</div>
</div>
<div class="mb-3">
<label asp-for="Card.Issuer" class="form-label">Issuer</label>
<input asp-for="Card.Issuer" class="form-control" placeholder="e.g., Chase, Bank of America" />
<span asp-validation-for="Card.Issuer" class="text-danger"></span>
<div class="form-text">Which bank or institution issued the card?</div>
</div>
<div class="mb-3">
<label asp-for="Card.Last4" class="form-label">Last 4 Digits</label>
<input asp-for="Card.Last4" class="form-control" placeholder="1234" maxlength="4" />
<span asp-validation-for="Card.Last4" class="text-danger"></span>
<div class="form-text">The last 4 digits of the card number</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
@(Model.IsNewCard ? "Add Card" : "Save Changes")
</button>
<a asp-page="/Cards" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header">
<strong>Tips</strong>
</div>
<div class="card-body">
<ul class="mb-0">
<li><strong>Owner:</strong> Use the cardholder's name for easy identification</li>
<li><strong>Issuer:</strong> The bank or credit card company (e.g., Chase, Discover, Capital One)</li>
<li><strong>Last 4:</strong> These digits help match transactions to the correct card</li>
<li>Cards with transactions cannot be deleted, only edited</li>
<li>Auto-imported cards will have "Unknown" as the owner - update them here</li>
</ul>
</div>
</div>
@if (!Model.IsNewCard)
{
<div class="alert alert-info mt-3">
<strong>Note:</strong> Updating card details will not affect existing transactions, but will change how the card is displayed going forward.
</div>
}
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@@ -0,0 +1,111 @@
using System.ComponentModel.DataAnnotations;
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 EditCardModel : PageModel
{
private readonly MoneyMapContext _db;
public EditCardModel(MoneyMapContext db)
{
_db = db;
}
[BindProperty]
public CardEditModel Card { get; set; } = new();
public bool IsNewCard { get; set; }
[TempData]
public string? SuccessMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id.HasValue)
{
var card = await _db.Cards.FindAsync(id.Value);
if (card == null)
return NotFound();
Card = new CardEditModel
{
Id = card.Id,
Owner = card.Owner,
Issuer = card.Issuer,
Last4 = card.Last4
};
IsNewCard = false;
}
else
{
IsNewCard = true;
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
IsNewCard = Card.Id == 0;
return Page();
}
if (Card.Id == 0)
{
// Create new card
var card = new Card
{
Owner = Card.Owner.Trim(),
Issuer = Card.Issuer.Trim(),
Last4 = Card.Last4.Trim()
};
_db.Cards.Add(card);
SuccessMessage = "Card added successfully!";
}
else
{
// Update existing card
var card = await _db.Cards.FindAsync(Card.Id);
if (card == null)
return NotFound();
card.Owner = Card.Owner.Trim();
card.Issuer = Card.Issuer.Trim();
card.Last4 = Card.Last4.Trim();
SuccessMessage = "Card updated successfully!";
}
await _db.SaveChangesAsync();
return RedirectToPage("/Cards");
}
public class CardEditModel
{
public int Id { get; set; }
[Required(ErrorMessage = "Owner is required")]
[StringLength(100)]
public string Owner { get; set; } = "";
[Required(ErrorMessage = "Issuer is required")]
[StringLength(100)]
public string Issuer { get; set; } = "";
[Required(ErrorMessage = "Last 4 digits are required")]
[StringLength(4, MinimumLength = 4, ErrorMessage = "Last 4 must be exactly 4 digits")]
[RegularExpression(@"^\d{4}$", ErrorMessage = "Last 4 must be 4 digits")]
public string Last4 { get; set; } = "";
}
}
}

View File

@@ -0,0 +1,8 @@
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MoneyMap.Pages
{
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
public PrivacyModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
}

View File

@@ -25,6 +25,9 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-page="/Transactions">Transactions</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-page="/Cards">Cards</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-page="/Upload">Upload CSV</a>
</li>
@@ -52,7 +55,7 @@
</footer>
<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/bootstrap/bootstrap.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)

View File

@@ -1,2 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script src="@Url.Content("~/lib/jquery-validation/jquery.validate.min.js")"></script>
<script src="@Url.Content("~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js")"></script>

View File

@@ -101,7 +101,7 @@ namespace MoneyMap.Services
"AMAZON MKTPL", "AMAZON.COM", "BATHANDBODYWORKS", "BATH AND BODY",
"SEPHORA.COM", "ULTA.COM", "WWW.KOHLS.COM", "GAPOUTLET.COM",
"NIKE.COM", "HOMEDEPOT.COM", "TEMU.COM", "APPLE.COM",
"JOURNEYS.COM", "DECKERS*UGG", "YUNNANSOURCINGUS");
"JOURNEYS.COM", "DECKERS*UGG", "YUNNANSOURCINGUS", "TARGET.COM");
// Walmart
AddMappings("Walmart Online", mappings, "WALMART.COM");
@@ -181,8 +181,8 @@ namespace MoneyMap.Services
AddMappings("Insurance", mappings,
"BOSTON MUTUAL", "IND FARMERS INS", "GERBER LIFE INS");
// Credit Card Payment
AddMappings("Credit Card Payment", mappings,
// Credit Card Payment (high priority to catch before Banking)
AddMappings("Credit Card Payment", mappings, 200,
"PAYMENT TO CREDIT CARD", "CAPITAL ONE", "MOBILE PAYMENT THANK YOU");
// Ice Cream
@@ -229,4 +229,12 @@ namespace MoneyMap.Services
}
}
}
// ===== Database Migration =====
// Add this to your DbContext:
// public DbSet<CategoryMapping> CategoryMappings { get; set; }
//
// Then create a migration:
// dotnet ef migrations add AddCategoryMappings
// dotnet ef database update
}