Sweep all .cshtml files for encoding corruption; add pre-commit guard
Replace all corruption variants with HTML entities across 226 view files: - 3-char UTF-8-as-Win1252 sequences (ae-corruption) - Standalone smart/curly quotes that break C# Razor expressions - Partially re-corrupted variants where the 3rd byte was normalised to ASCII tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the script itself never contains a literal non-ASCII character; supports -DryRun .githooks/pre-commit: blocks commits containing the ae-corruption byte signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the hook is repo-committed and active for all future work on this machine. Build clean; 225 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,8 @@
|
||||
var emailOptedOut = hasEmail && !Model.CustomerNotifyByEmail;
|
||||
var smsPhone = !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone) ? Model.CustomerMobilePhone : Model.CustomerPhone;
|
||||
var hasSms = !string.IsNullOrWhiteSpace(smsPhone) && Model.CustomerNotifyBySms;
|
||||
var showSendModal = hasEmail && !emailOptedOut && hasSms; // both channels — show choice modal
|
||||
var directSendSms = !hasEmail && hasSms; // SMS only — skip modal
|
||||
var showSendModal = hasEmail && !emailOptedOut && hasSms; // both channels — show choice modal
|
||||
var directSendSms = !hasEmail && hasSms; // SMS only — skip modal
|
||||
var hasAvailableCredits = ViewBag.AvailableCreditMemos != null && ((IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos).Any();
|
||||
var canIssueRefund = !isDraft && !isVoided && Model.AmountPaid > 0;
|
||||
var canApplyCredit = !isVoided && Model.BalanceDue > 0 && hasAvailableCredits;
|
||||
@@ -80,7 +80,7 @@
|
||||
<div class="alert alert-warning alert-permanent d-flex align-items-center gap-2 mb-4">
|
||||
<i class="bi bi-envelope-slash fs-5"></i>
|
||||
<span>
|
||||
<strong>@Model.CustomerName</strong> has no email address on file — you'll be prompted to enter one when sending.
|
||||
<strong>@Model.CustomerName</strong> has no email address on file — you'll be prompted to enter one when sending.
|
||||
<a asp-controller="Customers" asp-action="Edit" asp-route-id="@Model.CustomerId" class="alert-link">Add one in customer settings</a>.
|
||||
</span>
|
||||
</div>
|
||||
@@ -179,12 +179,12 @@
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small mb-1">Due Date</label>
|
||||
<p class="mb-0 @(Model.Status == InvoiceStatus.Overdue ? "text-danger fw-bold" : "")">
|
||||
@(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "—")
|
||||
@(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "—")
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small mb-1">Sent Date</label>
|
||||
<p class="mb-0">@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "—")</p>
|
||||
<p class="mb-0">@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "—")</p>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CustomerPO))
|
||||
{
|
||||
@@ -350,7 +350,7 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted">
|
||||
@(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "—")
|
||||
@(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "—")
|
||||
</td>
|
||||
<td class="text-end fw-semibold">@gcItem.TotalPrice.ToString("C")</td>
|
||||
<td>
|
||||
@@ -396,7 +396,7 @@
|
||||
<tr>
|
||||
<td>@p.PaymentDate.ToString("MM/dd/yyyy")</td>
|
||||
<td>@p.PaymentMethodDisplay</td>
|
||||
<td>@(p.Reference ?? "—")</td>
|
||||
<td>@(p.Reference ?? "—")</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(p.DepositAccountName))
|
||||
{
|
||||
@@ -404,10 +404,10 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>@(p.RecordedByName ?? "—")</td>
|
||||
<td>@(p.RecordedByName ?? "—")</td>
|
||||
<td class="text-end fw-semibold text-success">@p.Amount.ToString("C")</td>
|
||||
<td class="text-end">
|
||||
@if (!isVoided)
|
||||
@@ -463,7 +463,7 @@
|
||||
<td>@r.RefundDate.ToString("MM/dd/yyyy")</td>
|
||||
<td>@r.RefundMethodDisplay</td>
|
||||
<td>@r.Reason</td>
|
||||
<td>@(r.Reference ?? "—")</td>
|
||||
<td>@(r.Reference ?? "—")</td>
|
||||
<td><span class="badge bg-@refundStatusColor">@r.Status</span></td>
|
||||
<td class="text-end fw-semibold text-danger">(@r.Amount.ToString("C"))</td>
|
||||
<td class="text-nowrap">
|
||||
@@ -575,7 +575,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
||||
data-bs-title="Invoice Actions"
|
||||
data-bs-content="Workflow: Edit (Draft only) → Send Invoice (locks it, emails customer) → Record Payment. Partial payments are supported — record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts.">
|
||||
data-bs-content="Workflow: Edit (Draft only) → Send Invoice (locks it, emails customer) → Record Payment. Partial payments are supported — record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -601,7 +601,7 @@
|
||||
}
|
||||
else if (showSendModal)
|
||||
{
|
||||
@* Both email + SMS available — let staff choose *@
|
||||
@* Both email + SMS available — let staff choose *@
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#sendChannelModal">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
@@ -609,7 +609,7 @@
|
||||
}
|
||||
else if (directSendSms)
|
||||
{
|
||||
@* SMS only — send directly *@
|
||||
@* SMS only — send directly *@
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
onclick="submitSendInvoice(false, true)">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice via SMS
|
||||
@@ -792,7 +792,7 @@
|
||||
{
|
||||
<div class="mb-2">
|
||||
<span class="badge bg-success-subtle text-success mb-2">
|
||||
<i class="bi bi-check-circle me-1"></i>Active — expires @Model.PaymentLinkExpiresAt!.Value.ToString("MMM d")
|
||||
<i class="bi bi-check-circle me-1"></i>Active — expires @Model.PaymentLinkExpiresAt!.Value.ToString("MMM d")
|
||||
</span>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="paymentLinkInput" class="form-control font-monospace"
|
||||
@@ -970,7 +970,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
||||
data-bs-title="Payment Reference"
|
||||
data-bs-content="Optional identifier for reconciliation — e.g., the check number, last 4 digits of the card, ACH transaction ID, or Venmo/PayPal confirmation code. Appears in payment history so you can match payments to your bank statement.">
|
||||
data-bs-content="Optional identifier for reconciliation — e.g., the check number, last 4 digits of the card, ACH transaction ID, or Venmo/PayPal confirmation code. Appears in payment history so you can match payments to your bank statement.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -1189,7 +1189,7 @@
|
||||
</div>
|
||||
<div id="refundAlertCredit" class="alert alert-success small mb-3 d-none">
|
||||
<i class="bi bi-piggy-bank me-1"></i>
|
||||
The refund amount will be added to the customer's store credit balance immediately — no manual action needed.
|
||||
The refund amount will be added to the customer's store credit balance immediately — no manual action needed.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Amount <span class="text-danger">*</span></label>
|
||||
@@ -1326,7 +1326,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Select Credit Memo <span class="text-danger">*</span></label>
|
||||
<select name="CreditMemoId" class="form-select" required>
|
||||
<option value="">— Select —</option>
|
||||
<option value="">— Select —</option>
|
||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos)
|
||||
{
|
||||
<option value="@item.Value">@item.Text</option>
|
||||
@@ -1340,7 +1340,7 @@
|
||||
<input type="number" name="Amount" class="form-control" step="0.01" min="0.01"
|
||||
max="@Model.BalanceDue.ToString("F2")" value="@Model.BalanceDue.ToString("F2")" required />
|
||||
</div>
|
||||
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C") — the system will cap at the memo's remaining balance.</div>
|
||||
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C") — the system will cap at the memo's remaining balance.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -1422,7 +1422,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Bad Debt Expense Account</label>
|
||||
<select name="expenseAccountId" class="form-select">
|
||||
<option value="">— Use default bad debt account —</option>
|
||||
<option value="">— Use default bad debt account —</option>
|
||||
@if (ViewBag.ExpenseAccounts != null)
|
||||
{
|
||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
|
||||
@@ -1503,7 +1503,7 @@
|
||||
document.getElementById('gcAmountInput').value = max.toFixed(2);
|
||||
document.getElementById('gcAmountInput').max = max;
|
||||
const expiry = data.expiryDate ? ` · Expires ${data.expiryDate}` : '';
|
||||
result.innerHTML = `<div class="alert alert-success py-1 mb-0 small"><i class="bi bi-check-circle me-1"></i><strong>${data.certificateCode}</strong> — $${data.remainingBalance.toFixed(2)} remaining${expiry}</div>`;
|
||||
result.innerHTML = `<div class="alert alert-success py-1 mb-0 small"><i class="bi bi-check-circle me-1"></i><strong>${data.certificateCode}</strong> — $${data.remainingBalance.toFixed(2)} remaining${expiry}</div>`;
|
||||
}
|
||||
} catch { result.innerHTML = '<div class="alert alert-danger py-1 mb-0 small">Lookup failed.</div>'; }
|
||||
document.getElementById('gcLookupSpinner').style.display = 'none';
|
||||
@@ -1616,7 +1616,7 @@
|
||||
<td class="small">${escHtml(n.type.replace(/([A-Z])/g, ' $1').trim())}</td>
|
||||
<td class="small"><i class="bi ${channelIcon} me-1"></i>${escHtml(n.channel)}</td>
|
||||
<td class="small">${escHtml(n.recipientName)}<br><span class="text-muted">${escHtml(n.recipient)}</span></td>
|
||||
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted">—</span>'}</td>
|
||||
<td><span class="badge bg-${statusClass}">${escHtml(n.status)}</span>${expandBtn}</td>
|
||||
</tr>${errorRow}`;
|
||||
}).join('');
|
||||
|
||||
Reference in New Issue
Block a user