Compare commits

...

2 Commits

Author SHA1 Message Date
spouliot cf6acc125f Complete mobile card view coverage for all remaining pages
- CSS fix: change blanket .table-responsive hide to only trigger when
  a .mobile-card-view sibling exists (.mobile-card-view ~ .table-responsive
  and :has() rule) — auto-fixes 60+ forms/reports/detail/help pages that
  were showing blank on mobile by making their tables scroll instead
- Add mobile card views to remaining list pages:
  JobsPriority (overdue jobs, main board, maintenance sections)
  NotificationLogs (email/SMS log entries)
  AiUsageReport (per-company AI usage breakdown)
  GiftCertificates/BulkResult (batch certificate list)
  Inventory/SamplePanels (Need to Order + On Wall tabs)
  BannedIps (active bans + lifted/expired bans)
  OnboardingProgress (per-company activation funnel)
  ReleaseNotes/Manage (versioned changelog entries)
  StorageMigration/Results (file migration status list)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:31:38 -04:00
spouliot f467862877 Add mobile card views to 12 high-priority list pages
Pages were blank on phones because mobile-cards.css hides .table-responsive
below 992px. Added .mobile-card-view sections to: GiftCertificates, PurchaseOrders,
CreditMemos, VendorCredits, JournalEntries, Appointments, InAppNotifications,
BankReconciliations, FixedAssets, RecurringTemplates, SmsAgreements, SmsConsentAudit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:07:52 -04:00
22 changed files with 1551 additions and 2 deletions
@@ -109,6 +109,69 @@
<span class="fw-semibold">Per-Company Breakdown</span>
<span class="text-muted small">@Model.Rows.Count companies total</span>
</div>
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var row in Model.Rows)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);">
<i class="bi bi-robot"></i>
</div>
<div class="mobile-card-title">
<h6>@row.CompanyName @if (!row.IsActive) { <span class="badge bg-secondary ms-1">Inactive</span> }</h6>
<small><span class="badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">@row.Plan</span></small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Today</span>
<span class="mobile-card-value @(row.Today > 0 ? "fw-semibold" : "text-muted")">
@if (row.Today > 0) { @row.Today.ToString("N0") } else { <span>&mdash;</span> }
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">30 Days</span>
<span class="mobile-card-value @(row.Last30Days > 0 ? "fw-semibold" : "text-muted")">
@if (row.Last30Days > 0) { @row.Last30Days.ToString("N0") } else { <span>&mdash;</span> }
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">All Time</span>
<span class="mobile-card-value @(row.AllTime > 0 ? "" : "text-muted")">
@if (row.AllTime > 0) { @row.AllTime.ToString("N0") } else { <span>&mdash;</span> }
</span>
</div>
@if (row.TopFeature != null)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Top Feature</span>
<span class="mobile-card-value">
<i class="bi @FeatureIcon(row.TopFeature) me-1 text-muted"></i>@row.FeatureDisplayName(row.TopFeature)
</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Tier</span>
<span class="mobile-card-value"><span class="badge @row.TierBadgeClass">@row.UsageTier</span></span>
</div>
</div>
<div class="mobile-card-footer">
<a asp-controller="Companies" asp-action="Details" asp-route-id="@row.CompanyId" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-building me-1"></i>Company
</a>
</div>
</div>
}
@if (!Model.Rows.Any())
{
<div class="text-center text-muted py-5">
<i class="bi bi-robot fs-1 d-block mb-2 opacity-25"></i>
No AI usage logged yet.
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" id="aiUsageTable">
<thead class="table-light">
@@ -176,6 +176,60 @@
<div class="card-body">
@if (Model.Items.Any())
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var appointment in Model.Items)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);">
<i class="bi bi-calendar-event"></i>
</div>
<div class="mobile-card-title">
<h6>@appointment.Title</h6>
<small>@appointment.ScheduledStartTime.ToString("MMM dd, yyyy")<br />@(!appointment.IsAllDay ? $"{appointment.ScheduledStartTime:h:mm tt} &ndash; {appointment.ScheduledEndTime:h:mm tt}" : "All Day")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
<span class="badge bg-@appointment.StatusColorClass">@appointment.StatusDisplayName</span>
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Type</span>
<span class="mobile-card-value">
<span class="badge bg-@appointment.TypeColorClass">@appointment.TypeDisplayName</span>
</span>
</div>
@if (!string.IsNullOrEmpty(appointment.CustomerName))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Customer</span>
<span class="mobile-card-value">@appointment.CustomerName</span>
</div>
}
@if (!string.IsNullOrEmpty(appointment.AssignedWorkerName))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Worker</span>
<span class="mobile-card-value">@appointment.AssignedWorkerName</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@appointment.Id" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye me-1"></i>View
</a>
<a asp-action="Edit" asp-route-id="@appointment.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil me-1"></i>Edit
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
@@ -21,6 +21,64 @@
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var br in Model)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #14b8a6 0%, #0f766e 100%);">
<i class="bi bi-bank"></i>
</div>
<div class="mobile-card-title">
<h6>@br.Account?.Name</h6>
<small>Statement: @br.StatementDate.ToString("MMM d, yyyy")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@if (br.Status == BankReconciliationStatus.Completed)
{
<span class="badge bg-success">Completed</span>
}
else
{
<span class="badge bg-warning text-dark">In Progress</span>
}
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Ending Balance</span>
<span class="mobile-card-value fw-semibold">@br.EndingBalance.ToString("C")</span>
</div>
@if (br.CompletedAt.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Completed By</span>
<span class="mobile-card-value">@br.CompletedBy</span>
</div>
}
</div>
<div class="mobile-card-footer">
@if (br.Status == BankReconciliationStatus.Completed)
{
<a asp-action="Report" asp-route-id="@br.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-file-earmark-text me-1"></i>Report
</a>
}
else
{
<a asp-action="Reconcile" asp-route-id="@br.Id" class="btn btn-sm btn-outline-primary">
<i class="bi bi-check2-square me-1"></i>Continue
</a>
}
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -60,6 +60,59 @@
<div class="card-body p-0">
@if (active.Any())
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var ban in active)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);">
<i class="bi bi-slash-circle"></i>
</div>
<div class="mobile-card-title">
<h6 class="font-monospace">@ban.IpAddress</h6>
<small class="text-muted">@(ban.Reason ?? "No reason given")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Banned</span>
<span class="mobile-card-value">@ban.BannedAt.ToString("MMM d, yyyy HH:mm")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Expires</span>
<span class="mobile-card-value">
@if (ban.ExpiresAt.HasValue)
{
<span class="badge bg-warning text-dark">@ban.ExpiresAt.Value.ToString("MMM d, yyyy")</span>
}
else
{
<span class="badge bg-secondary">Permanent</span>
}
</span>
</div>
</div>
<div class="mobile-card-footer">
<form asp-action="Lift" asp-route-id="@ban.Id" method="post" class="d-inline"
onsubmit="return confirm('Lift the ban on @ban.IpAddress?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-success">
<i class="bi bi-check-circle me-1"></i>Lift
</button>
</form>
<form asp-action="Delete" asp-route-id="@ban.Id" method="post" class="d-inline"
onsubmit="return confirm('Delete ban record for @ban.IpAddress?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -130,6 +183,55 @@
<h6 class="mb-0 text-muted"><i class="bi bi-clock-history"></i> Lifted / Expired Bans</h6>
</div>
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var ban in inactive)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);">
<i class="bi bi-clock-history"></i>
</div>
<div class="mobile-card-title">
<h6 class="font-monospace">@ban.IpAddress</h6>
<small>
@if (!ban.IsActive)
{
<span class="badge bg-success">Lifted</span>
}
else
{
<span class="badge bg-secondary">Expired</span>
}
</small>
</div>
</div>
<div class="mobile-card-body">
@if (!string.IsNullOrEmpty(ban.Reason))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Reason</span>
<span class="mobile-card-value text-muted">@ban.Reason</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Banned</span>
<span class="mobile-card-value text-muted">@ban.BannedAt.ToString("MMM d, yyyy")</span>
</div>
</div>
<div class="mobile-card-footer">
<form asp-action="Delete" asp-route-id="@ban.Id" method="post" class="d-inline"
onsubmit="return confirm('Delete ban record for @ban.IpAddress?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash me-1"></i>Delete
</button>
</form>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
@@ -101,6 +101,73 @@
else
{
<div class="card">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var m in Model)
{
var expired2 = m.ExpiryDate.HasValue && m.ExpiryDate.Value < DateTime.UtcNow
&& m.Status != CreditMemoStatus.FullyApplied
&& m.Status != CreditMemoStatus.Voided;
var (cmBadge, cmLabel) = m.Status switch
{
CreditMemoStatus.Active => ("bg-success-subtle text-success", "Active"),
CreditMemoStatus.PartiallyApplied => ("bg-warning-subtle text-warning", "Partial"),
CreditMemoStatus.FullyApplied => ("bg-secondary-subtle text-secondary", "Applied"),
CreditMemoStatus.Voided => ("bg-danger-subtle text-danger", "Voided"),
_ => ("bg-secondary-subtle text-secondary", m.Status.ToString())
};
var cmCustomer = string.IsNullOrWhiteSpace(m.Customer?.CompanyName)
? $"{m.Customer?.ContactFirstName} {m.Customer?.ContactLastName}".Trim()
: m.Customer!.CompanyName;
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = m.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);">
<i class="bi bi-journal-minus"></i>
</div>
<div class="mobile-card-title">
<h6>@m.MemoNumber</h6>
<small>@cmCustomer</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge @cmBadge">@cmLabel</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Amount</span>
<span class="mobile-card-value">@m.Amount.ToString("C")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Remaining</span>
<span class="mobile-card-value @(m.RemainingBalance > 0 && m.Status != CreditMemoStatus.Voided ? "text-success fw-semibold" : "text-muted")">
@m.RemainingBalance.ToString("C")
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Issued</span>
<span class="mobile-card-value">@m.IssueDate.ToLocalTime().ToString("MM/dd/yy")</span>
</div>
@if (m.ExpiryDate.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Expires</span>
<span class="mobile-card-value @(expired2 ? "text-danger fw-semibold" : "")">
@m.ExpiryDate.Value.ToLocalTime().ToString("MM/dd/yy")
@if (expired2) { <small>(Expired)</small> }
</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@m.Id" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
Details
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
@@ -118,6 +118,63 @@
}
else
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var a in Model)
{
var fd = a.AccumulatedDepreciation >= (a.PurchaseCost - a.SalvageValue);
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = a.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%);">
<i class="bi bi-building-gear"></i>
</div>
<div class="mobile-card-title">
<h6>@a.Name</h6>
<small>Purchased @a.PurchaseDate.ToLocalTime().ToString("MM/dd/yyyy")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@if (a.IsDisposed)
{
<span class="badge bg-secondary">Disposed</span>
}
else if (fd)
{
<span class="badge bg-light text-dark border">Fully Depreciated</span>
}
else
{
<span class="badge bg-success">Active</span>
}
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Cost</span>
<span class="mobile-card-value">@a.PurchaseCost.ToString("C")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Book Value</span>
<span class="mobile-card-value @(a.BookValue <= 0 ? "text-muted" : "text-success fw-semibold")">
@a.BookValue.ToString("C")
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Monthly Depr.</span>
<span class="mobile-card-value">@a.MonthlyDepreciation.ToString("C")</span>
</div>
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@a.Id" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
<i class="bi bi-eye me-1"></i>View
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
@@ -26,6 +26,48 @@
</a>
</div>
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var cert in Model)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
<i class="bi bi-gift"></i>
</div>
<div class="mobile-card-title">
<h6 class="font-monospace">@cert.CertificateCode</h6>
<small>@cert.OriginalAmount.ToString("C")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Issued</span>
<span class="mobile-card-value">@cert.IssueDate.ToLocalTime().ToString("MMM d, yyyy")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Expiry</span>
<span class="mobile-card-value">
@if (cert.ExpiryDate.HasValue) { @cert.ExpiryDate.Value.ToLocalTime().ToString("MMM d, yyyy") } else { <span class="text-muted">&mdash;</span> }
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge bg-success">Active</span></span>
</div>
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@cert.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye me-1"></i>View
</a>
<a asp-action="DownloadPdf" asp-route-id="@cert.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-file-pdf me-1"></i>PDF
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -57,6 +57,73 @@ else
{
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var cert in Model)
{
var (gcBadge, gcLabel) = cert.Status switch
{
GiftCertificateStatus.Active => ("bg-success", "Active"),
GiftCertificateStatus.PartiallyRedeemed => ("bg-info text-dark", "Partial"),
GiftCertificateStatus.FullyRedeemed => ("bg-secondary", "Used"),
GiftCertificateStatus.Expired => ("bg-warning text-dark", "Expired"),
GiftCertificateStatus.Voided => ("bg-danger", "Voided"),
_ => ("bg-secondary", cert.Status.ToString())
};
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = cert.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%);">
<i class="bi bi-gift"></i>
</div>
<div class="mobile-card-title">
<h6 class="font-monospace">@cert.CertificateCode</h6>
<small>@(cert.RecipientName ?? cert.RecipientEmail ?? "No recipient")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge @gcBadge">@gcLabel</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Face Value</span>
<span class="mobile-card-value">@cert.OriginalAmount.ToString("C")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Remaining</span>
<span class="mobile-card-value @(cert.RemainingBalance > 0 ? "text-success fw-semibold" : "text-muted")">
@cert.RemainingBalance.ToString("C")
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Issued</span>
<span class="mobile-card-value">@cert.IssueDate.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy")</span>
</div>
@if (cert.ExpiryDate.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Expires</span>
<span class="mobile-card-value @(cert.ExpiryDate.Value < DateTime.Now ? "text-danger" : "")">
@cert.ExpiryDate.Value.ToString("MM/dd/yy")
</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@cert.Id" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
<i class="bi bi-eye me-1"></i>View
</a>
@if (cert.BatchId.HasValue)
{
<a asp-action="BulkResult" asp-route-batchId="@cert.BatchId" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
<i class="bi bi-collection me-1"></i>Batch
</a>
}
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -29,6 +29,61 @@
else
{
<div class="card border-0 shadow-sm">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var n in items)
{
bool mIsRead = (bool)n.IsRead;
string mTitle = (string)n.Title;
string mMessage = (string)n.Message;
string? mLink = (string?)n.Link;
string mType = (string)n.NotificationType;
DateTime mCreatedAt = ((DateTime)n.CreatedAt).Tz(ViewBag.CompanyTimeZone as string);
<div class="mobile-data-card notif-history-row @(!mIsRead ? "notif-unread" : "")"
data-id="@n.Id"
data-title="@mTitle"
data-message="@mMessage"
data-link="@(mLink ?? "")"
data-type="@mType"
data-is-read="@(mIsRead ? "1" : "0")"
data-created-at="@mCreatedAt.ToString("MMM d, yyyy h:mm tt")">
<div class="mobile-card-header" style="@(!mIsRead ? "background:rgba(99,102,241,0.08);" : "")">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);">
<i class="bi bi-bell"></i>
</div>
<div class="mobile-card-title">
<h6 class="@(!mIsRead ? "fw-semibold" : "text-muted")">
@if (!mIsRead)
{
<span style="display:inline-block;width:8px;height:8px;background:#6366f1;border-radius:50%;margin-right:6px;"></span>
}
@mTitle
</h6>
<small>@mCreatedAt.ToString("MMM d, yyyy h:mm tt")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Type</span>
<span class="mobile-card-value"><span class="badge bg-secondary bg-opacity-25 text-body small">@mType</span></span>
</div>
<div class="mobile-card-row" style="align-items:flex-start;">
<span class="mobile-card-label">Message</span>
<span class="mobile-card-value" style="white-space:normal;text-align:right;">@mMessage</span>
</div>
</div>
@if (!string.IsNullOrEmpty(mLink))
{
<div class="mobile-card-footer">
<a href="@mLink" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
<i class="bi bi-arrow-right me-1"></i>Open
</a>
</div>
}
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
@@ -126,6 +126,77 @@
}
else
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@{ lastMfr = null; }
@foreach (var item in needOrder)
{
if (string.IsNullOrWhiteSpace(selectedMfr) && item.Manufacturer != lastMfr)
{
lastMfr = item.Manufacturer;
<div class="text-uppercase fw-semibold text-muted small px-2 py-1 border-bottom mt-2">
@(string.IsNullOrWhiteSpace(item.Manufacturer) ? "No Manufacturer" : item.Manufacturer)
</div>
}
<div class="mobile-data-card">
<div class="mobile-card-header">
@if (!string.IsNullOrWhiteSpace(item.ColorCode))
{
<div class="mobile-card-icon" style="background: @(item.ColorCode.StartsWith("#") ? item.ColorCode : "#" + item.ColorCode); border: 1px solid var(--bs-border-color);"></div>
}
else
{
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #64748b 0%, #475569 100%);">
<i class="bi bi-palette"></i>
</div>
}
<div class="mobile-card-title">
<h6>@(item.ColorName ?? item.Name)</h6>
<small>@(item.Manufacturer ?? "No Manufacturer")</small>
</div>
</div>
<div class="mobile-card-body">
@if (!string.IsNullOrWhiteSpace(item.ManufacturerPartNumber))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Part #</span>
<span class="mobile-card-value text-muted">@item.ManufacturerPartNumber</span>
</div>
}
@if (!string.IsNullOrWhiteSpace(item.Finish))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Finish</span>
<span class="mobile-card-value">@item.Finish</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">In Stock</span>
<span class="mobile-card-value">
@if (item.QuantityOnHand > 0)
{
<span class="badge bg-success bg-opacity-10 text-success">@item.QuantityOnHand.ToString("N2") @item.UnitOfMeasure</span>
}
else
{
<span class="text-muted">None</span>
}
</span>
</div>
</div>
<div class="mobile-card-footer">
<button class="btn btn-sm btn-outline-success btn-toggle-panel"
data-item-id="@item.Id" data-has-panel="true">
<i class="bi bi-check-lg me-1"></i>Got It
</button>
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0" id="needTable">
<thead class="table-group-divider">
@@ -220,6 +291,68 @@
}
else
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@{ lastMfr = null; }
@foreach (var item in onHand)
{
if (string.IsNullOrWhiteSpace(selectedMfr) && item.Manufacturer != lastMfr)
{
lastMfr = item.Manufacturer;
<div class="text-uppercase fw-semibold text-muted small px-2 py-1 border-bottom mt-2">
@(string.IsNullOrWhiteSpace(item.Manufacturer) ? "No Manufacturer" : item.Manufacturer)
</div>
}
<div class="mobile-data-card">
<div class="mobile-card-header">
@if (!string.IsNullOrWhiteSpace(item.ColorCode))
{
<div class="mobile-card-icon" style="background: @(item.ColorCode.StartsWith("#") ? item.ColorCode : "#" + item.ColorCode); border: 1px solid var(--bs-border-color);"></div>
}
else
{
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #059669 0%, #047857 100%);">
<i class="bi bi-palette"></i>
</div>
}
<div class="mobile-card-title">
<h6>@(item.ColorName ?? item.Name)</h6>
<small>@(item.Manufacturer ?? "No Manufacturer")</small>
</div>
</div>
<div class="mobile-card-body">
@if (!string.IsNullOrWhiteSpace(item.ManufacturerPartNumber))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Part #</span>
<span class="mobile-card-value text-muted">@item.ManufacturerPartNumber</span>
</div>
}
@if (!string.IsNullOrWhiteSpace(item.Finish))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Finish</span>
<span class="mobile-card-value">@item.Finish</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>On Wall</span></span>
</div>
</div>
<div class="mobile-card-footer">
<button class="btn btn-sm btn-outline-danger btn-toggle-panel"
data-item-id="@item.Id" data-has-panel="false">
<i class="bi bi-x-lg me-1"></i>Remove
</button>
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-group-divider">
@@ -71,6 +71,59 @@
</div>
</div>
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var job in overdueJobs)
{
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);">
<i class="bi bi-exclamation-triangle"></i>
</div>
<div class="mobile-card-title">
<h6>@job.JobNumber</h6>
<small>@job.CustomerName</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge bg-@job.StatusColorClass">@job.StatusDisplayName</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Priority</span>
<span class="mobile-card-value"><span class="badge bg-@job.PriorityColorClass">@job.PriorityDisplayName</span></span>
</div>
@if (job.ScheduledDate.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Scheduled</span>
<span class="mobile-card-value text-danger fw-bold">@job.ScheduledDate.Value.ToString("MMM d, yyyy")</span>
</div>
}
@if (job.DueDate.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Due</span>
<span class="mobile-card-value text-danger">@job.DueDate.Value.ToString("MMM d, yyyy")</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@job.JobId" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
<i class="bi bi-eye me-1"></i>View
</a>
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openPriorityModal(@job.JobId, @job.JobPriorityId, '@job.JobNumber')">
<i class="bi bi-flag"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openWorkerModal(@job.JobId, '@(job.AssignedUserId ?? "")', '@job.JobNumber')">
<i class="bi bi-person"></i>
</button>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
@@ -191,6 +244,74 @@
</div>
</div>
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var job in Model)
{
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);">
<i class="bi bi-kanban"></i>
</div>
<div class="mobile-card-title">
<h6>@job.JobNumber</h6>
<small>@job.CustomerName</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge bg-@job.StatusColorClass" id="status-badge-@job.JobId">@job.StatusDisplayName</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Priority</span>
<span class="mobile-card-value"><span class="badge bg-@job.PriorityColorClass priority-badge-@job.JobId">@job.PriorityDisplayName</span></span>
</div>
@if (!string.IsNullOrEmpty(job.AssignedWorkerName))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Worker</span>
<span class="mobile-card-value"><span class="badge bg-info"><i class="bi bi-person me-1"></i>@job.AssignedWorkerName</span></span>
</div>
}
@if (job.ScheduledDate.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Scheduled</span>
<span class="mobile-card-value">@job.ScheduledDate.Value.ToString("MMM d, yyyy")</span>
</div>
}
@if (job.DueDate.HasValue)
{
var mJobOverdue = job.DueDate.Value.Date < DateTime.Today;
<div class="mobile-card-row">
<span class="mobile-card-label">Due</span>
<span class="mobile-card-value @(mJobOverdue ? "text-danger fw-bold" : "")">@job.DueDate.Value.ToString("MMM d, yyyy")</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@job.JobId" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
<i class="bi bi-eye me-1"></i>View
</a>
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openPriorityModal(@job.JobId, @job.JobPriorityId, '@job.JobNumber')" title="Change Priority">
<i class="bi bi-flag"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openWorkerModal(@job.JobId, '@(job.AssignedUserId ?? "")', '@job.JobNumber')" title="Assign Worker">
<i class="bi bi-person"></i>
</button>
</div>
</div>
}
@if (!Model.Any())
{
<div class="text-center text-muted py-5">
<i class="bi bi-calendar-check fs-1 d-block mb-2 opacity-25"></i>
No jobs scheduled for @scheduledDate.ToString("MMMM dd, yyyy").
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0" id="jobsTable">
<thead>
@@ -352,6 +473,65 @@
}
else
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var item in maintenanceItems)
{
var mPriorityBg = item.Priority switch
{
MaintenancePriority.Critical => "danger",
MaintenancePriority.High => "warning",
MaintenancePriority.Normal => "info",
_ => "secondary"
};
var mStatusBgM = item.Status == MaintenanceStatus.InProgress ? "success" : "primary";
var mStatusLbl = item.Status == MaintenanceStatus.InProgress ? "In Progress" : "Scheduled";
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Maintenance", new { id = item.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #b45309 100%);">
<i class="bi bi-tools"></i>
</div>
<div class="mobile-card-title">
<h6>@(item.Equipment?.EquipmentName ?? "Maintenance")</h6>
<small>@item.MaintenanceType</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Priority</span>
<span class="mobile-card-value"><span class="badge bg-@mPriorityBg">@item.Priority</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge bg-@mStatusBgM">@mStatusLbl</span></span>
</div>
@if (item.AssignedUser != null)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Worker</span>
<span class="mobile-card-value"><span class="badge bg-info text-dark"><i class="bi bi-person me-1"></i>@item.AssignedUser.FullName</span></span>
</div>
}
@if (!string.IsNullOrEmpty(item.Description))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Desc.</span>
<span class="mobile-card-value text-muted">@item.Description</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-controller="Maintenance" asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
<i class="bi bi-eye me-1"></i>View
</a>
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openMaintenanceWorkerModal(@item.Id, '@(item.AssignedUserId ?? "")', '@(item.Equipment?.EquipmentName ?? "Maintenance")')" title="Assign Worker">
<i class="bi bi-person"></i>
</button>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
@@ -47,6 +47,68 @@
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var je in Model)
{
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = je.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);">
<i class="bi bi-journal-text"></i>
</div>
<div class="mobile-card-title">
<h6>
@je.EntryNumber
@if (je.IsReversal)
{
<span class="badge bg-secondary ms-1">REV</span>
}
</h6>
<small>@je.EntryDate.ToString("MMM d, yyyy")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@if (je.Status == JournalEntryStatus.Draft)
{
<span class="badge bg-warning text-dark">Draft</span>
}
else if (je.Status == JournalEntryStatus.Posted)
{
<span class="badge bg-success">Posted</span>
}
else
{
<span class="badge bg-secondary">Reversed</span>
}
</span>
</div>
@if (!string.IsNullOrWhiteSpace(je.Description))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Description</span>
<span class="mobile-card-value" style="white-space:normal;text-align:right;">@je.Description</span>
</div>
}
@if (!string.IsNullOrWhiteSpace(je.Reference))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Reference</span>
<span class="mobile-card-value">@je.Reference</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@je.Id" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
View
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -134,6 +134,67 @@
}
else
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var item in Model.Items)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: @(item.Channel == PowderCoating.Core.Enums.NotificationChannel.Email ? "linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)" : "linear-gradient(135deg, #06b6d4 0%, #0e7490 100%)");">
<i class="bi @(item.Channel == PowderCoating.Core.Enums.NotificationChannel.Email ? "bi-envelope" : "bi-phone")"></i>
</div>
<div class="mobile-card-title">
<h6>@item.RecipientName</h6>
<small>@item.Recipient</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Type</span>
<span class="mobile-card-value">@item.NotificationTypeDisplay</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Sent</span>
<span class="mobile-card-value">@item.SentAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd HH:mm")</span>
</div>
@if (item.JobId.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Job</span>
<span class="mobile-card-value">@item.JobNumber</span>
</div>
}
else if (item.QuoteId.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Quote</span>
<span class="mobile-card-value">@item.QuoteNumber</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@{
var (mStatusBadge, mStatusIcon) = item.Status switch
{
PowderCoating.Core.Enums.NotificationStatus.Sent => ("bg-success", "bi-check-circle"),
PowderCoating.Core.Enums.NotificationStatus.Failed => ("bg-danger", "bi-x-circle"),
_ => ("bg-secondary", "bi-dash-circle")
};
}
<span class="badge @mStatusBadge"><i class="bi @mStatusIcon me-1"></i>@item.StatusDisplay</span>
</span>
</div>
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye me-1"></i>View
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
@@ -44,6 +44,91 @@
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var row in Model.Rows)
{
var oPct = row.TotalSteps == 0 ? 0 : row.StepsCompleted * 100 / row.TotalSteps;
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Companies", new { id = row.CompanyId })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);">
<i class="bi bi-building"></i>
</div>
<div class="mobile-card-title">
<h6>@row.CompanyName</h6>
<small>
@switch (row.Status)
{
case OnboardingStatus.Complete:
<span class="badge bg-success">Complete</span>
break;
case OnboardingStatus.InProgress:
<span class="badge bg-warning text-dark">In Progress</span>
break;
case OnboardingStatus.Dismissed:
<span class="badge bg-secondary">Dismissed</span>
break;
default:
<span class="badge bg-light text-muted border">Not Started</span>
break;
}
</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Wizard</span>
<span class="mobile-card-value">
@if (row.WizardCompleted)
{
<i class="bi bi-check-circle-fill text-success"></i>
<span class="text-success ms-1">Done</span>
}
else
{
<i class="bi bi-circle text-muted"></i>
<span class="text-muted ms-1">Pending</span>
}
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Milestones</span>
<span class="mobile-card-value">
<div class="d-flex align-items-center gap-2">
<div class="progress" style="height:5px; width:60px;">
<div class="progress-bar @(oPct == 100 ? "bg-success" : "bg-primary")" style="width:@oPct%"></div>
</div>
<small class="text-muted">@row.StepsCompleted/@row.TotalSteps</small>
</div>
</span>
</div>
@{
var oFirstActivity = row.FirstJobCreatedAt ?? row.FirstQuoteCreatedAt;
}
@if (oFirstActivity.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">First Activity</span>
<span class="mobile-card-value text-muted">@oFirstActivity.Value.ToString("MMM d, yyyy")</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-controller="Companies" asp-action="Details" asp-route-id="@row.CompanyId" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
<i class="bi bi-building me-1"></i>View
</a>
</div>
</div>
}
@if (!Model.Rows.Any())
{
<div class="text-center text-muted py-5">
<i class="bi bi-building fs-1 d-block mb-2 opacity-25"></i>
No companies found.
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="onboardingTable">
<thead class="table-light">
@@ -164,6 +164,56 @@
<!-- Grid -->
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var po in Model.Items)
{
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = po.Id })'">
<div class="mobile-card-header" style="@(po.IsOverdue ? "background:#fee2e2;" : "")">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);">
<i class="bi bi-cart-check"></i>
</div>
<div class="mobile-card-title">
<h6>@po.PoNumber @(po.IsOverdue ? " — Overdue" : "")</h6>
<small>@po.VendorName</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge bg-@StatusBadge(po.Status)">@po.Status</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Order Date</span>
<span class="mobile-card-value">@po.OrderDate.ToString("MM/dd/yy")</span>
</div>
@if (po.ExpectedDeliveryDate.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Expected</span>
<span class="mobile-card-value @(po.IsOverdue ? "text-danger fw-semibold" : "")">
@po.ExpectedDeliveryDate.Value.ToString("MM/dd/yy")
</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Items</span>
<span class="mobile-card-value">@po.ItemCount</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Total</span>
<span class="mobile-card-value fw-semibold">$@po.TotalAmount.ToString("N2")</span>
</div>
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@po.Id" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
<i class="bi bi-eye me-1"></i>View
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -38,6 +38,96 @@
}
else
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var t in Model)
{
var isOverdueRT = t.IsActive && t.NextFireDate.Date < DateTime.Today;
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);">
<i class="bi bi-arrow-repeat"></i>
</div>
<div class="mobile-card-title">
<h6>@t.Name</h6>
<small>
@if (t.TemplateType == RecurringTemplateType.Bill)
{
<span>Bill</span>
}
else
{
<span>Expense</span>
}
&mdash;
@(t.IntervalCount == 1 ? t.Frequency.ToString() : $"Every {t.IntervalCount} &times; {t.Frequency}")
</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@if (t.IsActive)
{
<span class="badge bg-success"><i class="bi bi-play-fill me-1"></i>Active</span>
}
else
{
<span class="badge bg-secondary"><i class="bi bi-pause-fill me-1"></i>Paused</span>
}
</span>
</div>
@if (t.IsActive)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Next Fire</span>
<span class="mobile-card-value @(isOverdueRT ? "text-danger fw-semibold" : "")">
@t.NextFireDate.ToString("MM/dd/yyyy")
@if (isOverdueRT) { <i class="bi bi-exclamation-circle ms-1"></i> }
</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Occurrences</span>
<span class="mobile-card-value">
@t.OccurrenceCount
@if (t.MaxOccurrences.HasValue) { <span class="text-muted"> / @t.MaxOccurrences</span> }
</span>
</div>
@if (!string.IsNullOrWhiteSpace(t.LastError))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Error</span>
<span class="mobile-card-value text-danger small">@t.LastError</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-action="Edit" asp-route-id="@t.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
<form asp-action="ToggleActive" asp-route-id="@t.Id" method="post" style="display:inline;">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm @(t.IsActive ? "btn-outline-warning" : "btn-outline-success")">
<i class="bi @(t.IsActive ? "bi-pause" : "bi-play")"></i>
</button>
</form>
@if (t.IsActive)
{
<form asp-action="GenerateNow" asp-route-id="@t.Id" method="post" style="display:inline;"
onsubmit="return confirm('Generate one occurrence now?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-lightning-charge"></i>
</button>
</form>
}
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
@@ -65,6 +65,77 @@
</div>
<div class="card border-0 shadow-sm">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var note in Model)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);">
<i class="bi bi-journal-text"></i>
</div>
<div class="mobile-card-title">
<h6><code>v@(note.Version)</code> &mdash; @note.Title</h6>
<small>
<span class="badge @TagBadge(note.Tag)">@note.Tag</span>
&nbsp;
@if (note.IsPublished)
{
<span class="badge bg-success">Published</span>
}
else
{
<span class="badge bg-warning text-dark">Draft</span>
}
</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Released</span>
<span class="mobile-card-value text-muted">@note.ReleasedAt.ToString("MM/dd/yyyy")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Created By</span>
<span class="mobile-card-value text-muted">@note.CreatedByUserName</span>
</div>
@if (note.Body.Length > 0)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Preview</span>
<span class="mobile-card-value text-muted">@(note.Body.Length > 60 ? note.Body[..60] + "…" : note.Body)</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-action="Edit" asp-route-id="@note.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
<form asp-action="TogglePublish" asp-route-id="@note.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm @(note.IsPublished ? "btn-outline-warning" : "btn-outline-success")">
<i class="bi @(note.IsPublished ? "bi-eye-slash" : "bi-eye")"></i>
</button>
</form>
<form asp-action="Delete" asp-route-id="@note.Id" method="post" class="d-inline"
onsubmit="return confirm('Delete v@(note.Version)?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
}
@if (!Model.Any())
{
<div class="text-center text-muted py-5">
<i class="bi bi-journal-x fs-1 d-block mb-2 opacity-25"></i>
No release notes yet. <a asp-action="Create">Create the first one.</a>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 small">
<thead class="table-light">
@@ -112,6 +112,101 @@
}
else
{
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var row in Model)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);">
<i class="bi bi-building"></i>
</div>
<div class="mobile-card-title">
<h6>
@row.CompanyName
@if (row.IsDeleted) { <span class="badge bg-secondary ms-1">Deleted</span> }
</h6>
<small>
@if (row.SmsDisabledByAdmin)
{
<span class="text-danger"><i class="bi bi-slash-circle me-1"></i>Admin-Disabled</span>
}
else if (row.SmsEnabled)
{
<span class="text-success"><i class="bi bi-chat-dots me-1"></i>SMS Enabled</span>
}
else
{
<span class="text-muted">SMS Off</span>
}
</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Terms</span>
<span class="mobile-card-value">
@{
var dispAgreement = row.CurrentAgreement ?? row.LatestAgreement;
}
@if (row.CurrentAgreement != null)
{
<span class="badge bg-success">v@(row.CurrentAgreement.TermsVersion)</span>
}
else if (row.LatestAgreement != null)
{
<span class="badge bg-warning text-dark">Stale (v@(row.LatestAgreement.TermsVersion))</span>
}
else
{
<span class="badge bg-light text-muted border">Never</span>
}
</span>
</div>
@if (dispAgreement != null)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Accepted By</span>
<span class="mobile-card-value @(row.CurrentAgreement == null ? "text-muted" : "")">
@dispAgreement.AgreedByUserName
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Accepted At</span>
<span class="mobile-card-value @(row.CurrentAgreement == null ? "text-muted" : "")">
@dispAgreement.AgreedAt.ToString("MM/dd/yy")
</span>
</div>
}
@if (row.AllAgreements.Count > 0)
{
<div class="mobile-card-row">
<span class="mobile-card-label">History</span>
<span class="mobile-card-value">
<button type="button"
class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#historyModal"
data-company="@row.CompanyName"
data-history="@System.Text.Json.JsonSerializer.Serialize(row.AllAgreements.Select(a => new {
a.TermsVersion,
a.AgreedByUserName,
a.AgreedByUserId,
AgreedAt = a.AgreedAt.ToString("MMM d, yyyy 'at' h:mm tt") + " UTC",
IpAddress = a.IpAddress ?? "—",
UserAgent = a.UserAgent ?? "—"
}), new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase })"
onclick="event.stopPropagation()">
@row.AllAgreements.Count <i class="bi bi-clock-history ms-1"></i>
</button>
</span>
</div>
}
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
@@ -110,6 +110,64 @@
</form>
</div>
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var row in Model.Rows)
{
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Customers", new { id = row.CustomerId })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #ec4899 0%, #be185d 100%);">
<i class="bi bi-phone-vibrate"></i>
</div>
<div class="mobile-card-title">
<h6>@row.CustomerName</h6>
<small>@(row.MobilePhone ?? row.Phone ?? "No phone")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">SMS Status</span>
<span class="mobile-card-value"><span class="badge @row.StatusBadgeClass">@row.StatusLabel</span></span>
</div>
@if (row.ConsentedAt.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Consented</span>
<span class="mobile-card-value">@row.ConsentedAt.Value.ToString("MMM d, yyyy")</span>
</div>
}
@if (!string.IsNullOrWhiteSpace(row.ConsentMethod))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Method</span>
<span class="mobile-card-value">@row.ConsentMethod</span>
</div>
}
@if (row.OptedOutAt.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Opted Out</span>
<span class="mobile-card-value text-danger">@row.OptedOutAt.Value.ToString("MMM d, yyyy")</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-controller="Customers" asp-action="Details" asp-route-id="@row.CustomerId"
class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
<i class="bi bi-person me-1"></i>Customer
</a>
</div>
</div>
}
@if (!Model.Rows.Any())
{
<div class="text-center text-muted py-5">
<i class="bi bi-phone-vibrate fs-1 d-block mb-2 opacity-25"></i>
No customers match the current filter.
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="table-light">
@@ -84,6 +84,46 @@
}
</div>
</div>
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var file in Model.Files.OrderBy(f => f.Status).ThenBy(f => f.RelativePath))
{
var fStatusBadge = file.Status switch
{
MigrationFileStatus.Migrated => "bg-success",
MigrationFileStatus.Skipped => "bg-secondary",
_ => "bg-danger"
};
var fStatusLabel = file.Status switch
{
MigrationFileStatus.Migrated => "Migrated",
MigrationFileStatus.Skipped => "Already in Azure",
_ => "Failed"
};
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0369a1 0%, #075985 100%);">
<i class="bi bi-file-earmark"></i>
</div>
<div class="mobile-card-title">
<h6 class="font-monospace" style="font-size:.75rem;">@file.RelativePath</h6>
<small><span class="badge bg-light text-dark border">@file.Container</span></small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Size</span>
<span class="mobile-card-value text-muted">@FormatBytes(file.FileSize)</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge @fStatusBadge">@fStatusLabel</span></span>
</div>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead>
@@ -68,6 +68,64 @@
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var vc in Model)
{
var (vcBadge, vcLabel) = vc.Status switch
{
VendorCreditStatus.Open => ("bg-success", "Open"),
VendorCreditStatus.PartiallyApplied => ("bg-warning text-dark", "Partial"),
VendorCreditStatus.Applied => ("bg-secondary", "Applied"),
VendorCreditStatus.Voided => ("bg-danger", "Voided"),
_ => ("bg-secondary", vc.Status.ToString())
};
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = vc.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
<i class="bi bi-credit-card"></i>
</div>
<div class="mobile-card-title">
<h6>@vc.CreditNumber</h6>
<small>@vc.Vendor?.CompanyName</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge @vcBadge">@vcLabel</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Date</span>
<span class="mobile-card-value">@vc.CreditDate.ToString("MM/dd/yy")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Total</span>
<span class="mobile-card-value">@vc.Total.ToString("C")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Remaining</span>
<span class="mobile-card-value @(vc.RemainingAmount > 0 ? "text-success fw-semibold" : "text-muted")">
@(vc.RemainingAmount > 0 ? vc.RemainingAmount.ToString("C") : "&mdash;")
</span>
</div>
@if (!string.IsNullOrWhiteSpace(vc.Memo))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Memo</span>
<span class="mobile-card-value">@vc.Memo</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a asp-action="Details" asp-route-id="@vc.Id" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
View
</a>
</div>
</div>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -9,8 +9,9 @@
}
@media (max-width: 991px) {
/* Hide desktop table view on mobile */
.table-responsive {
/* Hide desktop table only when a mobile card view sibling is present */
.mobile-card-view ~ .table-responsive,
.table-responsive:has(~ .mobile-card-view) {
display: none !important;
}