}
@@ -63,7 +65,8 @@
@{
var _activeGroup = ViewBag.StatusGroup as string;
var _activeSearch = ViewBag.SearchTerm as string;
- var _noFilter = string.IsNullOrEmpty(_activeGroup) && string.IsNullOrEmpty(_activeSearch) && string.IsNullOrEmpty(ViewBag.TagFilter as string);
+ // "all" is the explicit show-everything group (bare URL now redirects to "active")
+ var _noFilter = _activeGroup == "all" && string.IsNullOrEmpty(_activeSearch) && string.IsNullOrEmpty(ViewBag.TagFilter as string);
}
From 9b34ff564e6368a212ef1f1cdabc56004d4be55c Mon Sep 17 00:00:00 2001
From: Scott Pouliot
Date: Tue, 19 May 2026 20:15:58 -0400
Subject: [PATCH 03/22] Update AI assistant and help docs for recent changes
HelpKnowledgeBase:
- Jobs list: document On Floor default view and 5 filter pills (All/On Floor/Overdue/Ready/Completed) with global counts
- Creating a job: add Oven & Batch Settings step
- Completing a job: new entry explaining Complete Job modal, per-color powder grouping, QR scan credit
- Invoice from job: note that coat colors appear in line item descriptions
Help/Jobs.cshtml:
- Overview: mention On Floor default and filter pills
- Creating a job: add oven/batch settings step in the numbered list
- New "Completing a Job" section: modal fields, powder grouping by color, QR credit, SMS behavior
- Invoice from job step: mention coat color in line item descriptions
- Add "Completing a Job" to page nav
Co-Authored-By: Claude Sonnet 4.6
---
.../Helpers/HelpKnowledgeBase.cs | 9 +++-
src/PowderCoating.Web/Views/Help/Jobs.cshtml | 52 +++++++++++++++++--
2 files changed, 55 insertions(+), 6 deletions(-)
diff --git a/src/PowderCoating.Web/Helpers/HelpKnowledgeBase.cs b/src/PowderCoating.Web/Helpers/HelpKnowledgeBase.cs
index 409baec..79d09c2 100644
--- a/src/PowderCoating.Web/Helpers/HelpKnowledgeBase.cs
+++ b/src/PowderCoating.Web/Helpers/HelpKnowledgeBase.cs
@@ -265,12 +265,15 @@ public static class HelpKnowledgeBase
**Job Priorities (color-coded):**
- Low (grey), Normal (blue), High (orange), Urgent (red), Rush (purple)
+ **Jobs list default view:** The Jobs list opens on the **On Floor** filter by default — showing only active jobs (excludes Completed, Ready for Pickup, Delivered, Cancelled). Use the filter pills at the top to switch views: **All** shows every job regardless of status; **On Floor** shows active work; **Overdue** shows past-due active jobs; **Ready** shows jobs awaiting customer pickup; **Completed** shows all finished jobs (Completed + Ready for Pickup + Delivered). Each pill shows a live global count.
+
**How to create a job:**
1. Go to [Jobs](/Jobs) → "New Job"
2. Select customer
3. Add line items (same wizard as quotes: Calculated, Custom Work, or AI Photo)
4. Set priority, due date, assigned worker, special instructions
- 5. Save
+ 5. Optionally set Oven & Batch Settings — select a named oven, number of batches, and cycle time. These affect the oven cost in pricing.
+ 6. Save
**Job Priority Board:** [/JobsPriority](/JobsPriority) — Kanban-style view of all active jobs sorted by priority and status.
@@ -302,7 +305,9 @@ public static class HelpKnowledgeBase
**Changing the customer on a job:** On the Job Details page, the Customer field is an always-visible dropdown. Select a different customer — a confirmation banner appears. Click **Save** to apply or **Cancel** to revert. Use this to correct a misassigned job or to move a walk-in job to a customer's proper record after they've been added to the system.
- **Creating an invoice from a job:** On the Job Details page, look for the Invoice section and click "Create Invoice." The system pre-fills all line items, pricing, discount, tax rate, payment terms, and due date from the job and customer automatically. Review the Totals panel on the right — if a discount was applied to the job it will show as a red "Discount Applied" line. Adjust anything you need, then save.
+ **Completing a job:** When a job is ready to mark complete, click the **Complete Job** button on the Job Details page. A modal appears asking you to confirm the completion date, actual hours spent, and final price. If the job used powder from inventory, you will be asked to enter the actual lbs used — the modal groups all coats by unique powder color (not per coat or per item) so you fill in one quantity per powder. The system deducts the entered amounts from inventory, crediting any quantities already logged via QR scan. Once confirmed, the job advances to Completed status, and you are prompted to create the invoice if one does not exist.
+
+ **Creating an invoice from a job:** On the Job Details page, look for the Invoice section and click "Create Invoice." The system pre-fills all line items, pricing, discount, tax rate, payment terms, and due date from the job and customer automatically. Line item descriptions include the coat color(s) for each item (e.g., "Color1 / Color2" if multiple coats), helping customers distinguish repeated items on the invoice. Review the Totals panel on the right — if a discount was applied to the job it will show as a red "Discount Applied" line. Adjust anything you need, then save.
**Work Order QR Codes:** Every printed job work order includes two tiers of QR codes — one for viewing the job, and a separate set for taking action on it. All QR codes require the worker to be logged in.
diff --git a/src/PowderCoating.Web/Views/Help/Jobs.cshtml b/src/PowderCoating.Web/Views/Help/Jobs.cshtml
index 3dbe603..fb66bd8 100644
--- a/src/PowderCoating.Web/Views/Help/Jobs.cshtml
+++ b/src/PowderCoating.Web/Views/Help/Jobs.cshtml
@@ -25,9 +25,13 @@
a priority level, a due date, and one or more line items describing the work to be performed.
- You can find Jobs under Operations › Jobs in the left sidebar. The list is
- searchable and sortable by job number, status, priority, scheduled date, due date, and price. Jobs
- can be created manually or converted automatically from an approved quote — no need to re-enter
+ You can find Jobs under Operations › Jobs in the left sidebar. The list
+ opens on the On Floor filter by default, showing only active in-progress work
+ so completed jobs don’t clutter the screen. Use the filter pills at the top to switch
+ views: All, On Floor, Overdue,
+ Ready (awaiting pickup), and Completed. The list is sortable
+ and searchable by job number, status, priority, scheduled date, due date, and price. Jobs can be
+ created manually or converted automatically from an approved quote — no need to re-enter
information that is already in the system.
@@ -54,6 +58,11 @@
Enter the customer's PO Number if they require one for their own records.
Add any Special Instructions your team needs to know before starting work.
Add one or more Line Items describing each piece being coated. See the Job Items section below.
+
+ Optionally expand Oven & Batch Settings to assign a named oven, set the
+ number of batches, and enter the cure cycle time. These values feed directly into the oven cost
+ calculation so the job’s pricing reflects the actual oven run.
+
Click Save Job.
@@ -353,7 +362,7 @@
If an invoice already exists, you will see a link to open it.
-
Click Create Invoice. The system generates a new invoice pre-filled with all the job's line items and the final pricing.
+
Click Create Invoice. The system generates a new invoice pre-filled with all the job’s line items and the final pricing. Line item descriptions include the coat color(s) for each item (e.g., Gloss Black / Satin Clear), which helps customers distinguish repeated items such as multiple sets of calipers.
Review the invoice, confirm the due date, and save it.
@@ -370,6 +379,40 @@
+
+
+ Completing a Job
+
+
+ When work is done and the parts have passed quality check, click the Complete Job
+ button on the Job Details page. A modal opens where you confirm:
+
+
+
Completion date — defaults to today.
+
Actual hours spent on the job.
+
Final price — pre-filled from the job; adjust if needed.
+
+ Powder usage — if the job used powder from inventory, you are asked
+ to enter actual lbs used. The modal groups all coats by unique powder color and
+ shows one input row per powder, regardless of how many items or coats used that color.
+ This avoids entering the same number repeatedly. Any lbs already scanned via QR code on
+ the shop floor are credited automatically — you only enter the remaining amount.
+
+
+
+ Once confirmed, the job advances to Completed status,
+ inventory is updated, and you are prompted to create an invoice if one does not already exist.
+
+
+
+
+ Shop floor workers who complete a job via QR scan bypass the modal — the SMS
+ notification is sent immediately using the configured template. Managers and admins
+ get the full modal with a compose step before the SMS goes out.
+
+
+
+
Photos and Notes
@@ -699,6 +742,7 @@
Job ItemsConverting from a QuoteCreating an Invoice
+ Completing a JobPhotos and NotesTime Entries and ReworkJob Templates
From 7fa385aeb80a3942c32674a019be9a46c47b9e32 Mon Sep 17 00:00:00 2001
From: Scott Pouliot
Date: Wed, 20 May 2026 11:49:04 -0400
Subject: [PATCH 04/22] Inline item editing on details pages; fix Stripe
receipt_email
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Allow description, quantity, and price to be edited inline on Quote,
Job, and Invoice details pages without re-opening the wizard. Coating
and prep service rows remain read-only by design. Invoice editing is
gated to Draft/Sent/Overdue statuses; totals update live in the DOM.
Remove receipt_email from Stripe PaymentIntent creation so customers
can use any email they choose at checkout — Stripe validates format
and sends the receipt to whatever the customer enters in the Payment
Element, eliminating the risk of a stored email mismatch blocking a
payment from processing.
Co-Authored-By: Claude Sonnet 4.6
---
.../Interfaces/IStripeConnectService.cs | 2 -
.../Services/StripeConnectService.cs | 4 -
.../Controllers/InvoicesController.cs | 52 ++++++
.../Controllers/JobsController.cs | 60 +++++++
.../Controllers/PaymentController.cs | 5 -
.../Controllers/QuotesController.cs | 51 ++++++
.../Views/Invoices/Details.cshtml | 33 ++--
.../Views/Jobs/Details.cshtml | 45 +++--
.../Views/Quotes/Details.cshtml | 38 ++--
src/PowderCoating.Web/dotnet-tools.json | 2 +-
src/PowderCoating.Web/wwwroot/css/site.css | 11 ++
.../wwwroot/js/inline-item-edit.js | 167 ++++++++++++++++++
12 files changed, 418 insertions(+), 52 deletions(-)
create mode 100644 src/PowderCoating.Web/wwwroot/js/inline-item-edit.js
diff --git a/src/PowderCoating.Application/Interfaces/IStripeConnectService.cs b/src/PowderCoating.Application/Interfaces/IStripeConnectService.cs
index 9b7d162..432f48c 100644
--- a/src/PowderCoating.Application/Interfaces/IStripeConnectService.cs
+++ b/src/PowderCoating.Application/Interfaces/IStripeConnectService.cs
@@ -20,7 +20,6 @@ public interface IStripeConnectService
decimal invoiceTotal,
decimal surchargeAmount,
string currency,
- string customerEmail,
string invoiceNumber,
int invoiceId);
@@ -33,7 +32,6 @@ public interface IStripeConnectService
decimal depositAmount,
decimal surchargeAmount,
string currency,
- string customerEmail,
string quoteNumber,
int quoteId);
}
diff --git a/src/PowderCoating.Infrastructure/Services/StripeConnectService.cs b/src/PowderCoating.Infrastructure/Services/StripeConnectService.cs
index 4034057..caf1078 100644
--- a/src/PowderCoating.Infrastructure/Services/StripeConnectService.cs
+++ b/src/PowderCoating.Infrastructure/Services/StripeConnectService.cs
@@ -162,7 +162,6 @@ public class StripeConnectService : IStripeConnectService
decimal invoiceTotal,
decimal surchargeAmount,
string currency,
- string customerEmail,
string invoiceNumber,
int invoiceId)
{
@@ -175,7 +174,6 @@ public class StripeConnectService : IStripeConnectService
{
Amount = amountInCents,
Currency = currency.ToLower(),
- ReceiptEmail = customerEmail,
Description = $"Invoice {invoiceNumber}",
Metadata = new Dictionary
{
@@ -215,7 +213,6 @@ public class StripeConnectService : IStripeConnectService
decimal depositAmount,
decimal surchargeAmount,
string currency,
- string customerEmail,
string quoteNumber,
int quoteId)
{
@@ -228,7 +225,6 @@ public class StripeConnectService : IStripeConnectService
{
Amount = amountInCents,
Currency = currency.ToLower(),
- ReceiptEmail = customerEmail,
Description = $"Deposit for quote {quoteNumber}",
Metadata = new Dictionary
{
diff --git a/src/PowderCoating.Web/Controllers/InvoicesController.cs b/src/PowderCoating.Web/Controllers/InvoicesController.cs
index 8a6d39e..d6ad3ab 100644
--- a/src/PowderCoating.Web/Controllers/InvoicesController.cs
+++ b/src/PowderCoating.Web/Controllers/InvoicesController.cs
@@ -3035,6 +3035,50 @@ public class InvoicesController : Controller
return false;
}
+ ///
+ /// Inline-edits description, quantity, and unit price on a single invoice line item.
+ /// Blocked on paid/voided invoices (same gate as the full Edit action).
+ /// Returns updated totals so the page can reflect the change without a reload.
+ ///
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task PatchItem([FromBody] PatchInvoiceItemRequest request)
+ {
+ var currentUser = await _userManager.GetUserAsync(User);
+ if (currentUser == null) return Unauthorized();
+
+ var item = await _unitOfWork.InvoiceItems.GetByIdAsync(request.ItemId);
+ if (item == null) return NotFound();
+
+ var invoice = await _unitOfWork.Invoices.GetByIdAsync(item.InvoiceId);
+ if (invoice == null || invoice.CompanyId != currentUser.CompanyId) return NotFound();
+
+ if (invoice.Status is not (InvoiceStatus.Draft or InvoiceStatus.Sent or InvoiceStatus.Overdue))
+ return BadRequest(new { error = "Cannot edit items on a paid or voided invoice." });
+
+ item.Description = request.Description.Trim();
+ item.Quantity = request.Quantity;
+ item.UnitPrice = request.UnitPrice;
+ item.TotalPrice = Math.Round(request.Quantity * request.UnitPrice, 2);
+ await _unitOfWork.InvoiceItems.UpdateAsync(item);
+
+ var allItems = await _unitOfWork.InvoiceItems.FindAsync(ii => ii.InvoiceId == invoice.Id);
+ var newSubTotal = allItems.Sum(i => i.TotalPrice);
+ invoice.SubTotal = newSubTotal;
+ invoice.TaxAmount = Math.Round(newSubTotal * invoice.TaxPercent / 100m, 2);
+ invoice.Total = Math.Round(newSubTotal - invoice.DiscountAmount + invoice.TaxAmount, 2);
+ await _unitOfWork.Invoices.UpdateAsync(invoice);
+ await _unitOfWork.CompleteAsync();
+
+ return Json(new {
+ lineTotal = item.TotalPrice,
+ subtotal = invoice.SubTotal,
+ taxAmount = invoice.TaxAmount,
+ total = invoice.Total,
+ balanceDue = invoice.BalanceDue
+ });
+ }
+
///
/// Returns logo bytes and content type for PDF generation.
/// Prefers blob-stored logos (LogoFilePath) over the legacy DB column (LogoData).
@@ -3050,3 +3094,11 @@ public class InvoicesController : Controller
return (company.LogoData, company.LogoContentType);
}
}
+
+public class PatchInvoiceItemRequest
+{
+ public int ItemId { get; set; }
+ public string Description { get; set; } = string.Empty;
+ public decimal Quantity { get; set; }
+ public decimal UnitPrice { get; set; }
+}
diff --git a/src/PowderCoating.Web/Controllers/JobsController.cs b/src/PowderCoating.Web/Controllers/JobsController.cs
index a09a1e0..2582725 100644
--- a/src/PowderCoating.Web/Controllers/JobsController.cs
+++ b/src/PowderCoating.Web/Controllers/JobsController.cs
@@ -4216,9 +4216,69 @@ public class JobsController : Controller
return Json(new { success = false, message = "An error occurred. Please try again." });
}
}
+
+ ///
+ /// Inline-edits description, quantity, and unit price on a single job line item.
+ /// Adjusts FinalPrice and the stored PricingBreakdownJson snapshot by the price delta.
+ /// Returns updated totals so the page can reflect the change without a reload.
+ ///
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task PatchItem([FromBody] PatchJobItemRequest request)
+ {
+ var currentUser = await _userManager.GetUserAsync(User);
+ if (currentUser == null) return Unauthorized();
+
+ var item = await _unitOfWork.JobItems.GetByIdAsync(request.ItemId);
+ if (item == null) return NotFound();
+
+ var job = await _unitOfWork.Jobs.GetByIdAsync(item.JobId);
+ if (job == null || job.CompanyId != currentUser.CompanyId) return NotFound();
+
+ var oldTotal = item.TotalPrice;
+ item.Description = request.Description.Trim();
+ item.Quantity = request.Quantity;
+ item.UnitPrice = request.UnitPrice;
+ item.TotalPrice = Math.Round(request.Quantity * request.UnitPrice, 2);
+ await _unitOfWork.JobItems.UpdateAsync(item);
+
+ var delta = item.TotalPrice - oldTotal;
+ job.FinalPrice = Math.Round(job.FinalPrice + delta, 2);
+
+ // Keep the stored pricing snapshot in sync so the breakdown panel stays consistent
+ if (!string.IsNullOrEmpty(job.PricingBreakdownJson))
+ {
+ var pb = JsonSerializer.Deserialize(job.PricingBreakdownJson);
+ if (pb != null)
+ {
+ pb.ItemsSubtotal += delta;
+ pb.SubtotalBeforeDiscount += delta;
+ pb.SubtotalAfterDiscount = pb.SubtotalBeforeDiscount - pb.DiscountAmount;
+ pb.TaxAmount = Math.Round(pb.SubtotalAfterDiscount * pb.TaxPercent / 100m, 2);
+ pb.Total = Math.Round(pb.SubtotalAfterDiscount + pb.RushFee + pb.TaxAmount, 2);
+ job.FinalPrice = pb.Total;
+ job.PricingBreakdownJson = JsonSerializer.Serialize(pb);
+ }
+ }
+
+ await _unitOfWork.Jobs.UpdateAsync(job);
+ await _unitOfWork.CompleteAsync();
+
+ return Json(new {
+ lineTotal = item.TotalPrice,
+ finalPrice = job.FinalPrice
+ });
+ }
}
public class DeleteTimeEntryRequest { public int Id { get; set; } }
+public class PatchJobItemRequest
+{
+ public int ItemId { get; set; }
+ public string Description { get; set; } = string.Empty;
+ public decimal Quantity { get; set; }
+ public decimal UnitPrice { get; set; }
+}
public class LogMaterialRequest
{
public int JobId { get; set; }
diff --git a/src/PowderCoating.Web/Controllers/PaymentController.cs b/src/PowderCoating.Web/Controllers/PaymentController.cs
index abfd89e..9e2b8be 100644
--- a/src/PowderCoating.Web/Controllers/PaymentController.cs
+++ b/src/PowderCoating.Web/Controllers/PaymentController.cs
@@ -127,8 +127,6 @@ public class PaymentController : Controller
return BadRequest(new { error = "Invalid payment amount." });
var surcharge = CalculateSurcharge(request.Amount, company!);
- var customer = await _context.Customers.AsNoTracking()
- .FirstOrDefaultAsync(c => c.Id == invoice.CustomerId);
var (success, clientSecret, paymentIntentId, stripeError) =
await _stripeConnect.CreatePaymentIntentAsync(
@@ -136,7 +134,6 @@ public class PaymentController : Controller
invoiceTotal: request.Amount,
surchargeAmount: surcharge,
currency: "usd",
- customerEmail: customer?.Email ?? string.Empty,
invoiceNumber: invoice.InvoiceNumber,
invoiceId: invoice.Id);
@@ -296,7 +293,6 @@ public class PaymentController : Controller
var depositAmount = Math.Round(quote!.Total * (quote.DepositPercent / 100m), 2);
var surcharge = CalculateSurcharge(depositAmount, company!);
- var customerEmail = quote.Customer?.Email ?? quote.ProspectEmail ?? string.Empty;
var (success, clientSecret, paymentIntentId, stripeError) =
await _stripeConnect.CreateDepositPaymentIntentAsync(
@@ -304,7 +300,6 @@ public class PaymentController : Controller
depositAmount: depositAmount,
surchargeAmount: surcharge,
currency: "usd",
- customerEmail: customerEmail,
quoteNumber: quote.QuoteNumber,
quoteId: quote.Id);
diff --git a/src/PowderCoating.Web/Controllers/QuotesController.cs b/src/PowderCoating.Web/Controllers/QuotesController.cs
index 986ace2..879cd47 100644
--- a/src/PowderCoating.Web/Controllers/QuotesController.cs
+++ b/src/PowderCoating.Web/Controllers/QuotesController.cs
@@ -3824,6 +3824,49 @@ public class QuotesController : Controller
}
return (company.LogoData, company.LogoContentType);
}
+
+ ///
+ /// Inline-edits description, quantity, and unit price on a single quote line item.
+ /// Adjusts stored quote totals by the price delta so the sidebar stays accurate.
+ /// Returns updated totals so the page can reflect the change without a reload.
+ ///
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task PatchItem([FromBody] PatchQuoteItemRequest request)
+ {
+ var currentUser = await _userManager.GetUserAsync(User);
+ if (currentUser == null) return Unauthorized();
+
+ var item = await _unitOfWork.QuoteItems.GetByIdAsync(request.ItemId);
+ if (item == null) return NotFound();
+
+ var quote = await _unitOfWork.Quotes.GetByIdAsync(item.QuoteId);
+ if (quote == null || quote.CompanyId != currentUser.CompanyId) return NotFound();
+
+ var oldTotal = item.TotalPrice;
+ item.Description = request.Description.Trim();
+ item.Quantity = request.Quantity;
+ item.UnitPrice = request.UnitPrice;
+ item.TotalPrice = Math.Round(request.Quantity * request.UnitPrice, 2);
+ await _unitOfWork.QuoteItems.UpdateAsync(item);
+
+ // Cascade delta through stored totals without re-running the pricing engine
+ var delta = item.TotalPrice - oldTotal;
+ quote.ItemsSubtotal += delta;
+ quote.SubTotal += delta;
+ quote.SubtotalAfterDiscount = quote.SubTotal - quote.DiscountAmount;
+ quote.TaxAmount = Math.Round(quote.SubtotalAfterDiscount * quote.TaxPercent / 100m, 2);
+ quote.Total = Math.Round(quote.SubtotalAfterDiscount + quote.RushFee + quote.TaxAmount, 2);
+ await _unitOfWork.Quotes.UpdateAsync(quote);
+ await _unitOfWork.CompleteAsync();
+
+ return Json(new {
+ lineTotal = item.TotalPrice,
+ subtotal = quote.SubTotal,
+ taxAmount = quote.TaxAmount,
+ total = quote.Total
+ });
+ }
}
// Request model for AJAX pricing calculation
@@ -3834,3 +3877,11 @@ public class UpdateQuoteStatusRequest
public int QuoteId { get; set; }
public int StatusId { get; set; }
}
+
+public class PatchQuoteItemRequest
+{
+ public int ItemId { get; set; }
+ public string Description { get; set; } = string.Empty;
+ public decimal Quantity { get; set; }
+ public decimal UnitPrice { get; set; }
+}
diff --git a/src/PowderCoating.Web/Views/Invoices/Details.cshtml b/src/PowderCoating.Web/Views/Invoices/Details.cshtml
index f301a80..3079024 100644
--- a/src/PowderCoating.Web/Views/Invoices/Details.cshtml
+++ b/src/PowderCoating.Web/Views/Invoices/Details.cshtml
@@ -230,21 +230,21 @@
@foreach (var item in Model.InvoiceItems)
{
-
+ placeholder="Examples:
• We specialise in automotive restoration — wheels, frames, suspension brackets, and roll cages are our bread and butter.
• Our customers expect premium pricing. We rarely work on items over 20 sqft.
• Most items come to us already stripped; sandblasting adds roughly 15 min per item on average.
• We use a 2-stage cure cycle — pre-heat 10 min, coat, cure 20 min at 400°F.">@(Model.OperatingCosts?.AiContextProfile)
- Plain language — write it as if briefing a new estimator on your shop.
+ Plain language — write it as if briefing a new estimator on your shop.
@(Model.OperatingCosts?.AiContextProfile?.Length ?? 0)/2000
@@ -832,7 +832,7 @@
Save AI Profile
@@ -843,9 +843,9 @@
How AI Learning Works
-
Layer 1 — Pricing config: Your operating costs (labor, equipment, markup) are always injected automatically.
-
Layer 2 — Your shop profile: The description you write here is added to every AI analysis, guiding estimates toward your typical work.
-
Layer 3 — Automatic learning: Each time your team accepts an AI estimate without changing it, that item is silently added as a calibration example. The AI improves on its own the more you use it.
+
Layer 1 — Pricing config: Your operating costs (labor, equipment, markup) are always injected automatically.
+
Layer 2 — Your shop profile: The description you write here is added to every AI analysis, guiding estimates toward your typical work.
+
Layer 3 — Automatic learning: Each time your team accepts an AI estimate without changing it, that item is silently added as a calibration example. The AI improves on its own the more you use it.
@@ -874,10 +874,10 @@
Used by the AI when estimating job complexity and throughput.
Printed in italic at the bottom of every blank work order. Supports plain text — use * or ** for visual emphasis.
+
Printed in italic at the bottom of every blank work order. Supports plain text — use * or ** for visual emphasis.
@@ -2168,7 +2168,7 @@
- SMS Notifications — Terms of Service
+ SMS Notifications — Terms of Service
@@ -2178,9 +2178,9 @@
1. Prior Express Written Consent Required
-
You must obtain clear, documented consent from each customer before sending them any SMS message. This means each customer must have explicitly agreed — in writing or through a recorded digital interaction — that they wish to receive text messages from your business. Enabling this feature is not consent on their behalf. You must collect and record their authorization individually, before enabling SMS for their account in this system.
+
You must obtain clear, documented consent from each customer before sending them any SMS message. This means each customer must have explicitly agreed — in writing or through a recorded digital interaction — that they wish to receive text messages from your business. Enabling this feature is not consent on their behalf. You must collect and record their authorization individually, before enabling SMS for their account in this system.
-
2. Federal Law Governs SMS — Fines Are Real
+
2. Federal Law Governs SMS — Fines Are Real
The Telephone Consumer Protection Act (TCPA), enforced by the Federal Communications Commission (FCC), imposes fines of $500 to $1,500 per individual message sent without proper authorization. These fines apply per text, not per customer. A single campaign to 100 unconsented recipients could result in exposure of $50,000 to $150,000. The FCC and private plaintiffs both actively pursue TCPA violations.
3. Opt-Out Requests Must Be Honored Immediately
@@ -2189,7 +2189,7 @@
4. Message Rates & Content Restrictions
Every message sent must include your business name and an opt-out reminder (e.g., "Reply STOP to opt out"). Messages must be directly relevant to the service the customer consented to receive and must not contain solicitations, promotions, or third-party offers unless the customer has separately consented to those.
-
5. Your Responsibility — Not Ours
+
5. Your Responsibility — Not Ours
Powder Coating Logix provides this feature as a communication tool only. We are not responsible for how you use it. You agree that your company is solely responsible for obtaining proper consent, maintaining records of that consent, honoring opt-outs, and ensuring all outbound messages comply with the TCPA, FCC regulations, and any applicable state laws. You agree to indemnify and hold Powder Coating Logix harmless from any claims, fines, or damages arising from your company's use of SMS.
@@ -2201,7 +2201,7 @@
— navigate relative to the button
+ // Bootstrap teleports modals to — navigate relative to the button
const modalEl = btn ? btn.closest('.modal') : document.getElementById('gcModal');
const q = sel => modalEl ? modalEl.querySelector(sel) : document.querySelector(sel);
diff --git a/src/PowderCoating.Web/Views/Invoices/Details.cshtml b/src/PowderCoating.Web/Views/Invoices/Details.cshtml
index 30bd6e2..1b13309 100644
--- a/src/PowderCoating.Web/Views/Invoices/Details.cshtml
+++ b/src/PowderCoating.Web/Views/Invoices/Details.cshtml
@@ -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)ViewBag.AvailableCreditMemos).Any();
var canIssueRefund = !isDraft && !isVoided && Model.AmountPaid > 0;
var canApplyCredit = !isVoided && Model.BalanceDue > 0 && hasAvailableCredits;
@@ -80,7 +80,7 @@