Add invoice SMS notifications and customer intake kiosk
Invoice SMS:
- Send Invoice modal now prompts Email/SMS/Both based on customer contact data
- New /invoice/{token} customer-facing view page with full line items and pay button
- PublicViewToken (permanent) added to Invoice; separate from expiring PaymentLinkToken
- InvoiceSent SMS default template added; customizable via Notification Templates settings
- {{viewUrl}} placeholder documented in template editor
Customer Intake Kiosk:
- Tablet kiosk flow: Contact → Job → Terms/Signature → Confirmation
- Remote link mode for off-site customers (lighter form, no signature)
- KioskHub (AllowAnonymous SignalR) for staff-to-tablet push without login
- Staff activates tablet via cookie; sends remote link manually
- Submitted sessions create Customer + Job automatically; fires in-app notification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
@model InvoiceViewViewModel
|
||||
@using PowderCoating.Core.Enums
|
||||
@{
|
||||
Layout = "~/Views/Shared/_QuoteApprovalLayout.cshtml";
|
||||
ViewData["Title"] = $"Invoice {Model.InvoiceNumber}";
|
||||
var isPaid = Model.BalanceDue <= 0;
|
||||
}
|
||||
|
||||
<div class="container py-4" style="max-width:780px;">
|
||||
|
||||
@* ── Header ── *@
|
||||
<div class="text-center mb-4">
|
||||
@if (!string.IsNullOrEmpty(Model.LogoFilePath))
|
||||
{
|
||||
<img src="/media/@(Model.LogoFilePath.TrimStart('/'))" alt="@Model.CompanyName" style="max-height:80px;max-width:240px;object-fit:contain;" class="mb-3" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/images/pcl-logo.png" alt="@Model.CompanyName" style="max-height:60px;" class="mb-3" />
|
||||
}
|
||||
<h4 class="fw-semibold mb-0">@Model.CompanyName</h4>
|
||||
@if (!string.IsNullOrEmpty(Model.CompanyPhone))
|
||||
{
|
||||
<p class="text-muted small mb-0">@Model.CompanyPhone</p>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.CompanyAddress))
|
||||
{
|
||||
<p class="text-muted small">@Model.CompanyAddress</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ── Invoice meta ── *@
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<p class="text-muted small mb-1">Invoice</p>
|
||||
<p class="fw-semibold mb-0">@Model.InvoiceNumber</p>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<p class="text-muted small mb-1">Date</p>
|
||||
<p class="fw-semibold mb-0">@Model.InvoiceDate.ToString("MMM d, yyyy")</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p class="text-muted small mb-1">Bill To</p>
|
||||
<p class="fw-semibold mb-0">@Model.CustomerName</p>
|
||||
</div>
|
||||
@if (Model.DueDate.HasValue)
|
||||
{
|
||||
<div class="col-6 text-end">
|
||||
<p class="text-muted small mb-1">Due Date</p>
|
||||
<p class="fw-semibold mb-0 @(Model.DueDate < DateTime.UtcNow && !isPaid ? "text-danger" : "")">
|
||||
@Model.DueDate.Value.ToString("MMM d, yyyy")
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.JobNumber))
|
||||
{
|
||||
<div class="col-6">
|
||||
<p class="text-muted small mb-1">Job</p>
|
||||
<p class="fw-semibold mb-0">@Model.JobNumber</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Line items ── *@
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">Description</th>
|
||||
<th class="text-center" style="width:70px;">Qty</th>
|
||||
<th class="text-end" style="width:100px;">Unit Price</th>
|
||||
<th class="text-end pe-3" style="width:110px;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.LineItems)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3">@item.Description</td>
|
||||
<td class="text-center">@item.Quantity.ToString("G29")</td>
|
||||
<td class="text-end">@item.UnitPrice.ToString("C")</td>
|
||||
<td class="text-end pe-3">@item.TotalPrice.ToString("C")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Totals ── *@
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="text-muted">Subtotal</span>
|
||||
<span>@Model.SubTotal.ToString("C")</span>
|
||||
</div>
|
||||
@if (Model.DiscountAmount > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-1 text-success">
|
||||
<span>Discount</span>
|
||||
<span>-@Model.DiscountAmount.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.TaxAmount > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="text-muted">Tax (@Model.TaxPercent.ToString("0.##")%)</span>
|
||||
<span>@Model.TaxAmount.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
<hr class="my-2" />
|
||||
<div class="d-flex justify-content-between fw-semibold">
|
||||
<span>Total</span>
|
||||
<span>@Model.Total.ToString("C")</span>
|
||||
</div>
|
||||
@if (Model.AmountPaid > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between text-success mt-1">
|
||||
<span>Amount Paid</span>
|
||||
<span>-@Model.AmountPaid.ToString("C")</span>
|
||||
</div>
|
||||
<hr class="my-2" />
|
||||
<div class="d-flex justify-content-between fw-bold fs-5 @(isPaid ? "text-success" : "text-danger")">
|
||||
<span>Balance Due</span>
|
||||
<span>@Model.BalanceDue.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Pay button ── *@
|
||||
@if (!isPaid && !string.IsNullOrEmpty(Model.PaymentUrl))
|
||||
{
|
||||
<div class="text-center mb-4">
|
||||
<a href="@Model.PaymentUrl" class="btn btn-success btn-lg px-5">
|
||||
<i class="bi bi-credit-card me-2"></i>Pay @Model.BalanceDue.ToString("C") Online
|
||||
</a>
|
||||
<p class="text-muted small mt-2">Secure payment powered by Stripe. This pay link expires in 5 days.</p>
|
||||
</div>
|
||||
}
|
||||
else if (isPaid)
|
||||
{
|
||||
<div class="alert alert-success text-center" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>This invoice has been paid in full. Thank you!
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
<i class="bi bi-info-circle me-2"></i>To arrange payment, please contact @Model.CompanyName@(!string.IsNullOrEmpty(Model.CompanyPhone) ? $" at {Model.CompanyPhone}" : "").
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Notes / Terms ── *@
|
||||
@if (!string.IsNullOrEmpty(Model.Notes))
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1 fw-semibold">Notes</p>
|
||||
<p class="mb-0 small">@Model.Notes</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Terms))
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1 fw-semibold">Payment Terms</p>
|
||||
<p class="mb-0 small">@Model.Terms</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<p class="text-center text-muted small mt-4">
|
||||
Questions? Contact @Model.CompanyName@(!string.IsNullOrEmpty(Model.CompanyPhone) ? $" at {Model.CompanyPhone}" : "").
|
||||
</p>
|
||||
</div>
|
||||
Reference in New Issue
Block a user