Add mobile card view to Bills/Expenses list page

Wraps the desktop table in table-responsive to fix horizontal scrolling,
and adds a mobile-card-view section matching the pattern used on Invoices,
PurchaseOrders, and other list pages. Cards show type, number, vendor,
status, date, due date, amount, balance due, and memo/account.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 18:48:48 -04:00
parent deb248b2a6
commit 2c179bc892
+165 -78
View File
@@ -92,94 +92,181 @@
{ {
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-hover mb-0"> <div class="table-responsive">
<thead class="table-light"> <table class="table table-hover mb-0">
<tr> <thead class="table-light">
<th style="width:90px">Type</th> <tr>
<th>Number</th> <th style="width:90px">Type</th>
<th>Vendor</th> <th>Number</th>
<th>Memo / Account</th> <th>Vendor</th>
<th>Date</th> <th>Memo / Account</th>
<th>Due Date</th> <th>Date</th>
<th>Status</th> <th>Due Date</th>
<th class="text-end">Amount</th> <th>Status</th>
<th class="text-end">Balance Due</th> <th class="text-end">Amount</th>
<th></th> <th class="text-end">Balance Due</th>
</tr> <th></th>
</thead> </tr>
<tbody> </thead>
<tbody>
@foreach (var entry in Model)
{
<tr class="@(entry.IsOverdue ? "table-warning" : "")">
<td>
@if (entry.EntryType == "Bill")
{
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">
<i class="bi bi-file-text me-1"></i>Bill
</span>
}
else
{
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">
<i class="bi bi-receipt me-1"></i>Expense
</span>
}
</td>
<td>
@if (entry.EntryType == "Bill")
{
<a asp-controller="Bills" asp-action="Details" asp-route-id="@entry.Id"
class="fw-medium text-decoration-none">@entry.Number</a>
}
else
{
<a asp-controller="Expenses" asp-action="Details" asp-route-id="@entry.Id"
class="fw-medium text-decoration-none">@entry.Number</a>
}
</td>
<td>@entry.VendorName</td>
<td class="text-muted small">
@(entry.EntryType == "Bill" ? entry.Memo : entry.AccountName)
@if (entry.HasReceipt)
{
<i class="bi bi-paperclip ms-1" title="Has receipt"></i>
}
</td>
<td>@entry.Date.ToString("MMM d, yyyy")</td>
<td>
@if (entry.DueDate.HasValue)
{
<span class="@(entry.IsOverdue ? "text-danger fw-medium" : "")">
@entry.DueDate.Value.ToString("MMM d, yyyy")
@if (entry.IsOverdue) { <i class="bi bi-exclamation-circle ms-1"></i> }
</span>
}
else if (entry.EntryType == "Expense")
{
<span class="text-muted">&mdash;</span>
}
</td>
<td><span class="badge bg-@entry.StatusColor">@entry.StatusLabel</span></td>
<td class="text-end">@entry.Total.ToString("C")</td>
<td class="text-end fw-medium @(entry.BalanceDue > 0 ? "text-danger" : "text-muted")">
@Html.Raw(entry.EntryType == "Bill" ? entry.BalanceDue.ToString("C") : "&mdash;")
</td>
<td>
@if (entry.EntryType == "Bill")
{
<a asp-controller="Bills" asp-action="Details" asp-route-id="@entry.Id"
class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i></a>
}
else
{
<a asp-controller="Expenses" asp-action="Details" asp-route-id="@entry.Id"
class="btn btn-sm btn-outline-secondary"><i class="bi bi-eye"></i></a>
}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var entry in Model) @foreach (var entry in Model)
{ {
<tr class="@(entry.IsOverdue ? "table-warning" : "")"> var isBill = entry.EntryType == "Bill";
<td> var detailUrl = isBill
@if (entry.EntryType == "Bill") ? Url.Action("Details", "Bills", new { id = entry.Id })
{ : Url.Action("Details", "Expenses", new { id = entry.Id });
<span class="badge bg-primary-subtle text-primary border border-primary-subtle"> <div class="mobile-data-card" onclick="window.location='@detailUrl'"
<i class="bi bi-file-text me-1"></i>Bill style="@(entry.IsOverdue ? "border-left: 3px solid #f59e0b;" : "")">
</span> <div class="mobile-card-header">
} <div class="mobile-card-icon" style="background: linear-gradient(135deg, @(isBill ? "#3b82f6 0%, #2563eb" : "#6b7280 0%, #4b5563") 100%);">
else <i class="bi @(isBill ? "bi-file-text" : "bi-receipt")"></i>
{ </div>
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle"> <div class="mobile-card-title">
<i class="bi bi-receipt me-1"></i>Expense <h6>@entry.Number</h6>
</span> <small>@entry.VendorName</small>
} </div>
</td> <div class="ms-auto">
<td> @if (isBill)
@if (entry.EntryType == "Bill") {
{ <span class="badge bg-primary-subtle text-primary border border-primary-subtle">Bill</span>
<a asp-controller="Bills" asp-action="Details" asp-route-id="@entry.Id" }
class="fw-medium text-decoration-none">@entry.Number</a> else
} {
else <span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">Expense</span>
{ }
<a asp-controller="Expenses" asp-action="Details" asp-route-id="@entry.Id" </div>
class="fw-medium text-decoration-none">@entry.Number</a> </div>
} <div class="mobile-card-body">
</td> <div class="mobile-card-row">
<td>@entry.VendorName</td> <span class="mobile-card-label">Status</span>
<td class="text-muted small"> <span class="mobile-card-value"><span class="badge bg-@entry.StatusColor">@entry.StatusLabel</span></span>
@(entry.EntryType == "Bill" ? entry.Memo : entry.AccountName) </div>
@if (entry.HasReceipt) <div class="mobile-card-row">
{ <span class="mobile-card-label">Date</span>
<i class="bi bi-paperclip ms-1" title="Has receipt"></i> <span class="mobile-card-value">@entry.Date.ToString("MMM d, yyyy")</span>
} </div>
</td>
<td>@entry.Date.ToString("MMM d, yyyy")</td>
<td>
@if (entry.DueDate.HasValue) @if (entry.DueDate.HasValue)
{ {
<span class="@(entry.IsOverdue ? "text-danger fw-medium" : "")"> <div class="mobile-card-row">
@entry.DueDate.Value.ToString("MMM d, yyyy") <span class="mobile-card-label">Due</span>
@if (entry.IsOverdue) { <i class="bi bi-exclamation-circle ms-1"></i> } <span class="mobile-card-value @(entry.IsOverdue ? "text-danger fw-medium" : "")">
</span> @entry.DueDate.Value.ToString("MMM d, yyyy")
@if (entry.IsOverdue) { <i class="bi bi-exclamation-circle ms-1"></i> }
</span>
</div>
} }
else if (entry.EntryType == "Expense") <div class="mobile-card-row">
<span class="mobile-card-label">Amount</span>
<span class="mobile-card-value fw-semibold">@entry.Total.ToString("C")</span>
</div>
@if (isBill)
{ {
<span class="text-muted">&mdash;</span> <div class="mobile-card-row">
<span class="mobile-card-label">Balance Due</span>
<span class="mobile-card-value @(entry.BalanceDue > 0 ? "fw-semibold text-danger" : "text-muted")">
@entry.BalanceDue.ToString("C")
</span>
</div>
} }
</td> @{
<td><span class="badge bg-@entry.StatusColor">@entry.StatusLabel</span></td> var memoText = isBill ? entry.Memo : entry.AccountName;
<td class="text-end">@entry.Total.ToString("C")</td> }
<td class="text-end fw-medium @(entry.BalanceDue > 0 ? "text-danger" : "text-muted")"> @if (!string.IsNullOrEmpty(memoText))
@Html.Raw(entry.EntryType == "Bill" ? entry.BalanceDue.ToString("C") : "&mdash;")
</td>
<td>
@if (entry.EntryType == "Bill")
{ {
<a asp-controller="Bills" asp-action="Details" asp-route-id="@entry.Id" <div class="mobile-card-row">
class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i></a> <span class="mobile-card-label">@(isBill ? "Memo" : "Account")</span>
<span class="mobile-card-value text-muted small">
@memoText
@if (entry.HasReceipt) { <i class="bi bi-paperclip ms-1" title="Has receipt"></i> }
</span>
</div>
} }
else </div>
{ <div class="mobile-card-footer">
<a asp-controller="Expenses" asp-action="Details" asp-route-id="@entry.Id" <a href="@detailUrl" class="btn btn-sm @(isBill ? "btn-outline-primary" : "btn-outline-secondary")"
class="btn btn-sm btn-outline-secondary"><i class="bi bi-eye"></i></a> onclick="event.stopPropagation()">
} <i class="bi bi-eye me-1"></i>View
</td> </a>
</tr> </div>
</div>
} }
</tbody> </div>
</table> </div>
</div> </div>
</div> </div>
} }