Enhance card management with account linking and nicknames

- Added account linking functionality to cards with dropdown selector
- Added optional nickname field for easier card identification
- Updated Cards list page to show linked accounts with badges
- Reorganized card display to show issuer and last4 together
- Include account relationship when loading cards

This allows cards to be properly associated with their bank accounts,
which is essential for the transaction import and categorization flow.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AJ
2025-10-11 20:53:46 -04:00
parent eb3e9c5407
commit 338374f831
4 changed files with 80 additions and 6 deletions

View File

@@ -39,9 +39,9 @@
<table class="table table-hover mb-0"> <table class="table table-hover mb-0">
<thead> <thead>
<tr> <tr>
<th>Card</th>
<th>Owner</th> <th>Owner</th>
<th>Issuer</th> <th>Linked Account</th>
<th>Last 4</th>
<th class="text-end">Transactions</th> <th class="text-end">Transactions</th>
<th style="width: 150px;">Actions</th> <th style="width: 150px;">Actions</th>
</tr> </tr>
@@ -50,9 +50,27 @@
@foreach (var item in Model.Cards) @foreach (var item in Model.Cards)
{ {
<tr> <tr>
<td>
<div>
<strong>@item.Card.Issuer •••• @item.Card.Last4</strong>
@if (!string.IsNullOrEmpty(item.Card.Nickname))
{
<br />
<small class="text-muted">@item.Card.Nickname</small>
}
</div>
</td>
<td>@item.Card.Owner</td> <td>@item.Card.Owner</td>
<td>@item.Card.Issuer</td> <td>
<td>•••• @item.Card.Last4</td> @if (item.Card.Account != null)
{
<span class="badge bg-success">@item.Card.Account.DisplayLabel</span>
}
else
{
<span class="text-muted">None</span>
}
</td>
<td class="text-end">@item.TransactionCount</td> <td class="text-end">@item.TransactionCount</td>
<td> <td>
<div class="d-flex gap-1"> <div class="d-flex gap-1">

View File

@@ -29,6 +29,7 @@ namespace MoneyMap.Pages
public async Task OnGetAsync() public async Task OnGetAsync()
{ {
var cards = await _db.Cards var cards = await _db.Cards
.Include(c => c.Account)
.OrderBy(c => c.Owner) .OrderBy(c => c.Owner)
.ThenBy(c => c.Last4) .ThenBy(c => c.Last4)
.ToListAsync(); .ToListAsync();

View File

@@ -40,6 +40,33 @@
<div class="form-text">The last 4 digits of the card number</div> <div class="form-text">The last 4 digits of the card number</div>
</div> </div>
<div class="mb-3">
<label asp-for="Card.Nickname" class="form-label">Nickname (Optional)</label>
<input asp-for="Card.Nickname" class="form-control" placeholder="e.g., Personal Travel Card" />
<span asp-validation-for="Card.Nickname" class="text-danger"></span>
<div class="form-text">A friendly name to help identify this card</div>
</div>
<div class="mb-3">
<label asp-for="Card.AccountId" class="form-label">Linked Account</label>
<select asp-for="Card.AccountId" class="form-select">
<option value="">-- No linked account --</option>
@foreach (var account in Model.AvailableAccounts)
{
<option value="@account.Id">@account.DisplayLabel</option>
}
</select>
<span asp-validation-for="Card.AccountId" class="text-danger"></span>
<div class="form-text">
Link this card to the bank account it draws from or pays to
@if (!Model.AvailableAccounts.Any())
{
<br />
<strong class="text-warning">No accounts available. <a asp-page="/EditAccount">Create one first</a>.</strong>
}
</div>
</div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
@(Model.IsNewCard ? "Add Card" : "Save Changes") @(Model.IsNewCard ? "Add Card" : "Save Changes")
@@ -61,6 +88,8 @@
<li><strong>Owner:</strong> Use the cardholder's name for easy identification</li> <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>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><strong>Last 4:</strong> These digits help match transactions to the correct card</li>
<li><strong>Nickname:</strong> Optional friendly name like "Personal Travel Card" or "Business Card"</li>
<li><strong>Linked Account:</strong> Link credit cards to their payment accounts, or debit cards to their checking accounts</li>
<li>Cards with transactions cannot be deleted, only edited</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> <li>Auto-imported cards will have "Unknown" as the owner - update them here</li>
</ul> </ul>

View File

@@ -22,11 +22,15 @@ namespace MoneyMap.Pages
public bool IsNewCard { get; set; } public bool IsNewCard { get; set; }
public List<Account> AvailableAccounts { get; set; } = new();
[TempData] [TempData]
public string? SuccessMessage { get; set; } public string? SuccessMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int? id) public async Task<IActionResult> OnGetAsync(int? id)
{ {
await LoadAvailableAccountsAsync();
if (id.HasValue) if (id.HasValue)
{ {
var card = await _db.Cards.FindAsync(id.Value); var card = await _db.Cards.FindAsync(id.Value);
@@ -38,7 +42,9 @@ namespace MoneyMap.Pages
Id = card.Id, Id = card.Id,
Owner = card.Owner, Owner = card.Owner,
Issuer = card.Issuer, Issuer = card.Issuer,
Last4 = card.Last4 Last4 = card.Last4,
Nickname = card.Nickname,
AccountId = card.AccountId
}; };
IsNewCard = false; IsNewCard = false;
@@ -56,6 +62,7 @@ namespace MoneyMap.Pages
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
IsNewCard = Card.Id == 0; IsNewCard = Card.Id == 0;
await LoadAvailableAccountsAsync();
return Page(); return Page();
} }
@@ -66,7 +73,9 @@ namespace MoneyMap.Pages
{ {
Owner = Card.Owner.Trim(), Owner = Card.Owner.Trim(),
Issuer = Card.Issuer.Trim(), Issuer = Card.Issuer.Trim(),
Last4 = Card.Last4.Trim() Last4 = Card.Last4.Trim(),
Nickname = Card.Nickname?.Trim(),
AccountId = Card.AccountId
}; };
_db.Cards.Add(card); _db.Cards.Add(card);
@@ -82,6 +91,8 @@ namespace MoneyMap.Pages
card.Owner = Card.Owner.Trim(); card.Owner = Card.Owner.Trim();
card.Issuer = Card.Issuer.Trim(); card.Issuer = Card.Issuer.Trim();
card.Last4 = Card.Last4.Trim(); card.Last4 = Card.Last4.Trim();
card.Nickname = Card.Nickname?.Trim();
card.AccountId = Card.AccountId;
SuccessMessage = "Card updated successfully!"; SuccessMessage = "Card updated successfully!";
} }
@@ -90,6 +101,14 @@ namespace MoneyMap.Pages
return RedirectToPage("/Cards"); return RedirectToPage("/Cards");
} }
private async Task LoadAvailableAccountsAsync()
{
AvailableAccounts = await _db.Accounts
.OrderBy(a => a.Institution)
.ThenBy(a => a.Last4)
.ToListAsync();
}
public class CardEditModel public class CardEditModel
{ {
public int Id { get; set; } public int Id { get; set; }
@@ -106,6 +125,13 @@ namespace MoneyMap.Pages
[StringLength(4, MinimumLength = 4, ErrorMessage = "Last 4 must be exactly 4 digits")] [StringLength(4, MinimumLength = 4, ErrorMessage = "Last 4 must be exactly 4 digits")]
[RegularExpression(@"^\d{4}$", ErrorMessage = "Last 4 must be 4 digits")] [RegularExpression(@"^\d{4}$", ErrorMessage = "Last 4 must be 4 digits")]
public string Last4 { get; set; } = ""; public string Last4 { get; set; } = "";
[StringLength(50)]
[Display(Name = "Nickname (Optional)")]
public string? Nickname { get; set; }
[Display(Name = "Linked Account")]
public int? AccountId { get; set; }
} }
} }
} }