Replace literal Unicode special chars with HTML entities across all 233 views
Sweeps em dashes, en dashes, multiplication signs, ellipses, and curly quotes to their HTML entity equivalents (— – × … ‘ ’) in all .cshtml files, skipping <script> blocks. Prevents encoding corruption from AI tools and Windows encoding mismatches that caused recurring symbol bugs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,378 +0,0 @@
|
||||
@model PowderCoating.Application.DTOs.Invoice.UpdateInvoiceDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Invoice";
|
||||
ViewData["PageIcon"] = "bi-pencil-square";
|
||||
var invoiceNumber = ViewBag.InvoiceNumber as string;
|
||||
var invoiceId = (int)(ViewBag.InvoiceId ?? 0);
|
||||
var jobNumber = ViewBag.JobNumber as string;
|
||||
var customerName = ViewBag.CustomerName as string;
|
||||
var canResend = ViewBag.CanResend == true;
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<p class="text-muted mb-0 small">
|
||||
@invoiceNumber
|
||||
@if (!string.IsNullOrEmpty(jobNumber)) { <span>— Job #@jobNumber</span> }
|
||||
@if (!string.IsNullOrEmpty(customerName)) { <span>— @customerName</span> }
|
||||
</p>
|
||||
</div>
|
||||
<a asp-action="Details" asp-route-id="@invoiceId" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to Invoice
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form asp-action="Edit" asp-route-id="@invoiceId" method="post" id="invoiceForm">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- LEFT: Main form -->
|
||||
<div class="col-lg-8">
|
||||
|
||||
<!-- Invoice Details -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header border-0 py-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h6 class="mb-0"><i class="bi bi-info-circle me-2"></i>Invoice Details</h6>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Invoice Details"
|
||||
data-bs-content="Invoice Date is the date of issue and the reference for payment terms. Due Date drives overdue status and A/R aging. Payment Terms prints on the invoice — changing it here only affects this invoice. Draft, Sent, and Overdue invoices can be edited; Paid and Partially Paid invoices are locked.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label asp-for="InvoiceDate" class="form-label fw-semibold">Invoice Date <span class="text-danger">*</span></label>
|
||||
<input asp-for="InvoiceDate" type="date" class="form-control"
|
||||
value="@Model.InvoiceDate.ToString("yyyy-MM-dd")" />
|
||||
<span asp-validation-for="InvoiceDate" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="DueDate" class="form-label fw-semibold">Due Date</label>
|
||||
<input asp-for="DueDate" type="date" class="form-control"
|
||||
value="@(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("yyyy-MM-dd") : "")" />
|
||||
<span asp-validation-for="DueDate" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="CustomerPO" class="form-label fw-semibold">Customer PO #</label>
|
||||
<input asp-for="CustomerPO" class="form-control" placeholder="Optional" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-md-12">
|
||||
<label asp-for="Terms" class="form-label fw-semibold">Payment Terms</label>
|
||||
<select asp-for="Terms" asp-items="ViewBag.PaymentTermsOptions" class="form-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Items -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header border-0 py-3 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h6 class="mb-0"><i class="bi bi-list-ul me-2"></i>Line Items</h6>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Line Items"
|
||||
data-bs-content="Each row is a billable line on the invoice. Qty × Unit Price = Total per line; you can also override Total directly. Color is optional and appears under the description when printed. Add manual lines for charges not in the original job (e.g., rush fee, pickup charge).">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addLineItem()">
|
||||
<i class="bi bi-plus-circle me-1"></i>Add Line
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-borderless align-middle mb-0" id="lineItemsTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="min-width:220px;">Description</th>
|
||||
<th style="width:70px;" class="text-center">Qty</th>
|
||||
<th style="width:120px;" class="text-end">Unit Price</th>
|
||||
<th style="width:120px;" class="text-end">Total</th>
|
||||
<th style="width:80px;" class="text-center">Color</th>
|
||||
<th style="width:40px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="lineItemsBody">
|
||||
@for (int i = 0; i < Model.InvoiceItems.Count; i++)
|
||||
{
|
||||
var item = Model.InvoiceItems[i];
|
||||
<tr class="line-item-row" data-index="@i">
|
||||
<td>
|
||||
<input type="hidden" name="InvoiceItems[@i].SourceJobItemId" value="@item.SourceJobItemId" />
|
||||
<input type="hidden" name="InvoiceItems[@i].DisplayOrder" class="display-order-input" value="@(i + 1)" />
|
||||
<input type="hidden" name="InvoiceItems[@i].RevenueAccountId" value="@item.RevenueAccountId" />
|
||||
<input type="text" name="InvoiceItems[@i].Description"
|
||||
class="form-control form-control-sm"
|
||||
value="@item.Description" placeholder="Description" required />
|
||||
<input type="text" name="InvoiceItems[@i].Notes"
|
||||
class="form-control form-control-sm mt-1"
|
||||
value="@item.Notes" placeholder="Notes (optional)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="InvoiceItems[@i].Quantity"
|
||||
class="form-control form-control-sm text-center qty-input"
|
||||
value="@item.Quantity" min="0.01" step="0.01"
|
||||
onchange="recalcRow(this)" oninput="recalcRow(this)" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="InvoiceItems[@i].UnitPrice"
|
||||
class="form-control form-control-sm text-end unit-price-input"
|
||||
value="@item.UnitPrice.ToString("F2")" min="0" step="0.01"
|
||||
onchange="recalcRow(this)" oninput="recalcRow(this)" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="InvoiceItems[@i].TotalPrice"
|
||||
class="form-control form-control-sm text-end total-price-input"
|
||||
value="@item.TotalPrice.ToString("F2")" min="0" step="0.01"
|
||||
oninput="recalcTotals()" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="text" name="InvoiceItems[@i].ColorName"
|
||||
class="form-control form-control-sm"
|
||||
value="@item.ColorName" placeholder="Color" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeLineItem(this)" title="Remove">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="noItemsMessage" class="text-center py-3 text-muted @(Model.InvoiceItems.Any() ? "d-none" : "")">
|
||||
<i class="bi bi-receipt me-2"></i>No line items yet. Click "Add Line" to get started.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header border-0 py-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h6 class="mb-0"><i class="bi bi-chat-text me-2"></i>Notes</h6>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Notes"
|
||||
data-bs-content="Customer Notes appear on the printed and emailed invoice — use these for payment instructions, thank-you messages, or job-specific reminders. Internal Notes are only visible to staff in the app and are never sent to the customer.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label asp-for="Notes" class="form-label fw-semibold">Customer Notes</label>
|
||||
<textarea asp-for="Notes" class="form-control" rows="3"
|
||||
placeholder="Notes visible to the customer on the invoice..."></textarea>
|
||||
<div class="form-text">These notes appear on the printed/emailed invoice.</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label asp-for="InternalNotes" class="form-label fw-semibold">Internal Notes</label>
|
||||
<textarea asp-for="InternalNotes" class="form-control" rows="2"
|
||||
placeholder="Internal notes (not visible to customer)..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: Totals & Actions -->
|
||||
<div class="col-lg-4">
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header border-0 py-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h6 class="mb-0"><i class="bi bi-calculator me-2"></i>Totals</h6>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Totals"
|
||||
data-bs-content="Subtotal = sum of all line item totals. Discount is a flat dollar amount deducted before tax. Tax % is applied to (Subtotal − Discount). Both default from the company settings but can be overridden for this invoice.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Subtotal</span>
|
||||
<span id="displaySubtotal" class="fw-semibold">$0.00</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<label class="form-label mb-0 text-muted">Discount ($)</label>
|
||||
</div>
|
||||
<input asp-for="DiscountAmount" type="number" class="form-control form-control-sm text-end"
|
||||
min="0" step="0.01" oninput="recalcTotals()" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<label class="form-label mb-0 text-muted">Tax (%)</label>
|
||||
</div>
|
||||
<input asp-for="TaxPercent" type="number" class="form-control form-control-sm text-end"
|
||||
min="0" max="100" step="0.01" oninput="recalcTotals()" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="text-muted small">Tax Amount</span>
|
||||
<span id="displayTax" class="small">$0.00</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="fw-bold fs-5">Total</span>
|
||||
<span id="displayTotal" class="fw-bold fs-5 text-primary">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body d-grid gap-2">
|
||||
@if (canResend)
|
||||
{
|
||||
<div class="form-check mb-1">
|
||||
<input class="form-check-input" type="checkbox" name="resendToCustomer" value="true" id="resendCheck" />
|
||||
<label class="form-check-label small" for="resendCheck">
|
||||
<i class="bi bi-send me-1"></i>Re-send updated invoice to customer
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle me-2"></i>Save Changes
|
||||
</button>
|
||||
<a asp-action="Details" asp-route-id="@invoiceId" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-circle me-1"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-footer border-0 pt-0">
|
||||
<div class="alert alert-info alert-permanent mb-0 small py-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Draft, Sent,</strong> and <strong>Overdue</strong> invoices can be edited.
|
||||
Paid invoices are locked.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
let itemCount = @Model.InvoiceItems.Count;
|
||||
|
||||
function addLineItem() {
|
||||
const idx = itemCount++;
|
||||
const tbody = document.getElementById('lineItemsBody');
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'line-item-row';
|
||||
row.dataset.index = idx;
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<input type="hidden" name="InvoiceItems[${idx}].SourceJobItemId" value="" />
|
||||
<input type="hidden" name="InvoiceItems[${idx}].DisplayOrder" class="display-order-input" value="${idx + 1}" />
|
||||
<input type="hidden" name="InvoiceItems[${idx}].RevenueAccountId" value="" />
|
||||
<input type="text" name="InvoiceItems[${idx}].Description"
|
||||
class="form-control form-control-sm" placeholder="Description" required />
|
||||
<input type="text" name="InvoiceItems[${idx}].Notes"
|
||||
class="form-control form-control-sm mt-1" placeholder="Notes (optional)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="InvoiceItems[${idx}].Quantity"
|
||||
class="form-control form-control-sm text-center qty-input"
|
||||
value="1" min="0.01" step="0.01"
|
||||
onchange="recalcRow(this)" oninput="recalcRow(this)" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="InvoiceItems[${idx}].UnitPrice"
|
||||
class="form-control form-control-sm text-end unit-price-input"
|
||||
value="0.00" min="0" step="0.01"
|
||||
onchange="recalcRow(this)" oninput="recalcRow(this)" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="InvoiceItems[${idx}].TotalPrice"
|
||||
class="form-control form-control-sm text-end total-price-input"
|
||||
value="0.00" min="0" step="0.01"
|
||||
oninput="recalcTotals()" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="text" name="InvoiceItems[${idx}].ColorName"
|
||||
class="form-control form-control-sm" placeholder="Color" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeLineItem(this)" title="Remove">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>`;
|
||||
tbody.appendChild(row);
|
||||
document.getElementById('noItemsMessage').classList.add('d-none');
|
||||
reindexRows();
|
||||
recalcTotals();
|
||||
row.querySelector('input[name$="].Description"]').focus();
|
||||
}
|
||||
|
||||
function removeLineItem(btn) {
|
||||
const row = btn.closest('tr');
|
||||
row.remove();
|
||||
reindexRows();
|
||||
recalcTotals();
|
||||
const tbody = document.getElementById('lineItemsBody');
|
||||
if (tbody.querySelectorAll('tr.line-item-row').length === 0)
|
||||
document.getElementById('noItemsMessage').classList.remove('d-none');
|
||||
}
|
||||
|
||||
function reindexRows() {
|
||||
const rows = document.querySelectorAll('#lineItemsBody tr.line-item-row');
|
||||
rows.forEach((row, i) => {
|
||||
row.dataset.index = i;
|
||||
row.querySelectorAll('input, select, textarea').forEach(input => {
|
||||
if (input.name) {
|
||||
input.name = input.name.replace(/InvoiceItems\[\d+\]/, `InvoiceItems[${i}]`);
|
||||
}
|
||||
});
|
||||
const orderInput = row.querySelector('.display-order-input');
|
||||
if (orderInput) orderInput.value = i + 1;
|
||||
});
|
||||
itemCount = rows.length;
|
||||
}
|
||||
|
||||
function recalcRow(input) {
|
||||
const row = input.closest('tr');
|
||||
const qty = parseFloat(row.querySelector('.qty-input')?.value) || 0;
|
||||
const unit = parseFloat(row.querySelector('.unit-price-input')?.value) || 0;
|
||||
const totalInput = row.querySelector('.total-price-input');
|
||||
if (totalInput) totalInput.value = (qty * unit).toFixed(2);
|
||||
recalcTotals();
|
||||
}
|
||||
|
||||
function recalcTotals() {
|
||||
let subtotal = 0;
|
||||
document.querySelectorAll('#lineItemsBody .total-price-input').forEach(input => {
|
||||
subtotal += parseFloat(input.value) || 0;
|
||||
});
|
||||
|
||||
const discount = parseFloat(document.getElementById('DiscountAmount')?.value) || 0;
|
||||
const taxPct = parseFloat(document.getElementById('TaxPercent')?.value) || 0;
|
||||
const taxableAmount = subtotal - discount;
|
||||
const tax = Math.round(taxableAmount * taxPct) / 100;
|
||||
const total = taxableAmount + tax;
|
||||
|
||||
document.getElementById('displaySubtotal').textContent = formatCurrency(subtotal);
|
||||
document.getElementById('displayTax').textContent = formatCurrency(tax);
|
||||
document.getElementById('displayTotal').textContent = formatCurrency(total);
|
||||
}
|
||||
|
||||
function formatCurrency(value) {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value);
|
||||
}
|
||||
|
||||
recalcTotals();
|
||||
</script>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user