QuotesController — ConvertToCustomer POST was wrongly setting the quote
status to 'Converted' (which means a job exists) and redirecting to the
customer page with no job created. The quote then disappeared from the
default list filter and the user had no way to create the job without
hunting for it. Fix: leave the quote at 'Approved' after customer
creation and redirect back to the quote details page with a toast
prompting the next step. 'Converted' status is now set exclusively by
CreateJobFromQuote when a job actually exists.
NotificationService — add tenant reply-to email address as a visible
line in the email footer so customers who ignore or whose mail client
doesn't honour the Reply-To header still have a clear address to contact.
Also adds Warning-level logging when no reply-to is configured for a
company so future routing issues are diagnosable from app logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logs a Warning when no Reply-To email is configured for a company
(so the logs show why replies land at the platform sender address)
and a Debug entry when one is set, making future send issues
diagnosable without needing the SendGrid Activity API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Appointment reminders: add AppointmentReminderBackgroundService (60s poll), ReminderSentAt
dedup stamp, NotifyAppointmentReminderAsync sends both customer email and creator staff email;
AppointmentReminderStaff notification type + default template added; DateTime.Now used instead
of UtcNow to match locally-stored ScheduledStartTime; ToLocalTime() double-conversion removed
- NoExtraLayerCharge not persisted: flag existed on CreateQuoteItemCoatDto and was used by
pricing engine but never written to JobItemCoat/QuoteItemCoat entities — every edit reset it
to false and re-applied the extra layer charge; added column to both entities (migration
AddNoExtraLayerChargeToCoats), both read DTOs, all 3 JobItemAssemblyService overloads,
JobItemCoatSeed inner class, and existingItemsData JSON in all 5 wizard views; fixed JS
template path that hard-coded noExtraLayerCharge: false
- Coat notes not visible: notes were rendered in desktop job details but missing from the wizard
item card summary and the mobile card view; both fixed
- Scroll position lost on item save: sessionStorage save/restore added to item-wizard.js owner
form submit handler; path-keyed so cross-page navigation does not restore stale position;
requestAnimationFrame used for reliable mobile scroll restoration
- Invoice Send dead button: #sendChannelModal was gated inside @if (isDraft) but the button
targeting it fires for Sent/Overdue invoices too when customer has both email and SMS; modal
moved outside the Draft guard
- InitialCreate migration added for fresh database installs; Baseline migration guarded with
IF OBJECT_ID check so it no-ops on fresh DBs; Razor scoping bug fixed in Customers/Index.cshtml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Customer entity + DTO: new BillingEmail field (accounting/invoicing address)
- Email fields now accept comma-separated lists; DTO validates each address individually
- NotificationService: SendToEmailListAsync helper fans out to all addresses in a list;
NotifyQuoteSentAsync accepts optional overrideEmail so staff can send to an ad-hoc address
- Migration: AddCustomerBillingEmail
- Customer Create/Edit/Details views updated to show Billing Email field
- customer-billing-email.js: client-side helpers for billing email input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QuoteDeclinedByCustomer was used for both approve and decline responses,
so approval notifications showed the wrong type in the log. Added a distinct
QuoteApprovedByCustomer = 16 enum value, wired up the correct type in
NotificationService, added default templates in both the service fallback
dictionary and SeedData, and updated placeholder hints in CompanySettings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a customizable QuoteSent SMS template to seed data and
DefaultTemplates so companies can edit the quote approval message
from Notification Templates. Wires NotifyQuoteSentSmsAsync to use
the template system instead of a hardcoded string. Updates
SmsConsentConfirmation wording to mention quote approvals alongside
job updates. Help docs and AI knowledge base updated to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'Send Quote via SMS' button on quote details page that sends the approval
link to the customer via SMS (respects NotifyBySms, handles prospects via ProspectPhone)
- Reuses existing valid approval token rather than regenerating, so a previously
emailed link stays valid when SMS is also sent
- Fix Twilio appsettings.json placeholders (real credentials moved to gitignored
appsettings.Development.json)
- Fix passkey login ignoring ReturnUrl: biometric login on the login page now
respects the form's ReturnUrl hidden field so QR-code and deep-link flows
redirect correctly after authentication instead of always going to the dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Three-tier SMS gate: platform kill-switch → admin force-disable → plan AllowSms → company opt-in
- CompanySmsAgreement entity records admin acceptance of TCPA terms with IP, user agent, and terms version
- SMS terms of service modal on Company Settings with versioned re-agreement (AppConstants.SmsTermsVersion)
- Dev redirect: non-production SMS routed to Twilio:DevRedirectPhone to protect real customer numbers
- Removed redundant Ready for Pickup SMS (Job Completed covers it)
- Role-based compose modal on job completion: Admin/Manager reviews and edits before send; ShopFloor auto-sends
- Send SMS button on job details for ad-hoc messages (Admin/Manager only)
- SendJobSmsAsync auto-appends STOP opt-out language if missing
- Migrations: AddSmsGating, AddCompanySmsAgreement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>