Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 627d723c95 | |||
| 0498decfb0 | |||
| 2fae9aefad | |||
| 2c179bc892 | |||
| deb248b2a6 | |||
| eb8fc8b6d0 | |||
| 4f039b8281 |
@@ -37,6 +37,7 @@ public class PaymentDtos
|
||||
public string? Reference { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public int? DepositAccountId { get; set; }
|
||||
public bool SuppressNotification { get; set; }
|
||||
}
|
||||
|
||||
public class EditPaymentDto
|
||||
|
||||
@@ -82,14 +82,15 @@ public class InvoicesController : Controller
|
||||
// -----------------------------------------------------------------------
|
||||
/// <summary>
|
||||
/// Displays the paginated invoice list with multi-mode filtering. The filter cascade handles
|
||||
/// nine combinations of overdue/outstanding/thisMonth flags with status and search term so the
|
||||
/// database receives a single targeted predicate — no full-table load then in-memory LINQ.
|
||||
/// statusGroup pills (unpaid/partial/paid/all) plus legacy flag combinations (overdue/outstanding/thisMonth)
|
||||
/// so the database receives a single targeted predicate — no full-table load then in-memory LINQ.
|
||||
/// Balance-due sort is computed in the ORDER BY expression rather than a stored column because
|
||||
/// balance = Total − AmountPaid − CreditApplied − GiftCertificateRedeemed changes on every payment.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> Index(
|
||||
string? searchTerm,
|
||||
InvoiceStatus? statusFilter,
|
||||
string? statusGroup,
|
||||
string? sortColumn,
|
||||
string sortDirection = "desc",
|
||||
bool outstandingOnly = false,
|
||||
@@ -100,6 +101,11 @@ public class InvoicesController : Controller
|
||||
{
|
||||
try
|
||||
{
|
||||
// Default landing: show unpaid invoices so the list is immediately actionable.
|
||||
if (string.IsNullOrEmpty(statusGroup) && !statusFilter.HasValue &&
|
||||
string.IsNullOrEmpty(searchTerm) && !outstandingOnly && !thisMonthOnly && !overdueOnly)
|
||||
return RedirectToAction("Index", new { statusGroup = "unpaid" });
|
||||
|
||||
var today = DateTime.Today;
|
||||
var startOfMonth = new DateTime(today.Year, today.Month, 1);
|
||||
var endOfMonth = startOfMonth.AddMonths(1);
|
||||
@@ -116,7 +122,18 @@ public class InvoicesController : Controller
|
||||
|
||||
System.Linq.Expressions.Expression<Func<Invoice, bool>>? filter = null;
|
||||
|
||||
if (overdueOnly)
|
||||
// Status-group pills take priority over the dropdown and legacy flags.
|
||||
if (!string.IsNullOrEmpty(statusGroup))
|
||||
{
|
||||
filter = statusGroup switch
|
||||
{
|
||||
"unpaid" => i => i.Status == InvoiceStatus.Draft || i.Status == InvoiceStatus.Sent || i.Status == InvoiceStatus.Overdue,
|
||||
"partial" => i => i.Status == InvoiceStatus.PartiallyPaid,
|
||||
"paid" => i => i.Status == InvoiceStatus.Paid,
|
||||
_ => null // "all" — no predicate
|
||||
};
|
||||
}
|
||||
else if (overdueOnly)
|
||||
{
|
||||
filter = i => (i.Status == InvoiceStatus.Sent || i.Status == InvoiceStatus.PartiallyPaid || i.Status == InvoiceStatus.Overdue)
|
||||
&& i.DueDate.HasValue && i.DueDate.Value < today;
|
||||
@@ -215,12 +232,20 @@ public class InvoicesController : Controller
|
||||
|
||||
ViewBag.SearchTerm = searchTerm;
|
||||
ViewBag.StatusFilter = statusFilter;
|
||||
ViewBag.StatusGroup = statusGroup;
|
||||
ViewBag.OutstandingOnly = outstandingOnly;
|
||||
ViewBag.ThisMonthOnly = thisMonthOnly;
|
||||
ViewBag.OverdueOnly = overdueOnly;
|
||||
ViewBag.SortColumn = gridRequest.SortColumn;
|
||||
ViewBag.SortDirection = gridRequest.SortDirection;
|
||||
|
||||
// Pill badge counts — always global (not scoped to current filter/page)
|
||||
ViewBag.UnpaidCount = await _unitOfWork.Invoices.CountAsync(i =>
|
||||
i.Status == InvoiceStatus.Draft || i.Status == InvoiceStatus.Sent || i.Status == InvoiceStatus.Overdue);
|
||||
ViewBag.PartialCount = await _unitOfWork.Invoices.CountAsync(i => i.Status == InvoiceStatus.PartiallyPaid);
|
||||
ViewBag.PaidCount = await _unitOfWork.Invoices.CountAsync(i => i.Status == InvoiceStatus.Paid);
|
||||
ViewBag.AllCount = await _unitOfWork.Invoices.CountAsync();
|
||||
|
||||
return View(pagedResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1310,7 +1335,9 @@ public class InvoicesController : Controller
|
||||
|
||||
}); // end ExecuteInTransactionAsync
|
||||
|
||||
// Notify (non-blocking)
|
||||
// Notify (non-blocking) — skipped if user explicitly suppressed it
|
||||
if (!dto.SuppressNotification)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _notificationService.NotifyPaymentReceivedAsync(invoice, payment);
|
||||
@@ -1319,6 +1346,7 @@ public class InvoicesController : Controller
|
||||
{
|
||||
_logger.LogWarning(notifyEx, "Payment recorded but notification failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var paymentNotifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id, _tenantContext.GetCurrentCompanyId() ?? 0);
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
const preview = document.getElementById('announcementPreview');
|
||||
preview.className = 'alert mb-0 ' + (typeMap[type] || 'alert-info');
|
||||
document.getElementById('previewTitle').textContent = document.getElementById('Title').value || 'Title';
|
||||
document.getElementById('previewMessage').textContent = ' — ' + (document.getElementById('Message').value || 'Message');
|
||||
document.getElementById('previewMessage').textContent = '\u2014' + (document.getElementById('Message').value || 'Message');
|
||||
}
|
||||
document.getElementById('Type')?.addEventListener('change', updatePreview);
|
||||
document.getElementById('Title')?.addEventListener('input', updatePreview);
|
||||
|
||||
@@ -598,7 +598,7 @@
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('scanReceiptModal'));
|
||||
if (modal) modal.hide();
|
||||
|
||||
statusEl.textContent = 'Scan complete — review and adjust as needed.';
|
||||
statusEl.innerHTML = 'Scan complete — review and adjust as needed.';
|
||||
} catch (e) {
|
||||
statusEl.textContent = 'Error connecting to AI service.';
|
||||
} finally {
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header fw-semibold">Line Items</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
@@ -171,6 +172,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Receipt attachment -->
|
||||
@if (!string.IsNullOrEmpty(Model.ReceiptFilePath))
|
||||
@@ -210,6 +212,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
@@ -255,6 +258,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
@@ -181,6 +182,92 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mobile-card-view">
|
||||
<div class="mobile-card-list">
|
||||
@foreach (var entry in Model)
|
||||
{
|
||||
var isBill = entry.EntryType == "Bill";
|
||||
var detailUrl = isBill
|
||||
? Url.Action("Details", "Bills", new { id = entry.Id })
|
||||
: Url.Action("Details", "Expenses", new { id = entry.Id });
|
||||
<div class="mobile-data-card" onclick="window.location='@detailUrl'"
|
||||
style="@(entry.IsOverdue ? "border-left: 3px solid #f59e0b;" : "")">
|
||||
<div class="mobile-card-header">
|
||||
<div class="mobile-card-icon" style="background: linear-gradient(135deg, @(isBill ? "#3b82f6 0%, #2563eb" : "#6b7280 0%, #4b5563") 100%);">
|
||||
<i class="bi @(isBill ? "bi-file-text" : "bi-receipt")"></i>
|
||||
</div>
|
||||
<div class="mobile-card-title">
|
||||
<h6>@entry.Number</h6>
|
||||
<small>@entry.VendorName</small>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
@if (isBill)
|
||||
{
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">Bill</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">Expense</span>
|
||||
}
|
||||
</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-@entry.StatusColor">@entry.StatusLabel</span></span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Date</span>
|
||||
<span class="mobile-card-value">@entry.Date.ToString("MMM d, yyyy")</span>
|
||||
</div>
|
||||
@if (entry.DueDate.HasValue)
|
||||
{
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Due</span>
|
||||
<span class="mobile-card-value @(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>
|
||||
</div>
|
||||
}
|
||||
<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)
|
||||
{
|
||||
<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>
|
||||
}
|
||||
@{
|
||||
var memoText = isBill ? entry.Memo : entry.AccountName;
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(memoText))
|
||||
{
|
||||
<div class="mobile-card-row">
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<a href="@detailUrl" class="btn btn-sm @(isBill ? "btn-outline-primary" : "btn-outline-secondary")"
|
||||
onclick="event.stopPropagation()">
|
||||
<i class="bi bi-eye me-1"></i>View
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
|
||||
@@ -3382,7 +3382,7 @@
|
||||
document.getElementById('ovenCalcToggle').addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const hidden = _calcPanel.classList.toggle('d-none');
|
||||
if (!hidden) { _calcW.value = ''; _calcD.value = ''; _calcH.value = ''; _calcResult.textContent = '—'; _calcApply.disabled = true; _calcW.focus(); }
|
||||
if (!hidden) { _calcW.value = ''; _calcD.value = ''; _calcH.value = ''; _calcResult.innerHTML = '—'; _calcApply.disabled = true; _calcW.focus(); }
|
||||
});
|
||||
|
||||
function _updateCalc() {
|
||||
@@ -3402,7 +3402,7 @@
|
||||
_calcApply.disabled = false;
|
||||
_calcApply.dataset.val = val;
|
||||
} else {
|
||||
_calcResult.textContent = '—';
|
||||
_calcResult.innerHTML = '—';
|
||||
_calcApply.disabled = true;
|
||||
}
|
||||
}
|
||||
@@ -3420,7 +3420,7 @@
|
||||
document.getElementById('ovenModal').addEventListener('hidden.bs.modal', function () {
|
||||
_calcPanel.classList.add('d-none');
|
||||
_calcW.value = ''; _calcD.value = ''; _calcH.value = '';
|
||||
_calcResult.textContent = '—'; _calcApply.disabled = true;
|
||||
_calcResult.innerHTML = '—'; _calcApply.disabled = true;
|
||||
});
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1029,6 +1029,12 @@
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea name="Notes" class="form-control" rows="2" placeholder="Optional"></textarea>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="SuppressNotification" value="true" id="suppressNotificationCheck" />
|
||||
<label class="form-check-label text-muted small" for="suppressNotificationCheck">
|
||||
Don’t notify customer
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
|
||||
@@ -11,8 +11,13 @@
|
||||
ViewData["PageHelpContent"] = "Invoices are created from completed jobs and sent to the customer for payment. Lifecycle: Draft (editable) → Sent (locked, awaiting payment) → Partially Paid / Paid. Overdue = past due date with a balance still owed. Outstanding shows the total A/R balance across all unpaid invoices currently on screen. Use Void to cancel without deleting history.";
|
||||
var searchTerm = ViewBag.SearchTerm as string;
|
||||
var statusFilter = ViewBag.StatusFilter as InvoiceStatus?;
|
||||
var statusGroup = ViewBag.StatusGroup as string;
|
||||
var outstandingOnly = (bool)(ViewBag.OutstandingOnly ?? false);
|
||||
var thisMonthOnly = (bool)(ViewBag.ThisMonthOnly ?? false);
|
||||
var unpaidCount = (int)(ViewBag.UnpaidCount ?? 0);
|
||||
var partialCount = (int)(ViewBag.PartialCount ?? 0);
|
||||
var paidCount = (int)(ViewBag.PaidCount ?? 0);
|
||||
var allCount = (int)(ViewBag.AllCount ?? 0);
|
||||
}
|
||||
|
||||
@{
|
||||
@@ -52,19 +57,21 @@
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header border-0 py-3">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-center gap-3">
|
||||
<form asp-action="Index" method="get" class="d-flex flex-nowrap gap-2 align-items-center">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<!-- Row 1: search + dropdown + actions -->
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center">
|
||||
<form asp-action="Index" method="get" class="d-flex flex-nowrap gap-2 align-items-center flex-grow-1" style="max-width:560px;">
|
||||
<input type="hidden" name="sortColumn" value="@ViewBag.SortColumn" />
|
||||
<input type="hidden" name="sortDirection" value="@ViewBag.SortDirection" />
|
||||
<input type="hidden" name="pageSize" value="@Model.PageSize" />
|
||||
@if (outstandingOnly) { <input type="hidden" name="outstandingOnly" value="true" /> }
|
||||
@if (thisMonthOnly) { <input type="hidden" name="thisMonthOnly" value="true" /> }
|
||||
<div class="input-group" style="max-width:280px; min-width:180px;">
|
||||
<div class="input-group" style="min-width:180px;">
|
||||
<span class="input-group-text border-end-0"><i class="bi bi-search text-muted"></i></span>
|
||||
<input type="text" name="searchTerm" class="form-control border-start-0"
|
||||
placeholder="Search invoices..." value="@searchTerm">
|
||||
placeholder="Search invoices…" value="@searchTerm">
|
||||
</div>
|
||||
<select class="form-select" name="statusFilter" style="width:auto;">
|
||||
<select class="form-select" name="statusFilter" style="width:auto;" onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
@foreach (InvoiceStatus s in Enum.GetValues(typeof(InvoiceStatus)))
|
||||
{
|
||||
@@ -72,33 +79,56 @@
|
||||
}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
|
||||
@if (!string.IsNullOrEmpty(searchTerm) || statusFilter.HasValue || outstandingOnly || thisMonthOnly)
|
||||
@if (!string.IsNullOrEmpty(searchTerm) || statusFilter.HasValue || outstandingOnly || thisMonthOnly || !string.IsNullOrEmpty(statusGroup))
|
||||
{
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">Clear</a>
|
||||
}
|
||||
@if (outstandingOnly)
|
||||
{
|
||||
<span class="badge bg-info text-dark fs-6 fw-normal">
|
||||
<i class="bi bi-funnel-fill me-1"></i>Outstanding A/R
|
||||
</span>
|
||||
}
|
||||
@if (thisMonthOnly && statusFilter == InvoiceStatus.Paid)
|
||||
{
|
||||
<span class="badge bg-success fs-6 fw-normal">
|
||||
<i class="bi bi-funnel-fill me-1"></i>Paid — @DateTime.Now.ToString("MMMM yyyy")
|
||||
</span>
|
||||
}
|
||||
else if (thisMonthOnly)
|
||||
{
|
||||
<span class="badge bg-info text-dark fs-6 fw-normal">
|
||||
<i class="bi bi-funnel-fill me-1"></i>@DateTime.Now.ToString("MMMM yyyy")
|
||||
</span>
|
||||
<a asp-action="Index" asp-route-statusGroup="unpaid" class="btn btn-outline-secondary text-nowrap"><i class="bi bi-x-lg"></i></a>
|
||||
}
|
||||
</form>
|
||||
<a asp-action="Create" class="btn btn-primary text-nowrap">
|
||||
<i class="bi bi-plus-circle me-2"></i>New Invoice
|
||||
</a>
|
||||
</div>
|
||||
<!-- Row 2: status-group pills -->
|
||||
<div class="pcl-pill-group">
|
||||
<a href="@Url.Action("Index", new { statusGroup = "all" })" class="pcl-pill @(statusGroup == "all" ? "active" : "")">
|
||||
All <span class="pcl-pill-count">@allCount</span>
|
||||
</a>
|
||||
<a href="@Url.Action("Index", new { statusGroup = "unpaid" })" class="pcl-pill @(statusGroup == "unpaid" ? "active" : "")">
|
||||
Unpaid <span class="pcl-pill-count">@unpaidCount</span>
|
||||
</a>
|
||||
<a href="@Url.Action("Index", new { statusGroup = "partial" })" class="pcl-pill @(statusGroup == "partial" ? "active" : "")">
|
||||
Partial <span class="pcl-pill-count">@partialCount</span>
|
||||
</a>
|
||||
<a href="@Url.Action("Index", new { statusGroup = "paid" })" class="pcl-pill @(statusGroup == "paid" ? "active" : "")">
|
||||
Paid <span class="pcl-pill-count">@paidCount</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Legacy filter badges (outstanding A/R, this-month) -->
|
||||
@if (outstandingOnly)
|
||||
{
|
||||
<div>
|
||||
<span class="badge bg-info text-dark fs-6 fw-normal">
|
||||
<i class="bi bi-funnel-fill me-1"></i>Outstanding A/R
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (thisMonthOnly && statusFilter == InvoiceStatus.Paid)
|
||||
{
|
||||
<div>
|
||||
<span class="badge bg-success fs-6 fw-normal">
|
||||
<i class="bi bi-funnel-fill me-1"></i>Paid — @DateTime.Now.ToString("MMMM yyyy")
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else if (thisMonthOnly)
|
||||
{
|
||||
<div>
|
||||
<span class="badge bg-info text-dark fs-6 fw-normal">
|
||||
<i class="bi bi-funnel-fill me-1"></i>@DateTime.Now.ToString("MMMM yyyy")
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if (Model != null && Model.Items.Any())
|
||||
|
||||
@@ -2936,8 +2936,12 @@
|
||||
profitEl.className = profit >= 0 ? 'text-success' : 'text-danger';
|
||||
|
||||
document.getElementById('costingMargin').textContent = `${d.grossMargin}%`;
|
||||
document.getElementById('costingQuotedMargin').textContent =
|
||||
d.quotedPrice > 0 ? `${d.quotedMargin}% (quoted ${fmt(d.quotedPrice)})` : '—';
|
||||
const quotedMarginEl = document.getElementById('costingQuotedMargin');
|
||||
if (d.quotedPrice > 0) {
|
||||
quotedMarginEl.textContent = `${d.quotedMargin}% (quoted ${fmt(d.quotedPrice)})`;
|
||||
} else {
|
||||
quotedMarginEl.innerHTML = '—';
|
||||
}
|
||||
|
||||
// Powder detail lines
|
||||
const pBody = document.getElementById('powderLines');
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
|
||||
} catch (e) {
|
||||
document.getElementById('loadingState').classList.add('d-none');
|
||||
document.getElementById('errorMessage').textContent = 'Network error — please try again.';
|
||||
document.getElementById('errorMessage').innerHTML = 'Network error — please try again.';
|
||||
document.getElementById('errorState').classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
|
||||
} catch (e) {
|
||||
document.getElementById('loadingState').classList.add('d-none');
|
||||
document.getElementById('errorMessage').textContent = 'Network error — please try again.';
|
||||
document.getElementById('errorMessage').innerHTML = 'Network error — please try again.';
|
||||
document.getElementById('errorState').classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,13 +91,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
ownerForm.addEventListener('submit', writeHiddenFields, { capture: true });
|
||||
|
||||
// Save scroll position before the form causes a full-page reload so we can
|
||||
// restore it after the server redirects back to this page. Key is path-specific
|
||||
// so navigating away and back doesn't restore a stale position.
|
||||
// restore it after the server redirects back to this page on a validation error.
|
||||
// Key is path-specific; cleared on pagehide unless we're leaving via a submit so
|
||||
// a fresh navigation to this page never restores a stale position.
|
||||
const scrollKey = 'wizardScrollY:' + location.pathname;
|
||||
let wizardSubmitting = false;
|
||||
ownerForm.addEventListener('submit', () => {
|
||||
wizardSubmitting = true;
|
||||
sessionStorage.setItem(scrollKey, String(Math.round(window.scrollY)));
|
||||
}, { capture: true });
|
||||
|
||||
// If the page unloads for any reason other than our own form submit (e.g. the
|
||||
// user clicks a nav link or the server redirects to a success page), discard the
|
||||
// saved position so it doesn't fire on the next fresh visit.
|
||||
window.addEventListener('pagehide', () => {
|
||||
if (!wizardSubmitting) sessionStorage.removeItem(scrollKey);
|
||||
});
|
||||
|
||||
// Restore on load — fire after layout is painted so scrollTo lands correctly.
|
||||
const savedY = sessionStorage.getItem(scrollKey);
|
||||
if (savedY !== null) {
|
||||
|
||||
@@ -123,7 +123,6 @@
|
||||
var todayLine = result.dailyTotal.toFixed(2) + ' hrs today (' + result.segmentCount + (result.segmentCount === 1 ? ' segment' : ' segments') + ')';
|
||||
|
||||
document.getElementById('tc-confirm-icon').innerHTML = icon;
|
||||
document.getElementById('tc-confirm-title').textContent = result.displayName + ' — ' + title;
|
||||
document.getElementById('tc-confirm-title').innerHTML = escHtml(result.displayName) + ' — ' + title;
|
||||
document.getElementById('tc-confirm-time').textContent = timeStr;
|
||||
document.getElementById('tc-confirm-today').textContent = todayLine;
|
||||
|
||||
Reference in New Issue
Block a user