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:
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user